icu4x 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -24,7 +24,7 @@ No locale data is bundled with the gem. Users generate only the data they need,
24
24
 
25
25
  ## Requirements
26
26
 
27
- - Ruby 3.2+
27
+ - Ruby 3.3+
28
28
  - Rust toolchain (only required when building from source; prebuilt binary gems are available for major platforms)
29
29
 
30
30
  ## Setup
@@ -114,7 +114,7 @@ lf.format(%w[Apple Banana Cherry])
114
114
  # Relative time formatting
115
115
  rtf = ICU4X::RelativeTimeFormat.new(locale, provider:)
116
116
  rtf.format(-3, :day)
117
- # => "3日前"
117
+ # => "3 日前"
118
118
 
119
119
  # Display names
120
120
  dn = ICU4X::DisplayNames.new(locale, provider:, type: :language)
data/ext/icu4x/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "icu4x"
3
- version = "0.9.0"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  publish = false
6
6
 
@@ -10,14 +10,14 @@ crate-type = ["cdylib"]
10
10
  [dependencies]
11
11
  magnus = "0.8"
12
12
  writeable = "0.6"
13
- icu_locale = "2.1"
14
- icu_provider = "2.1"
15
- icu_provider_blob = { version = "2.1", features = ["alloc", "export"] }
16
- icu_provider_source = { version = "2.1", features = ["networking", "experimental"] }
17
- icu_provider_export = "2.1"
18
- icu_provider_registry = "2.1"
19
- icu_provider_adapters = "2.1"
20
- icu = { version = "2.1", features = ["experimental"] }
13
+ icu_locale = "2.2"
14
+ icu_provider = "2.2"
15
+ icu_provider_blob = { version = "2.2", features = ["alloc", "export"] }
16
+ icu_provider_source = { version = "2.2", features = ["networking", "unstable"] }
17
+ icu_provider_export = "2.2"
18
+ icu_provider_registry = "2.2"
19
+ icu_provider_adapters = "2.2"
20
+ icu = { version = "2.2", features = ["unstable"] }
21
21
  fixed_decimal = "0.7"
22
22
  tinystr = "0.8"
23
23
  jiff = "0.2"
