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/calendar/symmetry.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::calendar::gregorian::Gregorian;
6
use crate::calendar::prelude::CommonDate;
7
use crate::calendar::prelude::CommonWeekOfYear;
8
use crate::calendar::prelude::GuaranteedMonth;
9
use crate::calendar::prelude::HasLeapYears;
10
use crate::calendar::prelude::Quarter;
11
use crate::calendar::prelude::ToFromCommonDate;
12
use crate::calendar::AllowYearZero;
13
use crate::calendar::CalendarMoment;
14
use crate::calendar::OrdinalDate;
15
use crate::calendar::ToFromOrdinalDate;
16
use crate::common::error::CalendarError;
17
use crate::day_count::BoundedDayCount;
18
use crate::day_count::CalculatedBounds;
19
use crate::day_count::Epoch;
20
use crate::day_count::Fixed;
21
use crate::day_count::FromFixed;
22
use crate::day_count::ToFixed;
23
use std::num::NonZero;
24
25
use crate::common::math::TermNum;
26
27
#[allow(unused_imports)] //FromPrimitive is needed for derive
28
use num_traits::FromPrimitive;
29
30
#[allow(non_snake_case)]
31
struct SymmetryParams {
32
    C: i64,
33
    L: i64,
34
    K: i64,
35
}
36
37
const NORTHWARD_EQUINOX_PARAMS: SymmetryParams = SymmetryParams {
38
    C: 293,
39
    L: 52,
40
    K: 146,
41
};
42
43
const NORTH_SOLSTICE_PARAMS: SymmetryParams = SymmetryParams {
44
    C: 389,
45
    L: 69,
46
    K: 194,
47
};
48
49
/// Represents a month of the Symmetry calendars
50
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
51
pub enum SymmetryMonth {
52
    January = 1,
53
    February,
54
    March,
55
    April,
56
    May,
57
    June,
58
    July,
59
    August,
60
    September,
61
    October,
62
    November,
63
    December,
64
    /// Only appears in leap years
65
    Irvember,
66
}
67
68
/// Represents a date in one of the Symmetry calendars
69
///
70
/// ## Introduction
71
///
72
/// The Symmetry calendars are a collection of calendar systems developed by Dr. Irvin L. Bromberg.
73
/// Bromberg proposed 2 leap year rules and 2 month length rules, which can be combined to form
74
/// 4 variants of the Symmetry calendar.
75
///
76
/// ### Bromberg's Warning
77
///
78
/// The calculations used in this library mirror Dr. Bromberg's reference documents closely
79
/// while still being idiomatic Rust. From *Basic Symmetry454 and Symmetry010 Calendar
80
/// Arithmetic* by Bromberg:
81
///
82
/// > Symmetry454 and Symmetry010 calendar arithmetic is very simple, but there is a tendency
83
/// > for those who are programming their first implementation of these calendars to immediately
84
/// > cut corners that may suffice for a limited range of dates, or to skip thorough validation
85
/// > of their implementation.
86
/// >
87
/// > Please don’t deviate from the arithmetic outlined herein. Please “stick to the script”.
88
/// > Don’t try to invent your own arithmetic using novel expressions. There is no reason to do
89
/// > so, because this arithmetic is in the public domain, royalty free. The algorithm steps
90
/// > documented herein were carefully designed for efficiency, simplicity, and clarity of
91
/// > program code, and were thoroughly validated. Cutting corners will most likely result in
92
/// > harder-to-read programs that are more difficult to maintain and troubleshoot. In all
93
/// > probability a novel expression intended to “simplify” the arithmetic documented herein
94
/// > will actually prove to function erroneously under specific circumstances. It is just not
95
/// > worth wasting the time on the trouble that will make for you.
96
///
97
/// ## Basic Structure
98
///
99
/// ### Variants
100
///
101
///
102
/// | `T`       | `U`       | Alias                   | Leap cycle | Quarter length    |
103
/// |-----------|-----------|-------------------------|------------|-------------------|
104
/// | [`true`]  | [`true`]  | [`Symmetry454`]         | 293 year   | 4 + 5 + 4 weeks   |
105
/// | [`false`] | [`true`]  | [`Symmetry010`]         | 293 year   | 30 + 31 + 30 days |
106
/// | [`true`]  | [`false`] | [`Symmetry454Solstice`] | 389 year   | 4 + 5 + 4 weeks   |
107
/// | [`false`] | [`false`] | [`Symmetry010Solstice`] | 389 year   | 30 + 31 + 30 days |
108
///
109
/// The combinations are summarized in the table above. Columns `T` and `U` are the type parameters.
110
/// Column Alias refers to the type aliases provided for convenience.
111
///
112
/// The placement of leap years  is symmetric within a cycle - the length of the cycle is in
113
/// column Leap Cycle. The 293 year leap rule approximates the northward equinox, while the 389
114
/// year rule approximates the north solstice.
115
///
116
/// Column Quarter Length refers to how days are distributed within a common year.
117
/// Symmetry calendars have years split into 4 quarters. Each quarter is composed of
118
/// 3 months. In Symmetry454 calendars, the months have lengths of 4, 5, and 4 weeks respectively.
119
/// In Symmetry010 calendars, the months have lenghts of 30, 31, and 30 days respectively.
120
///
121
/// ### Leap cycle
122
///
123
/// For the 293 leap year rule: Y is a leap year only if the remainder of
124
/// ( 52 × Y + 146 ) / 293 is less than 52.
125
///
126
/// For the 393 leap year rule: Y is a leap year only if the remainder of
127
/// ( 69 × Y + 194 ) / 389 is less than 69.
128
///
129
/// ### Irvember
130
///
131
/// The Symmetry calendars have leap weeks instead of leap days. The extra week added in a leap
132
/// year is a standalone thirteenth month called [Irvember](SymmetryMonth::Irvember).
133
/// Dr. Bromberg suggested an alternative scheme where the extra week is added to
134
/// December instead of being standalone - however this alternative scheme is **not** implemented.
135
///
136
/// ## Epoch
137
///
138
/// The first day of the first year of all the Symmetry calendars is also the first day of the
139
/// first year of the proleptic Gregorian calendar.
140
///
141
/// ## Representation and Examples
142
///
143
/// The months are represented in this crate as [`SymmetryMonth`].
144
///
145
/// ```
146
/// use radnelac::calendar::*;
147
/// use radnelac::day_count::*;
148
///
149
/// let c = CommonDate::new(2025, 1, 1);
150
/// let s = Symmetry454::try_from_common_date(c).unwrap();
151
/// assert_eq!(s.month(), SymmetryMonth::January);
152
/// ```
153
///
154
/// Note that although many month names are shared with [`GregorianMonth`](crate::calendar::GregorianMonth),
155
/// the months of these two calendar systems are represented by distinct enums. This is because
156
/// [`SymmetryMonth::Irvember`] has no corresponding [`GregorianMonth`](crate::calendar::GregorianMonth).
157
///
158
/// ```
159
/// use radnelac::calendar::*;
160
/// use radnelac::day_count::*;
161
///
162
/// let Y: i32 = 2026;
163
/// assert!(Symmetry454::is_leap(Y));
164
/// let c = CommonDate::new(Y, 13, 1);
165
/// let s = Symmetry454::try_from_common_date(c).unwrap();
166
/// assert_eq!(s.month(), SymmetryMonth::Irvember);
167
/// ```
168
///
169
/// ## Further reading
170
/// + Dr. Irvin L. Bromberg
171
///   + [*Basic Symmetry454 and Symmetry010 Calendar Arithmetic*](https://kalendis.free.nf/Symmetry454-Arithmetic.pdf)
172
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
173
pub struct Symmetry<const T: bool, const U: bool>(CommonDate);
174
175
/// Symmetry454 calendar with 293 year leap rule
176
///
177
/// See [Symmetry] for more details.
178
pub type Symmetry454 = Symmetry<true, true>;
179
/// Symmetry010 calendar with 293 year leap rule
180
///
181
/// See [Symmetry] for more details.
182
pub type Symmetry010 = Symmetry<false, true>;
183
/// Symmetry454 calendar with 389 year leap rule
184
///
185
/// See [Symmetry] for more details.
186
pub type Symmetry454Solstice = Symmetry<true, false>;
187
/// Symmetry010 calendar with 389 year leap rule
188
///
189
/// See [Symmetry] for more details.
190
pub type Symmetry010Solstice = Symmetry<false, false>;
191
192
impl<const T: bool, const U: bool> Symmetry<T, U> {
193
127k
    fn params() -> SymmetryParams {
194
127k
        if U {
195
63.7k
            NORTHWARD_EQUINOX_PARAMS
196
        } else {
197
63.7k
            NORTH_SOLSTICE_PARAMS
198
        }
199
127k
    }
200
201
    /// Returns (T, U)
202
    ///
203
    /// T is true for Symmetry454 calendars and false for Symmetry010 calendars.
204
    /// U is true for the 292 year leap cycle and false for the 389 year leap cycle.
205
    /// See [Symmetry] for more details.
206
0
    pub fn mode(self) -> (bool, bool) {
207
0
        (T, U)
208
0
    }
209
210
    /// Returns the fixed day number of a Symmetry year
211
123k
    pub fn new_year_day_unchecked(sym_year: i32, sym_epoch: i64) -> i64 {
212
        //LISTING SymNewYearDay (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
213
123k
        let p = Self::params();
214
        #[allow(non_snake_case)]
215
123k
        let E = (sym_year - 1) as i64;
216
123k
        sym_epoch + (364 * E) + (7 * ((p.L * E) + p.K).div_euclid(p.C))
217
123k
    }
218
219
60.9k
    fn days_before_month_454(sym_month: i64) -> u16 {
220
        //LISTING DaysBeforeMonth (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
221
60.9k
        ((28 * (sym_month - 1)) + 7 * sym_month.div_euclid(3)) as u16
222
60.9k
    }
223
224
60.4k
    fn days_before_month_010(sym_month: i64) -> u16 {
225
        //LISTING DaysBeforeMonth (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
226
60.4k
        ((30 * (sym_month - 1)) + sym_month.div_euclid(3)) as u16
227
60.4k
    }
228
229
121k
    fn days_before_month(sym_month: u8) -> u16 {
230
121k
        if T {
231
60.9k
            Self::days_before_month_454(sym_month as i64)
232
        } else {
233
60.4k
            Self::days_before_month_010(sym_month as i64)
234
        }
235
121k
    }
236
237
50.7k
    fn day_of_year(sym_month: u8, sym_day: u8) -> u16 {
238
        //LISTING DayOfYear (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
239
50.7k
        Self::days_before_month(sym_month) + (sym_day as u16)
240
50.7k
    }
241
242
71.7k
    fn year_from_fixed(fixed: i64, epoch: i64) -> (i32, i64) {
243
        //LISTING FixedToSymYear (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
244
        // Tempting to cut "corners here" to avoid floating point.
245
        // But the notice at the top of the file reminds us to "stick to the script"
246
71.7k
        let fixed_date = fixed as f64;
247
71.7k
        let sym_epoch = epoch as f64;
248
71.7k
        let cycle_mean_year = if U {
249
35.8k
            365.0 + (71.0 / 293.0)
250
        } else {
251
35.8k
            365.0 + (94.0 / 389.0)
252
        };
253
71.7k
        let sym_year = ((fixed_date - sym_epoch) / cycle_mean_year).ceil() as i32;
254
71.7k
        let start_of_year = Self::new_year_day_unchecked(sym_year, epoch);
255
71.7k
        if start_of_year < fixed {
256
71.5k
            if (fixed - start_of_year) >= 364 {
257
240
                let start_of_next_year = Self::new_year_day_unchecked(sym_year + 1, epoch);
258
240
                if fixed >= start_of_next_year {
259
168
                    (sym_year + 1, start_of_next_year)
260
                } else {
261
72
                    (sym_year, start_of_year)
262
                }
263
            } else {
264
71.3k
                (sym_year, start_of_year)
265
            }
266
168
        } else if start_of_year > fixed {
267
124
            (
268
124
                sym_year - 1,
269
124
                Self::new_year_day_unchecked(sym_year - 1, epoch),
270
124
            )
271
        } else {
272
44
            (sym_year, start_of_year)
273
        }
274
71.7k
    }
275
}
276
277
impl<const T: bool, const U: bool> AllowYearZero for Symmetry<T, U> {}
278
279
impl<const T: bool, const U: bool> ToFromOrdinalDate for Symmetry<T, U> {
280
5.12k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
281
        // Not described by Dr. Bromberg
282
5.12k
        let new_year_0 = Self::new_year_day_unchecked(ord.year, Self::epoch().get_day_i());
283
5.12k
        let new_year_1 = Self::new_year_day_unchecked(ord.year + 1, Self::epoch().get_day_i());
284
5.12k
        let diff = new_year_1 - new_year_0;
285
5.12k
        if (ord.day_of_year as i64) < 1 || 
(ord.day_of_year as i64) > diff4.09k
{
286
2.92k
            Err(CalendarError::InvalidDayOfYear)
287
        } else {
288
2.19k
            Ok(())
289
        }
290
5.12k
    }
