Coverage Report

Created: 2025-10-19 21:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}