diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-22 11:54:39 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-22 11:54:39 +0300 |
| commit | cfd98d5bbf08680266e965c15e938aea87c4b832 (patch) | |
| tree | 56ba2a6741ce4e10461e494dc8dd5b32fb94a5dd | |
| parent | f3597d78a69003e73c5d9b90fd8382700eb770a8 (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.go | 53 | ||||
| -rw-r--r-- | internal/rpn/operations_metric.go | 10 |
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 |