291
292
71.7k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
293
        //LISTING FixedToSym (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
294
        //Only the SymYear and DayOfYear terms.
295
71.7k
        let date = fixed_date.get_day_i();
296
71.7k
        let (sym_year, start_of_year) = Self::year_from_fixed(date, Self::epoch().get_day_i());
297
71.7k
        let day_of_year = (date - start_of_year + 1) as u16;
298
71.7k
        OrdinalDate {
299
71.7k
            year: sym_year,
300
71.7k
            day_of_year: day_of_year,
301
71.7k
        }
302
71.7k
    }
303
304
9.21k
    fn to_ordinal(self) -> OrdinalDate {
305
9.21k
        OrdinalDate {
306
9.21k
            year: self.0.year,
307
9.21k
            day_of_year: Self::day_of_year(self.0.month, self.0.day),
308
9.21k
        }
309
9.21k
    }
310
311
70.7k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
312
        //LISTING FixedToSym (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
313
        // Originally second part of from_fixed
314
70.7k
        let (sym_year, day_of_year) = (ord.year, ord.day_of_year);
315
        // Thank Ferris for div_ceil
316
70.7k
        let week_of_year = day_of_year.div_ceil(7);
317
70.7k
        debug_assert!(week_of_year > 0 && week_of_year < 54);
