Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,43 @@ fmt.Println(poh) // 11

The package favors correctness and simplicity first, while keeping the implementation small and allocation-free in normal use.

## Optional Parsing

The core package does not parse unit names. Import `parse` when string input is
part of an application's boundary:

```go
length, err := parse.ParseLength("5 km")
mass, err := parse.ParseMass("10lb")
_ = length.To(quant.Meter) // 5000
_ = mass.To(quant.Kilogram) // 4.5359237
```

Custom dimensions can provide their own accepted symbols with `parse.UnitSet`:

```go
type Widget struct{}
type Box struct{}

func (Box) ToBase(v float64) float64 { return v * 12 }
func (Box) FromBase(v float64) float64 { return v / 12 }
func (Box) Dimension() Widget { return Widget{} }

widgets, err := parse.Parse("2 box", parse.UnitSet[Widget]{"box": Box{}})
```

## CODATA Constants

`codata` contains selected 2022 CODATA recommended values from NIST. Values
are plain `float64` constants with their SI units stated in the package docs;
uncertainties and correlations are intentionally left to the authoritative
CODATA tables.

```go
speedOfLight := codata.SpeedOfLight // m/s
elementaryCharge := codata.ElementaryCharge // C
```

## Development

Run tests with:
Expand Down
34 changes: 34 additions & 0 deletions codata/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Package codata contains selected 2022 CODATA recommended constants.
//
// Values are exposed as float64 constants in the SI units documented by NIST.
// Uncertainties are not represented; consult the source when uncertainty or
// correlations matter. This package intentionally does not invent Quantity
// dimensions for compound units that are not modeled by quant.
package codata

const (
// SpeedOfLight is the speed of light in vacuum, in m/s (exact).
SpeedOfLight = 299792458.0
// PlanckConstant is Planck's constant, in J s (exact).
PlanckConstant = 6.62607015e-34
// ReducedPlanckConstant is the reduced Planck constant, in J s.
ReducedPlanckConstant = 1.054571817e-34
// ElementaryCharge is the elementary charge, in C (exact).
ElementaryCharge = 1.602176634e-19
// BoltzmannConstant is the Boltzmann constant, in J/K (exact).
BoltzmannConstant = 1.380649e-23
// AvogadroConstant is the Avogadro constant, in 1/mol (exact).
AvogadroConstant = 6.02214076e23
// GravitationalConstant is Newton's constant of gravitation, in m^3 kg^-1 s^-2.
GravitationalConstant = 6.67430e-11
// FineStructureConstant is the fine-structure constant, dimensionless.
FineStructureConstant = 7.2973525643e-3
// ElectronMass is the electron mass, in kg.
ElectronMass = 9.1093837139e-31
// ProtonMass is the proton mass, in kg.
ProtonMass = 1.67262192595e-27
// MolarGasConstant is the molar gas constant, in J/(mol K).
MolarGasConstant = 8.314462618
// VacuumPermittivity is the vacuum electric permittivity, in F/m.
VacuumPermittivity = 8.8541878188e-12
)
19 changes: 19 additions & 0 deletions codata/constants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package codata_test

import (
"testing"

"github.com/nodivbyzero/quant/codata"
)

func TestDefiningConstants(t *testing.T) {
if codata.SpeedOfLight != 299792458 {
t.Fatalf("speed of light changed: got %g", codata.SpeedOfLight)
}
if codata.ElementaryCharge != 1.602176634e-19 {
t.Fatalf("elementary charge changed: got %g", codata.ElementaryCharge)
}
if codata.AvogadroConstant != 6.02214076e23 {
t.Fatalf("Avogadro constant changed: got %g", codata.AvogadroConstant)
}
}
122 changes: 122 additions & 0 deletions parse/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Package parse provides optional string parsing for quant quantities.
//
// The core quant package deliberately has no unit-name parser. Callers opt in
// here and provide the unit registry they want to accept.
package parse

import (
"fmt"
"strconv"
"strings"

"github.com/nodivbyzero/quant"
)

// UnitSet maps accepted symbols to units for one dimension.
type UnitSet[D quant.Dimension] map[string]quant.Unit[D]

// Parse parses a scalar followed by an optional unit symbol. Both "5 km" and
// "5km" are accepted. A missing symbol uses the empty-string entry in units.
func Parse[D quant.Dimension](input string, units UnitSet[D]) (quant.Quantity[D], error) {
value, symbol, err := split(input)
if err != nil {
return quant.Quantity[D]{}, err
}

u, ok := units[symbol]
if !ok {
return quant.Quantity[D]{}, fmt.Errorf("parse: unknown unit %q", symbol)
}

return quant.New[D](value, u), nil
}

