Skip to content

Commit

Permalink
Merge pull request #353 from Xiretza/number-function
Browse files Browse the repository at this point in the history
  • Loading branch information
alerque committed May 7, 2024
2 parents efec1f5 + 1984162 commit c904ac3
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 4 deletions.
14 changes: 14 additions & 0 deletions fluent-bundle/src/builtins.rs
@@ -0,0 +1,14 @@
use crate::{FluentArgs, FluentValue};

#[allow(non_snake_case)]
pub fn NUMBER<'a>(positional: &[FluentValue<'a>], named: &FluentArgs) -> FluentValue<'a> {
let Some(FluentValue::Number(n)) = positional.first() else {
return FluentValue::Error;
};

let mut n = n.clone();
n.options.merge(named);
println!("{named:?} => {n:?}");

FluentValue::Number(n)
}
55 changes: 55 additions & 0 deletions fluent-bundle/src/bundle.rs
Expand Up @@ -547,6 +547,61 @@ impl<R, M> FluentBundle<R, M> {
}),
}
}

/// Adds the builtin functions described in the [FTL syntax guide] to the bundle, making them
/// available in messages.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
/// use unic_langid::langid;
///
/// let ftl_string = String::from(r#"rank = { NUMBER($n, type: "ordinal") ->
/// [1] first
/// [2] second
/// [3] third
/// [one] {$n}st
/// [two] {$n}nd
/// [few] {$n}rd
/// *[other] {$n}th
/// }"#);
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(vec![langid_en]);
/// bundle.add_resource(&resource)
/// .expect("Failed to add FTL resources to the bundle.");
///
/// // Register the builtin functions (including NUMBER())
/// bundle.add_builtins().expect("Failed to add builtins to the bundle.");
///
/// let msg = bundle.get_message("rank").expect("Message doesn't exist.");
/// let mut errors = vec![];
/// let pattern = msg.value().expect("Message has no value.");
///
/// let mut args = FluentArgs::new();
///
/// args.set("n", 5);
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
/// assert_eq!(&value, "\u{2068}5\u{2069}th");
///
/// args.set("n", 12);
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
/// assert_eq!(&value, "\u{2068}12\u{2069}th");
///
/// args.set("n", 22);
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
/// assert_eq!(&value, "\u{2068}22\u{2069}nd");
/// ```
///
/// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
pub fn add_builtins(&mut self) -> Result<(), FluentError> {
self.add_function("NUMBER", crate::builtins::NUMBER)?;
// TODO: DATETIME()

Ok(())
}
}

impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
Expand Down
1 change: 1 addition & 0 deletions fluent-bundle/src/lib.rs
Expand Up @@ -99,6 +99,7 @@
//! the `fluent-bundle` crate directly, while the ecosystem
//! matures and higher level APIs are being developed.
mod args;
pub mod builtins;
pub mod bundle;
pub mod concurrent;
mod entry;
Expand Down
11 changes: 7 additions & 4 deletions fluent-bundle/src/types/mod.rs
Expand Up @@ -199,13 +199,16 @@ impl<'source> FluentValue<'source> {
};
// This string matches a plural rule keyword. Check if the number
// matches the plural rule category.
let r#type = match b.options.r#type {
FluentNumberType::Cardinal => PluralRuleType::CARDINAL,
FluentNumberType::Ordinal => PluralRuleType::ORDINAL,
};
scope
.bundle
.intls
.with_try_get_threadsafe::<PluralRules, _, _>(
(PluralRuleType::CARDINAL,),
|pr| pr.0.select(b) == Ok(cat),
)
.with_try_get_threadsafe::<PluralRules, _, _>((r#type,), |pr| {
pr.0.select(b) == Ok(cat)
})
.unwrap()
}
_ => false,
Expand Down
22 changes: 22 additions & 0 deletions fluent-bundle/src/types/number.rs
Expand Up @@ -8,6 +8,23 @@ use intl_pluralrules::operands::PluralOperands;
use crate::args::FluentArgs;
use crate::types::FluentValue;

