// Copyright ©2013 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package unit import ( "bytes" "cmp" "fmt" "slices" "sync" "unicode/utf8" ) // Uniter is a type that can be converted to a Unit. type Uniter interface { Unit() *Unit } // Dimension is a type representing an SI base dimension or a distinct // orthogonal dimension. Non-SI dimensions can be created using the NewDimension // function, typically within an init function. type Dimension int // NewDimension creates a new orthogonal dimension with the given symbol, and // returns the value of that dimension. The input symbol must not overlap with // any of the any of the SI base units or other symbols of common use in SI ("kg", // "J", etc.), and must not overlap with any other dimensions created by calls // to NewDimension. The SymbolExists function can check if the symbol exists. // NewDimension will panic if the input symbol matches an existing symbol. // // NewDimension should only be called for unit types that are actually orthogonal // to the base dimensions defined in this package. See the package-level // documentation for further explanation. func NewDimension(symbol string) Dimension { defer mu.Unlock() mu.Lock() _, ok := dimensions[symbol] if ok { panic("unit: dimension string \"" + symbol + "\" already used") } d := Dimension(len(symbols)) symbols = append(symbols, symbol) dimensions[symbol] = d return d } // String returns the string for the dimension. func (d Dimension) String() string { if d == reserved { return "reserved" } defer mu.RUnlock() mu.RLock() if int(d) < len(symbols) { return symbols[d] } panic("unit: illegal dimension") } // SymbolExists returns whether the given symbol is already in use. func SymbolExists(symbol string) bool { mu.RLock() _, ok := dimensions[symbol] mu.RUnlock() return ok } const ( // SI Base Units reserved Dimension = iota CurrentDim LengthDim LuminousIntensityDim MassDim MoleDim TemperatureDim TimeDim // Other common SI Dimensions AngleDim // e.g. radians ) var ( // mu protects symbols and dimensions for concurrent use. mu sync.RWMutex symbols = []string{ CurrentDim: "A", LengthDim: "m", LuminousIntensityDim: "cd", MassDim: "kg", MoleDim: "mol", TemperatureDim: "K", TimeDim: "s", AngleDim: "rad", } // dimensions guarantees there aren't two identical symbols // SI symbol list from http://lamar.colostate.edu/~hillger/basic.htm dimensions = map[string]Dimension{ "A": CurrentDim, "m": LengthDim, "cd": LuminousIntensityDim, "kg": MassDim, "mol": MoleDim, "K": TemperatureDim, "s": TimeDim, "rad": AngleDim, // Reserve common SI symbols // prefixes "Y": reserved, "Z": reserved, "E": reserved, "P": reserved, "T": reserved, "G": reserved, "M": reserved, "k": reserved, "h": reserved, "da": reserved, "d": reserved, "c": reserved, "μ": reserved, "n": reserved, "p": reserved, "f": reserved, "a": reserved, "z": reserved, "y": reserved, // SI Derived units with special symbols "sr": reserved, "F": reserved, "C": reserved, "S": reserved, "H": reserved, "V": reserved, "Ω": reserved, "J": reserved, "N": reserved, "Hz": reserved, "lx": reserved, "lm": reserved, "Wb": reserved, "W": reserved, "Pa": reserved, "Bq": reserved, "Gy": reserved, "Sv": reserved, "kat": reserved, // Units in use with SI "ha": reserved, "L": reserved, "l": reserved, // Units in Use Temporarily with SI "bar": reserved, "b": reserved, "Ci": reserved, "R": reserved, "rd": reserved, "rem": reserved, } ) // Dimensions represent the dimensionality of the unit in powers // of that dimension. If a key is not present, the power of that // dimension is zero. Dimensions is used in conjunction with New. type Dimensions map[Dimension]int func (d Dimensions) clone() Dimensions { if d == nil { return nil } c := make(Dimensions, len(d)) for dim, pow := range d { if pow != 0 { c[dim] = pow } } return c } // matches reports whether the dimensions of d and o match. Zero power // dimensions in d an o must be removed, otherwise matches may incorrectly // report a mismatch. func (d Dimensions) matches(o Dimensions) bool { if len(d) != len(o) { return false } for dim, pow := range d { if o[dim] != pow { return false } } return true } func (d Dimensions) String() string { // Map iterates randomly, but print should be in a fixed order. Can't use // dimension number, because for user-defined dimension that number may // not be fixed from run to run. atoms := make([]atom, 0, len(d)) for dimension, power := range d { if power != 0 { atoms = append(atoms, atom{dimension, power}) } } slices.SortFunc(atoms, func(a, b atom) int { // Order first by positive powers, then by name. if a.pow*b.pow < 0 { return cmp.Compare(0, a.pow) } return cmp.Compare(a.String(), b.String()) }) var b bytes.Buffer for i, a := range atoms { if i > 0 { b.WriteByte(' ') } fmt.Fprintf(&b, "%s", a.Dimension) if a.pow != 1 { fmt.Fprintf(&b, "^%d", a.pow) } } return b.String() } type atom struct { Dimension pow int } // Unit represents a dimensional value. The dimensions will typically be in SI // units, but can also include dimensions created with NewDimension. The Unit type // is most useful for ensuring dimensional consistency when manipulating types // with different units, for example, by multiplying an acceleration with a // mass to get a force. See the package documentation for further explanation. type Unit struct { dimensions Dimensions value float64 } // New creates a new variable of type Unit which has the value and dimensions // specified by the inputs. The built-in dimensions are always in SI units // (metres, kilograms, etc.). func New(value float64, d Dimensions) *Unit { return &Unit{ dimensions: d.clone(), value: value, } } // DimensionsMatch checks if the dimensions of two Uniters are the same. func DimensionsMatch(a, b Uniter) bool { return a.Unit().dimensions.matches(b.Unit().dimensions) } // Dimensions returns a copy of the dimensions of the unit. func (u *Unit) Dimensions() Dimensions { return u.dimensions.clone() } // Add adds the function argument to the receiver. Panics if the units of // the receiver and the argument don't match. func (u *Unit) Add(uniter Uniter) *Unit { a := uniter.Unit() if !DimensionsMatch(u, a) { panic("unit: mismatched dimensions in addition") } u.value += a.value return u } // Unit implements the Uniter interface, returning the receiver. If a // copy of the receiver is required, use the Copy method. func (u *Unit) Unit() *Unit { return u } // Copy returns a copy of the Unit that can be mutated without the change // being reflected in the original value. func (u *Unit) Copy() *Unit { return &Unit{ dimensions: u.dimensions.clone(), value: u.value, } } // Mul multiply the receiver by the input changing the dimensions // of the receiver as appropriate. The input is not changed. func (u *Unit) Mul(uniter Uniter) *Unit { a := uniter.Unit() for key, val := range a.dimensions { if d := u.dimensions[key]; d == -val { delete(u.dimensions, key) } else { u.dimensions[key] = d + val } } u.value *= a.value return u } // Div divides the receiver by the argument changing the // dimensions of the receiver as appropriate. func (u *Unit) Div(uniter Uniter) *Unit { a := uniter.Unit() u.value /= a.value for key, val := range a.dimensions { if d := u.dimensions[key]; d == val { delete(u.dimensions, key) } else { u.dimensions[key] = d - val } } return u } // Value return the raw value of the unit as a float64. Use of this // method is, in general, not recommended, though it can be useful // for printing. Instead, the From method of a specific dimension // should be used to guarantee dimension consistency. func (u *Unit) Value() float64 { return u.value } // SetValue sets the value of the unit. func (u *Unit) SetValue(v float64) { u.value = v } // Format makes Unit satisfy the fmt.Formatter interface. The unit is formatted // with dimensions appended. If the power of the dimension is not zero or one, // symbol^power is appended, if the power is one, just the symbol is appended // and if the power is zero, nothing is appended. Dimensions are appended // in order by symbol name with positive powers ahead of negative powers. func (u *Unit) Format(fs fmt.State, c rune) { if u == nil { fmt.Fprint(fs, "") } switch c { case 'v': if fs.Flag('#') { fmt.Fprintf(fs, "&%#v", *u) return } fallthrough case 'e', 'E', 'f', 'F', 'g', 'G': p, pOk := fs.Precision() w, wOk := fs.Width() units := u.dimensions.String() switch { case pOk && wOk: fmt.Fprintf(fs, "%*.*"+string(c), pos(w-utf8.RuneCount([]byte(units))-1), p, u.value) case pOk: fmt.Fprintf(fs, "%.*"+string(c), p, u.value) case wOk: fmt.Fprintf(fs, "%*"+string(c), pos(w-utf8.RuneCount([]byte(units))-1), u.value) default: fmt.Fprintf(fs, "%"+string(c), u.value) } fmt.Fprintf(fs, " %s", units) default: fmt.Fprintf(fs, "%%!%c(*Unit=%g)", c, u) } } func pos(a int) int { if a < 0 { return 0 } return a }