ratio.go raw
1 // Package ratio implements exact rational arithmetic using integer
2 // numerator/denominator pairs. All values are stored in canonical form:
3 // GCD-reduced with positive denominator. This guarantees deterministic
4 // JSON serialization — two Ratios representing the same value always
5 // produce identical bytes.
6 package ratio
7
8 import "fmt"
9
10 // Ratio is an exact rational number Num/Denom.
11 // The zero value (0/0) is not valid; use Zero instead.
12 // All constructors and arithmetic operations return normalized values.
13 type Ratio struct {
14 Num int64 `json:"num"`
15 Denom int64 `json:"denom"`
16 }
17
18 // Predefined constants.
19 var (
20 Zero = Ratio{0, 1}
21 One = Ratio{1, 1}
22 Half = Ratio{1, 2}
23 )
24
25 // New creates a normalized Ratio. Panics if denom is zero.
26 func New(num, denom int64) Ratio {
27 if denom == 0 {
28 panic("ratio: zero denominator")
29 }
30 return normalize(num, denom)
31 }
32
33 // FromInt returns n/1.
34 func FromInt(n int64) Ratio {
35 return Ratio{n, 1}
36 }
37
38 // normalize reduces num/denom by GCD and ensures positive denominator.
39 func normalize(num, denom int64) Ratio {
40 if num == 0 {
41 return Ratio{0, 1}
42 }
43 if denom < 0 {
44 num = -num
45 denom = -denom
46 }
47 g := gcd(abs(num), denom)
48 return Ratio{num / g, denom / g}
49 }
50
51 // Add returns r + other.
52 func (r Ratio) Add(other Ratio) Ratio {
53 return normalize(
54 r.Num*other.Denom+other.Num*r.Denom,
55 r.Denom*other.Denom,
56 )
57 }
58
59 // Sub returns r - other.
60 func (r Ratio) Sub(other Ratio) Ratio {
61 return normalize(
62 r.Num*other.Denom-other.Num*r.Denom,
63 r.Denom*other.Denom,
64 )
65 }
66
67 // Mul returns r * other.
68 func (r Ratio) Mul(other Ratio) Ratio {
69 return normalize(
70 r.Num*other.Num,
71 r.Denom*other.Denom,
72 )
73 }
74
75 // Div returns r / other. Panics if other is zero.
76 func (r Ratio) Div(other Ratio) Ratio {
77 if other.Num == 0 {
78 panic("ratio: division by zero")
79 }
80 return normalize(
81 r.Num*other.Denom,
82 r.Denom*other.Num,
83 )
84 }
85
86 // Neg returns -r.
87 func (r Ratio) Neg() Ratio {
88 return Ratio{-r.Num, r.Denom}
89 }
90
91 // Abs returns |r|.
92 func (r Ratio) Abs() Ratio {
93 if r.Num < 0 {
94 return Ratio{-r.Num, r.Denom}
95 }
96 return r
97 }
98
99 // Less returns true if r < other.
100 // Safe because denominators are always positive after normalization.
101 func (r Ratio) Less(other Ratio) bool {
102 return r.Num*other.Denom < other.Num*r.Denom
103 }
104
105 // LessEq returns true if r <= other.
106 func (r Ratio) LessEq(other Ratio) bool {
107 return r.Num*other.Denom <= other.Num*r.Denom
108 }
109
110 // Greater returns true if r > other.
111 func (r Ratio) Greater(other Ratio) bool {
112 return other.Less(r)
113 }
114
115 // Equal returns true if r == other.
116 // Both must be normalized for this to work (they always are).
117 func (r Ratio) Equal(other Ratio) bool {
118 return r.Num == other.Num && r.Denom == other.Denom
119 }
120
121 // IsZero returns true if r == 0.
122 func (r Ratio) IsZero() bool {
123 return r.Num == 0
124 }
125
126 // IsPositive returns true if r > 0.
127 func (r Ratio) IsPositive() bool {
128 return r.Num > 0
129 }
130
131 // IsNegative returns true if r < 0.
132 func (r Ratio) IsNegative() bool {
133 return r.Num < 0
134 }
135
136 // Float64 converts to float64 for display purposes only.
137 // Never use the result for computation.
138 func (r Ratio) Float64() float64 {
139 if r.Denom == 0 {
140 return 0
141 }
142 return float64(r.Num) / float64(r.Denom)
143 }
144
145 // ScaleInt computes (n * Num) / Denom using integer arithmetic.
146 // This replaces patterns like int(float64(n) * proportion).
147 func (r Ratio) ScaleInt(n int64) int64 {
148 return (n * r.Num) / r.Denom
149 }
150
151 // Max returns the larger of r and other.
152 func Max(a, b Ratio) Ratio {
153 if b.Less(a) {
154 return a
155 }
156 return b
157 }
158
159 // Min returns the smaller of r and other.
160 func Min(a, b Ratio) Ratio {
161 if a.Less(b) {
162 return a
163 }
164 return b
165 }
166
167 // Clamp returns r clamped to [lo, hi].
168 func (r Ratio) Clamp(lo, hi Ratio) Ratio {
169 if r.Less(lo) {
170 return lo
171 }
172 if hi.Less(r) {
173 return hi
174 }
175 return r
176 }
177
178 // String returns "Num/Denom" for debugging.
179 func (r Ratio) String() string {
180 return fmt.Sprintf("%d/%d", r.Num, r.Denom)
181 }
182
183 // gcd returns the greatest common divisor of a and b.
184 // Both arguments must be non-negative.
185 func gcd(a, b int64) int64 {
186 for b != 0 {
187 a, b = b, a%b
188 }
189 return a
190 }
191
192 // abs returns the absolute value of n.
193 func abs(n int64) int64 {
194 if n < 0 {
195 return -n
196 }
197 return n
198 }
199