icu_calendar 2.2.0

Date APIs for Gregorian and non-Gregorian calendars
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use calendrical_calculations::rata_die::RataDie;

use crate::duration::DateDuration;
use crate::error::{
    range_check, DateAddError, DateFromFieldsError, DateNewError, EcmaReferenceYearError,
    LunisolarDateError, MonthError, UnknownEraError,
};
use crate::options::{DateAddOptions, DateDifferenceOptions, DateDurationUnit};
use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow};
use crate::types::{DateFields, Month};
use crate::{types, Calendar, DateError, RangeError};
use core::cmp::Ordering;
use core::fmt::Debug;
use core::hash::{Hash, Hasher};
use core::ops::RangeInclusive;

/// This is checked by convenience constructors and `from_codes`.
/// Internally we don't care about this invariant.
pub(crate) const CONSTRUCTOR_YEAR_RANGE: RangeInclusive<i32> = -9999..=9999;

/// This is a fundamental invariant of `ArithmeticDate` and by extension all our
/// date types. Constructors that don't check [`CONSTRUCTOR_YEAR_RANGE`] check this range.
///
/// This is the range used by `Date::from_rata_die`, `Date::try_from_fields`,
/// and Date arithmetic operations.
pub(crate) const VALID_RD_RANGE: RangeInclusive<RataDie> =
    calendrical_calculations::gregorian::fixed_from_gregorian(-999999, 1, 1)
        ..=calendrical_calculations::gregorian::fixed_from_gregorian(999999, 12, 31);

/// This is a year range that exceeds the RD range for all calendars.
///
/// We *must* ensure dates are within `VALID_RD_RANGE` some point before constructing them.
///
/// However, we may need to perform a fair amount of calendar arithmetic before
/// getting to the point where we know if we are in range, and the calendar arithmetic
/// is fragile (chance of math issues, slowness, debug assertions) at high ranges.
///
/// So we try to early-check year values where possible. We use a "generous" year range
/// which is known to be wider than the valid RD range for any era in any currently
/// supported calendar.
///
/// `VALID_RD_RANGE` maps to 1031332 BH..=1030050 AH in the Islamic calendars, which have
/// the shortest years. We pick a slightly wider range so that temporary intermediate
/// values are not affected by this.
///
/// The tests in `extrema.rs` ensure that all in-range dates can be produced by the APIs that,
/// use this check, and that these year numbers map to out-of-range values for every era.
pub(crate) const GENEROUS_YEAR_RANGE: RangeInclusive<i32> = -1_040_000..=1_040_000;

/// These are for early-checking durations in `ArithmeticDate::add`
///
/// They are crate-public so they can be used in tests, but should probably not be used elsewhere.
pub(crate) const GENEROUS_MAX_YEARS: u32 =
    (*GENEROUS_YEAR_RANGE.end() - *GENEROUS_YEAR_RANGE.start()) as u32;
pub(crate) const GENEROUS_MAX_MONTHS: u32 = GENEROUS_MAX_YEARS * 13;
pub(crate) const GENEROUS_MAX_DAYS: u32 = GENEROUS_MAX_MONTHS * 31;

/// This is the year range all calendar specific computation can assume their data to be in.
///
/// It is wider than [`GENEROUS_YEAR_RANGE`]: `GENEROUS_YEAR_RANGE` is used for checking user
/// input, this is more of an internal invariant for assertions. New calendar implementations
/// must ensure they do not have overflows or panics for year (era year or extended year) within this range.
///
/// This value is computed by going past the max value producible by adding a duration,
/// which is preemptively capped at `GENEROUS_MAX_YEARS`, `GENEROUS_MAX_MONTHS`, `GENEROUS_MAX_DAYS`
/// in `added()`.
pub(crate) const SAFE_YEAR_RANGE: RangeInclusive<i32> = -8_000_000..=8_000_000;

// Invariant: VALID_RD_RANGE contains the date
#[derive(Debug)]
pub(crate) struct ArithmeticDate<C: DateFieldsResolver>(<C::YearInfo as PackWithMD>::Packed);

// Manual impls since the derive will introduce a C: Trait bound
// and only the year value should be compared
impl<C: DateFieldsResolver> Copy for ArithmeticDate<C> {}
impl<C: DateFieldsResolver> Clone for ArithmeticDate<C> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<C: DateFieldsResolver> PartialEq for ArithmeticDate<C> {
    fn eq(&self, other: &Self) -> bool {
        self.year().to_extended_year() == other.year().to_extended_year()
            && self.month() == other.month()
            && self.day() == other.day()
    }
}

impl<C: DateFieldsResolver> Eq for ArithmeticDate<C> {}

impl<C: DateFieldsResolver> Ord for ArithmeticDate<C> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.year()
            .to_extended_year()
            .cmp(&other.year().to_extended_year())
            .then(self.month().cmp(&other.month()))
            .then(self.day().cmp(&other.day()))
    }
}

impl<C: DateFieldsResolver> PartialOrd for ArithmeticDate<C> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<C: DateFieldsResolver> Hash for ArithmeticDate<C> {
    fn hash<H>(&self, state: &mut H)
    where
        H: Hasher,
    {
        self.year().to_extended_year().hash(state);
        self.month().hash(state);
        self.day().hash(state);
    }
}

/// Same data as [`ArithmeticDate`], but may be out of [`VALID_RD_RANGE`]
#[derive(Debug)]
struct UncheckedArithmeticDate<C: DateFieldsResolver> {
    year: C::YearInfo,
    ordinal_month: u8,
    day: u8,
}

impl<C: DateFieldsResolver> UncheckedArithmeticDate<C> {
    fn into_checked(self) -> Option<ArithmeticDate<C>> {
        let rd = C::to_rata_die_inner(self.year, self.ordinal_month, self.day);
        if !VALID_RD_RANGE.contains(&rd) {
            return None;
        }
        // Invariant checked above
        Some(ArithmeticDate::new_unchecked(
            self.year,
            self.ordinal_month,
            self.day,
        ))
    }
}

/// Maximum number of iterations when iterating through the days of a month; can be increased if necessary
#[allow(dead_code)] // TODO: Remove dead code tag after use
pub(crate) const MAX_ITERS_FOR_DAYS_OF_MONTH: u8 = 33;

pub(crate) trait ToExtendedYear {
    fn to_extended_year(&self) -> i32;
}

impl ToExtendedYear for i32 {
    fn to_extended_year(&self) -> i32 {
        *self
    }
}

pub(crate) trait PackWithMD: Copy {
    type Packed: Copy + Debug;

