Skip to content

Commit c1b4dfb

Browse files
authored
Merge branch 'main' into stateful-surpasses
2 parents 77eb609 + 24bac4e commit c1b4dfb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+776
-358
lines changed

.github/pull_request_template.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ Reminder: try to use [Conventional Comments](https://conventionalcomments.org/)
55
66
Please see https://github.com/unicode-org/icu4x/blob/main/CONTRIBUTING.md for general
77
information on contributing to ICU4X.
8-
-->
8+
-->
9+
10+
## Changelog
11+
12+
<!-- Please fill in according to documents/process/changelog.md -->

.github/workflows/changelog.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
5+
name: Changelog Check
6+
7+
on:
8+
merge_group:
9+
pull_request:
10+
types: [opened, reopened, synchronize, edited]
11+
12+
concurrency:
13+
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
14+
# Only one per PR.
15+
cancel-in-progress: true
16+
17+
jobs:
18+
changelog:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Check for Changelog section
22+
shell: python
23+
env:
24+
PR_BODY: ${{ github.event.pull_request.body }}
25+
run: |
26+
import os
27+
import sys
28+
import re
29+
30+
body = os.environ.get("PR_BODY", "")
31+
32+
# Remove HTML comments
33+
body = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL)
34+
35+
# Find # Changelog section header (case-insensitive)
36+
header_match = re.search(r'^##\s*Changelog\b(.*?)$', body, re.IGNORECASE | re.MULTILINE)
37+
38+
if not header_match:
39+
print("::error::No '## Changelog' section found in the PR description.")
40+
print("Please add a '## Changelog' section to your PR body.")
41+
sys.exit(1)
42+
43+
header_suffix = header_match.group(1).strip()
44+
45+
# Check for escape hatches in the header line
46+
# Allow: # Changelog (N/A), # Changelog: N/A, # Changelog: TBD, etc.
47+
if re.search(r'\(N/A\)|:\s*N/A|:\s*TBD', header_suffix, re.IGNORECASE):
48+
print("Changelog check passed (opted out or deferred via header).")
49+
sys.exit(0)
50+
51+
# Extract content after the header until the next header or end of string
52+
content_start = header_match.end()
53+
# We look for the next header by looking for '^#+\s+' after a newline
54+
next_header_match = re.search(r'\n#+\s+', body[content_start:])
55+
if next_header_match:
56+
content = body[content_start:content_start + next_header_match.start()]
57+
else:
58+
content = body[content_start:]
59+
60+
content = content.strip()
61+
62+
# Also check if the content itself starts with N/A or TBD
63+
if re.match(r'^(N/A|TBD)\b', content, re.IGNORECASE):
64+
print("Changelog check passed (opted out or deferred in content).")
65+
sys.exit(0)
66+
67+
if not content:
68+
print("::error::The '## Changelog' section is empty.")
69+
print("Please provide a changelog entry, or use '## Changelog (N/A)' or '## Changelog: TBD' if appropriate.")
70+
sys.exit(1)
71+
72+
print("Changelog check passed.")
73+

