2026-04-21 11:44:48 +02:00
2026-04-21 11:44:48 +02:00
2026-04-20 23:38:49 +02:00
2026-04-20 23:38:49 +02:00
2026-04-21 00:37:23 +02:00

zig_units

Compile-time dimensional analysis for Zig.

zig_units lets you attach physical units to numeric values so that dimension mismatches become compile errors rather than silent bugs. At runtime a Quantity is nothing but a single number — zero overhead.

velocity = distance / time   →  L¹T⁻¹  ✓
force    = mass + velocity   →  compile error: M¹ ≠ L¹T⁻¹

Requires Zig 0.16 or later.


Features

  • Seven SI base dimensions (L M T I Tp N J)
  • Full SI prefix support (P T G M k h da d c m u n p f)
  • Custom time aliases (.min, .hour, .year)
  • Automatic scale conversion on add/sub (finer unit wins)
  • Quantity(T, dims, scales) — scalar, any numeric backing type
  • QuantityVec3 — three-component vector with the same guarantees
  • Unicode superscript formatting (9.81m.s⁻²)
  • Integer-safe square root for Vec3.length()
  • All dimension tracking happens at comptime — no runtime cost

Installation

Add as a Zig dependency

zig fetch --save https://github.com/YOUR_USERNAME/zig_units/archive/refs/heads/main.tar.gz

This adds an entry to your build.zig.zon. Then wire it up in your build.zig:

const zig_units = b.dependency("zig_units", .{
    .target  = target,
    .optimize = optimize,
});
my_module.addImport("zig_units", zig_units.module("zig_units"));

Local path (monorepo / development)

// build.zig.zon
.dependencies = .{
    .zig_units = .{ .path = "../zig_units" },
},

Quick start

const units     = @import("zig_units");
const Quantity  = units.Quantity;
const Dims      = units.Dimensions;
const Scales    = units.Scales;

// Define named unit types
const Meter    = Quantity(f32, Dims.init(.{ .L = 1 }),           Scales.init(.{}));
const KiloMeter= Quantity(f32, Dims.init(.{ .L = 1 }),           Scales.init(.{ .L = .k }));
const Second   = Quantity(f32, Dims.init(.{ .T = 1 }),           Scales.init(.{}));
const MPerSec  = Quantity(f32, Dims.init(.{ .L = 1, .T = -1 }), Scales.init(.{}));

const dist = Meter{ .value = 100.0 };
const t    = Second{ .value = 5.0 };

// Dimension is tracked automatically — vel has type L¹T⁻¹
const vel = dist.divBy(t);

// Convert to an explicit type (same dims required, compile error otherwise)
const vel2 = vel.to(MPerSec);

// Cross-scale addition: km + m → result in metres (finer scale)
const km  = KiloMeter{ .value = 1.0 };
const sum = km.add(dist);   // 1100 m

API reference

Quantity(T, dims, scales)

Member Kind Description
value field The raw numeric value
ValueType comptime Alias for T
dims comptime The Dimensions of this type
scales comptime The Scales of this type
Vec3 comptime The matching QuantityVec3 type
add(rhs) fn Same-dimension addition, finer scale
sub(rhs) fn Same-dimension subtraction, finer scale
mulBy(rhs) fn Multiplication, dims are added
divBy(rhs) fn Division, dims are subtracted
scale(s: T) fn Dimensionless scalar multiply
to(Dest) fn Convert to another Quantity type (same dims)
vec3() fn Broadcast scalar to a Vec3
format(writer) fn Print value + unit string

QuantityVec3

Obtained via SomeQuantity.Vec3.

Member Kind Description
x, y, z fields The three components
zero comptime (0, 0, 0)
one comptime (1, 1, 1)
initDefault(v) fn Broadcast scalar to all components
add(rhs) fn Component-wise addition
sub(rhs) fn Component-wise subtraction
mulBy(rhs) fn Component-wise element-wise multiply
divBy(rhs) fn Component-wise element-wise divide
mulByScalar(q) fn Multiply by a scalar Quantity
divByScalar(q) fn Divide by a scalar Quantity
scale(s: T) fn Dimensionless scalar multiply
negate() fn Negate all components
to(DestQ) fn Convert to another vector quantity type
lengthSqr() fn Squared Euclidean length (no sqrt)
length() fn Euclidean length (integer-safe)
format(writer) fn Print (x, y, z) + unit string

Dimensions

A comptime struct storing a signed exponent per SI base dimension.

const Dims = @import("zig_units").Dimensions;