    fn pack(self, month: u8, day: u8) -> Self::Packed;
    fn unpack_year(packed: Self::Packed) -> Self;
    fn unpack_month(packed: Self::Packed) -> u8;
    fn unpack_day(packed: Self::Packed) -> u8;
}

impl PackWithMD for i32 {
    /// 2 bits unused, 21 bits year (`test_validity_ranges`),
    /// 4 bits month (1..13), 5 bits day (1..31)
    type Packed = [u8; 4];

    fn pack(self, month: u8, day: u8) -> Self::Packed {
        (self << 9 | i32::from(month) << 5 | i32::from(day)).to_le_bytes()
    }

    fn unpack_year(packed: Self::Packed) -> Self {
        let packed = i32::from_le_bytes(packed);
        packed >> 9
    }

    fn unpack_month(packed: Self::Packed) -> u8 {
        let packed = i32::from_le_bytes(packed);
        (packed >> 5 & 0b1111) as u8
    }

    fn unpack_day(packed: Self::Packed) -> u8 {
        let packed = i32::from_le_bytes(packed);
        (packed & 0b11111) as u8
    }
}

/// Trait for converting from era codes, month codes, and other fields to year/month/day ordinals.
pub(crate) trait DateFieldsResolver: Calendar {
    /// This stores the year as either an i32, or a type containing more
    /// useful computational information.
    type YearInfo: Copy + Debug + ToExtendedYear + PackWithMD;

    fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8;

    /// Converts the era and era year to a [`Self::YearInfo`]. If the calendar does not have eras,
    /// this should always return an Err result.
    ///
    /// Precondition: `era_year` is in [`SAFE_YEAR_RANGE`].
    fn extended_year_from_era_year_unchecked(
        &self,
        era: &[u8],
        era_year: i32,
    ) -> Result<i32, UnknownEraError>;

    /// Converts an extended year to a [`Self::YearInfo`].
    ///
    /// Precondition: `extended_year` is in [`GENEROUS_YEAR_RANGE`].
    ///
    /// Some calendars with complex calculations use this precondition to
    /// ensure that the year is in a range where their arithmetic code is well-behaved.
    ///
    /// Calendar implementors who need this are encouraged to debug assert that the invariant
    /// is upheld.
    ///
    /// This should primarily be used in constructors that range-check with
    /// the very narrow [`CONSTRUCTOR_YEAR_RANGE`], after that check. If the
    /// constructor checks [`VALID_RD_RANGE`], please use `year_info_from_extended_checked`.
    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo;

    /// Calculates the ECMA reference year (represented as an extended year)
    /// for the month code and day, or an error if the month code and day are invalid.
    ///
    /// Note that this is called before any potential `Overflow::Constrain` application,
    /// so this should accept out-of-range day values as if they are the highest possible
    /// day for the given month.
    fn reference_year_from_month_day(
        &self,
        month: Month,
        day: u8,
    ) -> Result<Self::YearInfo, EcmaReferenceYearError>;

    /// The number of months for the given year.
    ///
    /// The default impl is for non-lunisolar calendars with 12 months!
    fn months_in_provided_year(_year: Self::YearInfo) -> u8 {
        12
    }

    /// A lower bound for the number of months in `years` years, starting from the beginning of this year.
    ///
    /// This may be equal to the number of months, but should never be larger. Ideally,
    /// implementations should be close enough to the actual answer such that the error
    /// is bounded by a constant with respect to `years`.
    ///
    /// The default impl is for non-lunisolar calendars with 12 months!
    ///
    /// `until()` will debug assert if this ever returns a value greater than the
    /// month diff betweeen two dates.
    #[inline]
    fn min_months_from(_start: Self::YearInfo, years: i32) -> i32 {
        12 * years
    }

    /// Calculates the ordinal month for the given year and month code.
    ///
    /// The default impl is for non-lunisolar calendars!
    #[inline]
    fn ordinal_from_month(
        &self,
        year: Self::YearInfo,
        month: Month,
        _overflow: Overflow,
    ) -> Result<u8, MonthError> {
        match (month.number(), month.is_leap()) {
            (month_number, false)
                if (1..=Self::months_in_provided_year(year)).contains(&month_number) =>
            {
                Ok(month_number)
            }
            _ => Err(MonthError::NotInCalendar),
        }
    }

    /// Calculates the month code from the given ordinal month and year.
    ///
    /// The caller must ensure that the ordinal is in range.
    ///
    /// The default impl is for non-lunisolar calendars!
    #[inline]
    fn month_from_ordinal(&self, _year: Self::YearInfo, ordinal_month: u8) -> Month {
        Month::new_unchecked(ordinal_month, false)
    }

    // Date-to-RD conversion
    // Used internally for implementing date arithmetic
    fn to_rata_die_inner(year: Self::YearInfo, month: u8, day: u8) -> RataDie;
}

impl<C: DateFieldsResolver> ArithmeticDate<C> {
    pub(crate) fn to_rata_die(self) -> RataDie {
        C::to_rata_die_inner(self.year(), self.month(), self.day())
    }

    pub(crate) fn year(self) -> C::YearInfo {
        C::YearInfo::unpack_year(self.0)
    }

    pub(crate) fn month(self) -> u8 {
        C::YearInfo::unpack_month(self.0)
    }

    pub(crate) fn day(self) -> u8 {
        C::YearInfo::unpack_day(self.0)
    }

    /// Precondition: the date is in `VALID_RD_RANGE`.
    ///
    /// Use `UncheckedArithmeticDate` if you wish to generate intermediate out-of-range dates.
    #[inline]
    pub(crate) fn new_unchecked(year: C::YearInfo, month: u8, day: u8) -> Self {
        ArithmeticDate(C::YearInfo::pack(year, month, day))
    }

    pub(crate) fn cast<C2: DateFieldsResolver<YearInfo = C::YearInfo>>(self) -> ArithmeticDate<C2> {
        ArithmeticDate::new_unchecked(self.year(), self.month(), self.day())
    }