components/calendar/src/any_calendar.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ macro_rules! make_any_calendar {
3434
) => {
3535
$(#[$any_calendar_meta])*
3636
#[derive(Debug, Clone)]
37-
// TODO(#3469): Decide on the best way to implement Ord.
3837
pub enum $any_calendar_ident {
3938
$(
4039
#[doc = concat!("A [`", stringify!($ty), "`] calendar")]
@@ -102,7 +101,7 @@ macro_rules! make_any_calendar {
102101
impl $crate::Calendar for $any_calendar_ident {
103102
type DateInner = $any_date_ident;
104103
type Year = $crate::types::YearInfo;
105-
type DifferenceError = $crate::error::MismatchedCalendarError;
104+
type DateCompatibilityError = $crate::error::MismatchedCalendarError;
106105

107106
fn from_codes(
108107
&self,
@@ -351,24 +350,39 @@ macro_rules! make_any_calendar {
351350
date1: &Self::DateInner,
352351
date2: &Self::DateInner,
353352
options: $crate::options::DateDifferenceOptions,
354-
) -> Result<$crate::types::DateDuration, Self::DifferenceError> {
355-
let Ok(r) = match (self, date1, date2) {
353+
) -> $crate::types::DateDuration {
354+
match (self, date1, date2) {
356355
$(
357-
(Self::$variant(ref c1), $any_date_ident::$variant(d1, q1), $any_date_ident::$variant(d2, q2)) if AnyCalendarable::identity(c1) == *q1 && q1 == q2 => {
358-
c1.until(d1, d2, options)
356+
(Self::$variant(ref c), $any_date_ident::$variant(d1, q1), $any_date_ident::$variant(d2, q2)) if AnyCalendarable::identity(c) == *q1 && q1 == q2 => {
357+
c.until(d1, d2, options)
359358
}
360359
)+
361360
$(
362361
#[allow(deprecated)]
363-
(Self::$deprecated_variant(ref c1), $any_date_ident::$deprecated_variant(d1, q1), $any_date_ident::$deprecated_variant(d2, q2)) if AnyCalendarable::identity(c1) == *q1 && q1 == q2 => {
364-
c1.until(d1, d2, options)
362+
(Self::$deprecated_variant(ref c), $any_date_ident::$deprecated_variant(d1, q1), $any_date_ident::$deprecated_variant(d2, q2)) if AnyCalendarable::identity(c) == *q1 && q1 == q2 => {
363+
c.until(d1, d2, options)
365364
}
366365
)*
367-
_ => {
368-
return Err($crate::error::MismatchedCalendarError);
369-
}
370-
};
371-
Ok(r)
366+
// This is only reached from misuse of from_raw, a semi-internal api
367+
_ => panic!(concat!(stringify!($any_calendar_ident), " with mismatched date type")),
368+
}
369+
}
370+
371+
fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
372+
match (self, other) {
373+
$(
374+
(Self::$variant(ref c1), Self::$variant(ref c2)) if c1.check_date_compatibility(c2).is_ok() => {
375+
Ok(())
376+
}
377+
)+
378+
$(
379+
#[allow(deprecated)]
380+
(Self::$deprecated_variant(ref c1), Self::$deprecated_variant(ref c2)) if c1.check_date_compatibility(c2).is_ok() => {
381+
Ok(())
382+
}
383+
)*
384+
_ => Err($crate::error::MismatchedCalendarError),
385+
}
372386
}
373387

374388
fn debug_name(&self) -> &'static str {
@@ -732,7 +746,7 @@ impl AnyCalendar {
732746
impl<C: AsCalendar<Calendar = AnyCalendar>> Date<C> {
733747
/// Convert this `Date<AnyCalendar>` to another [`AnyCalendar`], if conversion is needed
734748
pub fn convert_any<'a>(&self, calendar: &'a AnyCalendar) -> Date<Ref<'a, AnyCalendar>> {
735-
if calendar == self.calendar() {
749+
if calendar.check_date_compatibility(self.calendar()).is_ok() {
736750
Date::from_raw(*self.inner(), Ref(calendar))
737751
} else {
738752
self.to_calendar(Ref(calendar))
@@ -1406,7 +1420,7 @@ mod tests {
14061420

14071421
assert_eq!(
14081422
(month, day),
1409-
(date.month().as_input(), date.day_of_month().0),
1423+
(date.month().to_input(), date.day_of_month().0),
14101424
"Failed to roundtrip for calendar {}",
14111425
calendar.debug_name()
14121426
);

components/calendar/src/cal/abstract_gregorian.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl<Y: GregorianYears> crate::cal::scaffold::UnstableSealed for AbstractGregori
8484
impl<Y: GregorianYears> Calendar for AbstractGregorian<Y> {
8585
type DateInner = ArithmeticDate<AbstractGregorian<IsoEra>>;
8686
type Year = EraYear;
87-
type DifferenceError = core::convert::Infallible;
87+
type DateCompatibilityError = core::convert::Infallible;
8888

8989
fn from_codes(
9090
&self,
@@ -159,8 +159,15 @@ impl<Y: GregorianYears> Calendar for AbstractGregorian<Y> {
159159
date1: &Self::DateInner,
160160
date2: &Self::DateInner,
161161
options: DateDifferenceOptions,
162-
) -> Result<types::DateDuration, Self::DifferenceError> {
163-
Ok(date1.until(date2, &AbstractGregorian(IsoEra), options))
162+
) -> types::DateDuration {
163+
date1.until(date2, &AbstractGregorian(IsoEra), options)
164+
}
165+
166+
fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
167+
// The GregorianYears generic only affects constructors and year_info,
168+
// it does not affect date identity.
169+
let _ignored = other;
170+
Ok(())
164171
}
165172

166173
fn year_info(&self, date: &Self::DateInner) -> Self::Year {
@@ -212,7 +219,7 @@ macro_rules! impl_with_abstract_gregorian {
212219
impl crate::Calendar for $cal_ty {
213220
type DateInner = $inner_date_ty;
214221
type Year = types::EraYear;
215-
type DifferenceError = core::convert::Infallible;
222+
type DateCompatibilityError = core::convert::Infallible;
216223

217224
fn from_codes(
218225
&self,
@@ -304,12 +311,23 @@ macro_rules! impl_with_abstract_gregorian {
304311
date1: &Self::DateInner,
305312
date2: &Self::DateInner,
306313
options: crate::options::DateDifferenceOptions,
307-
) -> Result<crate::types::DateDuration, Self::DifferenceError> {
314+
) -> crate::types::DateDuration {
308315
let $self_ident = self;
309316
crate::cal::abstract_gregorian::AbstractGregorian($eras_expr)
310317
.until(&date1.0, &date2.0, options)
311318
}
312319

320+
fn check_date_compatibility(
321+
&self,
322+
other: &Self,
323+
) -> Result<(), Self::DateCompatibilityError> {
324+
let $self_ident = self;
325+
let c1 = crate::cal::abstract_gregorian::AbstractGregorian($eras_expr);
326+
let $self_ident = other;
327+
let c2 = crate::cal::abstract_gregorian::AbstractGregorian($eras_expr);
328+
c1.check_date_compatibility(&c2)
329+
}
330+
313331
fn year_info(&self, date: &Self::DateInner) -> Self::Year {
314332
let $self_ident = self;
315333
crate::cal::abstract_gregorian::AbstractGregorian($eras_expr).year_info(&date.0)

components/calendar/src/cal/coptic.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl crate::cal::scaffold::UnstableSealed for Coptic {}
130130
impl Calendar for Coptic {
131131
type DateInner = CopticDateInner;
132132
type Year = types::EraYear;
133-
type DifferenceError = core::convert::Infallible;
133+
type DateCompatibilityError = core::convert::Infallible;
134134

135135
fn from_codes(
136136
&self,
@@ -201,8 +201,12 @@ impl Calendar for Coptic {
201201
date1: &Self::DateInner,
202202
date2: &Self::DateInner,
203203
options: DateDifferenceOptions,
204-
) -> Result<types::DateDuration, Self::DifferenceError> {
205-
Ok(date1.0.until(&date2.0, self, options))
204+
) -> types::DateDuration {
205+
date1.0.until(&date2.0, self, options)
206+
}
207+
208+
fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
209+
Ok(())
206210
}
207211

208212
fn year_info(&self, date: &Self::DateInner) -> Self::Year {

components/calendar/src/cal/east_asian_traditional.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ pub trait Rules: Clone + core::fmt::Debug + crate::cal::scaffold::UnstableSealed
203203
Err(EcmaReferenceYearError::Unimplemented)
204204
}
205205

206+
/// The error that is returned by [`Self::check_date_compatibility`].
207+
///
208+
/// Set this to [`core::convert::Infallible`] if the type is a singleton or
209+
/// the parameterization does not affect calendar semantics.
210+
type DateCompatibilityError: core::fmt::Debug;
211+
212+
/// Checks whether two [`Rules`] values are equal for the purpose of [`Date`] interaction.
213+
fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError>;
214+
206215
/// The debug name for the calendar defined by these [`Rules`].
207216
fn debug_name(&self) -> &'static str {
208217
"EastAsianTraditional (custom)"
@@ -302,6 +311,12 @@ impl Rules for China {
302311
ecma_reference_year_common(month, day, EastAsianCalendarKind::Chinese)
303312
}
304313

314+
type DateCompatibilityError = core::convert::Infallible;
315+
316+
fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
317+
Ok(())
318+
}
319+
305320
fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
306321
Some(CalendarAlgorithm::Chinese)
307322
}
@@ -437,6 +452,12 @@ impl Rules for Korea {
437452
ecma_reference_year_common(month, day, EastAsianCalendarKind::Korean)
438453
}
439454

455+
type DateCompatibilityError = core::convert::Infallible;
456+
457+
fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
458+
Ok(())
459+
}
460+
440461
fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
441462
Some(CalendarAlgorithm::Dangi)
442463
}
@@ -459,7 +480,7 @@ impl Date<KoreanTraditional> {
459480
/// .expect("Failed to initialize Date instance.");
460481
///
461482
/// assert_eq!(date.cyclic_year().related_iso, 2025);
462-
/// assert_eq!(date.month().as_input(), Month::new(5));
483+
/// assert_eq!(date.month().to_input(), Month::new(5));
463484
/// assert_eq!(date.day_of_month().0, 25);
464485
/// ```
465486
pub fn try_new_korean_traditional(
@@ -669,7 +690,7 @@ impl<R: Rules> crate::cal::scaffold::UnstableSealed for EastAsianTraditional<R>
669690
impl<R: Rules> Calendar for EastAsianTraditional<R> {
670691
type DateInner = ChineseDateInner<R>;
671692
type Year = types::CyclicYear;
672-
type DifferenceError = core::convert::Infallible;
693+
type DateCompatibilityError = R::DateCompatibilityError;
673694

674695
fn from_codes(
675696
&self,
@@ -740,8 +761,12 @@ impl<R: Rules> Calendar for EastAsianTraditional<R> {
740761
date1: &Self::DateInner,
741762
date2: &Self::DateInner,
742763
options: DateDifferenceOptions,
743-
) -> Result<types::DateDuration, Self::DifferenceError> {
744-
Ok(date1.0.until(&date2.0, self, options))
764+
) -> types::DateDuration {
765+
date1.0.until(&date2.0, self, options)
766+
}
767+
768+
fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
769+
self.0.check_date_compatibility(&other.0)
745770
}
746771

747772
/// Obtain a name for the calendar for debug printing
@@ -808,7 +833,7 @@ impl Date<ChineseTraditional> {
808833
/// .expect("Failed to initialize Date instance.");
809834
///
810835
/// assert_eq!(date.cyclic_year().related_iso, 2025);
811-
/// assert_eq!(date.month().as_input(), Month::new(5));
836+
/// assert_eq!(date.month().to_input(), Month::new(5));
812837
/// assert_eq!(date.day_of_month().0, 25);
813838
/// ```
814839
pub fn try_new_chinese_traditional(
@@ -1146,6 +1171,12 @@ impl<R: Rules> Rules for EastAsianTraditionalYears<R> {
11461171
fn year_containing_rd(&self, rd: RataDie) -> EastAsianTraditionalYear {
11471172
self.1.year_containing_rd(rd)
11481173
}
1174+
1175+
type DateCompatibilityError = R::DateCompatibilityError;
1176+
1177+
fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
1178+
self.1.check_date_compatibility(&other.1)
1179+
}
11491180
}
11501181

11511182
#[cfg(test)]
@@ -1376,7 +1407,7 @@ mod test {
13761407

13771408
assert_eq!(chinese.cyclic_year().related_iso, -2636);
13781409
assert_eq!(chinese.month().ordinal, 1);
1379-
assert_eq!(chinese.month().as_input(), Month::new(1));
1410+
assert_eq!(chinese.month().to_input(), Month::new(1));
13801411
assert_eq!(chinese.day_of_month().0, 1);
13811412
assert_eq!(chinese.cyclic_year().year, 1);
13821413
assert_eq!(chinese.cyclic_year().related_iso, -2636);
@@ -1569,7 +1600,7 @@ mod test {
15691600
let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
15701601
let chinese = iso.to_calendar(ChineseTraditional::new());
15711602
assert_eq!(
1572-
chinese.month().as_input(),
1603+
chinese.month().to_input(),
15731604
case.month,
15741605
"Month codes did not match for test case: {case:?}"
15751606
);
@@ -1732,7 +1763,7 @@ mod test {
17321763
};
17331764
let date = Date::try_from_fields(fields, options, cal).unwrap();
17341765
assert_eq!(
1735-
date.month().as_input(),
1766+
date.month().to_input(),
17361767
Month::new(1),
17371768
"Month was successfully constrained"
17381769
);

0 commit comments

Comments
 (0)