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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
use crate::data_provider::DataProvider;
|
|
2
2
|
use crate::helpers;
|
|
3
|
+
use crate::parts_collector::{PartsCollector, parts_to_ruby_array};
|
|
4
|
+
use icu::list::parts as list_parts;
|
|
3
5
|
use icu::list::ListFormatter;
|
|
4
6
|
use icu::list::options::{ListFormatterOptions, ListLength};
|
|
5
7
|
use icu_provider::buf::AsDeserializingBufferProvider;
|
|
6
8
|
use icu4x_macros::RubySymbol;
|
|
7
|
-
use magnus::{
|
|
8
|
-
|
|
9
|
-
};
|
|
9
|
+
use magnus::{Error, RArray, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*};
|
|
10
|
+
use writeable::{Part, Writeable};
|
|
10
11
|
|
|
11
12
|
/// The type of list formatting
|
|
12
13
|
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
@@ -34,6 +35,17 @@ impl ListStyle {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/// Convert ICU4X list Part to Ruby symbol name
|
|
39
|
+
fn part_to_symbol_name(part: &Part) -> &'static str {
|
|
40
|
+
if *part == list_parts::ELEMENT {
|
|
41
|
+
"element"
|
|
42
|
+
} else if *part == list_parts::LITERAL {
|
|
43
|
+
"literal"
|
|
44
|
+
} else {
|
|
45
|
+
"literal"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
/// Ruby wrapper for ICU4X ListFormatter
|
|
38
50
|
#[magnus::wrap(class = "ICU4X::ListFormat", free_immediately, size)]
|
|
39
51
|
pub struct ListFormat {
|
|
@@ -134,18 +146,42 @@ impl ListFormat {
|
|
|
134
146
|
/// A formatted string
|
|
135
147
|
fn format(&self, list: Value) -> Result<String, Error> {
|
|
136
148
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
149
|
+
let items = self.prepare_list(&ruby, list)?;
|
|
150
|
+
let formatted = self.inner.format(items.iter().map(|s| s.as_str()));
|
|
151
|
+
Ok(formatted.to_string())
|
|
152
|
+
}
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
/// Format a list of strings and return an array of FormattedPart
|
|
155
|
+
///
|
|
156
|
+
/// # Arguments
|
|
157
|
+
/// * `list` - An array of strings
|
|
158
|
+
///
|
|
159
|
+
/// # Returns
|
|
160
|
+
/// An array of FormattedPart objects with :type and :value
|
|
161
|
+
fn format_to_parts(&self, list: Value) -> Result<RArray, Error> {
|
|
162
|
+
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
163
|
+
let items = self.prepare_list(&ruby, list)?;
|
|
164
|
+
|
|
165
|
+
let formatted = self.inner.format(items.iter().map(|s| s.as_str()));
|
|
166
|
+
let mut collector = PartsCollector::new();
|
|
167
|
+
formatted
|
|
168
|
+
.write_to_parts(&mut collector)
|
|
169
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
170
|
+
|
|
171
|
+
parts_to_ruby_array(&ruby, collector, part_to_symbol_name)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Prepare a Ruby list for formatting.
|
|
175
|
+
///
|
|
176
|
+
/// Converts Ruby Array to Vec<String>.
|
|
177
|
+
fn prepare_list(&self, ruby: &Ruby, list: Value) -> Result<Vec<String>, Error> {
|
|
139
178
|
let array: RArray = TryConvert::try_convert(list)
|
|
140
179
|
.map_err(|_| Error::new(ruby.exception_type_error(), "list must be an Array"))?;
|
|
141
180
|
|
|
142
|
-
|
|
181
|
+
array
|
|
143
182
|
.into_iter()
|
|
144
183
|
.map(TryConvert::try_convert)
|
|
145
|
-
.collect::<Result<Vec<_>, _>>()
|
|
146
|
-
|
|
147
|
-
let formatted = self.inner.format(items.iter().map(|s| s.as_str()));
|
|
148
|
-
Ok(formatted.to_string())
|
|
184
|
+
.collect::<Result<Vec<_>, _>>()
|
|
149
185
|
}
|
|
150
186
|
|
|
151
187
|
/// Get the resolved options
|
|
@@ -172,6 +208,7 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
|
172
208
|
let class = module.define_class("ListFormat", ruby.class_object())?;
|
|
173
209
|
class.define_singleton_method("new", function!(ListFormat::new, -1))?;
|
|
174
210
|
class.define_method("format", method!(ListFormat::format, 1))?;
|
|
211
|
+
class.define_method("format_to_parts", method!(ListFormat::format_to_parts, 1))?;
|
|
175
212
|
class.define_method("resolved_options", method!(ListFormat::resolved_options, 0))?;
|
|
176
213
|
Ok(())
|
|
177
214
|
}
|
data/ext/icu4x/src/locale.rs
CHANGED
|
@@ -11,7 +11,7 @@ pub struct Locale {
|
|
|
11
11
|
|
|
12
12
|
impl Locale {
|
|
13
13
|
/// Parse a BCP 47 locale string
|
|
14
|
-
fn
|
|
14
|
+
fn parse_bcp47(ruby: &Ruby, s: String) -> Result<Self, Error> {
|
|
15
15
|
let locale: IcuLocale = s.parse().map_err(|e| {
|
|
16
16
|
Error::new(
|
|
17
17
|
helpers::get_exception_class(ruby, "ICU4X::LocaleError"),
|
|
@@ -32,7 +32,7 @@ impl Locale {
|
|
|
32
32
|
fn parse_posix(ruby: &Ruby, posix_str: String) -> Result<Self, Error> {
|
|
33
33
|
// Handle special cases
|
|
34
34
|
if posix_str == "C" || posix_str == "POSIX" {
|
|
35
|
-
return Self::
|
|
35
|
+
return Self::parse_bcp47(ruby, "und".to_string());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Handle empty string
|
|
@@ -90,7 +90,7 @@ impl Locale {
|
|
|
90
90
|
bcp47.push_str(&t.to_uppercase());
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
Self::
|
|
93
|
+
Self::parse_bcp47(ruby, bcp47)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/// Get the language component
|
|
@@ -208,7 +208,8 @@ impl Locale {
|
|
|
208
208
|
|
|
209
209
|
pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
210
210
|
let class = module.define_class("Locale", ruby.class_object())?;
|
|
211
|
-
class.define_singleton_method("
|
|
211
|
+
class.define_singleton_method("parse_bcp47", function!(Locale::parse_bcp47, 1))?;
|
|
212
|
+
class.singleton_class()?.define_alias("parse", "parse_bcp47")?;
|
|
212
213
|
class.define_singleton_method("parse_posix", function!(Locale::parse_posix, 1))?;
|
|
213
214
|
class.define_method("language", method!(Locale::language, 0))?;
|
|
214
215
|
class.define_method("script", method!(Locale::script, 0))?;
|
|
@@ -1,7 +1,9 @@
|
|
|
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 fixed_decimal::{Decimal, SignedRoundingMode, UnsignedRoundingMode};
|
|
4
5
|
use icu::decimal::options::{DecimalFormatterOptions, GroupingStrategy};
|
|
6
|
+
use icu::decimal::parts as decimal_parts;
|
|
5
7
|
use icu::decimal::{DecimalFormatter, DecimalFormatterPreferences};
|
|
6
8
|
use icu::experimental::dimension::currency::CurrencyCode;
|
|
7
9
|
use icu::experimental::dimension::currency::formatter::{
|
|
@@ -14,10 +16,9 @@ use icu::experimental::dimension::percent::formatter::{
|
|
|
14
16
|
use icu::experimental::dimension::percent::options::PercentFormatterOptions;
|
|
15
17
|
use icu_provider::buf::AsDeserializingBufferProvider;
|
|
16
18
|
use icu4x_macros::RubySymbol;
|
|
17
|
-
use magnus::{
|
|
18
|
-
Error, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*,
|
|
19
|
-
};
|
|
19
|
+
use magnus::{Error, RArray, RHash, RModule, Ruby, TryConvert, Value, function, method, prelude::*};
|
|
20
20
|
use tinystr::TinyAsciiStr;
|
|
21
|
+
use writeable::{Part, Writeable};
|
|
21
22
|
|
|
22
23
|
/// The style of number formatting
|
|
23
24
|
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
@@ -69,6 +70,29 @@ enum FormatterKind {
|
|
|
69
70
|
Currency(CurrencyFormatter, CurrencyCode),
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/// Convert ICU4X decimal Part to Ruby symbol name
|
|
74
|
+
fn part_to_symbol_name(part: &Part) -> &'static str {
|
|
75
|
+
if *part == decimal_parts::INTEGER {
|
|
76
|
+
"integer"
|
|
77
|
+
} else if *part == decimal_parts::FRACTION {
|
|
78
|
+
"fraction"
|
|
79
|
+
} else if *part == decimal_parts::DECIMAL {
|
|
80
|
+
"decimal"
|
|
81
|
+
} else if *part == decimal_parts::GROUP {
|
|
82
|
+
"group"
|
|
83
|
+
} else if *part == decimal_parts::MINUS_SIGN {
|
|
84
|
+
"minus_sign"
|
|
85
|
+
} else if *part == decimal_parts::PLUS_SIGN {
|
|
86
|
+
"plus_sign"
|
|
87
|
+
} else if part.category == "currency" {
|
|
88
|
+
"currency"
|
|
89
|
+
} else if part.category == "percent" {
|
|
90
|
+
"percent_sign"
|
|
91
|
+
} else {
|
|
92
|
+
"literal"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/// Ruby wrapper for ICU4X number formatters
|
|
73
97
|
#[magnus::wrap(class = "ICU4X::NumberFormat", free_immediately, size)]
|
|
74
98
|
pub struct NumberFormat {
|
|
@@ -268,8 +292,59 @@ impl NumberFormat {
|
|
|
268
292
|
/// A formatted string
|
|
269
293
|
fn format(&self, number: Value) -> Result<String, Error> {
|
|
270
294
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
295
|
+
let decimal = self.prepare_decimal(&ruby, number)?;
|
|
296
|
+
|
|
297
|
+
let formatted = match &self.inner {
|
|
298
|
+
FormatterKind::Decimal(formatter) => formatter.format(&decimal).to_string(),
|
|
299
|
+
FormatterKind::Percent(formatter) => formatter.format(&decimal).to_string(),
|
|
300
|
+
FormatterKind::Currency(formatter, currency_code) => formatter
|
|
301
|
+
.format_fixed_decimal(&decimal, *currency_code)
|
|
302
|
+
.to_string(),
|
|
303
|
+
};
|
|
304
|
+
Ok(formatted)
|
|
305
|
+
}
|
|
271
306
|
|
|
272
|
-
|
|
307
|
+
/// Format a number and return an array of FormattedPart
|
|
308
|
+
///
|
|
309
|
+
/// # Arguments
|
|
310
|
+
/// * `number` - An integer, float, or BigDecimal
|
|
311
|
+
///
|
|
312
|
+
/// # Returns
|
|
313
|
+
/// An array of FormattedPart objects with :type and :value
|
|
314
|
+
fn format_to_parts(&self, number: Value) -> Result<RArray, Error> {
|
|
315
|
+
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
316
|
+
let decimal = self.prepare_decimal(&ruby, number)?;
|
|
317
|
+
|
|
318
|
+
let mut collector = PartsCollector::new();
|
|
319
|
+
match &self.inner {
|
|
320
|
+
FormatterKind::Decimal(formatter) => {
|
|
321
|
+
formatter
|
|
322
|
+
.format(&decimal)
|
|
323
|
+
.write_to_parts(&mut collector)
|
|
324
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
325
|
+
}
|
|
326
|
+
FormatterKind::Percent(formatter) => {
|
|
327
|
+
formatter
|
|
328
|
+
.format(&decimal)
|
|
329
|
+
.write_to_parts(&mut collector)
|
|
330
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
331
|
+
}
|
|
332
|
+
FormatterKind::Currency(formatter, currency_code) => {
|
|
333
|
+
formatter
|
|
334
|
+
.format_fixed_decimal(&decimal, *currency_code)
|
|
335
|
+
.write_to_parts(&mut collector)
|
|
336
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
parts_to_ruby_array(&ruby, collector, part_to_symbol_name)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Prepare a Ruby number for formatting.
|
|
344
|
+
///
|
|
345
|
+
/// Converts to Decimal, adjusts for percent style, and applies digit options.
|
|
346
|
+
fn prepare_decimal(&self, ruby: &Ruby, number: Value) -> Result<Decimal, Error> {
|
|
347
|
+
let mut decimal = Self::convert_to_decimal(ruby, number)?;
|
|
273
348
|
|
|
274
349
|
// For percent style, multiply by 100 (same as Intl.NumberFormat)
|
|
275
350
|
if self.style == Style::Percent {
|
|
@@ -288,14 +363,7 @@ impl NumberFormat {
|
|
|
288
363
|
decimal.pad_start(min);
|
|
289
364
|
}
|
|
290
365
|
|
|
291
|
-
|
|
292
|
-
FormatterKind::Decimal(formatter) => formatter.format(&decimal).to_string(),
|
|
293
|
-
FormatterKind::Percent(formatter) => formatter.format(&decimal).to_string(),
|
|
294
|
-
FormatterKind::Currency(formatter, currency_code) => formatter
|
|
295
|
-
.format_fixed_decimal(&decimal, *currency_code)
|
|
296
|
-
.to_string(),
|
|
297
|
-
};
|
|
298
|
-
Ok(formatted)
|
|
366
|
+
Ok(decimal)
|
|
299
367
|
}
|
|
300
368
|
|
|
301
369
|
/// Convert Ruby number to Decimal
|
|
@@ -382,6 +450,10 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
|
382
450
|
let class = module.define_class("NumberFormat", ruby.class_object())?;
|
|
383
451
|
class.define_singleton_method("new", function!(NumberFormat::new, -1))?;
|
|
384
452
|
class.define_method("format", method!(NumberFormat::format, 1))?;
|
|
453
|
+
class.define_method(
|
|
454
|
+
"format_to_parts",
|
|
455
|
+
method!(NumberFormat::format_to_parts, 1),
|
|
456
|
+
)?;
|
|
385
457
|
class.define_method(
|
|
386
458
|
"resolved_options",
|
|
387
459
|
method!(NumberFormat::resolved_options, 0),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
use magnus::{Error, RArray, Ruby, Value, prelude::*};
|
|
2
|
+
use std::fmt;
|
|
3
|
+
use writeable::{Part, PartsWrite};
|
|
4
|
+
|
|
5
|
+
/// A collector for formatted parts that handles nested part annotations.
|
|
6
|
+
///
|
|
7
|
+
/// ICU4X uses nested parts - e.g., datetime/day wraps decimal/integer.
|
|
8
|
+
/// We track a stack of parts and prefer the outermost (top-level) annotations.
|
|
9
|
+
pub struct PartsCollector {
|
|
10
|
+
parts: Vec<(String, Part)>,
|
|
11
|
+
current_buffer: String,
|
|
12
|
+
/// Stack of part contexts for handling nested with_part calls
|
|
13
|
+
part_stack: Vec<Part>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl PartsCollector {
|
|
17
|
+
pub fn new() -> Self {
|
|
18
|
+
Self {
|
|
19
|
+
parts: Vec::new(),
|
|
20
|
+
current_buffer: String::new(),
|
|
21
|
+
part_stack: Vec::new(),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn flush(&mut self) {
|
|
26
|
+
// Store any remaining content as "literal"
|
|
27
|
+
if !self.current_buffer.is_empty() {
|
|
28
|
+
self.parts.push((
|
|
29
|
+
std::mem::take(&mut self.current_buffer),
|
|
30
|
+
Part {
|
|
31
|
+
category: "literal",
|
|
32
|
+
value: "literal",
|
|
33
|
+
},
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn into_parts(mut self) -> Vec<(String, Part)> {
|
|
39
|
+
self.flush();
|
|
40
|
+
self.parts
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl fmt::Write for PartsCollector {
|
|
45
|
+
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
46
|
+
self.current_buffer.push_str(s);
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl PartsWrite for PartsCollector {
|
|
52
|
+
type SubPartsWrite = Self;
|
|
53
|
+
|
|
54
|
+
fn with_part(
|
|
55
|
+
&mut self,
|
|
56
|
+
part: Part,
|
|
57
|
+
mut f: impl FnMut(&mut Self::SubPartsWrite) -> fmt::Result,
|
|
58
|
+
) -> fmt::Result {
|
|
59
|
+
// If at top level, store any buffered content as literal before entering new part
|
|
60
|
+
if self.part_stack.is_empty() && !self.current_buffer.is_empty() {
|
|
61
|
+
self.parts.push((
|
|
62
|
+
std::mem::take(&mut self.current_buffer),
|
|
63
|
+
Part {
|
|
64
|
+
category: "literal",
|
|
65
|
+
value: "literal",
|
|
66
|
+
},
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Push this part onto the stack
|
|
71
|
+
self.part_stack.push(part);
|
|
72
|
+
|
|
73
|
+
// Execute the writing function
|
|
74
|
+
f(self)?;
|
|
75
|
+
|
|
76
|
+
// Pop this part from the stack
|
|
77
|
+
self.part_stack.pop();
|
|
78
|
+
|
|
79
|
+
// If back at top level, store collected content with effective part
|
|
80
|
+
if self.part_stack.is_empty() && !self.current_buffer.is_empty() {
|
|
81
|
+
self.parts
|
|
82
|
+
.push((std::mem::take(&mut self.current_buffer), part));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Converts collected parts to a Ruby array of FormattedPart objects.
|
|
90
|
+
///
|
|
91
|
+
/// # Arguments
|
|
92
|
+
/// * `ruby` - The Ruby runtime reference
|
|
93
|
+
/// * `collector` - The PartsCollector with collected parts
|
|
94
|
+
/// * `part_mapper` - Function to convert a Part to a symbol name string
|
|
95
|
+
///
|
|
96
|
+
/// # Returns
|
|
97
|
+
/// A Ruby array containing FormattedPart objects.
|
|
98
|
+
pub fn parts_to_ruby_array<F>(
|
|
99
|
+
ruby: &Ruby,
|
|
100
|
+
collector: PartsCollector,
|
|
101
|
+
part_mapper: F,
|
|
102
|
+
) -> Result<RArray, Error>
|
|
103
|
+
where
|
|
104
|
+
F: Fn(&Part) -> &'static str,
|
|
105
|
+
{
|
|
106
|
+
let formatted_part_class: Value = ruby.eval("ICU4X::FormattedPart")?;
|
|
107
|
+
let result = ruby.ary_new();
|
|
108
|
+
|
|
109
|
+
for (value, part) in collector.into_parts() {
|
|
110
|
+
let symbol_name = part_mapper(&part);
|
|
111
|
+
let part_obj: Value =
|
|
112
|
+
formatted_part_class.funcall("[]", (ruby.to_symbol(symbol_name), value.as_str()))?;
|
|
113
|
+
result.push(part_obj)?;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Ok(result)
|
|
117
|
+
}
|
|
@@ -2,7 +2,7 @@ use crate::data_provider::DataProvider;
|
|
|
2
2
|
use crate::helpers;
|
|
3
3
|
use fixed_decimal::Decimal;
|
|
4
4
|
use icu::plurals::{
|
|
5
|
-
PluralCategory, PluralRuleType,
|
|
5
|
+
PluralCategory, PluralRuleType, PluralRulesPreferences, PluralRulesWithRanges,
|
|
6
6
|
};
|
|
7
7
|
use icu_provider::buf::AsDeserializingBufferProvider;
|
|
8
8
|
use magnus::{
|
|
@@ -12,7 +12,7 @@ use magnus::{
|
|
|
12
12
|
/// Ruby wrapper for ICU4X PluralRules
|
|
13
13
|
#[magnus::wrap(class = "ICU4X::PluralRules", free_immediately, size)]
|
|
14
14
|
pub struct PluralRules {
|
|
15
|
-
inner:
|
|
15
|
+
inner: PluralRulesWithRanges<icu::plurals::PluralRules>,
|
|
16
16
|
locale_str: String,
|
|
17
17
|
rule_type: PluralRuleType,
|
|
18
18
|
}
|
|
@@ -84,15 +84,20 @@ impl PluralRules {
|
|
|
84
84
|
)
|
|
85
85
|
})?;
|
|
86
86
|
|
|
87
|
-
// Create
|
|
87
|
+
// Create PluralRulesWithRanges from DataProvider
|
|
88
88
|
let rules = match rule_type {
|
|
89
|
-
PluralRuleType::Cardinal =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
PluralRuleType::Cardinal => PluralRulesWithRanges::try_new_cardinal_unstable(
|
|
90
|
+
&dp.inner.as_deserializing(),
|
|
91
|
+
prefs,
|
|
92
|
+
),
|
|
93
|
+
PluralRuleType::Ordinal => PluralRulesWithRanges::try_new_ordinal_unstable(
|
|
94
|
+
&dp.inner.as_deserializing(),
|
|
95
|
+
prefs,
|
|
96
|
+
),
|
|
97
|
+
_ => PluralRulesWithRanges::try_new_cardinal_unstable(
|
|
98
|
+
&dp.inner.as_deserializing(),
|
|
99
|
+
prefs,
|
|
100
|
+
),
|
|
96
101
|
}
|
|
97
102
|
.map_err(|e| Error::new(error_class, format!("Failed to create PluralRules: {}", e)))?;
|
|
98
103
|
|
|
@@ -120,7 +125,7 @@ impl PluralRules {
|
|
|
120
125
|
// For floats, convert to Decimal to preserve fractional digits
|
|
121
126
|
let s = format!("{}", f);
|
|
122
127
|
if let Ok(fd) = s.parse::<Decimal>() {
|
|
123
|
-
self.inner.category_for(&fd)
|
|
128
|
+
self.inner.rules().category_for(&fd)
|
|
124
129
|
} else {
|
|
125
130
|
return Err(Error::new(
|
|
126
131
|
ruby.exception_arg_error(),
|
|
@@ -129,7 +134,7 @@ impl PluralRules {
|
|
|
129
134
|
}
|
|
130
135
|
} else if number.is_kind_of(ruby.class_integer()) {
|
|
131
136
|
let n: i64 = TryConvert::try_convert(number)?;
|
|
132
|
-
self.inner.category_for(n as usize)
|
|
137
|
+
self.inner.rules().category_for(n as usize)
|
|
133
138
|
} else {
|
|
134
139
|
return Err(Error::new(
|
|
135
140
|
ruby.exception_type_error(),
|
|
@@ -140,6 +145,49 @@ impl PluralRules {
|
|
|
140
145
|
Ok(Self::category_to_symbol(&ruby, category))
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
/// Determine the plural category for a range of numbers
|
|
149
|
+
///
|
|
150
|
+
/// # Arguments
|
|
151
|
+
/// * `start` - The start of the range (integer or float)
|
|
152
|
+
/// * `end` - The end of the range (integer or float)
|
|
153
|
+
///
|
|
154
|
+
/// # Returns
|
|
155
|
+
/// A symbol: :zero, :one, :two, :few, :many, or :other
|
|
156
|
+
fn select_range(&self, start: Value, end: Value) -> Result<Symbol, Error> {
|
|
157
|
+
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
158
|
+
|
|
159
|
+
let start_decimal = Self::value_to_decimal(&ruby, start, "start")?;
|
|
160
|
+
let end_decimal = Self::value_to_decimal(&ruby, end, "end")?;
|
|
161
|
+
|
|
162
|
+
let category = self
|
|
163
|
+
.inner
|
|
164
|
+
.category_for_range(&start_decimal, &end_decimal);
|
|
165
|
+
|
|
166
|
+
Ok(Self::category_to_symbol(&ruby, category))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Convert a Ruby Value to a fixed_decimal::Decimal
|
|
170
|
+
fn value_to_decimal(ruby: &Ruby, value: Value, name: &str) -> Result<Decimal, Error> {
|
|
171
|
+
if value.is_kind_of(ruby.class_float()) {
|
|
172
|
+
let f: f64 = TryConvert::try_convert(value)?;
|
|
173
|
+
let s = format!("{}", f);
|
|
174
|
+
s.parse::<Decimal>().map_err(|_| {
|
|
175
|
+
Error::new(
|
|
176
|
+
ruby.exception_arg_error(),
|
|
177
|
+
format!("Failed to convert {} ({}) to Decimal", name, f),
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
} else if value.is_kind_of(ruby.class_integer()) {
|
|
181
|
+
let n: i64 = TryConvert::try_convert(value)?;
|
|
182
|
+
Ok(Decimal::from(n))
|
|
183
|
+
} else {
|
|
184
|
+
Err(Error::new(
|
|
185
|
+
ruby.exception_type_error(),
|
|
186
|
+
format!("{} must be an Integer or Float", name),
|
|
187
|
+
))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
/// Get the list of plural categories for this locale
|
|
144
192
|
///
|
|
145
193
|
/// # Returns
|
|
@@ -147,7 +195,7 @@ impl PluralRules {
|
|
|
147
195
|
fn categories(&self) -> RArray {
|
|
148
196
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
149
197
|
let array = ruby.ary_new();
|
|
150
|
-
for category in self.inner.categories() {
|
|
198
|
+
for category in self.inner.rules().categories() {
|
|
151
199
|
let _ = array.push(Self::category_to_symbol(&ruby, category));
|
|
152
200
|
}
|
|
153
201
|
array
|
|
@@ -187,6 +235,7 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
|
187
235
|
let class = module.define_class("PluralRules", ruby.class_object())?;
|
|
188
236
|
class.define_singleton_method("new", function!(PluralRules::new, -1))?;
|
|
189
237
|
class.define_method("select", method!(PluralRules::select, 1))?;
|
|
238
|
+
class.define_method("select_range", method!(PluralRules::select_range, 2))?;
|
|
190
239
|
class.define_method("categories", method!(PluralRules::categories, 0))?;
|
|
191
240
|
class.define_method(
|
|
192
241
|
"resolved_options",
|
|
@@ -1,6 +1,8 @@
|
|
|
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 fixed_decimal::Decimal;
|
|
5
|
+
use icu::decimal::parts as decimal_parts;
|
|
4
6
|
use icu::experimental::relativetime::options::Numeric;
|
|
5
7
|
use icu::experimental::relativetime::{
|
|
6
8
|
RelativeTimeFormatter, RelativeTimeFormatterOptions, RelativeTimeFormatterPreferences,
|
|
@@ -8,9 +10,10 @@ use icu::experimental::relativetime::{
|
|
|
8
10
|
use icu_provider::buf::AsDeserializingBufferProvider;
|
|
9
11
|
use icu4x_macros::RubySymbol;
|
|
10
12
|
use magnus::{
|
|
11
|
-
Error, ExceptionClass, RHash, RModule, Ruby, Symbol, TryConvert, Value, function,
|
|
12
|
-
prelude::*,
|
|
13
|
+
Error, ExceptionClass, RArray, RHash, RModule, Ruby, Symbol, TryConvert, Value, function,
|
|
14
|
+
method, prelude::*,
|
|
13
15
|
};
|
|
16
|
+
use writeable::{Part, Writeable};
|
|
14
17
|
|
|
15
18
|
/// The style of relative time formatting
|
|
16
19
|
#[derive(Clone, Copy, PartialEq, Eq, RubySymbol)]
|
|
@@ -64,6 +67,17 @@ impl Unit {
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/// Convert ICU4X relative time Part to Ruby symbol name
|
|
71
|
+
fn part_to_symbol_name(part: &Part) -> &'static str {
|
|
72
|
+
if *part == decimal_parts::INTEGER {
|
|
73
|
+
"integer"
|
|
74
|
+
} else if part.category == "relativetime" && part.value == "literal" {
|
|
75
|
+
"literal"
|
|
76
|
+
} else {
|
|
77
|
+
"literal"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
/// Ruby wrapper for ICU4X RelativeTimeFormatter
|
|
68
82
|
///
|
|
69
83
|
/// Stores formatters for all 8 time units for the selected style.
|
|
@@ -228,15 +242,45 @@ impl RelativeTimeFormat {
|
|
|
228
242
|
/// A formatted string
|
|
229
243
|
fn format(&self, value: i64, unit: Symbol) -> Result<String, Error> {
|
|
230
244
|
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
245
|
+
let (formatter, decimal) = self.prepare_value(&ruby, value, unit)?;
|
|
246
|
+
let formatted = formatter.format(decimal);
|
|
247
|
+
Ok(formatted.to_string())
|
|
248
|
+
}
|
|
231
249
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
250
|
+
/// Format a relative time value and return an array of FormattedPart
|
|
251
|
+
///
|
|
252
|
+
/// # Arguments
|
|
253
|
+
/// * `value` - The relative time value (negative = past, positive = future)
|
|
254
|
+
/// * `unit` - The time unit (:second, :minute, :hour, :day, :week, :month, :quarter, :year)
|
|
255
|
+
///
|
|
256
|
+
/// # Returns
|
|
257
|
+
/// An array of FormattedPart objects with :type and :value
|
|
258
|
+
fn format_to_parts(&self, value: i64, unit: Symbol) -> Result<RArray, Error> {
|
|
259
|
+
let ruby = Ruby::get().expect("Ruby runtime should be available");
|
|
260
|
+
let (formatter, decimal) = self.prepare_value(&ruby, value, unit)?;
|
|
237
261
|
|
|
238
262
|
let formatted = formatter.format(decimal);
|
|
239
|
-
|
|
263
|
+
let mut collector = PartsCollector::new();
|
|
264
|
+
formatted
|
|
265
|
+
.write_to_parts(&mut collector)
|
|
266
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{}", e)))?;
|
|
267
|
+
|
|
268
|
+
parts_to_ruby_array(&ruby, collector, part_to_symbol_name)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Prepare value for formatting.
|
|
272
|
+
///
|
|
273
|
+
/// Validates unit and converts value to Decimal.
|
|
274
|
+
fn prepare_value<'a>(
|
|
275
|
+
&'a self,
|
|
276
|
+
ruby: &Ruby,
|
|
277
|
+
value: i64,
|
|
278
|
+
unit: Symbol,
|
|
279
|
+
) -> Result<(&'a RelativeTimeFormatter, Decimal), Error> {
|
|
280
|
+
let unit = Unit::from_ruby_symbol(ruby, unit, "unit")?;
|
|
281
|
+
let formatter = &self.formatters[unit.index()];
|
|
282
|
+
let decimal = Decimal::from(value);
|
|
283
|
+
Ok((formatter, decimal))
|
|
240
284
|
}
|
|
241
285
|
|
|
242
286
|
/// Get the resolved options
|
|
@@ -263,6 +307,10 @@ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
|
263
307
|
let class = module.define_class("RelativeTimeFormat", ruby.class_object())?;
|
|
264
308
|
class.define_singleton_method("new", function!(RelativeTimeFormat::new, -1))?;
|
|
265
309
|
class.define_method("format", method!(RelativeTimeFormat::format, 2))?;
|
|
310
|
+
class.define_method(
|
|
311
|
+
"format_to_parts",
|
|
312
|
+
method!(RelativeTimeFormat::format_to_parts, 2),
|
|
313
|
+
)?;
|
|
266
314
|
class.define_method(
|
|
267
315
|
"resolved_options",
|
|
268
316
|
method!(RelativeTimeFormat::resolved_options, 0),
|
data/ext/icu4x_macros/Cargo.toml
CHANGED
data/lib/icu4x/data_gem_task.rb
CHANGED
|
@@ -84,13 +84,19 @@ module ICU4X
|
|
|
84
84
|
config:
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
lib_dir = gem_dir / "lib"
|
|
88
|
+
lib_data_dir = lib_dir / "icu4x" / "data"
|
|
88
89
|
lib_data_dir.mkpath
|
|
89
90
|
render_template(
|
|
90
91
|
"lib/icu4x/data/variant.rb.erb",
|
|
91
92
|
lib_data_dir / "#{variant}.rb",
|
|
92
93
|
variant:
|
|
93
94
|
)
|
|
95
|
+
render_template(
|
|
96
|
+
"lib/icu4x-data-variant.rb.erb",
|
|
97
|
+
lib_dir / "icu4x-data-#{variant}.rb",
|
|
98
|
+
variant:
|
|
99
|
+
)
|
|
94
100
|
|
|
95
101
|
# Copy LICENSE
|
|
96
102
|
FileUtils.cp("LICENSE.txt", gem_dir / "LICENSE.txt")
|