    // Used by `from_codes`, checks `CONSTRUCTOR_YEAR_RANGE`
    pub(crate) fn from_input_year_month_code_day(
        year: types::YearInput,
        month: Month,
        day: u8,
        calendar: &C,
    ) -> Result<Self, DateNewError> {
        let extended_year = match year {
            types::YearInput::Extended(y) => {
                if !CONSTRUCTOR_YEAR_RANGE.contains(&y) {
                    return Err(DateNewError::InvalidYear);
                }
                y
            }
            types::YearInput::EraYear(era, y) => {
                if !CONSTRUCTOR_YEAR_RANGE.contains(&y) {
                    return Err(DateNewError::InvalidYear);
                }
                calendar
                    .extended_year_from_era_year_unchecked(era.as_bytes(), y)
                    .map_err(|_| DateNewError::InvalidEra)?
            }
        };
        let year = calendar.year_info_from_extended(extended_year);

        let month = calendar
            .ordinal_from_month(year, month, Overflow::Reject)
            .map_err(|e| match e {
                MonthError::NotInCalendar => DateNewError::MonthNotInCalendar,
                MonthError::NotInYear => DateNewError::MonthNotInYear,
            })?;

        let max_day = C::days_in_provided_month(year, month);
        if day < 1 || day > max_day {
            return Err(DateNewError::InvalidDay { max: max_day });
        }

        // date is in the valid year range, and therefore in the valid RD range
        Ok(ArithmeticDate::new_unchecked(year, month, day))
    }

    // Used by calendar-specific constructors (lunisolar), checks `CONSTRUCTOR_YEAR_RANGE`
    pub(crate) fn try_from_ymd_lunisolar(
        year: i32,
        month: Month,
        day: u8,
        calendar: &C,
    ) -> Result<Self, LunisolarDateError> {
        if !CONSTRUCTOR_YEAR_RANGE.contains(&year) {
            return Err(LunisolarDateError::InvalidYear);
        }
        let year = calendar.year_info_from_extended(year);

        let month = match calendar.ordinal_from_month(year, month, Overflow::Reject) {
            Ok(month) => month,
            Err(MonthError::NotInCalendar) => return Err(LunisolarDateError::MonthNotInCalendar),
            Err(MonthError::NotInYear) => return Err(LunisolarDateError::MonthNotInYear),
        };

        let max_day = C::days_in_provided_month(year, month);
        if !(1..=max_day).contains(&day) {
            return Err(LunisolarDateError::InvalidDay { max: max_day });
        }

        // date is in the valid year range, and therefore in the valid RD range
        Ok(Self::new_unchecked(year, month, day))
    }

    pub(crate) fn from_fields(
        fields: DateFields,
        options: DateFromFieldsOptions,
        calendar: &C,
    ) -> Result<Self, DateFromFieldsError> {
        let missing_fields_strategy = options.missing_fields_strategy.unwrap_or_default();

        // DateFromFieldsOptions documents the default as reject
        let overflow = options.overflow.unwrap_or(Overflow::Reject);

        let day = match fields.day {
            Some(day) => day,
            None => match missing_fields_strategy {
                MissingFieldsStrategy::Reject => return Err(DateFromFieldsError::NotEnoughFields),
                MissingFieldsStrategy::Ecma => {
                    if fields.extended_year.is_some() || fields.era_year.is_some() {
                        // The ECMAScript strategy is to pick day 1, always, regardless of whether
                        // that day exists for the month/year combo
                        1
                    } else {
                        return Err(DateFromFieldsError::NotEnoughFields);
                    }
                }
            },
        };

        // We're returning these error early so that we return structural type
        // errors before range errors, see comment in the year code below.
        if fields.month_code.is_none() && fields.ordinal_month.is_none() && fields.month.is_none() {
            return Err(DateFromFieldsError::NotEnoughFields);
        }
        if fields.month_code.is_some() && fields.month.is_some() {
            return Err(DateFromFieldsError::TooManyFields);
        }

        let mut valid_month = None;

        // To better match the Temporal specification's order of operations, we try
        // to return structural type errors (`NotEnoughFields`) before checking for range errors.
        // This isn't behavior we *must* have, but it is not much additional work to maintain
        // so we make an attempt.
        let year = match (fields.era, fields.era_year) {
            (None, None) => match fields.extended_year {
                Some(extended_year) => {
                    if !GENEROUS_YEAR_RANGE.contains(&extended_year) {
                        return Err(DateFromFieldsError::Overflow);
                    }
                    calendar.year_info_from_extended(extended_year)
                }
                None => match missing_fields_strategy {
                    MissingFieldsStrategy::Reject => {
                        return Err(DateFromFieldsError::NotEnoughFields)
                    }
                    MissingFieldsStrategy::Ecma => {
                        let (m, d) = match (fields.month, fields.month_code, fields.ordinal_month) {
                            (Some(month), _, None) => (month, day),
                            (_, Some(month_code), None) => {
                                let validated = Month::try_from_utf8(month_code)?;
                                valid_month = Some(validated);
                                (validated, day)
                            }
                            _ => return Err(DateFromFieldsError::NotEnoughFields),
                        };
                        let ref_year = calendar.reference_year_from_month_day(m, d);
                        if ref_year.err() == Some(EcmaReferenceYearError::UseRegularIfConstrain)
                            && overflow == Overflow::Constrain
                        {
                            let new_valid_month = Month::new(m.number());
                            valid_month = Some(new_valid_month);
                            calendar.reference_year_from_month_day(new_valid_month, d)?
                        } else {
                            ref_year?
                        }
                    }
                },
            },
            (Some(era), Some(era_year)) => {
                if !GENEROUS_YEAR_RANGE.contains(&era_year) {
                    return Err(DateFromFieldsError::Overflow);
                }
                let extended_year =
                    calendar.extended_year_from_era_year_unchecked(era, era_year)?;
                let year = calendar.year_info_from_extended(extended_year);
                if let Some(extended_year) = fields.extended_year {
                    if year.to_extended_year() != extended_year {
                        return Err(DateFromFieldsError::InconsistentYear);
                    }
                }
                year
            }
            // Era and Era Year must be both or neither
            (Some(_), None) | (None, Some(_)) => return Err(DateFromFieldsError::NotEnoughFields),
        };

        let month = match (fields.month_code, fields.month) {
            (_, Some(month)) => {
                let computed_month = calendar.ordinal_from_month(year, month, overflow)?;
                if let Some(ordinal_month) = fields.ordinal_month {
                    if computed_month != ordinal_month {
                        return Err(DateFromFieldsError::InconsistentMonth);
                    }
                }
                computed_month
            }
            (Some(month_code), _) => {
                let validated = match valid_month {
                    Some(validated) => validated,
                    None => Month::try_from_utf8(month_code)?,
                };
                let computed_month = calendar.ordinal_from_month(year, validated, overflow)?;
                if let Some(ordinal_month) = fields.ordinal_month {
                    if computed_month != ordinal_month {
                        return Err(DateFromFieldsError::InconsistentMonth);
                    }
                }
                computed_month
            }
            (None, None) => match fields.ordinal_month {
                Some(month) => month,
                None => {
                    debug_assert!(false, "Already checked above");
                    return Err(DateFromFieldsError::NotEnoughFields);
                }
            },
        };

        let max_month = C::months_in_provided_year(year);
        let month = if matches!(overflow, Overflow::Constrain) {
            month.clamp(1, max_month)
        } else if (1..=max_month).contains(&month) {
            month
        } else {
            return Err(DateFromFieldsError::InvalidOrdinalMonth { max: max_month });
        };

        let max_day = C::days_in_provided_month(year, month);
        let day = if matches!(overflow, Overflow::Constrain) {
            day.clamp(1, max_day)
        } else if (1..=max_day).contains(&day) {
            day
        } else {
            return Err(DateFromFieldsError::InvalidDay { max: max_day });
        };
        let rd = C::to_rata_die_inner(year, month, day);

        // We early checked for a generous range of years, now we must check
        // to ensure we are actually in range for our core invariant.
        if !VALID_RD_RANGE.contains(&rd) {
            return Err(DateFromFieldsError::Overflow);
        }
        // We just checked the RD range above
        Ok(Self::new_unchecked(year, month, day))
    }

