quanta

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 10 Imported by: 0

README

quanta

Units of measure for Go — dimensional analysis at run time, with conversion factors held as exact rationals rather than pre-rounded floats.

Every units library can tell you a metre is not a second. The interesting part is what happens next: whether an inch is 0.0254 or exactly 254/10000, whether °C can sneak into a product and quietly produce nonsense, and whether you can add a unit the author never thought of. quanta takes those three questions seriously — conversions run in math/big.Rat and round to float64 once, affine units are structurally prevented from entering an expression, and the unit table is a registry you extend, not an enum you're stuck with.

  • Exact conversion factors, lossless conversion. An inch is 254/10000 m, a pound is 45359237/100000000 kg, a US gallon is 231 in³. A quantity carries its magnitude as an exact rational, so a mile converted to feet to nautical miles to metres and back is bit-for-bit the value you started with.
  • Affine units that can't misbehave. 0 °C == 273.15 K == 32 °F, and °C·m, °C² and k°C are all errors rather than silent absurdities. Temperature differences get their own linear units (Δ°C).
  • A registry, not an enum. Define your own units — including your own base dimensions — and parse expressions against them: USD/tonne works because you defined USD.
  • Compound expression parsing. m/s^2, kW·h, mg/dL, J/(kg·K), 1/s, m⁻², with SI prefixes on any unit and binary prefixes on B/bit.
  • Typed errors. ErrDimensionMismatch, ErrAffineUnit, ErrUnknownUnit, ErrDuplicateUnit, ErrSyntax, ErrExponentRange, plus a *ParseError with the offending input and byte offset.
  • Zero dependencies. Go 1.23+.
import "github.com/zkrebbekx/quanta"

Example

si := quanta.SI()

d, _ := si.Parse("26.2 mi")
km, _ := d.In(si.MustUnit("km"))
fmt.Println(km.Format(3)) // 42.165 km

speed, _ := si.Parse("100 km/h")
time, _ := km.Div(speed)
fmt.Println(time.MustIn(si.MustUnit("min")).Format(1)) // 25.3 min

_, err := d.Add(si.MustParse("3 s"))
fmt.Println(errors.Is(err, quanta.ErrDimensionMismatch)) // true

Exactness

A Quantity holds two things: the float64 you read back with Value(), and the exact magnitude as a big.Rat in coherent SI units. In() carries the rational through untouched and re-renders the float64 in the new unit — the conversion

value_to = (exact_si - offset_to) / factor_to

runs entirely in math/big.Rat. That is the whole precision story, and it is worth being precise about what it does and does not buy you:

  • You get lossless conversion, and losslessness composes. A hundred conversions carry no more error than one, and there-and-back always returns the original float64 — the test suite checks this over randomised values through five awkward unit chains, and none differ. This is stronger than the obvious approach of rounding correctly at each hop, which still drifts by an ulp about one time in ten. Multiplying pre-rounded float64 factors, the usual approach, is worse again.
  • You do not get exact magnitudes. Values are float64, and arithmetic on magnitudes (Add, Mul, …) is ordinary float64 arithmetic with ordinary float64 rounding — it drops the exact rational, keeping only the operand conversions exact. Conversions are lossless; arithmetic is float64.

Dimensions

Dimension is a comparable [7]int8 of the SI base-quantity exponents, so a dimension check is one machine comparison and a Dimension works as a map key.

acceleration := quanta.Length.Div(quanta.Time.Pow(2))
fmt.Println(acceleration)              // L·T⁻²
fmt.Println(acceleration == quanta.Length) // false

Exponents are int8. Quantity.Mul, Div and Pow return ErrExponentRange if an exponent would leave [-128, 127]; the bare Dimension methods can't return an error, so they saturate — use CheckedMul, CheckedDiv, CheckedPow when you need to know.

Affine units

°C and °F have a zero point, which makes them a different kind of thing from a scale factor. quanta converts them correctly and then confines them: they may not be multiplied, divided, raised to a power, prefixed, or written into a compound expression, and each of those returns ErrAffineUnit.

c := quanta.New(0, si.MustUnit("°C"))
fmt.Println(c.MustIn(si.MustUnit("K")))  // 273.15 K
fmt.Println(c.MustIn(si.MustUnit("°F"))) // 32 °F

_, err := si.ParseUnit("°C/m") // ErrAffineUnit

Differences of temperature are their own units. Unit.Delta() turns °C into Δ°C, and Sub uses it automatically:

hot, cold := si.MustParse("30 °C"), si.MustParse("10 °C")
diff, _ := hot.Sub(cold)
fmt.Println(diff) // 20 Δ°C

