icu4x 0.8.1 → 0.10.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.
@@ -1,22 +1,26 @@
1
1
  use crate::data_provider::DataProvider;
2
2
  use crate::helpers;
3
+ use crate::parts_collector::{PartsCollector, parts_to_ruby_array};
3
4
  use icu::calendar::preferences::CalendarAlgorithm;
4
5
  use icu::calendar::{AnyCalendarKind, Date, Gregorian};
5
6
  use icu::datetime::fieldsets::enums::{
6
- CompositeDateTimeFieldSet, DateAndTimeFieldSet, DateFieldSet, TimeFieldSet,
7
+ CalendarPeriodFieldSet, CompositeDateTimeFieldSet, DateAndTimeFieldSet, DateFieldSet,
8
+ TimeFieldSet,
7
9
  };
8
10
  use icu::datetime::fieldsets::{self};
11
+ use icu::datetime::options::{Length, YearStyle as IcuYearStyle};
9
12
  use icu::datetime::input::DateTime;
13
+ use icu::datetime::parts as dt_parts;
10
14
  use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences};
15
+ use icu::locale::preferences::extensions::unicode::keywords::HourCycle as IcuHourCycle;
11
16
  use icu::time::Time;
12
17
  use icu::time::zone::IanaParser;
13
18
  use icu_provider::buf::AsDeserializingBufferProvider;
14
19
  use icu4x_macros::RubySymbol;
15
20
  use jiff::Timestamp;
16
21
  use jiff::tz::TimeZone;
17
- use magnus::{
18
- Error, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*,
19
- };
22
+ use magnus::{Error, RArray, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*};
23
+ use writeable::{Part, Writeable};
20
24
 
21
25
  /// Date style option
22
26
  #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
@@ -36,6 +40,154 @@ enum TimeStyle {
36
40
  Short,
37
41
  }
38
42
 
43
+ /// Hour cycle option
44
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
45
+ enum HourCycle {
46
+ H11,
47
+ H12,
48
+ H23,
49
+ }
50
+
51
+ impl HourCycle {
52
+ fn to_icu_hour_cycle(self) -> IcuHourCycle {
53
+ match self {
54
+ HourCycle::H11 => IcuHourCycle::H11,
55
+ HourCycle::H12 => IcuHourCycle::H12,
56
+ HourCycle::H23 => IcuHourCycle::H23,
57
+ }
58
+ }
59
+ }
60
+
61
+ /// Year component option
62
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
63
+ enum YearStyle {
64
+ Numeric,
65
+ TwoDigit,
66
+ }
67
+
68
+ /// Era display option
69
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
70
+ enum EraStyle {
71
+ Auto,
72
+ Full,
73
+ WithEra,
74
+ Never,
75
+ }
76
+
77
+ impl EraStyle {
78
+ fn to_icu_year_style(self) -> IcuYearStyle {
79
+ match self {
80
+ EraStyle::Auto => IcuYearStyle::Auto,
81
+ EraStyle::Full => IcuYearStyle::Full,
82
+ EraStyle::WithEra => IcuYearStyle::WithEra,
83
+ EraStyle::Never => IcuYearStyle::NoEra,
84
+ }
85
+ }
86
+ }
87
+
88
+ /// Month component option
89
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
90
+ enum MonthStyle {
91
+ Numeric,
92
+ TwoDigit,
93
+ Long,
94
+ Short,
95
+ Narrow,
96
+ }
97
+
98
+ /// Day component option
99
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
100
+ enum DayStyle {
101
+ Numeric,
102
+ TwoDigit,
103
+ }
104
+
105
+ /// Weekday component option
106
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
107
+ enum WeekdayStyle {
108
+ Long,
109
+ Short,
110
+ Narrow,
111
+ }
112
+
113
+ /// Hour component option
114
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
115
+ enum HourStyle {
116
+ Numeric,
117
+ TwoDigit,
118
+ }
119
+
120
+ /// Minute component option
121
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
122
+ enum MinuteStyle {
123
+ Numeric,
124
+ TwoDigit,
125
+ }
126
+
127
+ /// Second component option
128
+ #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
129
+ enum SecondStyle {
130
+ Numeric,
131
+ TwoDigit,
132
+ }
133
+
134
+ /// Component options for date/time formatting
135
+ #[derive(Clone, Copy, Default)]
136
+ struct ComponentOptions {
137
+ year: Option<YearStyle>,
138
+ month: Option<MonthStyle>,
139
+ day: Option<DayStyle>,
140
+ weekday: Option<WeekdayStyle>,
141
+ hour: Option<HourStyle>,
142
+ minute: Option<MinuteStyle>,
143
+ second: Option<SecondStyle>,
144
+ }
145
+
146
+ impl ComponentOptions {
147
+ fn has_date_components(&self) -> bool {
148
+ self.year.is_some() || self.month.is_some() || self.day.is_some() || self.weekday.is_some()
149
+ }
150
+
151
+ fn has_time_components(&self) -> bool {
152
+ self.hour.is_some() || self.minute.is_some() || self.second.is_some()
153
+ }
154
+
155
+ fn is_empty(&self) -> bool {
156
+ !self.has_date_components() && !self.has_time_components()
157
+ }
158
+
159
+ /// Determine the appropriate ICU4X Length based on component option values.
160
+ ///
161
+ /// When text-based month or weekday styles (:long, :short, :narrow) are specified,
162
+ /// we use Length::Long to ensure the format uses localized text patterns
163
+ /// (e.g., "2026年2月" in Japanese instead of "2026/02").
164
+ ///
165
+ /// This matches JavaScript Intl.DateTimeFormat behavior where specifying
166
+ /// month: "short" produces text-based formats with abbreviated month names,
167
+ /// not numeric formats.
168
+ ///
169
+ /// - If any text-based component (:long, :short, :narrow) → Length::Long
170
+ /// - Default (all numeric) → Length::Short
171
+ fn determine_length(&self) -> Length {
172
+ // Check for any text-based month or weekday option
173
+ let has_text_month = matches!(
174
+ self.month,
175
+ Some(MonthStyle::Long) | Some(MonthStyle::Short) | Some(MonthStyle::Narrow)
176
+ );
177
+ let has_text_weekday = matches!(
178
+ self.weekday,
179
+ Some(WeekdayStyle::Long) | Some(WeekdayStyle::Short) | Some(WeekdayStyle::Narrow)
180
+ );
181
+
182
+ if has_text_month || has_text_weekday {
183
+ return Length::Long;
184
+ }
185
+
186
+ // Default for numeric-only options
187
+ Length::Short
188
+ }
189
+ }
190
+
39
191
  /// Calendar option
