From d0cb0dfad408553f4e90e12ea4b235b912885dd1 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 22 May 2026 13:00:45 +0300 Subject: 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. --- internal/rpn/operations_hyper.go | 48 ++++------------------------- internal/rpn/operations_metric.go | 65 +++++++++++++++++++++++---------------- 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. -- cgit v1.2.3