From a591736b19232aa5e41b7168f90fc775696bc436 Mon Sep 17 00:00:00 2001 From: adrien Date: Sun, 26 Apr 2026 00:29:00 +0200 Subject: [PATCH] Totally replaced Scalar with Quantity --- src/Quantity.zig | 353 ++++++++++++++++++-- src/Scalar.zig | 815 ----------------------------------------------- src/helper.zig | 21 +- 3 files changed, 340 insertions(+), 849 deletions(-) delete mode 100644 src/Scalar.zig diff --git a/src/Quantity.zig b/src/Quantity.zig index 9732fac..f96e872 100644 --- a/src/Quantity.zig +++ b/src/Quantity.zig @@ -46,6 +46,7 @@ pub fn Quantity( pub const Len: usize = N; pub const dims: Dimensions = Dimensions.init(d_opt); pub const scales: Scales = Scales.init(s_opt); + pub const ISQUANTITY = true; /// Scalar variant of this quantity (lane=1). Returned by dot(), product(), etc. pub const ScalarType: type = Quantity(T, 1, d_opt, s_opt); @@ -220,7 +221,7 @@ pub fn Quantity( /// Absolute value of every lane. Uses native `@abs` (SIMD for floats & ints). pub inline fn abs(self: Self) Self { - return .{ .data = @abs(self.data) }; + return .{ .data = @bitCast(@abs(self.data)) }; } /// Raise every lane to a comptime integer exponent. @@ -265,11 +266,10 @@ pub fn Quantity( const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); inline for (0..N) |i| { const v = self.data[i]; - if (v < 0) { - result[i] = 0; - continue; - } - result[i] = @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(v))))); + if (v < 0) + result[i] = 0 + else + result[i] = @as(T, @intCast(std.math.sqrt(@as(UnsignedT, @intCast(v))))); } return .{ .data = result }; } @@ -297,28 +297,32 @@ pub fn Quantity( const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims)); const DestVec = @Vector(N, DestT); - // ── Fast path: same numeric type → pure SIMD ────────────── + // ── Same numeric type path ── if (comptime T == DestT) { - if (comptime @typeInfo(T) == .float) { + if (comptime @typeInfo(T) == .float) return .{ .data = self.data * @as(DestVec, @splat(@as(T, @floatCast(ratio)))) }; - } - // Integer same-T - if (comptime ratio >= 1.0 and @round(ratio) == ratio) { - const mult: T = comptime @intFromFloat(ratio); + + // Integer logic: Branching prevents division by zero errors + if (comptime ratio >= 1.0) { + // Upscaling (e.g., km -> m, ratio = 1000) + const mult: T = comptime @intFromFloat(@round(ratio)); return .{ .data = self.data *| @as(Vec, @splat(mult)) }; + } else { + // Downscaling (e.g., m -> km, ratio = 0.001) + const div_val: T = comptime @intFromFloat(@round(1.0 / ratio)); + var result: DestVec = undefined; + const half: T = comptime @divTrunc(div_val, 2); + + inline for (0..N) |i| { + const val = self.data[i]; + // Rounding division for integers + result[i] = if (val >= 0) @divTrunc(val + half, div_val) else @divTrunc(val - half, div_val); + } + return .{ .data = result }; } - // Sub-unit ratio: rounded integer divide, element-wise. - const d: T = comptime @intFromFloat(1.0 / ratio); - var result: DestVec = undefined; - inline for (0..N) |i| { - const val = self.data[i]; - const half: T = comptime d / 2; - result[i] = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d); - } - return .{ .data = result }; } - // ── Cross-numeric-type: go through f64 element-wise ──────── + // ── Cross-numeric-type (unchanged) ── var result: DestVec = undefined; inline for (0..N) |i| { const float_val: f64 = switch (comptime @typeInfo(T)) { @@ -609,7 +613,7 @@ pub fn Quantity( }; } -fn Scalar(comptime T: type, comptime d: Dimensions.ArgOpts, comptime s: Scales.ArgOpts) type { +pub fn Scalar(comptime T: type, comptime d: Dimensions.ArgOpts, comptime s: Scales.ArgOpts) type { return Quantity(T, 1, d, s); } @@ -623,3 +627,306 @@ test "Generate quantity" { try std.testing.expectEqual(10, distance.value()); try std.testing.expectEqual(2, time.value()); } + +test "Comparisons (eq, ne, gt, gte, lt, lte)" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + + const m1000 = Meter.splat(1000); + const km1 = KiloMeter.splat(1); + const km2 = KiloMeter.splat(2); + + // Equal / Not Equal + try std.testing.expect(m1000.eq(km1)); + try std.testing.expect(km1.eq(m1000)); + try std.testing.expect(km2.ne(m1000)); + + // Greater Than / Greater Than or Equal + try std.testing.expect(km2.gt(m1000)); + try std.testing.expect(km2.gt(km1)); + try std.testing.expect(km1.gte(m1000)); + try std.testing.expect(km2.gte(m1000)); + + // Less Than / Less Than or Equal + try std.testing.expect(m1000.lt(km2)); + try std.testing.expect(km1.lt(km2)); + try std.testing.expect(km1.lte(m1000)); + try std.testing.expect(m1000.lte(km2)); +} + +test "Add" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + + const distance = Meter.splat(10); + const distance2 = Meter.splat(20); + + const added = distance.add(distance2); + try std.testing.expectEqual(30, added.value()); + try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); + + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const distance3 = KiloMeter.splat(2); + const added2 = distance.add(distance3); + try std.testing.expectEqual(2010, added2.value()); + try std.testing.expectEqual(1, @TypeOf(added2).dims.get(.L)); + + const added3 = distance3.add(distance).to(KiloMeter); + try std.testing.expectEqual(2, added3.value()); + try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L)); + + const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); + const distance4 = KiloMeter_f.splat(2); + const added4 = distance4.add(distance).to(KiloMeter_f); + try std.testing.expectApproxEqAbs(2.01, added4.value(), 0.000001); + try std.testing.expectEqual(1, @TypeOf(added4).dims.get(.L)); +} + +test "Sub" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); + + const a = Meter.splat(500); + const b = Meter.splat(200); + + const diff = a.sub(b); + try std.testing.expectEqual(300, diff.value()); + const diff2 = b.sub(a); + try std.testing.expectEqual(-300, diff2.value()); + + const km_f = KiloMeter_f.splat(2.5); + const m_f = Meter.splat(500); + const diff3 = km_f.sub(m_f); + try std.testing.expectApproxEqAbs(2000, diff3.value(), 1e-4); +} + +test "MulBy" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t); + try std.testing.expectEqual(12, area_time.value()); + try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); + + const d2 = Meter.splat(5.0); + const area = d.mul(d2); + try std.testing.expectEqual(15, area.value()); + try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); +} + +test "MulBy with scale" { + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k }); + + const dist = KiloMeter.splat(2.0); + const mass = KiloGram.splat(3.0); + const prod = dist.mul(mass); + try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L)); + try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M)); +} + +test "MulBy with type change" { + const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const Second = Scalar(f64, .{ .T = 1 }, .{}); + const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }); + const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k }); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t).to(KmSec); + const area_time_f = d.mul(t).to(KmSec_f); + try std.testing.expectEqual(12, area_time.value()); + try std.testing.expectApproxEqAbs(12.0, area_time_f.value(), 0.0001); +} + +test "MulBy small" { + const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n }); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const d = Meter.splat(3.0); + const t = Second.splat(4.0); + + const area_time = d.mul(t); + try std.testing.expectEqual(12, area_time.value()); +} + +test "MulBy dimensionless" { + const DimLess = Scalar(i128, .{}, .{}); + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + + const d = Meter.splat(7); + const scaled = d.mul(DimLess.splat(3)); + try std.testing.expectEqual(21, scaled.value()); +} + +test "Sqrt" { + const MeterSquare = Scalar(i128, .{ .L = 2 }, .{}); + + var d = MeterSquare.splat(9); + var scaled = d.sqrt(); + try std.testing.expectEqual(3, scaled.value()); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); + + d = MeterSquare.splat(-5); + scaled = d.sqrt(); + try std.testing.expectEqual(0, scaled.value()); + + const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{}); + const d2 = MeterSquare_f.splat(20); + const scaled2 = d2.sqrt(); + try std.testing.expectApproxEqAbs(4.472135955, scaled2.value(), 1e-4); +} + +test "Chained: velocity and acceleration" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const dist = Meter.splat(100.0); + const t1 = Second.splat(5.0); + const velocity = dist.div(t1); + try std.testing.expectEqual(20, velocity.value()); + + const t2 = Second.splat(4.0); + const accel = velocity.div(t2); + try std.testing.expectEqual(5, accel.value()); +} + +test "DivBy integer exact" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const Second = Scalar(f32, .{ .T = 1 }, .{}); + + const dist = Meter.splat(120); + const time = Second.splat(4); + const vel = dist.div(time); + + try std.testing.expectEqual(30, vel.value()); +} + +test "Finer scales skip dim 0" { + const Dimless = Scalar(i128, .{}, .{}); + const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + + const r = Dimless.splat(30); + const time = KiloMetre.splat(4); + const vel = r.mul(time); + + try std.testing.expectEqual(120, vel.value()); + try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L)); +} + +test "Conversion chain: km -> m -> cm" { + const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c }); + + const km = KiloMeter.splat(15); + const m = km.to(Meter); + const cm = m.to(CentiMeter); + + try std.testing.expectEqual(15_000, m.value()); + try std.testing.expectEqual(1_500_000, cm.value()); +} + +test "Conversion: hours -> minutes -> seconds" { + const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour }); + const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min }); + const Second = Scalar(i128, .{ .T = 1 }, .{}); + + const h = Hour.splat(1.0); + const min = h.to(Minute); + const sec = min.to(Second); + + try std.testing.expectEqual(60, min.value()); + try std.testing.expectEqual(3600, sec.value()); +} + +test "Format Scalar" { + const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + + const m = Meter.splat(1.23456); + const accel = MeterPerSecondSq.splat(9.81); + + var buf: [64]u8 = undefined; + var res = try std.fmt.bufPrint(&buf, "{d:.2}", .{m}); + try std.testing.expectEqualStrings("1.23m", res); + + res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); + try std.testing.expectEqualStrings("9.81m.ns⁻²", res); +} + +test "Abs" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const m1 = Meter.splat(-50); + const m2 = m1.abs(); + + try std.testing.expectEqual(50, m2.value()); + + const m_float = Scalar(f32, .{ .L = 1 }, .{}); + const m3 = m_float.splat(-42.5); + try std.testing.expectEqual(42.5, m3.abs().value()); +} + +test "Pow" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const d = Meter.splat(4); + + const area = d.pow(2); + try std.testing.expectEqual(16, area.value()); + + const volume = d.pow(3); + try std.testing.expectEqual(64, volume.value()); +} + +test "mul comptime_int" { + const Meter = Scalar(i128, .{ .L = 1 }, .{}); + const d = Meter.splat(7); + + const scaled = d.mul(3); + try std.testing.expectEqual(21, scaled.value()); +} + +test "add/sub bare number on dimensionless scalar" { + const DimLess = Scalar(i128, .{}, .{}); + const a = DimLess.splat(10); + + const b = a.add(5); + try std.testing.expectEqual(15, b.value()); + + const c = a.sub(3); + try std.testing.expectEqual(7, c.value()); +} + +test "Imperial length scales" { + const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft }); + const Meter = Scalar(f64, .{ .L = 1 }, .{}); + const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch }); + + const one_ft = Foot.splat(1.0); + try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value(), 1e-9); + + const twelve_in = Inch.splat(12.0); + try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value(), 1e-9); +} + +test "Imperial mass scales" { + const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb }); + const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz }); + + const two_lb = Pound.splat(2.0); + const eight_oz = Ounce.splat(8.0); + const total = two_lb.add(eight_oz).to(Pound); + try std.testing.expectApproxEqAbs(2.5, total.value(), 1e-6); +} + +test "comparisons with comptime_int on dimensionless scalar" { + const DimLess = Scalar(i128, .{}, .{}); + const x = DimLess.splat(42); + + try std.testing.expect(x.eq(42)); + try std.testing.expect(x.gt(10)); +} diff --git a/src/Scalar.zig b/src/Scalar.zig deleted file mode 100644 index 34877cb..0000000 --- a/src/Scalar.zig +++ /dev/null @@ -1,815 +0,0 @@ -const std = @import("std"); -const hlp = @import("helper.zig"); - -const Vector = @import("Vector.zig").Vector; -const Scales = @import("Scales.zig"); -const UnitScale = Scales.UnitScale; -const Dimensions = @import("Dimensions.zig"); -const Dimension = Dimensions.Dimension; - -/// A dimensioned scalar value. `T` is the numeric type, `d` the dimension exponents, `s` the SI scales. -/// All dimension and unit tracking is resolved at comptime — zero runtime overhead. -pub fn Scalar(comptime T: type, comptime d_opt: Dimensions.ArgOpts, comptime s_opt: Scales.ArgOpts) type { - @setEvalBranchQuota(10_000_000); - return struct { - value: T, - - const Self = @This(); - - /// Type of Vector(3, Self) - pub const Vec3: type = Vector(3, Self); - - /// Type of underline value, mostly use for Vector - pub const ValueType: type = T; - pub const dims: Dimensions = Dimensions.init(d_opt); - pub const scales = Scales.init(s_opt); - - // --------------------------------------------------------------- - // Internal: resolved-rhs shorthands - // --------------------------------------------------------------- - - /// Scalar type that `rhs` normalises to (bare numbers → dimensionless). - inline fn RhsT(comptime Rhs: type) type { - return hlp.rhsScalarType(T, Rhs); - } - - /// Normalise `rhs` (bare number or Scalar) into a proper Scalar value. - inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { - return hlp.toRhsScalar(T, r); - } - - // --------------------------------------------------------------- - // Arithmetic - // --------------------------------------------------------------- - - /// Add two quantities. Dimensions must match — compile error otherwise. - /// Scales are auto-resolved to the finer of the two. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float` - /// (bare numbers are treated as dimensionless). - pub inline fn add(self: Self, r: anytype) Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return .{ .value = self.value + rhs_s.value }; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val +| rhs_val else lhs_val + rhs_val }; - } - - /// Subtract two quantities. Dimensions must match — compile error otherwise. - /// Scales are auto-resolved to the finer of the two. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn sub(self: Self, r: anytype) Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return .{ .value = self.value - rhs_s.value }; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val -| rhs_val else lhs_val - rhs_val }; - } - - /// Multiply two quantities. Dimension exponents are summed: `L¹ * T⁻¹ → L¹T⁻¹`. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float` - /// (bare numbers are treated as dimensionless — dimensions pass through unchanged). - pub inline fn mul(self: Self, r: anytype) Scalar( - T, - dims.add(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - if (comptime Self == SelfNorm and RhsType == RhsNorm) - return .{ .value = self.value * rhs_s.value }; - - const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value; - const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value; - return .{ .value = if (comptime hlp.isInt(T)) lhs_val *| rhs_val else lhs_val * rhs_val }; - } - - /// Divide two quantities. Dimension exponents are subtracted: `L¹ / T¹ → L¹T⁻¹`. - /// Integer types use truncating division. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn div(self: Self, r: anytype) Scalar( - T, - dims.sub(RhsT(@TypeOf(r)).dims).argsOpt(), - hlp.finerScales(Self, RhsT(@TypeOf(r))).argsOpt(), - ) { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - const SelfNorm = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Scalar(T, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value; - const rhs_val = if (comptime RhsType == RhsNorm) rhs_s.value else rhs_s.to(RhsNorm).value; - if (comptime hlp.isInt(T)) { - return .{ .value = @divTrunc(lhs_val, rhs_val) }; - } else { - return .{ .value = lhs_val / rhs_val }; - } - } - - // --------------------------------------------------------------- - // Unary - // --------------------------------------------------------------- - - /// Returns the absolute value of the quantity. - /// Dimensions and scales remain entirely unchanged. - pub inline fn abs(self: Self) Self { - if (comptime @typeInfo(T) == .int) - return .{ .value = @intCast(@abs(self.value)) } - else - return .{ .value = @abs(self.value) }; - } - - /// Raises the quantity to a compile-time integer exponent. - /// Dimension exponents are multiplied by the exponent: `(L²)³ → L⁶`. - pub inline fn pow(self: Self, comptime exp: comptime_int) Scalar( - T, - dims.scale(exp).argsOpt(), - scales.argsOpt(), - ) { - if (comptime hlp.isInt(T)) - return .{ .value = std.math.powi(T, self.value, exp) catch std.math.maxInt(T) } - else - return .{ .value = std.math.pow(T, self.value, @as(T, @floatFromInt(exp))) }; - } - - pub inline fn sqrt(self: Self) Scalar( - T, - dims.div(2).argsOpt(), - scales.argsOpt(), - ) { - if (comptime !dims.isSquare()) // Check if all exponents are divisible by 2 - @compileError("Cannot take sqrt of " ++ dims.str() ++ ": exponents must be even."); - if (self.value < 0) return .{ .value = 0 }; - - if (comptime hlp.isInt(T)) { - const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); - const u_len_sq = @as(UnsignedT, @intCast(self.value)); - return .{ .value = @as(T, @intCast(std.math.sqrt(u_len_sq))) }; - } else { - return .{ .value = @sqrt(self.value) }; - } - } - - // --------------------------------------------------------------- - // Conversion - // --------------------------------------------------------------- - - /// Convert to a compatible unit type. The scale ratio is computed at comptime. - /// Compile error if dimensions don't match. - pub inline fn to(self: Self, comptime Dest: type) Dest { - if (comptime !dims.eql(Dest.dims)) - @compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str()); - if (comptime @TypeOf(self) == Dest) - return self; - - const DestT = Dest.ValueType; - const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims)); - - // Fast-path: Native pure-integer exact conversions - if (comptime @typeInfo(T) == .int and @typeInfo(DestT) == .int) { - if (comptime ratio >= 1.0 and @round(ratio) == ratio) { - const mult: DestT = comptime @intFromFloat(ratio); - return .{ .value = @as(DestT, @intCast(self.value)) * mult }; - } else if (comptime ratio < 1.0 and @round(1.0 / ratio) == 1.0 / ratio) { - const d: DestT = comptime @intFromFloat(1.0 / ratio); - const val = @as(DestT, @intCast(self.value)); - const half = comptime d / 2; - const rounded = if (val >= 0) @divTrunc(val + half, d) else @divTrunc(val - half, d); - return .{ .value = rounded }; - } - } - - // Fallback preserving native Float types (e.g., f128 shouldn't downcast to f64) - if (comptime @typeInfo(DestT) == .float) { - const val_f = switch (@typeInfo(T)) { - inline .int => @as(DestT, @floatFromInt(self.value)), - inline .float => @as(DestT, @floatCast(self.value)), - else => unreachable, - }; - return .{ .value = val_f * @as(DestT, @floatCast(ratio)) }; - } else { - const val_f = switch (@typeInfo(T)) { - inline .int => @as(f64, @floatFromInt(self.value)), - inline .float => @as(f64, @floatCast(self.value)), - else => unreachable, - }; - return .{ .value = @intFromFloat(@round(val_f * ratio)) }; - } - } - - // --------------------------------------------------------------- - // Comparisons - // --------------------------------------------------------------- - - /// Compares two Scalar for exact equality. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn eq(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value == rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val == rhs_val; - } - - /// Compares two quantities for inequality. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn ne(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in ne: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value != rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val != rhs_val; - } - - /// Returns true if this quantity is strictly greater than the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn gt(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in gt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value > rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val > rhs_val; - } - - /// Returns true if this quantity is greater than or equal to the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn gte(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in gte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value >= rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val >= rhs_val; - } - - /// Returns true if this quantity is strictly less than the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn lt(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in lt: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value < rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val < rhs_val; - } - - /// Returns true if this quantity is less than or equal to the right-hand side. - /// Dimensions must match — compile error otherwise. Scales are auto-resolved. - /// `rhs` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. - pub inline fn lte(self: Self, r: anytype) bool { - const rhs_s = rhs(r); - const RhsType = @TypeOf(rhs_s); - if (comptime !dims.eql(RhsType.dims)) - @compileError("Dimension mismatch in lte: " ++ dims.str() ++ " vs " ++ RhsType.dims.str()); - if (comptime RhsType == Self) - return self.value <= rhs_s.value; - - const TargetType = Scalar(T, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const lhs_val = if (comptime Self == TargetType) self.value else self.to(TargetType).value; - const rhs_val = if (comptime RhsType == TargetType) rhs_s.value else rhs_s.to(TargetType).value; - return lhs_val <= rhs_val; - } - - // --------------------------------------------------------------- - // Vector helpers - // --------------------------------------------------------------- - - /// Return a `Vector(len, Self)` type. - pub fn Vec(_: Self, comptime len: comptime_int) type { - return Vector(len, Self); - } - - /// Return a `Vector(len, Self)` with all components set to this value. - pub fn vec(self: Self, comptime len: comptime_int) Vector(len, Self) { - return Vector(len, Self).initDefault(self.value); - } - - /// Shorthand for `Vec(3)` — wrap this value into a 3-component vector. - pub fn vec3(self: Self) Vec3 { - return Vec3.initDefault(self.value); - } - - // --------------------------------------------------------------- - // Formatting - // --------------------------------------------------------------- - - pub fn formatNumber( - self: Self, - writer: *std.Io.Writer, - options: std.fmt.Number, - ) !void { - switch (@typeInfo(T)) { - .float, .comptime_float => try writer.printFloat(self.value, options), - .int, .comptime_int => try writer.printInt(self.value, 10, .lower, .{ - .width = options.width, - .alignment = options.alignment, - .fill = options.fill, - .precision = options.precision, - }), - else => unreachable, - } - var first = true; - inline for (std.enums.values(Dimension)) |bu| { - const v = dims.get(bu); - if (comptime v == 0) continue; - if (!first) - try writer.writeAll("."); - - first = false; - - const uscale = scales.get(bu); - if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year)) - try writer.print("{s}", .{uscale.str()}) - else - try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); - - if (v != 1) - try hlp.printSuperscript(writer, v); - } - } - }; -} - -test "Generate quantity" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = @enumFromInt(-3) }); - const Second = Scalar(f32, .{ .T = 1 }, .{ .T = .n }); - - const distance = Meter{ .value = 10 }; - const time = Second{ .value = 2 }; - - try std.testing.expectEqual(10, distance.value); - try std.testing.expectEqual(2, time.value); -} - -test "Comparisons (eq, ne, gt, gte, lt, lte)" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - - const m1000 = Meter{ .value = 1000 }; - const km1 = KiloMeter{ .value = 1 }; - const km2 = KiloMeter{ .value = 2 }; - - // Equal / Not Equal - try std.testing.expect(m1000.eq(km1)); - try std.testing.expect(km1.eq(m1000)); - try std.testing.expect(km2.ne(m1000)); - - // Greater Than / Greater Than or Equal - try std.testing.expect(km2.gt(m1000)); - try std.testing.expect(km2.gt(km1)); - try std.testing.expect(km1.gte(m1000)); - try std.testing.expect(km2.gte(m1000)); - - // Less Than / Less Than or Equal - try std.testing.expect(m1000.lt(km2)); - try std.testing.expect(km1.lt(km2)); - try std.testing.expect(km1.lte(m1000)); - try std.testing.expect(m1000.lte(km2)); -} - -test "Add" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const distance = Meter{ .value = 10 }; - const distance2 = Meter{ .value = 20 }; - - const added = distance.add(distance2); - try std.testing.expectEqual(30, added.value); - try std.testing.expectEqual(1, @TypeOf(added).dims.get(.L)); - - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const distance3 = KiloMeter{ .value = 2 }; - const added2 = distance.add(distance3); - try std.testing.expectEqual(2010, added2.value); - try std.testing.expectEqual(1, @TypeOf(added2).dims.get(.L)); - - const added3 = distance3.add(distance).to(KiloMeter); - try std.testing.expectEqual(2, added3.value); - try std.testing.expectEqual(1, @TypeOf(added3).dims.get(.L)); - - const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - const distance4 = KiloMeter_f{ .value = 2 }; - const added4 = distance4.add(distance).to(KiloMeter_f); - try std.testing.expectApproxEqAbs(2.01, added4.value, 0.000001); - try std.testing.expectEqual(1, @TypeOf(added4).dims.get(.L)); -} - -test "Sub" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const KiloMeter_f = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - - const a = Meter{ .value = 500 }; - const b = Meter{ .value = 200 }; - const diff = a.sub(b); - try std.testing.expectEqual(300, diff.value); - const diff2 = b.sub(a); - try std.testing.expectEqual(-300, diff2.value); - - const km_f = KiloMeter_f{ .value = 2.5 }; - const m_f = Meter{ .value = 500 }; - const diff3 = km_f.sub(m_f); - try std.testing.expectApproxEqAbs(2000, diff3.value, 1e-4); -} - -test "MulBy" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); - - const d2 = Meter{ .value = 5.0 }; - const area = d.mul(d2); - try std.testing.expectEqual(15, area.value); - try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(area).dims.get(.T)); -} - -test "MulBy with scale" { - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - const KiloGram = Scalar(f32, .{ .M = 1 }, .{ .M = .k }); - - const dist = KiloMeter{ .value = 2.0 }; - const mass = KiloGram{ .value = 3.0 }; - const prod = dist.mul(mass); - try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(prod).dims.get(.M)); -} - -test "MulBy with type change" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const Second = Scalar(f64, .{ .T = 1 }, .{}); - const KmSec = Scalar(i64, .{ .L = 1, .T = 1 }, .{ .L = .k }); - const KmSec_f = Scalar(f32, .{ .L = 1, .T = 1 }, .{ .L = .k }); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t).to(KmSec); - const area_time_f = d.mul(t).to(KmSec_f); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectApproxEqAbs(12, area_time_f.value, 0.0001); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); -} - -test "MulBy small" { - const Meter = Scalar(i128, .{ .L = 1 }, .{ .L = .n }); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const d = Meter{ .value = 3.0 }; - const t = Second{ .value = 4.0 }; - - const area_time = d.mul(t); - try std.testing.expectEqual(12, area_time.value); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.L)); - try std.testing.expectEqual(1, @TypeOf(area_time).dims.get(.T)); -} - -test "MulBy dimensionless" { - const DimLess = Scalar(i128, .{}, .{}); - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const d = Meter{ .value = 7 }; - const scaled = d.mul(DimLess{ .value = 3 }); - try std.testing.expectEqual(21, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "Sqrt" { - const MeterSquare = Scalar(i128, .{ .L = 2 }, .{}); - - var d = MeterSquare{ .value = 9 }; - var scaled = d.sqrt(); - try std.testing.expectEqual(3, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - - d = MeterSquare{ .value = -5 }; - scaled = d.sqrt(); - try std.testing.expectEqual(0, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - - const MeterSquare_f = Scalar(f64, .{ .L = 2 }, .{}); - const d2 = MeterSquare_f{ .value = 20 }; - const scaled2 = d2.sqrt(); - try std.testing.expectApproxEqAbs(4.472135955, scaled2.value, 1e-4); - try std.testing.expectEqual(1, @TypeOf(scaled2).dims.get(.L)); -} - -test "Chained: velocity and acceleration" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const dist = Meter{ .value = 100.0 }; - const t1 = Second{ .value = 5.0 }; - const velocity = dist.div(t1); - try std.testing.expectEqual(20, velocity.value); - try std.testing.expectEqual(1, @TypeOf(velocity).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(velocity).dims.get(.T)); - - const t2 = Second{ .value = 4.0 }; - const accel = velocity.div(t2); - try std.testing.expectEqual(5, accel.value); - try std.testing.expectEqual(1, @TypeOf(accel).dims.get(.L)); - try std.testing.expectEqual(-2, @TypeOf(accel).dims.get(.T)); -} - -test "DivBy integer exact" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const Second = Scalar(f32, .{ .T = 1 }, .{}); - - const dist = Meter{ .value = 120 }; - const time = Second{ .value = 4 }; - const vel = dist.div(time); - - try std.testing.expectEqual(30, vel.value); - try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); -} - -test "Finer scales skip dim 0" { - const Dimless = Scalar(i128, .{}, .{}); - const KiloMetre = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - - const r = Dimless{ .value = 30 }; - const time = KiloMetre{ .value = 4 }; - const vel = r.mul(time); - - try std.testing.expectEqual(120, vel.value); - try std.testing.expectEqual(Scales.UnitScale.k, @TypeOf(vel).scales.get(.L)); -} - -test "Conversion chain: km -> m -> cm" { - const KiloMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .k }); - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const CentiMeter = Scalar(i128, .{ .L = 1 }, .{ .L = .c }); - - const km = KiloMeter{ .value = 15 }; - const m = km.to(Meter); - const cm = m.to(CentiMeter); - - try std.testing.expectEqual(15_000, m.value); - try std.testing.expectEqual(1_500_000, cm.value); -} - -test "Conversion: hours -> minutes -> seconds" { - const Hour = Scalar(i128, .{ .T = 1 }, .{ .T = .hour }); - const Minute = Scalar(i128, .{ .T = 1 }, .{ .T = .min }); - const Second = Scalar(i128, .{ .T = 1 }, .{}); - - const h = Hour{ .value = 1.0 }; - const min = h.to(Minute); - const sec = min.to(Second); - - try std.testing.expectEqual(60, min.value); - try std.testing.expectEqual(3600, sec.value); -} - -test "Negative values" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - - const a = Meter{ .value = 5 }; - const b = Meter{ .value = 20 }; - const diff = a.sub(b); - try std.testing.expectEqual(-15, diff.value); -} - -test "Format Scalar" { - const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); - const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - - const m = Meter{ .value = 1.23456 }; - const accel = MeterPerSecondSq{ .value = 9.81 }; - const momentum = KgMeterPerSecond{ .value = 42.0 }; - - var buf: [64]u8 = undefined; - var res = try std.fmt.bufPrint(&buf, "{d:.2}", .{m}); - try std.testing.expectEqualStrings("1.23m", res); - - res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); - try std.testing.expectEqualStrings("9.81m.ns⁻²", res); - - res = try std.fmt.bufPrint(&buf, "{d}", .{momentum}); - try std.testing.expectEqualStrings("42m.kg.s⁻¹", res); - - res = try std.fmt.bufPrint(&buf, "{d:_>10.1}", .{m}); - try std.testing.expectEqualStrings("_______1.2m", res); -} - -test "Abs" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const m1 = Meter{ .value = -50 }; - const m2 = m1.abs(); - - try std.testing.expectEqual(50, m2.value); - try std.testing.expectEqual(1, @TypeOf(m2).dims.get(.L)); - - const m_float = Scalar(f32, .{ .L = 1 }, .{}); - const m3 = m_float{ .value = -42.5 }; - try std.testing.expectEqual(42.5, m3.abs().value); -} - -test "Pow" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 4 }; - - const area = d.pow(2); - try std.testing.expectEqual(16, area.value); - try std.testing.expectEqual(2, @TypeOf(area).dims.get(.L)); - - const volume = d.pow(3); - try std.testing.expectEqual(64, volume.value); - try std.testing.expectEqual(3, @TypeOf(volume).dims.get(.L)); - - // Float test - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const d_f = MeterF{ .value = 2.0 }; - const area_f = d_f.pow(3); - try std.testing.expectEqual(8.0, area_f.value); - try std.testing.expectEqual(3, @TypeOf(area_f).dims.get(.L)); -} - -test "mul comptime_int" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 7 }; - - const scaled = d.mul(3); // comptime_int → dimensionless - try std.testing.expectEqual(21, scaled.value); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T)); -} - -test "mul comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 4.0 }; - - const scaled = d.mul(2.5); // comptime_float → dimensionless - try std.testing.expectApproxEqAbs(10.0, scaled.value, 1e-9); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "mul T (value type)" { - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 6.0 }; - const factor: f32 = 0.5; - - const scaled = d.mul(factor); // bare f32 → dimensionless - try std.testing.expectApproxEqAbs(3.0, scaled.value, 1e-6); - try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); -} - -test "div comptime_int" { - const Meter = Scalar(i128, .{ .L = 1 }, .{}); - const d = Meter{ .value = 100 }; - - const half = d.div(4); // comptime_int → dimensionless divisor - try std.testing.expectEqual(25, half.value); - try std.testing.expectEqual(1, @TypeOf(half).dims.get(.L)); -} - -test "div comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const d = MeterF{ .value = 9.0 }; - - const r = d.div(3.0); - try std.testing.expectApproxEqAbs(3.0, r.value, 1e-9); - try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L)); -} - -test "add/sub bare number on dimensionless scalar" { - // Bare numbers are dimensionless, so add/sub only works when Self is also dimensionless. - const DimLess = Scalar(i128, .{}, .{}); - const a = DimLess{ .value = 10 }; - - const b = a.add(5); // comptime_int, both dimensionless → ok - try std.testing.expectEqual(15, b.value); - - const c = a.sub(3); - try std.testing.expectEqual(7, c.value); -} - -test "Imperial length scales" { - const Foot = Scalar(f64, .{ .L = 1 }, .{ .L = .ft }); - const Meter = Scalar(f64, .{ .L = 1 }, .{}); - const Inch = Scalar(f64, .{ .L = 1 }, .{ .L = .inch }); - const CentiMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .c }); - const Mile = Scalar(f64, .{ .L = 1 }, .{ .L = .mi }); - const KiloMeter = Scalar(f64, .{ .L = 1 }, .{ .L = .k }); - const Yard = Scalar(f64, .{ .L = 1 }, .{ .L = .yd }); - - // 1 ft → 0.3048 m - const one_ft = Foot{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(0.3048, one_ft.to(Meter).value, 1e-9); - - // 12 in → 1 ft - const twelve_in = Inch{ .value = 12.0 }; - try std.testing.expectApproxEqAbs(1.0, twelve_in.to(Foot).value, 1e-9); - - // 1 in → 2.54 cm - const one_in = Inch{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(2.54, one_in.to(CentiMeter).value, 1e-9); - - // 1 mi → 1.609344 km - const one_mi = Mile{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(1.609344, one_mi.to(KiloMeter).value, 1e-9); - - // 3 ft → 1 yd - const three_ft = Foot{ .value = 3.0 }; - try std.testing.expectApproxEqAbs(1.0, three_ft.to(Yard).value, 1e-9); -} - -test "Imperial mass scales" { - const Pound = Scalar(f64, .{ .M = 1 }, .{ .M = .lb }); - const KiloGram = Scalar(f64, .{ .M = 1 }, .{ .M = .k }); - const Ounce = Scalar(f64, .{ .M = 1 }, .{ .M = .oz }); - const Stone = Scalar(f64, .{ .M = 1 }, .{ .M = .st }); - - // 1 lb → ~0.453592 kg - const one_lb = Pound{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(0.45359237, one_lb.to(KiloGram).value, 1e-6); - - // 16 oz → 1 lb - const sixteen_oz = Ounce{ .value = 16.0 }; - try std.testing.expectApproxEqAbs(1.0, sixteen_oz.to(Pound).value, 1e-6); - - // 1 stone → 14 lb - const one_st = Stone{ .value = 1.0 }; - try std.testing.expectApproxEqAbs(14.0, one_st.to(Pound).value, 1e-4); - - // 2 lb + 8 oz → 2.5 lb - const two_lb = Pound{ .value = 2.0 }; - const eight_oz = Ounce{ .value = 8.0 }; - const total = two_lb.add(eight_oz).to(Pound); - try std.testing.expectApproxEqAbs(2.5, total.value, 1e-6); -} - -test "comparisons with comptime_int on dimensionless scalar" { - const DimLess = Scalar(i128, .{}, .{}); - const x = DimLess{ .value = 42 }; - - try std.testing.expect(x.eq(42)); - try std.testing.expect(x.ne(0)); - try std.testing.expect(x.gt(10)); - try std.testing.expect(x.gte(42)); - try std.testing.expect(x.lt(100)); - try std.testing.expect(x.lte(42)); -} diff --git a/src/helper.zig b/src/helper.zig index 30c89fc..fb104e1 100644 --- a/src/helper.zig +++ b/src/helper.zig @@ -62,14 +62,13 @@ pub fn finerScales(comptime T1: type, comptime T2: type) Scales { // RHS normalisation helpers // --------------------------------------------------------------------------- -const Scalar = @import("Scalar.zig").Scalar; +const Quantity = @import("Quantity.zig").Quantity; /// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`). pub fn isScalarType(comptime T: type) bool { return @typeInfo(T) == .@"struct" and - @hasDecl(T, "dims") and - @hasDecl(T, "scales") and - @hasField(T, "value"); + @hasDecl(T, "ISQUANTITY") and + @field(T, "ISQUANTITY"); } /// Resolve the Scalar type that `rhs` will be treated as. @@ -80,19 +79,19 @@ pub fn isScalarType(comptime T: type) bool { /// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})` /// /// Everything else is a compile error, including other int/float types. -pub fn rhsScalarType(comptime BaseT: type, comptime RhsT: type) type { +pub fn rhsQuantityType(comptime ValueType: type, N: usize, comptime RhsT: type) type { if (comptime isScalarType(RhsT)) return RhsT; - if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT) - return Scalar(BaseT, .{}, .{}); + if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == ValueType) + return Quantity(ValueType, N, .{}, .{}); @compileError( - "rhs must be a Scalar, " ++ @typeName(BaseT) ++ + "rhs must be a Scalar, " ++ @typeName(ValueType) ++ ", comptime_int, or comptime_float; got " ++ @typeName(RhsT), ); } /// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`). -pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) { +pub inline fn toRhsQuantity(comptime BaseT: type, N: usize, rhs: anytype) rhsQuantityType(BaseT, N, @TypeOf(rhs)) { if (comptime isScalarType(@TypeOf(rhs))) return rhs; - const DimLess = Scalar(BaseT, .{}, .{}); - return DimLess{ .value = @as(BaseT, rhs) }; + const DimLess = Quantity(BaseT, N, .{}, .{}); + return DimLess{ .data = @splat(@as(BaseT, rhs)) }; }