summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 12:26:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 12:26:15 +0300
commit28c911caa58f8dda05f97b77ea8b47f26610052e (patch)
tree42278e6e9d2d249d9aadc4a9edf2e32bb8a13369
parent9eef36a097be877388e059fd1183aaf9ab166ee4 (diff)
wire prefixMode from RPN into Operations for metric-aware arithmetic
Add prefixMode field to Operations struct with thread-safe Get/Set accessors, propagating the user-set prefix mode (SI/IEC) from the RPN parser through the evaluate path. - Replace all hardcoded SI arguments in convertToBase/convertFromBase calls with o.GetPrefixMode() across arithmetic, comparison, and convert operations - Cache GetPrefixMode() once per operation to avoid repeated mutex overhead - Sync r.ops.SetPrefixMode() alongside r.prefixMode in metric binary/ decimal set handlers (no deadlock: Operations.mu is separate from RPN.mu) - Update MetricShow to display factor using current prefix mode - Add SetPrefixMode to the Operator interface - Add end-to-end tests verifying prefix mode affects conversion, arithmetic, and comparison results
-rw-r--r--internal/rpn/operations.go20
-rw-r--r--internal/rpn/operations_arithmetic.go35
-rw-r--r--internal/rpn/operations_compare.go5
-rw-r--r--internal/rpn/operations_metric.go5
-rw-r--r--internal/rpn/operations_metric_cmd.go2
-rw-r--r--internal/rpn/operations_metric_cmd_test.go173
-rw-r--r--internal/rpn/rpn_parse.go2
7 files changed, 222 insertions, 20 deletions
diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go
index d483cc8..04c2014 100644
--- a/internal/rpn/operations.go
+++ b/internal/rpn/operations.go
@@ -156,6 +156,8 @@ type Operator interface {
PowerIntOperator
// SetMode sets the calculation mode for number formatting
SetMode(CalculationMode)
+ // SetPrefixMode sets the prefix mode for data size calculations
+ SetPrefixMode(PrefixMode)
// Metric command handlers
MetricShow(stack *Stack) (string, error)
MetricList(stack *Stack) (string, error)
@@ -168,6 +170,7 @@ type Operations struct {
vars VariableStore
consts ConstantsProvider
mode CalculationMode
+ prefixMode PrefixMode
metricRegistry *MetricRegistry
mu sync.RWMutex
}
@@ -184,6 +187,7 @@ func NewOperations(vars VariableStore) *Operations {
vars: vars,
consts: consts,
mode: FloatMode, // default
+ prefixMode: SI, // default
metricRegistry: GetMetricRegistry(),
}
}
@@ -204,6 +208,22 @@ func (o *Operations) GetMode() CalculationMode {
return o.mode
}
+// GetPrefixMode returns the current prefix mode.
+// This method is thread-safe for reads.
+func (o *Operations) GetPrefixMode() PrefixMode {
+ o.mu.RLock()
+ defer o.mu.RUnlock()
+ return o.prefixMode
+}
+
+// SetPrefixMode sets the prefix mode for data size calculations.
+// This method is thread-safe for writes.
+func (o *Operations) SetPrefixMode(mode PrefixMode) {
+ o.mu.Lock()
+ defer o.mu.Unlock()
+ o.prefixMode = mode
+}
+
// OperatorHandler represents a function that handles an operator.
// Returns (result string, handled bool, error error).
// result is non-empty only for commands that return immediately (like show, vars).
diff --git a/internal/rpn/operations_arithmetic.go b/internal/rpn/operations_arithmetic.go
index c28209b..eb45db3 100644
--- a/internal/rpn/operations_arithmetic.go
+++ b/internal/rpn/operations_arithmetic.go
@@ -22,17 +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, SI)
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError("addition", err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ 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, SI)
+ resultVal := convertFromBase(o.metricRegistry, aBase+bBase, resultMetric, pm)
stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
@@ -50,16 +51,17 @@ func (o *Operations) Subtract(stack *Stack) error {
return metricError("-", aM, bM)
}
- aBase, err := convertToBase(o.metricRegistry, a, SI)
+ pm := o.GetPrefixMode()
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError("subtraction", err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ bBase, err := convertToBase(o.metricRegistry, b, pm)
if err != nil {
return buildError("subtraction", err)
}
resultMetric := compatibleMetric(o.metricRegistry, aM, bM)
- resultVal := convertFromBase(o.metricRegistry, aBase-bBase, resultMetric, SI)
+ resultVal := convertFromBase(o.metricRegistry, aBase-bBase, resultMetric, pm)
stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
@@ -74,17 +76,18 @@ func (o *Operations) Multiply(stack *Stack) error {
aM, bM := resolveMetric(o.metricRegistry, a), resolveMetric(o.metricRegistry, b)
+ pm := o.GetPrefixMode()
// Convert both to base units, multiply, convert back to result metric
- aBase, err := convertToBase(o.metricRegistry, a, SI)
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError("multiplication", err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ bBase, err := convertToBase(o.metricRegistry, b, pm)
if err != nil {
return buildError("multiplication", err)
}
resultMetric := resultMetricForMul(o.metricRegistry, aM, bM)
- resultVal := convertFromBase(o.metricRegistry, aBase*bBase, resultMetric, SI)
+ resultVal := convertFromBase(o.metricRegistry, aBase*bBase, resultMetric, pm)
stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
@@ -108,16 +111,17 @@ func (o *Operations) Divide(stack *Stack) error {
aM, bM := resolveMetric(o.metricRegistry, a), resolveMetric(o.metricRegistry, b)
- aBase, err := convertToBase(o.metricRegistry, a, SI)
+ pm := o.GetPrefixMode()
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError("division", err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ bBase, err := convertToBase(o.metricRegistry, b, pm)
if err != nil {
return buildError("division", err)
}
resultMetric := resultMetricForDiv(o.metricRegistry, aM, bM)
- resultVal := convertFromBase(o.metricRegistry, aBase/bBase, resultMetric, SI)
+ resultVal := convertFromBase(o.metricRegistry, aBase/bBase, resultMetric, pm)
stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
@@ -167,16 +171,17 @@ func (o *Operations) Modulo(stack *Stack) error {
return metricError("%", aM, bM)
}
- aBase, err := convertToBase(o.metricRegistry, a, SI)
+ pm := o.GetPrefixMode()
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError("modulo", err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ bBase, err := convertToBase(o.metricRegistry, b, pm)
if err != nil {
return buildError("modulo", err)
}
resultMetric := compatibleMetric(o.metricRegistry, aM, bM)
- resultVal := convertFromBase(o.metricRegistry, math.Mod(aBase, bBase), resultMetric, SI)
+ resultVal := convertFromBase(o.metricRegistry, math.Mod(aBase, bBase), resultMetric, pm)
stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric))
return nil
diff --git a/internal/rpn/operations_compare.go b/internal/rpn/operations_compare.go
index 2fb5870..9d68318 100644
--- a/internal/rpn/operations_compare.go
+++ b/internal/rpn/operations_compare.go
@@ -18,11 +18,12 @@ func compareValues(o *Operations, stack *Stack, op string, cmp func(float64, flo
return metricError(op, aM, bM)
}
- aBase, err := convertToBase(o.metricRegistry, a, SI)
+ pm := o.GetPrefixMode()
+ aBase, err := convertToBase(o.metricRegistry, a, pm)
if err != nil {
return buildError(op, err)
}
- bBase, err := convertToBase(o.metricRegistry, b, SI)
+ bBase, err := convertToBase(o.metricRegistry, b, pm)
if err != nil {
return buildError(op, err)
}
diff --git a/internal/rpn/operations_metric.go b/internal/rpn/operations_metric.go
index 5072d8e..a78c773 100644
--- a/internal/rpn/operations_metric.go
+++ b/internal/rpn/operations_metric.go
@@ -176,11 +176,12 @@ func (o *Operations) Convert(stack *Stack) error {
return metricError("convert", valueMetric, targetMetric)
}
// 5. Convert through base unit: value → base → target
- baseVal, err := convertToBase(o.metricRegistry, value, SI)
+ pm := o.GetPrefixMode()
+ baseVal, err := convertToBase(o.metricRegistry, value, pm)
if err != nil {
return buildError("convert", err)
}
- resultVal := convertFromBase(o.metricRegistry, baseVal, targetMetric, SI)
+ resultVal := convertFromBase(o.metricRegistry, baseVal, targetMetric, pm)
// 6. Push result with target metric
stack.Push(NewNumber(resultVal, o.GetMode(), targetMetric))
return nil
diff --git a/internal/rpn/operations_metric_cmd.go b/internal/rpn/operations_metric_cmd.go
index 57303a6..5ed5fc6 100644
--- a/internal/rpn/operations_metric_cmd.go
+++ b/internal/rpn/operations_metric_cmd.go
@@ -22,7 +22,7 @@ func (o *Operations) MetricShow(stack *Stack) (string, error) {
if m == nil || m.Category == Universal {
return "Cool (Universal)", nil
}
- factor := m.Factor(SI) // display uses SI; prefixMode is not yet wired into computations
+ factor := m.Factor(o.GetPrefixMode())
return fmt.Sprintf("%s, %s, base: %s, factor: %.0g", m.Name, m.Category, m.BaseUnit, factor), nil
}
diff --git a/internal/rpn/operations_metric_cmd_test.go b/internal/rpn/operations_metric_cmd_test.go
index a2bfcca..6e5b422 100644
--- a/internal/rpn/operations_metric_cmd_test.go
+++ b/internal/rpn/operations_metric_cmd_test.go
@@ -4,6 +4,7 @@
package rpn
import (
+ "strconv"
"strings"
"testing"
)
@@ -248,3 +249,175 @@ func TestMetricCompatibleEmptyStack(t *testing.T) {
t.Error("expected error for empty stack")
}
}
+
+func TestPrefixModeEndToEndConvert(t *testing.T) {
+ // SI mode: 1GB → MB = 1000
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1GB @MB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 999.9 || resultVal > 1000.1 {
+ t.Errorf("SI: 1GB→MB = %g, want 1000", resultVal)
+ }
+
+ // IEC mode: 1GB → MB = 1024
+ vars2 := NewVariables()
+ rpn2 := NewRPN(vars2)
+ _, err = rpn2.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result2, err := rpn2.ParseAndEvaluate("1GB @MB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal2, _ := strconv.ParseFloat(result2, 64)
+ if resultVal2 < 1023.9 || resultVal2 > 1024.1 {
+ t.Errorf("IEC: 1GB→MB = %g, want 1024", resultVal2)
+ }
+}
+
+func TestPrefixModeAffectsCrossMetricConvert(t *testing.T) {
+ // SI mode: 1GiB → GB = ~1.07374
+ // (GiB is always 2^30, GB in SI mode is 10^9)
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1GiB @GB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 1.07 || resultVal > 1.08 {
+ t.Errorf("SI: 1GiB→GB = %g, want ~1.074", resultVal)
+ }
+
+ // IEC mode: 1GiB → GB = 1.0
+ // (GiB is 2^30, GB in IEC mode is also 2^30)
+ vars2 := NewVariables()
+ rpn2 := NewRPN(vars2)
+ _, err = rpn2.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result2, err := rpn2.ParseAndEvaluate("1GiB @GB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal2, _ := strconv.ParseFloat(result2, 64)
+ if resultVal2 < 0.999 || resultVal2 > 1.001 {
+ t.Errorf("IEC: 1GiB→GB = %g, want 1.0", resultVal2)
+ }
+}
+
+func TestPrefixModeAffectsArithmetic(t *testing.T) {
+ // SI mode: 1024KB + 1KB in SI = 1024*8000 + 8000 = 8232800 bits → /8000 = 1025KB
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1024KB 1KB + @KB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal, _ := strconv.ParseFloat(result, 64)
+ if resultVal < 1024.9 || resultVal > 1025.1 {
+ t.Errorf("SI: 1024KB+1KB→KB = %g, want 1025", resultVal)
+ }
+
+ // IEC mode: 1024KB + 1KB where KB = 8*1024 bits
+ // 1024*8192 + 8192 = 8401408 bits → /8192 = 1025KB
+ vars2 := NewVariables()
+ rpn2 := NewRPN(vars2)
+ _, err = rpn2.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result2, err := rpn2.ParseAndEvaluate("1024KB 1KB + @KB convert")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ resultVal2, _ := strconv.ParseFloat(result2, 64)
+ if resultVal2 < 1024.9 || resultVal2 > 1025.1 {
+ t.Errorf("IEC: 1024KB+1KB→KB = %g, want 1025", resultVal2)
+ }
+}
+
+func TestPrefixModeAffectsComparison(t *testing.T) {
+ // SI mode: 1GB (8e9 bits) vs 1000MB (1000*8e6 = 8e9 bits) → equal
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1GB 1000MB eq")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if result != "true" {
+ t.Errorf("SI: 1GB == 1000MB should be true, got %s", result)
+ }
+
+ // IEC mode: 1GB (8*2^30 bits) vs 1000MB (1000*8*2^20 bits)
+ // 8*2^30 = 8589934592, 1000*8*2^20 = 8388608000 → not equal
+ vars2 := NewVariables()
+ rpn2 := NewRPN(vars2)
+ _, err = rpn2.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result2, err := rpn2.ParseAndEvaluate("1GB 1000MB eq")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if result2 != "false" {
+ t.Errorf("IEC: 1GB == 1000MB should be false, got %s", result2)
+ }
+
+ // IEC mode: 1GB == 1024MB (both use 2^30 / 2^20) — use fresh RPN to avoid stack carryover
+ vars3 := NewVariables()
+ rpn3 := NewRPN(vars3)
+ _, err = rpn3.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result3, err := rpn3.ParseAndEvaluate("1GB 1024MB eq")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if result3 != "true" {
+ t.Errorf("IEC: 1GB == 1024MB should be true, got %s", result3)
+ }
+}
+
+func TestMetricShowReflectsPrefixMode(t *testing.T) {
+ // In SI mode, GB factor should be 8e+09
+ vars := NewVariables()
+ rpn := NewRPN(vars)
+ result, err := rpn.ParseAndEvaluate("1GB metric show")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !strings.Contains(result, "GB") {
+ t.Errorf("expected 'GB' in result, got: %s", result)
+ }
+ // SI GB factor = 8e9, formatted with %.0g = "8e+09"
+ if !strings.Contains(result, "8e+09") {
+ t.Errorf("SI mode GB factor should be 8e+09, got: %s", result)
+ }
+
+ // Switch to IEC mode, GB factor should be 8*2^30 = 8589934592
+ // formatted with %.0g = "9e+09"
+ vars2 := NewVariables()
+ rpn2 := NewRPN(vars2)
+ _, err = rpn2.ParseAndEvaluate("metric binary set")
+ if err != nil {
+ t.Fatalf("metric binary set failed: %v", err)
+ }
+ result2, err := rpn2.ParseAndEvaluate("1GB metric show")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // IEC GB factor = 8*2^30 ≈ 8.59e9, formatted with %.0g = "9e+09"
+ // (different from SI's "8e+09")
+ if !strings.Contains(result2, "9e+09") {
+ t.Errorf("IEC mode GB factor should be ~9e+09, got: %s", result2)
+ }
+}
diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go
index bc25d52..2d2963a 100644
--- a/internal/rpn/rpn_parse.go
+++ b/internal/rpn/rpn_parse.go
@@ -368,12 +368,14 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) {
case "binary":
if i+2 < len(tokens) && tokens[i+2] == "set" {
r.prefixMode = IEC
+ r.ops.SetPrefixMode(IEC)
return "prefix mode: IEC", nil
}
return "", fmt.Errorf("rpn: metric binary: use 'metric binary set'")
case "decimal":
if i+2 < len(tokens) && tokens[i+2] == "set" {
r.prefixMode = SI
+ r.ops.SetPrefixMode(SI)
return "prefix mode: SI", nil
}
return "", fmt.Errorf("rpn: metric decimal: use 'metric decimal set'")