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