2026-04-22 00:19:07 +02:00
2026-04-22 00:19:07 +02:00
2026-04-20 23:38:49 +02:00
2026-04-22 00:00:18 +02:00
2026-04-22 00:00:18 +02:00
2026-04-21 15:44:31 +02:00

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⁻¹
}

Core Arithmetic Operations

Dimensional analysis is handled entirely at compile-time. If the math doesn't make physical sense, it won't compile.

const M = units.Base.Meter.Of(f32);
const S = units.Base.Second.Of(f32);

const dist = M{ .value = 100.0 };
const time = S{ .value = 5.0 };

// 1. Addition & Subtraction (Must have same dimensions)
const two_dist = dist.add(dist); // 200.0m
const zero     = dist.sub(dist); // 0.0m

// 2. Division (Subtracts dimension exponents)
// Result: Velocity (L¹ T⁻¹)
const vel = dist.divBy(time); // 20.0 m.s⁻¹

// 3. Multiplication (Adds dimension exponents)
// Result: Area (L²)
const area = dist.mulBy(dist); // 100.0 m²

// 4. Chained Operations
// Result: Acceleration (L¹ T⁻²)
const accel = dist.divBy(time).divBy(time); // 4.0 m.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., km to m), the library uses fast-path native multiplication.
  • 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.
  • Precision-First Scaling: When operating on two different scales (e.g., adding km and mm), the result automatically adopts the finer scale (mm). This ensures zero implicit data loss during calculation. You only lose precision if you explicitly choose to convert back to a coarser scale using .to().

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)

Testing & Benchmarks

zig_units comes with a comprehensive test suite that verifies dimensional correctness, SI prefix scaling, and vector math accuracy across all numeric types.

Running Tests

To run the full suite of unit tests and performance benchmarks:

zig build test

Benchmarks

When you run the tests, the library also executes a performance benchmark. This measures the cost of operations (in nanoseconds per operation) across different backing types (i32 to f128) and vector lengths.

Because all dimensional logic is resolved at compile-time, you will see that Quantity operations perform at the same speed as raw primitive math.

Example Benchmark Output:

 Quantity<T> benchmark — 100,000 iterations, 10 samples/cell

┌───────────────────┬──────┬─────────────────────┬─────────────────────┐
│ Operation         │ Type │ ns / op (± delta)   │ Throughput (ops/s)  │
├───────────────────┼──────┼─────────────────────┼─────────────────────┤
│ add               │ i32  │     0.18 ns ±0.02   │          5555555556 │
│ mulBy             │ f64  │     0.22 ns ±0.01   │          4545454545 │
└───────────────────┴──────┴─────────────────────┴─────────────────────┘

Verification Examples

The test suite ensures that:

  • Dimension Safety: Chained operations like dist.divBy(time).divBy(time) correctly result in an Acceleration type.
  • Scale Accuracy: Adding 1km + 1mm results exactly in 1000001mm without truncation.
  • Formatting: Quantities print correctly with Unicode superscripts (e.g., 9.81m.s⁻²).
  • Vector Math: Euclidean lengths for both floats and integers are verified against known constants.
Description
Zero-overhead, compile-time dimensional analysis and unit conversion for Zig.
Readme GPL-3.0 523 KiB
First version Latest
2026-04-21 23:20:09 +00:00
Languages
Zig 100%