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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/Cargo.lock +119 -139
- data/README.md +3 -8
- data/ext/icu4x/Cargo.toml +10 -9
- data/ext/icu4x/src/data_generator.rs +1 -1
- data/ext/icu4x/src/datetime_format.rs +470 -19
- data/ext/icu4x/src/display_names.rs +3 -3
- data/ext/icu4x/src/lib.rs +1 -0
- data/ext/icu4x/src/list_format.rs +46 -9
- data/ext/icu4x/src/locale.rs +62 -4
- data/ext/icu4x/src/number_format.rs +85 -17
- data/ext/icu4x/src/parts_collector.rs +117 -0
- data/ext/icu4x/src/plural_rules.rs +62 -13
- data/ext/icu4x/src/relative_time_format.rs +58 -11
- data/ext/icu4x_macros/Cargo.toml +1 -1
- data/lib/icu4x/data_gem_task.rb +7 -1
- data/lib/icu4x/version.rb +1 -1
- data/lib/icu4x/yard_docs.rb +180 -6
- data/lib/icu4x.rb +44 -11
- data/sig/icu4x.rbs +45 -3
- metadata +19 -4
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
157
|
-
|
|
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
|
-
"
|
|
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
|
|
207
|
-
let field_set =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
RegionDisplayNames, ScriptDisplayNames,
|
|
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;
|