1use num_rational::Rational32;
18use num_traits::{
19 ConstZero,
20 cast::{FromPrimitive, ToPrimitive},
21};
22use std::fmt::Display;
23
24#[derive(Clone, Copy, Debug, Default, PartialEq)]
25pub struct LengthInFeet {
26 feet: u32,
27 inches: f32,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub enum Units {
32 #[default]
33 Metric,
34 Imperial,
35}
36
37impl Display for LengthInFeet {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(
40 f,
41 "{}",
42 if f.alternate() {
43 if let Some((whole, fractional)) = self.inches_only_fractional() {
44 format!(
45 "{}′{}",
46 self.feet,
47 match (whole, fractional) {
48 (0, z) if z == Rational32::ZERO => String::default(),
49 (whole, z) if z == Rational32::ZERO => format!(" {whole}″"),
50 (0, fractional) => format!(" {fractional}″"),
51 (whole, fractional) => format!(" {whole} {fractional}″"),
52 }
53 )
54 } else {
55 format!("{}′ {}″", self.feet, self.inches)
56 }
57 } else {
58 format!(
59 "{}′{}",
60 self.feet,
61 if self.inches != f32::ZERO {
62 format!(" {}″", self.inches)
63 } else {
64 String::default()
65 }
66 )
67 }
68 )
69 }
70}
71
72impl PartialOrd for LengthInFeet {
73 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74 match self.feet.partial_cmp(&other.feet) {
75 Some(core::cmp::Ordering::Equal) => {}
76 ord => return ord,
77 }
78 self.inches.partial_cmp(&other.inches)
79 }
80}
81
82impl LengthInFeet {
83 pub fn new(feet: f64) -> Self {
84 let uint_feet = feet.floor() as u32;
85 let float_inches = feet.fract() as f32;
86 Self::feet_and_inches(uint_feet, float_inches * 12.0)
87 }
88
89 pub fn inches(inches: f64) -> Self {
90 let feet = (inches / 12.0) as u32;
91 let float_inches = inches.rem_euclid(12.0) as f32;
92 Self::feet_and_inches(feet, float_inches)
93 }
94
95 pub fn feet_and_inches(feet: u32, inches: f32) -> Self {
96 Self { feet, inches }
97 }
98 pub fn feet_and_inches_fractional(
99 feet: u32,
100 inches: u32,
101 fractional: Rational32,
102 ) -> Option<Self> {
103 (fractional + Rational32::from_u32(inches).unwrap())
104 .to_f32()
105 .map(|inches| Self::feet_and_inches(feet, inches))
106 }
107
108 pub fn feet_only(&self) -> u32 {
109 self.feet
110 }
111
112 pub fn inches_only_decimal(&self) -> f32 {
113 self.inches
114 }
115
116 pub fn inches_only_fractional(&self) -> Option<(u32, Rational32)> {
117 if let Some(fractional) = Rational32::from_f32(self.inches.fract()) {
118 let uint_inches = self.inches.floor() as u32;
119 Some((uint_inches, fractional))
120 } else {
121 None
122 }
123 }
124}
125
126#[cfg(test)]
127mod test {
128 use crate::non_si::LengthInFeet;
129
130 #[test]
131 fn test_construct_feet() {
132 assert_eq!(
133 LengthInFeet::new(0.0),
134 LengthInFeet {
135 feet: 0,
136 inches: 0.0
137 }
138 );
139 assert_eq!(LengthInFeet::new(0.0).to_string(), "0′".to_string());
140 assert_eq!(format!("{:#}", LengthInFeet::new(0.0)), "0′".to_string());
141
142 assert_eq!(
143 LengthInFeet::new(1.5),
144 LengthInFeet {
145 feet: 1,
146 inches: 6.0
147 }
148 );
149 assert_eq!(LengthInFeet::new(1.5).to_string(), "1′ 6″".to_string());
150 assert_eq!(format!("{:#}", LengthInFeet::new(1.5)), "1′ 6″".to_string());
151
152 assert_eq!(
153 LengthInFeet::new(2.55),
154 LengthInFeet {
155 feet: 2,
156 inches: 6.6000004
157 }
158 );
159 assert_eq!(
160 LengthInFeet::new(2.6875).to_string(),
161 "2′ 8.25″".to_string()
162 );
163 assert_eq!(
164 format!("{:#}", LengthInFeet::new(2.6875)),
165 "2′ 8 1/4″".to_string()
166 );
167 }
168}