summaryrefslogtreecommitdiff
path: root/internal/rpn/operations_hyper.go
blob: ce73d7abd05481ab444688eb705839470e5c5b87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Paul Buetow

package rpn

import (
	"errors"
	"fmt"
	"math"
)

// nAryMetricOp handles metric-aware n-ary operations.
// Resolves metrics, validates categories, converts to base units,
// applies fn left-associatively, converts back, and pushes the result.
// If validate is non-nil, it's called on each converted base value before
// the binary fn; a non-nil error stops processing.
func (o *Operations) nAryMetricOp(stack *Stack, opName string, fn func(float64, float64) float64, validate func(int, float64) error) error {
	values, err := popAll(stack, opName)
	if err != nil {
		return err
	}
	if len(values) == 0 {
		return buildError(opName, fmt.Errorf("need at least one operand"))
	}

	metrics := make([]*Metric, len(values))
	for i, v := range values {
		m, err := resolveMetric(o.metricRegistry, v)
		if err != nil {
			return buildError(opName, err)
		}
		metrics[i] = m
	}

	if err := validateCategories(metrics, opName); err != nil {
		return err
	}

	resultMetric := resultMetricForAdd(metrics)
	pm := o.GetPrefixMode()

	firstBase, err := convertToBase(o.metricRegistry, values[0], pm, resultMetric)
	if err != nil {
		return buildError(opName, fmt.Errorf("operand 0: %w", err))
	}
	result := firstBase
	for i := 1; i < len(values); i++ {
		base, err := convertToBase(o.metricRegistry, values[i], pm, resultMetric)
		if err != nil {
			return buildError(opName, fmt.Errorf("operand %d: %w", i, err))
		}
		if validate != nil {
			if err := validate(i, base); err != nil {
				return err
			}
		}
		result = fn(result, base)
	}

	resultVal, err := convertFromBase(o.metricRegistry, result, resultMetric, pm)
	if err != nil {
		return buildError(opName, err)
	}

	stack.Push(NewNumberWithMetric(resultVal, o.GetMode(), resultMetric))
	return nil
}

// nAryScalarOp applies a binary function to pre-converted float64 values
// and pushes the result with Cool metric.
func (o *Operations) nAryScalarOp(stack *Stack, opName string, floats []float64, fn func(float64, float64) float64) error {
	if len(floats) == 0 {
		return buildError(opName, fmt.Errorf("need at least one operand"))
	}
	result := floats[0]
	for i := 1; i < len(floats); i++ {
		result = fn(result, floats[i])
	}

	cool, err := coolMetric(o.metricRegistry)
	if err != nil {
		return buildError(opName, err)
	}
	stack.Push(NewNumberWithMetric(result, o.GetMode(), cool))
	return nil
}

// HyperAdd pops all values from stack, adds 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 non-Cool metric (or Cool if all are Cool).
func (o *Operations) HyperAdd(stack *Stack) error {
	return o.nAryMetricOp(stack, "[+]", func(a, b float64) float64 { return a + b }, 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 {
	values, err := popAll(stack, "[*]")
	if err != nil {
		return err
	}

	floats := make([]float64, len(values))
	for i, v := range values {
		val, err := toFloat64(v, "[*]")
		if err != nil {
			return buildError("[*]", fmt.Errorf("operand %d: %w", i, err))
		}
		floats[i] = val
	}

	return o.nAryScalarOp(stack, "[*]", floats, func(a, b float64) float64 { return a * b })
}

// 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 non-Cool metric (or Cool if all are Cool).
func (o *Operations) HyperSubtract(stack *Stack) error {
	return o.nAryMetricOp(stack, "[-]", func(a, b float64) float64 { return a - b }, 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 {
	values, err := popAll(stack, "[/]")
	if err != nil {
		return err
	}

	floats := make([]float64, len(values))
	for i, v := range values {
		val, err := toFloat64(v, "[/]")
		if err != nil {
			return buildError("[/]", fmt.Errorf("operand %d: %w", i, err))
		}
		if i > 0 && val == 0 {
			return buildError("[/]", fmt.Errorf("division by zero at operand %d", i))
		}
		floats[i] = val
	}

	return o.nAryScalarOp(stack, "[/]", floats, func(a, b float64) float64 { return a / b })
}

// 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 {
	values, err := popAll(stack, "[^]")
	if err != nil {
		return err
	}

	floats := make([]float64, len(values))
	for i, v := range values {
		val, err := toFloat64(v, "[^]")
		if err != nil {
			return buildError("[^]", fmt.Errorf("operand %d: %w", i, err))
		}
		floats[i] = val
	}

	return o.nAryScalarOp(stack, "[^]", floats, math.Pow)
}

// 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 non-Cool metric (or Cool if all are Cool).
func (o *Operations) HyperModulo(stack *Stack) error {
	return o.nAryMetricOp(stack, "[%]", math.Mod, func(i int, base float64) error {
		if base == 0 {
			return buildError("[%]", fmt.Errorf("modulo by zero at operand %d", i))
		}
		return nil
	})
}

// hyperLog computes the sum of a log function over all stack values.
// Each value must be positive. Result is pushed with Cool metric.
func (o *Operations) hyperLog(stack *Stack, opName string, logFn func(float64) float64, errMsg string) error {
	values, err := popAll(stack, opName)
	if err != nil {
		return err
	}

	var result float64
	for i, v := range values {
		val, err := toFloat64(v, opName)
		if err != nil {
			return buildError(opName, fmt.Errorf("operand %d: %w", i, err))
		}
		if val <= 0 {
			return buildError(opName, errors.New(errMsg))
		}
		result += logFn(val)
	}

	cool, err := coolMetric(o.metricRegistry)
	if err != nil {
		return buildError(opName, err)
	}
	stack.Push(NewNumberWithMetric(result, o.GetMode(), cool))
	return nil
}

// HyperLog2 pops all values from stack, computes sum of log2 for all values, and pushes result.
// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLog2(stack *Stack) error {
	return o.hyperLog(stack, "[lg]", math.Log2, "log2 undefined for non-positive numbers")
}

// HyperLog10 pops all values from stack, computes sum of log10 for all values, and pushes result.
// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLog10(stack *Stack) error {
	return o.hyperLog(stack, "[log]", math.Log10, "log10 undefined for non-positive numbers")
}

// HyperLn pops all values from stack, computes sum of natural log for all values, and pushes result.
// No metric validation; uses raw float64 values. Result is always Cool (unitless).
func (o *Operations) HyperLn(stack *Stack) error {
	return o.hyperLog(stack, "[ln]", math.Log, "ln undefined for non-positive numbers")
}