#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub enum FluentNumberType {
#[default]
Cardinal,
Ordinal,
}

impl From<&str> for FluentNumberType {
fn from(input: &str) -> Self {
match input {
"cardinal" => Self::Cardinal,
"ordinal" => Self::Ordinal,
_ => Self::default(),
}
}
}

#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq)]
pub enum FluentNumberStyle {
#[default]
Expand Down Expand Up @@ -48,6 +65,7 @@ impl From<&str> for FluentNumberCurrencyDisplayStyle {

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct FluentNumberOptions {
pub r#type: FluentNumberType,
pub style: FluentNumberStyle,
pub currency: Option<String>,
pub currency_display: FluentNumberCurrencyDisplayStyle,
Expand All @@ -62,6 +80,7 @@ pub struct FluentNumberOptions {
impl Default for FluentNumberOptions {
fn default() -> Self {
Self {
r#type: Default::default(),
style: Default::default(),
currency: None,
currency_display: Default::default(),
Expand All @@ -79,6 +98,9 @@ impl FluentNumberOptions {
pub fn merge(&mut self, opts: &FluentArgs) {
for (key, value) in opts.iter() {
match (key, value) {
("type", FluentValue::String(n)) => {
self.r#type = n.as_ref().into();
}
("style", FluentValue::String(n)) => {
self.style = n.as_ref().into();
}
Expand Down
67 changes: 67 additions & 0 deletions fluent-bundle/tests/builtins.rs
@@ -0,0 +1,67 @@
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use fluent_syntax::ast::Pattern;

#[test]
fn test_builtin_number() {
// 1. Create bundle
let ftl_string = String::from(
r#"
count = { NUMBER($num, type: "cardinal") ->
*[other] A
[one] B
}
order = { NUMBER($num, type: "ordinal") ->
*[other] {$num}th
[one] {$num}st
[two] {$num}nd
[few] {$num}rd
}
"#,
);

let mut bundle = FluentBundle::default();
bundle
.add_resource(FluentResource::try_new(ftl_string).expect("Could not parse an FTL string."))
.expect("Failed to add FTL resources to the bundle.");
bundle
.add_builtins()
.expect("Failed to add builtin functions to the bundle.");

let get_val = |pattern: &Pattern<&'_ str>, num: isize| {
let mut args = FluentArgs::new();
args.set("num", FluentValue::from(num));
let mut errors = vec![];
let val = bundle.format_pattern(pattern, Some(&args), &mut errors);
if errors.is_empty() {
Ok(val.into_owned())
} else {
Err(errors)
}
};

let count = bundle
.get_message("count")
.expect("Message doesn't exist")
.value()
.expect("Message has no value");

assert_eq!(get_val(count, 0).unwrap(), "A");
assert_eq!(get_val(count, 1).unwrap(), "B");
assert_eq!(get_val(count, 2).unwrap(), "A");
assert_eq!(get_val(count, 12).unwrap(), "A");
assert_eq!(get_val(count, 15).unwrap(), "A");
assert_eq!(get_val(count, 123).unwrap(), "A");

let order = bundle
.get_message("order")
.expect("Message doesn't exist")
.value()
.expect("Message has no value");

assert_eq!(get_val(order, 0).unwrap(), "\u{2068}0\u{2069}th");
assert_eq!(get_val(order, 1).unwrap(), "\u{2068}1\u{2069}st");
assert_eq!(get_val(order, 2).unwrap(), "\u{2068}2\u{2069}nd");
assert_eq!(get_val(order, 12).unwrap(), "\u{2068}12\u{2069}th");
assert_eq!(get_val(order, 15).unwrap(), "\u{2068}15\u{2069}th");
assert_eq!(get_val(order, 123).unwrap(), "\u{2068}123\u{2069}rd");
}

0 comments on commit c904ac3

Please sign in to comment.