40
192
  #[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
41
193
  enum Calendar {
@@ -86,7 +238,7 @@ impl Calendar {
86
238
  | AnyCalendarKind::HijriTabularTypeIIThursday
87
239
  | AnyCalendarKind::HijriUmmAlQura => Calendar::Islamic,
88
240
  AnyCalendarKind::Iso => Calendar::Gregory,
89
- AnyCalendarKind::Japanese | AnyCalendarKind::JapaneseExtended => Calendar::Japanese,
241
+ AnyCalendarKind::Japanese => Calendar::Japanese,
90
242
  AnyCalendarKind::Persian => Calendar::Persian,
91
243
  AnyCalendarKind::Roc => Calendar::Roc,
92
244
  _ => Calendar::Gregory,
@@ -94,6 +246,33 @@ impl Calendar {
94
246
  }
95
247
  }
96
248
 
249
+ /// Convert ICU4X datetime Part to Ruby symbol name
250
+ fn part_to_symbol_name(part: &Part) -> &'static str {
251
+ if *part == dt_parts::YEAR {
252
+ "year"
253
+ } else if *part == dt_parts::MONTH {
254
+ "month"
255
+ } else if *part == dt_parts::DAY {
256
+ "day"
257
+ } else if *part == dt_parts::WEEKDAY {
258
+ "weekday"
259
+ } else if *part == dt_parts::HOUR {
260
+ "hour"
261
+ } else if *part == dt_parts::MINUTE {
262
+ "minute"
263
+ } else if *part == dt_parts::SECOND {
264
+ "second"
265
+ } else if *part == dt_parts::DAY_PERIOD {
266
+ "day_period"
267
+ } else if *part == dt_parts::ERA {
268
+ "era"
269
+ } else if *part == dt_parts::TIME_ZONE_NAME {
270
+ "time_zone_name"
271
+ } else {
272
+ "literal"
273
+ }
274
+ }
275
+
97
276
  /// Ruby wrapper for ICU4X datetime formatters
