Skip to main content

icu_calendar/
calendar_arithmetic.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::duration::{DateDuration, DateDurationUnit};
6use crate::error::{
7    range_check, range_check_with_overflow, DateFromFieldsError, EcmaReferenceYearError,
8    MonthCodeError, MonthCodeParseError, UnknownEraError,
9};
10use crate::options::{DateAddOptions, DateDifferenceOptions};
11use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow};
12use crate::types::{DateFields, ValidMonthCode};
13use crate::{types, Calendar, DateError, RangeError};
14use core::cmp::Ordering;
15use core::fmt::Debug;
16use core::hash::{Hash, Hasher};
17use core::ops::RangeInclusive;
18
19/// The range ±2²⁷. We use i32::MIN since it is -2³¹
20///
21/// This range is currently global, and applied to both era years and
22/// extended years, but may be replaced with a per-calendar check in the future.
23///
24/// <https://github.com/unicode-org/icu4x/issues/7076>
25const VALID_YEAR_RANGE: RangeInclusive<i32> = (i32::MIN / 16)..=-(i32::MIN / 16);
26
27#[derive(Debug)]
28pub(crate) struct ArithmeticDate<C: DateFieldsResolver> {
29    pub year: C::YearInfo,
30    /// 1-based month of year
31    pub month: u8,
32    /// 1-based day of month
33    pub day: u8,
34}
35
36// Manual impls since the derive will introduce a C: Trait bound
37// and only the year value should be compared
38impl<C: DateFieldsResolver> Copy for ArithmeticDate<C> {}
39impl<C: DateFieldsResolver> Clone for ArithmeticDate<C> {
40    fn clone(&self) -> Self {
41        *self
42    }
43}
44
45impl<C: DateFieldsResolver> PartialEq for ArithmeticDate<C> {
46    fn eq(&self, other: &Self) -> bool {
47        self.year.to_extended_year() == other.year.to_extended_year()
48            && self.month == other.month
49            && self.day == other.day
50    }
51}
52
53impl<C: DateFieldsResolver> Eq for ArithmeticDate<C> {}
54
55impl<C: DateFieldsResolver> Ord for ArithmeticDate<C> {
56    fn cmp(&self, other: &Self) -> Ordering {
57        self.year
58            .to_extended_year()
59            .cmp(&other.year.to_extended_year())
60            .then(self.month.cmp(&other.month))
61            .then(self.day.cmp(&other.day))
62    }
63}
64
65impl<C: DateFieldsResolver> PartialOrd for ArithmeticDate<C> {
66    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
67        Some(self.cmp(other))
68    }
69}
70
71impl<C: DateFieldsResolver> Hash for ArithmeticDate<C> {
72    fn hash<H>(&self, state: &mut H)
73    where
74        H: Hasher,
75    {
76        self.year.to_extended_year().hash(state);
77        self.month.hash(state);
78        self.day.hash(state);
79    }
80}
81
82/// Maximum number of iterations when iterating through the days of a month; can be increased if necessary
83#[allow(dead_code)] // TODO: Remove dead code tag after use
84pub(crate) const MAX_ITERS_FOR_DAYS_OF_MONTH: u8 = 33;
85
86pub(crate) trait ToExtendedYear {
87    fn to_extended_year(&self) -> i32;
88}
89
90impl ToExtendedYear for i32 {
91    fn to_extended_year(&self) -> i32 {
92        *self
93    }
94}
95
96/// Trait for converting from era codes, month codes, and other fields to year/month/day ordinals.
97pub(crate) trait DateFieldsResolver: Calendar {
98    /// This stores the year as either an i32, or a type containing more
99    /// useful computational information.
100    type YearInfo: Copy + Debug + PartialEq + ToExtendedYear;
101
102    fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8;
103
104    fn months_in_provided_year(year: Self::YearInfo) -> u8;
105
106    /// Converts the era and era year to a YearInfo. If the calendar does not have eras,
107    /// this should always return an Err result.
108    fn year_info_from_era(
109        &self,
110        era: &[u8],
111        era_year: i32,
112    ) -> Result<Self::YearInfo, UnknownEraError>;
113
114    /// Converts an extended year to a YearInfo.
115    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo;
116
117    /// Calculates the ECMA reference year for the month code and day, or an error
118    /// if the month code and day are invalid.
119    ///
120    /// Note that this is called before any potential Overflow::Constrain application,
121    /// so this should accept out-of-range day values as if they are the highest possible
122    /// day for the given month.
123    fn reference_year_from_month_day(
124        &self,
125        month_code: ValidMonthCode,
126        day: u8,
127    ) -> Result<Self::YearInfo, EcmaReferenceYearError>;
128
129    /// Calculates the ordinal month for the given year and month code.
130    ///
131    /// The default impl is for non-lunisolar calendars with 12 months!
132    #[inline]
133    fn ordinal_month_from_code(
134        &self,
135        _year: &Self::YearInfo,
136        month_code: ValidMonthCode,
137        _options: DateFromFieldsOptions,
138    ) -> Result<u8, MonthCodeError> {
139        match month_code.to_tuple() {
140            (month_number @ 1..=12, false) => Ok(month_number),
141            _ => Err(MonthCodeError::NotInCalendar),
142        }
143    }
144
145    /// Calculates the month code from the given ordinal month and year.
146    ///
147    /// The caller must ensure that the ordinal is in range.
148    ///
149    /// The default impl is for non-lunisolar calendars!
150    #[inline]
151    fn month_code_from_ordinal(&self, _year: &Self::YearInfo, ordinal_month: u8) -> ValidMonthCode {
152        ValidMonthCode::new_unchecked(ordinal_month, false)
153    }
154}
155
156impl<C: DateFieldsResolver> ArithmeticDate<C> {
157    #[inline]
158    pub(crate) const fn new_unchecked(year: C::YearInfo, month: u8, day: u8) -> Self {
159        ArithmeticDate { year, month, day }
160    }
161
162    pub(crate) const fn cast<C2: DateFieldsResolver<YearInfo = C::YearInfo>>(
163        self,
164    ) -> ArithmeticDate<C2> {
165        ArithmeticDate {
166            year: self.year,
167            month: self.month,
168            day: self.day,
169        }
170    }
171
172    pub(crate) fn from_codes(
173        era: Option<&str>,
174        year: i32,
175        month_code: types::MonthCode,
176        day: u8,
177        calendar: &C,
178    ) -> Result<Self, DateError> {
179        let year = range_check(year, "year", VALID_YEAR_RANGE)?;
180        let year = if let Some(era) = era {
181            calendar.year_info_from_era(era.as_bytes(), year)?
182        } else {
183            calendar.year_info_from_extended(year)
184        };
185        let validated =
186            ValidMonthCode::try_from_utf8(month_code.0.as_bytes()).map_err(|e| match e {
187                MonthCodeParseError::InvalidSyntax => DateError::UnknownMonthCode(month_code),
188            })?;
189        let month = calendar
190            .ordinal_month_from_code(&year, validated, Default::default())
191            .map_err(|e| match e {
192                MonthCodeError::NotInCalendar | MonthCodeError::NotInYear => {
193                    DateError::UnknownMonthCode(month_code)
194                }
195            })?;
196
197        let day = range_check(day, "day", 1..=C::days_in_provided_month(year, month))?;
198
199        Ok(ArithmeticDate::new_unchecked(year, month, day))
200    }
201
202    pub(crate) fn from_fields(
203        fields: DateFields,
204        options: DateFromFieldsOptions,
205        calendar: &C,
206    ) -> Result<Self, DateFromFieldsError> {
207        let missing_fields_strategy = options.missing_fields_strategy.unwrap_or_default();
208
209        let day = match fields.day {
210            Some(day) => day,
211            None => match missing_fields_strategy {
212                MissingFieldsStrategy::Reject => return Err(DateFromFieldsError::NotEnoughFields),
213                MissingFieldsStrategy::Ecma => {
214                    if fields.extended_year.is_some() || fields.era_year.is_some() {
215                        // The ECMAScript strategy is to pick day 1, always, regardless of whether
216                        // that day exists for the month/year combo
217                        1
218                    } else {
219                        return Err(DateFromFieldsError::NotEnoughFields);
220                    }
221                }
222            },
223        };
224
225        if fields.month_code.is_none() && fields.ordinal_month.is_none() {
226            // We're returning this error early so that we return structural type
227            // errors before range errors, see comment in the year code below.
228            return Err(DateFromFieldsError::NotEnoughFields);
229        }
230
231        let mut valid_month_code = None;
232
233        // NOTE: The year/extendedyear range check is important to avoid arithmetic
234        // overflow in `year_info_from_era` and `year_info_from_extended`. It
235        // must happen before they are called.
236        //
237        // To better match the Temporal specification's order of operations, we try
238        // to return structural type errors (`NotEnoughFields`) before checking for range errors.
239        // This isn't behavior we *must* have, but it is not much additional work to maintain
240        // so we make an attempt.
241        let year = match (fields.era, fields.era_year) {
242            (None, None) => match fields.extended_year {
243                Some(extended_year) => calendar.year_info_from_extended(range_check(
244                    extended_year,
245                    "year",
246                    VALID_YEAR_RANGE,
247                )?),
248                None => match missing_fields_strategy {
249                    MissingFieldsStrategy::Reject => {
250                        return Err(DateFromFieldsError::NotEnoughFields)
251                    }
252                    MissingFieldsStrategy::Ecma => {
253                        match (fields.month_code, fields.ordinal_month) {
254                            (Some(month_code), None) => {
255                                let validated = ValidMonthCode::try_from_utf8(month_code)?;
256                                valid_month_code = Some(validated);
257                                calendar.reference_year_from_month_day(validated, day)?
258                            }
259                            _ => return Err(DateFromFieldsError::NotEnoughFields),
260                        }
261                    }
262                },
263            },
264            (Some(era), Some(era_year)) => {
265                let era_year_as_year_info = calendar
266                    .year_info_from_era(era, range_check(era_year, "year", VALID_YEAR_RANGE)?)?;
267                if let Some(extended_year) = fields.extended_year {
268                    if era_year_as_year_info
269                        != calendar.year_info_from_extended(range_check(
270                            extended_year,
271                            "year",
272                            VALID_YEAR_RANGE,
273                        )?)
274                    {
275                        return Err(DateFromFieldsError::InconsistentYear);
276                    }
277                }
278                era_year_as_year_info
279            }
280            // Era and Era Year must be both or neither
281            (Some(_), None) | (None, Some(_)) => return Err(DateFromFieldsError::NotEnoughFields),
282        };
283
284        let month = match fields.month_code {
285            Some(month_code) => {
286                let validated = match valid_month_code {
287                    Some(validated) => validated,
288                    None => ValidMonthCode::try_from_utf8(month_code)?,
289                };
290                let computed_month = calendar.ordinal_month_from_code(&year, validated, options)?;
291                if let Some(ordinal_month) = fields.ordinal_month {
292                    if computed_month != ordinal_month {
293                        return Err(DateFromFieldsError::InconsistentMonth);
294                    }
295                }
296                computed_month
297            }
298            None => match fields.ordinal_month {
299                Some(month) => month,
300                None => {
301                    debug_assert!(false, "Already checked above");
302                    return Err(DateFromFieldsError::NotEnoughFields);
303                }
304            },
305        };
306
307        let constrained_month = range_check_with_overflow(
308            month,
309            "month",
310            1..=C::months_in_provided_year(year),
311            options.overflow.unwrap_or_default(),
312        )?;
313        Ok(Self::new_unchecked(
314            year,
315            constrained_month,
316            range_check_with_overflow(
317                day,
318                "day",
319                1..=C::days_in_provided_month(year, constrained_month),
320                options.overflow.unwrap_or_default(),
321            )?,
322        ))
323    }
324
325    pub(crate) fn try_from_ymd(year: C::YearInfo, month: u8, day: u8) -> Result<Self, RangeError> {
326        range_check(month, "month", 1..=C::months_in_provided_year(year))?;
327        range_check(day, "day", 1..=C::days_in_provided_month(year, month))?;
328        Ok(ArithmeticDate::new_unchecked(year, month, day))
329    }
330
331    /// Implements the Temporal abstract operation BalanceNonISODate.
332    ///
333    /// This takes a year, month, and day, where the month and day might be out of range, then
334    /// balances excess months into the year field and excess days into the month field.
335    pub(crate) fn new_balanced(year: C::YearInfo, ordinal_month: i64, day: i64, cal: &C) -> Self {
336        // 1. Let _resolvedYear_ be _arithmeticYear_.
337        // 1. Let _resolvedMonth_ be _ordinalMonth_.
338        let mut resolved_year = year;
339        let mut resolved_month = ordinal_month;
340        // 1. Let _monthsInYear_ be CalendarMonthsInYear(_calendar_, _resolvedYear_).
341        let mut months_in_year = C::months_in_provided_year(resolved_year);
342        // 1. Repeat, while _resolvedMonth_ &le; 0,
343        //   1. Set _resolvedYear_ to _resolvedYear_ - 1.
344        //   1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
345        //   1. Set _resolvedMonth_ to _resolvedMonth_ + _monthsInYear_.
346        while resolved_month <= 0 {
347            resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1);
348            months_in_year = C::months_in_provided_year(resolved_year);
349            resolved_month += i64::from(months_in_year);
350        }
351        // 1. Repeat, while _resolvedMonth_ &gt; _monthsInYear_,
352        //   1. Set _resolvedMonth_ to _resolvedMonth_ - _monthsInYear_.
353        //   1. Set _resolvedYear_ to _resolvedYear_ + 1.
354        //   1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
355        while resolved_month > i64::from(months_in_year) {
356            resolved_month -= i64::from(months_in_year);
357            resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1);
358            months_in_year = C::months_in_provided_year(resolved_year);
359        }
360        debug_assert!(u8::try_from(resolved_month).is_ok());
361        let mut resolved_month = resolved_month as u8;
362        // 1. Let _resolvedDay_ be _day_.
363        let mut resolved_day = day;
364        // 1. Let _daysInMonth_ be CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
365        let mut days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
366        // 1. Repeat, while _resolvedDay_ &le; 0,
367        while resolved_day <= 0 {
368            //   1. Set _resolvedMonth_ to _resolvedMonth_ - 1.
369            //   1. If _resolvedMonth_ is 0, then
370            resolved_month -= 1;
371            if resolved_month == 0 {
372                //     1. Set _resolvedYear_ to _resolvedYear_ - 1.
373                //     1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
374                //     1. Set _resolvedMonth_ to _monthsInYear_.
375                resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1);
376                months_in_year = C::months_in_provided_year(resolved_year);
377                resolved_month = months_in_year;
378            }
379            //   1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
380            //   1. Set _resolvedDay_ to _resolvedDay_ + _daysInMonth_.
381            days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
382            resolved_day += i64::from(days_in_month);
383        }
384        // 1. Repeat, while _resolvedDay_ &gt; _daysInMonth_,
385        while resolved_day > i64::from(days_in_month) {
386            //   1. Set _resolvedDay_ to _resolvedDay_ - _daysInMonth_.
387            //   1. Set _resolvedMonth_ to _resolvedMonth_ + 1.
388            //   1. If _resolvedMonth_ &gt; _monthsInYear_, then
389            resolved_day -= i64::from(days_in_month);
390            resolved_month += 1;
391            if resolved_month > months_in_year {
392                //     1. Set _resolvedYear_ to _resolvedYear_ + 1.
393                //     1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_).
394                //     1. Set _resolvedMonth_ to 1.
395                resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1);
396                months_in_year = C::months_in_provided_year(resolved_year);
397                resolved_month = 1;
398            }
399            //   1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_).
400            days_in_month = C::days_in_provided_month(resolved_year, resolved_month);
401        }
402        debug_assert!(u8::try_from(resolved_day).is_ok());
403        let resolved_day = resolved_day as u8;
404        // 1. Return the Record { [[Year]]: _resolvedYear_, [[Month]]: _resolvedMonth_, [[Day]]: _resolvedDay_ }.
405        Self::new_unchecked(resolved_year, resolved_month, resolved_day)
406    }
407
408    /// Implements the Temporal abstract operation NonISODateSurpasses.
409    ///
410    /// This takes two dates (`self` and `other`), `duration`, and `sign` (either -1 or 1), then
411    /// returns whether adding the duration to `self` results in a year/month/day that exceeds
412    /// `other` in the direction indicated by `sign`, constraining the month but not the day.
413    pub(crate) fn surpasses(
414        &self,
415        other: &Self,
416        duration: DateDuration,
417        sign: i64,
418        cal: &C,
419    ) -> bool {
420        // 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_).
421        // 1. Let _y0_ be _parts_.[[Year]] + _years_.
422        let y0 = cal.year_info_from_extended(duration.add_years_to(self.year.to_extended_year()));
423        // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)).
424        let base_month_code = cal.month_code_from_ordinal(&self.year, self.month);
425        let constrain = DateFromFieldsOptions {
426            overflow: Some(Overflow::Constrain),
427            ..Default::default()
428        };
429        let m0_result = cal.ordinal_month_from_code(&y0, base_month_code, constrain);
430        let m0 = match m0_result {
431            Ok(m0) => m0,
432            Err(_) => {
433                debug_assert!(
434                    false,
435                    "valid month code for calendar, and constrained to the year"
436                );
437                1
438            }
439        };
440        // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_ + 1, 0).
441        let end_of_month = Self::new_balanced(y0, duration.add_months_to(m0) + 1, 0, cal);
442        // 1. Let _baseDay_ be _parts_.[[Day]].
443        let base_day = self.day;
444        let y1;
445        let m1;
446        let d1;
447        // 1. If _weeks_ is not 0 or _days_ is not 0, then
448        if duration.weeks != 0 || duration.days != 0 {
449            //   1. If _baseDay_ &lt; _endOfMonth_.[[Day]], then
450            //     1. Let _regulatedDay_ be _baseDay_.
451            //   1. Else,
452            //     1. Let _regulatedDay_ be _endOfMonth_.[[Day]].
453            let regulated_day = if base_day < end_of_month.day {
454                base_day
455            } else {
456                end_of_month.day
457            };
458            //   1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + 7 * _weeks_ + _days_).
459            //   1. Let _y1_ be _balancedDate_.[[Year]].
460            //   1. Let _m1_ be _balancedDate_.[[Month]].
461            //   1. Let _d1_ be _balancedDate_.[[Day]].
462            let balanced_date = Self::new_balanced(
463                end_of_month.year,
464                i64::from(end_of_month.month),
465                duration.add_weeks_and_days_to(regulated_day),
466                cal,
467            );
468            y1 = balanced_date.year;
469            m1 = balanced_date.month;
470            d1 = balanced_date.day;
471        } else {
472            // 1. Else,
473            //   1. Let _y1_ be _endOfMonth_.[[Year]].
474            //   1. Let _m1_ be _endOfMonth_.[[Month]].
475            //   1. Let _d1_ be _baseDay_.
476            y1 = end_of_month.year;
477            m1 = end_of_month.month;
478            d1 = base_day;
479        }
480        // 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_).
481        // 1. If _y1_ ≠ _calDate2_.[[Year]], then
482        //   1. If _sign_ × (_y1_ - _calDate2_.[[Year]]) > 0, return *true*.
483        // 1. Else if _m1_ ≠ _calDate2_.[[Month]], then
484        //   1. If _sign_ × (_m1_ - _calDate2_.[[Month]]) > 0, return *true*.
485        // 1. Else if _d1_ ≠ _calDate2_.[[Day]], then
486        //   1. If _sign_ × (_d1_ - _calDate2_.[[Day]]) > 0, return *true*.
487        #[allow(clippy::collapsible_if)] // to align with the spec
488        if y1 != other.year {
489            if sign * (i64::from(y1.to_extended_year()) - i64::from(other.year.to_extended_year()))
490                > 0
491            {
492                return true;
493            }
494        } else if m1 != other.month {
495            if sign * (i64::from(m1) - i64::from(other.month)) > 0 {
496                return true;
497            }
498        } else if d1 != other.day {
499            if sign * (i64::from(d1) - i64::from(other.day)) > 0 {
500                return true;
501            }
502        }
503        // 1. Return *false*.
504        false
505    }
506
507    /// Implements the Temporal abstract operation NonISODateAdd.
508    ///
509    /// This takes a date (`self`) and `duration`, then returns a new date resulting from
510    /// adding `duration` to `self`, constrained according to `options`.
511    pub(crate) fn added(
512        &self,
513        duration: DateDuration,
514        cal: &C,
515        options: DateAddOptions,
516    ) -> Result<Self, DateError> {
517        // 1. Let _parts_ be CalendarISOToDate(_calendar_, _isoDate_).
518        // 1. Let _y0_ be _parts_.[[Year]] + _duration_.[[Years]].
519        let y0 = cal.year_info_from_extended(duration.add_years_to(self.year.to_extended_year()));
520        // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], _overflow_)).
521        let base_month = cal.month_code_from_ordinal(&self.year, self.month);
522        let m0 = cal
523            .ordinal_month_from_code(
524                &y0,
525                base_month,
526                DateFromFieldsOptions::from_add_options(options),
527            )
528            .map_err(|e| {
529                // TODO: Use a narrower error type here. For now, convert into DateError.
530                match e {
531                    MonthCodeError::NotInCalendar => {
532                        DateError::UnknownMonthCode(base_month.to_month_code())
533                    }
534                    MonthCodeError::NotInYear => {
535                        DateError::UnknownMonthCode(base_month.to_month_code())
536                    }
537                }
538            })?;
539        // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _duration_.[[Months]] + 1, 0).
540        let end_of_month = Self::new_balanced(y0, duration.add_months_to(m0) + 1, 0, cal);
541        // 1. Let _baseDay_ be _parts_.[[Day]].
542        let base_day = self.day;
543        // 1. If _baseDay_ &lt; _endOfMonth_.[[Day]], then
544        //   1. Let _regulatedDay_ be _baseDay_.
545        let regulated_day = if base_day < end_of_month.day {
546            base_day
547        } else {
548            // 1. Else,
549            //   1. If _overflow_ is ~reject~, throw a *RangeError* exception.
550            // Note: ICU4X default is constrain here
551            if matches!(options.overflow, Some(Overflow::Reject)) {
552                return Err(DateError::Range {
553                    field: "day",
554                    value: i32::from(base_day),
555                    min: 1,
556                    max: i32::from(end_of_month.day),
557                });
558            }
559            end_of_month.day
560        };
561        // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + 7 * _duration_.[[Weeks]] + _duration_.[[Days]]).
562        // 1. Let _result_ be ? CalendarIntegersToISO(_calendar_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]]).
563        // 1. Return _result_.
564        Ok(Self::new_balanced(
565            end_of_month.year,
566            i64::from(end_of_month.month),
567            duration.add_weeks_and_days_to(regulated_day),
568            cal,
569        ))
570    }
571
572    /// Implements the Temporal abstract operation NonISODateUntil.
573    ///
574    /// This takes a duration (`self`) and a date (`other`), then returns a duration that, when
575    /// added to `self`, results in `other`, with largest unit according to `options`.
576    pub(crate) fn until(
577        &self,
578        other: &Self,
579        cal: &C,
580        options: DateDifferenceOptions,
581    ) -> DateDuration {
582        // 1. Let _sign_ be -1 × CompareISODate(_one_, _two_).
583        // 1. If _sign_ = 0, return ZeroDateDuration().
584        let sign = match other.cmp(self) {
585            Ordering::Greater => 1i64,
586            Ordering::Equal => return DateDuration::default(),
587            Ordering::Less => -1i64,
588        };
589        // 1. Let _years_ be 0.
590        // 1. If _largestUnit_ is ~year~, then
591        //   1. Let _candidateYears_ be _sign_.
592        //   1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _candidateYears_, 0, 0, 0) is *false*,
593        //     1. Set _years_ to _candidateYears_.
594        //     1. Set _candidateYears_ to _candidateYears_ + _sign_.
595        let mut years = 0;
596        if matches!(options.largest_unit, Some(DateDurationUnit::Years)) {
597            let mut candidate_years = sign;
598            while !self.surpasses(
599                other,
600                DateDuration::from_signed_ymwd(candidate_years, 0, 0, 0),
601                sign,
602                cal,
603            ) {
604                years = candidate_years;
605                candidate_years += sign;
606            }
607        }
608        // 1. Let _months_ be 0.
609        // 1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
610        //   1. Let _candidateMonths_ be _sign_.
611        //   1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _candidateMonths_, 0, 0) is *false*,
612        //     1. Set _months_ to _candidateMonths_.
613        //     1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
614        let mut months = 0;
615        if matches!(
616            options.largest_unit,
617            Some(DateDurationUnit::Years) | Some(DateDurationUnit::Months)
618        ) {
619            let mut candidate_months = sign;
620            while !self.surpasses(
621                other,
622                DateDuration::from_signed_ymwd(years, candidate_months, 0, 0),
623                sign,
624                cal,
625            ) {
626                months = candidate_months;
627                candidate_months += sign;
628            }
629        }
630        // 1. Let _weeks_ be 0.
631        // 1. If _largestUnit_ is ~week~, then
632        //   1. Let _candidateWeeks_ be _sign_.
633        //   1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _candidateWeeks_, 0) is *false*,
634        //     1. Set _weeks_ to _candidateWeeks_.
635        //     1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
636        let mut weeks = 0;
637        if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) {
638            let mut candidate_weeks = sign;
639            while !self.surpasses(
640                other,
641                DateDuration::from_signed_ymwd(years, months, candidate_weeks, 0),
642                sign,
643                cal,
644            ) {
645                weeks = candidate_weeks;
646                candidate_weeks += sign;
647            }
648        }
649        // 1. Let _days_ be 0.
650        // 1. Let _candidateDays_ be _sign_.
651        // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _weeks_, _candidateDays_) is *false*,
652        //   1. Set _days_ to _candidateDays_.
653        //   1. Set _candidateDays_ to _candidateDays_ + _sign_.
654        let mut days = 0;
655        let mut candidate_days = sign;
656        while !self.surpasses(
657            other,
658            DateDuration::from_signed_ymwd(years, months, weeks, candidate_days),
659            sign,
660            cal,
661        ) {
662            days = candidate_days;
663            candidate_days += sign;
664        }
665        // 1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).
666        DateDuration::from_signed_ymwd(years, months, weeks, days)
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::cal::{abstract_gregorian::AbstractGregorian, iso::IsoEra};
674
675    #[test]
676    fn test_ord() {
677        let dates_in_order = [
678            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-10, 1, 1),
679            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-10, 1, 2),
680            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-10, 2, 1),
681            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-1, 1, 1),
682            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-1, 1, 2),
683            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(-1, 2, 1),
684            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(0, 1, 1),
685            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(0, 1, 2),
686            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(0, 2, 1),
687            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(1, 1, 1),
688            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(1, 1, 2),
689            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(1, 2, 1),
690            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(10, 1, 1),
691            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(10, 1, 2),
692            ArithmeticDate::<AbstractGregorian<IsoEra>>::new_unchecked(10, 2, 1),
693        ];
694        for (i, i_date) in dates_in_order.iter().enumerate() {
695            for (j, j_date) in dates_in_order.iter().enumerate() {
696                let result1 = i_date.cmp(j_date);
697                let result2 = j_date.cmp(i_date);
698                assert_eq!(result1.reverse(), result2);
699                assert_eq!(i.cmp(&j), i_date.cmp(j_date));
700            }
701        }
702    }
703
704    #[test]
705    pub fn zero() {
706        use crate::Date;
707        Date::try_new_iso(2024, 0, 1).unwrap_err();
708        Date::try_new_iso(2024, 1, 0).unwrap_err();
709        Date::try_new_iso(2024, 0, 0).unwrap_err();
710    }
711}