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