98
277
  #[magnus::wrap(class = "ICU4X::DateTimeFormat", free_immediately, size)]
99
278
  pub struct DateTimeFormat {
@@ -104,6 +283,10 @@ pub struct DateTimeFormat {
104
283
  time_zone: Option<String>,
105
284
  jiff_timezone: Option<TimeZone>,
106
285
  calendar: Calendar,
286
+ hour_cycle: Option<HourCycle>,
287
+ hour12: Option<bool>,
288
+ era: Option<EraStyle>,
289
+ component_options: Option<ComponentOptions>,
107
290
  }
108
291
 
109
292
  // SAFETY: This type is marked as Send to allow Ruby to move it between threads.
@@ -131,6 +314,7 @@ impl DateTimeFormat {
131
314
  /// * `time_zone:` - IANA timezone name (e.g., "Asia/Tokyo")
132
315
  /// * `calendar:` - :gregory, :japanese, :buddhist, :chinese, :hebrew, :islamic,
133
316
  /// :persian, :indian, :ethiopian, :coptic, :roc, :dangi
317
+ /// * `hour_cycle:` - :h11, :h12, or :h23
134
318
  fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
135
319
  // Parse arguments: (locale, **kwargs)
136
320
  let (icu_locale, locale_str) = helpers::extract_locale(ruby, args)?;
@@ -153,14 +337,35 @@ impl DateTimeFormat {
153
337
  let time_style =
154
338
  helpers::extract_symbol(ruby, &kwargs, "time_style", TimeStyle::from_ruby_symbol)?;
155
339
 
156
- // At least one of date_style or time_style must be specified
157
- if date_style.is_none() && time_style.is_none() {
340
+ // Extract component options
341
+ let component_options = Self::extract_component_options(ruby, &kwargs)?;
342
+
343
+ // Validate: style options and component options are mutually exclusive
344
+ let has_style_options = date_style.is_some() || time_style.is_some();
345
+ let has_component_options = !component_options.is_empty();
346
+
347
+ if has_style_options && has_component_options {
158
348
  return Err(Error::new(
159
349
  ruby.exception_arg_error(),
160
- "at least one of date_style or time_style must be specified",
350
+ "cannot use date_style/time_style together with component options (year, month, day, etc.)",
161
351
  ));
162
352
  }
163
353
 
354
+ // Apply default component options if no options specified
355
+ // Default: year: :numeric, month: :numeric, day: :numeric
356
+ // This matches JavaScript Intl.DateTimeFormat default behavior
357
+ let component_options = if !has_style_options && !has_component_options {
358
+ ComponentOptions {
359
+ year: Some(YearStyle::Numeric),
360
+ month: Some(MonthStyle::Numeric),
361
+ day: Some(DayStyle::Numeric),
362
+ ..Default::default()
363
+ }
364
+ } else {
365
+ component_options
366
+ };
367
+ let has_component_options = !component_options.is_empty();
368
+
164
369
  // Extract time_zone option and parse it
165
370
  let time_zone: Option<String> =
166
371
  kwargs.lookup::<_, Option<String>>(ruby.to_symbol("time_zone"))?;
@@ -192,6 +397,16 @@ impl DateTimeFormat {
192
397
  let calendar =
193
398
  helpers::extract_symbol(ruby, &kwargs, "calendar", Calendar::from_ruby_symbol)?;
194
399
 
400
+ // Extract hour_cycle option
401
+ let hour_cycle =
402
+ helpers::extract_symbol(ruby, &kwargs, "hour_cycle", HourCycle::from_ruby_symbol)?;
403
+
404
+ let hour12: Option<bool> = kwargs.lookup::<_, Option<bool>>(ruby.to_symbol("hour12"))?;
405
+
406
+ // Extract era option
407
+ let era =
408
+ helpers::extract_symbol(ruby, &kwargs, "era", EraStyle::from_ruby_symbol)?;
409
+
195
410
  // Get the error exception class
196
411
  let error_class = helpers::get_exception_class(ruby, "ICU4X::Error");
197
412
 
@@ -203,14 +418,23 @@ impl DateTimeFormat {
203
418
  )
204
419
  })?;
205
420
 
206
- // Create field set based on date_style and time_style
207
- let field_set = Self::create_field_set(date_style, time_style);
421
+ // Create field set based on options
422
+ let field_set = if has_component_options {
423
+ Self::create_field_set_from_components(ruby, &component_options, era)?
424
+ } else {
425
+ Self::create_field_set_from_style(date_style, time_style, era)
426
+ };
208
427
 
209
- // Create formatter with calendar preference
428
+ // Create formatter with calendar and hour_cycle preferences
210
429
  let mut prefs: DateTimeFormatterPreferences = (&icu_locale).into();
211
430
  if let Some(cal) = calendar {
212
431
  prefs.calendar_algorithm = Some(cal.to_calendar_algorithm());
213
432
  }
433
+ if let Some(hc) = hour_cycle {
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 });
437
+ }
214
438
 
215
439
  let formatter =
216
440
  DateTimeFormatter::try_new_unstable(&dp.inner.as_deserializing(), prefs, field_set)
@@ -232,13 +456,146 @@ impl DateTimeFormat {
232
456
  time_zone,
233
457
  jiff_timezone,
234
458
  calendar: resolved_calendar,
459
+ hour_cycle,
460
+ hour12,
461
+ era,
462
+ component_options: if has_component_options {
463
+ Some(component_options)
464
+ } else {
465
+ None
466
+ },
467
+ })
468
+ }
469
+
470
+ /// Extract component options from kwargs
471
+ fn extract_component_options(ruby: &Ruby, kwargs: &RHash) -> Result<ComponentOptions, Error> {
472
+ let year = helpers::extract_symbol(ruby, kwargs, "year", YearStyle::from_ruby_symbol)?;
473
+ let month = helpers::extract_symbol(ruby, kwargs, "month", MonthStyle::from_ruby_symbol)?;
474
+ let day = helpers::extract_symbol(ruby, kwargs, "day", DayStyle::from_ruby_symbol)?;
475
+ let weekday =
476
+ helpers::extract_symbol(ruby, kwargs, "weekday", WeekdayStyle::from_ruby_symbol)?;
477
+ let hour = helpers::extract_symbol(ruby, kwargs, "hour", HourStyle::from_ruby_symbol)?;
478
+ let minute =
479
+ helpers::extract_symbol(ruby, kwargs, "minute", MinuteStyle::from_ruby_symbol)?;
480
+ let second =
481
+ helpers::extract_symbol(ruby, kwargs, "second", SecondStyle::from_ruby_symbol)?;
482
+
483
+ Ok(ComponentOptions {
484
+ year,
485
+ month,
486
+ day,
487
+ weekday,
488
+ hour,
489
+ minute,
490
+ second,
235
491
  })
236
492
  }
