zig-dimal/src/Scalar.zig
2026-04-22 15:43:17 +02:00

643 lines
27 KiB
Zig

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;
// TODO: Be able to use comptime float and int and T for mulBy ect
// Which endup being Dimension less
/// 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: Dimensions, comptime s: Scales) 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;
/// Dimensions of this type
pub const dims: Dimensions = d;
/// Scales of this type
pub const scales = s;
/// Add two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
pub inline fn add(self: Self, rhs: anytype) Scalar(
T,
dims,
hlp.finerScales(Self, @TypeOf(rhs)),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return .{ .value = self.value + rhs.value };
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return .{ .value = lhs_val + rhs_val };
}
/// Subtract two quantities. Dimensions must match — compile error otherwise.
/// Scales are auto-resolved to the finer of the two.
pub inline fn sub(self: Self, rhs: anytype) Scalar(
T,
dims,
hlp.finerScales(Self, @TypeOf(rhs)),
) {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in sub: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return .{ .value = self.value - rhs.value };
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return .{ .value = lhs_val - rhs_val };
}
/// Multiply two quantities. Dimension exponents are summed: `L¹ * T⁻¹ → L¹T⁻¹`.
pub inline fn mulBy(self: Self, rhs: anytype) Scalar(
T,
dims.add(@TypeOf(rhs).dims),
hlp.finerScales(Self, @TypeOf(rhs)),
) {
const RhsType = @TypeOf(rhs);
const SelfNorm = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const RhsNorm = Scalar(T, RhsType.dims, hlp.finerScales(Self, @TypeOf(rhs)));
if (comptime Self == SelfNorm and RhsType == RhsNorm)
return .{ .value = self.value * rhs.value };
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs.value else rhs.to(RhsNorm).value;
return .{ .value = lhs_val * rhs_val };
}
/// Divide two quantities. Dimension exponents are subtracted: `L¹ / T¹ → L¹T⁻¹`.
/// Integer types use truncating division.
pub inline fn divBy(self: Self, rhs: anytype) Scalar(
T,
dims.sub(@TypeOf(rhs).dims),
hlp.finerScales(Self, @TypeOf(rhs)),
) {
const RhsType = @TypeOf(rhs);
const SelfNorm = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const RhsNorm = Scalar(T, RhsType.dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime Self == SelfNorm) self.value else self.to(SelfNorm).value;
const rhs_val = if (comptime RhsType == RhsNorm) rhs.value else rhs.to(RhsNorm).value;
if (comptime @typeInfo(T) == .int) {
return .{ .value = @divTrunc(lhs_val, rhs_val) };
} else {
return .{ .value = lhs_val / rhs_val };
}
}
/// 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),
s,
) {
if (comptime @typeInfo(T) == .int)
return .{ .value = std.math.powi(T, self.value, exp) catch @panic("Integer overflow in pow") }
else
return .{ .value = std.math.pow(T, self.value, @as(T, @floatFromInt(exp))) };
}
pub inline fn sqrt(self: Self) Scalar(
T,
dims.div(2),
s,
) {
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 @typeInfo(T) == .int) {
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) };
}
}
/// 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 div: DestT = comptime @intFromFloat(1.0 / ratio);
const val = @as(DestT, @intCast(self.value));
const half = comptime div / 2;
const rounded = if (val >= 0) @divTrunc(val + half, div) else @divTrunc(val - half, div);
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)) };
}
}
/// Compares two Scalar for exact equality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
pub inline fn eq(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value == rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return lhs_val == rhs_val;
}
/// Compares two quantities for inequality.
/// Dimensions must match — compile error otherwise. Scales are auto-resolved.
pub inline fn ne(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value != rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.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.
pub inline fn gt(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value > rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.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.
pub inline fn gte(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value >= rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.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.
pub inline fn lt(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value < rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.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.
pub inline fn lte(self: Self, rhs: anytype) bool {
if (comptime !dims.eql(@TypeOf(rhs).dims))
@compileError("Dimension mismatch in add: " ++ dims.str() ++ " vs " ++ @TypeOf(rhs).dims.str());
if (comptime @TypeOf(rhs) == Self)
return self.value <= rhs.value;
const TargetType = Scalar(T, dims, hlp.finerScales(Self, @TypeOf(rhs)));
const lhs_val = if (comptime @TypeOf(self) == TargetType) self.value else self.to(TargetType).value;
const rhs_val = if (comptime @TypeOf(rhs) == TargetType) rhs.value else rhs.to(TargetType).value;
return lhs_val <= rhs_val;
}
/// 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);
}
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = -3 }));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const KiloMeter_f = Scalar(f64, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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.mulBy(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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const KiloGram = Scalar(f32, Dimensions.init(.{ .M = 1 }), Scales.init(.{ .M = .k }));
const dist = KiloMeter{ .value = 2.0 };
const mass = KiloGram{ .value = 3.0 };
const prod = dist.mulBy(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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Second = Scalar(f64, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const KmSec = Scalar(i64, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const KmSec_f = Scalar(f32, Dimensions.init(.{ .L = 1, .T = 1 }), Scales.init(.{ .L = .k }));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(t).to(KmSec);
const area_time_f = d.mulBy(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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .n }));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const d = Meter{ .value = 3.0 };
const t = Second{ .value = 4.0 };
const area_time = d.mulBy(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, Dimensions.init(.{}), Scales.init(.{}));
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const d = Meter{ .value = 7 };
const scaled = d.mulBy(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, Dimensions.init(.{ .L = 2 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 2 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 100.0 };
const t1 = Second{ .value = 5.0 };
const velocity = dist.divBy(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.divBy(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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Scalar(f32, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
const dist = Meter{ .value = 120 };
const time = Second{ .value = 4 };
const vel = dist.divBy(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, Dimensions.init(.{}), Scales.init(.{}));
const KiloMetre = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const r = Dimless{ .value = 30 };
const time = KiloMetre{ .value = 4 };
const vel = r.mulBy(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, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const CentiMeter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{ .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, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .hour }));
const Minute = Scalar(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{ .T = .min }));
const Second = Scalar(i128, Dimensions.init(.{ .T = 1 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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,
Dimensions.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .T = .n }),
);
const KgMeterPerSecond = Scalar(
f32,
Dimensions.init(.{ .M = 1, .L = 1, .T = -1 }),
Scales.init(.{ .M = .k }),
);
const Meter = Scalar(f32, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
const m3 = m_float{ .value = -42.5 };
try std.testing.expectEqual(42.5, m3.abs().value);
}
test "Pow" {
const Meter = Scalar(i128, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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, Dimensions.init(.{ .L = 1 }), Scales.init(.{}));
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));
}