summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 13:00:45 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 13:00:45 +0300
commitd0cb0dfad408553f4e90e12ea4b235b912885dd1 (patch)
treef85cbb78f7b59fc7668be75e52d75f392f4ed16a
parent09b3a1e5b09bd136afa348efcaf53dc4e4189a27 (diff)
refactor(rpn): deduplicate category helpers between metric and hyper ops
Extract validateCategories and resultMetricForAdd as shared N-ary helpers in operations_metric.go. Update binary helpers (categoriesCompatible, compatibleMetric) to delegate to them. Remove the duplicated validateSameCategory and resultMetricForHyperAdd from operations_hyper.go and switch all callers to the shared versions.
-rw-r--r--internal/rpn/operations_hyper.go48
-rw-r--r--internal/rpn/operations_metric.go65
2 files changed, 44 insertions, 69 deletions
diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go
index cd4e546..c6c4beb 100644
--- a/internal/rpn/operations_hyper.go
+++ b/internal/rpn/operations_hyper.go
@@ -26,12 +26,12 @@ func (o *Operations) HyperAdd(stack *Stack) error {
}
// Validate all are compatible (all same category, or Cool absorbs)
- if err := validateSameCategory(metrics, "[+]"); err != nil {
+ if err := validateCategories(metrics, "[+]"); err != nil {
return err
}
// Result metric: first non-Cool metric (Cool absorbs), or Cool
- resultMetric := resultMetricForHyperAdd(metrics)
+ resultMetric := resultMetricForAdd(metrics)
// Convert all to base units, sum, convert back
pm := o.GetPrefixMode()
@@ -92,12 +92,12 @@ func (o *Operations) HyperSubtract(stack *Stack) error {
}
// Validate all are compatible (all same category, or Cool absorbs)
- if err := validateSameCategory(metrics, "[-]"); err != nil {
+ if err := validateCategories(metrics, "[-]"); err != nil {
return err
}
// Result metric: first non-Cool metric (Cool absorbs), or Cool
- resultMetric := resultMetricForHyperAdd(metrics)
+ resultMetric := resultMetricForAdd(metrics)
// Convert all to base units, subtract, convert back
pm := o.GetPrefixMode()
@@ -191,12 +191,12 @@ func (o *Operations) HyperModulo(stack *Stack) error {
}
// Validate all are compatible (all same category, or Cool absorbs)
- if err := validateSameCategory(metrics, "[%]"); err != nil {
+ if err := validateCategories(metrics, "[%]"); err != nil {
return err
}
// Result metric: first non-Cool metric (Cool absorbs), or Cool
- resultMetric := resultMetricForHyperAdd(metrics)
+ resultMetric := resultMetricForAdd(metrics)
// Convert all to base units, compute modulo, convert back
pm := o.GetPrefixMode()
@@ -297,40 +297,4 @@ func (o *Operations) HyperLn(stack *Stack) error {
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
- }
- }
- // All Universal (Cool) or empty slice — default to Cool.
- // In practice, metrics is never empty (popAll enforces >= 2 operands),
- // but we handle it defensively.
- if len(metrics) > 0 && metrics[0] != nil {
- return metrics[0]
- }
- m, _ := GetMetricRegistry().Find("Cool")
- return m
-}
-// 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_metric.go b/internal/rpn/operations_metric.go
index c5f60d2..c3e8326 100644
--- a/internal/rpn/operations_metric.go
+++ b/internal/rpn/operations_metric.go
@@ -14,39 +14,50 @@ func resolveMetric(reg *MetricRegistry, n Number) *Metric {
return m
}
-// categoriesCompatible checks if two metrics are compatible for arithmetic.
-// Cool (Universal) is compatible with anything. Same category is compatible.
-func categoriesCompatible(a, b *Metric) bool {
- if a == nil || b == nil {
- return true
+// validateCategories checks that all metrics belong to the same category.
+// Cool (Universal) absorbs — it is compatible with any single non-Universal category.
+// Returns nil if compatible, error if metrics span multiple non-Universal categories.
+// Works for both binary (2 metrics) and N-ary (slice) cases.
+func validateCategories(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)
+ }
}
- if a.Category == Universal || b.Category == Universal {
- return true
+ return nil
+}
+
+// resultMetricForAdd finds the appropriate result metric for add/subtract/modulo operations.
+// When Cool absorbs a non-Cool category, use the first non-Cool metric.
+// When all are Cool, use Cool.
+func resultMetricForAdd(metrics []*Metric) *Metric {
+ for _, m := range metrics {
+ if m != nil && m.Category != Universal {
+ return m
+ }
+ }
+ if len(metrics) > 0 && metrics[0] != nil {
+ return metrics[0]
}
- return a.Category == b.Category
+ m, _ := GetMetricRegistry().Find("Cool")
+ return m
+}
+
+// categoriesCompatible checks if two metrics are compatible for arithmetic.
+func categoriesCompatible(a, b *Metric) bool {
+ return validateCategories([]*Metric{a, b}, "") == nil
}
// compatibleMetric returns the resulting metric for + and - operations.
-// Cool absorbs: if either is Cool, result is the other's metric (or Cool if both).
-// Same category: result uses left operand's metric.
func compatibleMetric(reg *MetricRegistry, a, b *Metric) *Metric {
- if a == nil {
- a = coolMetric(reg)
- }
- if b == nil {
- b = coolMetric(reg)
- }
- if a.Category == Universal && b.Category == Universal {
- return a // Cool
- }
- if a.Category == Universal {
- return b
- }
- if b.Category == Universal {
- return a
- }
- // Same category: use left operand's metric
- return a
+ return resultMetricForAdd([]*Metric{a, b})
}
// convertToBase converts a Number's value to its metric's base unit.