ratio_test.go raw
1 package ratio
2
3 import (
4 "encoding/json"
5 "testing"
6 )
7
8 func TestNew(t *testing.T) {
9 tests := []struct {
10 num, denom int64
11 wantNum int64
12 wantDenom int64
13 }{
14 {6, 4, 3, 2},
15 {-6, 4, -3, 2},
16 {6, -4, -3, 2},
17 {-6, -4, 3, 2},
18 {0, 5, 0, 1},
19 {7, 1, 7, 1},
20 {100, 100, 1, 1},
21 {15, 100, 3, 20},
22 {5, 100, 1, 20},
23 {80, 100, 4, 5},
24 }
25 for _, tt := range tests {
26 r := New(tt.num, tt.denom)
27 if r.Num != tt.wantNum || r.Denom != tt.wantDenom {
28 t.Errorf("New(%d, %d) = %d/%d, want %d/%d",
29 tt.num, tt.denom, r.Num, r.Denom, tt.wantNum, tt.wantDenom)
30 }
31 }
32 }
33
34 func TestNewPanicsOnZeroDenom(t *testing.T) {
35 defer func() {
36 if r := recover(); r == nil {
37 t.Error("New(1, 0) did not panic")
38 }
39 }()
40 New(1, 0)
41 }
42
43 func TestFromInt(t *testing.T) {
44 r := FromInt(42)
45 if r.Num != 42 || r.Denom != 1 {
46 t.Errorf("FromInt(42) = %d/%d, want 42/1", r.Num, r.Denom)
47 }
48 }
49
50 func TestAdd(t *testing.T) {
51 tests := []struct {
52 a, b Ratio
53 want Ratio
54 }{
55 {New(1, 2), New(1, 3), New(5, 6)},
56 {New(1, 4), New(1, 4), New(1, 2)},
57 {New(1, 2), Zero, New(1, 2)},
58 {New(1, 2), New(-1, 2), Zero},
59 {New(3, 7), New(2, 7), New(5, 7)},
60 }
61 for _, tt := range tests {
62 got := tt.a.Add(tt.b)
63 if !got.Equal(tt.want) {
64 t.Errorf("%s + %s = %s, want %s", tt.a, tt.b, got, tt.want)
65 }
66 }
67 }
68
69 func TestSub(t *testing.T) {
70 tests := []struct {
71 a, b Ratio
72 want Ratio
73 }{
74 {New(1, 2), New(1, 3), New(1, 6)},
75 {New(1, 2), New(1, 2), Zero},
76 {New(1, 4), New(3, 4), New(-1, 2)},
77 }
78 for _, tt := range tests {
79 got := tt.a.Sub(tt.b)
80 if !got.Equal(tt.want) {
81 t.Errorf("%s - %s = %s, want %s", tt.a, tt.b, got, tt.want)
82 }
83 }
84 }
85
86 func TestMul(t *testing.T) {
87 tests := []struct {
88 a, b Ratio
89 want Ratio
90 }{
91 {New(2, 3), New(3, 4), New(1, 2)},
92 {New(1, 2), One, New(1, 2)},
93 {New(5, 7), Zero, Zero},
94 {New(-1, 3), New(-1, 3), New(1, 9)},
95 {New(3, 20), New(7, 10), New(21, 200)},
96 }
97 for _, tt := range tests {
98 got := tt.a.Mul(tt.b)
99 if !got.Equal(tt.want) {
100 t.Errorf("%s * %s = %s, want %s", tt.a, tt.b, got, tt.want)
101 }
102 }
103 }
104
105 func TestDiv(t *testing.T) {
106 tests := []struct {
107 a, b Ratio
108 want Ratio
109 }{
110 {New(1, 2), New(1, 3), New(3, 2)},
111 {One, New(2, 1), New(1, 2)},
112 {New(3, 4), One, New(3, 4)},
113 }
114 for _, tt := range tests {
115 got := tt.a.Div(tt.b)
116 if !got.Equal(tt.want) {
117 t.Errorf("%s / %s = %s, want %s", tt.a, tt.b, got, tt.want)
118 }
119 }
120 }
121
122 func TestDivPanicsOnZero(t *testing.T) {
123 defer func() {
124 if r := recover(); r == nil {
125 t.Error("Div by zero did not panic")
126 }
127 }()
128 One.Div(Zero)
129 }
130
131 func TestLess(t *testing.T) {
132 tests := []struct {
133 a, b Ratio
134 want bool
135 }{
136 {New(1, 3), New(1, 2), true},
137 {New(1, 2), New(1, 3), false},
138 {New(1, 2), New(1, 2), false},
139 {New(-1, 2), New(1, 2), true},
140 {Zero, New(1, 1000), true},
141 }
142 for _, tt := range tests {
143 got := tt.a.Less(tt.b)
144 if got != tt.want {
145 t.Errorf("%s < %s = %v, want %v", tt.a, tt.b, got, tt.want)
146 }
147 }
148 }
149
150 func TestEqual(t *testing.T) {
151 if !New(2, 4).Equal(New(1, 2)) {
152 t.Error("2/4 should equal 1/2")
153 }
154 if !New(15, 100).Equal(New(3, 20)) {
155 t.Error("15/100 should equal 3/20")
156 }
157 if New(1, 2).Equal(New(1, 3)) {
158 t.Error("1/2 should not equal 1/3")
159 }
160 }
161
162 func TestScaleInt(t *testing.T) {
163 tests := []struct {
164 r Ratio
165 n int64
166 want int64
167 }{
168 {New(1, 2), 100, 50},
169 {New(1, 3), 100, 33},
170 {New(2, 3), 99, 66},
171 {One, 42, 42},
172 {Zero, 100, 0},
173 {New(3, 20), 1000, 150},
174 }
175 for _, tt := range tests {
176 got := tt.r.ScaleInt(tt.n)
177 if got != tt.want {
178 t.Errorf("%s.ScaleInt(%d) = %d, want %d", tt.r, tt.n, got, tt.want)
179 }
180 }
181 }
182
183 func TestFloat64(t *testing.T) {
184 tests := []struct {
185 r Ratio
186 want float64
187 }{
188 {New(1, 2), 0.5},
189 {One, 1.0},
190 {Zero, 0.0},
191 {New(1, 4), 0.25},
192 }
193 for _, tt := range tests {
194 got := tt.r.Float64()
195 if got != tt.want {
196 t.Errorf("%s.Float64() = %f, want %f", tt.r, got, tt.want)
197 }
198 }
199 }
200
201 func TestNeg(t *testing.T) {
202 r := New(3, 4)
203 neg := r.Neg()
204 if neg.Num != -3 || neg.Denom != 4 {
205 t.Errorf("Neg(3/4) = %s, want -3/4", neg)
206 }
207 if !neg.Neg().Equal(r) {
208 t.Error("double negation should be identity")
209 }
210 }
211
212 func TestAbs(t *testing.T) {
213 if !New(-3, 4).Abs().Equal(New(3, 4)) {
214 t.Error("Abs(-3/4) should be 3/4")
215 }
216 if !New(3, 4).Abs().Equal(New(3, 4)) {
217 t.Error("Abs(3/4) should be 3/4")
218 }
219 }
220
221 func TestClamp(t *testing.T) {
222 lo := New(1, 5) // 0.2
223 hi := New(9, 10) // 0.9
224
225 below := New(1, 10) // 0.1
226 if !below.Clamp(lo, hi).Equal(lo) {
227 t.Error("0.1 clamped to [0.2, 0.9] should be 0.2")
228 }
229
230 above := One
231 if !above.Clamp(lo, hi).Equal(hi) {
232 t.Error("1.0 clamped to [0.2, 0.9] should be 0.9")
233 }
234
235 mid := Half
236 if !mid.Clamp(lo, hi).Equal(Half) {
237 t.Error("0.5 clamped to [0.2, 0.9] should be 0.5")
238 }
239 }
240
241 func TestMaxMin(t *testing.T) {
242 a := New(1, 3)
243 b := New(1, 2)
244 if !Max(a, b).Equal(b) {
245 t.Error("Max(1/3, 1/2) should be 1/2")
246 }
247 if !Min(a, b).Equal(a) {
248 t.Error("Min(1/3, 1/2) should be 1/3")
249 }
250 }
251
252 func TestJSONRoundTrip(t *testing.T) {
253 original := New(3, 20)
254 data, err := json.Marshal(original)
255 if err != nil {
256 t.Fatal(err)
257 }
258
259 var decoded Ratio
260 if err := json.Unmarshal(data, &decoded); err != nil {
261 t.Fatal(err)
262 }
263
264 if !decoded.Equal(original) {
265 t.Errorf("JSON round-trip: %s != %s", decoded, original)
266 }
267
268 // Re-serialize and verify byte-identical output.
269 data2, _ := json.Marshal(decoded)
270 if string(data) != string(data2) {
271 t.Errorf("JSON not deterministic:\n %s\n %s", data, data2)
272 }
273 }
274
275 func TestFitnessWeights(t *testing.T) {
276 // Verify 3/20 + 1/20 + 16/20 = 1
277 w := New(3, 20).Add(New(1, 20)).Add(New(16, 20))
278 if !w.Equal(One) {
279 t.Errorf("fitness weights sum to %s, want 1/1", w)
280 }
281 }
282
283 func TestComputeFitness(t *testing.T) {
284 // Source=1, Binary=1, Behav=1 → Overall should be 1
285 s := New(3, 20).Mul(One).Add(New(1, 20).Mul(One)).Add(New(16, 20).Mul(One))
286 if !s.Equal(One) {
287 t.Errorf("perfect fitness = %s, want 1/1", s)
288 }
289
290 // Source=1/2, Binary=0, Behav=1 → 3/40 + 0 + 16/20 = 3/40 + 32/40 = 35/40 = 7/8
291 s2 := New(3, 20).Mul(Half).Add(New(1, 20).Mul(Zero)).Add(New(16, 20).Mul(One))
292 if !s2.Equal(New(7, 8)) {
293 t.Errorf("mixed fitness = %s, want 7/8", s2)
294 }
295 }
296