warmer, _ := cold.Add(si.MustParse("5 Δ°C"))
fmt.Println(warmer) // 15 °C

Adding two absolute temperatures is an error, because it means nothing.

Registries and parsing

SI() returns a fresh registry with the SI base and derived units plus the common non-SI ones (imperial and US customary lengths, masses and volumes; minute/hour/day; bar, atmosphere, calorie, watt-hour, electronvolt; byte and bit with binary prefixes). Extend it, or start from NewRegistry() and define everything yourself — including new base dimensions.

si := quanta.SI()
usd, _ := si.DefineBase("US dollar", "USD", quanta.Dimension{})
price, _ := si.Parse("640 USD/tonne")

Parsing rules worth knowing:

  • An exact symbol always beats a prefixed reading. min is the minute, not a milli-inch; cd is the candela, not a centi-day.
  • The kilogram is handled properly. kg is the coherent base unit and is not itself prefixable; the gram carries the prefixes, so mg is 10⁻⁶ kg.
  • · and / bind equally and associate left, so a/b/c is (a/b)/c and J/kg·K is (J/kg)·K. Write J/(kg·K) if that's what you meant.
  • Binary prefixes are restricted to units that opt in — B and bit. There is no Mim.
  • Failures are a *ParseError with Input and Offset, wrapping a sentinel.

