summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 11:54:39 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 11:54:39 +0300
commitcfd98d5bbf08680266e965c15e938aea87c4b832 (patch)
tree56ba2a6741ce4e10461e494dc8dd5b32fb94a5dd
parentf3597d78a69003e73c5d9b90fd8382700eb770a8 (diff)
fix(rpn): use convertToBase/convertFromBase in Convert; fix test tolerance and add missing cases
Refactor Convert() to use existing helper functions instead of inline factor math, consistent with Add/Subtract/Multiply/Divide. Fix TestConvertCoolAbsorbing tolerance (was 0.0001 but value is 1.25e-8, rendering the test useless). Use relative tolerance. Add missing test cases: - metric→Cool conversion (1hr @Cool convert) - zero value conversion (0GB @MB convert) - negative value conversion (-100km @mi convert) - missing target operand (100Mbps convert) - error message content verification
-rw-r--r--internal/rpn/metric_test.go53
-rw-r--r--internal/rpn/operations_metric.go10
2 files changed, 51 insertions, 12 deletions
diff --git a/internal/rpn/metric_test.go b/internal/rpn/metric_test.go
index d187332..aeb69d4 100644
--- a/internal/rpn/metric_test.go
+++ b/internal/rpn/metric_test.go
@@ -6,6 +6,7 @@ package rpn
import (
"fmt"
"strconv"
+ "strings"
"testing"
)
@@ -1017,6 +1018,7 @@ func TestMetricOperationsUnit(t *testing.T) {
}
func TestConvertSameCategory(t *testing.T) {
+ reg := GetMetricRegistry()
tests := []struct {
expr string
wantNum float64
@@ -1029,6 +1031,10 @@ func TestConvertSameCategory(t *testing.T) {
{"1hr @min convert", 60, "min", 0.001},
{"3.14kg @lb convert", 6.922, "lb", 0.01},
{"100mph @kmh convert", 160.934, "kmh", 0.01},
+ // Zero value
+ {"0GB @MB convert", 0, "MB", 0.0001},
+ // Negative value
+ {"-100km @mi convert", -62.1371, "mi", 0.001},
}
for _, tt := range tests {
@@ -1046,7 +1052,8 @@ func TestConvertSameCategory(t *testing.T) {
stack := rpn.GetCurrentStack()
if len(stack) > 0 {
m := stack[0].Metric()
- if m == nil || m.Name != tt.wantMet {
+ expected, _ := reg.Find(tt.wantMet)
+ if m != expected {
t.Errorf("metric = %v, want %s", m, tt.wantMet)
}
}
@@ -1055,26 +1062,48 @@ func TestConvertSameCategory(t *testing.T) {
}
func TestConvertCoolAbsorbing(t *testing.T) {
+ reg := GetMetricRegistry()
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 * 1 / (8e9) = 1.25e-8 GB
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
- if resultVal < expected-0.0001 || resultVal > expected+0.0001 {
- t.Errorf("result = %g, want %g", resultVal, expected)
+ 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)
}
stack := rpn.GetCurrentStack()
if len(stack) > 0 {
m := stack[0].Metric()
- if m == nil || m.Name != "GB" {
+ gb, _ := reg.Find("GB")
+ if m != gb {
t.Errorf("metric = %v, want GB", m)
}
}
+
+ // Metric to Cool: 1hr @Cool convert → 3600 Cool
+ rpn2 := NewRPN(NewVariables())
+ result2, err := rpn2.ParseAndEvaluate("1hr @Cool convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal2, _ := strconv.ParseFloat(result2, 64)
+ if resultVal2 != 3600 {
+ t.Errorf("metric→cool result = %g, want 3600", resultVal2)
+ }
+ stack2 := rpn2.GetCurrentStack()
+ if len(stack2) > 0 {
+ m := stack2[0].Metric()
+ cool, _ := reg.Find("Cool")
+ if m != cool {
+ t.Errorf("metric = %v, want Cool", m)
+ }
+ }
}
func TestConvertIncompatible(t *testing.T) {
@@ -1085,14 +1114,26 @@ func TestConvertIncompatible(t *testing.T) {
if err == nil {
t.Error("expected error for incompatible categories")
}
+ // Verify the error message mentions the incompatible metrics
+ if err != nil && !strings.Contains(err.Error(), "incompatible") {
+ t.Errorf("expected error to mention 'incompatible', got: %v", err)
+ }
}
func TestConvertInsufficientOperands(t *testing.T) {
vars := NewVariables()
rpn := NewRPN(vars)
+ // Missing value (only target on stack)
_, err := rpn.ParseAndEvaluate("@Gbps convert")
if err == nil {
- t.Error("expected error for insufficient operands")
+ t.Error("expected error for missing value operand")
+ }
+
+ // Missing target (only value on stack, no @X before convert)
+ rpn2 := NewRPN(NewVariables())
+ _, err = rpn2.ParseAndEvaluate("100Mbps convert")
+ if err == nil {
+ t.Error("expected error for missing target metric operand")
}
}
diff --git a/internal/rpn/operations_metric.go b/internal/rpn/operations_metric.go
index bf5afb5..5072d8e 100644
--- a/internal/rpn/operations_metric.go
+++ b/internal/rpn/operations_metric.go
@@ -155,7 +155,7 @@ func baseMetric(reg *MetricRegistry, name string) *Metric {
// Convert converts a value from its current metric to a target metric.
// Pops target metric (from @X syntax), then pops value to convert.
-// Validates category compatibility, computes result = value * fromFactor / toFactor,
+// Validates category compatibility, converts through base unit,
// and pushes the result with the target metric.
func (o *Operations) Convert(stack *Stack) error {
// 1. Pop target (from @X syntax)
@@ -175,14 +175,12 @@ func (o *Operations) Convert(stack *Stack) error {
if !categoriesCompatible(valueMetric, targetMetric) {
return metricError("convert", valueMetric, targetMetric)
}
- // 5. Convert: value * fromFactor / toFactor
- valueF, err := value.Float64()
+ // 5. Convert through base unit: value → base → target
+ baseVal, err := convertToBase(o.metricRegistry, value, SI)
if err != nil {
return buildError("convert", err)
}
- fromFactor := valueMetric.Factor(SI)
- toFactor := targetMetric.Factor(SI)
- resultVal := valueF * fromFactor / toFactor
+ resultVal := convertFromBase(o.metricRegistry, baseVal, targetMetric, SI)
// 6. Push result with target metric
stack.Push(NewNumber(resultVal, o.GetMode(), targetMetric))
return nil