@@ -19,7 +19,7 @@ fn marker_lookup() -> &'static HashMap<&'static str, DataMarkerInfo> {
19
19
  LOOKUP.get_or_init(|| {
20
20
  let mut map = HashMap::new();
21
21
  macro_rules! cb {
22
- ($($marker_ty:ty:$marker:ident,)+ #[experimental] $($emarker_ty:ty:$emarker:ident,)+) => {
22
+ ($($marker_ty:ty:$marker:ident,)+ #[unstable] $($emarker_ty:ty:$emarker:ident,)+) => {
23
23
  $(
24
24
  // Add both the full type name and the short marker name
25
25
  map.insert(stringify!($marker_ty), <$marker_ty>::INFO);
@@ -4,21 +4,20 @@ use crate::parts_collector::{PartsCollector, parts_to_ruby_array};
4
4
  use icu::calendar::preferences::CalendarAlgorithm;
5
5
  use icu::calendar::{AnyCalendarKind, Date, Gregorian};
6
6
  use icu::datetime::fieldsets::enums::{
7
- CalendarPeriodFieldSet, CompositeDateTimeFieldSet, DateAndTimeFieldSet, DateFieldSet,
8
- TimeFieldSet,
7
+ CalendarPeriodFieldSet, CompositeDateTimeFieldSet, CompositeFieldSet, DateAndTimeFieldSet,
8
+ DateFieldSet, TimeFieldSet,
9
9
  };
10
- use icu::datetime::fieldsets::{self};
11
- use icu::datetime::options::Length;
12
- use icu::datetime::input::DateTime;
10
+ use icu::datetime::fieldsets::{self, zone};
11
+ use icu::datetime::options::{Length, TimePrecision, YearStyle as IcuYearStyle};
13
12
  use icu::datetime::parts as dt_parts;
14
13
  use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences};
15
14
  use icu::locale::preferences::extensions::unicode::keywords::HourCycle as IcuHourCycle;
16
- use icu::time::Time;
17
- use icu::time::zone::IanaParser;
15
+ use icu::time::zone::{models, IanaParser, UtcOffset, ZoneNameTimestamp};
16
+ use icu::time::{Time, TimeZone, TimeZoneInfo, ZonedDateTime};
18
17
  use icu_provider::buf::AsDeserializingBufferProvider;
19
18
  use icu4x_macros::RubySymbol;
20
19
  use jiff::Timestamp;
21
- use jiff::tz::TimeZone;
20
+ use jiff::tz::TimeZone as JiffTimeZone;
22
21
  use magnus::{Error, RArray, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*};
23
22
  use writeable::{Part, Writeable};
24
23
 
@@ -65,6 +64,26 @@ enum YearStyle {
65
64
  TwoDigit,
66
65
  }
67
66
 
67
+ /// Era display option
68
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
69
+ enum EraStyle {
70
+ Auto,
71
+ Full,
72
+ WithEra,
73
+ Never,
74
+ }
75
+
76
+ impl EraStyle {
77
+ fn to_icu_year_style(self) -> IcuYearStyle {
78
+ match self {
79
+ EraStyle::Auto => IcuYearStyle::Auto,
80
+ EraStyle::Full => IcuYearStyle::Full,
81
+ EraStyle::WithEra => IcuYearStyle::WithEra,
82
+ EraStyle::Never => IcuYearStyle::NoEra,
83
+ }
84
+ }
85
+ }
86
+
68
87
  /// Month component option
69
88
  #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
70
89
  enum MonthStyle {
@@ -218,7 +237,7 @@ impl Calendar {
218
237
  | AnyCalendarKind::HijriTabularTypeIIThursday
219
238
  | AnyCalendarKind::HijriUmmAlQura => Calendar::Islamic,
220
239
  AnyCalendarKind::Iso => Calendar::Gregory,
221
- AnyCalendarKind::Japanese | AnyCalendarKind::JapaneseExtended => Calendar::Japanese,
240
+ AnyCalendarKind::Japanese => Calendar::Japanese,
222
241
  AnyCalendarKind::Persian => Calendar::Persian,
223
242
  AnyCalendarKind::Roc => Calendar::Roc,
224
243
  _ => Calendar::Gregory,
@@ -256,14 +275,16 @@ fn part_to_symbol_name(part: &Part) -> &'static str {
256
275
  /// Ruby wrapper for ICU4X datetime formatters
257
276
  #[magnus::wrap(class = "ICU4X::DateTimeFormat", free_immediately, size)]
258
277
  pub struct DateTimeFormat {
259
- inner: DateTimeFormatter<CompositeDateTimeFieldSet>,
278
+ inner: DateTimeFormatter<CompositeFieldSet>,
260
279
  locale_str: String,
261
280
  date_style: Option<DateStyle>,
262
281
  time_style: Option<TimeStyle>,
263
282
  time_zone: Option<String>,
264
- jiff_timezone: Option<TimeZone>,
283
+ jiff_timezone: Option<JiffTimeZone>,
265
284
  calendar: Calendar,
266
285
  hour_cycle: Option<HourCycle>,
286
+ hour12: Option<bool>,
287
+ era: Option<EraStyle>,
267
288
  component_options: Option<ComponentOptions>,
268
289
  }
269
290
 
@@ -360,7 +381,7 @@ impl DateTimeFormat {
360
381
  ));
361
382
  }
362
383
  // Then create jiff TimeZone for offset calculation
363
- let jiff_tz = TimeZone::get(tz_str).map_err(|e| {
384
+ let jiff_tz = JiffTimeZone::get(tz_str).map_err(|e| {
364
385
  Error::new(
365
386
  ruby.exception_arg_error(),
366
387
  format!("invalid IANA timezone: {} ({})", tz_str, e),
@@ -379,15 +400,11 @@ impl DateTimeFormat {
379
400
  let hour_cycle =
380
401
  helpers::extract_symbol(ruby, &kwargs, "hour_cycle", HourCycle::from_ruby_symbol)?;
381
402
 
382
- // Extract hour12 option and convert to hour_cycle if hour_cycle is not specified
383
- // hour12: true → :h12, hour12: false → :h23
384
403
  let hour12: Option<bool> = kwargs.lookup::<_, Option<bool>>(ruby.to_symbol("hour12"))?;
385
- let hour_cycle = match (hour_cycle, hour12) {
386
- (Some(hc), _) => Some(hc), // hour_cycle takes precedence
387
- (None, Some(true)) => Some(HourCycle::H12),
388
- (None, Some(false)) => Some(HourCycle::H23),
389
- (None, None) => None,
390
- };
404
+
405
+ // Extract era option
406
+ let era =
407
+ helpers::extract_symbol(ruby, &kwargs, "era", EraStyle::from_ruby_symbol)?;
391
408
 
392
409
  // Get the error exception class
393
410
  let error_class = helpers::get_exception_class(ruby, "ICU4X::Error");
@@ -402,9 +419,10 @@ impl DateTimeFormat {
402
419
 
403
420
  // Create field set based on options
404
421
  let field_set = if has_component_options {
405
- Self::create_field_set_from_components(ruby, &component_options)?
422
+ Self::create_field_set_from_components(ruby, &component_options, era)?
423
+ .to_composite_field_set()
406
424
  } else {
407
- Self::create_field_set_from_style(date_style, time_style)
425
+ Self::create_field_set_from_style(date_style, time_style, era)
408
426
  };
409
427
 
410
428
  // Create formatter with calendar and hour_cycle preferences
@@ -414,6 +432,8 @@ impl DateTimeFormat {
414
432
  }
415
433
  if let Some(hc) = hour_cycle {
416
434
  prefs.hour_cycle = Some(hc.to_icu_hour_cycle());
435
+ } else if let Some(h12) = hour12 {
436
+ prefs.hour_cycle = Some(if h12 { IcuHourCycle::Clock12 } else { IcuHourCycle::Clock24 });
417
437
  }
418
438
 
419
439
  let formatter =
@@ -437,6 +457,8 @@ impl DateTimeFormat {
437
457
  jiff_timezone,
438
458
  calendar: resolved_calendar,
439
459
  hour_cycle,
460
+ hour12,
461
+ era,
440
462
  component_options: if has_component_options {
441
463
  Some(component_options)
442
464
  } else {
@@ -477,6 +499,7 @@ impl DateTimeFormat {
477
499
  fn create_field_set_from_components(
478
500
  ruby: &Ruby,
479
501
  opts: &ComponentOptions,
502
+ era: Option<EraStyle>,
480
503
  ) -> Result<CompositeDateTimeFieldSet, Error> {
481
504
  let has_date = opts.has_date_components();
482
505
  let has_time = opts.has_time_components();
@@ -485,9 +508,9 @@ impl DateTimeFormat {
485
508
  match (has_date, has_time) {
486
509
  (true, true) => {
487
510
  // Date and time components
488
- Ok(CompositeDateTimeFieldSet::DateTime(
489
- DateAndTimeFieldSet::YMDT(fieldsets::YMDT::for_length(length)),
490
- ))
511
+ let fs = fieldsets::YMDT::for_length(length);
512
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
513
+ Ok(CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDT(fs)))
491
514
  }
492
515
  (true, false) => {
493
516
  // Date only - choose field set based on which components are specified
@@ -498,13 +521,17 @@ impl DateTimeFormat {
498
521
 
499
522
  match (has_year, has_month, has_day, has_weekday) {
500
523
  // Year + Month + Day + Weekday
501
- (true, true, true, true) => Ok(CompositeDateTimeFieldSet::Date(
502
- DateFieldSet::YMDE(fieldsets::YMDE::for_length(length)),
503
- )),
524
+ (true, true, true, true) => {
525
+ let fs = fieldsets::YMDE::for_length(length);
526
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
527
+ Ok(CompositeDateTimeFieldSet::Date(DateFieldSet::YMDE(fs)))
528
+ }
504
529
  // Year + Month + Day
505
- (true, true, true, false) => Ok(CompositeDateTimeFieldSet::Date(
506
- DateFieldSet::YMD(fieldsets::YMD::for_length(length)),
507
- )),
530
+ (true, true, true, false) => {
531
+ let fs = fieldsets::YMD::for_length(length);
532
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
533
+ Ok(CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(fs)))
534
+ }
508
535
  // Month + Day + Weekday
509
536
  (false, true, true, true) => Ok(CompositeDateTimeFieldSet::Date(
510
537
  DateFieldSet::MDE(fieldsets::MDE::for_length(length)),
@@ -514,9 +541,11 @@ impl DateTimeFormat {
514
541
  DateFieldSet::MD(fieldsets::MD::for_length(length)),
515
542
  )),
516
543
  // Year + Month (calendar period)
517
- (true, true, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
518
- CalendarPeriodFieldSet::YM(fieldsets::YM::for_length(length)),
519
- )),
544
+ (true, true, false, _) => {
545
+ let fs = fieldsets::YM::for_length(length);
546
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
547
+ Ok(CompositeDateTimeFieldSet::CalendarPeriod(CalendarPeriodFieldSet::YM(fs)))
548
+ }
520
549
  // Month only (calendar period)
521
550
  (false, true, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
522
551
  CalendarPeriodFieldSet::M(fieldsets::M::for_length(length)),
@@ -534,13 +563,17 @@ impl DateTimeFormat {
534
563
  DateFieldSet::E(fieldsets::E::for_length(length)),
535
564
  )),
536
565
  // Year only (calendar period)
537
- (true, false, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
538
- CalendarPeriodFieldSet::Y(fieldsets::Y::for_length(length)),
539
- )),
566
+ (true, false, false, _) => {
567
+ let fs = fieldsets::Y::for_length(length);
568
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
569
+ Ok(CompositeDateTimeFieldSet::CalendarPeriod(CalendarPeriodFieldSet::Y(fs)))
570
+ }
540
571
  // Year + Day (not a standard combination, use YMD as fallback)
541
- (true, false, true, _) => Ok(CompositeDateTimeFieldSet::Date(
542
- DateFieldSet::YMD(fieldsets::YMD::for_length(length)),
543
- )),
572
+ (true, false, true, _) => {
573
+ let fs = fieldsets::YMD::for_length(length);
574
+ let fs = if let Some(s) = era { fs.with_year_style(s.to_icu_year_style()) } else { fs };
575
+ Ok(CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(fs)))
576
+ }
544
577
  // Should not happen - we checked has_date_components
545
578
  (false, false, false, false) => unreachable!(),
546
579
  }
@@ -562,7 +595,8 @@ impl DateTimeFormat {
562
595
  fn create_field_set_from_style(
563
596
  date_style: Option<DateStyle>,
564
597
  time_style: Option<TimeStyle>,
565
- ) -> CompositeDateTimeFieldSet {
598
+ era: Option<EraStyle>,
599
+ ) -> CompositeFieldSet {
566
600
  match (date_style, time_style) {
567
601
  (Some(ds), Some(ts)) => {
568
602
  // Both date and time
@@ -571,7 +605,9 @@ impl DateTimeFormat {
571
605
  (DateStyle::Medium, _) => fieldsets::YMDT::medium(),
572
606
  (DateStyle::Short, _) => fieldsets::YMDT::short(),
573
607
  };
608
+ let ymdt = if let Some(s) = era { ymdt.with_year_style(s.to_icu_year_style()) } else { ymdt };
574
609
  CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDT(ymdt))
610
+ .to_composite_field_set()
575
611
  }
576
612
  (Some(ds), None) => {
577
613
  // Date only
@@ -580,16 +616,30 @@ impl DateTimeFormat {
580
616
  DateStyle::Medium => fieldsets::YMD::medium(),
581
617
  DateStyle::Short => fieldsets::YMD::short(),
582
618
  };
583
- CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(ymd))
619
+ let ymd = if let Some(s) = era { ymd.with_year_style(s.to_icu_year_style()) } else { ymd };
620
+ CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(ymd)).to_composite_field_set()
584
621
  }
585
622
  (None, Some(ts)) => {
586
- // Time only
587
- let t = match ts {
588
- TimeStyle::Full | TimeStyle::Long => fieldsets::T::long(),
589
- TimeStyle::Medium => fieldsets::T::medium(),
590
- TimeStyle::Short => fieldsets::T::short(),
591
- };
592
- CompositeDateTimeFieldSet::Time(TimeFieldSet::T(t))
623
+ // Time only; long/full include timezone per CLDR convention
624
+ match ts {
625
+ TimeStyle::Full => CompositeFieldSet::TimeZone(
626
+ fieldsets::T::long().with_zone(zone::SpecificLong).into_enums(),
627
+ ),
628
+ TimeStyle::Long => CompositeFieldSet::TimeZone(
629
+ fieldsets::T::long().with_zone(zone::SpecificShort).into_enums(),
630
+ ),
631
+ TimeStyle::Medium => {
632
+ CompositeDateTimeFieldSet::Time(TimeFieldSet::T(fieldsets::T::medium()))
633
+ .to_composite_field_set()
634
+ }
635
+ // short omits seconds to match Intl.DateTimeFormat timeStyle: "short"
636
+ TimeStyle::Short => {
637
+ CompositeDateTimeFieldSet::Time(TimeFieldSet::T(
638
+ fieldsets::T::short().with_time_precision(TimePrecision::Minute),
639
+ ))
640
+ .to_composite_field_set()
641
+ }
642
+ }
593
643
  }
594
644
  (None, None) => {
595
645
  // Should not happen due to validation
@@ -635,8 +685,12 @@ impl DateTimeFormat {
635
685
  /// Prepare a Ruby Time value for formatting.
636
686
  ///
637
687
  /// Converts objects responding to #to_time, validates the result,
638
- /// and converts to ICU4X DateTime.
639
- fn prepare_datetime(&self, ruby: &Ruby, time: Value) -> Result<DateTime<Gregorian>, Error> {
688
+ /// and converts to ICU4X ZonedDateTime.
689
+ fn prepare_datetime(
690
+ &self,
691
+ ruby: &Ruby,
692
+ time: Value,
693
+ ) -> Result<ZonedDateTime<Gregorian, TimeZoneInfo<models::AtTime>>, Error> {
640
694
  // Convert to Time if the object responds to #to_time
641
695
  let time_value = if time.respond_to("to_time", false)? {
642
696
  time.funcall::<_, _, Value>("to_time", ())?
@@ -653,75 +707,57 @@ impl DateTimeFormat {
653
707
  ));
654
708
  }
655
709
 
656
- self.convert_time_to_datetime(ruby, time_value)
710
+ self.convert_time_to_zoned_datetime(ruby, time_value)
657
711
  }
658
712
 
659
- /// Convert Ruby Time to ICU4X DateTime<Gregorian>
713
+ /// Convert Ruby Time to ICU4X ZonedDateTime<Gregorian, TimeZoneInfo<AtTime>>
660
714
  ///
661
- /// If time_zone is specified, the UTC time is converted to local time in that timezone.
662
- /// Otherwise, the time is treated as UTC.
663
- fn convert_time_to_datetime(
715
+ /// If time_zone is specified, the time is represented in that timezone.
716
+ /// Otherwise, UTC is used.
717
+ fn convert_time_to_zoned_datetime(
664
718
  &self,
665
719
  ruby: &Ruby,
666
720
  time: Value,
667
- ) -> Result<DateTime<Gregorian>, Error> {
668
- // Get UTC time from Ruby Time object
669
- let utc_time: Value = time.funcall("getutc", ())?;
670
-
671
- let utc_year: i32 = utc_time.funcall("year", ())?;
672
- let utc_month: i32 = utc_time.funcall("month", ())?;
673
- let utc_day: i32 = utc_time.funcall("day", ())?;
674
- let utc_hour: i32 = utc_time.funcall("hour", ())?;
675
- let utc_min: i32 = utc_time.funcall("min", ())?;
676
- let utc_sec: i32 = utc_time.funcall("sec", ())?;
677
-
678
- // Get year, month, day, hour, min, sec in the target timezone
679
- let (year, month, day, hour, min, sec) = if let Some(ref tz) = self.jiff_timezone {
680
- // Create a jiff Timestamp from UTC components
681
- let timestamp = Timestamp::from_second(utc_time.funcall::<_, _, i64>("to_i", ())?)
682
- .map_err(|e| {
683
- Error::new(
684
- ruby.exception_arg_error(),
685
- format!("Invalid timestamp: {}", e),
686
- )
687
- })?;
721
+ ) -> Result<ZonedDateTime<Gregorian, TimeZoneInfo<models::AtTime>>, Error> {
722
+ let ts_secs: i64 = time.funcall("to_i", ())?;
688
723
 
689
- // Convert to local time in the target timezone
690
- let zoned = timestamp.to_zoned(tz.clone());
691
- let dt = zoned.datetime();
692
-
693
- (
694
- dt.year() as i32,
695
- dt.month() as i32,
696
- dt.day() as i32,
697
- dt.hour() as i32,
698
- dt.minute() as i32,
699
- dt.second() as i32,
700
- )
724
+ let timestamp = Timestamp::from_second(ts_secs).map_err(|e| {
725
+ Error::new(ruby.exception_arg_error(), format!("Invalid timestamp: {}", e))
726
+ })?;
727
+
728
+ let (jiff_tz, iana_name) = if let Some(ref tz) = self.jiff_timezone {
729
+ let name = tz.iana_name().unwrap_or("UTC").to_owned();
730
+ (tz.clone(), name)
701
731
  } else {
702
- // No timezone specified, use UTC
703
- (utc_year, utc_month, utc_day, utc_hour, utc_min, utc_sec)
732
+ (JiffTimeZone::UTC, "UTC".to_owned())
704
733
  };
705
734
 
706
- // Create ISO date and convert to Gregorian
707
- let iso_date = Date::try_new_iso(year, month as u8, day as u8)
735
+ let zoned = timestamp.to_zoned(jiff_tz);
736
+ let dt = zoned.datetime();
737
+
738
+ let iso_date = Date::try_new_iso(dt.year() as i32, dt.month() as u8, dt.day() as u8)
708
739
  .map_err(|e| Error::new(ruby.exception_arg_error(), format!("Invalid date: {}", e)))?;
709
740
  let gregorian_date = iso_date.to_calendar(Gregorian);
710
741
 
711
- // Create time
712
- let time_of_day = Time::try_new(hour as u8, min as u8, sec as u8, 0)
742
+ let icu_time = Time::try_new(dt.hour() as u8, dt.minute() as u8, dt.second() as u8, 0)
713
743
  .map_err(|e| Error::new(ruby.exception_arg_error(), format!("Invalid time: {}", e)))?;
714
744
 
715
- Ok(DateTime {
745
+ let icu_tz: TimeZone = IanaParser::new().parse(&iana_name);
746
+ let utc_offset = UtcOffset::from_seconds_unchecked(zoned.offset().seconds());
747
+ let zone_name_ts = ZoneNameTimestamp::from_epoch_seconds(ts_secs);
748
+ let zone_info = icu_tz.with_offset(Some(utc_offset)).with_zone_name_timestamp(zone_name_ts);
749
+
750
+ Ok(ZonedDateTime {
716
751
  date: gregorian_date,
717
- time: time_of_day,
752
+ time: icu_time,
753
+ zone: zone_info,
718
754
  })
719
755
  }
720
756
 
721
757
  /// Get the resolved options
722
758
  ///
723
759
  /// # Returns
724
- /// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone, :hour_cycle
760
+ /// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone, :hour_cycle, :hour12
725
761
  fn resolved_options(&self) -> Result<RHash, Error> {
726
762
  let ruby = Ruby::get().expect("Ruby runtime should be available");
727
763
  let hash = ruby.hash_new();
@@ -757,6 +793,17 @@ impl DateTimeFormat {
757
793
  )?;
758
794
  }
759
795
 
796
+ if let Some(h12) = self.hour12 {
797
+ hash.aset(ruby.to_symbol("hour12"), h12)?;
798
+ }
799
+
800
+ if let Some(era) = self.era {
801
+ hash.aset(
802
+ ruby.to_symbol("era"),
803
+ ruby.to_symbol(era.to_symbol_name()),
804
+ )?;
805
+ }
806
+
760
807
  // Add component options if they were used
761
808
  if let Some(ref opts) = self.component_options {
762
809
  if let Some(year) = opts.year {
@@ -1,8 +1,8 @@
1
1
  use crate::data_provider::DataProvider;
2
2
  use crate::helpers;
3
- use icu::experimental::displaynames::{
4
- DisplayNamesOptions, Fallback, LanguageDisplayNames, LocaleDisplayNamesFormatter,
5
- RegionDisplayNames, ScriptDisplayNames, Style,
3
+ use icu::experimental::displaynames::{DisplayNamesOptions, Fallback, Style};
4
+ use icu::experimental::displaynames::multi::{
5
+ LanguageDisplayNames, LocaleDisplayNamesFormatter, RegionDisplayNames, ScriptDisplayNames,
6
6
  };
7
7
  use icu_locale::LanguageIdentifier;
8
8
  use icu_provider::buf::AsDeserializingBufferProvider;
@@ -1,5 +1,6 @@
1
1
  use crate::helpers;
2
2
  use icu_locale::{Locale as IcuLocale, LocaleExpander, TransformResult};
3
+ use icu_locale::subtags::Variant;
3
4
  use magnus::{Error, RHash, RModule, Ruby, function, method, prelude::*, typed_data::Obj};
4
5
  use std::cell::RefCell;
5
6
 
@@ -204,6 +205,57 @@ impl Locale {
204
205
  inner: RefCell::new(IcuLocale::from(new_id)),
205
206
  }
206
207
  }
208
+
209
+ /// Get the list of variants
210
+ fn variants(&self) -> Vec<String> {
211
+ self.inner
212
+ .borrow()
213
+ .id
214
+ .variants
215
+ .iter()
216
+ .map(|v| v.to_string())
217
+ .collect()
218
+ }
219
+
220
+ fn parse_variant(s: &str) -> Result<Variant, Error> {
221
+ let ruby = Ruby::get().expect("Ruby runtime should be available");
222
+ s.parse::<Variant>().map_err(|e| {
223
+ Error::new(
224
+ helpers::get_exception_class(&ruby, "ICU4X::LocaleError"),
225
+ format!("Invalid variant: {e}"),
226
+ )
227
+ })
228
+ }
229
+
230
+ /// Add a variant in place; returns self if added, nil if already present
231
+ fn add_variant_bang(rb_self: Obj<Self>, variant_str: String) -> Result<Option<Obj<Self>>, Error> {
232
+ let variant = Self::parse_variant(&variant_str)?;
233
+ let added = rb_self.inner.borrow_mut().id.variants.push(variant);
234
+ Ok(if added { Some(rb_self) } else { None })
235
+ }
236
+
237
+ /// Return a new Locale with the variant added
238
+ fn add_variant(&self, variant_str: String) -> Result<Self, Error> {
239
+ let variant = Self::parse_variant(&variant_str)?;
240
+ let mut new_locale = self.inner.borrow().clone();
241
+ new_locale.id.variants.push(variant);
242
+ Ok(Self { inner: RefCell::new(new_locale) })
243
+ }
244
+
245
+ /// Remove a variant in place; returns self if removed, nil if not present
246
+ fn remove_variant_bang(rb_self: Obj<Self>, variant_str: String) -> Result<Option<Obj<Self>>, Error> {
247
+ let variant = Self::parse_variant(&variant_str)?;
248
+ let removed = rb_self.inner.borrow_mut().id.variants.remove(&variant);
249
+ Ok(if removed { Some(rb_self) } else { None })
250
+ }
251
+
252
+ /// Return a new Locale with the variant removed
253
+ fn remove_variant(&self, variant_str: String) -> Result<Self, Error> {
254
+ let variant = Self::parse_variant(&variant_str)?;
255
+ let mut new_locale = self.inner.borrow().clone();
256
+ new_locale.id.variants.remove(&variant);
257
+ Ok(Self { inner: RefCell::new(new_locale) })
258
+ }
207
259
  }
208
260
 
209
261
  pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
@@ -221,5 +273,10 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
221
273
  class.define_method("maximize", method!(Locale::maximize, 0))?;
222
274
  class.define_method("minimize!", method!(Locale::minimize_bang, 0))?;
223
275
  class.define_method("minimize", method!(Locale::minimize, 0))?;
276
+ class.define_method("variants", method!(Locale::variants, 0))?;
277
+ class.define_method("add_variant!", method!(Locale::add_variant_bang, 1))?;
278
+ class.define_method("add_variant", method!(Locale::add_variant, 1))?;
279
+ class.define_method("remove_variant!", method!(Locale::remove_variant_bang, 1))?;
280
+ class.define_method("remove_variant", method!(Locale::remove_variant, 1))?;
224
281
  Ok(())
225
282
  }
@@ -298,7 +298,7 @@ impl NumberFormat {
298
298
  FormatterKind::Decimal(formatter) => formatter.format(&decimal).to_string(),
299
299
  FormatterKind::Percent(formatter) => formatter.format(&decimal).to_string(),
300
300
  FormatterKind::Currency(formatter, currency_code) => formatter
301
- .format_fixed_decimal(&decimal, *currency_code)
301
+ .format_fixed_decimal(&decimal, currency_code)
302
302
  .to_string(),
303
303
  };
304
304
  Ok(formatted)
@@ -331,7 +331,7 @@ impl NumberFormat {
331
331
  }
332
332
  FormatterKind::Currency(formatter, currency_code) => {
333
333
  formatter
334
- .format_fixed_decimal(&decimal, *currency_code)
334
+ .format_fixed_decimal(&decimal, currency_code)
335
335
  .write_to_parts(&mut collector)
336
336
  .map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
337
337
  }
@@ -399,11 +399,7 @@ impl NumberFormat {
399
399
 
400
400
  /// Check if value is a BigDecimal
401
401
  fn is_big_decimal(ruby: &Ruby, value: Value) -> bool {
402
- // Try to get BigDecimal class; if bigdecimal is not loaded, return false
403
- if let Ok(bigdecimal_class) = ruby.eval::<Value>("defined?(BigDecimal) && BigDecimal") {
404
- if bigdecimal_class.is_nil() {
405
- return false;
406
- }
402
+ if let Ok(bigdecimal_class) = ruby.eval::<Value>("BigDecimal") {
407
403
  if let Ok(class) = magnus::RClass::try_convert(bigdecimal_class) {
408
404
  return value.is_kind_of(class);
409
405
  }
@@ -146,9 +146,8 @@ impl RelativeTimeFormat {
146
146
  })?;
147
147
 
148
148
  // Build formatter options
149
- let options = RelativeTimeFormatterOptions {
150
- numeric: numeric.to_icu_numeric(),
151
- };
149
+ let mut options = RelativeTimeFormatterOptions::default();
150
+ options.numeric = numeric.to_icu_numeric();
152
151
  let prefs: RelativeTimeFormatterPreferences = (&icu_locale).into();
153
152
 
154
153
  // Create formatters for all units based on style
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "icu4x_macros"
3
- version = "0.9.0"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
 
6
6
  [lib]
data/lib/icu4x/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ICU4X
4
- VERSION = "0.9.0"
4
+ VERSION = "0.11.0"
5
5
  public_constant :VERSION
6
6
  end