From 17003ea83a862b0bddff13c4e5f0a66d1fda25dd Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 22 May 2026 12:52:26 +0300 Subject: fix(rpn): treat Cool values in result metric space during absorption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Cool (Universal) absorbs into a metric, treat the Cool value as units of the result metric, not base units. Previously: 5 100Mbps [+] → ~110 Mbps (5 base bps is negligible) Now: 5 100Mbps [+] → 105 Mbps (5 treated as 5 Mbps) This makes Cool absorption intuitive: a bare number combined with a metric-aware value is interpreted in the same unit space. Changes: - convertToBase now takes resultMetric param for Cool absorption - All callers compute result metric first, then pass to convertToBase - Updated tests to match new behavior --- internal/rpn/metric_test.go | 13 +++++----- internal/rpn/operations_arithmetic.go | 48 +++++++++++++++++------------------ internal/rpn/operations_compare.go | 5 ++-- internal/rpn/operations_hyper.go | 25 ++++++++++-------- internal/rpn/operations_hyper_test.go | 18 ++++++------- internal/rpn/operations_metric.go | 13 +++++++--- 6 files changed, 66 insertions(+), 56 deletions(-) diff --git a/internal/rpn/metric_test.go b/internal/rpn/metric_test.go index aeb69d4..280a803 100644 --- a/internal/rpn/metric_test.go +++ b/internal/rpn/metric_test.go @@ -866,7 +866,7 @@ func TestMetricAwareArithmetic(t *testing.T) { wantErr bool }{ {"100Mbps 50Mbps +", 150, "Mbps", false}, - {"1 100hr +", 100, "hr", false}, + {"1 100hr +", 101, "hr", false}, {"3 4 +", 7, "Cool", false}, {"100Mbps 1hr *", 360000000000, "bits", false}, {"1000000000bits 1s /", 1000000000, "bps", false}, @@ -942,7 +942,7 @@ func TestMetricOperationsUnit(t *testing.T) { s.Push(NewFloatWithMetric(100, mbps)) }, op: func(o *Operations, s *Stack) error { return o.Add(s) }, - wantVal: 100.000005, + wantVal: 105, wantMet: "Mbps", }, { @@ -1066,16 +1066,15 @@ func TestConvertCoolAbsorbing(t *testing.T) { vars := NewVariables() rpn := NewRPN(vars) - // Cool to metric: 100 @GB convert → 100 * 1 / (8e9) = 1.25e-8 GB + // Cool to metric: 100 @GB convert → 100 GB + // With Cool absorption, 100 is treated as 100 in GB's space result, err := rpn.ParseAndEvaluate("100 @GB convert") if err != nil { t.Fatalf("unexpected error: %v", err) } resultVal, _ := strconv.ParseFloat(result, 64) - expected := 100.0 / 8e9 - tolerance := expected * 0.001 // 0.1% relative tolerance - if resultVal < expected-tolerance || resultVal > expected+tolerance { - t.Errorf("result = %g, want %g (relative tolerance %g)", resultVal, expected, tolerance) + if resultVal != 100 { + t.Errorf("result = %g, want 100", resultVal) } stack := rpn.GetCurrentStack() if len(stack) > 0 { diff --git a/internal/rpn/operations_arithmetic.go b/internal/rpn/operations_arithmetic.go index eb45db3..dca0afe 100644 --- a/internal/rpn/operations_arithmetic.go +++ b/internal/rpn/operations_arithmetic.go @@ -22,18 +22,18 @@ func (o *Operations) Add(stack *Stack) error { return metricError("+", aM, bM) } - pm := o.GetPrefixMode() - // Convert both to base units, add, convert back to result metric - aBase, err := convertToBase(o.metricRegistry, a, pm) - if err != nil { - return buildError("addition", err) - } - bBase, err := convertToBase(o.metricRegistry, b, pm) - if err != nil { - return buildError("addition", err) - } - resultMetric := compatibleMetric(o.metricRegistry, aM, bM) - resultVal := convertFromBase(o.metricRegistry, aBase+bBase, resultMetric, pm) + pm := o.GetPrefixMode() + resultMetric := compatibleMetric(o.metricRegistry, aM, bM) + // Convert both to base units, add, convert back to result metric + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) + if err != nil { + return buildError("addition", err) + } + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) + if err != nil { + return buildError("addition", err) + } + resultVal := convertFromBase(o.metricRegistry, aBase+bBase, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) return nil @@ -52,15 +52,15 @@ func (o *Operations) Subtract(stack *Stack) error { } pm := o.GetPrefixMode() - aBase, err := convertToBase(o.metricRegistry, a, pm) + resultMetric := compatibleMetric(o.metricRegistry, aM, bM) + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) if err != nil { return buildError("subtraction", err) } - bBase, err := convertToBase(o.metricRegistry, b, pm) + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) if err != nil { return buildError("subtraction", err) } - resultMetric := compatibleMetric(o.metricRegistry, aM, bM) resultVal := convertFromBase(o.metricRegistry, aBase-bBase, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) @@ -77,16 +77,16 @@ func (o *Operations) Multiply(stack *Stack) error { aM, bM := resolveMetric(o.metricRegistry, a), resolveMetric(o.metricRegistry, b) pm := o.GetPrefixMode() + resultMetric := resultMetricForMul(o.metricRegistry, aM, bM) // Convert both to base units, multiply, convert back to result metric - aBase, err := convertToBase(o.metricRegistry, a, pm) + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) if err != nil { return buildError("multiplication", err) } - bBase, err := convertToBase(o.metricRegistry, b, pm) + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) if err != nil { return buildError("multiplication", err) } - resultMetric := resultMetricForMul(o.metricRegistry, aM, bM) resultVal := convertFromBase(o.metricRegistry, aBase*bBase, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) @@ -112,15 +112,15 @@ func (o *Operations) Divide(stack *Stack) error { aM, bM := resolveMetric(o.metricRegistry, a), resolveMetric(o.metricRegistry, b) pm := o.GetPrefixMode() - aBase, err := convertToBase(o.metricRegistry, a, pm) + resultMetric := resultMetricForDiv(o.metricRegistry, aM, bM) + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) if err != nil { return buildError("division", err) } - bBase, err := convertToBase(o.metricRegistry, b, pm) + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) if err != nil { return buildError("division", err) } - resultMetric := resultMetricForDiv(o.metricRegistry, aM, bM) resultVal := convertFromBase(o.metricRegistry, aBase/bBase, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) @@ -172,15 +172,15 @@ func (o *Operations) Modulo(stack *Stack) error { } pm := o.GetPrefixMode() - aBase, err := convertToBase(o.metricRegistry, a, pm) + resultMetric := compatibleMetric(o.metricRegistry, aM, bM) + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) if err != nil { return buildError("modulo", err) } - bBase, err := convertToBase(o.metricRegistry, b, pm) + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) if err != nil { return buildError("modulo", err) } - resultMetric := compatibleMetric(o.metricRegistry, aM, bM) resultVal := convertFromBase(o.metricRegistry, math.Mod(aBase, bBase), resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) diff --git a/internal/rpn/operations_compare.go b/internal/rpn/operations_compare.go index 9d68318..d55451b 100644 --- a/internal/rpn/operations_compare.go +++ b/internal/rpn/operations_compare.go @@ -19,11 +19,12 @@ func compareValues(o *Operations, stack *Stack, op string, cmp func(float64, flo } pm := o.GetPrefixMode() - aBase, err := convertToBase(o.metricRegistry, a, pm) + resultMetric := compatibleMetric(o.metricRegistry, aM, bM) + aBase, err := convertToBase(o.metricRegistry, a, pm, resultMetric) if err != nil { return buildError(op, err) } - bBase, err := convertToBase(o.metricRegistry, b, pm) + bBase, err := convertToBase(o.metricRegistry, b, pm, resultMetric) if err != nil { return buildError(op, err) } diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go index b261fd2..cd4e546 100644 --- a/internal/rpn/operations_hyper.go +++ b/internal/rpn/operations_hyper.go @@ -30,19 +30,20 @@ func (o *Operations) HyperAdd(stack *Stack) error { return err } + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + // Convert all to base units, sum, convert back pm := o.GetPrefixMode() var sum float64 for i, v := range values { - base, err := convertToBase(o.metricRegistry, v, pm) + base, err := convertToBase(o.metricRegistry, v, pm, resultMetric) if err != nil { return buildError("[+]", fmt.Errorf("operand %d: %w", i, err)) } sum += base } - // Result metric: first non-Cool metric (Cool absorbs), or Cool - resultMetric := resultMetricForHyperAdd(metrics) resultVal := convertFromBase(o.metricRegistry, sum, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) @@ -95,23 +96,24 @@ func (o *Operations) HyperSubtract(stack *Stack) error { return err } + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + // Convert all to base units, subtract, convert back pm := o.GetPrefixMode() - firstBase, err := convertToBase(o.metricRegistry, values[0], pm) + firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric) if err != nil { return buildError("[-]", fmt.Errorf("operand 0: %w", err)) } result := firstBase for i := 1; i < len(values); i++ { - base, err := convertToBase(o.metricRegistry, values[i], pm) + base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric) if err != nil { return buildError("[-]", fmt.Errorf("operand %d: %w", i, err)) } result -= base } - // Result metric: first non-Cool metric (Cool absorbs), or Cool - resultMetric := resultMetricForHyperAdd(metrics) resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) @@ -193,15 +195,18 @@ func (o *Operations) HyperModulo(stack *Stack) error { return err } + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + // Convert all to base units, compute modulo, convert back pm := o.GetPrefixMode() - firstBase, err := convertToBase(o.metricRegistry, values[0], pm) + firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric) if err != nil { return buildError("[%]", fmt.Errorf("operand 0: %w", err)) } result := firstBase for i := 1; i < len(values); i++ { - base, err := convertToBase(o.metricRegistry, values[i], pm) + base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric) if err != nil { return buildError("[%]", fmt.Errorf("operand %d: %w", i, err)) } @@ -211,8 +216,6 @@ func (o *Operations) HyperModulo(stack *Stack) error { result = math.Mod(result, base) } - // Result metric: first non-Cool metric (Cool absorbs), or Cool - resultMetric := resultMetricForHyperAdd(metrics) resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm) stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) diff --git a/internal/rpn/operations_hyper_test.go b/internal/rpn/operations_hyper_test.go index d6f8205..270b7c5 100644 --- a/internal/rpn/operations_hyper_test.go +++ b/internal/rpn/operations_hyper_test.go @@ -36,9 +36,9 @@ func TestHyperAddCoolAbsorbing(t *testing.T) { reg := GetMetricRegistry() tolerance := 0.001 - // 5 (Cool) 100Mbps 10Mbps [+] ≈ 110.000005Mbps - // Cool (factor 1.0) contributes 5 base units (bps) which is negligible - // Result metric is Mbps (first non-Cool), value ≈ 110Mbps + // 5 (Cool) 100Mbps 10Mbps [+] = 115Mbps + // With Cool absorption, 5 is treated as 5 Mbps + // 5 + 100 + 10 = 115 Mbps vars := NewVariables() rpn := NewRPN(vars) result, err := rpn.ParseAndEvaluate("5 100Mbps 10Mbps [+]") @@ -46,8 +46,8 @@ func TestHyperAddCoolAbsorbing(t *testing.T) { t.Fatalf("unexpected error: %v", err) } resultVal, _ := strconv.ParseFloat(result, 64) - if resultVal < 110-tolerance || resultVal > 110+tolerance { - t.Errorf("result = %g, want ~110", resultVal) + if resultVal < 115-tolerance || resultVal > 115+tolerance { + t.Errorf("result = %g, want 115", resultVal) } stack := rpn.GetCurrentStack() m := stack[len(stack)-1].Metric() @@ -182,8 +182,8 @@ func TestHyperSubtractMixedUnits(t *testing.T) { func TestHyperSubtractCoolAbsorbing(t *testing.T) { reg := GetMetricRegistry() - // 100km 5 [-] = 100km - 5m = 99.995km - // Cool (factor 1.0) contributes 5 base units (meters) = 0.005km + // 100km 5 [-] = 100km - 5km = 95km + // With Cool absorption, 5 is treated as 5 in km's space vars := NewVariables() rpn := NewRPN(vars) result, err := rpn.ParseAndEvaluate("100km 5 [-]") @@ -191,8 +191,8 @@ func TestHyperSubtractCoolAbsorbing(t *testing.T) { t.Fatalf("unexpected error: %v", err) } resultVal, _ := strconv.ParseFloat(result, 64) - if resultVal < 99.99 || resultVal > 99.996 { - t.Errorf("result = %g, want ~99.995", resultVal) + if resultVal < 94 || resultVal > 96 { + t.Errorf("result = %g, want 95", resultVal) } stack := rpn.GetCurrentStack() m := stack[len(stack)-1].Metric() diff --git a/internal/rpn/operations_metric.go b/internal/rpn/operations_metric.go index a78c773..c5f60d2 100644 --- a/internal/rpn/operations_metric.go +++ b/internal/rpn/operations_metric.go @@ -50,13 +50,20 @@ func compatibleMetric(reg *MetricRegistry, a, b *Metric) *Metric { } // convertToBase converts a Number's value to its metric's base unit. -// Returns the converted float64 value. -func convertToBase(reg *MetricRegistry, n Number, mode PrefixMode) (float64, error) { +// When the number's metric is Cool (Universal) and resultMetric is non-Cool, +// the Cool value is treated in resultMetric's space (not base units). +// Returns the converted float64 value in base units. +func convertToBase(reg *MetricRegistry, n Number, mode PrefixMode, resultMetric *Metric) (float64, error) { m := resolveMetric(reg, n) val, err := n.Float64() if err != nil { return 0, fmt.Errorf("convertToBase: %w", err) } + // Cool absorbing: if operand is Cool but result metric is not, + // treat the Cool value as units of the result metric. + if m.Category == Universal && resultMetric != nil && resultMetric.Category != Universal { + return val * resultMetric.Factor(mode), nil + } return val * m.Factor(mode), nil } @@ -177,7 +184,7 @@ func (o *Operations) Convert(stack *Stack) error { } // 5. Convert through base unit: value → base → target pm := o.GetPrefixMode() - baseVal, err := convertToBase(o.metricRegistry, value, pm) + baseVal, err := convertToBase(o.metricRegistry, value, pm, targetMetric) if err != nil { return buildError("convert", err) } -- cgit v1.2.3