summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 12:46:49 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 12:46:49 +0300
commitc22cd367c9b6edc9744701fe7f912de487ec0953 (patch)
tree14a6ce51e8e2986ed503b16437c2cc66bcab0394
parent75f5f402a22b302b85bd69fe9589b82e5caade4d (diff)
fix(rpn): guard resultMetricForHyperAdd against empty slice; add missing hyper metric tests
- resultMetricForHyperAdd: handle empty/nil metrics defensively (returns Cool metric instead of potential panic) - Add tests for Cool absorbing in HyperSubtract, negative result with metric, and result metric assertion in mixed units
-rw-r--r--internal/rpn/operations_hyper.go9
-rw-r--r--internal/rpn/operations_hyper_test.go55
2 files changed, 61 insertions, 3 deletions
diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go
index b0bb24b..b261fd2 100644
--- a/internal/rpn/operations_hyper.go
+++ b/internal/rpn/operations_hyper.go
@@ -303,7 +303,14 @@ func resultMetricForHyperAdd(metrics []*Metric) *Metric {
return m
}
}
- return metrics[0]
+ // All Universal (Cool) or empty slice — default to Cool.
+ // In practice, metrics is never empty (popAll enforces >= 2 operands),
+ // but we handle it defensively.
+ if len(metrics) > 0 && metrics[0] != nil {
+ return metrics[0]
+ }
+ m, _ := GetMetricRegistry().Find("Cool")
+ return m
}
// validateSameCategory checks that all metrics belong to the same category.
diff --git a/internal/rpn/operations_hyper_test.go b/internal/rpn/operations_hyper_test.go
index 700614f..d6f8205 100644
--- a/internal/rpn/operations_hyper_test.go
+++ b/internal/rpn/operations_hyper_test.go
@@ -157,6 +157,7 @@ func TestHyperSubtractMetricAware(t *testing.T) {
}
func TestHyperSubtractMixedUnits(t *testing.T) {
+ reg := GetMetricRegistry()
tolerance := 0.001
// 2km 500m 100m [-] = (2000 - 500 - 100)m = 1400m = 1.4km
@@ -170,6 +171,57 @@ func TestHyperSubtractMixedUnits(t *testing.T) {
if resultVal < 1.4-tolerance || resultVal > 1.4+tolerance {
t.Errorf("result = %g, want 1.4", resultVal)
}
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ km, _ := reg.Find("km")
+ if m != km {
+ t.Errorf("metric = %v, want km", m)
+ }
+}
+
+func TestHyperSubtractCoolAbsorbing(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 100km 5 [-] = 100km - 5m = 99.995km
+ // Cool (factor 1.0) contributes 5 base units (meters) = 0.005km
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("100km 5 [-]")
+ if err != nil {
+ 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)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ km, _ := reg.Find("km")
+ if m != km {
+ t.Errorf("metric = %v, want km", m)
+ }
+}
+
+func TestHyperSubtractNegativeResult(t *testing.T) {
+ reg := GetMetricRegistry()
+
+ // 1km 2km [-] = -1km
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1km 2km [-]")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < -1.1 || resultVal > -0.9 {
+ t.Errorf("result = %g, want -1", resultVal)
+ }
+ stack := rpn.GetCurrentStack()
+ m := stack[len(stack)-1].Metric()
+ km, _ := reg.Find("km")
+ if m != km {
+ t.Errorf("metric = %v, want km", m)
+ }
}
func TestHyperSubtractIncompatible(t *testing.T) {
@@ -249,8 +301,6 @@ func TestHyperModuloMetricAware(t *testing.T) {
}
func TestHyperModuloMixedUnits(t *testing.T) {
- tolerance := 0.001
-
// 1000m 300m 200m [%] = ((1000 % 300) % 200)m = (100 % 200)m = 100m = 0.1km
// Result uses first operand's metric (m)
vars := NewVariables()
@@ -260,6 +310,7 @@ func TestHyperModuloMixedUnits(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
resultVal, _ := strconv.ParseFloat(result, 64)
+ tolerance := 0.001
if resultVal < 100-tolerance || resultVal > 100+tolerance {
t.Errorf("result = %g, want 100", resultVal)
}