1use core::fmt::{Display, Formatter};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub struct Ratio {
16 numerator: isize,
17 denominator: usize,
18}
19
20impl Ratio {
21 pub fn new(numerator: isize, denominator: usize) -> Self {
27 assert!(denominator != 0, "ratio denominator must not be zero");
28 let divisor = gcd(numerator.unsigned_abs(), denominator);
29 let reduced_mag = (numerator.unsigned_abs() / divisor) as isize;
30 Self {
31 numerator: if numerator < 0 {
32 reduced_mag.wrapping_neg()
33 } else {
34 reduced_mag
35 },
36 denominator: denominator / divisor,
37 }
38 }
39
40 pub fn numerator(self) -> isize {
42 self.numerator
43 }
44
45 pub fn denominator(self) -> usize {
47 self.denominator
48 }
49
50 pub fn as_f32(self) -> f32 {
52 self.numerator as f32 / self.denominator as f32
53 }
54
55 pub fn as_f64(self) -> f64 {
57 self.numerator as f64 / self.denominator as f64
58 }
59}
60
61impl Display for Ratio {
62 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
63 write!(f, "{}/{}", self.numerator, self.denominator)
64 }
65}
66
67fn gcd(a: usize, b: usize) -> usize {
68 let (mut a, mut b) = (a, b);
69 while b != 0 {
70 (a, b) = (b, a % b);
71 }
72 a
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use alloc::format;
79
80 #[test]
81 fn reduces_to_lowest_terms() {
82 let r = Ratio::new(2, 4);
83 assert_eq!(r.numerator(), 1);
84 assert_eq!(r.denominator(), 2);
85 }
86
87 #[test]
88 fn reduces_negative_to_lowest_terms() {
89 let r = Ratio::new(-2, 4);
90 assert_eq!(r.numerator(), -1);
91 assert_eq!(r.denominator(), 2);
92 }
93
94 #[test]
95 fn equal_fractions_are_equal() {
96 assert_eq!(Ratio::new(1, 2), Ratio::new(2, 4));
97 assert_eq!(Ratio::new(3, 9), Ratio::new(1, 3));
98 assert_eq!(Ratio::new(-1, 2), Ratio::new(-2, 4));
99 assert_eq!(Ratio::new(-3, 9), Ratio::new(-1, 3));
100 }
101
102 #[test]
103 fn zero_numerator_reduces_to_zero_over_one() {
104 let r = Ratio::new(0, 5);
105 assert_eq!(r.numerator(), 0);
106 assert_eq!(r.denominator(), 1);
107 }
108
109 #[test]
110 fn handles_isize_min_without_overflow() {
111 let r = Ratio::new(isize::MIN, 2);
112 assert_eq!(r.numerator(), isize::MIN / 2);
113 assert_eq!(r.denominator(), 1);
114
115 let r2 = Ratio::new(isize::MIN, 1);
116 assert_eq!(r2.numerator(), isize::MIN);
117 assert_eq!(r2.denominator(), 1);
118
119 let r3 = Ratio::new(isize::MIN, isize::MIN.unsigned_abs());
120 assert_eq!(r3.numerator(), -1);
121 assert_eq!(r3.denominator(), 1);
122 }
123
124 #[test]
125 #[should_panic(expected = "denominator must not be zero")]
126 fn zero_denominator_panics() {
127 Ratio::new(1, 0);
128 }
129
130 #[test]
131 fn conversions_are_accurate() {
132 let r = Ratio::new(1, 4);
133 assert_eq!(r.as_f32(), 0.25);
134 assert_eq!(r.as_f64(), 0.25);
135
136 let neg_r = Ratio::new(-1, 4);
137 assert_eq!(neg_r.as_f32(), -0.25);
138 assert_eq!(neg_r.as_f64(), -0.25);
139 }
140
141 #[test]
142 fn display_shows_reduced_form() {
143 let r = Ratio::new(6, 8);
144 assert_eq!(format!("{}", r), "3/4");
145
146 let neg_r = Ratio::new(-6, 8);
147 assert_eq!(format!("{}", neg_r), "-3/4");
148 }
149}