318
70.7k
        let quarter = (4 * week_of_year).div_ceil(53);
319
70.7k
        debug_assert!(quarter > 0 && quarter < 5);
320
70.7k
        let day_of_quarter = day_of_year - (91 * (quarter - 1));
321
70.7k
        let week_of_quarter = day_of_quarter.div_ceil(7);
322
70.7k
        let month_of_quarter = if T {
323
35.3k
            (2 * week_of_quarter).div_ceil(9)
324
        } else {
325
35.3k
            (2 * day_of_quarter).div_ceil(61)
326
        };
327
70.7k
        let sym_month = (3 * (quarter - 1) + month_of_quarter) as u8;
328
        // Skipping optionals
329
70.7k
        let sym_day = (day_of_year - Self::days_before_month(sym_month)) as u8;
330
70.7k
        Self(CommonDate::new(sym_year, sym_month, sym_day))
331
70.7k
    }
332
}
333
334
impl<const T: bool, const U: bool> HasLeapYears for Symmetry<T, U> {
335
3.59k
    fn is_leap(sym_year: i32) -> bool {
336
        //LISTING isSymLeapYear (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
337
3.59k
        let p = Self::params();
338
3.59k
        let sym_year = sym_year as i64;
339
3.59k
        ((p.L * sym_year) + p.K).modulus(p.C) < p.L
340
3.59k
    }
341
}
342
343
impl<const T: bool, const U: bool> CalculatedBounds for Symmetry<T, U> {}
344
345
impl<const T: bool, const U: bool> Epoch for Symmetry<T, U> {
346
129k
    fn epoch() -> Fixed {
347
129k
        Gregorian::epoch()
348
129k
    }
349
}
350
351
impl<const T: bool, const U: bool> FromFixed for Symmetry<T, U> {
352
69.6k
    fn from_fixed(fixed_date: Fixed) -> Symmetry<T, U> {
353
        //LISTING FixedToSym (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
354
        // Compared to Dr. Bromberg's original, this function is split in two
355
69.6k
        let ord = Self::ordinal_from_fixed(fixed_date);
356
69.6k
        Self::from_ordinal_unchecked(ord)
357
69.6k
    }
358
}
359
360
impl<const T: bool, const U: bool> ToFixed for Symmetry<T, U> {
361
41.5k
    fn to_fixed(self) -> Fixed {
362
        //LISTING SymToFixed (*Basic Symmetry454 and Symmetry010 Calendar Arithmetic* by Dr. Irvin L. Bromberg)
363
41.5k
        let new_year_day = Self::new_year_day_unchecked(self.0.year, Self::epoch().get_day_i());
364
41.5k
        let day_of_year = Self::day_of_year(self.0.month, self.0.day) as i64;
365
41.5k
        Fixed::cast_new(new_year_day + day_of_year - 1)
366
41.5k
    }
367
}
368
369
impl<const T: bool, const U: bool> ToFromCommonDate<SymmetryMonth> for Symmetry<T, U> {
370
162k
    fn to_common_date(self) -> CommonDate {
371
162k
        self.0
372
162k
    }
373
374
31.4k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
375
31.4k
        debug_assert!(Self::valid_ymd(date).is_ok());
376
31.4k
        Self(date)
377
31.4k
    }
