icu4x 0.8.1 → 0.9.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 +18 -0
- data/Cargo.lock +3 -2
- data/README.md +2 -7
- data/ext/icu4x/Cargo.toml +2 -1
- data/ext/icu4x/src/datetime_format.rs +422 -18
- data/ext/icu4x/src/lib.rs +1 -0
- data/ext/icu4x/src/list_format.rs +46 -9
- data/ext/icu4x/src/locale.rs +5 -4
- data/ext/icu4x/src/number_format.rs +84 -12
- 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 +56 -8
- 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 +22 -0
- data/sig/icu4x.rbs +42 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c0cb2adcc5d5af24a00272def5a66e3b03a928b0cb522a0d7c5ee6b50eacdb5
|
|
4
|
+
data.tar.gz: 2f44d6cddb74e64fc922c6269d32b060ed8b03e751969d7bd0e1c87d584f0a59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15b7967a5eecc044bbd3d3c1f3dfc36c08fd655cf9e83164c418239aa24a6cad7b622ac25d923cbc1d8f453cb8996be8ce5a1e38e3114f511a5f4ef1ebae2950
|
|
7
|
+
data.tar.gz: 68e9db100a5255557d6bf1c298f990dae44f8743ce0829ecc050a56491dd9ba27ac9d42aff3df583f1111b919c9af8395cca75f1c3eccc98ce6789502e826f56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-02-01
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `hour12` option for `ICU4X::DateTimeFormat` as a simpler alternative to `hour_cycle` (#132)
|
|
8
|
+
- Default behavior for `ICU4X::DateTimeFormat` when no options specified, matching JavaScript Intl.DateTimeFormat (#130)
|
|
9
|
+
- Component options for `ICU4X::DateTimeFormat` (`year`, `month`, `day`, `weekday`, `hour`, `minute`, `second`) as an alternative to style options (#129)
|
|
10
|
+
- Document numbering system support via BCP 47 locale extensions (`-u-nu-xxx`) for `NumberFormat`, `DateTimeFormat`, and `RelativeTimeFormat` (#127)
|
|
11
|
+
- `hour_cycle` option for `ICU4X::DateTimeFormat` to control 12/24-hour time display (#112)
|
|
12
|
+
- `ICU4X::RelativeTimeFormat#format_to_parts` method for breaking down formatted output into typed parts (#117)
|
|
13
|
+
- `ICU4X::ListFormat#format_to_parts` method for breaking down formatted output into typed parts (#116)
|
|
14
|
+
- `ICU4X::NumberFormat#format_to_parts` method for breaking down formatted output into typed parts (#115)
|
|
15
|
+
- `ICU4X::DateTimeFormat#format_to_parts` method for breaking down formatted output into typed parts (#114)
|
|
16
|
+
- `ICU4X::FormattedPart` data class for representing formatted parts (#113)
|
|
17
|
+
- `ICU4X::PluralRules#select_range` method for plural category selection on number ranges (#106)
|
|
18
|
+
- Allow data gems to be required by gem name (#104)
|
|
19
|
+
- `ICU4X::Locale.parse_bcp47` method for explicit BCP 47 parsing; `parse` is now an alias (#108)
|
|
20
|
+
|
|
3
21
|
## [0.8.1] - 2026-01-12
|
|
4
22
|
|
|
5
23
|
### Added
|
data/Cargo.lock
CHANGED
|
@@ -417,7 +417,7 @@ dependencies = [
|
|
|
417
417
|
|
|
418
418
|
[[package]]
|
|
419
419
|
name = "icu4x"
|
|
420
|
-
version = "0.
|
|
420
|
+
version = "0.9.0"
|
|
421
421
|
dependencies = [
|
|
422
422
|
"fixed_decimal",
|
|
423
423
|
"icu",
|
|
@@ -432,11 +432,12 @@ dependencies = [
|
|
|
432
432
|
"jiff",
|
|
433
433
|
"magnus",
|
|
434
434
|
"tinystr",
|
|
435
|
+
"writeable",
|
|
435
436
|
]
|
|
436
437
|
|
|
437
438
|
[[package]]
|
|
438
439
|
name = "icu4x_macros"
|
|
439
|
-
version = "0.
|
|
440
|
+
version = "0.9.0"
|
|
440
441
|
dependencies = [
|
|
441
442
|
"proc-macro2",
|
|
442
443
|
"quote",
|
data/README.md
CHANGED
|
@@ -41,16 +41,11 @@ Prebuilt binary gems are available for x86_64-linux, aarch64-linux, x86_64-darwi
|
|
|
41
41
|
|
|
42
42
|
#### Option 1: Use Pre-built Data Gem (Quick Start)
|
|
43
43
|
|
|
44
|
-
Add a companion data gem
|
|
44
|
+
Add a companion data gem to your Gemfile:
|
|
45
45
|
|
|
46
46
|
```ruby
|
|
47
47
|
gem "icu4x"
|
|
48
|
-
gem "icu4x-data-recommended" # 164 locales, ~24MB
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
```ruby
|
|
52
|
-
require "icu4x"
|
|
53
|
-
require "icu4x/data/recommended" # Auto-configures default provider
|
|
48
|
+
gem "icu4x-data-recommended" # 164 locales, ~24MB, auto-configures default provider
|
|
54
49
|
```
|
|
55
50
|
|
|
56
51
|
Available data gems:
|
data/ext/icu4x/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "icu4x"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ crate-type = ["cdylib"]
|
|
|
9
9
|
|
|
10
10
|
[dependencies]
|
|
11
11
|
magnus = "0.8"
|
|
12
|
+
writeable = "0.6"
|
|
12
13
|
icu_locale = "2.1"
|
|
13
14
|
icu_provider = "2.1"
|
|
14
15
|
icu_provider_blob = { version = "2.1", features = ["alloc", "export"] }
|
|
@@ -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;
|
|
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,134 @@ 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
|
+
/// Month component option
|
|
69
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
70
|
+
enum MonthStyle {
|
|
71
|
+
Numeric,
|
|
72
|
+
TwoDigit,
|
|
73
|
+
Long,
|
|
74
|
+
Short,
|
|
75
|
+
Narrow,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Day component option
|
|
79
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
80
|
+
enum DayStyle {
|
|
81
|
+
Numeric,
|
|
82
|
+
TwoDigit,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Weekday component option
|
|
86
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
87
|
+
enum WeekdayStyle {
|
|
88
|
+
Long,
|
|
89
|
+
Short,
|
|
90
|
+
Narrow,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Hour component option
|
|
94
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
95
|
+
enum HourStyle {
|
|
96
|
+
Numeric,
|
|
97
|
+
TwoDigit,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Minute component option
|
|
101
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
102
|
+
enum MinuteStyle {
|
|
103
|
+
Numeric,
|
|
104
|
+
TwoDigit,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Second component option
|
|
108
|
+
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
109
|
+
enum SecondStyle {
|
|
110
|
+
Numeric,
|
|
111
|
+
TwoDigit,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Component options for date/time formatting
|
|
115
|
+
#[derive(Clone, Copy, Default)]
|
|
116
|
+
struct ComponentOptions {
|
|
117
|
+
year: Option<YearStyle>,
|
|
118
|
+
month: Option<MonthStyle>,
|
|
119
|
+
day: Option<DayStyle>,
|
|
120
|
+
weekday: Option<WeekdayStyle>,
|
|
121
|
+
hour: Option<HourStyle>,
|
|
122
|
+
minute: Option<MinuteStyle>,
|
|
123
|
+
second: Option<SecondStyle>,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
impl ComponentOptions {
|
|
127
|
+
fn has_date_components(&self) -> bool {
|
|
128
|
+
self.year.is_some() || self.month.is_some() || self.day.is_some() || self.weekday.is_some()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn has_time_components(&self) -> bool {
|
|
132
|
+
self.hour.is_some() || self.minute.is_some() || self.second.is_some()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn is_empty(&self) -> bool {
|
|
136
|
+
!self.has_date_components() && !self.has_time_components()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Determine the appropriate ICU4X Length based on component option values.
|
|
140
|
+
///
|
|
141
|
+
/// When text-based month or weekday styles (:long, :short, :narrow) are specified,
|
|
142
|
+
/// we use Length::Long to ensure the format uses localized text patterns
|
|
143
|
+
/// (e.g., "2026年2月" in Japanese instead of "2026/02").
|
|
144
|
+
///
|
|
145
|
+
/// This matches JavaScript Intl.DateTimeFormat behavior where specifying
|
|
146
|
+
/// month: "short" produces text-based formats with abbreviated month names,
|
|
147
|
+
/// not numeric formats.
|
|
148
|
+
///
|
|
149
|
+
/// - If any text-based component (:long, :short, :narrow) → Length::Long
|
|
150
|
+
/// - Default (all numeric) → Length::Short
|
|
151
|
+
fn determine_length(&self) -> Length {
|
|
152
|
+
// Check for any text-based month or weekday option
|
|
153
|
+
let has_text_month = matches!(
|
|
154
|
+
self.month,
|
|
155
|
+
Some(MonthStyle::Long) | Some(MonthStyle::Short) | Some(MonthStyle::Narrow)
|
|
156
|
+
);
|
|
157
|
+
let has_text_weekday = matches!(
|
|
158
|
+
self.weekday,
|
|
159
|
+
Some(WeekdayStyle::Long) | Some(WeekdayStyle::Short) | Some(WeekdayStyle::Narrow)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if has_text_month || has_text_weekday {
|
|
163
|
+
return Length::Long;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default for numeric-only options
|
|
167
|
+
Length::Short
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
39
171
|
/// Calendar option
|
|
40
172
|
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
41
173
|
enum Calendar {
|
|
@@ -94,6 +226,33 @@ impl Calendar {
|
|
|
94
226
|
}
|
|
95
227
|
}
|
|
96
228
|
|
|
229
|
+
/// Convert ICU4X datetime Part to Ruby symbol name
|
|
230
|
+
fn part_to_symbol_name(part: &Part) -> &'static str {
|
|
231
|
+
if *part == dt_parts::YEAR {
|
|
232
|
+
"year"
|
|
233
|
+
} else if *part == dt_parts::MONTH {
|
|
234
|
+
"month"
|
|
235
|
+
} else if *part == dt_parts::DAY {
|
|
236
|
+
"day"
|
|
237
|
+
} else if *part == dt_parts::WEEKDAY {
|
|
238
|
+
"weekday"
|
|
239
|
+
} else if *part == dt_parts::HOUR {
|
|
240
|
+
"hour"
|
|
241
|
+
} else if *part == dt_parts::MINUTE {
|
|
242
|
+
"minute"
|
|
243
|
+
} else if *part == dt_parts::SECOND {
|
|
244
|
+
"second"
|
|
245
|
+
} else if *part == dt_parts::DAY_PERIOD {
|
|
246
|
+
"day_period"
|
|
247
|
+
} else if *part == dt_parts::ERA {
|
|
248
|
+
"era"
|
|
249
|
+
} else if *part == dt_parts::TIME_ZONE_NAME {
|
|
250
|
+
"time_zone_name"
|
|
251
|
+
} else {
|
|
252
|
+
"literal"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
97
256
|
/// Ruby wrapper for ICU4X datetime formatters
|
|
98
257
|
#[magnus::wrap(class = "ICU4X::DateTimeFormat", free_immediately, size)]
|
|
99
258
|
pub struct DateTimeFormat {
|
|
@@ -104,6 +263,8 @@ pub struct DateTimeFormat {
|
|
|
104
263
|
time_zone: Option<String>,
|
|
105
264
|
jiff_timezone: Option<TimeZone>,
|
|
106
265
|
calendar: Calendar,
|
|
266
|
+
hour_cycle: Option<HourCycle>,
|
|
267
|
+
component_options: Option<ComponentOptions>,
|
|
107
268
|
}
|
|
108
269
|
|
|
109
270
|
// SAFETY: This type is marked as Send to allow Ruby to move it between threads.
|
|
@@ -131,6 +292,7 @@ impl DateTimeFormat {
|
|
|
131
292
|
/// * `time_zone:` - IANA timezone name (e.g., "Asia/Tokyo")
|
|
132
293
|
/// * `calendar:` - :gregory, :japanese, :buddhist, :chinese, :hebrew, :islamic,
|
|
133
294
|
/// :persian, :indian, :ethiopian, :coptic, :roc, :dangi
|
|
295
|
+
/// * `hour_cycle:` - :h11, :h12, or :h23
|
|
134
296
|
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
135
297
|
// Parse arguments: (locale, **kwargs)
|
|
136
298
|
let (icu_locale, locale_str) = helpers::extract_locale(ruby, args)?;
|
|
@@ -153,14 +315,35 @@ impl DateTimeFormat {
|
|
|
153
315
|
let time_style =
|
|
154
316
|
helpers::extract_symbol(ruby, &kwargs, "time_style", TimeStyle::from_ruby_symbol)?;
|
|
155
317
|
|
|
156
|
-
//
|
|
157
|
-
|
|
318
|
+
// Extract component options
|
|
319
|
+
let component_options = Self::extract_component_options(ruby, &kwargs)?;
|
|
320
|
+
|
|
321
|
+
// Validate: style options and component options are mutually exclusive
|
|
322
|
+
let has_style_options = date_style.is_some() || time_style.is_some();
|
|
323
|
+
let has_component_options = !component_options.is_empty();
|
|
324
|
+
|
|
325
|
+
if has_style_options && has_component_options {
|
|
158
326
|
return Err(Error::new(
|
|
159
327
|
ruby.exception_arg_error(),
|
|
160
|
-
"
|
|
328
|
+
"cannot use date_style/time_style together with component options (year, month, day, etc.)",
|
|
161
329
|
));
|
|
162
330
|
}
|
|
163
331
|
|
|
332
|
+
// Apply default component options if no options specified
|
|
333
|
+
// Default: year: :numeric, month: :numeric, day: :numeric
|
|
334
|
+
// This matches JavaScript Intl.DateTimeFormat default behavior
|
|
335
|
+
let component_options = if !has_style_options && !has_component_options {
|
|
336
|
+
ComponentOptions {
|
|
337
|
+
year: Some(YearStyle::Numeric),
|
|
338
|
+
month: Some(MonthStyle::Numeric),
|
|
339
|
+
day: Some(DayStyle::Numeric),
|
|
340
|
+
..Default::default()
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
component_options
|
|
344
|
+
};
|
|
345
|
+
let has_component_options = !component_options.is_empty();
|
|
346
|
+
|
|
164
347
|
// Extract time_zone option and parse it
|
|
165
348
|
let time_zone: Option<String> =
|
|
166
349
|
kwargs.lookup::<_, Option<String>>(ruby.to_symbol("time_zone"))?;
|
|
@@ -192,6 +375,20 @@ impl DateTimeFormat {
|
|
|
192
375
|
let calendar =
|
|
193
376
|
helpers::extract_symbol(ruby, &kwargs, "calendar", Calendar::from_ruby_symbol)?;
|
|
194
377
|
|
|
378
|
+
// Extract hour_cycle option
|
|
379
|
+
let hour_cycle =
|
|
380
|
+
helpers::extract_symbol(ruby, &kwargs, "hour_cycle", HourCycle::from_ruby_symbol)?;
|
|
381
|
+
|
|
382
|
+
// Extract hour12 option and convert to hour_cycle if hour_cycle is not specified
|
|
383
|
+
// hour12: true → :h12, hour12: false → :h23
|
|
384
|
+
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
|
+
};
|
|
391
|
+
|
|
195
392
|
// Get the error exception class
|
|
196
393
|
let error_class = helpers::get_exception_class(ruby, "ICU4X::Error");
|
|
197
394
|
|
|
@@ -203,14 +400,21 @@ impl DateTimeFormat {
|
|
|
203
400
|
)
|
|
204
401
|
})?;
|
|
205
402
|
|
|
206
|
-
// Create field set based on
|
|
207
|
-
let field_set =
|
|
403
|
+
// Create field set based on options
|
|
404
|
+
let field_set = if has_component_options {
|
|
405
|
+
Self::create_field_set_from_components(ruby, &component_options)?
|
|
406
|
+
} else {
|
|
407
|
+
Self::create_field_set_from_style(date_style, time_style)
|
|
408
|
+
};
|
|
208
409
|
|
|
209
|
-
// Create formatter with calendar
|
|
410
|
+
// Create formatter with calendar and hour_cycle preferences
|
|
210
411
|
let mut prefs: DateTimeFormatterPreferences = (&icu_locale).into();
|
|
211
412
|
if let Some(cal) = calendar {
|
|
212
413
|
prefs.calendar_algorithm = Some(cal.to_calendar_algorithm());
|
|
213
414
|
}
|
|
415
|
+
if let Some(hc) = hour_cycle {
|
|
416
|
+
prefs.hour_cycle = Some(hc.to_icu_hour_cycle());
|
|
417
|
+
}
|
|
214
418
|
|
|
215
419
|
let formatter =
|
|
216
420
|
DateTimeFormatter::try_new_unstable(&dp.inner.as_deserializing(), prefs, field_set)
|
|
@@ -232,11 +436,130 @@ impl DateTimeFormat {
|
|
|
232
436
|
time_zone,
|
|
233
437
|
jiff_timezone,
|
|
234
438
|
calendar: resolved_calendar,
|
|
439
|
+
hour_cycle,
|
|
440
|
+
component_options: if has_component_options {
|
|
441
|
+
Some(component_options)
|
|
442
|
+
} else {
|
|
443
|
+
None
|
|
444
|
+
},
|
|
235
445
|
})
|
|
236
446
|
}
|
|
237
447
|
|
|
448
|
+
/// Extract component options from kwargs
|
|
449
|
+
fn extract_component_options(ruby: &Ruby, kwargs: &RHash) -> Result<ComponentOptions, Error> {
|
|
450
|
+
let year = helpers::extract_symbol(ruby, kwargs, "year", YearStyle::from_ruby_symbol)?;
|
|
451
|
+
let month = helpers::extract_symbol(ruby, kwargs, "month", MonthStyle::from_ruby_symbol)?;
|
|
452
|
+
let day = helpers::extract_symbol(ruby, kwargs, "day", DayStyle::from_ruby_symbol)?;
|
|
453
|
+
let weekday =
|
|
454
|
+
helpers::extract_symbol(ruby, kwargs, "weekday", WeekdayStyle::from_ruby_symbol)?;
|
|
455
|
+
let hour = helpers::extract_symbol(ruby, kwargs, "hour", HourStyle::from_ruby_symbol)?;
|
|
456
|
+
let minute =
|
|
457
|
+
helpers::extract_symbol(ruby, kwargs, "minute", MinuteStyle::from_ruby_symbol)?;
|
|
458
|
+
let second =
|
|
459
|
+
helpers::extract_symbol(ruby, kwargs, "second", SecondStyle::from_ruby_symbol)?;
|
|
460
|
+
|
|
461
|
+
Ok(ComponentOptions {
|
|
462
|
+
year,
|
|
463
|
+
month,
|
|
464
|
+
day,
|
|
465
|
+
weekday,
|
|
466
|
+
hour,
|
|
467
|
+
minute,
|
|
468
|
+
second,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// Create field set from component options
|
|
473
|
+
///
|
|
474
|
+
/// Maps component combinations to appropriate ICU4X Field Sets.
|
|
475
|
+
/// Field Sets determine which components appear; the locale determines their order.
|
|
476
|
+
/// The length is determined by the component option values (e.g., :long → Long).
|
|
477
|
+
fn create_field_set_from_components(
|
|
478
|
+
ruby: &Ruby,
|
|
479
|
+
opts: &ComponentOptions,
|
|
480
|
+
) -> Result<CompositeDateTimeFieldSet, Error> {
|
|
481
|
+
let has_date = opts.has_date_components();
|
|
482
|
+
let has_time = opts.has_time_components();
|
|
483
|
+
let length = opts.determine_length();
|
|
484
|
+
|
|
485
|
+
match (has_date, has_time) {
|
|
486
|
+
(true, true) => {
|
|
487
|
+
// Date and time components
|
|
488
|
+
Ok(CompositeDateTimeFieldSet::DateTime(
|
|
489
|
+
DateAndTimeFieldSet::YMDT(fieldsets::YMDT::for_length(length)),
|
|
490
|
+
))
|
|
491
|
+
}
|
|
492
|
+
(true, false) => {
|
|
493
|
+
// Date only - choose field set based on which components are specified
|
|
494
|
+
let has_year = opts.year.is_some();
|
|
495
|
+
let has_month = opts.month.is_some();
|
|
496
|
+
let has_day = opts.day.is_some();
|
|
497
|
+
let has_weekday = opts.weekday.is_some();
|
|
498
|
+
|
|
499
|
+
match (has_year, has_month, has_day, has_weekday) {
|
|
500
|
+
// Year + Month + Day + Weekday
|
|
501
|
+
(true, true, true, true) => Ok(CompositeDateTimeFieldSet::Date(
|
|
502
|
+
DateFieldSet::YMDE(fieldsets::YMDE::for_length(length)),
|
|
503
|
+
)),
|
|
504
|
+
// Year + Month + Day
|
|
505
|
+
(true, true, true, false) => Ok(CompositeDateTimeFieldSet::Date(
|
|
506
|
+
DateFieldSet::YMD(fieldsets::YMD::for_length(length)),
|
|
507
|
+
)),
|
|
508
|
+
// Month + Day + Weekday
|
|
509
|
+
(false, true, true, true) => Ok(CompositeDateTimeFieldSet::Date(
|
|
510
|
+
DateFieldSet::MDE(fieldsets::MDE::for_length(length)),
|
|
511
|
+
)),
|
|
512
|
+
// Month + Day
|
|
513
|
+
(false, true, true, false) => Ok(CompositeDateTimeFieldSet::Date(
|
|
514
|
+
DateFieldSet::MD(fieldsets::MD::for_length(length)),
|
|
515
|
+
)),
|
|
516
|
+
// Year + Month (calendar period)
|
|
517
|
+
(true, true, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
|
|
518
|
+
CalendarPeriodFieldSet::YM(fieldsets::YM::for_length(length)),
|
|
519
|
+
)),
|
|
520
|
+
// Month only (calendar period)
|
|
521
|
+
(false, true, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
|
|
522
|
+
CalendarPeriodFieldSet::M(fieldsets::M::for_length(length)),
|
|
523
|
+
)),
|
|
524
|
+
// Day + Weekday
|
|
525
|
+
(false, false, true, true) => Ok(CompositeDateTimeFieldSet::Date(
|
|
526
|
+
DateFieldSet::DE(fieldsets::DE::for_length(length)),
|
|
527
|
+
)),
|
|
528
|
+
// Day only
|
|
529
|
+
(false, false, true, false) => Ok(CompositeDateTimeFieldSet::Date(
|
|
530
|
+
DateFieldSet::D(fieldsets::D::for_length(length)),
|
|
531
|
+
)),
|
|
532
|
+
// Weekday only
|
|
533
|
+
(false, false, false, true) => Ok(CompositeDateTimeFieldSet::Date(
|
|
534
|
+
DateFieldSet::E(fieldsets::E::for_length(length)),
|
|
535
|
+
)),
|
|
536
|
+
// Year only (calendar period)
|
|
537
|
+
(true, false, false, _) => Ok(CompositeDateTimeFieldSet::CalendarPeriod(
|
|
538
|
+
CalendarPeriodFieldSet::Y(fieldsets::Y::for_length(length)),
|
|
539
|
+
)),
|
|
540
|
+
// 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
|
+
)),
|
|
544
|
+
// Should not happen - we checked has_date_components
|
|
545
|
+
(false, false, false, false) => unreachable!(),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
(false, true) => {
|
|
549
|
+
// Time only - use medium as default since time components are always numeric
|
|
550
|
+
Ok(CompositeDateTimeFieldSet::Time(TimeFieldSet::T(
|
|
551
|
+
fieldsets::T::for_length(length),
|
|
552
|
+
)))
|
|
553
|
+
}
|
|
554
|
+
(false, false) => Err(Error::new(
|
|
555
|
+
ruby.exception_arg_error(),
|
|
556
|
+
"at least one component option must be specified",
|
|
557
|
+
)),
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
238
561
|
/// Create field set based on date_style and time_style
|
|
239
|
-
fn
|
|
562
|
+
fn create_field_set_from_style(
|
|
240
563
|
date_style: Option<DateStyle>,
|
|
241
564
|
time_style: Option<TimeStyle>,
|
|
242
565
|
) -> CompositeDateTimeFieldSet {
|
|
@@ -284,7 +607,36 @@ impl DateTimeFormat {
|
|
|
284
607
|
/// A formatted string
|
|
285
608
|
fn format(&self, time: Value) -> Result<String, Error> {
|
|
286
609
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
610
|
+
let datetime = self.prepare_datetime(&ruby, time)?;
|
|
611
|
+
let formatted = self.inner.format(&datetime);
|
|
612
|
+
Ok(formatted.to_string())
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// Format a Ruby Time object and return an array of FormattedPart
|
|
616
|
+
///
|
|
617
|
+
/// # Arguments
|
|
618
|
+
/// * `time` - A Ruby Time object or an object responding to #to_time (e.g., Date, DateTime)
|
|
619
|
+
///
|
|
620
|
+
/// # Returns
|
|
621
|
+
/// An array of FormattedPart objects with :type and :value
|
|
622
|
+
fn format_to_parts(&self, time: Value) -> Result<RArray, Error> {
|
|
623
|
+
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
624
|
+
let datetime = self.prepare_datetime(&ruby, time)?;
|
|
625
|
+
|
|
626
|
+
let formatted = self.inner.format(&datetime);
|
|
627
|
+
let mut collector = PartsCollector::new();
|
|
628
|
+
formatted
|
|
629
|
+
.write_to_parts(&mut collector)
|
|
630
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
631
|
+
|
|
632
|
+
parts_to_ruby_array(&ruby, collector, part_to_symbol_name)
|
|
633
|
+
}
|
|
287
634
|
|
|
635
|
+
/// Prepare a Ruby Time value for formatting.
|
|
636
|
+
///
|
|
637
|
+
/// 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> {
|
|
288
640
|
// Convert to Time if the object responds to #to_time
|
|
289
641
|
let time_value = if time.respond_to("to_time", false)? {
|
|
290
642
|
time.funcall::<_, _, Value>("to_time", ())?
|
|
@@ -301,12 +653,7 @@ impl DateTimeFormat {
|
|
|
301
653
|
));
|
|
302
654
|
}
|
|
303
655
|
|
|
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())
|
|
656
|
+
self.convert_time_to_datetime(ruby, time_value)
|
|
310
657
|
}
|
|
311
658
|
|
|
312
659
|
/// Convert Ruby Time to ICU4X DateTime<Gregorian>
|
|
@@ -374,7 +721,7 @@ impl DateTimeFormat {
|
|
|
374
721
|
/// Get the resolved options
|
|
375
722
|
///
|
|
376
723
|
/// # Returns
|
|
377
|
-
/// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone
|
|
724
|
+
/// A hash with :locale, :calendar, :date_style, :time_style, and optionally :time_zone, :hour_cycle
|
|
378
725
|
fn resolved_options(&self) -> Result<RHash, Error> {
|
|
379
726
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
380
727
|
let hash = ruby.hash_new();
|
|
@@ -403,6 +750,59 @@ impl DateTimeFormat {
|
|
|
403
750
|
hash.aset(ruby.to_symbol("time_zone"), tz.as_str())?;
|
|
404
751
|
}
|
|
405
752
|
|
|
753
|
+
if let Some(hc) = self.hour_cycle {
|
|
754
|
+
hash.aset(
|
|
755
|
+
ruby.to_symbol("hour_cycle"),
|
|
756
|
+
ruby.to_symbol(hc.to_symbol_name()),
|
|
757
|
+
)?;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Add component options if they were used
|
|
761
|
+
if let Some(ref opts) = self.component_options {
|
|
762
|
+
if let Some(year) = opts.year {
|
|
763
|
+
hash.aset(
|
|
764
|
+
ruby.to_symbol("year"),
|
|
765
|
+
ruby.to_symbol(year.to_symbol_name()),
|
|
766
|
+
)?;
|
|
767
|
+
}
|
|
768
|
+
if let Some(month) = opts.month {
|
|
769
|
+
hash.aset(
|
|
770
|
+
ruby.to_symbol("month"),
|
|
771
|
+
ruby.to_symbol(month.to_symbol_name()),
|
|
772
|
+
)?;
|
|
773
|
+
}
|
|
774
|
+
if let Some(day) = opts.day {
|
|
775
|
+
hash.aset(
|
|
776
|
+
ruby.to_symbol("day"),
|
|
777
|
+
ruby.to_symbol(day.to_symbol_name()),
|
|
778
|
+
)?;
|
|
779
|
+
}
|
|
780
|
+
if let Some(weekday) = opts.weekday {
|
|
781
|
+
hash.aset(
|
|
782
|
+
ruby.to_symbol("weekday"),
|
|
783
|
+
ruby.to_symbol(weekday.to_symbol_name()),
|
|
784
|
+
)?;
|
|
785
|
+
}
|
|
786
|
+
if let Some(hour) = opts.hour {
|
|
787
|
+
hash.aset(
|
|
788
|
+
ruby.to_symbol("hour"),
|
|
789
|
+
ruby.to_symbol(hour.to_symbol_name()),
|
|
790
|
+
)?;
|
|
791
|
+
}
|
|
792
|
+
if let Some(minute) = opts.minute {
|
|
793
|
+
hash.aset(
|
|
794
|
+
ruby.to_symbol("minute"),
|
|
795
|
+
ruby.to_symbol(minute.to_symbol_name()),
|
|
796
|
+
)?;
|
|
797
|
+
}
|
|
798
|
+
if let Some(second) = opts.second {
|
|
799
|
+
hash.aset(
|
|
800
|
+
ruby.to_symbol("second"),
|
|
801
|
+
ruby.to_symbol(second.to_symbol_name()),
|
|
802
|
+
)?;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
406
806
|
Ok(hash)
|
|
407
807
|
}
|
|
408
808
|
}
|
|
@@ -411,6 +811,10 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
|
411
811
|
let class = module.define_class("DateTimeFormat", ruby.class_object())?;
|
|
412
812
|
class.define_singleton_method("new", function!(DateTimeFormat::new, -1))?;
|
|
413
813
|
class.define_method("format", method!(DateTimeFormat::format, 1))?;
|
|
814
|
+
class.define_method(
|
|
815
|
+
"format_to_parts",
|
|
816
|
+
method!(DateTimeFormat::format_to_parts, 1),
|
|
817
|
+
)?;
|
|
414
818
|
class.define_method(
|
|
415
819
|
"resolved_options",
|
|
416
820
|
method!(DateTimeFormat::resolved_options, 0),
|