    // Used by calendar-specific constructors (non-lunisolar), checks `CONSTRUCTOR_YEAR_RANGE`
    pub(crate) fn from_year_month_day(
        year: i32,
        month: u8,
        day: u8,
        cal: &C,
    ) -> Result<Self, RangeError> {
        range_check(year, "year", CONSTRUCTOR_YEAR_RANGE)?;
        let year_info = cal.year_info_from_extended(year);
        range_check(month, "month", 1..=C::months_in_provided_year(year_info))?;
        range_check(day, "day", 1..=C::days_in_provided_month(year_info, month))?;
        // date is in the valid year range, and therefore in the valid RD range
        Ok(ArithmeticDate::new_unchecked(year_info, month, day))
    }

    // Used by calendar-specific constructors (Japanese), checks `CONSTRUCTOR_YEAR_RANGE`
    pub(crate) fn from_era_year_month_day(
        era: &str,
        year: i32,
        month: u8,
        day: u8,
        cal: &C,
    ) -> Result<Self, DateError> {
        range_check(year, "year", CONSTRUCTOR_YEAR_RANGE)?;
        let extended_year = cal.extended_year_from_era_year_unchecked(era.as_bytes(), year)?;
        let year_info = cal.year_info_from_extended(extended_year);
        range_check(month, "month", 1..=C::months_in_provided_year(year_info))?;
        range_check(day, "day", 1..=C::days_in_provided_month(year_info, month))?;
        // date is in the valid year range, and therefore in the valid RD range
        Ok(ArithmeticDate::new_unchecked(year_info, month, day))
    }

