gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/unit/unittype.go (about) 1 // Copyright ©2013 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package unit 6 7 import ( 8 "bytes" 9 "cmp" 10 "fmt" 11 "slices" 12 "sync" 13 "unicode/utf8" 14 ) 15 16 // Uniter is a type that can be converted to a Unit. 17 type Uniter interface { 18 Unit() *Unit 19 } 20 21 // Dimension is a type representing an SI base dimension or a distinct 22 // orthogonal dimension. Non-SI dimensions can be created using the NewDimension 23 // function, typically within an init function. 24 type Dimension int 25 26 // NewDimension creates a new orthogonal dimension with the given symbol, and 27 // returns the value of that dimension. The input symbol must not overlap with 28 // any of the any of the SI base units or other symbols of common use in SI ("kg", 29 // "J", etc.), and must not overlap with any other dimensions created by calls 30 // to NewDimension. The SymbolExists function can check if the symbol exists. 31 // NewDimension will panic if the input symbol matches an existing symbol. 32 // 33 // NewDimension should only be called for unit types that are actually orthogonal 34 // to the base dimensions defined in this package. See the package-level 35 // documentation for further explanation. 36 func NewDimension(symbol string) Dimension { 37 defer mu.Unlock() 38 mu.Lock() 39 _, ok := dimensions[symbol] 40 if ok { 41 panic("unit: dimension string \"" + symbol + "\" already used") 42 } 43 d := Dimension(len(symbols)) 44 symbols = append(symbols, symbol) 45 dimensions[symbol] = d 46 return d 47 } 48 49 // String returns the string for the dimension. 50 func (d Dimension) String() string { 51 if d == reserved { 52 return "reserved" 53 } 54 defer mu.RUnlock() 55 mu.RLock() 56 if int(d) < len(symbols) { 57 return symbols[d] 58 } 59 panic("unit: illegal dimension") 60 } 61 62 // SymbolExists returns whether the given symbol is already in use. 63 func SymbolExists(symbol string) bool { 64 mu.RLock() 65 _, ok := dimensions[symbol] 66 mu.RUnlock() 67 return ok 68 } 69 70 const ( 71 // SI Base Units 72 reserved Dimension = iota 73 CurrentDim 74 LengthDim 75 LuminousIntensityDim 76 MassDim 77 MoleDim 78 TemperatureDim 79 TimeDim 80 // Other common SI Dimensions 81 AngleDim // e.g. radians 82 ) 83 84 var ( 85 // mu protects symbols and dimensions for concurrent use. 86 mu sync.RWMutex 87 symbols = []string{ 88 CurrentDim: "A", 89 LengthDim: "m", 90 LuminousIntensityDim: "cd", 91 MassDim: "kg", 92 MoleDim: "mol", 93 TemperatureDim: "K", 94 TimeDim: "s", 95 AngleDim: "rad", 96 } 97 98 // dimensions guarantees there aren't two identical symbols 99 // SI symbol list from http://lamar.colostate.edu/~hillger/basic.htm 100 dimensions = map[string]Dimension{ 101 "A": CurrentDim, 102 "m": LengthDim, 103 "cd": LuminousIntensityDim, 104 "kg": MassDim, 105 "mol": MoleDim, 106 "K": TemperatureDim, 107 "s": TimeDim, 108 "rad": AngleDim, 109 110 // Reserve common SI symbols 111 // prefixes 112 "Y": reserved, 113 "Z": reserved, 114 "E": reserved, 115 "P": reserved, 116 "T": reserved, 117 "G": reserved, 118 "M": reserved, 119 "k": reserved, 120 "h": reserved, 121 "da": reserved, 122 "d": reserved, 123 "c": reserved, 124 "μ": reserved, 125 "n": reserved, 126 "p": reserved, 127 "f": reserved, 128 "a": reserved, 129 "z": reserved, 130 "y": reserved, 131 // SI Derived units with special symbols 132 "sr": reserved, 133 "F": reserved, 134 "C": reserved, 135 "S": reserved, 136 "H": reserved, 137 "V": reserved, 138 "Ω": reserved, 139 "J": reserved, 140 "N": reserved, 141 "Hz": reserved, 142 "lx": reserved, 143 "lm": reserved, 144 "Wb": reserved, 145 "W": reserved, 146 "Pa": reserved, 147 "Bq": reserved, 148 "Gy": reserved, 149 "Sv": reserved, 150 "kat": reserved, 151 // Units in use with SI 152 "ha": reserved, 153 "L": reserved, 154 "l": reserved, 155 // Units in Use Temporarily with SI 156 "bar": reserved, 157 "b": reserved, 158 "Ci": reserved, 159 "R": reserved, 160 "rd": reserved, 161 "rem": reserved, 162 } 163 ) 164 165 // Dimensions represent the dimensionality of the unit in powers 166 // of that dimension. If a key is not present, the power of that 167 // dimension is zero. Dimensions is used in conjunction with New. 168 type Dimensions map[Dimension]int 169 170 func (d Dimensions) clone() Dimensions { 171 if d == nil { 172 return nil 173 } 174 c := make(Dimensions, len(d)) 175 for dim, pow := range d { 176 if pow != 0 { 177 c[dim] = pow 178 } 179 } 180 return c 181 } 182 183 // matches reports whether the dimensions of d and o match. Zero power 184 // dimensions in d an o must be removed, otherwise matches may incorrectly 185 // report a mismatch. 186 func (d Dimensions) matches(o Dimensions) bool { 187 if len(d) != len(o) { 188 return false 189 } 190 for dim, pow := range d { 191 if o[dim] != pow { 192 return false 193 } 194 } 195 return true 196 } 197 198 func (d Dimensions) String() string { 199 // Map iterates randomly, but print should be in a fixed order. Can't use 200 // dimension number, because for user-defined dimension that number may 201 // not be fixed from run to run. 202 atoms := make([]atom, 0, len(d)) 203 for dimension, power := range d { 204 if power != 0 { 205 atoms = append(atoms, atom{dimension, power}) 206 } 207 } 208 slices.SortFunc(atoms, func(a, b atom) int { 209 // Order first by positive powers, then by name. 210 if a.pow*b.pow < 0 { 211 return cmp.Compare(0, a.pow) 212 } 213 return cmp.Compare(a.String(), b.String()) 214 }) 215 var b bytes.Buffer 216 for i, a := range atoms { 217 if i > 0 { 218 b.WriteByte(' ') 219 } 220 fmt.Fprintf(&b, "%s", a.Dimension) 221 if a.pow != 1 { 222 fmt.Fprintf(&b, "^%d", a.pow) 223 } 224 } 225 226 return b.String() 227 } 228 229 type atom struct { 230 Dimension 231 pow int 232 } 233 234 // Unit represents a dimensional value. The dimensions will typically be in SI 235 // units, but can also include dimensions created with NewDimension. The Unit type 236 // is most useful for ensuring dimensional consistency when manipulating types 237 // with different units, for example, by multiplying an acceleration with a 238 // mass to get a force. See the package documentation for further explanation. 239 type Unit struct { 240 dimensions Dimensions 241 value float64 242 } 243 244 // New creates a new variable of type Unit which has the value and dimensions 245 // specified by the inputs. The built-in dimensions are always in SI units 246 // (metres, kilograms, etc.). 247 func New(value float64, d Dimensions) *Unit { 248 return &Unit{ 249 dimensions: d.clone(), 250 value: value, 251 } 252 } 253 254 // DimensionsMatch checks if the dimensions of two Uniters are the same. 255 func DimensionsMatch(a, b Uniter) bool { 256 return a.Unit().dimensions.matches(b.Unit().dimensions) 257 } 258 259 // Dimensions returns a copy of the dimensions of the unit. 260 func (u *Unit) Dimensions() Dimensions { 261 return u.dimensions.clone() 262 } 263 264 // Add adds the function argument to the receiver. Panics if the units of 265 // the receiver and the argument don't match. 266 func (u *Unit) Add(uniter Uniter) *Unit { 267 a := uniter.Unit() 268 if !DimensionsMatch(u, a) { 269 panic("unit: mismatched dimensions in addition") 270 } 271 u.value += a.value 272 return u 273 } 274 275 // Unit implements the Uniter interface, returning the receiver. If a 276 // copy of the receiver is required, use the Copy method. 277 func (u *Unit) Unit() *Unit { 278 return u 279 } 280 281 // Copy returns a copy of the Unit that can be mutated without the change 282 // being reflected in the original value. 283 func (u *Unit) Copy() *Unit { 284 return &Unit{ 285 dimensions: u.dimensions.clone(), 286 value: u.value, 287 } 288 } 289 290 // Mul multiply the receiver by the input changing the dimensions 291 // of the receiver as appropriate. The input is not changed. 292 func (u *Unit) Mul(uniter Uniter) *Unit { 293 a := uniter.Unit() 294 for key, val := range a.dimensions { 295 if d := u.dimensions[key]; d == -val { 296 delete(u.dimensions, key) 297 } else { 298 u.dimensions[key] = d + val 299 } 300 } 301 u.value *= a.value 302 return u 303 } 304 305 // Div divides the receiver by the argument changing the 306 // dimensions of the receiver as appropriate. 307 func (u *Unit) Div(uniter Uniter) *Unit { 308 a := uniter.Unit() 309 u.value /= a.value 310 for key, val := range a.dimensions { 311 if d := u.dimensions[key]; d == val { 312 delete(u.dimensions, key) 313 } else { 314 u.dimensions[key] = d - val 315 } 316 } 317 return u 318 } 319 320 // Value return the raw value of the unit as a float64. Use of this 321 // method is, in general, not recommended, though it can be useful 322 // for printing. Instead, the From method of a specific dimension 323 // should be used to guarantee dimension consistency. 324 func (u *Unit) Value() float64 { 325 return u.value 326 } 327 328 // SetValue sets the value of the unit. 329 func (u *Unit) SetValue(v float64) { 330 u.value = v 331 } 332 333 // Format makes Unit satisfy the fmt.Formatter interface. The unit is formatted 334 // with dimensions appended. If the power of the dimension is not zero or one, 335 // symbol^power is appended, if the power is one, just the symbol is appended 336 // and if the power is zero, nothing is appended. Dimensions are appended 337 // in order by symbol name with positive powers ahead of negative powers. 338 func (u *Unit) Format(fs fmt.State, c rune) { 339 if u == nil { 340 fmt.Fprint(fs, "<nil>") 341 } 342 switch c { 343 case 'v': 344 if fs.Flag('#') { 345 fmt.Fprintf(fs, "&%#v", *u) 346 return 347 } 348 fallthrough 349 case 'e', 'E', 'f', 'F', 'g', 'G': 350 p, pOk := fs.Precision() 351 w, wOk := fs.Width() 352 units := u.dimensions.String() 353 switch { 354 case pOk && wOk: 355 fmt.Fprintf(fs, "%*.*"+string(c), pos(w-utf8.RuneCount([]byte(units))-1), p, u.value) 356 case pOk: 357 fmt.Fprintf(fs, "%.*"+string(c), p, u.value) 358 case wOk: 359 fmt.Fprintf(fs, "%*"+string(c), pos(w-utf8.RuneCount([]byte(units))-1), u.value) 360 default: 361 fmt.Fprintf(fs, "%"+string(c), u.value) 362 } 363 fmt.Fprintf(fs, " %s", units) 364 default: 365 fmt.Fprintf(fs, "%%!%c(*Unit=%g)", c, u) 366 } 367 } 368 369 func pos(a int) int { 370 if a < 0 { 371 return 0 372 } 373 return a 374 }