Skip to content

Commit c904ac3

Browse files
authored
Merge pull request #353 from Xiretza/number-function
2 parents efec1f5 + 1984162 commit c904ac3

File tree

6 files changed

+166
-4
lines changed

6 files changed

+166
-4
lines changed

fluent-bundle/src/builtins.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use crate::{FluentArgs, FluentValue};
2+
3+
#[allow(non_snake_case)]
4+
pub fn NUMBER<'a>(positional: &[FluentValue<'a>], named: &FluentArgs) -> FluentValue<'a> {
5+
let Some(FluentValue::Number(n)) = positional.first() else {
6+
return FluentValue::Error;
7+
};
8+
9+
let mut n = n.clone();
10+
n.options.merge(named);
11+
println!("{named:?} => {n:?}");
12+
13+
FluentValue::Number(n)
14+
}

fluent-bundle/src/bundle.rs

+55
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,61 @@ impl<R, M> FluentBundle<R, M> {
547547
}),
548548
}
549549
}
550+
551+
/// Adds the builtin functions described in the [FTL syntax guide] to the bundle, making them
552+
/// available in messages.
553+
///
554+
/// # Examples
555+
///
556+
/// ```
557+
/// use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
558+
/// use unic_langid::langid;
559+
///
560+
/// let ftl_string = String::from(r#"rank = { NUMBER($n, type: "ordinal") ->
561+
/// [1] first
562+
/// [2] second
563+
/// [3] third
564+
/// [one] {$n}st
565+
/// [two] {$n}nd
566+
/// [few] {$n}rd
567+
/// *[other] {$n}th
568+
/// }"#);
569+
/// let resource = FluentResource::try_new(ftl_string)
570+
/// .expect("Could not parse an FTL string.");
571+
/// let langid_en = langid!("en-US");
572+
/// let mut bundle = FluentBundle::new(vec![langid_en]);
573+
/// bundle.add_resource(&resource)
574+
/// .expect("Failed to add FTL resources to the bundle.");
575+
///
576+
/// // Register the builtin functions (including NUMBER())
577+
/// bundle.add_builtins().expect("Failed to add builtins to the bundle.");
578+
///
579+
/// let msg = bundle.get_message("rank").expect("Message doesn't exist.");
580+
/// let mut errors = vec![];
581+
/// let pattern = msg.value().expect("Message has no value.");
582+
///
583+
/// let mut args = FluentArgs::new();
584+
///
585+
/// args.set("n", 5);
586+
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
587+
/// assert_eq!(&value, "\u{2068}5\u{2069}th");
588+
///
589+
/// args.set("n", 12);
590+
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
591+
/// assert_eq!(&value, "\u{2068}12\u{2069}th");
592+
///
593+
/// args.set("n", 22);
594+
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
595+
/// assert_eq!(&value, "\u{2068}22\u{2069}nd");
596+
/// ```
597+
///
598+
/// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
599+
pub fn add_builtins(&mut self) -> Result<(), FluentError> {
600+
self.add_function("NUMBER", crate::builtins::NUMBER)?;
601+
// TODO: DATETIME()
602+
603+
Ok(())
604+
}
550605
}
551606

552607
impl<R> Default for FluentBundle<R, IntlLangMemoizer> {

fluent-bundle/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
//! the `fluent-bundle` crate directly, while the ecosystem
100100
//! matures and higher level APIs are being developed.
101101
mod args;
102+
pub mod builtins;
102103
pub mod bundle;
103104
pub mod concurrent;
104105
mod entry;

fluent-bundle/src/types/mod.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,16 @@ impl<'source> FluentValue<'source> {
199199
};
200200
// This string matches a plural rule keyword. Check if the number
201201
// matches the plural rule category.
202+
let r#type = match b.options.r#type {
203+
FluentNumberType::Cardinal => PluralRuleType::CARDINAL,
204+
FluentNumberType::Ordinal => PluralRuleType::ORDINAL,
205+
};
202206
scope
203207
.bundle
204208
.intls
205-
.with_try_get_threadsafe::<PluralRules, _, _>(
206-
(PluralRuleType::CARDINAL,),
207-
|pr| pr.0.select(b) == Ok(cat),
208-
)
209+
.with_try_get_threadsafe::<PluralRules, _, _>((r#type,), |pr| {
210+
pr.0.select(b) == Ok(cat)
211+
})
209212
.unwrap()
210213
}
211214
_ => false,

fluent-bundle/src/types/number.rs

+22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ use intl_pluralrules::operands::PluralOperands;
88
use crate::args::FluentArgs;
99
use crate::types::FluentValue;
1010

