/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 | | } |