unitmaker

command module
v0.0.0-...-e12ee5f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2025 License: MIT Imports: 11 Imported by: 0

README

unitmaker

Go Reference

A golang code generator to avoid incorrectly mixing different units, while still allowing you to express values natively. It lets you keep your variable names unit-agnostic, and ensures your usage is always explicit.

This would have saved the Mars Climate Orbiter, and might save you too!

Setup

This assumes your project is using golang version 1.24 or higher.

  1. Add unitmaker as a tool for your project
go get -tool github.com/nicois/unitmaker
  1. Define your unit interfaces in one or more source files, including the go:generate directive (more example/pregenerated definitions are available here)
//go:generate go tool unitmaker

package mypackage

type Length interface {
    Picometre() float64        // scale:"0.000000000001", display:"pm"
    Nanometre() float64        // scale:"0.000000001", display:"nm"
    Micrometre() float64       // scale:"0.000001", display:"μm"
    Millimetre() float64       // scale:"0.001", display:"mm"
    Centimetre() float64       // scale:"0.01", display:"cm"
    Metre() float64            // display:"m"
    Inch() float64             // scale:"0.0254", display:"in", hide:"true"
    Foot() float64             // scale:"0.0254*12", display:"ft", hide:"true"
    Yard() float64             // scale:"0.9144", display:"y", hide:"true"
    Mile() float64             // scale:"1609.344", display:"Mi", hide:"true"
    Kilometre() float64        // scale:"1000", display:"km"
    AstronomicalUnit() float64 // scale:"149_597_870_700", display:"au"
    Parsec() float64           // scale:"30856775814913673", display:"pc"
}

type Force interface {
    Dyne() float64          // display:"dyn", scale:"0.00001"
    Poundal() float64       // display:"pdl", scale:"0.1382550"
    Newton() float64        // display:"N"
    PoundForce() float64    // display:"lbf", scale:"4.48222"
    Kilopond() float64      // display:"kp", scale:"9.806650", hide:"true"
    KilogramForce() float64 // display:"kgf", scale:"9.806650"
    Kip() float64           // display:"kip", scale:"4448.222"
}

type Temperature interface {
    Celsius() float32    // display:"°C"
    Fahrenheit() float32 // display:"°F", scale:"5.0/9.0", addBefore:"-32"
    Kelvin() float32     // display:"K", addBefore:"-273.15"
}

type Time interface {
    Microsecond() float64 // scale:"0.000001"
    Millisecond() float64 // scale:"0.001"
    Second() float64
    Minute() float64 // scale:"60."
    Hour() float64   // scale:"3600."
}

// Derived interface: name:"Velocity",definition:"Length/Time"
  1. Trigger code generation

    For each processed source file <filename>.go, a new file named gen-<filename>.go will be created.

go generate

4 . Profit!

Unit directives

Unfortunately interface methods cannot be assigned backtick comments (used to hold json encoding directives, for example). Instead, regular comment blocks are used to contain directives for unitmaker

keyword meaning default value
display abbreviation / name to use in String() the unit's name
hide never select this unit in String() false
addBefore add this value before scaling 0
addAfter add this value before scaling 0
scale multiplier for this unit 1

Derived interfaces

Quite often it's not sufficient to work with a single concept at a time:

// Derived interface: name:"Velocity",definition:"Length/Time",secondaryUnits:"Second,Hour"
// Derived interface: name:"Impulse",definition:"Force*Time",secondaryUnits:"Second"
// Derived interface: name:"Area",definition:"Length*Length"
keyword meaning default value
name name of this derived concept. Make sure it is in CamelCase! (required)
definition two concepts (possibly derived) separated with * or / (required)
secondaryUnits unit names in the second concept to generate functions/methods for (all units)

If SecondaryUnits is defined, it should be a comma-separated list of units to create methods and functions for. This is simply to reduce the namespace clutter; it is always possible to manipulate variables using any units, whether they are referenced or not:

milePerMinute := Mile(1).DivideByTime(Minute(1))
distancePerDay := milePerMinute.MultiplyByTime(Day(1))

squareMile := Mile(1).MultiplyByLength(Mile(1))
lengthOfSquareMileIfOneKilometreWide := squareMile.DivideByLength(Kilometre(1))

Usage

Assuming the above interfaces are defined and go generate has been run:

package main
import "fmt"

func main() {
    // Any of the interface's method names can be used as a constructor
    x := Millimetre(3000)

    // values can be added (or subtracted)
    y := x.Add(Yard(2))

    // (alternatively, `AddTo`/`SubtractFrom` will modify the value in-place)
    y = Millimetre(3000)
    y.AddTo(Yard(2))

    // the Stringer implementation will generate a human-friendly representation
    fmt.Println("The distance is", y)

    // the actual value is available via any of the interface methods
    fmt.Println(y.Foot(), "feet")

    // but you want it printed with its units and a reasonable precision:
    fmt.Println(y.FootString())

    // scalar multiplication is done via Scale() or ScaleBy()
    z := y.Scale(10)
    z = y
    z.ScaleBy(10)
    fmt.Println(z)

    // scalar division is also possible, providing a dimensionless value
    fmt.Println(Metre(1).Divide(Yard(1)))
    // If a derived interface has been defined, additional
    // multiplication and division operations become available:
    speed := Mile(60).DivideByTime(Hour(1))
    fmt.Println(speed, "or", speed.MetrePerSecondString())
    fmt.Println("distance covered in 10 minutes:", speed.MultiplyByTime(Minute(10)))
}

would print

The distance is 482.9cm
15.84251968503937 feet
15.84ft
48.29m
1.0936132983377078
96.56km/h or 26.8224m/s
distance covered in 10 minutes: 16.09km

String() behaviour

The String() method will attempt to use the most appropriate unit, given the variable's value. Remember that units annotated with hide:"true" will never be chosen. "Most appropriate" means the unit's value is geometrically closest to 100, so 50cm will be chosen over 500mm, but 200mm will be chosen over 20cm. 4 significant figures is usally shown.

This can be demonstrated by running the following:

package main

import (
    "fmt"
    "math"
)

func main() {
    for power := range 40 {
        fmt.Println(power, Picometre(0.012345*math.Pow(10, float64(power))))
    }
}

which outputs

0 0.0123pm
1 0.123pm
2 1.234pm
3 12.35pm
4 123.5pm
5 1234pm
6 12.34nm
7 123.5nm
8 1234nm
9 12.35μm
10 123.5μm
11 1234μm
12 12.35mm
13 123.5mm
14 123.4cm
15 12.35m
16 123.5m
17 1234m
18 12.35km
19 123.5km
20 1234km
21 12345km
22 123450km
23 0.00825au
24 0.0825au
25 0.825au
26 8.252au
27 82.52au
28 825.2au
29 8252au
30 0.400pc
31 4.001pc
32 40.01pc
33 400.1pc
34 4001pc
35 40007pc
36 400074pc
37 4000742pc
38 40007420pc
39 400074203pc

Additional methods and functions

You may define additional functions and methods which interact with the generated object:

// Sin returns the sine of this Angle.
func (a *angle) Sin() float64 {
       return math.Sin(a.Radian())
}

// Atan2 returns the arc tangent of y/x, using the signs of the two to determine the quadrant of the Angle.
func Atan2(y, x float64) *angle {
    result := angle(math.Atan2(y, x))
    return &result
}

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL