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  }