summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/rpn/operations_hyper.go347
-rw-r--r--internal/rpn/operations_hyper_test.go418
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")
+ }
+}