diff options
| -rw-r--r-- | internal/rpn/operations_hyper.go | 347 | ||||
| -rw-r--r-- | internal/rpn/operations_hyper_test.go | 418 |
2 files changed, 591 insertions, 174 deletions
diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go index 5f4a51d..b0bb24b 100644 --- a/internal/rpn/operations_hyper.go +++ b/internal/rpn/operations_hyper.go @@ -11,317 +11,316 @@ import ( // Hyper operators - operate on all values on the stack // HyperAdd pops all values from stack, adds them left-associative (with boolean-to-number coercion), and pushes result. +// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the +// computation, and pushes the result with the first operand's metric. func (o *Operations) HyperAdd(stack *Stack) error { - values, err := popAll(stack, "hyperadd") + values, err := popAll(stack, "[+]") if err != nil { return err } - // Process left-associative with Number interface - sum := 0.0 - for i := 0; i < len(values); i++ { - val, err := values[i].Float64() + // Resolve metrics for all values + metrics := make([]*Metric, len(values)) + for i, v := range values { + metrics[i] = resolveMetric(o.metricRegistry, v) + } + + // Validate all are compatible (all same category, or Cool absorbs) + if err := validateSameCategory(metrics, "[+]"); err != nil { + return err + } + + // Convert all to base units, sum, convert back + pm := o.GetPrefixMode() + var sum float64 + for i, v := range values { + base, err := convertToBase(o.metricRegistry, v, pm) if err != nil { - return buildError("hyperadd", fmt.Errorf("failed to get float64 value: %w", err)) + return buildError("[+]", fmt.Errorf("operand %d: %w", i, err)) } - sum += val + sum += base } - mode := o.GetMode() - stack.Push(NewNumber(sum, mode)) + + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + resultVal := convertFromBase(o.metricRegistry, sum, resultMetric, pm) + + stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) return nil } // HyperMultiply pops all values from stack, multiplies them left-associative, and pushes result. +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperMultiply(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hypermultiply: need at least 2 values") + values, err := popAll(stack, "[*]") + if err != nil { + return err } - product := 1.0 - for stack.Len() > 0 { - val, err := stack.Pop() + var product float64 = 1 + for i, v := range values { + val, err := v.Float64() if err != nil { - return fmt.Errorf("hypermultiply: %w", err) + return buildError("[*]", fmt.Errorf("operand %d: %w", i, err)) } - floatVal, err := val.Float64() - if err != nil { - return fmt.Errorf("hypermultiply: failed to get float64 value: %w", err) + if i == 0 { + product = val + } else { + product *= val } - product *= floatVal } - mode := o.GetMode() - stack.Push(NewNumber(product, mode)) + + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(product, o.GetMode(), cool)) return nil } // HyperSubtract pops all values from stack, subtracts them left-associative, and pushes result. +// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the +// computation, and pushes the result with the first operand's metric. func (o *Operations) HyperSubtract(stack *Stack) error { - values, err := popAll(stack, "hypersubtract") + values, err := popAll(stack, "[-]") if err != nil { return err } - // Process left-associative with Number interface - firstVal, err := values[0].Float64() + // Resolve metrics for all values + metrics := make([]*Metric, len(values)) + for i, v := range values { + metrics[i] = resolveMetric(o.metricRegistry, v) + } + + // Validate all are compatible (all same category, or Cool absorbs) + if err := validateSameCategory(metrics, "[-]"); err != nil { + return err + } + + // Convert all to base units, subtract, convert back + pm := o.GetPrefixMode() + firstBase, err := convertToBase(o.metricRegistry, values[0], pm) if err != nil { - return buildError("hypersubtract", fmt.Errorf("failed to get float64 value: %w", err)) + return buildError("[-]", fmt.Errorf("operand 0: %w", err)) } - result := firstVal + result := firstBase for i := 1; i < len(values); i++ { - val, err := values[i].Float64() + base, err := convertToBase(o.metricRegistry, values[i], pm) if err != nil { - return buildError("hypersubtract", fmt.Errorf("failed to get float64 value: %w", err)) + return buildError("[-]", fmt.Errorf("operand %d: %w", i, err)) } - result -= val + result -= base } - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm) + + stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) return nil } // HyperDivide pops all values from stack, divides them left-associative, and pushes result. +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperDivide(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hyperdivide: need at least 2 values") - } - - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hyperdivide: %w", err) - } - values = append(values, val) - } - - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + values, err := popAll(stack, "[/]") + if err != nil { + return err } - // Process left-associative with Number interface firstVal, err := values[0].Float64() if err != nil { - return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err) + return buildError("[/]", fmt.Errorf("operand 0: %w", err)) } result := firstVal for i := 1; i < len(values); i++ { val, err := values[i].Float64() if err != nil { - return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err) + return buildError("[/]", fmt.Errorf("operand %d: %w", i, err)) } if val == 0 { - return fmt.Errorf("division by zero") + return buildError("[/]", fmt.Errorf("division by zero at operand %d", i)) } result /= val } - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(result, o.GetMode(), cool)) return nil } // HyperPower pops all values from stack, raises to power left-associative, and pushes result. +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperPower(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hyperpower: need at least 2 values") - } - - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hyperpower: %w", err) - } - values = append(values, val) - } - - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + values, err := popAll(stack, "[^]") + if err != nil { + return err } - // Process left-associative with Number interface firstVal, err := values[0].Float64() if err != nil { - return fmt.Errorf("hyperpower: failed to get float64 value: %w", err) + return buildError("[^]", fmt.Errorf("operand 0: %w", err)) } result := firstVal for i := 1; i < len(values); i++ { val, err := values[i].Float64() if err != nil { - return fmt.Errorf("hyperpower: failed to get float64 value: %w", err) + return buildError("[^]", fmt.Errorf("operand %d: %w", i, err)) } result = math.Pow(result, val) } - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(result, o.GetMode(), cool)) return nil } // HyperModulo pops all values from stack, computes modulo left-associative, and pushes result. +// Metric-aware: validates all operands share the same category (Cool absorbs), converts to base units for the +// computation, and pushes the result with the first operand's metric. func (o *Operations) HyperModulo(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hypermodulo: need at least 2 values") + values, err := popAll(stack, "[%]") + if err != nil { + return err } - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hypermodulo: %w", err) - } - values = append(values, val) + // Resolve metrics for all values + metrics := make([]*Metric, len(values)) + for i, v := range values { + metrics[i] = resolveMetric(o.metricRegistry, v) } - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + // Validate all are compatible (all same category, or Cool absorbs) + if err := validateSameCategory(metrics, "[%]"); err != nil { + return err } - // Process left-associative with Number interface - firstVal, err := values[0].Float64() + // Convert all to base units, compute modulo, convert back + pm := o.GetPrefixMode() + firstBase, err := convertToBase(o.metricRegistry, values[0], pm) if err != nil { - return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err) + return buildError("[%]", fmt.Errorf("operand 0: %w", err)) } - result := firstVal + result := firstBase for i := 1; i < len(values); i++ { - val, err := values[i].Float64() + base, err := convertToBase(o.metricRegistry, values[i], pm) if err != nil { - return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err) + return buildError("[%]", fmt.Errorf("operand %d: %w", i, err)) } - if val == 0 { - return fmt.Errorf("modulo by zero") + if base == 0 { + return buildError("[%]", fmt.Errorf("modulo by zero at operand %d", i)) } - result = math.Mod(result, val) + result = math.Mod(result, base) } - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + + // Result metric: first non-Cool metric (Cool absorbs), or Cool + resultMetric := resultMetricForHyperAdd(metrics) + resultVal := convertFromBase(o.metricRegistry, result, resultMetric, pm) + + stack.Push(NewNumber(resultVal, o.GetMode(), resultMetric)) return nil } // HyperLog2 pops all values from stack, computes sum of log2 for all values, and pushes result. -// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperLog2(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hyperlog2: need at least 2 values") - } - - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hyperlog2: %w", err) - } - values = append(values, val) - } - - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + values, err := popAll(stack, "[lg]") + if err != nil { + return err } - // Sum the log2 of all values using Float64() for value conversion: - // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val, err := values[i].Float64() if err != nil { - return fmt.Errorf("hyperlog2: failed to get float64 value: %w", err) + return buildError("[lg]", fmt.Errorf("operand %d: %w", i, err)) } if val <= 0 { - return fmt.Errorf("hyperlog2 undefined for non-positive numbers") + return buildError("[lg]", fmt.Errorf("log2 undefined for non-positive numbers")) } result += math.Log2(val) } - // Push the result as a Number - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(result, o.GetMode(), cool)) return nil } // HyperLog10 pops all values from stack, computes sum of log10 for all values, and pushes result. -// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperLog10(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hyperlog10: need at least 2 values") - } - - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hyperlog10: %w", err) - } - values = append(values, val) - } - - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + values, err := popAll(stack, "[log]") + if err != nil { + return err } - // Sum the log10 of all values using Float64() for value conversion: - // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val, err := values[i].Float64() if err != nil { - return fmt.Errorf("hyperlog10: failed to get float64 value: %w", err) + return buildError("[log]", fmt.Errorf("operand %d: %w", i, err)) } if val <= 0 { - return fmt.Errorf("hyperlog10 undefined for non-positive numbers") + return buildError("[log]", fmt.Errorf("log10 undefined for non-positive numbers")) } result += math.Log10(val) } - // Push the result as a Number - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(result, o.GetMode(), cool)) return nil } // HyperLn pops all values from stack, computes sum of natural log for all values, and pushes result. -// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +// No metric validation; uses raw float64 values. Result is always Cool (unitless). func (o *Operations) HyperLn(stack *Stack) error { - if stack.Len() < 2 { - return fmt.Errorf("insufficient operands for hyperln: need at least 2 values") - } - - // Pop all values into a slice (in reverse order - top first) - var values []Number - for stack.Len() > 0 { - val, err := stack.Pop() - if err != nil { - return fmt.Errorf("hyperln: %w", err) - } - values = append(values, val) - } - - // Reverse to get left-to-right order (first pushed = first in) - for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { - values[i], values[j] = values[j], values[i] + values, err := popAll(stack, "[ln]") + if err != nil { + return err } - // Sum the natural log of all values using Float64() for value conversion: - // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val, err := values[i].Float64() if err != nil { - return fmt.Errorf("hyperln: failed to get float64 value: %w", err) + return buildError("[ln]", fmt.Errorf("operand %d: %w", i, err)) } if val <= 0 { - return fmt.Errorf("hyperln undefined for non-positive numbers") + return buildError("[ln]", fmt.Errorf("ln undefined for non-positive numbers")) } result += math.Log(val) } - mode := o.GetMode() - stack.Push(NewNumber(result, mode)) + + cool := coolMetric(o.metricRegistry) + stack.Push(NewNumber(result, o.GetMode(), cool)) + return nil +} + +// resultMetricForHyperAdd finds the appropriate result metric for add/subtract/modulo hyper operations. +// When Cool absorbs a non-Cool category, use the first non-Cool metric. +// When all are Cool, use Cool. +func resultMetricForHyperAdd(metrics []*Metric) *Metric { + for _, m := range metrics { + if m != nil && m.Category != Universal { + return m + } + } + return metrics[0] +} + +// validateSameCategory checks that all metrics belong to the same category. +// Cool (Universal) absorbs — it is compatible with any single non-Universal category. +// Returns an error if metrics span multiple non-Universal categories. +func validateSameCategory(metrics []*Metric, opName string) error { + var dominantCat Category = Universal + for _, m := range metrics { + if m == nil || m.Category == Universal { + continue + } + if dominantCat == Universal { + dominantCat = m.Category + } else if m.Category != dominantCat { + return fmt.Errorf("%s: incompatible metrics: mixed %s and %s categories", + opName, dominantCat, m.Category) + } + } return nil } diff --git a/internal/rpn/operations_hyper_test.go b/internal/rpn/operations_hyper_test.go new file mode 100644 index 0000000..700614f --- /dev/null +++ b/internal/rpn/operations_hyper_test.go @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "math" + "strconv" + "testing" +) + +func TestHyperAddMetricAware(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // Same category: [100Mbps 50Mbps 25Mbps [+] ] = 175Mbps + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("100Mbps 50Mbps 25Mbps [+]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 175-tolerance || resultVal > 175+tolerance { + t.Errorf("result = %g, want 175", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + mbps, _ := reg.Find("Mbps") + if m != mbps { + t.Errorf("metric = %v, want Mbps", m) + } +} + +func TestHyperAddCoolAbsorbing(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // 5 (Cool) 100Mbps 10Mbps [+] ≈ 110.000005Mbps + // Cool (factor 1.0) contributes 5 base units (bps) which is negligible + // Result metric is Mbps (first non-Cool), value ≈ 110Mbps + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("5 100Mbps 10Mbps [+]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 110-tolerance || resultVal > 110+tolerance { + t.Errorf("result = %g, want ~110", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + mbps, _ := reg.Find("Mbps") + if m != mbps { + t.Errorf("metric = %v, want Mbps", m) + } +} + +func TestHyperAddIncompatible(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("100Mbps 2hr [+]") + if err == nil { + t.Error("expected error for incompatible categories") + } +} + +func TestHyperAddMixedUnitsSameCategory(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // 1km 500m 100m [+] = 1600m converted back to km = 1.6km + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("1km 500m 100m [+]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 1.6-tolerance || resultVal > 1.6+tolerance { + t.Errorf("result = %g, want 1.6", 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 TestHyperMultiplyMetricResultIsCool(t *testing.T) { + reg := GetMetricRegistry() + + // 3 4 5 [*] = 60 with Cool metric + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("3 4 5 [*]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal != 60 { + t.Errorf("result = %g, want 60", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperMultiplyMixedMetricsIsCool(t *testing.T) { + reg := GetMetricRegistry() + + // 100Mbps 2hr [*] = 200 (raw product, Cool metric) + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("100Mbps 2hr [*]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal != 200 { + t.Errorf("result = %g, want 200", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperSubtractMetricAware(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // 1000Mbps 100Mbps 50Mbps [-] = 850Mbps + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("1000Mbps 100Mbps 50Mbps [-]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 850-tolerance || resultVal > 850+tolerance { + t.Errorf("result = %g, want 850", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + mbps, _ := reg.Find("Mbps") + if m != mbps { + t.Errorf("metric = %v, want Mbps", m) + } +} + +func TestHyperSubtractMixedUnits(t *testing.T) { + tolerance := 0.001 + + // 2km 500m 100m [-] = (2000 - 500 - 100)m = 1400m = 1.4km + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("2km 500m 100m [-]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 1.4-tolerance || resultVal > 1.4+tolerance { + t.Errorf("result = %g, want 1.4", resultVal) + } +} + +func TestHyperSubtractIncompatible(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("100km 2hr [-]") + if err == nil { + t.Error("expected error for incompatible categories") + } +} + +func TestHyperDivideResultIsCool(t *testing.T) { + reg := GetMetricRegistry() + + // 100 20 2 [/] = (100 / 20) / 2 = 2.5 with Cool metric + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("100 20 2 [/]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal != 2.5 { + t.Errorf("result = %g, want 2.5", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperPowerResultIsCool(t *testing.T) { + reg := GetMetricRegistry() + + // 2 3 2 [^] = (2^3)^2 = 64 with Cool metric + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("2 3 2 [^]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal != 64 { + t.Errorf("result = %g, want 64", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperModuloMetricAware(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // 10km 3km 2km [%] = ((10 % 3) % 2)km = (1 % 2)km = 1km + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("10km 3km 2km [%]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 1-tolerance || resultVal > 1+tolerance { + 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 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() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("1000m 300m 200m [%]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 100-tolerance || resultVal > 100+tolerance { + t.Errorf("result = %g, want 100", resultVal) + } +} + +func TestHyperModuloIncompatible(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("100km 2hr [%]") + if err == nil { + t.Error("expected error for incompatible categories") + } +} + +func TestHyperLog2CoolResult(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // [lg] on all values → sum of log2: log2(2) + log2(4) + log2(8) = 1 + 2 + 3 = 6 → Cool + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("2 4 8 [lg]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 6-tolerance || resultVal > 6+tolerance { + t.Errorf("result = %g, want 6", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperLog10CoolResult(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // [log] on all values → sum of log10: log10(10) + log10(100) = 1 + 2 = 3 → Cool + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("10 100 [log]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 3-tolerance || resultVal > 3+tolerance { + t.Errorf("result = %g, want 3", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperLnCoolResult(t *testing.T) { + reg := GetMetricRegistry() + tolerance := 0.001 + + // [ln] on all values → sum of ln: ln(e) + ln(e*e) = 1 + 2 = 3 → Cool + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("2.718281828459045 7.38905609893065 [ln]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 3-tolerance || resultVal > 3+tolerance { + t.Errorf("result = %g, want 3", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperLogWithMetrics(t *testing.T) { + reg := GetMetricRegistry() + + // [lg] with metrics still uses raw values and returns Cool + // log2(100) + log2(1000) ≈ 6.64 + 9.97 ≈ 16.61 + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("100Mbps 1000Mbps [lg]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + expected := math.Log2(100) + math.Log2(1000) + tolerance := 0.01 + if resultVal < expected-tolerance || resultVal > expected+tolerance { + t.Errorf("result = %g, want ~%g", resultVal, expected) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperAddAllCool(t *testing.T) { + reg := GetMetricRegistry() + + // 1 2 3 [+] with no metrics = 6 Cool + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("1 2 3 [+]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal != 6 { + t.Errorf("result = %g, want 6", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + cool, _ := reg.Find("Cool") + if m != cool { + t.Errorf("metric = %v, want Cool", m) + } +} + +func TestHyperModuloByZero(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("10km 0km [%]") + if err == nil { + t.Error("expected error for modulo by zero") + } +} + +func TestHyperDivideByZero(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("10 0 [/]") + if err == nil { + t.Error("expected error for division by zero") + } +} + +func TestHyperLogNonPositive(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("0 5 [lg]") + if err == nil { + t.Error("expected error for log2 of non-positive number") + } +} |
