icu4x 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e325a3954b6ced7e74233f9932c136e417028cda9f87770fe3f6d46daadd6e93
4
- data.tar.gz: 61fdc9da91abc2e3f1146e1398be2ac7248dd8e37f8158f5c4f9acd71a065b05
3
+ metadata.gz: d20c68d78ce5ac2f944e9f7d1d5439ddcd7cfbf38de6545a3471114d2930be63
4
+ data.tar.gz: e164bb4226ed47018d14ea9642b95bf5b0dc3900e45d0097077a9ad896288654
5
5
  SHA512:
6
- metadata.gz: 50ffdd36e7e8aaff6cb04136616314b295c7bec0909357d770d2cb8307a892d54820298cebf35221afc99587d4fcbbb55a4ac68bdfa125b3de46af93c59c2d5a
7
- data.tar.gz: 0b632c37b59bb2abbada6686dd3a51b32b59d7619f41893f2da1b070cef59a53e896dc6aa15b564c9c7a41ca23f6cda39e8009e6aff50314b27e8f1944d0ca83
6
+ metadata.gz: a423d9b426a240c82e5d8cd6452b473707093ff5db8c9d28f71ef3607a97178811f3dc3087428ad5e5418cc180a5b0104bb7b59ce4bc058458d2943e048d69f4
7
+ data.tar.gz: f718d99ee81feb48d054ffa2d2943eefb0985af3e31f3e746084b52964143dfc4bddb88fb8044b4d8fb4a5082aeb0ef9ff53e18188da446b7142ee7a778ee727
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.0] - 2026-05-18
4
+
5
+ ### Fixed
6
+
7
+ - Fix `time_style: :long` and `time_style: :full` to include timezone information (#156)
8
+ - Fix `time_style: :short` to omit seconds (#155)
9
+
10
+ ### Changed
11
+
12
+ - Update minimum Ruby version requirement to 3.3
13
+
3
14
  ## [0.10.0] - 2026-05-08
4
15
 
5
16
  ### Added
data/Cargo.lock CHANGED
@@ -405,7 +405,7 @@ dependencies = [
405
405
 
406
406
  [[package]]
407
407
  name = "icu4x"
408
- version = "0.10.0"
408
+ version = "0.11.0"
409
409
  dependencies = [
410
410
  "fixed_decimal",
411
411
  "icu",
@@ -425,7 +425,7 @@ dependencies = [
425
425
 
426
426
  [[package]]
427
427
  name = "icu4x_macros"
428
- version = "0.10.0"
428
+ version = "0.11.0"
429
429
  dependencies = [
430
430
  "proc-macro2",
431
431
  "quote",
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
data/ext/icu4x/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "icu4x"
3
- version = "0.10.0"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  publish = false
6
6
 
@@ -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, YearStyle as IcuYearStyle};
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
 
@@ -276,12 +275,12 @@ fn part_to_symbol_name(part: &Part) -> &'static str {
276
275
  /// Ruby wrapper for ICU4X datetime formatters
277
276
  #[magnus::wrap(class = "ICU4X::DateTimeFormat", free_immediately, size)]
278
277
  pub struct DateTimeFormat {
279
- inner: DateTimeFormatter<CompositeDateTimeFieldSet>,
278
+ inner: DateTimeFormatter<CompositeFieldSet>,
280
279
  locale_str: String,
281
280
  date_style: Option<DateStyle>,
282
281
  time_style: Option<TimeStyle>,
283
282
  time_zone: Option<String>,
284
- jiff_timezone: Option<TimeZone>,
283
+ jiff_timezone: Option<JiffTimeZone>,
285
284
  calendar: Calendar,
286
285
  hour_cycle: Option<HourCycle>,
287
286
  hour12: Option<bool>,
@@ -382,7 +381,7 @@ impl DateTimeFormat {
382
381
  ));
383
382
  }
384
383
  // Then create jiff TimeZone for offset calculation
385
- let jiff_tz = TimeZone::get(tz_str).map_err(|e| {
384
+ let jiff_tz = JiffTimeZone::get(tz_str).map_err(|e| {
386
385
  Error::new(
387
386
  ruby.exception_arg_error(),
388
387
  format!("invalid IANA timezone: {} ({})", tz_str, e),
@@ -421,6 +420,7 @@ impl DateTimeFormat {
421
420
  // Create field set based on options
422
421
  let field_set = if has_component_options {
423
422
  Self::create_field_set_from_components(ruby, &component_options, era)?
423
+ .to_composite_field_set()
424
424
  } else {
425
425
  Self::create_field_set_from_style(date_style, time_style, era)
426
426
  };
@@ -596,7 +596,7 @@ impl DateTimeFormat {
596
596
  date_style: Option<DateStyle>,
597
597
  time_style: Option<TimeStyle>,
598
598
  era: Option<EraStyle>,
599
- ) -> CompositeDateTimeFieldSet {
599
+ ) -> CompositeFieldSet {
600
600
  match (date_style, time_style) {
601
601
  (Some(ds), Some(ts)) => {
602
602
  // Both date and time
@@ -607,6 +607,7 @@ impl DateTimeFormat {
607
607
  };
608
608
  let ymdt = if let Some(s) = era { ymdt.with_year_style(s.to_icu_year_style()) } else { ymdt };
609
609
  CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDT(ymdt))
610
+ .to_composite_field_set()
610
611
  }
611
612
  (Some(ds), None) => {
612
613
  // Date only
@@ -616,16 +617,29 @@ impl DateTimeFormat {
616
617
  DateStyle::Short => fieldsets::YMD::short(),
617
618
  };
618
619
  let ymd = if let Some(s) = era { ymd.with_year_style(s.to_icu_year_style()) } else { ymd };
619
- CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(ymd))
620
+ CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(ymd)).to_composite_field_set()
620
621
  }
621
622
  (None, Some(ts)) => {
622
- // Time only
623
- let t = match ts {
624
- TimeStyle::Full | TimeStyle::Long => fieldsets::T::long(),
625
- TimeStyle::Medium => fieldsets::T::medium(),
626
- TimeStyle::Short => fieldsets::T::short(),
627
- };
628
- 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
+ }
629
643
  }
630
644
  (None, None) => {
631
645
  // Should not happen due to validation
@@ -671,8 +685,12 @@ impl DateTimeFormat {
671
685
  /// Prepare a Ruby Time value for formatting.
672
686
  ///
673
687
  /// Converts objects responding to #to_time, validates the result,
674
- /// and converts to ICU4X DateTime.
675
- 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> {
676
694
  // Convert to Time if the object responds to #to_time
677
695
  let time_value = if time.respond_to("to_time", false)? {
678
696
  time.funcall::<_, _, Value>("to_time", ())?
@@ -689,68 +707,50 @@ impl DateTimeFormat {
689
707
  ));
690
708
  }
691
709
 
692
- self.convert_time_to_datetime(ruby, time_value)
710
+ self.convert_time_to_zoned_datetime(ruby, time_value)
693
711
  }
694
712
 
695
- /// Convert Ruby Time to ICU4X DateTime<Gregorian>
713
+ /// Convert Ruby Time to ICU4X ZonedDateTime<Gregorian, TimeZoneInfo<AtTime>>
696
714
  ///
697
- /// If time_zone is specified, the UTC time is converted to local time in that timezone.
698
- /// Otherwise, the time is treated as UTC.
699
- 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(
700
718
  &self,
701
719
  ruby: &Ruby,
702
720
  time: Value,
703
- ) -> Result<DateTime<Gregorian>, Error> {
704
- // Get UTC time from Ruby Time object
705
- let utc_time: Value = time.funcall("getutc", ())?;
706
-
707
- let utc_year: i32 = utc_time.funcall("year", ())?;
708
- let utc_month: i32 = utc_time.funcall("month", ())?;
709
- let utc_day: i32 = utc_time.funcall("day", ())?;
710
- let utc_hour: i32 = utc_time.funcall("hour", ())?;
711
- let utc_min: i32 = utc_time.funcall("min", ())?;
712
- let utc_sec: i32 = utc_time.funcall("sec", ())?;
713
-
714
- // Get year, month, day, hour, min, sec in the target timezone
715
- let (year, month, day, hour, min, sec) = if let Some(ref tz) = self.jiff_timezone {
716
- // Create a jiff Timestamp from UTC components
717
- let timestamp = Timestamp::from_second(utc_time.funcall::<_, _, i64>("to_i", ())?)
718
- .map_err(|e| {
719
- Error::new(
720
- ruby.exception_arg_error(),
721
- format!("Invalid timestamp: {}", e),
722
- )
723
- })?;
721
+ ) -> Result<ZonedDateTime<Gregorian, TimeZoneInfo<models::AtTime>>, Error> {
722
+ let ts_secs: i64 = time.funcall("to_i", ())?;
724
723
 
725
- // Convert to local time in the target timezone
726
- let zoned = timestamp.to_zoned(tz.clone());
727
- let dt = zoned.datetime();
728
-
729
- (
730
- dt.year() as i32,
731
- dt.month() as i32,
732
- dt.day() as i32,
733
- dt.hour() as i32,
734
- dt.minute() as i32,
735
- dt.second() as i32,
736
- )
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)
737
731
  } else {
738
- // No timezone specified, use UTC
739
- (utc_year, utc_month, utc_day, utc_hour, utc_min, utc_sec)
732
+ (JiffTimeZone::UTC, "UTC".to_owned())
740
733
  };
741
734
 
742
- // Create ISO date and convert to Gregorian
743
- 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)
744
739
  .map_err(|e| Error::new(ruby.exception_arg_error(), format!("Invalid date: {}", e)))?;
745
740
  let gregorian_date = iso_date.to_calendar(Gregorian);
746
741
 
747
- // Create time
748
- 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)
749
743
  .map_err(|e| Error::new(ruby.exception_arg_error(), format!("Invalid time: {}", e)))?;
750
744
 
751
- 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 {
752
751
  date: gregorian_date,
753
- time: time_of_day,
752
+ time: icu_time,
753
+ zone: zone_info,
754
754
  })
755
755
  }
756
756
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "icu4x_macros"
3
- version = "0.10.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.10.0"
4
+ VERSION = "0.11.0"
5
5
  public_constant :VERSION
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icu4x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OZAWA Sakuro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-08 00:00:00.000000000 Z
11
+ date: 2026-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal