/home/a220/proj/radnelac/src/clock/time_of_day.rs
Line | Count | Source |
1 | | // This Source Code Form is subject to the terms of the Mozilla Public |
2 | | // License, v. 2.0. If a copy of the MPL was not distributed with this |
3 | | // file, You can obtain one at https://mozilla.org/MPL/2.0/. |
4 | | |
5 | | use crate::common::math::TermNum; |
6 | | use crate::day_count::BoundedDayCount; |
7 | | use crate::day_count::Fixed; |
8 | | use crate::day_count::FromFixed; |
9 | | use crate::CalendarError; |
10 | | |
11 | | /// Represents a clock time as hours, minutes and seconds |
12 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] |
13 | | pub struct ClockTime { |
14 | | pub hours: u8, |
15 | | pub minutes: u8, |
16 | | pub seconds: f32, |
17 | | } |
18 | | |
19 | | impl ClockTime { |
20 | | /// Returns an error if the ClockTime is invalid. |
21 | 12.4k | pub fn validate(self) -> Result<(), CalendarError> { |
22 | 12.4k | if self.hours > 23 { |
23 | 562 | Err(CalendarError::InvalidHour) |
24 | 11.8k | } else if self.minutes >= 60 { |
25 | 109 | Err(CalendarError::InvalidMinute) |
26 | 11.7k | } else if self.seconds > 60.0 { |
27 | | //Allow 60.0 for leap second |
28 | 97 | Err(CalendarError::InvalidSecond) |
29 | | } else { |
30 | 11.6k | Ok(()) |
31 | | } |
32 | 12.4k | } |
33 | | |
34 | 512 | pub fn hour_1_to_12(self) -> u8 { |
35 | 512 | (self.hours as i64).adjusted_remainder(12) as u8 |
36 | 512 | } |
37 | | } |
38 | | |
39 | | /// Represents a clock time as a fraction of a day |
40 | | /// |
41 | | /// This is internally a floating point number, where the fractional portion represents |
42 | | /// a particular time of day. For example 1.0 is midnight at the start of day 1, and 1.5 is |
43 | | /// noon on day 1. |
44 | | /// |
45 | | /// Note that equality and ordering operations are subject to limitations similar to |
46 | | /// equality and ordering operations on a floating point number. Two `TimeOfDay` values represent |
47 | | /// the same day or even the same second, but still appear different on the sub-second level. |
48 | | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Default)] |
49 | | pub struct TimeOfDay(f64); |
50 | | |
51 | | impl TimeOfDay { |
52 | | /// Create a new `TimeOfDay` |
53 | 75.5k | pub fn new(t: f64) -> Self { |
54 | 75.5k | TimeOfDay(t) |
55 | 75.5k | } |
56 | | |
57 | 23.9k | pub fn midnight() -> Self { |
58 | 23.9k | TimeOfDay(0.0) |
59 | 23.9k | } |
60 | | |
61 | 512 | pub fn noon() -> Self { |
62 | 512 | TimeOfDay(0.5) |
63 | 512 | } |
64 | | |
65 | | /// Get underlying floating point from `TimeOfDay` |
66 | 103k | pub fn get(self) -> f64 { |
67 | 103k | self.0 |
68 | 103k | } |
69 | | |
70 | | /// Split `TimeOfDay` into hours, minutes, and seconds |
71 | 94.1k | pub fn to_clock(self) -> ClockTime { |
72 | | //LISTING 1.44 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
73 | 94.1k | let b = [24.0, 60.0, 60.0]; |
74 | 94.1k | let mut a = [0.0, 0.0, 0.0, 0.0]; |
75 | 94.1k | TermNum::to_mixed_radix(self.get(), &b, 0, &mut a) |
76 | 94.1k | .expect("Valid inputs, other failures are impossible."); |
77 | 94.1k | ClockTime { |
78 | 94.1k | hours: a[1] as u8, |
79 | 94.1k | minutes: a[2] as u8, |
80 | 94.1k | seconds: a[3] as f32, |
81 | 94.1k | } |
82 | 94.1k | } |
83 | | |
84 | | /// Aggregate hours, minutes and second fields into a `TimeOfDay` |
85 | 12.1k | pub fn try_from_clock(clock: ClockTime) -> Result<Self, CalendarError> { |
86 | | //LISTING 1.43 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.) |
87 | 12.1k | clock.validate()?768 ; |
88 | 11.4k | let a = [ |
89 | 11.4k | 0.0, |
90 | 11.4k | clock.hours as f64, |
91 | 11.4k | clock.minutes as f64, |
92 | 11.4k | clock.seconds as f64, |
93 | 11.4k | ]; |
94 | 11.4k | let b = [24.0, 60.0, 60.0]; |
95 | 11.4k | let t = TermNum::from_mixed_radix(&a, &b, 0)?0 ; |
96 | 11.4k | Ok(TimeOfDay::new(t)) |
97 | 12.1k | } |
98 | | } |
99 | | |
100 | | impl FromFixed for TimeOfDay { |
101 | 64.0k | fn from_fixed(t: Fixed) -> TimeOfDay { |
102 | 64.0k | TimeOfDay::new(t.to_time_of_day().get()) |
103 | 64.0k | } |
104 | | } |
105 | | |
106 | | #[cfg(test)] |
107 | | mod tests { |
108 | | use super::*; |
109 | | use crate::day_count::BoundedDayCount; |
110 | | use crate::day_count::JulianDay; |
111 | | use crate::day_count::ToFixed; |
112 | | use crate::day_count::FIXED_MAX; |
113 | | use crate::day_count::FIXED_MIN; |
114 | | use proptest::proptest; |
115 | | |
116 | | #[test] |
117 | 1 | fn time() { |
118 | 1 | let j0: JulianDay = JulianDay::new(0.0); |
119 | 1 | assert_eq!(j0.convert::<TimeOfDay>().0, 0.5); |
120 | 1 | } |
121 | | |
122 | | #[test] |
123 | 1 | fn obvious_clock_times() { |
124 | 1 | assert_eq!( |
125 | 1 | TimeOfDay::try_from_clock(ClockTime { |
126 | 1 | hours: 0, |
127 | 1 | minutes: 0, |
128 | 1 | seconds: 0.0 |
129 | 1 | }) |
130 | 1 | .unwrap(), |
131 | 1 | TimeOfDay::new(0.0) |
132 | | ); |
133 | 1 | assert_eq!( |
134 | 1 | TimeOfDay::try_from_clock(ClockTime { |
135 | 1 | hours: 0, |
136 | 1 | minutes: 0, |
137 | 1 | seconds: 1.0 |
138 | 1 | }) |
139 | 1 | .unwrap(), |
140 | 1 | TimeOfDay::new(1.0 / (24.0 * 60.0 * 60.0)) |
141 | | ); |
142 | 1 | assert_eq!( |
143 | 1 | TimeOfDay::try_from_clock(ClockTime { |
144 | 1 | hours: 0, |
145 | 1 | minutes: 1, |
146 | 1 | seconds: 0.0 |
147 | 1 | }) |
148 | 1 | .unwrap(), |
149 | 1 | TimeOfDay::new(1.0 / (24.0 * 60.0)) |
150 | | ); |
151 | 1 | assert_eq!( |
152 | 1 | TimeOfDay::try_from_clock(ClockTime { |
153 | 1 | hours: 6, |
154 | 1 | minutes: 0, |
155 | 1 | seconds: 0.0 |
156 | 1 | }) |
157 | 1 | .unwrap(), |
158 | 1 | TimeOfDay::new(0.25) |
159 | | ); |
160 | 1 | assert_eq!( |
161 | 1 | TimeOfDay::try_from_clock(ClockTime { |
162 | 1 | hours: 12, |
163 | 1 | minutes: 0, |
164 | 1 | seconds: 0.0 |
165 | 1 | }) |
166 | 1 | .unwrap(), |
167 | 1 | TimeOfDay::new(0.5) |
168 | | ); |
169 | 1 | assert_eq!( |
170 | 1 | TimeOfDay::try_from_clock(ClockTime { |
171 | 1 | hours: 18, |
172 | 1 | minutes: 0, |
173 | 1 | seconds: 0.0 |
174 | 1 | }) |
175 | 1 | .unwrap(), |
176 | 1 | TimeOfDay::new(0.75) |
177 | | ); |
178 | 1 | } |
179 | | |
180 | | proptest! { |
181 | | #[test] |
182 | | fn clock_time_round_trip(ahr in 0..24,amn in 0..59,asc in 0..59) { |
183 | | let hours = ahr as u8; |
184 | | let minutes = amn as u8; |
185 | | let seconds = asc as f32; |
186 | | let c0 = ClockTime { hours, minutes, seconds }; |
187 | | let t = TimeOfDay::try_from_clock(c0).unwrap(); |
188 | | let c1 = t.to_clock(); |
189 | | assert_eq!(c0, c1); |
190 | | } |
191 | | |
192 | | #[test] |
193 | | fn clock_time_from_moment(x in FIXED_MIN..FIXED_MAX) { |
194 | | let t = TimeOfDay::from_fixed(Fixed::new(x)); |
195 | | let c = t.to_clock(); |
196 | | c.validate().unwrap(); |
197 | | } |
198 | | |
199 | | #[test] |
200 | | fn invalid_hour(ahr in 25..u8::MAX,amn in 0..59,asc in 0..59) { |
201 | | let c0 = ClockTime { hours: ahr as u8, minutes: amn as u8, seconds: asc as f32 }; |
202 | | assert!(TimeOfDay::try_from_clock(c0).is_err()); |
203 | | } |
204 | | |
205 | | #[test] |
206 | | fn invalid_minute(ahr in 0..59,amn in 60..u8::MAX,asc in 0..59) { |
207 | | let c0 = ClockTime { hours: ahr as u8, minutes: amn as u8, seconds: asc as f32 }; |
208 | | assert!(TimeOfDay::try_from_clock(c0).is_err()); |
209 | | } |
210 | | |
211 | | #[test] |
212 | | fn invalid_second(ahr in 0..59,amn in 0..59,asc in 61..u8::MAX) { |
213 | | let c0 = ClockTime { hours: ahr as u8, minutes: amn as u8, seconds: asc as f32 }; |
214 | | assert!(TimeOfDay::try_from_clock(c0).is_err()); |
215 | | } |
216 | | |
217 | | } |
218 | | } |