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/french_rev_arith.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::HasLeapYears;
8
use crate::calendar::prelude::Perennial;
9
use crate::calendar::prelude::Quarter;
10
use crate::calendar::prelude::ToFromCommonDate;
11
use crate::calendar::AllowYearZero;
12
use crate::calendar::CalendarMoment;
13
use crate::calendar::HasEpagemonae;
14
use crate::calendar::OrdinalDate;
15
use crate::calendar::ToFromOrdinalDate;
16
use crate::common::error::CalendarError;
17
use crate::common::math::TermNum;
18
use crate::day_count::BoundedDayCount;
19
use crate::day_count::CalculatedBounds;
20
use crate::day_count::Epoch;
21
use crate::day_count::Fixed;
22
use crate::day_count::FromFixed;
23
use crate::day_count::ToFixed;
24
#[allow(unused_imports)] //FromPrimitive is needed for derive
25
use num_traits::FromPrimitive;
26
use std::num::NonZero;
27
28
const FRENCH_EPOCH_GREGORIAN: CommonDate = CommonDate {
29
    year: 1792,
30
    month: 9,
31
    day: 22,
32
};
33
const NON_MONTH: u8 = 13;
34
35
/// Represents a month in the French Revolutionary Calendar
36
///
37
/// Note that the Sansculottides at the end of the French Revolutionary calendar
38
/// year have no month and thus are not represented by FrenchRevMonth. When representing
39
/// an arbitrary day in the French Revolutionary calendar, use an `Option<FrenchRevMonth>`
40
/// for the the month field.
41
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
42
pub enum FrenchRevMonth {
43
    Vendemiaire = 1,
44
    Brumaire,
45
    Frimaire,
46
    Nivose,
47
    Pluviose,
48
    Ventose,
49
    Germinal,
50
    Floreal,
51
    Prairial,
52
    Messidor,
53
    Thermidor,
54
    Fructidor,
55
}
56
57
/// Represents a weekday in the French Revolutionary Calendar
58
///
59
/// The calendar reforms during the French Revolution included the creation of
60
/// a ten-day week. The name of each day is based on the numeric position in the week.
61
///
62
/// Note that the Sansculottides at the end of the French Revolutionary calendar
63
/// year do not have a weekday.
64
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
65
pub enum FrenchRevWeekday {
66
    Primidi = 1,
67
    Duodi,
68
    Tridi,
69
    Quartidi,
70
    Quintidi,
71
    Sextidi,
72
    Septidi,
73
    Octidi,
74
    Nonidi,
75
    Decadi,
76
}
77
78
/// Represents an epagomenal day at the end of the French Revolutionary calendar year
79
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, FromPrimitive, ToPrimitive)]
80
pub enum Sansculottide {
81
    Vertu = 1,
82
    Genie,
83
    Travail,
84
    Opinion,
85
    Recompense,
86
    Revolution,
87
}
88
89
/// Represents a date in the algorithmic approximation of the French Revolutionary calendar
90
///
91
/// ## Introduction
92
///
93
/// The French Revolutionary calendar (also called French Republican calendar) was the official
94
/// calendar during the French First Republic. It was also used briefly by the Paris Commune.
95
///
96
/// `FrenchRevArith` is **an approximation** of the French Revolutionary calendar. The
97
/// difference between the approximation and the historic calendar is the leap year rule.
98
///
99
/// The structure of an individual year has similarities to the [Coptic](crate::calendar::Coptic)
100
/// and [ancient Egyptian](crate::calendar::Egyptian) calendars.
101
///
102
/// ### Leap Year Approximation
103
///
104
/// The calendar actually implemented during the French First Republic relied on astronomical
105
/// observations to determine whether a given year was a leap year. **FrenchRevArith does not
106
/// read astronomical data nor approximate such data - instead it relies on algorithmic
107
/// rules to determine the start of new years, similar to those used by the Gregorian calendar**.
108
///
109
/// The leap year rule is determined by the parameter L.
110
/// * L = false: According to this rule, any year which is a multiple of 4 is a leap year
111
///    unless it is a multiple of 100. Any year which is a multiple of 100 is not a leap year
112
///    unless it is a multiple of 400. Any year which is a multiple of 400 is a leap year
113
///    unless it is a multiple of 4000. For example, years 4, 8, and 12 are leap years.
114
/// * L = true: This approximation is exactly the same as the one used where L = false, except
115
///    that an offset of 1 is added to the year before starting the calculation. For example,
116
///    years 3, 7 and 11 are leap years.
117
///
118
/// The approximation where L = false was proposed by Gilbert Romme, who directed the creation
119
/// of the calendar. It is commonly used by other software approximating the French Revolutionary
120
/// calendar. However, it was never used by any French government -
121
/// the calendar actually used during the French First Republic used astronomical observations
122
/// to determine leap years, and contradicted Romme's approximations. The official leap years
123
/// during the Revolution were years 3, 7, and 11 whereas the leap years produced by Romme's
124
/// approximation are years 4, 8, and 12.
125
///
126
/// The approximation where L = true ensures that leap years are consistent with the French
127
/// government for the years where the Revolutionary calendar was officially used. This is a
128
/// rather crude approximation which is not astronomically accurate outside those particular
129
/// years.
130
///
131
/// The value of L should be determined by the caller's use case:
132
/// * for consistency with other software using Romme's approximation: L = false
133
/// * for consistency with Romme's wishes: L = false
134
/// * for consistency with historical dates during the French First Republic: L = true
135
/// * for consistency with historical dates during the Paris Commune: L = true
136
/// * for consistency with how the calendar was "originally intended" to work for
137
///   time periods not mentioned above: **not supported**
138
///
139
/// The final use case in the list above is not currently supported by this library.
140
/// Implementing that feature requires calculating the date of the autumnal equinox
141
/// at the Paris Observatory. If a future version of this library implements such
142
/// astronomical calculations, those calculations will not be provided by FrenchRevArith.
143
/// Instead, such calculations shall be provided by a new struct with a new name.
144
///
145
/// ## Basic Structure
146
///
147
/// Years are divided into 12 months. All months have 3 weeks of 10 days each.
148
///
149
/// There are additional days at the end of the year which are not part of any week or month -
150
/// these are called "sansculottides". Leap years have 6 sansculottides, other years have 5
151
/// sansculottides.
152
///
153
/// Two leap year rules are available: see the "Leap Year Approximation" section.
154
///
155
/// ## Epoch
156
///
157
/// Years are numbered from the proclamation of the French First Republic. The first day of the
158
/// first year of the French Revolutionary calendar is 22 September 1792 Common Era in the
159
/// Gregorian calendar.
160
///
161
/// This epoch is called the Republican Era.
162
///
163
/// ## Representation and Examples
164
///
165
/// The months are represented in this crate as [`FrenchRevMonth`].
166
///
167
/// ```
168
/// use radnelac::calendar::*;
169
/// use radnelac::day_count::*;
170
///
171
/// let coup = CommonDate::new(8, 2, 18);
172
/// let fr = FrenchRevArith::<true>::try_from_common_date(coup).unwrap();
173
/// assert_eq!(fr.try_month().unwrap(), FrenchRevMonth::Brumaire);
174
/// ```
175
///
176
/// The parameter `L` determines the leap year rule. Use `L = true` for historical dates.
177
/// For more details, see the "Leap Year Approximation" section.
178
///
179
/// ```
180
/// use radnelac::calendar::*;
181
/// use radnelac::day_count::*;
182
///
183
/// let coup = CommonDate::new(8, 2, 18);
184
/// let fr_t = FrenchRevArith::<true>::try_from_common_date(coup).unwrap();
185
/// let fr_f = FrenchRevArith::<false>::try_from_common_date(coup).unwrap();
186
/// let g_known = Gregorian::try_new(1799, GregorianMonth::November, 9).unwrap();
187
/// assert_eq!(fr_t.convert::<Gregorian>(), g_known);
188
/// assert_ne!(fr_f.convert::<Gregorian>(), g_known);
189
/// ```
190
///
191
/// When converting to and from a [`CommonDate`](crate::calendar::CommonDate), the sansculottides
192
/// are treated as a 13th month.
193
///
194
/// ```
195
/// use radnelac::calendar::*;
196
/// use radnelac::day_count::*;
197
///
198
/// let c = CommonDate::new(7, 13, 5);
199
/// let fr = FrenchRevArith::<true>::try_from_common_date(c).unwrap();
200
/// assert!(fr.try_month().is_none());
201
/// assert_eq!(fr.epagomenae().unwrap(), Sansculottide::Recompense);
202
/// ```
203
///
204
/// The start of the Republican Era can be read programatically.
205
///
206
/// ```
207
/// use radnelac::calendar::*;
208
/// use radnelac::day_count::*;
209
///
210
/// let e = FrenchRevArith::<true>::epoch();
211
/// let g = Gregorian::from_fixed(e);
212
/// let fr = FrenchRevArith::<true>::from_fixed(e);
213
/// assert_eq!(g.year(), 1792);
214
/// assert_eq!(g.month(), GregorianMonth::September);
215
/// assert_eq!(g.day(), 22);
216
/// assert_eq!(fr.year(), 1);
217
/// assert_eq!(fr.try_month().unwrap(), FrenchRevMonth::Vendemiaire);
218
/// assert_eq!(fr.day(), 1);
219
/// ```
220
221
///
222
/// ## Further reading
223
/// + [Wikipedia](https://en.wikipedia.org/wiki/French_Republican_calendar)
224
/// + [Guanzhong "quantum" Chen](https://quantum5.ca/2022/03/09/art-of-time-keeping-part-4-french-republican-calendar/)
225
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
226
pub struct FrenchRevArith<const L: bool>(CommonDate);
227
228
impl<const L: bool> AllowYearZero for FrenchRevArith<L> {}
229
230
impl<const L: bool> ToFromOrdinalDate for FrenchRevArith<L> {
231
2.56k
    fn valid_ordinal(ord: OrdinalDate) -> Result<(), CalendarError> {
232
2.56k
        let correction = if Self::is_leap(ord.year) { 
1661
} else {
01.89k
};
233
2.56k
        if ord.day_of_year > 0 && 
ord.day_of_year <= (365 + correction)2.04k
{
234
1.15k
            Ok(())
235
        } else {
236
1.40k
            Err(CalendarError::InvalidDayOfYear)
237
        }
238
2.56k
    }
239
240
31.7k
    fn ordinal_from_fixed(fixed_date: Fixed) -> OrdinalDate {
241
        //LISTING 17.10 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
242
        //This does not include the month and day terms
243
31.7k
        let date = fixed_date.get_day_i();
244
31.7k
        let epoch = Self::epoch().get_day_i();
245
31.7k
        let approx = ((4000 * (date - epoch + 2)).div_euclid(1460969) + 1) as i32;
246
31.7k
        let approx_start = Self(CommonDate::new(approx, 1, 1)).to_fixed().get_day_i();
247
31.7k
        let year = if date < approx_start {
248
110
            approx - 1
249
        } else {
250
31.6k
            approx
251
        };
252
31.7k
        let year_start = Self(CommonDate::new(year, 1, 1)).to_fixed().get_day_i();
253
31.7k
        let doy = (date - year_start + 1) as u16;
254
31.7k
        OrdinalDate {
255
31.7k
            year: year,
256
31.7k
            day_of_year: doy,
257
31.7k
        }
258
31.7k
    }
259
260
109k
    fn to_ordinal(self) -> OrdinalDate {
261
        //LISTING 17.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
262
        //This is only the terms relying on month and day.
263
109k
        let offset_m = 30 * ((self.0.month as u16) - 1);
264
109k
        let offset_d = self.0.day as u16;
265
109k
        OrdinalDate {
266
109k
            year: self.0.year,
267
109k
            day_of_year: offset_m + offset_d,
268
109k
        }
269
109k
    }
270
271
31.2k
    fn from_ordinal_unchecked(ord: OrdinalDate) -> Self {
272
        //LISTING 17.10 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
273
        //This is only the terms relying on month and day.
274
        //This is modified to use ordinal days instead of days from epoch.
275
31.2k
        let month = (1 + (ord.day_of_year - 1).div_euclid(30)) as u8;
276
31.2k
        let month_start = Self(CommonDate::new(ord.year, month, 1)).to_ordinal();
277
31.2k
        let day = (1 + ord.day_of_year - month_start.day_of_year) as u8;
278
31.2k
        FrenchRevArith(CommonDate::new(ord.year, month, day))
279
31.2k
    }
280
}
281
282
impl<const L: bool> FrenchRevArith<L> {
283
    /// Returns L
284
0
    pub fn is_adjusted(self) -> bool {
285
0
        L
286
0
    }
287
}
288
289
impl<const L: bool> HasEpagemonae<Sansculottide> for FrenchRevArith<L> {
290
12.3k
    fn epagomenae(self) -> Option<Sansculottide> {
291
12.3k
        if self.0.month == NON_MONTH {
292
5.81k
            Sansculottide::from_u8(self.0.day)
293
        } else {
294
6.56k
            None
295
        }
296
12.3k
    }
297
298
16.4k
    fn epagomenae_count(f_year: i32) -> u8 {
299
16.4k
        if FrenchRevArith::<L>::is_leap(f_year) {
300
5.17k
            6
301
        } else {
302
11.2k
            5
303
        }
304
16.4k
    }
305
}
306
307
impl<const L: bool> Perennial<FrenchRevMonth, FrenchRevWeekday> for FrenchRevArith<L> {
308
18.8k
    fn weekday(self) -> Option<FrenchRevWeekday> {
309
18.8k
        if self.0.month == NON_MONTH {
310
99
            None
311
        } else {
312
18.7k
            FrenchRevWeekday::from_i64((self.0.day as i64).adjusted_remainder(10))
313
        }
314
18.8k
    }
315
316
5.09k
    fn days_per_week() -> u8 {
317
5.09k
        10
318
5.09k
    }
319
320
5.09k
    fn weeks_per_month() -> u8 {
321
5.09k
        3
322
5.09k
    }
323
}
324
325
impl<const L: bool> HasLeapYears for FrenchRevArith<L> {
326
20.7k
    fn is_leap(year: i32) -> bool {
327
        //LISTING 17.8 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
328
        //Modified to use L
329
20.7k
        let f_year = if L { 
year + 19.73k
} else {
year11.0k
};
330
20.7k
        let m4 = f_year.modulus(4);
331
20.7k
        let m400 = f_year.modulus(400);
332
20.7k
        let m4000 = f_year.modulus(4000);
333
20.7k
        m4 == 0 && (
m400 != 1006.59k
&&
m400 != 2006.50k
&&
m400 != 3006.49k
) &&
m4000 != 06.47k
334
20.7k
    }
335
}
336
337
impl<const L: bool> CalculatedBounds for FrenchRevArith<L> {}
338
339
impl<const L: bool> Epoch for FrenchRevArith<L> {
340
108k
    fn epoch() -> Fixed {
341
108k
        Gregorian::try_from_common_date(FRENCH_EPOCH_GREGORIAN)
342
108k
            .expect("Epoch known to be valid")
343
108k
            .to_fixed()
344
108k
    }
345
}
346
347
impl<const L: bool> FromFixed for FrenchRevArith<L> {
348
30.7k
    fn from_fixed(fixed_date: Fixed) -> FrenchRevArith<L> {
349
        //LISTING 17.10 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
350
        //Split compared to original
351
30.7k
        let ord = Self::ordinal_from_fixed(fixed_date);
352
30.7k
        Self::from_ordinal_unchecked(ord)
353
30.7k
    }
354
}
355
356
impl<const L: bool> ToFixed for FrenchRevArith<L> {
357
73.8k
    fn to_fixed(self) -> Fixed {
358
        //LISTING 17.9 (*Calendrical Calculations: The Ultimate Edition* by Reingold & Dershowitz.)
359
        //Split compared to original: terms relying on month and day are processed in to_ordinal
360
73.8k
        let year = self.0.year as i64;
361
73.8k
        let y_adj = if L { 
136.9k
} else {
036.9k
};
362
363
73.8k
        let offset_e = Self::epoch().get_day_i() - 1;
364
73.8k
        let offset_y = 365 * (year - 1);
365
73.8k
        let offset_leap = (year + y_adj - 1).div_euclid(4) - (year + y_adj - 1).div_euclid(100)
366
73.8k
            + (year + y_adj - 1).div_euclid(400)
367
73.8k
            - (year + y_adj - 1).div_euclid(4000);
368
73.8k
        let ord = self.to_ordinal().day_of_year as i64;
369
73.8k
        Fixed::cast_new(offset_e + offset_y + offset_leap + ord)
370
73.8k
    }
371
}
372
373
impl<const L: bool> ToFromCommonDate<FrenchRevMonth> for FrenchRevArith<L> {
374
75.0k
    fn to_common_date(self) -> CommonDate {
375
75.0k
        self.0
376
75.0k
    }
377
378
21.7k
    fn from_common_date_unchecked(date: CommonDate) -> Self {
379
21.7k
        debug_assert!(Self::valid_ymd(date).is_ok());
380
21.7k
        Self(date)
381
21.7k
    }
382
383
46.6k
    fn valid_ymd(date: CommonDate) -> Result<(), CalendarError> {
384
46.6k
        if date.month < 1 || 
date.month > NON_MONTH46.1k
{
385
1.53k
            Err(CalendarError::InvalidMonth)
386
45.1k
        } else if date.day < 1 {
387
512
            Err(CalendarError::InvalidDay)
388
44.5k
        } else if date.month < NON_MONTH && 
date.day > 3028.7k
{
389
512
            Err(CalendarError::InvalidDay)
390
44.0k
        } else if date.month == NON_MONTH
391
15.8k
            && date.day > FrenchRevArith::<L>::epagomenae_count(date.year)
392
        {
393
0
            Err(CalendarError::InvalidDay)
394
        } else {
395
44.0k
            Ok(())
396
        }
397
46.6k
    }
398
399
515
    fn year_end_date(year: i32) -> CommonDate {
400
515
        CommonDate::new(year, NON_MONTH, FrenchRevArith::<L>::epagomenae_count(year))
401
515
    }
402
403
0
    fn month_length(_year: i32, _month: FrenchRevMonth) -> u8 {
404
0
        30
405
0
    }
406
}
407
408
impl<const L: bool> Quarter for FrenchRevArith<L> {
409
5.63k
    fn quarter(self) -> NonZero<u8> {
410
5.63k
        let m = self.to_common_date().month;
411
5.63k
        if m == NON_MONTH {
412
538
            NonZero::new(4 as u8).expect("4 != 0")
413
        } else {
414
5.09k
            NonZero::new(((m - 1) / 3) + 1).expect("(m-1)/3 > -1")
415
        }
416
5.63k
    }
417
}
418
419
/// Represents a date *and time* in the algorithmic approximation of the French Revolutionary Calendar
420
pub type FrenchRevArithMoment<const L: bool> = CalendarMoment<FrenchRevArith<L>>;
421
422
#[cfg(test)]
423
mod tests {
424
    use super::*;
425
    use proptest::proptest;
426
427
    #[test]
428
1
    fn leaps() {
429
1
        assert!(FrenchRevArith::<true>::is_leap(3));
430
1
        assert!(FrenchRevArith::<true>::is_leap(7));
431
1
        assert!(FrenchRevArith::<true>::is_leap(11));
432
1
        assert!(FrenchRevArith::<false>::is_leap(4));
433
1
        assert!(FrenchRevArith::<false>::is_leap(8));
434
1
        assert!(FrenchRevArith::<false>::is_leap(12));
435
1
    }
436
437
    #[test]
438
1
    fn revolutionary_events() {
439
        // https://en.wikipedia.org/wiki/Glossary_of_the_French_Revolution#Events_commonly_known_by_their_Revolutionary_dates
440
        // 13 Vendémiaire and 18 Brumaire can be mangled when L = false
441
1
        let event_list = [
442
1
            (
443
1
                CommonDate::new(2, FrenchRevMonth::Prairial as u8, 22),
444
1
                CommonDate::new(2, FrenchRevMonth::Prairial as u8, 22),
445
1
                CommonDate::new(1794, 6, 10),
446
1
            ),
447
1
            (
448
1
                CommonDate::new(2, FrenchRevMonth::Thermidor as u8, 9),
449
1
                CommonDate::new(2, FrenchRevMonth::Thermidor as u8, 9),
450
1
                CommonDate::new(1794, 7, 27),
451
1
            ),
452
1
            (
453
1
                CommonDate::new(4, FrenchRevMonth::Vendemiaire as u8, 13),
454
1
                CommonDate::new(4, FrenchRevMonth::Vendemiaire as u8, 13 + 1), //Supposed to be 13
455
1
                CommonDate::new(1795, 10, 5),
456
1
            ),
457
1
            (
458
1
                CommonDate::new(5, FrenchRevMonth::Fructidor as u8, 18),
459
1
                CommonDate::new(5, FrenchRevMonth::Fructidor as u8, 18),
460
1
                CommonDate::new(1797, 9, 4),
461
1
            ),
462
1
            (
463
1
                CommonDate::new(6, FrenchRevMonth::Floreal as u8, 22),
464
1
                CommonDate::new(6, FrenchRevMonth::Floreal as u8, 22),
465
1
                CommonDate::new(1798, 5, 11),
466
1
            ),
467
1
            (
468
1
                CommonDate::new(7, FrenchRevMonth::Prairial as u8, 30),
469
1
                CommonDate::new(7, FrenchRevMonth::Prairial as u8, 30),
470
1
                CommonDate::new(1799, 6, 18),
471
1
            ),
472
1
            (
473
1
                CommonDate::new(8, FrenchRevMonth::Brumaire as u8, 18),
474
1
                CommonDate::new(8, FrenchRevMonth::Brumaire as u8, 18 + 1), //Supposed to be 18
475
1
                CommonDate::new(1799, 11, 9),
476
1
            ),
477
1
            // Paris Commune
478
1
            (
479
1
                CommonDate::new(79, FrenchRevMonth::Floreal as u8, 16),
480
1
                CommonDate::new(79, FrenchRevMonth::Floreal as u8, 16),
481
1
                CommonDate::new(1871, 5, 6),
482
1
            ),
483
1
        ];
484
9
        for 
pair8
in event_list {
485
8
            let df0 = FrenchRevArith::<true>::try_from_common_date(pair.0)
486
8
                .unwrap()
487
8
                .to_fixed();
488
8
            let df1 = FrenchRevArith::<false>::try_from_common_date(pair.1)
489
8
                .unwrap()
490
8
                .to_fixed();
491
8
            let dg = Gregorian::try_from_common_date(pair.2).unwrap().to_fixed();
492
8
            assert_eq!(df0, dg);
493
8
            assert_eq!(df1, dg);
494
        }
495
1
    }
496
497
    proptest! {
498
        #[test]
499
        fn align_to_gregorian(year in 0..100) {
500
            // https://en.wikipedia.org/wiki/French_Republican_calendar
501
            // > Autumn:
502
            // >     Vendémiaire (...), starting 22, 23, or 24 September
503
            // >     Brumaire (...), starting 22, 23, or 24 October
504
            // >     Frimaire (...), starting 21, 22, or 23 November
505
            // > Winter:
506
            // >     Nivôse (...), starting 21, 22, or 23 December
507
            // >     Pluviôse (...), starting 20, 21, or 22 January
508
            // >     Ventôse (...), starting 19, 20, or 21 February
509
            // > Spring:
510
            // >     Germinal (...), starting 21 or 22 March
511
            // >     Floréal (...), starting 20 or 21 April
512
            // >     Prairial (...), starting 20 or 21 May
513
            // > Summer:
514
            // >     Messidor (...), starting 19 or 20 June
515
            // >     Thermidor (...), starting 19 or 20 July; ...
516
            // >     Fructidor (...), starting 18 or 19 August
517
            // Not clear how long this property is supposed to hold, given
518
            // the differing leap year rule. There can be off by one errors
519
            // if L is false.
520
            let d_list = [
521
                ( CommonDate{ year, month: 1, day: 1 }, 9, 22, 24),
522
                ( CommonDate{ year, month: 2, day: 1 }, 10, 22, 24),
523
                ( CommonDate{ year, month: 3, day: 1 }, 11, 21, 23),
524
                ( CommonDate{ year, month: 4, day: 1 }, 12, 21, 23),
525
                ( CommonDate{ year, month: 5, day: 1 }, 1, 20, 22),
526
                ( CommonDate{ year, month: 6, day: 1 }, 2, 19, 21),
527
                ( CommonDate{ year, month: 7, day: 1 }, 3, 21, 22),
528
                ( CommonDate{ year, month: 8, day: 1 }, 4, 20, 21),
529
                ( CommonDate{ year, month: 9, day: 1 }, 5, 20, 21),
530
                ( CommonDate{ year, month: 10, day: 1 }, 6, 19, 20),
531
                ( CommonDate{ year, month: 11, day: 1 }, 7, 19, 20),
532
                ( CommonDate{ year, month: 12, day: 1 }, 8, 18, 19),
533
            ];
534
            for item in d_list {
535
                let r0 = FrenchRevArith::<true>::try_from_common_date(item.0).unwrap();
536
                let f0 = r0.to_fixed();
537
                let r1 = FrenchRevArith::<false>::try_from_common_date(item.0).unwrap();
538
                let f1 = r1.to_fixed();
539
                let g = Gregorian::from_fixed(f0);
540
                let gc = g.to_common_date();
541
                assert_eq!(gc.month, item.1);
542
                assert!(item.2 <= gc.day && item.3 >= gc.day);
543
                assert!((f1.get_day_i() - f0.get_day_i()).abs() < 2);
544
            }
545
        }
546
    }
547
}