diff --git a/README.md b/README.md index cfdd5b6..65e4fd7 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ - [x] Parsing MO files (10.3) - [x] Parsing metadata (6.2) - [x] Supporting encodings other than UTF-8 -- [ ] Parsing the plural expression? (11.2.6) +- [x] Parsing the plural expression (11.2.6) - [ ] Correct pathfinding? (11.2.3) diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index f2ddf93..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -wrap_comments = false diff --git a/src/lib.rs b/src/lib.rs index 30be7de..26bd9da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,11 +37,15 @@ //! ``` // https://pascalhertleif.de/artikel/good-practices-for-writing-rust-libraries/ -#![deny(missing_docs, missing_debug_implementations, - trivial_casts, trivial_numeric_casts, unused_import_braces)] - -#![cfg_attr(feature="clippy", feature(plugin))] -#![cfg_attr(feature="clippy", plugin(clippy))] +#![deny( + missing_docs, + missing_debug_implementations, + trivial_casts, + trivial_numeric_casts, + unused_import_braces +)] +#![cfg_attr(feature = "clippy", feature(plugin))] +#![cfg_attr(feature = "clippy", plugin(clippy))] mod metadata; mod parser; @@ -51,8 +55,8 @@ use std::collections::HashMap; use std::io::Read; use std::ops::Deref; -pub use parser::{Error, ParseOptions}; -use plurals::Resolver::{self, Function}; +pub use parser::{default_resolver, Error, ParseOptions}; +use plurals::*; fn key_with_context(context: &str, key: &str) -> String { let mut result = context.to_owned(); @@ -63,7 +67,7 @@ fn key_with_context(context: &str, key: &str) -> String { /// Catalog represents a set of translation strings /// parsed out of one MO file. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Catalog { strings: HashMap, resolver: Resolver, @@ -74,13 +78,7 @@ impl Catalog { fn new() -> Self { Catalog { strings: HashMap::new(), - resolver: Function(Box::new(|n| { - if n != 1 { - 1 - } else { - 0 - } - })), + resolver: Resolver::Function(default_resolver), } } @@ -100,7 +98,6 @@ impl Catalog { /// let file = File::open("french.mo").unwrap(); /// let catalog = Catalog::parse(file).unwrap(); /// ``` - pub fn parse(reader: R) -> Result { ParseOptions::new().parse(reader) } @@ -116,7 +113,10 @@ impl Catalog { /// Returns the singular translation of `msg_id` from the given catalog /// or `msg_id` itself if a translation does not exist. pub fn gettext<'a>(&'a self, msg_id: &'a str) -> &'a str { - self.strings.get(msg_id).and_then(|msg| msg.get_translated(0)).unwrap_or(msg_id) + self.strings + .get(msg_id) + .and_then(|msg| msg.get_translated(0)) + .unwrap_or(msg_id) } /// Returns the plural translation of `msg_id` from the given catalog @@ -129,9 +129,9 @@ impl Catalog { let form_no = self.resolver.resolve(n); match self.strings.get(msg_id) { - Some(msg) => { - msg.get_translated(form_no).unwrap_or_else(|| [msg_id, msg_id_plural][form_no]) - } + Some(msg) => msg + .get_translated(form_no) + .unwrap_or_else(|| [msg_id, msg_id_plural][form_no]), None if n == 1 => msg_id, None if n != 1 => msg_id_plural, _ => unreachable!(), @@ -144,7 +144,10 @@ impl Catalog { // TODO: DRY gettext/pgettext pub fn pgettext<'a>(&'a self, msg_context: &'a str, msg_id: &'a str) -> &'a str { let key = key_with_context(msg_context, &msg_id); - self.strings.get(&key).and_then(|msg| msg.get_translated(0)).unwrap_or(msg_id) + self.strings + .get(&key) + .and_then(|msg| msg.get_translated(0)) + .unwrap_or(msg_id) } /// Returns the plural translation of `msg_id` @@ -155,18 +158,19 @@ impl Catalog { /// /// Currently, the only supported plural formula is `n != 1`. // TODO: DRY ngettext/npgettext - pub fn npgettext<'a>(&'a self, - msg_context: &'a str, - msg_id: &'a str, - msg_id_plural: &'a str, - n: u64) - -> &'a str { + pub fn npgettext<'a>( + &'a self, + msg_context: &'a str, + msg_id: &'a str, + msg_id_plural: &'a str, + n: u64, + ) -> &'a str { let key = key_with_context(msg_context, &msg_id); let form_no = self.resolver.resolve(n); match self.strings.get(&key) { - Some(msg) => { - msg.get_translated(form_no).unwrap_or_else(|| [msg_id, msg_id_plural][form_no]) - } + Some(msg) => msg + .get_translated(form_no) + .unwrap_or_else(|| [msg_id, msg_id_plural][form_no]), None if n == 1 => msg_id, None if n != 1 => msg_id_plural, _ => unreachable!(), @@ -174,7 +178,7 @@ impl Catalog { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] struct Message { id: String, context: Option, @@ -245,45 +249,40 @@ fn catalog_pgettext() { #[test] fn catalog_npgettext() { let mut cat = Catalog::new(); - cat.insert(Message::new("Text", Some("unit test"), vec!["Tekstas", "Tekstai"])); + cat.insert(Message::new( + "Text", + Some("unit test"), + vec!["Tekstas", "Tekstai"], + )); assert_eq!(cat.npgettext("unit test", "Text", "Texts", 1), "Tekstas"); assert_eq!(cat.npgettext("unit test", "Text", "Texts", 0), "Tekstai"); assert_eq!(cat.npgettext("unit test", "Text", "Texts", 2), "Tekstai"); - assert_eq!(cat.npgettext("integration test", "Text", "Texts", 1), - "Text"); - assert_eq!(cat.npgettext("integration test", "Text", "Texts", 0), - "Texts"); - assert_eq!(cat.npgettext("integration test", "Text", "Texts", 2), - "Texts"); + assert_eq!( + cat.npgettext("integration test", "Text", "Texts", 1), + "Text" + ); + assert_eq!( + cat.npgettext("integration test", "Text", "Texts", 0), + "Texts" + ); + assert_eq!( + cat.npgettext("integration test", "Text", "Texts", 2), + "Texts" + ); } -#[cfg(test)] -fn lithuanian_plural(n: u64) -> usize { - if (n % 10) == 1 && (n % 100) != 11 { - 0 - } else if ((n % 10) >= 2) && ((n % 100) < 10 || (n % 100) >= 20) { - 1 - } else { - 2 - } -} #[test] -fn catalog_ngettext_resolver() { - let mut cat = Catalog::new(); - cat.insert(Message::new("Garlic", None, vec!["Česnakas", "Česnakai", "Česnakų"])); - // https://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html - cat.resolver = Resolver::Function(Box::new(lithuanian_plural)); +fn test_complex_plural() { + let reader: &[u8] = include_bytes!("../test_cases/complex_plural.mo"); + let cat = parser::parse_catalog(reader, ParseOptions::new()).unwrap(); - assert_eq!(cat.ngettext("Garlic", "Garlics", 0), "Česnakų"); - assert_eq!(cat.ngettext("Garlic", "Garlics", 1), "Česnakas"); - for i in 2..9 { - assert_eq!(cat.ngettext("Garlic", "Garlics", i), "Česnakai"); - } - for i in 10..20 { - assert_eq!(cat.ngettext("Garlic", "Garlics", i), "Česnakų"); + assert_eq!(cat.ngettext("Test", "Tests", 0), "Plural 2"); + assert_eq!(cat.ngettext("Test", "Tests", 1), "Singular"); + assert_eq!(cat.ngettext("Test", "Tests", 2), "Plural 1"); + for i in 3..20 { + assert_eq!(cat.ngettext("Test", "Tests", i), "Plural 2"); } - assert_eq!(cat.ngettext("Garlic", "Garlics", 21), "Česnakas"); } diff --git a/src/metadata.rs b/src/metadata.rs index 194e7b0..6506b42 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -10,7 +10,32 @@ pub struct MetadataMap<'a>(HashMap<&'a str, &'a str>); impl<'a> MetadataMap<'a> { /// Returns a string that indicates the character set. pub fn charset(&self) -> Option<&'a str> { - self.get("Content-Type").and_then(|x| x.split("charset=").skip(1).next()) + self.get("Content-Type") + .and_then(|x| x.split("charset=").skip(1).next()) + } + + /// Returns the number of different plurals and the boolean + /// expression to determine the form to use depending on + /// the number of elements. + /// + /// Defaults to `n_plurals = 2` and `plural = n!=1` (as in English). + pub fn plural_forms(&self) -> (Option, Option<&'a str>) { + self.get("Plural-Forms") + .map(|f| f.split(';').fold((None, None), |(n_pl, pl), prop| { + match prop.chars().position(|c| c == '=') { + Some(index) => { + let (name, value) = prop.split_at(index); + let value = value[1..value.len()].trim(); + match name.trim() { + "n_plurals" => (usize::from_str_radix(value, 10).ok(), pl), + "plural" => (n_pl, Some(value)), + _ => (n_pl, pl) + } + }, + None => (n_pl, pl) + } + })) + .unwrap_or((None, None)) } } @@ -52,3 +77,26 @@ fn test_metadatamap_charset() { assert_eq!(map.charset().unwrap(), "utf-42"); } } + +#[test] +fn test_metadatamap_plural() { + { + let mut map = MetadataMap(HashMap::new()); + assert_eq!(map.plural_forms(), (None, None)); + + map.insert("Plural-Forms", ""); + assert_eq!(map.plural_forms(), (None, None)); + // n_plural + map.insert("Plural-Forms", "n_plurals=42"); + assert_eq!(map.plural_forms(), (Some(42), None)); + // plural is specified + map.insert("Plural-Forms", "n_plurals=2; plural=n==12"); + assert_eq!(map.plural_forms(), (Some(2), Some("n==12"))); + // plural before n_plurals + map.insert("Plural-Forms", "plural=n==12; n_plurals=2"); + assert_eq!(map.plural_forms(), (Some(2), Some("n==12"))); + // with spaces + map.insert("Plural-Forms", " n_plurals = 42 ; plural = n > 10 "); + assert_eq!(map.plural_forms(), (Some(42), Some("n > 10"))); + } +} diff --git a/src/parser.rs b/src/parser.rs index 6a2d7c9..528823a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,13 +7,13 @@ use std::error; use std::fmt; use std::io; -use self::byteorder::{ByteOrder, BigEndian, LittleEndian}; +use self::byteorder::{BigEndian, ByteOrder, LittleEndian}; use self::encoding::label::encoding_from_whatwg_label; -use self::encoding::types::EncodingRef; use self::encoding::types::DecoderTrap::Strict; +use self::encoding::types::EncodingRef; +use super::plurals::{Ast, Resolver}; use super::{Catalog, Message}; -use super::plurals::Resolver; use metadata::parse_metadata; #[allow(non_upper_case_globals)] @@ -36,11 +36,10 @@ pub enum Error { MisplacedMetadata, /// An unknown encoding was specified in the metadata UnknownEncoding, + /// Invalid Plural-Forms metadata + PluralParsing, } -// Can not use use `Error::*` as per this issue: -// (https://github.com/rust-lang/rust/issues/4865) -use Error::{BadMagic, DecodingError, Eof, Io, MalformedMetadata, MisplacedMetadata, - UnknownEncoding}; +use Error::*; impl error::Error for Error { fn description(&self) -> &str { @@ -52,6 +51,7 @@ impl error::Error for Error { MalformedMetadata => "metadata syntax error", MisplacedMetadata => "misplaced metadata", UnknownEncoding => "unknown encoding specified", + PluralParsing => "invalid plural expression", } } } @@ -91,7 +91,7 @@ impl From> for Error { #[derive(Default)] pub struct ParseOptions { force_encoding: Option, - force_plural: Option usize + 'static>>, + force_plural: Option usize>, } impl ParseOptions { @@ -120,8 +120,8 @@ impl ParseOptions { /// If this option is not enabled, /// the parser uses the default formula /// (`n != 1`). - pub fn force_plural usize + 'static>(mut self, plural: T) -> Self { - self.force_plural = Some(Box::new(plural)); + pub fn force_plural(mut self, plural: fn(u64) -> usize) -> Self { + self.force_plural = Some(plural); self } } @@ -138,7 +138,10 @@ fn get_read_u32_fn(magic: &[u8]) -> Option u32> { } } -pub fn parse_catalog(mut file: R, opts: ParseOptions) -> Result { +pub fn parse_catalog<'a, R: io::Read>( + mut file: R, + opts: ParseOptions, +) -> Result { let mut contents = vec![]; let n = try!(file.read_to_end(&mut contents)); if n < 28 { @@ -159,69 +162,72 @@ pub fn parse_catalog(mut file: R, opts: ParseOptions) -> Result Some(Resolver::Function(func)), - None => None, - }; + if let Some(f) = opts.force_plural { + catalog.resolver = Resolver::Function(f); + } let mut encoding = opts.force_encoding.unwrap_or(utf8_encoding); for i in 0..num_strings { - let id; - let context; - let translated: Vec; // Parse the original string - { - if n < off_otable + 8 { - return Err(Eof); - } - let len = read_u32(&contents[off_otable..off_otable + 4]) as usize; - let off = read_u32(&contents[off_otable + 4..off_otable + 8]) as usize; - // +1 compensates for the ending NUL byte which is not included in length - if n < off + len + 1 { - return Err(Eof); - } - let mut original = &contents[off..off + len + 1]; - // check for context - context = match original.iter().position(|x| *x == 4) { - Some(idx) => { - let ctx = &original[..idx]; - original = &original[idx + 1..]; - Some(try!(encoding.decode(ctx, Strict))) - } - None => None, - }; - // extract msg_id singular, ignoring the plural - id = match original.iter().position(|x| *x == 0).map(|i| &original[..i]) { - Some(b) => try!(encoding.decode(b, Strict)), - None => return Err(Eof), - }; - if id == "" && i != 0 { - return Err(MisplacedMetadata); + if n < off_otable + 8 { + return Err(Eof); + } + let len = read_u32(&contents[off_otable..off_otable + 4]) as usize; + let off = read_u32(&contents[off_otable + 4..off_otable + 8]) as usize; + // +1 compensates for the ending NUL byte which is not included in length + if n < off + len + 1 { + return Err(Eof); + } + let mut original = &contents[off..off + len + 1]; + // check for context + let context = match original.iter().position(|x| *x == 4) { + Some(idx) => { + let ctx = &original[..idx]; + original = &original[idx + 1..]; + Some(try!(encoding.decode(ctx, Strict))) } + None => None, + }; + // extract msg_id singular, ignoring the plural + let id = match original + .iter() + .position(|x| *x == 0) + .map(|i| &original[..i]) + { + Some(b) => try!(encoding.decode(b, Strict)), + None => return Err(Eof), + }; + if id == "" && i != 0 { + return Err(MisplacedMetadata); } // Parse the translation strings - { - if n < off_ttable + 8 { - return Err(Eof); - } - let len = read_u32(&contents[off_ttable..off_ttable + 4]) as usize; - let off = read_u32(&contents[off_ttable + 4..off_ttable + 8]) as usize; - // +1 compensates for the ending NUL byte which is not included in length - if n < off + len + 1 { - return Err(Eof); + if n < off_ttable + 8 { + return Err(Eof); + } + let len = read_u32(&contents[off_ttable..off_ttable + 4]) as usize; + let off = read_u32(&contents[off_ttable + 4..off_ttable + 8]) as usize; + // +1 compensates for the ending NUL byte which is not included in length + if n < off + len + 1 { + return Err(Eof); + } + let translated = try!( + (&contents[off..off + len]) + .split(|x| *x == 0) + .map(|b| encoding.decode(b, Strict)) + .collect::, _>>() + ); + if id == "" { + let map = parse_metadata(&*translated[0]).unwrap(); + if let (Some(c), None) = (map.charset(), opts.force_encoding) { + encoding = match encoding_from_whatwg_label(c) { + Some(enc_ref) => enc_ref, + None => return Err(UnknownEncoding), + } } - translated = try!((&contents[off..off + len]) - .split(|x| *x == 0) - .map(|b| encoding.decode(b, Strict)) - .collect::, _>>()); - if id == "" { - let map = parse_metadata(&*translated[0]).unwrap(); - if let (Some(c), None) = (map.charset(), opts.force_encoding) { - encoding = match encoding_from_whatwg_label(c) { - Some(enc_ref) => enc_ref, - None => return Err(UnknownEncoding), - } + if opts.force_plural.is_none() { + if let Some(p) = map.plural_forms().1 { + catalog.resolver = Ast::parse(p).map(Resolver::Expr)?; } } } @@ -232,13 +238,24 @@ pub fn parse_catalog(mut file: R, opts: ParseOptions) -> Result usize { + if n == 1 { + 0 + } else { + 1 + } +} + #[test] fn test_get_read_u32_fn() { use std::mem; @@ -310,16 +327,20 @@ fn test_parse_catalog() { let reader: &[u8] = include_bytes!("../test_cases/1.mo"); let catalog = parse_catalog(reader, ParseOptions::new()).unwrap(); assert_eq!(catalog.strings.len(), 1); - assert_eq!(catalog.strings["this is context\x04Text"], - Message::new("Text", Some("this is context"), vec!["Tekstas", "Tekstai"])); + assert_eq!( + catalog.strings["this is context\x04Text"], + Message::new("Text", Some("this is context"), vec!["Tekstas", "Tekstai"]) + ); } { let reader: &[u8] = include_bytes!("../test_cases/2.mo"); let catalog = parse_catalog(reader, ParseOptions::new()).unwrap(); assert_eq!(catalog.strings.len(), 2); - assert_eq!(catalog.strings["Image"], - Message::new("Image", None, vec!["Nuotrauka", "Nuotraukos"])); + assert_eq!( + catalog.strings["Image"], + Message::new("Image", None, vec!["Nuotrauka", "Nuotraukos"]) + ); } { diff --git a/src/plurals.rs b/src/plurals.rs index d90b3cf..33e7417 100644 --- a/src/plurals.rs +++ b/src/plurals.rs @@ -1,10 +1,223 @@ -use std::fmt; +use parser::Error; -use self::Resolver::Function; +use self::Resolver::*; +#[derive(Clone, Debug)] pub enum Resolver { - /// A function/closure manually supplied by the user. - Function(Box usize>), + /// A boolean expression + /// Use Ast::parse to get an Ast + Expr(Ast), + /// A function + Function(fn(u64) -> usize) +} + +/// Finds the index of a pattern, outside of parenthesis +fn index_of<'a>(src: &'a str, pat: &'static str) -> Option { + src.chars().fold((None, 0, 0, 0), |(match_index, i, n_matches, paren_level), ch| { + if let Some(x) = match_index { + return (Some(x), i, n_matches, paren_level); + } else { + let new_par_lvl = match ch { + '(' => paren_level + 1, + ')' => paren_level - 1, + _ => paren_level + }; + + if Some(ch) == pat.chars().nth(n_matches) { + let length = n_matches + 1; + if length == pat.len() && new_par_lvl == 0 { + (Some(i - n_matches), i + 1, length, new_par_lvl) + } else { + (match_index, i + 1, length, new_par_lvl) + } + } else { + (match_index, i + 1, 0, new_par_lvl) + } + } + }).0 +} + +use self::Ast::*; +#[derive(Clone, Debug, PartialEq)] +pub enum Ast { + /// A ternary expression + /// x ? a : b + /// + /// the three Ast<'a> are respectively x, a and b. + Ternary(Box, Box, Box), + /// The n variable. + N, + /// Integer literals. + Integer(u64), + /// Binary operators. + Op(Operator, Box, Box), + /// ! operator. + Not(Box), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Operator { + Equal, + NotEqual, + GreaterOrEqual, + SmallerOrEqual, + Greater, + Smaller, + And, + Or, + Modulo, +} + +impl Ast { + fn resolve(&self, n: u64) -> usize { + match *self { + Ternary(ref cond, ref ok, ref nok) => if cond.resolve(n) == 0 { + nok.resolve(n) + } else { + ok.resolve(n) + }, + N => n as usize, + Integer(x) => x as usize, + Op(ref op, ref lhs, ref rhs) => match *op { + Operator::Equal => (lhs.resolve(n) == rhs.resolve(n)) as usize, + Operator::NotEqual => (lhs.resolve(n) != rhs.resolve(n)) as usize, + Operator::GreaterOrEqual => (lhs.resolve(n) >= rhs.resolve(n)) as usize, + Operator::SmallerOrEqual => (lhs.resolve(n) <= rhs.resolve(n)) as usize, + Operator::Greater => (lhs.resolve(n) > rhs.resolve(n)) as usize, + Operator::Smaller => (lhs.resolve(n) < rhs.resolve(n)) as usize, + Operator::And => (lhs.resolve(n) != 0 && rhs.resolve(n) != 0) as usize, + Operator::Or => (lhs.resolve(n) != 0 || rhs.resolve(n) != 0) as usize, + Operator::Modulo => lhs.resolve(n) % rhs.resolve(n), + }, + Not(ref val) => match val.resolve(n) { + 0 => 1, + _ => 0, + } + } + } + + pub fn parse<'a>(src: &'a str) -> Result { + Self::parse_parens(src.trim()) + } + + fn parse_parens<'a>(src: &'a str) -> Result { + if src.starts_with('(') && src.ends_with(')') { + Ast::parse(src[1..src.len() - 1].trim()) + } else { + Ast::parse_and(src.trim()) + } + } + + fn parse_and<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "&&") { + Ok(Ast::Op(Operator::And, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_or(src) + } + } + + fn parse_or<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "||") { + Ok(Ast::Op(Operator::Or, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_ternary(src) + } + } + + fn parse_ternary<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "?") { + if let Some(l) = index_of(src, ":") { + Ok(Ast::Ternary( + Box::new(Ast::parse(&src[0..i])?), + Box::new(Ast::parse(&src[i + 1..l])?), + Box::new(Ast::parse(&src[l + 1..])?), + )) + } else { + Err(Error::PluralParsing) + } + } else { + Self::parse_ge(src) + } + } + + fn parse_ge<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, ">=") { + Ok(Ast::Op(Operator::GreaterOrEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_gt(src) + } + } + + fn parse_gt<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, ">") { + Ok(Ast::Op(Operator::Greater, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?))) + } else { + Self::parse_le(src) + } + } + + fn parse_le<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "<=") { + Ok(Ast::Op(Operator::SmallerOrEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_lt(src) + } + } + + fn parse_lt<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "<") { + Ok(Ast::Op(Operator::Smaller, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?))) + } else { + Self::parse_eq(src) + } + } + + fn parse_eq<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "==") { + Ok(Ast::Op(Operator::Equal, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_neq(src) + } + } + + fn parse_neq<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "!=") { + Ok(Ast::Op(Operator::NotEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?))) + } else { + Self::parse_mod(src) + } + } + fn parse_mod<'a>(src: &'a str) -> Result { + if let Some(i) = index_of(src, "%") { + Ok(Ast::Op(Operator::Modulo, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?))) + } else { + Self::parse_not(src.trim()) + } + } + + fn parse_not<'a>(src: &'a str) -> Result { + if index_of(src, "!") == Some(0) { + Ok(Ast::Not(Box::new(Ast::parse(&src[1..])?))) + } else { + Self::parse_int(src.trim()) + } + } + + fn parse_int<'a>(src: &'a str) -> Result { + if let Ok(x) = u64::from_str_radix(src, 10) { + Ok(Ast::Integer(x)) + } else { + Self::parse_n(src.trim()) + } + } + + fn parse_n<'a>(src: &'a str) -> Result { + if src == "n" { + Ok(Ast::N) + } else { + Err(Error::PluralParsing) + } + } } impl Resolver { @@ -12,15 +225,59 @@ impl Resolver { /// for `n` objects, as defined by the rule contained in this resolver. pub fn resolve(&self, n: u64) -> usize { match *self { - Function(ref func) => func(n), + Expr(ref ast) => ast.resolve(n), + Function(ref f) => f(n) } } } -impl fmt::Debug for Resolver { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match *self { - Function(..) => fmt.write_str("Function(..)"), - } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_expr_resolver() { + assert_eq!(Expr(N).resolve(42), 42); + } + + #[test] + fn test_parser() { + assert_eq!(Ast::parse("n == 42 ? n : 6 && n < 7").expect("Invalid plural"), Ast::Op( + Operator::And, + Box::new(Ast::Ternary( + Box::new(Ast::Op( + Operator::Equal, + Box::new(Ast::N), + Box::new(Ast::Integer(42)) + )), + Box::new(Ast::N), + Box::new(Ast::Integer(6)) + )), + Box::new(Ast::Op( + Operator::Smaller, + Box::new(Ast::N), + Box::new(Ast::Integer(7)) + )) + )); + + assert_eq!(Ast::parse("(n)").expect("Invalid plural"), Ast::N); + + assert_eq!(Ast::parse("(n == 1 || n == 2) ? 0 : 1").expect("Invalid plural"), Ast::Ternary( + Box::new(Ast::Op( + Operator::Or, + Box::new(Ast::Op( + Operator::Equal, + Box::new(Ast::N), + Box::new(Ast::Integer(1)) + )), + Box::new(Ast::Op( + Operator::Equal, + Box::new(Ast::N), + Box::new(Ast::Integer(2)) + )) + )), + Box::new(Ast::Integer(0)), + Box::new(Ast::Integer(1)) + )) } } diff --git a/test_cases/complex_plural.mo b/test_cases/complex_plural.mo new file mode 100644 index 0000000..9f5d64f Binary files /dev/null and b/test_cases/complex_plural.mo differ diff --git a/test_cases/complex_plural.po b/test_cases/complex_plural.po new file mode 100644 index 0000000..7ec6d43 --- /dev/null +++ b/test_cases/complex_plural.po @@ -0,0 +1,12 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural = n == 1 ? 0 : n == 2 ? 1 : 2;\n" + +msgid "Test" +msgid_plural "Tests" +msgstr[0] "Singular" +msgstr[1] "Plural 1" +msgstr[2] "Plural 2" diff --git a/test_cases/integration.mo b/test_cases/integration.mo index b02bb78..105021d 100644 Binary files a/test_cases/integration.mo and b/test_cases/integration.mo differ diff --git a/test_cases/integration.po b/test_cases/integration.po index 5374a05..81165dc 100644 --- a/test_cases/integration.po +++ b/test_cases/integration.po @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +"Plural-Forms: nplurals=2; plural=n!=1;\n" #: ../tests/lib.rs:12 msgid "non-existent"