| // This file is part of ICU4X. For terms of use, please see the file |
| // called LICENSE at the top level of the ICU4X source tree |
| // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
| |
| use crate::provider::{AndListV1Marker, ErasedListV1Marker, OrListV1Marker, UnitListV1Marker}; |
| use crate::ListError; |
| use crate::ListLength; |
| use core::fmt::{self, Write}; |
| use icu_provider::prelude::*; |
| use writeable::*; |
| |
| #[cfg(doc)] |
| extern crate writeable; |
| |
| /// A formatter that renders sequences of items in an i18n-friendly way. See the |
| /// [crate-level documentation](crate) for more details. |
| #[derive(Debug)] |
| pub struct ListFormatter { |
| data: DataPayload<ErasedListV1Marker>, |
| length: ListLength, |
| } |
| |
| macro_rules! constructor { |
| ($name: ident, $name_any: ident, $name_buffer: ident, $name_unstable: ident, $marker: ty, $doc: literal) => { |
| icu_provider::gen_any_buffer_data_constructors!( |
| locale: include, |
| style: ListLength, |
| error: ListError, |
| #[doc = concat!("Creates a new [`ListFormatter`] that produces a ", $doc, "-type list using compiled data.")] |
| /// |
| /// See the [CLDR spec](https://unicode.org/reports/tr35/tr35-general.html#ListPatterns) for |
| /// an explanation of the different types. |
| /// |
| /// ✨ *Enabled with the `compiled_data` Cargo feature.* |
| /// |
| /// [📚 Help choosing a constructor](icu_provider::constructors) |
| functions: [ |
| $name, |
| $name_any, |
| $name_buffer, |
| $name_unstable, |
| Self |
| ] |
| ); |
| |
| #[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::$name)] |
| pub fn $name_unstable( |
| provider: &(impl DataProvider<$marker> + ?Sized), |
| locale: &DataLocale, |
| length: ListLength, |
| ) -> Result<Self, ListError> { |
| let data = provider |
| .load(DataRequest { |
| locale, |
| metadata: Default::default(), |
| })? |
| .take_payload()?.cast(); |
| Ok(Self { data, length }) |
| } |
| }; |
| } |
| |
| impl ListFormatter { |
| constructor!( |
| try_new_and_with_length, |
| try_new_and_with_length_with_any_provider, |
| try_new_and_with_length_with_buffer_provider, |
| try_new_and_with_length_unstable, |
| AndListV1Marker, |
| "and" |
| ); |
| constructor!( |
| try_new_or_with_length, |
| try_new_or_with_length_with_any_provider, |
| try_new_or_with_length_with_buffer_provider, |
| try_new_or_with_length_unstable, |
| OrListV1Marker, |
| "or" |
| ); |
| constructor!( |
| try_new_unit_with_length, |
| try_new_unit_with_length_with_any_provider, |
| try_new_unit_with_length_with_buffer_provider, |
| try_new_unit_with_length_unstable, |
| UnitListV1Marker, |
| "unit" |
| ); |
| |
| /// Returns a [`Writeable`] composed of the input [`Writeable`]s and the language-dependent |
| /// formatting. |
| /// |
| /// The [`Writeable`] is annotated with [`parts::ELEMENT`] for input elements, |
| /// and [`parts::LITERAL`] for list literals. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// use icu::list::*; |
| /// # use icu::locid::locale; |
| /// # use writeable::*; |
| /// let formatteur = ListFormatter::try_new_and_with_length( |
| /// &locale!("fr").into(), |
| /// ListLength::Wide, |
| /// ) |
| /// .unwrap(); |
| /// let pays = ["Italie", "France", "Espagne", "Allemagne"]; |
| /// |
| /// assert_writeable_parts_eq!( |
| /// formatteur.format(pays.iter()), |
| /// "Italie, France, Espagne et Allemagne", |
| /// [ |
| /// (0, 6, parts::ELEMENT), |
| /// (6, 8, parts::LITERAL), |
| /// (8, 14, parts::ELEMENT), |
| /// (14, 16, parts::LITERAL), |
| /// (16, 23, parts::ELEMENT), |
| /// (23, 27, parts::LITERAL), |
| /// (27, 36, parts::ELEMENT), |
| /// ] |
| /// ); |
| /// ``` |
| pub fn format<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a>( |
| &'a self, |
| values: I, |
| ) -> FormattedList<'a, W, I> { |
| FormattedList { |
| formatter: self, |
| values, |
| } |
| } |
| |
| /// Returns a [`String`] composed of the input [`Writeable`]s and the language-dependent |
| /// formatting. |
| pub fn format_to_string<W: Writeable, I: Iterator<Item = W> + Clone>( |
| &self, |
| values: I, |
| ) -> alloc::string::String { |
| self.format(values).write_to_string().into_owned() |
| } |
| } |
| |
| /// The [`Part`]s used by [`ListFormatter`]. |
| pub mod parts { |
| use writeable::Part; |
| |
| /// The [`Part`] used by [`FormattedList`](super::FormattedList) to mark the part of the string that is an element. |
| /// |
| /// * `category`: `"list"` |
| /// * `value`: `"element"` |
| pub const ELEMENT: Part = Part { |
| category: "list", |
| value: "element", |
| }; |
| |
| /// The [`Part`] used by [`FormattedList`](super::FormattedList) to mark the part of the string that is a list literal, |
| /// such as ", " or " and ". |
| /// |
| /// * `category`: `"list"` |
| /// * `value`: `"literal"` |
| pub const LITERAL: Part = Part { |
| category: "list", |
| value: "literal", |
| }; |
| } |
| |
| /// The [`Writeable`] implementation that is returned by [`ListFormatter::format`]. See |
| /// the [`writeable`] crate for how to consume this. |
| #[derive(Debug)] |
| pub struct FormattedList<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> { |
| formatter: &'a ListFormatter, |
| values: I, |
| } |
| |
| impl<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> Writeable |
| for FormattedList<'a, W, I> |
| { |
| fn write_to_parts<V: PartsWrite + ?Sized>(&self, sink: &mut V) -> fmt::Result { |
| macro_rules! literal { |
| ($lit:ident) => { |
| sink.with_part(parts::LITERAL, |l| l.write_str($lit)) |
| }; |
| } |
| macro_rules! value { |
| ($val:expr) => { |
| sink.with_part(parts::ELEMENT, |e| $val.write_to_parts(e)) |
| }; |
| } |
| |
| let mut values = self.values.clone(); |
| |
| if let Some(first) = values.next() { |
| if let Some(second) = values.next() { |
| if let Some(third) = values.next() { |
| // Start(values[0], middle(..., middle(values[n-3], End(values[n-2], values[n-1]))...)) = |
| // start_before + values[0] + start_between + (values[1..n-3] + middle_between)* + |
| // values[n-2] + end_between + values[n-1] + end_after |
| |
| let (start_before, start_between, _) = self |
| .formatter |
| .data |
| .get() |
| .start(self.formatter.length) |
| .parts(&second); |
| |
| literal!(start_before)?; |
| value!(first)?; |
| literal!(start_between)?; |
| value!(second)?; |
| |
| let mut next = third; |
| |
| for next_next in values { |
| let (_, between, _) = self |
| .formatter |
| .data |
| .get() |
| .middle(self.formatter.length) |
| .parts(&next); |
| literal!(between)?; |
| value!(next)?; |
| next = next_next; |
| } |
| |
| let (_, end_between, end_after) = self |
| .formatter |
| .data |
| .get() |
| .end(self.formatter.length) |
| .parts(&next); |
| literal!(end_between)?; |
| value!(next)?; |
| literal!(end_after) |
| } else { |
| // Pair(values[0], values[1]) = pair_before + values[0] + pair_between + values[1] + pair_after |
| let (before, between, after) = self |
| .formatter |
| .data |
| .get() |
| .pair(self.formatter.length) |
| .parts(&second); |
| literal!(before)?; |
| value!(first)?; |
| literal!(between)?; |
| value!(second)?; |
| literal!(after) |
| } |
| } else { |
| value!(first) |
| } |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn writeable_length_hint(&self) -> LengthHint { |
| let mut count = 0; |
| let item_length = self |
| .values |
| .clone() |
| .map(|w| { |
| count += 1; |
| w.writeable_length_hint() |
| }) |
| .sum::<LengthHint>(); |
| item_length |
| + self |
| .formatter |
| .data |
| .get() |
| .size_hint(self.formatter.length, count) |
| } |
| } |
| |
| impl<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> core::fmt::Display |
| for FormattedList<'a, W, I> |
| { |
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| self.write_to(f) |
| } |
| } |
| |
| #[cfg(all(test, feature = "datagen"))] |
| mod tests { |
| use super::*; |
| use writeable::{assert_writeable_eq, assert_writeable_parts_eq}; |
| |
| fn formatter(length: ListLength) -> ListFormatter { |
| ListFormatter { |
| data: DataPayload::from_owned(crate::patterns::test::test_patterns()), |
| length, |
| } |
| } |
| |
| #[test] |
| fn test_slices() { |
| let formatter = formatter(ListLength::Wide); |
| let values = ["one", "two", "three", "four", "five"]; |
| |
| assert_writeable_eq!(formatter.format(values[0..0].iter()), ""); |
| assert_writeable_eq!(formatter.format(values[0..1].iter()), "one"); |
| assert_writeable_eq!(formatter.format(values[0..2].iter()), "$one;two+"); |
| assert_writeable_eq!(formatter.format(values[0..3].iter()), "@one:two.three!"); |
| assert_writeable_eq!( |
| formatter.format(values[0..4].iter()), |
| "@one:two,three.four!" |
| ); |
| |
| assert_writeable_parts_eq!( |
| formatter.format(values.iter()), |
| "@one:two,three,four.five!", |
| [ |
| (0, 1, parts::LITERAL), |
| (1, 4, parts::ELEMENT), |
| (4, 5, parts::LITERAL), |
| (5, 8, parts::ELEMENT), |
| (8, 9, parts::LITERAL), |
| (9, 14, parts::ELEMENT), |
| (14, 15, parts::LITERAL), |
| (15, 19, parts::ELEMENT), |
| (19, 20, parts::LITERAL), |
| (20, 24, parts::ELEMENT), |
| (24, 25, parts::LITERAL) |
| ] |
| ); |
| } |
| |
| #[test] |
| fn test_into_iterator() { |
| let formatter = formatter(ListLength::Wide); |
| |
| let mut vecdeque = std::collections::vec_deque::VecDeque::<u8>::new(); |
| vecdeque.push_back(10); |
| vecdeque.push_front(48); |
| |
| assert_writeable_parts_eq!( |
| formatter.format(vecdeque.iter()), |
| "$48;10+", |
| [ |
| (0, 1, parts::LITERAL), |
| (1, 3, parts::ELEMENT), |
| (3, 4, parts::LITERAL), |
| (4, 6, parts::ELEMENT), |
| (6, 7, parts::LITERAL), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn test_iterator() { |
| let formatter = formatter(ListLength::Wide); |
| |
| assert_writeable_parts_eq!( |
| formatter.format(core::iter::repeat(5).take(2)), |
| "$5;5+", |
| [ |
| (0, 1, parts::LITERAL), |
| (1, 2, parts::ELEMENT), |
| (2, 3, parts::LITERAL), |
| (3, 4, parts::ELEMENT), |
| (4, 5, parts::LITERAL), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn test_conditional() { |
| let formatter = formatter(ListLength::Narrow); |
| |
| assert_writeable_eq!(formatter.format(["Beta", "Alpha"].iter()), "Beta :o Alpha"); |
| } |
| |
| macro_rules! test { |
| ($locale:literal, $type:ident, $(($input:expr, $output:literal),)+) => { |
| let f = ListFormatter::$type( |
| &icu::locid::locale!($locale).into(), |
| ListLength::Wide |
| ).unwrap(); |
| $( |
| assert_writeable_eq!(f.format($input.iter()), $output); |
| )+ |
| }; |
| } |
| |
| #[test] |
| fn test_basic() { |
| test!("fr", try_new_or_with_length, (["A", "B"], "A ou B"),); |
| } |
| |
| #[test] |
| fn test_spanish() { |
| test!( |
| "es", |
| try_new_and_with_length, |
| (["x", "Mallorca"], "x y Mallorca"), |
| (["x", "Ibiza"], "x e Ibiza"), |
| (["x", "Hidalgo"], "x e Hidalgo"), |
| (["x", "Hierva"], "x y Hierva"), |
| ); |
| |
| test!( |
| "es", |
| try_new_or_with_length, |
| (["x", "Ibiza"], "x o Ibiza"), |
| (["x", "Okinawa"], "x u Okinawa"), |
| (["x", "8 más"], "x u 8 más"), |
| (["x", "8"], "x u 8"), |
| (["x", "87 más"], "x u 87 más"), |
| (["x", "87"], "x u 87"), |
| (["x", "11 más"], "x u 11 más"), |
| (["x", "11"], "x u 11"), |
| (["x", "110 más"], "x o 110 más"), |
| (["x", "110"], "x o 110"), |
| (["x", "11.000 más"], "x u 11.000 más"), |
| (["x", "11.000"], "x u 11.000"), |
| (["x", "11.000,92 más"], "x u 11.000,92 más"), |
| (["x", "11.000,92"], "x u 11.000,92"), |
| ); |
| |
| test!( |
| "es-AR", |
| try_new_and_with_length, |
| (["x", "Ibiza"], "x e Ibiza"), |
| ); |
| } |
| |
| #[test] |
| fn test_hebrew() { |
| test!( |
| "he", |
| try_new_and_with_length, |
| (["x", "יפו"], "x ויפו"), |
| (["x", "Ibiza"], "x ו‑Ibiza"), |
| ); |
| } |
| } |