diff --git a/README.md b/README.md index 206da18..8fcd82d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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] @@ -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 diff --git a/quantity.go b/quantity.go index 0ea2b0a..885332b 100644 --- a/quantity.go +++ b/quantity.go @@ -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. @@ -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) diff --git a/quantity_bench_test.go b/quantity_bench_test.go index 1a077ef..5d5b9d4 100644 --- a/quantity_bench_test.go +++ b/quantity_bench_test.go @@ -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) { diff --git a/quantity_test.go b/quantity_test.go index bd35e12..7460d3f 100644 --- a/quantity_test.go +++ b/quantity_test.go @@ -2,6 +2,7 @@ package quant_test import ( "database/sql/driver" + "encoding" "encoding/json" "fmt" "math" @@ -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 { @@ -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)