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