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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ It uses phantom types to model dimensions like length, area, mass, volume, flow,
- JSON marshaling based on the scalar base-unit value
- JSON scalar output rounded to 15 significant digits, plus `MarshalJSONIn` for a chosen unit
- Text marshaling and `database/sql` interop helpers
- Allocation-conscious `AppendText` helpers and base-unit `ParseBase`
- `time.Duration` interop with `FromDuration` and `ToDuration`
- Support for affine temperature conversions
- Separate temperature-delta quantities for safe temperature differences
Expand Down Expand Up @@ -53,10 +54,10 @@ Output:
## Core API

```go
type Quantity[D any] struct
type Quantity[D Dimension] struct

func New[D any, U Unit[D]](v float64, u U) Quantity[D]
func From[D any](v float64, u BuiltinUnit[D]) Quantity[D]
func New[D Dimension, U Unit[D]](v float64, u U) Quantity[D]
func From[D Dimension](v float64, u BuiltinUnit[D]) Quantity[D]

func (q Quantity[D]) To(u Unit[D]) float64
func (q Quantity[D]) Value(u Unit[D]) float64
Expand All @@ -65,6 +66,9 @@ func (q Quantity[D]) MarshalJSON() ([]byte, error)
func (q *Quantity[D]) UnmarshalJSON(data []byte) error
func (q Quantity[D]) MarshalText() ([]byte, error)
func (q *Quantity[D]) UnmarshalText(text []byte) error
func (q Quantity[D]) AppendText(dst []byte) ([]byte, error)
func (q Quantity[D]) AppendTextIn(dst []byte, u Unit[D]) []byte
func ParseBase[D Dimension](text string) (Quantity[D], error)
func (q Quantity[D]) Add(other Quantity[D]) Quantity[D]
func (q Quantity[D]) Sub(other Quantity[D]) Quantity[D]
func (q Quantity[D]) Mul(scalar float64) Quantity[D]
Expand Down Expand Up @@ -128,6 +132,12 @@ text, _ := quant.Pounds(10).MarshalText()
fmt.Println(string(text)) // 4.5359237
```

For allocation-conscious text output, use `AppendText`; `ParseBase` parses a
base-unit scalar without introducing unit-name parsing into the core package.
The existing unit-aware `Format(unit)` method prevents implementing
`fmt.Formatter` without an API-breaking rename, so formatting in a selected
unit remains explicit.

For `database/sql`, use the adapter helper:

```go
Expand Down
25 changes: 24 additions & 1 deletion quantity.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ func (q *Quantity[D]) UnmarshalJSON(data []byte) error {

// MarshalText encodes the quantity as a base-unit scalar value.
func (q Quantity[D]) MarshalText() ([]byte, error) {
return []byte(formatScalar(q.value)), nil
return q.AppendText(nil)
}

// AppendText appends the base-unit scalar representation to dst. It mirrors
// MarshalText without requiring an intermediate allocation.
func (q Quantity[D]) AppendText(dst []byte) ([]byte, error) {
return strconv.AppendFloat(dst, q.value, 'g', 15, 64), nil
}

// AppendTextIn appends the scalar representation in the requested unit to dst.
// The unit symbol is intentionally omitted, matching Format's scalar portion.
func (q Quantity[D]) AppendTextIn(dst []byte, u Unit[D]) []byte {
return strconv.AppendFloat(dst, q.To(u), 'g', 12, 64)
}

// UnmarshalText decodes a base-unit scalar value into the quantity.
Expand All @@ -70,6 +82,17 @@ func (q *Quantity[D]) UnmarshalText(text []byte) error {
return nil
}

// ParseBase parses a base-unit scalar into a quantity. It does not parse unit
// names or symbols; use New when the input is expressed in another unit.
func ParseBase[D Dimension](text string) (Quantity[D], error) {
v, err := strconv.ParseFloat(text, 64)
if err != nil {
return Quantity[D]{}, err
}

return Quantity[D]{value: v}, nil
}

// Format formats the quantity in the provided unit.
func (q Quantity[D]) Format(u Unit[D]) string {
value := strconv.FormatFloat(q.To(u), 'g', 12, 64)
Expand Down
4 changes: 2 additions & 2 deletions quantity_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ func BenchmarkAdd(b *testing.B) {
b.ResetTimer()

for i := 0; i < b.N; i++ {
result = result.Add(c)
benchmarkQuantity = result.Add(c)
result = benchmarkQuantity
}
benchmarkQuantity = result
}

func BenchmarkDiv(b *testing.B) {
Expand Down
25 changes: 25 additions & 0 deletions quantity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package quant_test

import (
"database/sql/driver"
"encoding"
"encoding/json"
"fmt"
"math"
Expand Down Expand Up @@ -505,6 +506,26 @@ func TestTextMarshalingUsesBaseUnit(t *testing.T) {
}
}

func TestTextAppendAndParseBase(t *testing.T) {
data, err := quant.Pounds(10).AppendText([]byte("value="))
if err != nil || string(data) != "value=4.5359237" {
t.Fatalf("AppendText mismatch: data=%q err=%v", data, err)
}

data = quant.Pounds(10).AppendTextIn(nil, quant.Pound)
if string(data) != "10" {
t.Fatalf("AppendTextIn mismatch: got %q", data)
}

q, err := quant.ParseBase[quant.Mass]("4.5359237")
if err != nil {
t.Fatalf("ParseBase error: %v", err)
}
if got, want := q.To(quant.Kilogram), 4.5359237; !almostEqual(got, want) {
t.Fatalf("ParseBase mismatch: got %.10f want %.10f", got, want)
}
}

func TestSQLQuantityInterop(t *testing.T) {
value, err := quant.SQL(quant.Kilograms(5)).Value()
if err != nil {
Expand Down Expand Up @@ -539,6 +560,10 @@ func TestSQLQuantityImplementsValuer(t *testing.T) {
var _ driver.Valuer = quant.SQL(quant.Kilograms(1))
}

func TestQuantityImplementsTextAppender(t *testing.T) {
var _ encoding.TextAppender = quant.Pounds(1)
}

func Example_massConversion() {
kg := quant.Amount[quant.Mass](10).From(quant.Pound).To(quant.Kilogram)
fmt.Printf("%.6f\n", kg)
Expand Down
Loading