11+
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
12+
pub enum FluentNumberType {
13+
#[default]
14+
Cardinal,
15+
Ordinal,
16+
}
17+
18+
impl From<&str> for FluentNumberType {
19+
fn from(input: &str) -> Self {
20+
match input {
21+
"cardinal" => Self::Cardinal,
22+
"ordinal" => Self::Ordinal,
23+
_ => Self::default(),
24+
}
25+
}
26+
}
27+
1128
#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq)]
1229
pub enum FluentNumberStyle {
1330
#[default]
@@ -48,6 +65,7 @@ impl From<&str> for FluentNumberCurrencyDisplayStyle {
4865

4966
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
5067
pub struct FluentNumberOptions {
68+
pub r#type: FluentNumberType,
5169
pub style: FluentNumberStyle,
5270
pub currency: Option<String>,
5371
pub currency_display: FluentNumberCurrencyDisplayStyle,
@@ -62,6 +80,7 @@ pub struct FluentNumberOptions {
6280
impl Default for FluentNumberOptions {
6381
fn default() -> Self {
6482
Self {
83+
r#type: Default::default(),
6584
style: Default::default(),
6685
currency: None,
6786
currency_display: Default::default(),
@@ -79,6 +98,9 @@ impl FluentNumberOptions {
7998
pub fn merge(&mut self, opts: &FluentArgs) {
8099
for (key, value) in opts.iter() {
81100
match (key, value) {
101+
("type", FluentValue::String(n)) => {
102+
self.r#type = n.as_ref().into();
103+
}
82104
("style", FluentValue::String(n)) => {
83105
self.style = n.as_ref().into();
84106
}

fluent-bundle/tests/builtins.rs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
2+
use fluent_syntax::ast::Pattern;
3+
4+
#[test]
5+
fn test_builtin_number() {
6+
// 1. Create bundle
7+
let ftl_string = String::from(
8+
r#"
9+
count = { NUMBER($num, type: "cardinal") ->
10+
*[other] A
11+
[one] B
12+
}
13+
order = { NUMBER($num, type: "ordinal") ->
14+
*[other] {$num}th
15+
[one] {$num}st
16+
[two] {$num}nd
17+
[few] {$num}rd
18+
}
19+
"#,
20+
);
21+
22+
let mut bundle = FluentBundle::default();
23+
bundle
24+
.add_resource(FluentResource::try_new(ftl_string).expect("Could not parse an FTL string."))
25+
.expect("Failed to add FTL resources to the bundle.");
26+
bundle
27+
.add_builtins()
28+
.expect("Failed to add builtin functions to the bundle.");
29+
30+
let get_val = |pattern: &Pattern<&'_ str>, num: isize| {
31+
let mut args = FluentArgs::new();
32+
args.set("num", FluentValue::from(num));
33+
let mut errors = vec![];
34+
let val = bundle.format_pattern(pattern, Some(&args), &mut errors);
35+
if errors.is_empty() {
36+
Ok(val.into_owned())
37+
} else {
38+
Err(errors)
39+
}
40+
};
41+
42+
let count = bundle
43+
.get_message("count")
44+
.expect("Message doesn't exist")
45+
.value()
46+
.expect("Message has no value");
47+
48+
assert_eq!(get_val(count, 0).unwrap(), "A");
49+
assert_eq!(get_val(count, 1).unwrap(), "B");
50+
assert_eq!(get_val(count, 2).unwrap(), "A");
51+
assert_eq!(get_val(count, 12).unwrap(), "A");
52+
assert_eq!(get_val(count, 15).unwrap(), "A");
53+
assert_eq!(get_val(count, 123).unwrap(), "A");
54+
55+
let order = bundle
56+
.get_message("order")
57+
.expect("Message doesn't exist")
58+
.value()
59+
.expect("Message has no value");
60+
61+
assert_eq!(get_val(order, 0).unwrap(), "\u{2068}0\u{2069}th");
62+
assert_eq!(get_val(order, 1).unwrap(), "\u{2068}1\u{2069}st");
63+
assert_eq!(get_val(order, 2).unwrap(), "\u{2068}2\u{2069}nd");
64+
assert_eq!(get_val(order, 12).unwrap(), "\u{2068}12\u{2069}th");
65+
assert_eq!(get_val(order, 15).unwrap(), "\u{2068}15\u{2069}th");
66+
assert_eq!(get_val(order, 123).unwrap(), "\u{2068}123\u{2069}rd");
67+
}

0 commit comments

Comments
 (0)