// Acceleration: L¹ T⁻²
const accel_dims = Dims.init(.{ .L = 1, .T = -2 });
Function Description
init(struct_literal) Create from named exponents; unset dims default to 0
initFill(val: i8) Set all exponents to val
get(dim) Read a single exponent
set(dim, val) Write a single exponent
add(a, b) Component-wise sum (for mulBy)
sub(a, b) Component-wise difference (for divBy)
eql(a, b) Equality check
str() Comptime human-readable string, e.g. "L1T-2"

Scales

A comptime struct storing a UnitScale per SI base dimension.

const Scales = @import("zig_units").Scales;

// Kilometres per nanosecond
const spd_scales = Scales.init(.{ .L = .k, .T = .n });
UnitScale variant Factor
.P ×10¹⁵
.T ×10¹²
.G ×10⁹
.M ×10⁶
.k ×10³
.h ×10²
.da ×10¹
.none ×1
.d ×10⁻¹
.c ×10⁻²
.m ×10⁻³
.u ×10⁻⁶
.n ×10⁻⁹
.p ×10⁻¹²
.f ×10⁻¹⁵
.min ×60 (seconds)
.hour ×3 600
.year ×31 536 000

Examples

Kinematics

const Meter  = Quantity(f64, Dims.init(.{ .L = 1 }),           Scales.init(.{}));
const Second = Quantity(f64, Dims.init(.{ .T = 1 }),           Scales.init(.{}));

const pos  = Meter{ .value = 200.0 };
const time = Second{ .value = 8.0 };

const vel  = pos.divBy(time);        // L¹T⁻¹  — 25 m/s
const accel = vel.divBy(time);       // L¹T⁻²  — 3.125 m/s²

Cross-scale addition

const KM = Quantity(i64, Dims.init(.{ .L = 1 }), Scales.init(.{ .L = .k }));
const M  = Quantity(i64, Dims.init(.{ .L = 1 }), Scales.init(.{}));

const a = KM{ .value = 2 };   // 2 km
const b = M{ .value = 500 };  // 500 m

const sum = a.add(b);          // result scale = metres (finer) → 2500 m

Time conversion

const Hour   = Quantity(i64, Dims.init(.{ .T = 1 }), Scales.init(.{ .T = .hour }));
const Minute = Quantity(i64, Dims.init(.{ .T = 1 }), Scales.init(.{ .T = .min  }));
const Second = Quantity(i64, Dims.init(.{ .T = 1 }), Scales.init(.{}));

const h   = Hour{ .value = 2 };
const min = h.to(Minute);     // 120
const sec = min.to(Second);   // 7200

Vec3 velocity

const Meter  = Quantity(f32, Dims.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dims.init(.{ .T = 1 }), Scales.init(.{}));

const pos  = Meter.Vec3{ .x = 30.0, .y = 60.0, .z = 90.0 };
const time = Second{ .value = 3.0 };

const vel  = pos.divByScalar(time);   // Vec3 with dims L¹T⁻¹
const dist = vel.length();            // Euclidean length

Dimension mismatch — compile error

const Meter  = Quantity(f32, Dims.init(.{ .L = 1 }), Scales.init(.{}));
const Second = Quantity(f32, Dims.init(.{ .T = 1 }), Scales.init(.{}));

const d = Meter{ .value = 5.0 };
const t = Second{ .value = 2.0 };

// This will NOT compile:
const bad = d.add(t);  // error: Dimension mismatch in add: L1 vs T1

Running the tests

zig build test

The test suite covers scalar and vector arithmetic, cross-scale operations, conversion chains, negative values, formatting, and an optional benchmark ("Comprehensive Benchmark: All Ops × All Types").


Project layout

zig_units/
├── build.zig          # Build script; exposes the "zig_units" module
├── build.zig.zon      # Package manifest
├── src/
│   ├── main.zig       # Quantity, QuantityVec3, tests
│   ├── Dimensions.zig # SI base dimensions + comptime arithmetic
│   ├── Scales.zig     # SI prefixes + scale helpers
│   └── helper.zig     # Internal utilities (isInt, printSuperscript)
└── README.md

Design notes

Why comptime parameters? Zig's comptime means the compiler can evaluate all dimension arithmetic before any machine code is generated. Two quantities with mismatched dimensions simply fail to compile — there is no runtime overhead and no need for exception handling.

Scale selection on arithmetic. When two operands have different scales (e.g. km and m), zig_units automatically picks the finer (smaller-factor) scale for the result. This prevents silent precision loss at the cost of an automatic rescaling of both operands.

Integer backing types. Division uses an f64 intermediate and rounds back to the integer type. For best accuracy, prefer f32/f64 for quantities that will be divided frequently.


License

MIT — see LICENSE for details.

Description
Zero-overhead, compile-time dimensional analysis and unit conversion for Zig.
Readme GPL-3.0 529 KiB
First version Latest
2026-04-21 23:20:09 +00:00
Languages
Zig 100%