diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-22 12:26:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-22 12:26:15 +0300 |
| commit | 28c911caa58f8dda05f97b77ea8b47f26610052e (patch) | |
| tree | 42278e6e9d2d249d9aadc4a9edf2e32bb8a13369 | |
| parent | 9eef36a097be877388e059fd1183aaf9ab166ee4 (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.go | 20 | ||||
| -rw-r--r-- | internal/rpn/operations_arithmetic.go | 35 | ||||
| -rw-r--r-- | internal/rpn/operations_compare.go | 5 | ||||
| -rw-r--r-- | internal/rpn/operations_metric.go | 5 | ||||
| -rw-r--r-- | internal/rpn/operations_metric_cmd.go | 2 | ||||
| -rw-r--r-- | internal/rpn/operations_metric_cmd_test.go | 173 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 2 |
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'") |
