summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 12:52:26 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 12:52:26 +0300
commit17003ea83a862b0bddff13c4e5f0a66d1fda25dd (patch)
tree8a998c80295382d298df5a750cd25f9f2b8dc2ef
parentc22cd367c9b6edc9744701fe7f912de487ec0953 (diff)
fix(rpn): treat Cool values in result metric space during absorption
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
-rw-r--r--internal/rpn/metric_test.go13
-rw-r--r--internal/rpn/operations_arithmetic.go48
-rw-r--r--internal/rpn/operations_compare.go5
-rw-r--r--internal/rpn/operations_hyper.go25
-rw-r--r--internal/rpn/operations_hyper_test.go18
-rw-r--r--internal/rpn/operations_metric.go13
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)
}