378
379
68.9k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
380
68.9k
        let month_opt = SymmetryMonth::from_u8(date.month);
381
68.9k
        if month_opt.is_none() {
382
3.07k
            Err(CalendarError::InvalidMonth)
383
65.9k
        } else if date.day < 1 {
384
1.02k
            Err(CalendarError::InvalidDay)
385
64.8k
        } else if date.day > Self::month_length(date.year, month_opt.unwrap()) {
386
1.02k
            Err(CalendarError::InvalidDay)
387
        } else {
388
63.8k
            Ok(())
389
        }
390
68.9k
    }
391
392
1.02k
    fn year_end_date(year: i32) -> CommonDate {
393
1.02k
        let m = if Self::is_leap(year) {
394
186
            SymmetryMonth::Irvember
395
        } else {
396
838
            SymmetryMonth::December
397
        };
398
1.02k
        CommonDate::new(year, m as u8, Self::month_length(year, m))
399
1.02k
    }
400
401
67.1k
    fn month_length(_year: i32, month: SymmetryMonth) -> u8 {
402
        // This function is not described by Dr. Bromberg and is not
403
        // used in conversion to and from other timekeeping systems.
404
        // Instead it is used for checking if a [CommonDate] is valid.
405
67.1k
        match (month, T) {
406
1.40k
            (SymmetryMonth::Irvember, _) => 7,
407
33.5k
            (_, true) => (28 + (7 * ((month as u8).modulus(3).div_euclid(2)))) as u8,
408
32.2k
            (_, false) => (30 + (month as u8).modulus(3).div_euclid(2)) as u8,
409
        }
410
67.1k
    }