237
493
 
494
+ /// Create field set from component options
495
+ ///
496
+ /// Maps component combinations to appropriate ICU4X Field Sets.
497
+ /// Field Sets determine which components appear; the locale determines their order.
498
+ /// The length is determined by the component option values (e.g., :long → Long).
499
+ fn create_field_set_from_components(
500
+ ruby: &Ruby,
501
+ opts: &ComponentOptions,
502
+ era: Option<EraStyle>,
503
+ ) -> Result<CompositeDateTimeFieldSet, Error> {
504
+ let has_date = opts.has_date_components();
505
+ let has_time = opts.has_time_components();
506
+ let length = opts.determine_length();
507
+
508
+ match (has_date, has_time) {
509
+ (true, true) => {
510
+ // Date and time components
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)))
514
+ }
515
+ (true, false) => {
516
+ // Date only - choose field set based on which components are specified
517
+ let has_year = opts.year.is_some();
518
+ let has_month = opts.month.is_some();
519
+ let has_day = opts.day.is_some();
520
+ let has_weekday = opts.weekday.is_some();
521
+
522
+ match (has_year, has_month, has_day, has_weekday) {
523
+ // Year + Month + Day + Weekday
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
+ }
529
+ // Year + Month + Day
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
+ }
535
+ // Month + Day + Weekday
536
+ (false, true, true, true) => Ok(CompositeDateTimeFieldSet::Date(
537
+ DateFieldSet::MDE(fieldsets::MDE::for_length(length)),
538
+ )),
539
+ // Month + Day
540
+ (false, true, true, false) => Ok(CompositeDateTimeFieldSet::Date(
541
+ DateFieldSet::MD(fieldsets::MD::for_length(length)),
542
+ )),
543
+ // Year + Month (calendar period)
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
+ }
549
+ // Month only (calendar period)
550
+ (false, true, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
551
+ CalendarPeriodFieldSet::M(fieldsets::M::for_length(length)),
552
+ )),
553
+ // Day + Weekday
554
+ (false, false, true, true) => Ok(CompositeDateTimeFieldSet::Date(
555
+ DateFieldSet::DE(fieldsets::DE::for_length(length)),
556
+ )),
557
+ // Day only
558
+ (false, false, true, false) => Ok(CompositeDateTimeFieldSet::Date(
559
+ DateFieldSet::D(fieldsets::D::for_length(length)),
560
+ )),
561
+ // Weekday only
562
+ (false, false, false, true) => Ok(CompositeDateTimeFieldSet::Date(
563
+ DateFieldSet::E(fieldsets::E::for_length(length)),
564
+ )),
565
+ // Year only (calendar period)
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
+ }
571
+ // Year + Day (not a standard combination, use YMD as fallback)
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
+ }
577
+ // Should not happen - we checked has_date_components
578
+ (false, false, false, false) => unreachable!(),
579
+ }
580
+ }
581
+ (false, true) => {
582
+ // Time only - use medium as default since time components are always numeric
583
+ Ok(CompositeDateTimeFieldSet::Time(TimeFieldSet::T(
584
+ fieldsets::T::for_length(length),
585
+ )))
586
+ }
587
+ (false, false) => Err(Error::new(
588
+ ruby.exception_arg_error(),
589
+ "at least one component option must be specified",
590
+ )),
591
+ }
592
+ }
593
+
238
594
  /// Create field set based on date_style and time_style