    /// Implements the Temporal abstract operation `BalanceNonISODate`.
    ///
    /// This takes a year, month, and day, where the month and day might be out of range, then
    /// balances excess months into the year field and excess days into the month field.
    ///
    /// In addition to specced behavior, this guarantees that it will produce an in-generous-year-range date.
    ///
    /// This does *not* necessarily produce something within [`VALID_RD_RANGE`], but it will ensure things
    /// are within [`GENEROUS_YEAR_RANGE`].
    fn new_balanced(
        year: C::YearInfo,
        ordinal_month: i32,
        day: i32,
        cal: &C,
    ) -> UncheckedArithmeticDate<C> {
        // 1. Let _resolvedYear_ be _arithmeticYear_.
        // 1. Let _resolvedMonth_ be _ordinalMonth_.
        let mut resolved_year = year;
        let mut resolved_month = ordinal_month;
        // 1. Let _monthsInYear_ be CalendarMonthsInYear(_calendar_, _resolvedYear_).
        let mut months_in_year = C::months_in_provided_year(resolved_year);
        // 1. Repeat, while _resolvedMonth_ &le; 0,
        //   1. Set _resolvedYear_ to _resolvedYear_ - 1.
        //   1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
        //   1. Set _resolvedMonth_ to _resolvedMonth_ + _monthsInYear_.
        while resolved_month <= 0 {
            resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1);
            months_in_year = C::months_in_provided_year(resolved_year);
            resolved_month += i32::from(months_in_year);
        }
        // 1. Repeat, while _resolvedMonth_ &gt; _monthsInYear_,
        //   1. Set _resolvedMonth_ to _resolvedMonth_ - _monthsInYear_.
        //   1. Set _resolvedYear_ to _resolvedYear_ + 1.
        //   1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
        while resolved_month > i32::from(months_in_year) {
            resolved_month -= i32::from(months_in_year);
            resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1);
            months_in_year = C::months_in_provided_year(resolved_year);
        }
        debug_assert!(u8::try_from(resolved_month).is_ok());
        let mut resolved_month = resolved_month as u8;
        // 1. Let _resolvedDay_ be _day_.
        let mut resolved_day = day;
        // 1. Let _daysInMonth_ be CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
        let mut days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
        // 1. Repeat, while _resolvedDay_ &le; 0,
        while resolved_day <= 0 {
            //   1. Set _resolvedMonth_ to _resolvedMonth_ - 1.
            //   1. If _resolvedMonth_ is 0, then
            resolved_month -= 1;
            if resolved_month == 0 {
                //     1. Set _resolvedYear_ to _resolvedYear_ - 1.
                //     1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
                //     1. Set _resolvedMonth_ to _monthsInYear_.
                resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1);
                months_in_year = C::months_in_provided_year(resolved_year);
                resolved_month = months_in_year;
            }
            //   1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
            //   1. Set _resolvedDay_ to _resolvedDay_ + _daysInMonth_.
            days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
            resolved_day += i32::from(days_in_month);
        }
        // 1. Repeat, while _resolvedDay_ &gt; _daysInMonth_,
        while resolved_day > i32::from(days_in_month) {
            //   1. Set _resolvedDay_ to _resolvedDay_ - _daysInMonth_.
            //   1. Set _resolvedMonth_ to _resolvedMonth_ + 1.
            //   1. If _resolvedMonth_ &gt; _monthsInYear_, then
            resolved_day -= i32::from(days_in_month);
            resolved_month += 1;
            if resolved_month > months_in_year {
                //     1. Set _resolvedYear_ to _resolvedYear_ + 1.
                //     1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
                //     1. Set _resolvedMonth_ to 1.
                resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1);
                months_in_year = C::months_in_provided_year(resolved_year);
                resolved_month = 1;
            }
            //   1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
            days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
        }
        debug_assert!(u8::try_from(resolved_day).is_ok());
        let resolved_day = resolved_day as u8;
        // 1. Return the Record { [[Year]]: _resolvedYear_, [[Month]]: _resolvedMonth_, [[Day]]: _resolvedDay_ }.
        UncheckedArithmeticDate {
            year: resolved_year,
            ordinal_month: resolved_month,
            day: resolved_day,
        }
    }

    /// Implements the Temporal abstract operation `CompareSurpasses` based on month code
    #[allow(clippy::collapsible_if, clippy::collapsible_else_if)] // to match the spec
    fn compare_surpasses_lexicographic(
        sign: i32,
        year: C::YearInfo,
        month: Month,
        day: u8,
        target: &Self,
        cal: &C,
    ) -> bool {
        // 1. If _year_ ≠ _target_.[[Year]], then
        //   1. If _sign_ × (_year_ - _target_.[[Year]]) > 0, return *true*.
        // 1. Else if _monthCode_ ≠ _target_.[[MonthCode]], then
        //   1. If _sign > 0, then
        //     1. If _monthCode_ is lexicographically greater than _target_.[[MonthCode]], return *true*.
        //   1. Else,
        //     1. If _target_.[[MonthCode]] is lexicographically greater than _monthCode_, return *true*.
        // (note: integer steps omitted)
        // 1. Else if _day_ ≠ _target_.[[Day]], then
        //   1. If _sign_ × (_day_ - _target_.[[Day]]) > 0, return *true*.
        if year.to_extended_year() != target.year().to_extended_year() {
            if sign * (year.to_extended_year() - target.year().to_extended_year()) > 0 {
                return true;
            }
        } else {
            let target_month = cal.month_from_ordinal(target.year(), target.month());
            if month != target_month {
                if sign > 0 {
                    if month > target_month {
                        return true;
                    }
                } else {
                    if month <= target_month {
                        return true;
                    }
                }
            } else if day != target.day() {
                if sign * (i32::from(day) - i32::from(target.day())) > 0 {
                    return true;
                }
            }
        }
        false
    }

    /// Implements the Temporal abstract operation `CompareSurpasses` based on month ordinal
    #[allow(clippy::collapsible_if, clippy::collapsible_else_if)] // to match the spec
    fn compare_surpasses_ordinal(
        sign: i32,
        year: C::YearInfo,
        month: u8,
        day: u8,
        target: &Self,
    ) -> bool {
        // 1. If _year_ ≠ _target_.[[Year]], then
        //   1. If _sign_ × (_year_ - _target_.[[Year]]) > 0, return *true*.
        // (note: month code steps omitted)
        // 1. Else if _month_ ≠ _target_.[[Month]], then
        //   1. If _sign_ × (_month_ - _target_.[[Month]]) > 0, return *true*.
        // 1. Else if _day_ ≠ _target_.[[Day]], then
        //   1. If _sign_ × (_day_ - _target_.[[Day]]) > 0, return *true*.
        if year.to_extended_year() != target.year().to_extended_year() {
            if sign * (year.to_extended_year() - target.year().to_extended_year()) > 0 {
                return true;
            }
        } else if month != target.month() {
            if sign * (i32::from(month) - i32::from(target.month())) > 0 {
                return true;
            }
        } else if day != target.day() {
            if sign * (i32::from(day) - i32::from(target.day())) > 0 {
                return true;
            }
        }
        false
    }

    /// Implements the Temporal abstract operation `NonISODateSurpasses`.
    ///
    /// This takes two dates (`self` and `other`), `duration`, and `sign` (either -1 or 1), then
    /// returns whether adding the duration to `self` results in a year/month/day that exceeds
    /// `other` in the direction indicated by `sign`, constraining the month but not the day.
    ///
    /// Note: This function is no longer used, but the code is retained for
    /// reference. The `until()` implementation uses `SurpassesChecker`, which
    /// implements the same spec, but with better performance.
    #[allow(dead_code)] // TODO: remove surpasses() method when no longer needed
    pub(crate) fn surpasses(
        &self,
        other: &Self,
        duration: DateDuration,
        sign: i32,
        cal: &C,
    ) -> bool {
        // 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_).
        let parts = self;
        // 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_).
        let cal_date_2 = other;
        // 1. Let _y0_ be _parts_.[[Year]] + _years_.
        let y0 =
            cal.year_info_from_extended(duration.add_years_to(parts.year().to_extended_year()));
        // 1. If CompareSurpasses(_sign_, _y0_, _parts_.[[MonthCode]], _parts_.[[Day]], _calDate2_) is *true*, return *true*.
        let base_month = cal.month_from_ordinal(parts.year(), parts.month());
        if Self::compare_surpasses_lexicographic(sign, y0, base_month, parts.day(), cal_date_2, cal)
        {
            return true;
        }
        // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)).
        let m0_result = cal.ordinal_from_month(y0, base_month, Overflow::Constrain);
        let m0 = match m0_result {
            Ok(m0) => m0,
            Err(_) => {
                debug_assert!(
                    false,
                    "valid month code for calendar, and constrained to the year"
                );
                1
            }
        };
        // 1. Let _monthsAdded_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_, 1).
        let months_added = Self::new_balanced(y0, duration.add_months_to(m0), 1, cal);

        // 1. If CompareSurpasses(_sign_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]], _parts_.[[Day]], _calDate2_) is *true*, return *true*.
        if Self::compare_surpasses_ordinal(
            sign,
            months_added.year,
            months_added.ordinal_month,
            parts.day(),
            cal_date_2,
        ) {
            return true;
        }
        // 1. If _weeks_ = 0 and _days_ = 0, return *false*.
        if duration.weeks == 0 && duration.days == 0 {
            return false;
        }
        // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]] + 1, 0).
        let end_of_month = Self::new_balanced(
            months_added.year,
            i32::from(months_added.ordinal_month + 1),
            0,
            cal,
        );
        // 1. Let _baseDay_ be _parts_.[[Day]].
        let base_day = parts.day();
        // 1. If _baseDay_ &le; _endOfMonth_.[[Day]], then
        //   1. Let _regulatedDay_ be _baseDay_.
        // 1. Else,
        //   1. Let _regulatedDay_ be _endOfMonth_.[[Day]].
        let regulated_day = if base_day < end_of_month.day {
            base_day
        } else {
            end_of_month.day
        };
        // 1. Let _daysInWeek_ be 7 (the number of days in a week for all supported calendars).
        // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + _daysInWeek_ * _weeks_ + _days_).
        // 1. Return CompareSurpasses(_sign_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]], _calDate2_).
        let balanced_date = Self::new_balanced(
            end_of_month.year,
            i32::from(end_of_month.ordinal_month),
            duration.add_weeks_and_days_to(regulated_day),
            cal,
        );

        Self::compare_surpasses_ordinal(
            sign,
            balanced_date.year,
            balanced_date.ordinal_month,
            balanced_date.day,
            cal_date_2,
        )
    }

    /// Implements the Temporal abstract operation `NonISODateAdd`.
    ///
    /// This takes a date (`self`) and `duration`, then returns a new date resulting from
    /// adding `duration` to `self`, constrained according to `options`.
    pub(crate) fn added(
        &self,
        duration: DateDuration,
        cal: &C,
        options: DateAddOptions,
    ) -> Result<Self, DateAddError> {
        // We preemptively protect ourselves from overly-large values
        //
        // This is mostly needed for avoiding overflows on Duration arithmetic,
        // this could also be handled by updating DateDuration::add_foo_to()
        // to return an error. However, that would lead to more error handling cluttering
        // surpasses().

        if duration.years > GENEROUS_MAX_YEARS
            || duration.months > GENEROUS_MAX_MONTHS
            || duration
                .weeks
                .saturating_mul(7)
                .saturating_add(duration.days)
                > GENEROUS_MAX_DAYS
        {
            return Err(DateAddError::Overflow);
        }

        // DateAddOptions documents the default as constrain
        let overflow = options.overflow.unwrap_or(Overflow::Constrain);

        // 1. Let _parts_ be CalendarISOToDate(_calendar_, _isoDate_).
        // 1. Let _y0_ be _parts_.[[Year]] + _duration_.[[Years]].
        let extended_year = duration.add_years_to(self.year().to_extended_year());
        if !GENEROUS_YEAR_RANGE.contains(&extended_year) {
            return Err(DateAddError::Overflow);
        }
        let y0 = cal.year_info_from_extended(extended_year);
        // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], _overflow_)).
        let base_month = cal.month_from_ordinal(self.year(), self.month());
        let m0_result = cal.ordinal_from_month(y0, base_month, overflow);
        let m0 = match m0_result {
            Ok(m0) => m0,
            Err(MonthError::NotInCalendar) => {
                debug_assert!(
                    false,
                    "Should never get NotInCalendar when performing arithmetic"
                );
                return Err(DateAddError::MonthNotInYear);
            }
            Err(MonthError::NotInYear) => return Err(DateAddError::MonthNotInYear),
        };
        // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _duration_.[[Months]] + 1, 0).
        let end_of_month = Self::new_balanced(y0, duration.add_months_to(m0) + 1, 0, cal);
        // 1. Let _baseDay_ be _parts_.[[Day]].
        let base_day = self.day();
        // 1. If _baseDay_ &le; _endOfMonth_.[[Day]], then
        //   1. Let _regulatedDay_ be _baseDay_.
        let regulated_day = if base_day <= end_of_month.day {
            base_day
        } else {
            // 1. Else,
            //   1. If _overflow_ is ~reject~, throw a *RangeError* exception.
            // Note: ICU4X default is constrain here
            if overflow == Overflow::Reject {
                return Err(DateAddError::InvalidDay {
                    max: end_of_month.day,
                });
            }
            end_of_month.day
        };

        // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + 7 * _duration_.[[Weeks]] + _duration_.[[Days]]).
        // 1. Let _result_ be ? CalendarIntegersToISO(_calendar_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]]).
        // 1. Return _result_.
        let balanced = Self::new_balanced(
            end_of_month.year,
            i32::from(end_of_month.ordinal_month),
            duration.add_weeks_and_days_to(regulated_day),
            cal,
        );

        balanced.into_checked().ok_or(DateAddError::Overflow)
    }

    /// Implements the Temporal abstract operation `NonISODateUntil`.
    ///
    /// This takes two dates (`self` and `other`), then returns a duration that, when
    /// added to `self`, results in `other`, with largest unit according to `options`.
    pub(crate) fn until(
        &self,
        other: &Self,
        cal: &C,
        options: DateDifferenceOptions,
    ) -> DateDuration {
        // Non-spec optimization: fast path for day/week diffs
        // Avoids quadratic behavior in surpasses() for days/weeks
        if matches!(
            options.largest_unit,
            Some(DateDurationUnit::Days) | Some(DateDurationUnit::Weeks)
        ) {
            let from = self.to_rata_die();
            let to = other.to_rata_die();
            let diff = to - from;
            let diff = match i32::try_from(diff) {
                Ok(d) => d,
                Err(_) => {
                    debug_assert!(false, "rata die diff out of i32 range");
                    if diff > 0 {
                        i32::MAX
                    } else {
                        i32::MIN
                    }
                }
            };
            if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) {
                return DateDuration::for_weeks_and_days(diff);
            } else {
                return DateDuration::for_days(diff);
            }
        }

        // 1. Let sign be -1 × CompareISODate(one, two).
        // 2. If sign = 0, return ZeroDateDuration().
        let sign = match other.cmp(self) {
            Ordering::Greater => 1,
            Ordering::Equal => return DateDuration::default(),
            Ordering::Less => -1,
        };

        // Non-spec optimization: Instead of calling a stateless
        // NonISODateSurpasses implementation that repeatedly recomputes many
        // values, we use a stateful implementation. It caches intermediate
        // produced by NonISODateSurpasses and reuses them in subsequent calls,
        // to avoid unnecessary recalculations.
        let mut surpasses_checker: SurpassesChecker<'_, C> =
            SurpassesChecker::new(self, other, sign, cal);

        // Preparation for non-specced optimization:
        // We don't want to spend time incrementally bumping it up one year
        // at a time, so let's pre-guess a year delta that is guaranteed to not
        // surpass.
        let year_diff = other.year().to_extended_year() - self.year().to_extended_year();
        let min_years = if year_diff == 0 { 0 } else { year_diff - sign };

        // clippy rejects: debug_assert!(!surpasses_checker.surpasses_months(min_months));
        #[cfg(debug_assertions)]
        {
            let mut debug_checker = SurpassesChecker::new(self, other, sign, cal);
            let min_years_valid = !debug_checker.surpasses_years(min_years);
            debug_assert!(min_years_valid);
        }

        // 3. Let years be 0.
        // 4. If largestUnit is year, then
        //   a. Let candidateYears be sign.
        //   b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, candidateYears, 0, 0, 0) is false,
        //     i. Set years to candidateYears.
        //     ii. Set candidateYears to candidateYears + sign.
        let mut years = 0;
        if matches!(options.largest_unit, Some(DateDurationUnit::Years)) {
            let mut candidate_years = sign;

            // Non-spec optimization: try to fast-forward candidate_years to
            // a year value that does not surpass
            if min_years != 0 {
                candidate_years = min_years
            };

            while !surpasses_checker.surpasses_years(candidate_years) {
                years = candidate_years;
                candidate_years += sign;
            }
        }
        // Freeze the checker's Years field state.
        surpasses_checker.set_years(years);

        // 5. Let months be 0.
        // 6. If largestUnit is year or largestUnit is month, then
        //   a. Let candidateMonths be sign.
        //   b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, candidateMonths, 0, 0) is false,
        //     i. Set months to candidateMonths.
        //     ii. Set candidateMonths to candidateMonths + sign.
        let mut months = 0;
        if matches!(
            options.largest_unit,
            Some(DateDurationUnit::Years) | Some(DateDurationUnit::Months)
        ) {
            let mut candidate_months = sign;

            // Non-spec optimization: try to fast-forward candidate_months to
            // a month value that does not surpass
            if options.largest_unit == Some(DateDurationUnit::Months) && min_years != 0 {
                // If largest_unit = Months, then compute the calendar-specific minimum number of
                // months corresponding to min_years.
                let min_months = C::min_months_from(self.year(), min_years);
                // clippy rejects: debug_assert!(!surpasses_checker.surpasses_months(min_months));
                #[cfg(debug_assertions)]
                {
                    let mut debug_checker = SurpassesChecker::new(self, other, sign, cal);
                    debug_checker.surpasses_years(years);
                    let min_months_valid = !debug_checker.surpasses_months(min_months);
                    debug_assert!(min_months_valid);
                }
                candidate_months = min_months
            }

            while !surpasses_checker.surpasses_months(candidate_months) {
                months = candidate_months;
                candidate_months += sign;
            }
        }
        // Freeze the checker's Months field state.
        surpasses_checker.set_months(months);

        // 7. Let weeks be 0.
        // 8. If largestUnit is week, then
        //   a. Let candidateWeeks be sign.
        //   b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, candidateWeeks, 0) is false,
        //     i. Set weeks to candidateWeeks.
        //     ii. Set candidateWeeks to candidateWeeks + sign.
        let mut weeks = 0;
        if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) {
            let mut candidate_weeks = sign;
            while !surpasses_checker.surpasses_weeks(candidate_weeks) {
                weeks = candidate_weeks;
                candidate_weeks += sign;
            }
        }
        // Freeze the checker's Weeks field state.
        surpasses_checker.set_weeks(weeks);

        // 9. Let days be 0.
        // 10. Let candidateDays be sign.
        // 11. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, weeks, candidateDays) is false,
        //   a. Set days to candidateDays.
        //   b. Set candidateDays to candidateDays + sign.
        let mut days = 0;
        // There is no pressing need to optimize candidate_days here: the early-return RD arithmetic
        // optimization will be hit if the largest_unit is weeks/days, and if it is months or years we will
        // arrive here with a candidate date that is at most 31 days off. We can run this loop 31 times.
        let mut candidate_days = sign;
        while !surpasses_checker.surpasses_days(candidate_days) {
            days = candidate_days;
            candidate_days += sign;
        }

        // 12. Return ! CreateDateDurationRecord(years, months, weeks, days).
        DateDuration::from_signed_ymwd(years, months, weeks, days)
    }
}

