Coverage Report

Created: 2025-08-13 21:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/day_count/fixed.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::common::math::EFFECTIVE_MAX;
7
use crate::common::math::EFFECTIVE_MIN;
8
use crate::day_count::prelude::BoundedDayCount;
9
use crate::day_count::prelude::EffectiveBound;
10
11
const FIXED_MAX_SCALE: f64 = 2048.0;
12
13
/// Maximum supported value for a `Fixed`
14
///
15
/// This somewhere after January 1, 47000000 Common Era in the proleptic Gregorian calendar.
16
///
17
/// It is possible to create a `Fixed` at with a higher value - this is permitted for
18
/// intermediate results of calculations. However in general a `Fixed` with a value beyond
19
/// `FIXED_MAX` is at risk of reduced accuracy calculations.
20
pub const FIXED_MAX: f64 = (EFFECTIVE_MAX * (FIXED_MAX_SCALE - 1.0)) / FIXED_MAX_SCALE;
21
/// Minimum supported value for a `Fixed`
22
///
23
/// This somewhere before January 1, 47000000 Before Common Era in the proleptic Gregorian calendar.
24
///
25
/// It is possible to create a `Fixed` at with a lower value - this is permitted for
26
/// intermediate results of calculations. However in general a `Fixed` with a value beyond
27
/// `FIXED_MIN` is at risk of reduced accuracy calculations.
28
pub const FIXED_MIN: f64 = (EFFECTIVE_MIN * (FIXED_MAX_SCALE - 1.0)) / FIXED_MAX_SCALE;
29
30
/// Represents a fixed point in time
31
///
32
/// This is internally a floating point number, where the integer portion represents a
33
/// particular day and the fractional portion represents a particular time of day.
34
///
35
/// The epoch used for this data structure is considered an internal implementation detail.
36
///
37
/// Note that equality and ordering operations are subject to limitations similar to
38
/// equality and ordering operations on a floating point number. Two `Fixed` values represent
39
/// the same day or even the same second, but still appear different on the sub-second level.
40
/// Use `get_day_i` to compare days, and use `same_second` to compare seconds.
41
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Default)]
42
pub struct Fixed(f64);
43
44
impl Fixed {
45
    /// Returns a new `Fixed` with day 0 and the same time of day.
46
64.2k
    pub fn to_time_of_day(self) -> Fixed {
47
        //LISTING 1.18 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
48
64.2k
        Fixed(self.0.modulus(1.0))
49
64.2k
    }
50
51
    /// Returns a new `Fixed` with the same day and midnight as the time of day.
52
1.84M
    pub fn to_day(self) -> Fixed {
53
        //LISTING 1.12 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
54
1.84M
        Fixed(self.0.floor())
55
1.84M
    }
56
57
    /// Returns the day as an integer
58
1.70M
    pub fn get_day_i(self) -> i64 {
59
1.70M
        self.to_day().get() as i64
60
1.70M
    }
61
62
    /// Returns true if `self` and `other` represent the same second of time.
63
5.63k
    pub fn same_second(self, other: Self) -> bool {
64
5.63k
        self.0.approx_eq(other.0)
65
5.63k
    }
66
}
67
68
impl EffectiveBound for Fixed {
69
2.74M
    fn effective_min() -> Fixed {
70
2.74M
        Fixed(FIXED_MIN)
71
2.74M
    }
72
73
2.74M
    fn effective_max() -> Fixed {
74
2.74M
        Fixed(FIXED_MAX)
75
2.74M
    }
76
}
77
78
impl BoundedDayCount<f64> for Fixed {
79
1.87M
    fn new(t: f64) -> Fixed {
80
        //It's expected to go beyond the FIXED_MAX/FIXED_MIN for intermediate results
81
        //of calculations. However going beyond EFFECTIVE_MAX/EFFECTIVE_MIN is
82
        //probably a bug.
83
1.87M
        debug_assert!(
84
1.87M
            Fixed::almost_in_effective_bounds(t, FIXED_MAX / FIXED_MAX_SCALE).is_ok(),
85
0
            "t = {}",
86
            t
87
        );
88
1.87M
        Fixed(t)
89
1.87M
    }
90
7.36M
    fn get(self) -> f64 {
91
7.36M
        self.0
92
7.36M
    }
93
}
94
95
pub trait FromFixed: Copy + Clone {
96
    fn from_fixed(t: Fixed) -> Self;
97
}
98
99
pub trait ToFixed: Copy + Clone {
100
    fn to_fixed(self) -> Fixed;
101
61.8k
    fn convert<T: FromFixed>(self) -> T {
102
        //LISTING ?? SECTION 1.1 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
103
61.8k
        T::from_fixed(self.to_fixed())
104
61.8k
    }
105
}
106
107
pub trait Epoch: FromFixed {
108
    fn epoch() -> Fixed;
109
}
110
111
pub trait CalculatedBounds: FromFixed + ToFixed + PartialEq + PartialOrd {}
112
113
impl<T: CalculatedBounds> EffectiveBound for T {
114
862k
    fn effective_min() -> Self {
115
862k
        Self::from_fixed(Fixed::effective_min())
116
862k
    }
117
118
862k
    fn effective_max() -> Self {
119
862k
        Self::from_fixed(Fixed::effective_max())
120
862k
    }
121
}
122
123
#[cfg(test)]
124
mod tests {
125
    use super::*;
126
    use crate::common::math::EFFECTIVE_EPSILON;
127
    use crate::day_count::FIXED_MAX;
128
    use crate::day_count::FIXED_MIN;
129
    use proptest::proptest;
130
131
    #[test]
132
1
    fn bounds_propeties() {
133
1
        assert!(FIXED_MAX < EFFECTIVE_MAX && FIXED_MAX > (EFFECTIVE_MAX / 2.0));
134
1
        assert!(FIXED_MIN > EFFECTIVE_MIN && FIXED_MIN < (EFFECTIVE_MIN / 2.0));
135
1
    }
136
137
    #[test]
138
1
    fn reject_weird() {
139
1
        let weird_values = [
140
1
            f64::NAN,
141
1
            f64::INFINITY,
142
1
            f64::NEG_INFINITY,
143
1
            FIXED_MAX + 1.0,
144
1
            FIXED_MIN - 1.0,
145
1
        ];
146
6
        for 
x5
in weird_values {
147
5
            assert!(Fixed::in_effective_bounds(x).is_err());
148
        }
149
1
    }
150
151
    #[test]
152
1
    fn accept_ok() {
153
1
        let ok_values = [FIXED_MAX, FIXED_MIN, 0.0, -0.0, EFFECTIVE_EPSILON];
154
6
        for 
x5
in ok_values {
155
5
            assert!(Fixed::in_effective_bounds(x).is_ok());
156
        }
157
1
    }
158
159
    #[test]
160
1
    fn comparisons() {
161
1
        let f_min = Fixed::effective_min();
162
1
        let f_mbig = Fixed::new(-100000.0 * 365.25);
163
1
        let f_m1 = Fixed::new(-1.0);
164
1
        let f_0 = Fixed::new(0.0);
165
1
        let f_p1 = Fixed::new(1.0);
166
1
        let f_pbig = Fixed::new(100000.0 * 365.25);
167
1
        let f_max = Fixed::effective_max();
168
1
        assert!(f_min < f_mbig);
169
1
        assert!(f_mbig < f_m1);
170
1
        assert!(f_m1 < f_0);
171
1
        assert!(f_0 < f_p1);
172
1
        assert!(f_p1 < f_pbig);
173
1
        assert!(f_pbig < f_max);
174
1
    }
175
176
    proptest! {
177
        #[test]
178
        fn time_of_day(t in FIXED_MIN..FIXED_MAX) {
179
            let f = Fixed::new(t).to_time_of_day().get();
180
            assert!(f <= 1.0 && f >= 0.0);
181
        }
182
183
        #[test]
184
        fn day(t in FIXED_MIN..FIXED_MAX) {
185
            let f = Fixed::new(t).to_day().get();
186
            assert!(f <= (t + 1.0) && f >= (t - 1.0));
187
            let d = Fixed::new(t).get_day_i();
188
            assert_eq!(d as f64, f);
189
        }
190
    }
191
}