6.3 KiB
zig_units
zig_units lets you attach physical units to numeric values so that dimension mismatches (like adding distance to time) become compile errors rather than silent bugs.
At runtime, a Quantity is just its underlying numeric value — zero memory overhead.
const velocity = distance.divBy(time); // Result type: L¹T⁻¹ ✓
const error = mass.add(velocity); // COMPILE ERROR: M¹ != L¹T⁻¹
Requirements: Zig 0.16.0
Installation
1. Add as a Zig dependency
zig fetch --save https://github.com/YOUR_USERNAME/zig_units/archive/refs/heads/main.tar.gz
2. Configure build.zig
const zig_units = b.dependency("zig_units", .{
.target = target,
.optimize = optimize,
});
// Add to your module or executable
exe.root_module.addImport("units", zig_units.module("zig_units"));
Quick Start: Using Predefined Quantities
units.Base provides a clean way to instantiate common physical types without manually defining dimensions.
const std = @import("std");
const units = @import("units");
pub fn main() !void {
// Instantiate types for f32 backing
const Meter = units.Base.Meter.Of(f32);
const Second = units.Base.Second.Of(f32);
const dist = Meter{ .value = 10.0 };
const time = Second{ .value = 2.0 };
// Arithmetic is type-safe and creates the correct resulting dimension
const vel = dist.divBy(time); // Type is Velocity (L/T)
std.debug.print("Speed: {f}\n", .{vel}); // Output: 5m.s⁻¹
}
Defining Custom Quantities
You aren't limited to the built-in library. You can define any physical quantity by specifying its Dimensions (powers of base units) and its Scale (SI prefixes).
1. Create a custom dimension
Dimensions are defined by 7 base SI units: L (Length), M (Mass), T (Time), I (Current), Tp (Temp), N (Substance), J (Intensity).
const Dims = units.Dimensions;
const Scales = units.Scales;
// Frequency is T⁻¹
const FreqDims = Dims.init(.{ .T = -1 });
// Force is M¹ L¹ T⁻²
const ForceDims = Dims.init(.{ .M = 1, .L = 1, .T = -2 });
2. Create a custom Type
Combine a numeric type, the dimensions, and a scale.
const Hertz = units.Quantity(f32, FreqDims, Scales.init(.{}));
// A specialized scale: Millimeters per Second Squared
const MmPerSecSq = units.Quantity(f32,
Dims.init(.{ .L = 1, .T = -2 }),
Scales.init(.{ .L = .m }) // .m = milli
);
Unit Conversions
The library handles SI prefixes (k, m, u, n, etc.) and time aliases (.min, .hour) automatically.
When performing arithmetic between different scales, the finer (smaller) scale wins to preserve precision.
const KM = units.Base.Meter.Scaled(f32, Scales.init(.{ .L = .k })); // Kilometers
const M = units.Base.Meter.Of(f32); // Meters
const d1 = KM{ .value = 1.2 }; // 1.2 km
const d2 = M{ .value = 300.0 }; // 300 m
const total = d1.add(d2); // Result is 1500.0 (Meters)
const final = total.to(KM); // Explicitly convert back to KM -> 1.5
Physical Vectors (Vec3)
Physical quantities often come in 3D vectors (Position, Velocity, Force). Every Quantity type has a .Vec3 alias built-in.
const Vec3M = units.Base.Meter.Of(f32).Vec3;
const gravity = Vec3M{ .data = .{ 0, -9.81, 0 } };
const pos = Vec3M.initDefault(0); // [0, 0, 0]
// Vectors support standard operations
const length = gravity.length(); // Returns f32: 9.81
const double = gravity.scale(2.0);
You can also create a Vector of any length. Vec3 found in a Quantity is just a convenience.
const M = units.Base.Meter.Of(f32);
const Vec10M = units.QuantityVec(10, Meter);
const gravity = Vec10M.initDefault(1);
const length = gravity.length(); // Returns f32: 1.0
High Precision & Integer Backing
While most libraries default to f32 or f64, zig_units is mainly designed to support large-bit integers (i128, i256).
This is critical for applications like space simulations, where floating-point numbers suffer from "jitter" or "flickering"
once you travel far from the origin. By using an i128 with a millimeter scale, you can represent the diameter
of the observable universe with millimeter precision—something impossible with f64.
Avoiding Floating-Point Jitter
// Millimeter precision using 128-bit integers
const MM = units.Base.Meter.Scaled(i128, units.Scales.init(.{ .L = .m }));
const KM = units.Base.Meter.Scaled(i128, units.Scales.init(.{ .L = .k }));
const solar_system_dist = KM{ .value = 150_000_000 }; // 150 million km
const ship_nudge = MM{ .value = 5 }; // 5 mm
// The library performs exact integer math for conversions.
// Resulting type is MM (the finer scale), maintaining perfect precision.
const new_pos = solar_system_dist.add(ship_nudge);
Integer-Specific Features
- Exact Conversions: When converting between integer scales (e.g.,
kmtom), the library uses fast-path native multiplication/division. - Round-to-Nearest: When downscaling integers (e.g., converting
1400mmtom), the library uses native round-to-nearest logic (val + half / div) to minimize truncation errors. - Safe Vector Lengths:
QuantityVec.length()includes a custom integer square root implementation, allowing you to calculate distances between coordinates without ever casting to a float. - Zero Drift: Unlike floats, repeated additions and subtractions of integers never accumulate "epsilon" drift, ensuring your simulation remains deterministic.
SI Scales Reference
| Prefix | Enum | Factor |
|---|---|---|
| Kilo | .k |
10³ |
| Mega | .M |
10⁶ |
| Giga | .G |
10⁹ |
| Milli | .m |
10⁻³ |
| Micro | .u |
10⁻⁶ |
| Minute | .min |
60 |
| Hour | .hour |
3,600 |
API Summary
Quantity(T, dims, scales)
.add(rhs)/.sub(rhs): Automatic scaling, requires same dimensions..mulBy(rhs)/.divBy(rhs): Composes dimensions (e.g.,L \times L = L^2)..scale(scalar): Multiply by a raw number (preserves dimensions)..to(OtherType): Safely convert between scales of the same dimension..vec3(): Create a 3D vector from a scalar.
Dimensions
L: Length (m)M: Mass (g)T: Time (s)I: Current (A)Tp: Temperature (K)N: Amount (mol)J: Intensity (cd)