Non-goals

  • No contexts. No domain-specific conversions that hold only under an assumption (spectroscopy's wavelength/frequency trade, for instance).
  • No uncertainty propagation. Magnitudes carry no error bars.
  • No currency rates. Define a currency unit if you like; quanta ships no exchange rates.
  • No exact magnitudes. Only factors are rational — see Exactness above.
  • No UnmarshalText. Decoding needs a registry to resolve the symbol and a method has nowhere to get one, so Quantity implements MarshalText only. Decode with Registry.Parse, which makes the registry explicit.

License

MIT

Documentation

Overview

Package quanta is a units-of-measure library for Go: dimensional analysis at run time, with conversion factors held as exact rationals.

A Quantity is a float64 magnitude paired with a Unit. A Unit knows its Dimension — the vector of the seven SI base-quantity exponents — and its exact conversion to the coherent SI unit of that dimension. Adding a length to a time is an error, not a silent absurdity; multiplying metres by metres gives m²; and converting a mile to metres and back returns the number you started with, bit for bit.

si := quanta.SI()
d, _ := si.Parse("26.2 mi")
km, _ := d.In(si.MustUnit("km"))
fmt.Println(km.Format(3)) // 42.165 km

Dimensional analysis

Dimension is a comparable [7]int8 array, so dimension checks are a single machine comparison and a Dimension may be used as a map key. Every operation that combines quantities either verifies dimensions (Quantity.Add, Quantity.Sub, Quantity.Cmp, Quantity.In) or derives the new one (Quantity.Mul, Quantity.Div, Quantity.Pow). Exponents live in an int8; operations that would push one outside [-128, 127] report ErrExponentRange rather than wrapping. The bare Dimension.Mul, Dimension.Div and Dimension.Pow cannot return an error, so they saturate instead — use Dimension.CheckedMul and its siblings when you need to know.

Exact rational factors

Conversion factors are math/big.Rat values, never floats. An inch is exactly 254/10000 metre; a pound is exactly 45359237/100000000 kilogram; a US gallon is exactly 231 cubic inches.

A Quantity carries its magnitude as an exact rational in coherent SI units alongside the float64 you read with Quantity.Value, and Quantity.In passes that rational through untouched — the float64 is only ever the rational rendered in the current unit. Conversion is therefore lossless, and losslessness composes: a chain of a hundred conversions has no more error than one, and converting there and back returns bit-for-bit the value you started with. That is a strictly stronger guarantee than rounding correctly at every hop, which still drifts by an ulp roughly one time in ten.

What this does not do is make the magnitudes exact. Values are float64, and a float64 is what you get back. Arithmetic on magnitudes (Quantity.Add, Quantity.Mul and friends) is ordinary float64 arithmetic, with ordinary float64 rounding, and it discards the exact rational — only the operand conversions it performs along the way are exact. The precise guarantee is: conversions are lossless, arithmetic is float64.

Affine units

Degrees Celsius and Fahrenheit have a zero point of their own, which is why 0 °C is 273.15 K but 0 m/s is 0 mph. quanta models that with an exact rational offset, and then confines it. Affine units convert correctly in both directions, compare, and negate, but they may not be multiplied, divided, raised to a power, prefixed, or written into a compound unit expression; each of those returns an error wrapping ErrAffineUnit. This closes the trap where "°C·m" or "°C²" quietly produces a nonsense factor.

Differences of temperature are a separate idea from temperatures, so they get separate units: Unit.Delta turns °C into Δ°C, the linear unit of temperature difference, and SI registers Δ°C and Δ°F alongside the kelvin. Quantity.Sub applies this automatically — 30 °C minus 10 °C is 20 Δ°C, and 20 °C plus 5 Δ°C is 25 °C — while adding two absolute temperatures is an error.

Registries

There is no fixed enumeration of units. A Registry is a namespace of symbols that callers extend: Registry.DefineBase introduces a coherent unit (or an entirely new dimension of your own), Registry.Define scales an existing one by an exact rational, Registry.DefineAffine adds a zero point, and Registry.Alias adds spellings. Registering a symbol twice is ErrDuplicateUnit, never a silent overwrite. SI returns a fresh registry preloaded with the SI base and derived units plus the common non-SI units; because it is fresh on each call, extending it cannot disturb anyone else.

Registry.ParseUnit parses compound expressions — "m/s^2", "kW·h", "mg/dL", "J/(kg·K)", "1/s", "m⁻²" — including SI decimal prefixes on any prefixable unit and IEC binary prefixes on the information units. An exactly registered symbol always beats a prefixed reading, so "min" is the minute rather than a milli-inch. Multiplication and division bind equally and associate to the left, so "a/b/c" is (a/b)/c; parenthesise when that is not what you mean. Parse failures are a *ParseError carrying the input and a byte offset.

Immutability and concurrency

Unit and Quantity are immutable values. Every method returns a new value and none mutate the receiver, so they are safe to copy and safe to share across goroutines. A Registry is guarded internally and is safe for concurrent definition and lookup. Accessors that would otherwise expose internal state (Unit.Factor, Unit.Offset) return fresh copies.

Non-goals

quanta deliberately leaves these out:

  • No contexts. There is no mechanism for domain-specific conversions that are only valid under an assumption, such as trading wavelength for frequency in spectroscopy.
  • No uncertainty. Magnitudes carry no error bars and nothing is propagated.
  • No currency rates. You can define currency units — nothing stops you from a USD/tonne — but quanta ships no exchange rates and never will.
  • No exact magnitudes. Only factors are rational; values are float64.
  • No UnmarshalText. Decoding a quantity needs a registry to resolve the symbol, and a method has nowhere to get one; use Registry.Parse.
Example

Example converts a marathon from miles to kilometres.

package main

import (
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()

	d, _ := si.Parse("26.2 mi")
	km, _ := d.In(si.MustUnit("km"))

	fmt.Println(km.Format(3))
}
Output:
42.165 km
Example (AffineRejected)

Example_affineRejected shows that an affine unit cannot enter a compound expression.

package main

import (
	"errors"
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()

	_, err := si.ParseUnit("°C/m")
	fmt.Println(errors.Is(err, quanta.ErrAffineUnit))

	var pe *quanta.ParseError
	if errors.As(err, &pe) {
		fmt.Println(pe.Offset)
	}
}
Output:
true
3
Example (BinaryPrefix)

Example_binaryPrefix shows IEC prefixes on the information units.

package main

import (
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()

	size := si.MustParse("1.5 GiB")
	fmt.Println(size.MustIn(si.MustUnit("MiB")))
	fmt.Println(size.MustIn(si.MustParseUnit("MB")).Format(1))
}
Output:
1536 MiB
1610.6 MB
Example (CustomUnit)

Example_customUnit defines a unit the library does not ship and parses an expression that uses it.

package main

import (
	"fmt"
	"math/big"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()
	usd, err := si.DefineBase("US dollar", "USD", quanta.Dimensionless)
	if err != nil {
		panic(err)
	}
	// A furlong is exactly 660 feet.
	if _, err := si.Define("furlong", "fur", big.NewRat(660, 1), si.MustUnit("ft")); err != nil {
		panic(err)
	}

	price, _ := si.Parse("640 USD/tonne")
	fmt.Println(price)
	fmt.Println(usd.Dimension().IsDimensionless())
	fmt.Println(si.MustParse("2 fur").MustIn(si.MustUnit("m")).Format(1))
}
Output:
640 USD/t
true
402.3 m
Example (DimensionMismatch)

Example_dimensionMismatch shows the error returned when dimensions disagree.

package main

import (
	"errors"
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()

	_, err := si.MustParse("5 m").Add(si.MustParse("3 s"))
	fmt.Println(errors.Is(err, quanta.ErrDimensionMismatch))
	fmt.Println(err)
}
Output:
true
cannot add L and T: quanta: dimension mismatch
Example (DimensionalAnalysis)

Example_dimensionalAnalysis derives a unit by dividing quantities.

package main

import (
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()

	distance := si.MustParse("100 m")
	elapsed := si.MustParse("9.58 s")
	speed, _ := distance.Div(elapsed)

	fmt.Println(speed.Format(2))
	fmt.Println(speed.Dimension())
	fmt.Println(speed.MustIn(si.MustParseUnit("km/h")).Format(1))
}
Output:
10.44 m/s
L·T⁻¹
37.6 km/h
Example (ExactRoundTrip)

Example_exactRoundTrip shows that a conversion chain returns the original float64: factors are exact rationals, so nothing drifts.

package main

import (
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()
	start := quanta.New(3.7, si.MustUnit("mi"))

	there := start.
		MustIn(si.MustUnit("ft")).
		MustIn(si.MustUnit("nmi")).
		MustIn(si.MustUnit("m"))
	back := there.MustIn(si.MustUnit("mi"))

	fmt.Println(back.Value() == start.Value())
}
Output:
true
Example (Temperature)

Example_temperature shows affine conversion and the delta unit that subtraction produces.

package main

import (
	"fmt"

	"github.com/zkrebbekx/quanta"
)

func main() {
	si := quanta.SI()
	freezing := quanta.New(0, si.MustUnit("°C"))

	fmt.Println(freezing.MustIn(si.MustUnit("K")))
	fmt.Println(freezing.MustIn(si.MustUnit("°F")))

	diff, _ := si.MustParse("30 °C").Sub(si.MustParse("10 °C"))
	fmt.Println(diff)
}
Output:
273.15 K
32 °F
20 Δ°C

Index

Examples

Constants

View Source
const (
	IdxLength = iota
	IdxMass
	IdxTime
	IdxCurrent
	IdxTemperature
	IdxAmount
	IdxLuminousIntensity

	// NumBaseQuantities is the number of SI base quantities.
	NumBaseQuantities
)

Base-quantity indices into a Dimension.

Variables

View Source
var (
	Dimensionless     = Dimension{}
	Length            = Dimension{IdxLength: 1}
	Mass              = Dimension{IdxMass: 1}
	Time              = Dimension{IdxTime: 1}
	Current           = Dimension{IdxCurrent: 1}
	Temperature       = Dimension{IdxTemperature: 1}
	Amount            = Dimension{IdxAmount: 1}
	LuminousIntensity = Dimension{IdxLuminousIntensity: 1}
)

The seven base dimensions, plus the dimensionless (zero) dimension.

View Source
var (
	// ErrDimensionMismatch is returned when an operation requires two
	// quantities to share a dimension and they do not — for example adding a
	// length to a time, or converting metres to seconds.
	ErrDimensionMismatch = errors.New("quanta: dimension mismatch")

	// ErrAffineUnit is returned when an affine unit (one with a non-zero zero
	// point, such as °C or °F) is used where only a linear unit is meaningful:
	// multiplication, division, exponentiation, or as part of a compound unit
	// expression.
	ErrAffineUnit = errors.New("quanta: affine unit not permitted here")

	// ErrUnknownUnit is returned when a symbol cannot be resolved by a
	// registry, either directly or as a prefixed form of a known unit.
	ErrUnknownUnit = errors.New("quanta: unknown unit")

	// ErrDuplicateUnit is returned when a definition or alias would overwrite
	// a symbol that is already registered. Registration never silently
	// replaces an existing unit.
	ErrDuplicateUnit = errors.New("quanta: duplicate unit symbol")

	// ErrSyntax is returned for malformed unit or quantity expressions. Such
	// errors are always also a [*ParseError], which carries the offending
	// input and the byte offset at which the problem was detected.
	ErrSyntax = errors.New("quanta: syntax error")

	// ErrExponentRange is returned when an operation would drive a dimension
	// exponent outside the representable int8 range [-128, 127].
	ErrExponentRange = errors.New("quanta: dimension exponent out of range")

	// ErrInvalidUnit is returned when a unit definition is malformed: a nil or
	// zero conversion factor, or an affine unit used as the base of another
	// definition.
	ErrInvalidUnit = errors.New("quanta: invalid unit definition")
)

Sentinel errors returned (usually wrapped) by this package. Match them with errors.Is rather than by comparing error strings.

View Source
var One = Unit{}

One is the dimensionless unit one — the identity for unit multiplication and the unit of a bare number.

Functions

This section is empty.

Types

type Dimension

type Dimension [NumBaseQuantities]int8

Dimension is a vector of the seven SI base-quantity exponents, in the order length, mass, time, electric current, thermodynamic temperature, amount of substance, luminous intensity.

Dimension is a comparable array type: it may be used as a map key and compared with ==. The zero Dimension is Dimensionless.

func (Dimension) CheckedDiv

func (d Dimension) CheckedDiv(o Dimension) (Dimension, bool)

CheckedDiv is Dimension.Div with an overflow report, as for Dimension.CheckedMul.

func (Dimension) CheckedMul

func (d Dimension) CheckedMul(o Dimension) (Dimension, bool)

CheckedMul is Dimension.Mul with an overflow report: ok is false if any resulting exponent would fall outside the int8 range, in which case the returned dimension is saturated.

func (Dimension) CheckedPow

func (d Dimension) CheckedPow(n int) (Dimension, bool)

CheckedPow is Dimension.Pow with an overflow report, as for Dimension.CheckedMul.

func (Dimension) Div

func (d Dimension) Div(o Dimension) Dimension

Div returns the dimension of a quotient: exponents are subtracted element-wise. Exponents saturate exactly as described for Dimension.Mul.

func (Dimension) IsDimensionless

func (d Dimension) IsDimensionless() bool

IsDimensionless reports whether every base exponent is zero.

func (Dimension) Mul

func (d Dimension) Mul(o Dimension) Dimension

Mul returns the dimension of a product: exponents are added element-wise.

Exponents saturate at the bounds of int8 rather than wrapping. Saturation is silent here because Mul cannot fail; the quantity-level operations (Quantity.Mul, Quantity.Div, Quantity.Pow) detect the same condition and report ErrExponentRange instead. Use Dimension.CheckedMul when you need to know.

func (Dimension) Pow

func (d Dimension) Pow(n int) Dimension

Pow returns d raised to the integer power n: every exponent is multiplied by n. Exponents saturate exactly as described for Dimension.Mul.

func (Dimension) String

func (d Dimension) String() string

String renders the dimension using the conventional base symbols M, L, T, I, Θ, N and J joined by the middle dot, with Unicode superscript exponents — for example "L·T⁻²" for acceleration and "M·L²·T⁻²" for energy. Exponents of one are omitted and zero exponents are skipped entirely. The dimensionless dimension renders as "1".

The ordering is fixed (mass, length, time, current, temperature, amount, luminous intensity), so the output is deterministic and safe to compare.

type Option

type Option func(*Unit)

Option adjusts how a unit is registered. Options are passed to Registry.DefineBase, Registry.Define and Registry.DefineAffine.

func BinaryPrefix

func BinaryPrefix() Option

BinaryPrefix marks a unit as accepting IEC binary prefixes (Ki, Mi, Gi, …) in addition to the decimal ones. Only information units (bit, byte) are registered this way by SI.

func NoPrefix

func NoPrefix() Option

NoPrefix marks a unit as not accepting SI prefixes. It is the right choice for a unit whose symbol already carries one — the kilogram — and for symbols where prefixing would produce nonsense.

type ParseError

type ParseError struct {
	// Input is the complete string that was being parsed.
	Input string
	// Offset is the byte offset within Input at which the error was detected.
	// It is len(Input) when the input ended unexpectedly.
	Offset int
	// Msg is a short human-readable description of the problem.
	Msg string
	// Err is the wrapped sentinel error.
	Err error
}

ParseError describes a failure to parse a unit or quantity expression. It records the full input and the byte offset at which the parser gave up, and wraps a sentinel error (ErrSyntax, ErrUnknownUnit, ErrAffineUnit or ErrExponentRange) so that errors.Is works as expected.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements the error interface.

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

Unwrap returns the wrapped sentinel error so that errors.Is can match it.

type Quantity

type Quantity struct {
	// contains filtered or unexported fields
}

Quantity is a float64 magnitude paired with a Unit.

Quantities are immutable values: every operation returns a new Quantity and none of them mutate the receiver, so a Quantity is safe to copy and safe for concurrent use.

The zero Quantity is the dimensionless number zero.

func New

func New(value float64, u Unit) Quantity

New returns the quantity value·u.

func (Quantity) Abs

func (q Quantity) Abs() Quantity

Abs returns |q|, in the same unit, with the same caveat about affine scales as Quantity.Neg.

func (Quantity) Add

func (q Quantity) Add(o Quantity) (Quantity, error)

Add returns q+o, expressed in q's unit. Both quantities must have the same dimension. o is converted losslessly, but the addition itself is ordinary float64 arithmetic — see the package documentation on what is and is not exact.

Affine units follow the rules of temperature arithmetic: an absolute temperature may not be added to anything, so o must be linear. If q itself is affine, o is treated as a difference and added to q's magnitude — 20 °C plus 5 Δ°C is 25 °C. Otherwise the operation is an ordinary conversion and sum.

func (Quantity) Cmp

func (q Quantity) Cmp(o Quantity) (int, error)

Cmp compares two quantities of the same dimension, returning -1 if q sorts before o, 0 if they are equal and +1 if q sorts after o.

o is converted losslessly into q's unit and the two float64 magnitudes are then compared, so 1 km compares greater than 999 m and 0 °C compares equal to 273.15 K. Comparison is deliberately done at float64 precision in q's unit rather than on the exact rationals: the latter would make 273.15 K differ from 0 °C by the ulp that float64 cannot represent, which is true but useless.

It returns an error wrapping ErrDimensionMismatch if the dimensions differ. If either magnitude is NaN the comparison is not meaningful and the result is 0 with no error, matching the behaviour of float64 comparison operators.

func (Quantity) Dimension

func (q Quantity) Dimension() Dimension

Dimension returns the dimension of the quantity's unit.

func (Quantity) Div

func (q Quantity) Div(o Quantity) (Quantity, error)

Div returns q/o. The dimensions divide and the units compound, so metres divided by seconds is m/s. Affine units may not be divided; doing so returns an error wrapping ErrAffineUnit.

func (Quantity) Equal

func (q Quantity) Equal(o Quantity) bool

Equal reports whether two quantities describe the same physical value. It is dimension-aware: 1000 m equals 1 km, and a length is never equal to a time. Comparing across dimensions is false rather than an error.

func (Quantity) Format

func (q Quantity) Format(prec int) string

Format renders the quantity with exactly prec digits after the decimal point, for example Format(2) on 9.80665 m/s² gives "9.81 m/s²". A negative prec means "as many digits as needed", as in Quantity.String.

func (Quantity) In

func (q Quantity) In(u Unit) (Quantity, error)

In converts the quantity to unit u, which must have the same dimension. Offsets are applied in both directions, so New(0, degC).In(kelvin) is 273.15 K and New(0, degC).In(degF) is 32 °F.

Conversion is lossless. The exact magnitude is held as a rational and carried through unchanged; the float64 you read back with Quantity.Value is that rational rendered in the current unit. A conversion therefore never accumulates error, however long the chain, and converting there and back always returns bit-for-bit the value you started with:

q.MustIn(foot).MustIn(nauticalMile).MustIn(metre).MustIn(mile) == q

It returns an error wrapping ErrDimensionMismatch if the dimensions differ.

func (Quantity) IsZero

func (q Quantity) IsZero() bool

IsZero reports whether the magnitude is exactly zero. Note that this is a question about the magnitude in the quantity's own unit, which for an affine unit is not the same question as whether the quantity is at the origin of its dimension: 0 °C is not 0 K.

func (Quantity) MarshalText

func (q Quantity) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, producing the same text as Quantity.String.

There is deliberately no UnmarshalText: reading a quantity back requires a Registry to resolve the unit symbol, and a method on Quantity has nowhere to obtain one. Decode with Registry.Parse instead, which makes the choice of registry explicit.

func (Quantity) Mul

func (q Quantity) Mul(o Quantity) (Quantity, error)

Mul returns q·o. The dimensions multiply and the units compound, so metres times metres is m² and newtons times metres is N·m. Affine units may not be multiplied; doing so returns an error wrapping ErrAffineUnit.

func (Quantity) MustIn

func (q Quantity) MustIn(u Unit) Quantity

MustIn is Quantity.In for cases where the conversion is known to be valid, such as package-level constants and tests. It panics on error.

func (Quantity) Neg

func (q Quantity) Neg() Quantity

Neg returns -q, in the same unit. For an affine unit this negates the magnitude on that unit's own scale: -q of 5 °C is -5 °C, not -278.15 K.

func (Quantity) Pow

func (q Quantity) Pow(n int) (Quantity, error)

Pow returns q raised to the integer power n. Pow(0) is the dimensionless 1. Affine units may not be raised to a power; doing so returns an error wrapping ErrAffineUnit.

func (Quantity) String

func (q Quantity) String() string

String renders the quantity as its magnitude and symbol, for example "9.81 m/s²". The magnitude uses the shortest representation that round-trips through strconv.ParseFloat. A dimensionless quantity renders as the bare number.

func (Quantity) Sub

func (q Quantity) Sub(o Quantity) (Quantity, error)

Sub returns q-o, expressed in q's unit, with one exception: subtracting one affine quantity from another yields a difference, so the result is expressed in q's delta unit (see Unit.Delta) — 30 °C minus 10 °C is 20 Δ°C, not 20 °C. Subtracting a linear quantity from an affine one shifts it and stays affine. Subtracting an affine quantity from a linear one is meaningless and returns an error wrapping ErrAffineUnit.

func (Quantity) Unit

func (q Quantity) Unit() Unit

Unit returns the quantity's unit.

func (Quantity) Value

func (q Quantity) Value() float64

Value returns the magnitude in the quantity's own unit.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a namespace of unit symbols. It is the extension point of the package: rather than a fixed enumeration of conversions, callers define whatever units they need — including entirely new base dimensions — and parse expressions against their own registry.

A Registry is safe for concurrent use, including concurrent definition and lookup.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry. No symbol is known to it until one is defined; only the seven base dimensions themselves are built in.

func SI

func SI() *Registry

SI returns a registry preloaded with the SI base and derived units plus the common non-SI units that appear alongside them: imperial and US customary lengths, masses and volumes, the pressure, energy and information units in everyday use, and the two affine temperature scales.

Each call builds and returns a fresh registry, so callers may extend the result — with Registry.Define and friends — without affecting anyone else. Building costs a few tens of microseconds; hoist the result out of hot loops rather than calling SI repeatedly.

Some notes on the choices made:

  • The coherent SI unit of mass is the kilogram, so "kg" is registered as the base unit with factor 1 and is not itself prefixable. The gram is a derived unit of 1/1000 kg, and it is the gram that carries prefixes, so "mg" is 10⁻⁶ kg, exactly as SI intends.
  • Information units (bit, byte) have no SI dimension, so they are modelled as dimensionless. They are the only units that accept binary prefixes, so "MiB" resolves but "Mim" does not.
  • Temperature differences are expressed with the kelvin, or with the linear delta units "Δ°C" and "Δ°F". See Unit.Delta.
  • The US and imperial gallons differ, so neither claims the symbol "gal"; they are "gal_US" and "gal_UK".

func (*Registry) Alias

func (r *Registry) Alias(symbol string, u Unit) error

Alias registers an additional symbol for an already-defined unit — "meter" for "m", say, or "sec" for "s". Aliases resolve to the same unit but do not appear in Registry.Units.

func (*Registry) Define

func (r *Registry) Define(name, symbol string, factor *big.Rat, base Unit, opts ...Option) (Unit, error)

Define registers a unit as an exact rational multiple of an existing one: one unit of the new symbol equals factor units of base. The factor must be non-zero, and base must be linear — an affine unit cannot be scaled.

Because factor is a big.Rat, conversions stay exact: an inch is defined as exactly 254/10000 of a metre, not as the float64 nearest 0.0254.

func (*Registry) DefineAffine

func (r *Registry) DefineAffine(name, symbol string, factor, offset *big.Rat, base Unit, opts ...Option) (Unit, error)

DefineAffine registers a unit with a zero point of its own: a value v in the new unit equals v*factor + offset units of base. The degree Celsius is factor 1 and offset 5463/20 (273.15) relative to the kelvin; the degree Fahrenheit is factor 5/9 and offset 45967/180.

Affine units are deliberately restricted: they may not be prefixed, may not appear in a compound unit expression, and may not be multiplied, divided or raised to a power. See Unit.Delta for expressing differences.

func (*Registry) DefineBase

func (r *Registry) DefineBase(name, symbol string, d Dimension, opts ...Option) (Unit, error)

DefineBase registers a coherent base unit: one whose conversion factor to itself is exactly 1, such as the metre for length. Use it to introduce a dimension's reference unit, or to invent a dimension of your own.

func (*Registry) MustParse

func (r *Registry) MustParse(s string) Quantity

MustParse is Registry.Parse for inputs known to be valid, such as package-level variables and tests. It panics on error.

func (*Registry) MustParseUnit

func (r *Registry) MustParseUnit(s string) Unit

MustParseUnit is Registry.ParseUnit for expressions known to be valid, such as package-level variables and tests. It panics on error.

func (*Registry) MustUnit

func (r *Registry) MustUnit(symbol string) Unit

MustUnit is Registry.Unit for symbols known to exist, such as package-level variables and tests. It panics if the symbol is not registered.

func (*Registry) Parse

func (r *Registry) Parse(s string) (Quantity, error)

Parse parses a quantity: a float64 magnitude followed by an optional unit expression, as in "9.81 m/s^2", "1.5 kW·h", "-40 °C" or "1e3" (which is the dimensionless 1000). The space between magnitude and unit is optional.

The unit part is parsed by Registry.ParseUnit and obeys all of its rules.

func (*Registry) ParseUnit

func (r *Registry) ParseUnit(s string) (Unit, error)

ParseUnit parses a compound unit expression against the registry.

The grammar accepts unit symbols, SI and binary prefixes, parentheses, multiplication (written "·", "*", "×", "." or simply a space), division ("/"), and integer exponents written "^2", "**2" or with Unicode superscripts ("²"). Exponents may be negative, and the numeral 1 may stand alone as a numerator, so "1/s", "s^-1" and "s⁻¹" all denote the hertz.

Multiplication and division bind equally and associate to the left: "J/kg·K" parses as "(J/kg)·K", which is probably not what was meant — write "J/(kg·K)". This is stated rather than guessed at, because unit strings in the wild disagree with each other.

An exact registered symbol always wins over a prefixed reading: "min" is the minute, not a milli-inch, and "cd" is the candela, not a centi-day.

Affine units such as °C may be parsed alone but not as part of a compound expression; that returns an error wrapping ErrAffineUnit. An empty or blank expression denotes the dimensionless unit one.

Errors are always a *ParseError carrying the input and the byte offset of the problem, wrapping ErrSyntax, ErrUnknownUnit, ErrAffineUnit or ErrExponentRange.

func (*Registry) Unit

func (r *Registry) Unit(symbol string) (Unit, bool)

Unit looks up a single unit symbol, resolving SI and binary prefixes: "m" and "km" both succeed, and so does the alias "metre". An exactly registered symbol always wins over a prefixed reading, so "min" is the minute rather than a milli-inch.

Unit does not parse expressions — there is no "m/s" here. Use Registry.ParseUnit for that.

func (*Registry) Units

func (r *Registry) Units() []Unit

Units returns every unit defined in the registry — excluding aliases — sorted by symbol, so the order is deterministic across runs and independent of definition order.

type Unit

type Unit struct {
	// contains filtered or unexported fields
}

Unit is a named scale on a Dimension.

A unit records an exact conversion to the coherent SI unit of its dimension as a rational factor, plus an optional rational offset for affine units such as °C and °F. The relationship is

value_in_coherent_SI = value*factor + offset

Units are immutable values produced by a Registry or by combining other units (see Quantity.Mul and friends). Copying a Unit is free and safe; a Unit is safe for concurrent use.

The zero Unit is the dimensionless unit one: factor 1, no offset, empty symbol. It is fully usable, so New(2, quanta.Unit{}) is a valid count of two.

Unit is not comparable with == (it carries a slice of factorisation terms); use Unit.Equal, or compare Unit.Symbol values when a map key is needed.

func (Unit) Delta

func (u Unit) Delta() Unit

Delta returns the linear "difference" unit that shares this unit's scale but has no zero point — the unit in which differences of affine quantities are expressed. Delta on °C yields Δ°C, one kelvin of temperature difference; on a unit that is already linear it returns the unit unchanged.

func (Unit) Dimension

func (u Unit) Dimension() Dimension

Dimension returns the unit's dimension.

func (Unit) Equal

func (u Unit) Equal(o Unit) bool

Equal reports whether two units are interchangeable: same dimension, same exact factor, same exact offset and same symbol.

func (Unit) Factor

func (u Unit) Factor() *big.Rat

Factor returns a copy of the exact rational conversion factor from this unit to the coherent SI unit of its dimension. The returned value is a fresh big.Rat the caller may modify freely.

func (Unit) IsAffine

func (u Unit) IsAffine() bool

IsAffine reports whether the unit has a non-zero zero point, as °C and °F do. Affine units may be converted to and from, compared, and used with Add and Sub under the rules described on those methods, but they may never take part in multiplication, division, exponentiation, or a compound unit expression.

func (Unit) Name

func (u Unit) Name() string

Name returns the unit's full name, such as "metre". Compound and prefixed units synthesise a name where they can and return the empty string otherwise.

func (Unit) Offset

func (u Unit) Offset() *big.Rat

Offset returns a copy of the exact rational offset to the coherent SI unit, which is zero for every linear unit. The returned value is a fresh big.Rat the caller may modify freely.

func (Unit) String

func (u Unit) String() string

String returns the unit's symbol, so a Unit may be printed directly.

func (Unit) Symbol

func (u Unit) Symbol() string

Symbol returns the unit's symbol, such as "m" or "kg·m/s²". The symbol of the dimensionless unit one is the empty string.

A compound symbol always re-parses to the same unit, which is why a denominator of several factors is parenthesised: "J/(kg·K)", not "J/kg·K".

Jump to

Keyboard shortcuts

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