411
}
412
413
impl<const T: bool, const U: bool> Quarter for Symmetry<T, U> {
414
10.2k
    fn quarter(self) -> NonZero<u8> {
415
10.2k
        match self.month() {
416
4
            SymmetryMonth::Irvember => NonZero::new(4 as u8).unwrap(),
417
10.2k
            m => NonZero::new((((m as u8) - 1) / 3) + 1).expect("(m-1)/3 > -1"),
418
        }
419
10.2k
    }
420
}
421
422
impl<const T: bool, const U: bool> GuaranteedMonth<SymmetryMonth> for Symmetry<T, U> {}
423
impl<const T: bool, const U: bool> CommonWeekOfYear<SymmetryMonth> for Symmetry<T, U> {}
424
425
/// Represents a date *and time* in the Symmetry454 Calendar
426
pub type Symmetry454Moment = CalendarMoment<Symmetry454>;
427
428
/// Represents a date *and time* in the Symmetry010 Calendar
429
pub type Symmetry010Moment = CalendarMoment<Symmetry010>;
430
431
/// Represents a date *and time* in the Symmetry454 Calendar (solstice-approximating)
432
pub type Symmetry454SolsticeMoment = CalendarMoment<Symmetry454Solstice>;
433
434
/// Represents a date *and time* in the Symmetry010 Calendar (solstice-approximating)
435
pub type Symmetry010SolsticeMoment = CalendarMoment<Symmetry010Solstice>;
436
437
#[cfg(test)]
438
mod tests {
439
    use super::*;
440
    use crate::day_count::RataDie;
441
    use crate::day_count::FIXED_MAX;
442
    use crate::day_cycle::Weekday;
443
    use proptest::proptest;
444
    const MAX_YEARS: i32 = (FIXED_MAX / 365.25) as i32;
445
446
    #[test]
447
1
    fn is_leap_example() {
448
1
        assert!(Symmetry454::is_leap(2009));
449
1
        assert!(Symmetry454::is_leap(2015));
450
1
        assert!(Symmetry010::is_leap(2009));
451
1
        assert!(Symmetry010::is_leap(2015));
452
453
1
        assert!(!Symmetry454Solstice::is_leap(2009));
454
1
        assert!(!Symmetry010Solstice::is_leap(2009));
455
1
        assert!(Symmetry454Solstice::is_leap(2010));
456
1
        assert!(Symmetry010Solstice::is_leap(2016));
457
1
    }
458
459
    #[test]
460
1
    fn new_year_day_example() {
461
1
        assert_eq!(Symmetry454::new_year_day_unchecked(2010, 1), 733776);
462
1
        assert_eq!(Symmetry010::new_year_day_unchecked(2010, 1), 733776);
463
1
        assert_eq!(Symmetry454Solstice::new_year_day_unchecked(2010, 1), 733769);
464
1
        assert_eq!(Symmetry010Solstice::new_year_day_unchecked(2010, 1), 733769);
465
1
    }
466
467
    #[test]
468
1
    fn days_before_month() {
469
1
        assert_eq!(Symmetry454::days_before_month(1), 0);
470
1
        assert_eq!(Symmetry454::days_before_month(13), 364);
471
1
        assert_eq!(Symmetry454::days_before_month(6), 154);
472
1
        assert_eq!(Symmetry010::days_before_month(1), 0);
473
1
        assert_eq!(Symmetry010::days_before_month(13), 364);
474
1
        assert_eq!(Symmetry010::days_before_month(6), 152);
475
476
1
        assert_eq!(Symmetry454Solstice::days_before_month(1), 0);
477
1
        assert_eq!(Symmetry454Solstice::days_before_month(13), 364);
478
1
        assert_eq!(Symmetry454Solstice::days_before_month(6), 154);
479
1
        assert_eq!(Symmetry010Solstice::days_before_month(1), 0);
480
1
        assert_eq!(Symmetry010Solstice::days_before_month(13), 364);
481
1
        assert_eq!(Symmetry010Solstice::days_before_month(6), 152);
482
1
    }
483
484
    #[test]
485
1
    fn day_of_year() {
486
1
        assert_eq!(Symmetry454::day_of_year(6, 17), 171);
487
1
        assert_eq!(Symmetry010::day_of_year(6, 17), 169);
488
1
        assert_eq!(Symmetry454Solstice::day_of_year(6, 17), 171);
489
1
        assert_eq!(Symmetry010Solstice::day_of_year(6, 17), 169);
490
1
    }
491
492
    #[test]
493
1
    fn to_fixed() {
494
1
        assert_eq!(
495
1
            Symmetry454::try_from_common_date(CommonDate::new(2009, 4, 5))
496
1
                .unwrap()
497
1
                .to_fixed()
498
1
                .get_day_i(),
499
            733500
500
        );
501
1
        assert_eq!(
502
1
            Symmetry010::try_from_common_date(CommonDate::new(2009, 4, 5))
503
1
                .unwrap()
504
1
                .to_fixed()
505
1
                .get_day_i(),
506
            733500
507
        );
508
1
        assert_eq!(
509
1
            Symmetry454Solstice::try_from_common_date(CommonDate::new(2009, 4, 5))
510
1
                .unwrap()
511
1
                .to_fixed()
512
1
                .get_day_i(),
513
            733500
514
        );
515
1
        assert_eq!(
516
1
            Symmetry010Solstice::try_from_common_date(CommonDate::new(2009, 4, 5))
517
1
                .unwrap()
518
1
                .to_fixed()
519
1
                .get_day_i(),
520
            733500
521
        );
522
1
    }
523
524
    #[test]
525
1
    fn year_from_fixed() {
526
1
        assert_eq!(Symmetry454::year_from_fixed(733649, 1), (2009, 733405));
527
1
        assert_eq!(Symmetry010::year_from_fixed(733649, 1), (2009, 733405));
528
1
        assert_eq!(Symmetry454::year_from_fixed(733406, 1), (2009, 733405));
529
1
        assert_eq!(Symmetry010::year_from_fixed(733406, 1), (2009, 733405));
530
1
        assert_eq!(Symmetry454::year_from_fixed(733774, 1).0, 2009);
531
1
        assert_eq!(Symmetry010::year_from_fixed(733774, 1).0, 2009);
532
1
    }
533
534
    #[test]
535
1
    fn example_data() {
536
1
        let d_list = [
537
1
            (
538
1
                -44444 as i32,
539
1
                CommonDate::new(-121, 4, 27),
540
1
                CommonDate::new(-121, 4, 27),
541
1
                CommonDate::new(-121, 4, 27),
542
1
                CommonDate::new(-121, 4, 27),
543
1
            ),
544
1
            (
545
1
                -33333 as i32,
546
1
                CommonDate::new(-91, 9, 22),
547
1
                CommonDate::new(-91, 9, 24),
548
1
                CommonDate::new(-91, 9, 22),
549
1
                CommonDate::new(-91, 9, 24),
550
1
            ),
551
1
            (
552
1
                44444 as i32,
553
1
                CommonDate::new(122, 9, 8),
554
1
                CommonDate::new(122, 9, 10),
555
1
                CommonDate::new(122, 9, 8),
556
1
                CommonDate::new(122, 9, 10),
557
1
            ),
558
1
            (
559
1
                648491 as i32,
560
1
                CommonDate::new(1776, 7, 4),
561
1
                CommonDate::new(1776, 7, 4),
562
1
                CommonDate::new(1776, 7, 4),
563
1
                CommonDate::new(1776, 7, 4),
564
1
            ),
565
1
            (
566
1
                681724 as i32,
567
1
                CommonDate::new(1867, 7, 1),
568
1
                CommonDate::new(1867, 7, 1),
569
1
                CommonDate::new(1867, 7, 1),
570
1
                CommonDate::new(1867, 7, 1),
571
1
            ),
572
1
            (
573
1
                711058 as i32,
574
1
                CommonDate::new(1947, 10, 26),
575
1
                CommonDate::new(1947, 10, 26),
576
1
                CommonDate::new(1947, 10, 26),
577
1
                CommonDate::new(1947, 10, 26),
578
1
            ),
579
1
            (
580
1
                728515 as i32,
581
1
                CommonDate::new(1995, 8, 11),
582
1
                CommonDate::new(1995, 8, 9),
583
1
                CommonDate::new(1995, 8, 11),
584
1
                CommonDate::new(1995, 8, 9),
585
1
            ),
586
1
            (
587
1
                730179 as i32,
588
1
                CommonDate::new(2000, 2, 30),
589
1
                CommonDate::new(2000, 2, 28),
590
1
                CommonDate::new(2000, 2, 30),
591
1
                CommonDate::new(2000, 2, 28),
592
1
            ),
593
1
            (
594
1
                731703 as i32,
595
1
                CommonDate::new(2004, 5, 7),
596
1
                CommonDate::new(2004, 5, 5),
597
1
                CommonDate::new(2004, 5, 7),
598
1
                CommonDate::new(2004, 5, 5),
599
1
            ),
600
1
            (
601
1
                731946 as i32,
602
1
                CommonDate::new(2004, 13, 5),
603
1
                CommonDate::new(2004, 13, 5),
604
1
                CommonDate::new(2005, 1, 5),
605
1
                CommonDate::new(2005, 1, 5),
606
1
            ),
607
1
            (
608
1
                737475 as i32,
609
1
                CommonDate::new(2020, 2, 25),
610
1
                CommonDate::new(2020, 2, 23),
611
1
                CommonDate::new(2020, 2, 25),
612
1
                CommonDate::new(2020, 2, 23),
613
1
            ),
614
1
            (
615
1
                811236 as i32,
616
1
                CommonDate::new(2222, 2, 6),
617
1
                CommonDate::new(2222, 2, 4),
618
1
                CommonDate::new(2222, 2, 6),
619
1
                CommonDate::new(2222, 2, 4),
620
1
            ),
621
1
            (
622
1
                1217048 as i32,
623
1
                CommonDate::new(3333, 2, 35),
624
1
                CommonDate::new(3333, 3, 2),
625
1
                CommonDate::new(3333, 2, 35),
626
1
                CommonDate::new(3333, 3, 2),
627
1
            ),
628
1
        ];
629
14
        for 
item13
in d_list {
630
13
            let rd = RataDie::new(item.0 as f64);
631
13
            let s454q = Symmetry454::try_from_common_date(item.1).unwrap();
632
13
            let s010q = Symmetry010::try_from_common_date(item.2).unwrap();
633
13
            let s454s = Symmetry454Solstice::try_from_common_date(item.3).unwrap();
634
13
            let s010s = Symmetry010Solstice::try_from_common_date(item.4).unwrap();
635
13
            assert_eq!(rd.to_fixed(), s454q.to_fixed());
636
13
            assert_eq!(rd.to_fixed(), s010q.to_fixed());
637
13
            assert_eq!(rd.to_fixed(), s454s.to_fixed());
638
13
            assert_eq!(rd.to_fixed(), s010s.to_fixed());
639
        }
640
1
    }
641
642
    proptest! {
643
        #[test]
644
        fn month_start_on_monday_454(year in -MAX_YEARS..MAX_YEARS, month in 1..12) {
645
            let c = CommonDate::new(year as i32, month as u8, 1);
646
            let d = Symmetry454::try_from_common_date(c).unwrap();
647
            assert_eq!(d.convert::<Weekday>(), Weekday::Monday);
648
            let d = Symmetry454Solstice::try_from_common_date(c).unwrap();
649
            assert_eq!(d.convert::<Weekday>(), Weekday::Monday);
650
        }
651
652
        #[test]
653
        fn month_end_on_sunday_454(year in -MAX_YEARS..MAX_YEARS, month in 1..12) {
654
            let m = SymmetryMonth::from_i32(month).unwrap();
655
            let c = CommonDate::new(year as i32, month as u8, Symmetry454::month_length(year, m));
656
            let d = Symmetry454::try_from_common_date(c).unwrap();
657
            assert_eq!(d.convert::<Weekday>(), Weekday::Sunday);
658
            let d = Symmetry454Solstice::try_from_common_date(c).unwrap();
659
            assert_eq!(d.convert::<Weekday>(), Weekday::Sunday);
660
        }
661
662
        #[test]
663
        fn no_friday_13_454(year in -MAX_YEARS..MAX_YEARS, month in 1..12) {
664
            let c = CommonDate::new(year as i32, month as u8, 13);
665
            let d = Symmetry454::try_from_common_date(c).unwrap();
666
            assert_ne!(d.convert::<Weekday>(), Weekday::Friday);
667
            let d = Symmetry454Solstice::try_from_common_date(c).unwrap();
668
            assert_ne!(d.convert::<Weekday>(), Weekday::Friday);
669
        }
670
671
        #[test]
672
        fn year_start_on_monday_010(year in -MAX_YEARS..MAX_YEARS) {
673
            let c = CommonDate::new(year as i32, 1, 1);
674
            let d = Symmetry010::try_from_common_date(c).unwrap();
675
            assert_eq!(d.convert::<Weekday>(), Weekday::Monday);
676
            let d = Symmetry010Solstice::try_from_common_date(c).unwrap();
677
            assert_eq!(d.convert::<Weekday>(), Weekday::Monday);
678
        }
679
680
        #[test]
681
        fn no_friday_13_010(year in -MAX_YEARS..MAX_YEARS, month in 1..12) {
682
            let c = CommonDate::new(year as i32, month as u8, 13);
683
            let d = Symmetry010::try_from_common_date(c).unwrap();
684
            assert_ne!(d.convert::<Weekday>(), Weekday::Friday);
685
            let d = Symmetry010Solstice::try_from_common_date(c).unwrap();
686
            assert_ne!(d.convert::<Weekday>(), Weekday::Friday);
687
        }
688
    }
689
}