/// Stateful checker for iterative per-field `surpasses()` checks
///
/// This checker for `surpasses()` is designed to iteratively evaluate one field
/// at a time, from largest to smallest. After each field value is determined,
/// the caller must cache the computed state of the field's value by calling
/// `set_<field>(<field_value>)` before moving on to the next smaller field.
struct SurpassesChecker<'a, C: DateFieldsResolver> {
    parts: &'a ArithmeticDate<C>,
    cal_date_2: &'a ArithmeticDate<C>,
    sign: i32,
    cal: &'a C,
    y0: <C as DateFieldsResolver>::YearInfo,
    m0: u8,
    end_of_month: UncheckedArithmeticDate<C>,
    regulated_day: u8,
    weeks: i32,
}

impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> {
    fn new(
        parts: &'a ArithmeticDate<C>,
        cal_date_2: &'a ArithmeticDate<C>,
        sign: i32,
        cal: &'a C,
    ) -> Self {
        Self {
            parts,
            cal_date_2,
            sign,
            cal,
            y0: cal.year_info_from_extended(0),
            m0: 0,
            end_of_month: UncheckedArithmeticDate {
                year: cal.year_info_from_extended(0),
                ordinal_month: 0,
                day: 0,
            },
            regulated_day: 0,
            weeks: 0,
        }
    }

    // NOTE: Numbered comments refer to the Temporal `NonISODateSurpasses`
    // spec. A design goal of this code is to mirror the spec as closely as
    // possible. This dictates the names of variables and the sequence of
    // operations.

    fn surpasses_years(&mut self, years: i32) -> bool {
        // 1. Let parts be CalendarISOToDate(calendar, fromIsoDate).
        // 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate).
        // 3. Let y0 be parts.[[Year]] + years.
        self.y0 = self
            .cal
            .year_info_from_extended(self.parts.year().to_extended_year() + years);
        // 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true.
        let base_month = self
            .cal
            .month_from_ordinal(self.parts.year(), self.parts.month());
        let surpasses_years = ArithmeticDate::<C>::compare_surpasses_lexicographic(
            self.sign,
            self.y0,
            base_month,
            self.parts.day(),
            self.cal_date_2,
            self.cal,
        );
        // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)).

        let m0_result = self
            .cal
            .ordinal_from_month(self.y0, base_month, Overflow::Constrain);
        self.m0 = match m0_result {
            Ok(m0) => m0,
            Err(_) => {
                debug_assert!(
                    false,
                    "valid month code for calendar, and constrained to the year"
                );
                1
            }
        };

        // If the lexicographic check returns false, the spec continues to the
        // ordinal check, which is implemented in `surpasses_months`.
        surpasses_years || self.surpasses_months(0)
    }

    #[inline]
    fn set_years(&mut self, years: i32) {
        self.surpasses_years(years);
    }

    fn surpasses_months(&mut self, months: i32) -> bool {
        // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1).
        let months_added =
            ArithmeticDate::<C>::new_balanced(self.y0, months + i32::from(self.m0), 1, self.cal);

        // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true.
        // We do not need to perform any other checks because step 8 of the spec
        // guarantees an early return if weeks and days are 0.
        ArithmeticDate::<C>::compare_surpasses_ordinal(
            self.sign,
            months_added.year,
            months_added.ordinal_month,
            self.parts.day(),
            self.cal_date_2,
        )
    }

    fn set_months(&mut self, months: i32) {
        // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1).
        let months_added =
            ArithmeticDate::<C>::new_balanced(self.y0, months + i32::from(self.m0), 1, self.cal);

        // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0).
        self.end_of_month = ArithmeticDate::<C>::new_balanced(
            months_added.year,
            i32::from(months_added.ordinal_month) + 1,
            0,
            self.cal,
        );
        // 10. Let baseDay be parts.[[Day]].
        let base_day = self.parts.day();
        // 11. If baseDay ≤ endOfMonth.[[Day]], then
        //     a. Let regulatedDay be baseDay.
        // 12. Else,
        //     a. Let regulatedDay be endOfMonth.[[Day]].
        self.regulated_day = if base_day < self.end_of_month.day {
            base_day
        } else {
            self.end_of_month.day
        };
    }

    fn surpasses_weeks(&mut self, weeks: i32) -> bool {
        // 8. If weeks = 0 (*and days = 0*), return false.
        if weeks == 0 {
            return false;
        }

        // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars).
        // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days).
        // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2).
        let balanced_date = ArithmeticDate::<C>::new_balanced(
            self.end_of_month.year,
            i32::from(self.end_of_month.ordinal_month),
            7 * weeks + i32::from(self.regulated_day),
            self.cal,
        );

        self.weeks = weeks;

        ArithmeticDate::<C>::compare_surpasses_ordinal(
            self.sign,
            balanced_date.year,
            balanced_date.ordinal_month,
            balanced_date.day,
            self.cal_date_2,
        )
    }

    #[inline]
    fn set_weeks(&mut self, weeks: i32) {
        self.surpasses_weeks(weeks);
    }

    fn surpasses_days(&mut self, days: i32) -> bool {
        // 8. If weeks = 0 and days = 0, return false.
        if self.weeks == 0 && days == 0 {
            return false;
        }

        // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars).
        // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days).
        // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2).
        let balanced_date = ArithmeticDate::<C>::new_balanced(
            self.end_of_month.year,
            i32::from(self.end_of_month.ordinal_month),
            7 * self.weeks + days + i32::from(self.regulated_day),
            self.cal,
        );

        ArithmeticDate::<C>::compare_surpasses_ordinal(
            self.sign,
            balanced_date.year,
            balanced_date.ordinal_month,
            balanced_date.day,
            self.cal_date_2,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cal::Coptic;

    #[test]
    fn test_ord() {
        let dates_in_order = [
            ArithmeticDate::<Coptic>::new_unchecked(-10, 1, 1),
            ArithmeticDate::<Coptic>::new_unchecked(-10, 1, 2),
            ArithmeticDate::<Coptic>::new_unchecked(-10, 2, 1),
            ArithmeticDate::<Coptic>::new_unchecked(-1, 1, 1),
            ArithmeticDate::<Coptic>::new_unchecked(-1, 1, 2),
            ArithmeticDate::<Coptic>::new_unchecked(-1, 2, 1),
            ArithmeticDate::<Coptic>::new_unchecked(0, 1, 1),
            ArithmeticDate::<Coptic>::new_unchecked(0, 1, 2),
            ArithmeticDate::<Coptic>::new_unchecked(0, 2, 1),
            ArithmeticDate::<Coptic>::new_unchecked(1, 1, 1),
            ArithmeticDate::<Coptic>::new_unchecked(1, 1, 2),
            ArithmeticDate::<Coptic>::new_unchecked(1, 2, 1),
            ArithmeticDate::<Coptic>::new_unchecked(10, 1, 1),
            ArithmeticDate::<Coptic>::new_unchecked(10, 1, 2),
            ArithmeticDate::<Coptic>::new_unchecked(10, 2, 1),
        ];
        for (i, i_date) in dates_in_order.iter().enumerate() {
            for (j, j_date) in dates_in_order.iter().enumerate() {
                let result1 = i_date.cmp(j_date);
                let result2 = j_date.cmp(i_date);
                assert_eq!(result1.reverse(), result2);
                assert_eq!(i.cmp(&j), i_date.cmp(j_date));
            }
        }
    }

    #[test]
    pub fn zero() {
        use crate::Date;
        Date::try_new_iso(2024, 0, 1).unwrap_err();
        Date::try_new_iso(2024, 1, 0).unwrap_err();
        Date::try_new_iso(2024, 0, 0).unwrap_err();
    }

    #[test]
    fn test_validity_ranges() {
        use crate::{cal::*, Date};

        #[rustfmt::skip]
        let lowest_years = [
            Date::from_rata_die(*VALID_RD_RANGE.start(), Buddhist).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), ChineseTraditional::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Coptic).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteMihret)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Gregorian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Hebrew).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Hijri::new_umm_al_qura()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Hijri::new_tabular(HijriTabularLeapYears::TypeII, HijriTabularEpoch::Thursday)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Indian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Iso).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Japanese::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Julian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), KoreanTraditional::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Persian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.start(), Roc).year().extended_year(),
        ];

        #[rustfmt::skip]
        let highest_years = [
            Date::from_rata_die(*VALID_RD_RANGE.end(), Buddhist).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), ChineseTraditional::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Coptic).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteMihret)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Gregorian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Hebrew).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Hijri::new_umm_al_qura()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Hijri::new_tabular(HijriTabularLeapYears::TypeII, HijriTabularEpoch::Thursday)).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Indian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Iso).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Japanese::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Julian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), KoreanTraditional::new()).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Persian).year().extended_year(),
            Date::from_rata_die(*VALID_RD_RANGE.end(), Roc).year().extended_year(),
        ];

        // Valid RDs can represent all valid years
        assert!(lowest_years
            .iter()
            .all(|y| y <= CONSTRUCTOR_YEAR_RANGE.start()));
        assert!(highest_years
            .iter()
            .all(|y| y >= CONSTRUCTOR_YEAR_RANGE.end()));

        // All years are 21-bits
        assert!(-lowest_years.iter().copied().min().unwrap() < 1 << 20);
        assert!(highest_years.iter().copied().max().unwrap() < 1 << 20);
    }
}