diff --git a/components/pluralrules/benches/pluralrules.rs b/components/pluralrules/benches/pluralrules.rs index 745e0b0dd52..14d5152e3d6 100644 --- a/components/pluralrules/benches/pluralrules.rs +++ b/components/pluralrules/benches/pluralrules.rs @@ -6,7 +6,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use icu_pluralrules::PluralCategory; fn plurals_bench(c: &mut Criterion) { - use icu_pluralrules::rules::{parse, Lexer}; + use icu_pluralrules::rules::{parse_condition, Lexer}; let path = "./benches/fixtures/plurals.json"; let data: fixtures::PluralsFixture = @@ -34,7 +34,7 @@ fn plurals_bench(c: &mut Criterion) { group.bench_function("parse", |b| { b.iter(|| { for val in &pl_data { - let _ = parse(black_box(val.as_bytes())); + let _ = parse_condition(black_box(val.as_bytes())); } }) }); diff --git a/components/pluralrules/src/data/io/bincode/mod.rs b/components/pluralrules/src/data/io/bincode/mod.rs index d691ad6535f..43607dfa6a2 100644 --- a/components/pluralrules/src/data/io/bincode/mod.rs +++ b/components/pluralrules/src/data/io/bincode/mod.rs @@ -1,7 +1,7 @@ use crate::data::cldr_resource::Resource; use crate::data::provider::{DataProviderError, DataProviderType}; use crate::data::{PluralRuleList, RulesSelector}; -use crate::rules::parse; +use crate::rules::parse_condition; use crate::PluralCategory; use crate::PluralRuleType; use icu_locale::LanguageIdentifier; @@ -89,7 +89,7 @@ pub fn get_rules( let result = PluralCategory::all() .filter_map(|pc| { let input = lang_rules.get(pc)?; - Some(parse(input.as_bytes()).map(|ast| (*pc, ast))) + Some(parse_condition(input.as_bytes()).map(|ast| (*pc, ast))) }) .collect::>()?; diff --git a/components/pluralrules/src/data/io/json/mod.rs b/components/pluralrules/src/data/io/json/mod.rs index 95b975dbd80..0c544736ecc 100644 --- a/components/pluralrules/src/data/io/json/mod.rs +++ b/components/pluralrules/src/data/io/json/mod.rs @@ -1,7 +1,7 @@ use crate::data::cldr_resource::Resource; use crate::data::provider::{DataProviderError, DataProviderType}; use crate::data::{PluralRuleList, RulesSelector}; -use crate::rules::parse; +use crate::rules::parse_condition; use crate::PluralCategory; use crate::PluralRuleType; use icu_locale::LanguageIdentifier; @@ -88,7 +88,7 @@ pub fn get_rules( let result = PluralCategory::all() .filter_map(|pc| { let input = lang_rules.get(pc)?; - Some(parse(input.as_bytes()).map(|ast| (*pc, ast))) + Some(parse_condition(input.as_bytes()).map(|ast| (*pc, ast))) }) .collect::>()?; diff --git a/components/pluralrules/src/data/provider.rs b/components/pluralrules/src/data/provider.rs index cd08344ef4c..20c0bddd75f 100644 --- a/components/pluralrules/src/data/provider.rs +++ b/components/pluralrules/src/data/provider.rs @@ -20,7 +20,7 @@ //! use icu_pluralrules::rules::ast; //! use icu_locale::LanguageIdentifier; //! -//! use icu_pluralrules::rules::parse; +//! use icu_pluralrules::rules::parse_condition; //! //! struct MyDataProvider {} //! @@ -39,7 +39,7 @@ //! //! let conditions: Vec<(PluralCategory, ast::Condition)> = //! sources.iter().map(|(category, rule_str)| { -//! let condition = parse(rule_str.as_bytes()) +//! let condition = parse_condition(rule_str.as_bytes()) //! .expect("Failed to parse the plural rule."); //! (*category, condition) //! }).collect(); diff --git a/components/pluralrules/src/rules/ast.rs b/components/pluralrules/src/rules/ast.rs index 148d8a39f6f..62279dcac13 100644 --- a/components/pluralrules/src/rules/ast.rs +++ b/components/pluralrules/src/rules/ast.rs @@ -5,12 +5,12 @@ //! # Examples //! //! ``` -//! use icu_pluralrules::rules::parse; +//! use icu_pluralrules::rules::parse_condition; //! use icu_pluralrules::rules::ast::*; //! //! let input = "i = 1"; //! -//! let ast = parse(input.as_bytes()) +//! let ast = parse_condition(input.as_bytes()) //! .expect("Parsing failed."); //! //! assert_eq!(ast, Condition(Box::new([ @@ -36,19 +36,59 @@ //! [`test_condition`]: ../fn.test_condition.html use std::ops::RangeInclusive; +/// A complete AST representation of a plural rule. +/// Comprises a vector of AndConditions and optionally a set of Samples. +/// +/// # Examples +/// +/// ``` +/// use icu_pluralrules::rules::ast::*; +/// use icu_pluralrules::rules::{parse, parse_condition}; +/// +/// let condition = parse_condition(b"i = 5 or v = 2") +/// .expect("Parsing failed."); +/// +/// let samples = Samples { +/// integer: Some(SampleList { +/// sample_ranges: Box::new([SampleRange { +/// lower_val: DecimalValue("2".to_string()), +/// upper_val: None, +/// }]), +/// ellipsis: true +/// }), +/// decimal: Some(SampleList { +/// sample_ranges: Box::new([SampleRange { +/// lower_val: DecimalValue("2.5".to_string()), +/// upper_val: None, +/// }]), +/// ellipsis: false +/// }), +/// }; +/// +/// let rule = Rule { +/// condition, +/// samples: Some(samples), +/// }; +/// +/// assert_eq!( +/// rule, +/// parse("i = 5 or v = 2 @integer 2, … @decimal 2.5".as_bytes()) +/// .expect("Parsing failed") +/// ) +/// ``` #[derive(Debug, Clone, PartialEq)] pub struct Rule { pub condition: Condition, pub samples: Option, } -/// A complete (and the only complete) AST representation of a plural rule. Comprises a vector of AndConditions. +/// A complete AST representation of a plural rule's condition. Comprises a vector of AndConditions. /// /// # Examples /// /// ``` /// use icu_pluralrules::rules::ast::*; -/// use icu_pluralrules::rules::parse; +/// use icu_pluralrules::rules::parse_condition; /// /// let condition = Condition(Box::new([ /// AndCondition(Box::new([Relation { @@ -71,7 +111,7 @@ pub struct Rule { /// /// assert_eq!( /// condition, -/// parse(b"i = 5 or v = 2") +/// parse_condition(b"i = 5 or v = 2") /// .expect("Parsing failed") /// ) /// ``` @@ -157,7 +197,7 @@ pub struct Relation { /// | Eq | "=" | /// | NotEq | "!=" | /// -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Operator { Eq, NotEq, @@ -207,7 +247,7 @@ pub struct Expression { /// /// Operand::I; /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Operand { /// Absolute value of input N, @@ -306,20 +346,14 @@ pub struct Value(pub u64); /// Samples { /// integer: Some(SampleList { /// sample_ranges: Box::new([SampleRange { -/// lower_val: DecimalValue { -/// integer: Value(2), -/// decimal: None -/// }, +/// lower_val: DecimalValue("2".to_string()), /// upper_val: None, /// }]), /// ellipsis: true /// }), /// decimal: Some(SampleList { /// sample_ranges: Box::new([SampleRange { -/// lower_val: DecimalValue { -/// integer: Value(2), -/// decimal: Some(Value(5)) -/// }, +/// lower_val: DecimalValue("2.5".to_string()), /// upper_val: None, /// }]), /// ellipsis: false @@ -345,14 +379,8 @@ pub struct Samples { /// SampleList { /// sample_ranges: Box::new([ /// SampleRange { -/// lower_val: DecimalValue { -/// integer: Value(0), -/// decimal: Some(Value(0)), -/// }, -/// upper_val: Some(DecimalValue { -/// integer: Value(1), -/// decimal: Some(Value(5)), -/// }), +/// lower_val: DecimalValue("0.0".to_string()), +/// upper_val: Some(DecimalValue("1.5".to_string())), /// } /// ]), /// ellipsis: true @@ -375,14 +403,8 @@ pub struct SampleList { /// ``` /// use icu_pluralrules::rules::ast::*; /// SampleRange { -/// lower_val: DecimalValue { -/// integer: Value(0), -/// decimal: Some(Value(0)), -/// }, -/// upper_val: Some(DecimalValue { -/// integer: Value(1), -/// decimal: Some(Value(5)), -/// }), +/// lower_val: DecimalValue("0.0".to_string()), +/// upper_val: Some(DecimalValue("1.5".to_string())), /// }; /// ``` #[derive(Debug, Clone, PartialEq)] @@ -396,18 +418,12 @@ pub struct SampleRange { /// # Examples /// /// ```text -/// 1.5 +/// 1.00 /// ``` /// /// ``` /// use icu_pluralrules::rules::ast::*; -/// DecimalValue { -/// integer: Value(1), -/// decimal: Some(Value(5)), -/// }; +/// DecimalValue("1.00".to_string()); /// ``` #[derive(Debug, Clone, PartialEq)] -pub struct DecimalValue { - pub integer: Value, - pub decimal: Option, -} +pub struct DecimalValue(pub String); diff --git a/components/pluralrules/src/rules/lexer.rs b/components/pluralrules/src/rules/lexer.rs index 2d71ce5ce98..f032d7576dd 100644 --- a/components/pluralrules/src/rules/lexer.rs +++ b/components/pluralrules/src/rules/lexer.rs @@ -5,9 +5,9 @@ pub enum Token { Operand(ast::Operand), Operator(ast::Operator), Number(u32), + Zero, Dot, DotDot, - DotDotDot, Comma, Or, And, @@ -106,7 +106,9 @@ impl<'l> Lexer<'l> { b'v' => Token::Operand(ast::Operand::V), b'w' => Token::Operand(ast::Operand::W), b'=' => Token::Operator(ast::Operator::Eq), - b'0'..=b'9' => { + // Zero is special, because we need to preserve it for Samples. + b'0' => Token::Zero, + b'1'..=b'9' => { let start = self.ptr - 1; while let Some(b'0'..=b'9') = self.chars.get(self.ptr) { diff --git a/components/pluralrules/src/rules/mod.rs b/components/pluralrules/src/rules/mod.rs index b468ac2fe70..511e9960737 100644 --- a/components/pluralrules/src/rules/mod.rs +++ b/components/pluralrules/src/rules/mod.rs @@ -49,12 +49,12 @@ //! When parsed, the resulting [`AST`] will look like this: //! //! ``` -//! use icu_pluralrules::rules::parse; +//! use icu_pluralrules::rules::parse_condition; //! use icu_pluralrules::rules::ast::*; //! //! let input = "i = 1 and v = 0 @integer 1"; //! -//! let ast = parse(input.as_bytes()) +//! let ast = parse_condition(input.as_bytes()) //! .expect("Parsing failed."); //! assert_eq!(ast, Condition(Box::new([ //! AndCondition(Box::new([ @@ -91,14 +91,14 @@ //! matches: //! //! ``` -//! use icu_pluralrules::rules::{test_condition, parse}; +//! use icu_pluralrules::rules::{test_condition, parse_condition}; //! use icu_pluralrules::PluralOperands; //! //! let input = "i = 1 and v = 0 @integer 1"; //! //! let operands = PluralOperands::from(1_u32); //! -//! let ast = parse(input.as_bytes()) +//! let ast = parse_condition(input.as_bytes()) //! .expect("Parsing failed."); //! //! assert!(test_condition(&ast, &operands)); @@ -141,7 +141,9 @@ pub mod ast; pub(crate) mod lexer; pub(crate) mod parser; pub(crate) mod resolver; +pub(crate) mod serializer; pub use lexer::Lexer; -pub use parser::parse; +pub use parser::{parse, parse_condition}; pub use resolver::test_condition; +pub use serializer::serialize; diff --git a/components/pluralrules/src/rules/parser.rs b/components/pluralrules/src/rules/parser.rs index 0357a890af1..ad32a4e8d45 100644 --- a/components/pluralrules/src/rules/parser.rs +++ b/components/pluralrules/src/rules/parser.rs @@ -9,20 +9,29 @@ pub enum ParserError { ExpectedOperator, ExpectedOperand, ExpectedValue, + ExpectedSampleType, } /// Unicode Plural Rule parser converts an -/// input string into an [`AST`]. +/// input string into a Rule [`AST`]. /// -/// That [`AST`] can be then used by the [`resolver`] to test +/// A single [`Rule`] contains a [`Condition`] and optionally a set of +/// [`Samples`]. +/// +/// A [`Condition`] can be then used by the [`resolver`] to test /// against [`PluralOperands`], to find the appropriate [`PluralCategory`]. /// +/// [`Samples`] are useful for tooling to help translators understand examples of numbers +/// covered by the given [`Rule`]. +/// +/// At runtime, only the [`Condition`] is used and for that, consider using [`parse_condition`]. +/// /// # Examples /// /// ``` /// use icu_pluralrules::rules::parse; /// -/// let input = b"i = 5"; +/// let input = b"i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04"; /// assert_eq!(parse(input).is_ok(), true); /// ``` /// @@ -30,11 +39,39 @@ pub enum ParserError { /// [`resolver`]: ../rules/resolver/index.html /// [`PluralOperands`]: ../struct.PluralOperands.html /// [`PluralCategory`]: ../enum.PluralCategory.html -pub fn parse(input: &[u8]) -> Result { +/// [`Rule`]: ../rules/ast/struct.Rule.html +/// [`Samples`]: ../rules/ast/struct.Samples.html +/// [`Condition`]: ../rules/ast/struct.Condition.html +/// [`parse_condition`]: ./fn.parse_condition.html +pub fn parse(input: &[u8]) -> Result { let parser = Parser::new(input); parser.parse() } +/// Unicode Plural Rule parser converts an +/// input string into an [`AST`]. +/// +/// That [`AST`] can be then used by the [`resolver`] to test +/// against [`PluralOperands`], to find the appropriate [`PluralCategory`]. +/// +/// # Examples +/// +/// ``` +/// use icu_pluralrules::rules::parse_condition; +/// +/// let input = b"i = 0 or n = 1"; +/// assert_eq!(parse_condition(input).is_ok(), true); +/// ``` +/// +/// [`AST`]: ../rules/ast/index.html +/// [`resolver`]: ../rules/resolver/index.html +/// [`PluralOperands`]: ../struct.PluralOperands.html +/// [`PluralCategory`]: ../enum.PluralCategory.html +pub fn parse_condition(input: &[u8]) -> Result { + let parser = Parser::new(input); + parser.parse_condition() +} + struct Parser<'p> { lexer: Peekable>, } @@ -46,7 +83,22 @@ impl<'p> Parser<'p> { } } - fn parse(mut self) -> Result { + pub fn parse(mut self) -> Result { + self.get_rule() + } + + pub fn parse_condition(mut self) -> Result { + self.get_condition() + } + + fn get_rule(&mut self) -> Result { + Ok(ast::Rule { + condition: self.get_condition()?, + samples: self.get_samples()?, + }) + } + + fn get_condition(&mut self) -> Result { let mut result = vec![]; if let Some(cond) = self.get_and_condition()? { @@ -101,11 +153,12 @@ impl<'p> Parser<'p> { } fn get_expression(&mut self) -> Result, ParserError> { - let operand = match self.lexer.next() { - Some(Token::Operand(op)) => op, + let operand = match self.lexer.peek() { + Some(Token::Operand(op)) => *op, Some(Token::At) | None => return Ok(None), _ => return Err(ParserError::ExpectedOperand), }; + self.lexer.next(); let modulus = if self.take_if(Token::Modulo) { Some(self.get_value()?) } else { @@ -145,10 +198,94 @@ impl<'p> Parser<'p> { } fn get_value(&mut self) -> Result { - if let Some(Token::Number(v)) = self.lexer.next() { - Ok(ast::Value(v as u64)) + match self.lexer.next() { + Some(Token::Number(v)) => Ok(ast::Value(v as u64)), + Some(Token::Zero) => Ok(ast::Value(0)), + _ => Err(ParserError::ExpectedValue), + } + } + + fn get_samples(&mut self) -> Result, ParserError> { + let mut integer = None; + let mut decimal = None; + + while self.take_if(Token::At) { + match self.lexer.next() { + Some(Token::Integer) => integer = Some(self.get_sample_list()?), + Some(Token::Decimal) => decimal = Some(self.get_sample_list()?), + _ => return Err(ParserError::ExpectedSampleType), + }; + } + if integer.is_some() || decimal.is_some() { + Ok(Some(ast::Samples { integer, decimal })) + } else { + Ok(None) + } + } + + fn get_sample_list(&mut self) -> Result { + let mut ranges = vec![self.get_sample_range()?]; + let mut ellipsis = false; + + while self.take_if(Token::Comma) { + if self.take_if(Token::Ellipsis) { + ellipsis = true; + break; + } + ranges.push(self.get_sample_range()?); + } + Ok(ast::SampleList { + sample_ranges: ranges.into_boxed_slice(), + ellipsis, + }) + } + + fn get_sample_range(&mut self) -> Result { + let lower_val = self.get_decimal_value()?; + let upper_val = if self.take_if(Token::Tilde) { + Some(self.get_decimal_value()?) } else { + None + }; + Ok(ast::SampleRange { + lower_val, + upper_val, + }) + } + + fn get_decimal_value(&mut self) -> Result { + let mut s = String::new(); + loop { + match self.lexer.peek() { + Some(Token::Zero) => s.push('0'), + Some(Token::Number(v)) => { + s.push_str(&v.to_string()); + } + _ => { + break; + } + } + self.lexer.next(); + } + if self.take_if(Token::Dot) { + s.push('.'); + loop { + match self.lexer.peek() { + Some(Token::Zero) => s.push('0'), + Some(Token::Number(v)) => { + s.push_str(&v.to_string()); + } + _ => { + break; + } + } + self.lexer.next(); + } + } + if s.is_empty() { Err(ParserError::ExpectedValue) + } else { + Ok(ast::DecimalValue(s)) } } } diff --git a/components/pluralrules/src/rules/resolver.rs b/components/pluralrules/src/rules/resolver.rs index 6d6dbe26ea1..9b46dfadf59 100644 --- a/components/pluralrules/src/rules/resolver.rs +++ b/components/pluralrules/src/rules/resolver.rs @@ -8,10 +8,10 @@ use crate::operands::PluralOperands; /// /// ``` /// use icu_pluralrules::PluralOperands; -/// use icu_pluralrules::rules::{parse, test_condition}; +/// use icu_pluralrules::rules::{parse_condition, test_condition}; /// /// let operands = PluralOperands::from(5_usize); -/// let condition = parse(b"i = 4..6") +/// let condition = parse_condition(b"i = 4..6") /// .expect("Failde to parse a rule."); /// /// assert_eq!(test_condition(&condition, &operands), true); diff --git a/components/pluralrules/src/rules/serializer.rs b/components/pluralrules/src/rules/serializer.rs new file mode 100644 index 00000000000..5b7b4eba98d --- /dev/null +++ b/components/pluralrules/src/rules/serializer.rs @@ -0,0 +1,187 @@ +use crate::rules::ast; +use std::fmt; +use std::ops::RangeInclusive; + +/// Unicode Plural Rule serializer converts an [`AST`] to a `String`. +/// +/// # Examples +/// +/// ``` +/// use icu_pluralrules::rules::parse; +/// use icu_pluralrules::rules::ast; +/// use icu_pluralrules::rules::serialize; +/// +/// let input = "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04"; +/// +/// let ast = parse(input.as_bytes()) +/// .expect("Parsing failed."); +/// +/// assert_eq!(ast.condition.0[0].0[0].expression.operand, ast::Operand::I); +/// assert_eq!(ast.condition.0[1].0[0].expression.operand, ast::Operand::N); +/// +/// let mut result = String::new(); +/// serialize(&ast, &mut result) +/// .expect("Serialization failed."); +/// +/// assert_eq!(input, result); +/// ``` +/// +/// [`AST`]: ../rules/ast/index.html +/// [`resolver`]: ../rules/resolver/index.html +/// [`PluralOperands`]: ../struct.PluralOperands.html +/// [`PluralCategory`]: ../enum.PluralCategory.html +/// [`Rule`]: ../rules/ast/struct.Rule.html +/// [`Samples`]: ../rules/ast/struct.Samples.html +/// [`Condition`]: ../rules/ast/struct.Condition.html +/// [`parse_condition`]: ./fn.parse_condition.html +pub fn serialize(rule: &ast::Rule, w: &mut impl fmt::Write) -> fmt::Result { + serialize_condition(&rule.condition, w)?; + if let Some(samples) = &rule.samples { + serialize_samples(samples, w)?; + } + Ok(()) +} + +pub fn serialize_condition(cond: &ast::Condition, w: &mut impl fmt::Write) -> fmt::Result { + let mut first = true; + + for cond in cond.0.iter() { + if first { + first = false; + } else { + w.write_str(" or ")?; + } + serialize_andcondition(cond, w)?; + } + Ok(()) +} + +fn serialize_andcondition(cond: &ast::AndCondition, w: &mut impl fmt::Write) -> fmt::Result { + let mut first = true; + + for relation in cond.0.iter() { + if first { + first = false; + } else { + w.write_str(" and ")?; + } + serialize_relation(relation, w)?; + } + Ok(()) +} + +fn serialize_relation(relation: &ast::Relation, w: &mut impl fmt::Write) -> fmt::Result { + serialize_expression(&relation.expression, w)?; + w.write_char(' ')?; + serialize_operator(&relation.operator, w)?; + w.write_char(' ')?; + serialize_rangelist(&relation.range_list, w) +} + +fn serialize_expression(exp: &ast::Expression, w: &mut impl fmt::Write) -> fmt::Result { + serialize_operand(&exp.operand, w)?; + if let Some(modulus) = &exp.modulus { + w.write_str(" % ")?; + serialize_value(modulus, w)?; + } + Ok(()) +} + +fn serialize_operator(operator: &ast::Operator, w: &mut impl fmt::Write) -> fmt::Result { + match operator { + ast::Operator::Eq => w.write_char('='), + ast::Operator::NotEq => w.write_str("!="), + } +} + +fn serialize_operand(operand: &ast::Operand, w: &mut impl fmt::Write) -> fmt::Result { + match operand { + ast::Operand::N => w.write_char('n'), + ast::Operand::I => w.write_char('i'), + ast::Operand::V => w.write_char('v'), + ast::Operand::W => w.write_char('w'), + ast::Operand::F => w.write_char('f'), + ast::Operand::T => w.write_char('t'), + } +} + +fn serialize_rangelist(rl: &ast::RangeList, w: &mut impl fmt::Write) -> fmt::Result { + let mut first = true; + + for rli in rl.0.iter() { + if first { + first = false; + } else { + w.write_str(",")?; + } + serialize_rangelistitem(rli, w)? + } + Ok(()) +} + +fn serialize_rangelistitem(rli: &ast::RangeListItem, w: &mut impl fmt::Write) -> fmt::Result { + match rli { + ast::RangeListItem::Range(range) => serialize_range(range, w), + ast::RangeListItem::Value(v) => serialize_value(v, w), + } +} + +fn serialize_range(range: &RangeInclusive, w: &mut impl fmt::Write) -> fmt::Result { + serialize_value(&range.start(), w)?; + w.write_str("..")?; + serialize_value(&range.end(), w)?; + Ok(()) +} + +fn serialize_value(value: &ast::Value, w: &mut impl fmt::Write) -> fmt::Result { + write!(w, "{}", value.0) +} + +pub fn serialize_samples(samples: &ast::Samples, w: &mut impl fmt::Write) -> fmt::Result { + if let Some(sample_list) = &samples.integer { + w.write_str(" @integer ")?; + serialize_sample_list(sample_list, w)?; + } else { + // Quirk of the current serializer + w.write_str(" ")?; + } + if let Some(sample_list) = &samples.decimal { + w.write_str(" @decimal ")?; + serialize_sample_list(sample_list, w)?; + } + Ok(()) +} + +pub fn serialize_sample_list(samples: &ast::SampleList, w: &mut impl fmt::Write) -> fmt::Result { + let mut first = true; + + for sample_range in samples.sample_ranges.iter() { + if first { + first = false; + } else { + w.write_str(", ")?; + } + serialize_sample_range(sample_range, w)?; + } + + if samples.ellipsis { + w.write_str(", …")?; + } + Ok(()) +} + +pub fn serialize_sample_range( + sample_range: &ast::SampleRange, + w: &mut impl fmt::Write, +) -> fmt::Result { + serialize_decimal_value(&sample_range.lower_val, w)?; + if let Some(upper_val) = &sample_range.upper_val { + w.write_char('~')?; + serialize_decimal_value(upper_val, w)?; + } + Ok(()) +} + +pub fn serialize_decimal_value(val: &ast::DecimalValue, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str(&val.0) +} diff --git a/components/pluralrules/tests/rules.rs b/components/pluralrules/tests/rules.rs index 1791d439b29..65c3313522e 100644 --- a/components/pluralrules/tests/rules.rs +++ b/components/pluralrules/tests/rules.rs @@ -1,7 +1,7 @@ mod fixtures; mod helpers; -use icu_pluralrules::rules::{parse, test_condition, Lexer}; +use icu_pluralrules::rules::{parse, parse_condition, test_condition, Lexer}; use icu_pluralrules::PluralOperands; #[test] @@ -15,7 +15,7 @@ fn test_parsing_operands() { fixtures::RuleTestOutput::Value(val) => { let lex = Lexer::new(test.rule.as_bytes()); lex.count(); - let ast = parse(test.rule.as_bytes()).expect("Failed to parse."); + let ast = parse_condition(test.rule.as_bytes()).expect("Failed to parse."); let operands: PluralOperands = test.input.into(); if val { @@ -34,8 +34,9 @@ fn test_parsing_operands() { #[cfg(feature = "io-json")] #[test] -fn test_parsing_all() { +fn test_round_trip() { use icu_pluralrules::data::cldr_resource::Resource; + use icu_pluralrules::rules::serialize; use icu_pluralrules::PluralCategory; let path = "./data/plurals.json"; @@ -47,7 +48,10 @@ fn test_parsing_all() { if let Some(rule) = rules.get(category) { let lexer = Lexer::new(rule.as_bytes()); let _ = lexer.collect::>(); - let _ = parse(rule.as_bytes()); + let ast = parse(rule.as_bytes()).expect("Parsing failed."); + let mut output = String::new(); + serialize(&ast, &mut output).unwrap(); + assert_eq!(rule, output); } } } @@ -61,7 +65,10 @@ fn test_parsing_all() { if let Some(rule) = rules.get(category) { let lexer = Lexer::new(rule.as_bytes()); let _ = lexer.collect::>(); - let _ = parse(rule.as_bytes()); + let ast = parse(rule.as_bytes()).expect("Parsing failed."); + let mut output = String::new(); + serialize(&ast, &mut output).unwrap(); + assert_eq!(rule, output); } } }