239
- fn create_field_set(
595
+ fn create_field_set_from_style(
240
596
  date_style: Option<DateStyle>,
241
597
  time_style: Option<TimeStyle>,
598
+ era: Option<EraStyle>,
242
599
  ) -> CompositeDateTimeFieldSet {
243
600
  match (date_style, time_style) {
244
601
  (Some(ds), Some(ts)) => {
@@ -248,6 +605,7 @@ impl DateTimeFormat {
248
605
  (DateStyle::Medium, _) => fieldsets::YMDT::medium(),
249
606
  (DateStyle::Short, _) => fieldsets::YMDT::short(),
250
607
  };
608
+ let ymdt = if let Some(s) = era { ymdt.with_year_style(s.to_icu_year_style()) } else { ymdt };
251
609
  CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDT(ymdt))
252
610
  }
253
611
  (Some(ds), None) => {
@@ -257,6 +615,7 @@ impl DateTimeFormat {
257
615
  DateStyle::Medium => fieldsets::YMD::medium(),
258
616
  DateStyle::Short => fieldsets::YMD::short(),
259
617
  };
618
+ let ymd = if let Some(s) = era { ymd.with_year_style(s.to_icu_year_style()) } else { ymd };
260
619
  CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(ymd))
261
620
  }
262
621
  (None, Some(ts)) => {
@@ -284,7 +643,36 @@ impl DateTimeFormat {
284
643
  /// A formatted string
285
644
  fn format(&self, time: Value) -> Result<String, Error> {
286
645
  let ruby = Ruby::get().expect("Ruby runtime should be available");
646
+ let datetime = self.prepare_datetime(&ruby, time)?;
647
+ let formatted = self.inner.format(&datetime);
648
+ Ok(formatted.to_string())
649
+ }
650
+
651
+ /// Format a Ruby Time object and return an array of FormattedPart
652
+ ///
653
+ /// # Arguments
654
+ /// * `time` - A Ruby Time object or an object responding to #to_time (e.g., Date, DateTime)
655
+ ///
656
+ /// # Returns
657
+ /// An array of FormattedPart objects with :type and :value
658
+ fn format_to_parts(&self, time: Value) -> Result<RArray, Error> {
659
+ let ruby = Ruby::get().expect("Ruby runtime should be available");
660
+ let datetime = self.prepare_datetime(&ruby, time)?;
661
+
662
+ let formatted = self.inner.format(&datetime);
663
+ let mut collector = PartsCollector::new();
664
+ formatted
665
+ .write_to_parts(&mut collector)
666
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
287
667
 
668
+ parts_to_ruby_array(&ruby, collector, part_to_symbol_name)
669
+ }
670
+
671
+ /// Prepare a Ruby Time value for formatting.
672
+ ///
673
+ /// 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> {
288
676
  // Convert to Time if the object responds to #to_time
289
677
  let time_value = if time.respond_to("to_time", false)? {
290
678
  time.funcall::<_, _, Value>("to_time", ())?
@@ -301,12 +689,7 @@ impl DateTimeFormat {
301
689
  ));
302
690
  }
303
691
 
304
- // Convert Ruby Time to ICU4X DateTime, applying timezone if specified
305
- let datetime = self.convert_time_to_datetime(&ruby, time_value)?;
306
-
307
- // Format the datetime
308
- let formatted = self.inner.format(&datetime);
309
- Ok(formatted.to_string())
692
+ self.convert_time_to_datetime(ruby, time_value)
310
693
  }
311
694
 
312
695
  /// Convert Ruby Time to ICU4X DateTime<Gregorian>
@@ -374,7 +757,7 @@ impl DateTimeFormat {
374
757
  /// Get the resolved options
375
758
  ///
376
759
  /// # Returns
377
- /// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone
760
+ /// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone, :hour_cycle, :hour12
378
761
  fn resolved_options(&self) -> Result<RHash, Error> {
379
762
  let ruby = Ruby::get().expect("Ruby runtime should be available");
380
763
  let hash = ruby.hash_new();
@@ -403,6 +786,70 @@ impl DateTimeFormat {
403
786
  hash.aset(ruby.to_symbol("time_zone"), tz.as_str())?;
404
787
  }
405
788
 
789
+ if let Some(hc) = self.hour_cycle {
790
+ hash.aset(
791
+ ruby.to_symbol("hour_cycle"),
792
+ ruby.to_symbol(hc.to_symbol_name()),
793
+ )?;
794
+ }
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
+
807
+ // Add component options if they were used
808
+ if let Some(ref opts) = self.component_options {
809
+ if let Some(year) = opts.year {
810
+ hash.aset(
811
+ ruby.to_symbol("year"),
812
+ ruby.to_symbol(year.to_symbol_name()),
813
+ )?;
814
+ }
815
+ if let Some(month) = opts.month {
816
+ hash.aset(
817
+ ruby.to_symbol("month"),
818
+ ruby.to_symbol(month.to_symbol_name()),
819
+ )?;
820
+ }
821
+ if let Some(day) = opts.day {
822
+ hash.aset(
823
+ ruby.to_symbol("day"),
824
+ ruby.to_symbol(day.to_symbol_name()),
825
+ )?;
826
+ }
827
+ if let Some(weekday) = opts.weekday {
828
+ hash.aset(
829
+ ruby.to_symbol("weekday"),
830
+ ruby.to_symbol(weekday.to_symbol_name()),
831
+ )?;
832
+ }
833
+ if let Some(hour) = opts.hour {
834
+ hash.aset(
835
+ ruby.to_symbol("hour"),
836
+ ruby.to_symbol(hour.to_symbol_name()),
837
+ )?;
838
+ }
839
+ if let Some(minute) = opts.minute {
840
+ hash.aset(
841
+ ruby.to_symbol("minute"),
842
+ ruby.to_symbol(minute.to_symbol_name()),
843
+ )?;
844
+ }
845
+ if let Some(second) = opts.second {
846
+ hash.aset(
847
+ ruby.to_symbol("second"),
848
+ ruby.to_symbol(second.to_symbol_name()),
849
+ )?;
850
+ }
851
+ }
852
+
406
853
  Ok(hash)
407
854
  }
408
855
  }
@@ -411,6 +858,10 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
411
858
  let class = module.define_class("DateTimeFormat", ruby.class_object())?;
412
859
  class.define_singleton_method("new", function!(DateTimeFormat::new, -1))?;
413
860
  class.define_method("format", method!(DateTimeFormat::format, 1))?;
861
+ class.define_method(
862
+ "format_to_parts",
863
+ method!(DateTimeFormat::format_to_parts, 1),
864
+ )?;
414
865
  class.define_method(
415
866
  "resolved_options",
416
867
  method!(DateTimeFormat::resolved_options, 0),
@@ -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;
data/ext/icu4x/src/lib.rs CHANGED
@@ -7,6 +7,7 @@ mod helpers;
7
7
  mod list_format;
8
8
  mod locale;
9
9
  mod number_format;
10
+ mod parts_collector;
10
11
  mod plural_rules;
11
12
  mod relative_time_format;
12
13
  mod segmenter;