1 module gfm.math.rational;
2 
3 import std.traits,
4        std.string;
5 
6 /**
7   
8   A rational number, always in reduced form. Supports basic arithmetic.
9 
10   Bugs: Remove this module once std.rational is here.
11 
12  */
13 align(1) struct Rational
14 {
15     align(1):    
16     public
17     {
18         long num;   /// Numerator.
19         long denom; /// Denominator.
20 
21         /// Construct a Rational from an integer.
22         this(long n) pure nothrow
23         {
24             opAssign!long(n);
25         }
26 
27         /// Construct a Rational from another Rational.
28         this(Rational f) pure nothrow
29         {
30             opAssign!Rational(f);
31         }
32 
33         /// Construct a Rational from numerator and denominator.
34         this(long numerator, long denominator) pure nothrow
35         {
36             num = numerator;
37             denom = denominator;
38             reduce();
39         }
40 
41         /// Converts to pretty string.
42         string toString() const
43         {
44             return format("%s/%s", num, denom);
45         }
46 
47         /// Cast to floating point.
48         T opCast(T)() pure const nothrow if (isFloatingPoint!T)
49         {
50             return cast(T)num / cast(T)denom;
51         }
52 
53         /// Assign with another Rational.
54         ref Rational opAssign(T)(T other) pure nothrow if (is(Unqual!T == Rational))
55         {
56             num = other.num;
57             denom = other.denom;
58             return this;
59         }
60 
61         /// Assign with an integer.
62         ref Rational opAssign(T)(T n) pure nothrow if (isIntegral!T)
63         {
64             num = n;
65             denom = 1;
66             return this;
67         }
68 
69         Rational opBinary(string op, T)(T o) pure const nothrow
70         {
71             Rational r = this;
72             Rational y = o;
73             return r.opOpAssign!(op)(y);
74         }
75 
76         ref Rational opOpAssign(string op, T)(T o) pure nothrow if (!is(Unqual!T == Rational))
77         {
78             const(self) o = y;
79             return opOpAssign!(op)(o);
80         }
81 
82         ref Rational opOpAssign(string op, T)(T o) pure nothrow if (is(Unqual!T == Rational))
83         {
84             static if (op == "+")
85             {
86                 num = num * o.denom + o.num * denom;
87                 denom = denom * o.denom;
88                 reduce();
89             }
90             else static if (op == "-")
91             {
92                 opOpAssign!"+"(-o);
93             }
94             else static if (op == "*")
95             {
96                 denom = denom * o.denom;
97                 num = num * o.num;
98                 reduce();
99             }
100             else static if (op == "/")
101             {
102                 opOpAssign!"*"(o.inverse());
103             }
104             else
105             {
106                 static assert(false, "unsupported operation '" ~ op ~ "'");
107             }
108             return this;
109         }
110 
111         // const unary operations
112         Rational opUnary(string op)() pure const nothrow if (op == "+" || op == "-")
113         {
114             static if (op == "-")
115             {
116                 Rational f = this;
117                 f.num = -f.num;
118                 return f;
119             }
120             else static if (op == "+")
121                 return this;
122         }
123 
124         // non-const unary operations
125         Rational opUnary(string op)() pure nothrow if (op=="++" || op=="--")
126         {
127             static if (op=="++")
128             {
129                 num += denom;
130                 debug checkInvariant(); // should still be reduced
131             }
132             else static if (op=="--")
133             {
134                 num -= denom;
135                 debug checkInvariant(); // should still be reduced
136             }
137             return this;
138         }
139 
140         bool opEquals(T)(T y) pure const if (!is(Unqual!T == Rational))
141         {
142             return this == Rational(y);
143         }
144 
145         bool opEquals(T)(T o) pure const if (is(Unqual!T == Rational))
146         {
147             // invariants ensures two equal Rationals have equal representations
148             return num == o.num && denom == o.denom;
149         }
150 
151         int opCmp(T)(T o) pure const if (!is(Unqual!T == Rational))
152         {
153             return opCmp(Rational(o));
154         }
155 
156         int opCmp(T)(T o) pure const if (is(Unqual!T == Rational))
157         {
158             assert(denom > 0);
159             assert(o.denom > 0);
160             long det = num * o.denom - denom * o.num;
161             if (det > 0)
162                 return 1;
163             else if (det < 0)
164                 return -1;
165             else
166                 return 0;
167         }
168 
169         /// Returns: Inverse of this rational number.
170         Rational inverse() pure const nothrow
171         {
172             return Rational(denom, num);
173         }
174     }
175 
176     private
177     {
178         // FIXME: be consistent with regards to sign
179         static long GCD(long a, long b) pure nothrow
180         {
181             if (b == 0)
182                 return a;
183             else
184                 return GCD(b, a % b);
185         }
186 
187         void reduce() pure nothrow
188         {
189             const(long) gcd = GCD(num, denom);
190             num /= gcd;
191             denom /= gcd;
192             if (denom < 0)
193             {
194                 num = -num;
195                 denom = -denom;
196             }
197             debug checkInvariant();
198         }
199 
200         void checkInvariant() pure nothrow // can't do this in invariant() because of opAssign
201         {
202             assert(denom > 0);
203             auto gcd = GCD(num, denom);
204             assert(gcd == 1 || gcd == -1);
205         }
206     }
207 }
208 
209 unittest
210 {
211     Rational x = Rational(9, 3);
212     assert(x.num == 3);
213     assert(x.denom == 1);
214 
215     assert(x < 4);
216     assert(x > 2);
217     assert(x > Rational(8,3));
218     assert(x > Rational(-8,3));
219     assert(x == Rational(-27, -9));
220 
221     assert(Rational(-4, 7) + 2 == Rational(10, 7));
222     assert(Rational(-4, 7) == Rational(10, 7) - 2);
223 
224     assert(++Rational(3,7) == Rational(10,7));
225     assert(--Rational(3,7) == Rational(-4,7));
226     assert(+x == 3);
227     assert(-x == -3);
228 
229     Rational y = -4;
230     assert(y.num == -4);
231     assert(y.denom == 1);
232 
233     assert(Rational(2, 3) * Rational(15, 7) == Rational(10, 7));
234     assert(Rational(2, 3) == Rational(10, 7) / Rational(15, 7));
235 }