// ParseLength parses a length using the built-in length symbols in LengthUnits.
func ParseLength(input string) (quant.Quantity[quant.Length], error) {
return Parse(input, LengthUnits)
}

// ParseMass parses a mass using the built-in mass symbols in MassUnits.
func ParseMass(input string) (quant.Quantity[quant.Mass], error) {
return Parse(input, MassUnits)
}

// ParseTime parses a time using the built-in time symbols in TimeUnits.
func ParseTime(input string) (quant.Quantity[quant.Time], error) {
return Parse(input, TimeUnits)
}

// ParseTemperature parses a temperature using the built-in temperature symbols
// in TemperatureUnits.
func ParseTemperature(input string) (quant.Quantity[quant.Temperature], error) {
return Parse(input, TemperatureUnits)
}

var LengthUnits = UnitSet[quant.Length]{
"": quant.Meter, "m": quant.Meter, "km": quant.Kilometer, "cm": quant.Centimeter,
"mm": quant.Millimeter, "mi": quant.Mile, "ft": quant.Foot, "in": quant.Inch,
}

var MassUnits = UnitSet[quant.Mass]{
"": quant.Kilogram, "kg": quant.Kilogram, "g": quant.Gram, "mg": quant.Milligram,
"lb": quant.Pound, "oz": quant.Ounce,
}

var TimeUnits = UnitSet[quant.Time]{
"": quant.Second, "s": quant.Second, "ms": quant.Millisecond, "min": quant.Minute,
"h": quant.Hour, "d": quant.Day,
}

var TemperatureUnits = UnitSet[quant.Temperature]{
"": quant.Kelvin, "K": quant.Kelvin, "C": quant.Celsius, "F": quant.Fahrenheit,
}

func split(input string) (float64, string, error) {
s := strings.TrimSpace(input)
if s == "" {
return 0, "", fmt.Errorf("parse: empty input")
}

end := 0
if s[0] == '+' || s[0] == '-' {
end++
}
digits := false
for end < len(s) && s[end] >= '0' && s[end] <= '9' {
end++
digits = true
}
if end < len(s) && s[end] == '.' {
end++
for end < len(s) && s[end] >= '0' && s[end] <= '9' {
end++
digits = true
}
}
if !digits {
return 0, "", fmt.Errorf("parse: invalid number in %q", input)
}
if end < len(s) && (s[end] == 'e' || s[end] == 'E') {
end++
if end < len(s) && (s[end] == '+' || s[end] == '-') {
end++
}
expStart := end
for end < len(s) && s[end] >= '0' && s[end] <= '9' {
end++
}
if expStart == end {
return 0, "", fmt.Errorf("parse: invalid exponent in %q", input)
}
}

value, err := strconv.ParseFloat(s[:end], 64)
if err != nil {
return 0, "", fmt.Errorf("parse: %w", err)
}
symbol := strings.TrimSpace(s[end:])
if strings.ContainsAny(symbol, " \t\r\n") {
return 0, "", fmt.Errorf("parse: invalid unit %q", symbol)
}
return value, symbol, nil
}
43 changes: 43 additions & 0 deletions parse/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package parse_test

import (
"math"
"testing"

"github.com/nodivbyzero/quant"
"github.com/nodivbyzero/quant/parse"
)

func TestBuiltInParsers(t *testing.T) {
length, err := parse.ParseLength("5 km")
if err != nil || length.To(quant.Meter) != 5000 {
t.Fatalf("length parse: value=%v err=%v", length.To(quant.Meter), err)
}

mass, err := parse.ParseMass("10lb")
if err != nil || math.Abs(mass.To(quant.Kilogram)-4.5359237) > 1e-12 {
t.Fatalf("mass parse: value=%v err=%v", mass.To(quant.Kilogram), err)
}

temperature, err := parse.ParseTemperature("32 F")
if err != nil || math.Abs(temperature.To(quant.Kelvin)-273.15) > 1e-12 {
t.Fatalf("temperature parse: value=%v err=%v", temperature.To(quant.Kelvin), err)
}
}

func TestCustomRegistry(t *testing.T) {
units := parse.UnitSet[Widget]{
"box": BoxUnit{},
}
q, err := parse.Parse("2 box", units)
if err != nil || q.To(BoxUnit{}) != 2 {
t.Fatalf("custom parse: value=%v err=%v", q.To(BoxUnit{}), err)
}
}

type Widget struct{}
type BoxUnit struct{}

func (BoxUnit) ToBase(v float64) float64 { return v * 12 }
func (BoxUnit) FromBase(v float64) float64 { return v / 12 }
func (BoxUnit) Dimension() Widget { return Widget{} }
Loading