1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::provider::{AndListV1Marker, ErasedListV1Marker, OrListV1Marker, UnitListV1Marker};
6use crate::ListError;
7use crate::ListLength;
8use core::fmt::{self, Write};
9use icu_provider::prelude::*;
10use writeable::*;
11
12#[cfg(doc)]
13extern crate writeable;
14
15/// A formatter that renders sequences of items in an i18n-friendly way. See the
16/// [crate-level documentation](crate) for more details.
17#[derive(Debug)]
18pub struct ListFormatter {
19 data: DataPayload<ErasedListV1Marker>,
20 length: ListLength,
21}
22
23macro_rules! constructor {
24 ($name: ident, $name_any: ident, $name_buffer: ident, $name_unstable: ident, $marker: ty, $doc: literal) => {
25 icu_provider::gen_any_buffer_data_constructors!(
26 locale: include,
27 style: ListLength,
28 error: ListError,
29 #[doc = concat!("Creates a new [`ListFormatter`] that produces a ", $doc, "-type list using compiled data.")]
30 ///
31 /// See the [CLDR spec](https://unicode.org/reports/tr35/tr35-general.html#ListPatterns) for
32 /// an explanation of the different types.
33 ///
34 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
35 ///
36 /// [📚 Help choosing a constructor](icu_provider::constructors)
37 functions: [
38 $name,
39 $name_any,
40 $name_buffer,
41 $name_unstable,
42 Self
43 ]
44 );
45
46 #[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::$name)]
47 pub fn $name_unstable(
48 provider: &(impl DataProvider<$marker> + ?Sized),
49 locale: &DataLocale,
50 length: ListLength,
51 ) -> Result<Self, ListError> {
52 let data = provider
53 .load(DataRequest {
54 locale,
55 metadata: Default::default(),
56 })?
57 .take_payload()?.cast();
58 Ok(Self { data, length })
59 }
60 };
61}
62
63impl ListFormatter {
64 constructor!(
65 try_new_and_with_length,
66 try_new_and_with_length_with_any_provider,
67 try_new_and_with_length_with_buffer_provider,
68 try_new_and_with_length_unstable,
69 AndListV1Marker,
70 "and"
71 );
72 constructor!(
73 try_new_or_with_length,
74 try_new_or_with_length_with_any_provider,
75 try_new_or_with_length_with_buffer_provider,
76 try_new_or_with_length_unstable,
77 OrListV1Marker,
78 "or"
79 );
80 constructor!(
81 try_new_unit_with_length,
82 try_new_unit_with_length_with_any_provider,
83 try_new_unit_with_length_with_buffer_provider,
84 try_new_unit_with_length_unstable,
85 UnitListV1Marker,
86 "unit"
87 );
88
89 /// Returns a [`Writeable`] composed of the input [`Writeable`]s and the language-dependent
90 /// formatting.
91 ///
92 /// The [`Writeable`] is annotated with [`parts::ELEMENT`] for input elements,
93 /// and [`parts::LITERAL`] for list literals.
94 ///
95 /// # Example
96 ///
97 /// ```
98 /// use icu::list::*;
99 /// # use icu::locid::locale;
100 /// # use writeable::*;
101 /// let formatteur = ListFormatter::try_new_and_with_length(
102 /// &locale!("fr").into(),
103 /// ListLength::Wide,
104 /// )
105 /// .unwrap();
106 /// let pays = ["Italie", "France", "Espagne", "Allemagne"];
107 ///
108 /// assert_writeable_parts_eq!(
109 /// formatteur.format(pays.iter()),
110 /// "Italie, France, Espagne et Allemagne",
111 /// [
112 /// (0, 6, parts::ELEMENT),
113 /// (6, 8, parts::LITERAL),
114 /// (8, 14, parts::ELEMENT),
115 /// (14, 16, parts::LITERAL),
116 /// (16, 23, parts::ELEMENT),
117 /// (23, 27, parts::LITERAL),
118 /// (27, 36, parts::ELEMENT),
119 /// ]
120 /// );
121 /// ```
122 pub fn format<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a>(
123 &'a self,
124 values: I,
125 ) -> FormattedList<'a, W, I> {
126 FormattedList {
127 formatter: self,
128 values,
129 }
130 }
131
132 /// Returns a [`String`] composed of the input [`Writeable`]s and the language-dependent
133 /// formatting.
134 pub fn format_to_string<W: Writeable, I: Iterator<Item = W> + Clone>(
135 &self,
136 values: I,
137 ) -> alloc::string::String {
138 self.format(values).write_to_string().into_owned()
139 }
140}
141
142/// The [`Part`]s used by [`ListFormatter`].
143pub mod parts {
144 use writeable::Part;
145
146 /// The [`Part`] used by [`FormattedList`](super::FormattedList) to mark the part of the string that is an element.
147 ///
148 /// * `category`: `"list"`
149 /// * `value`: `"element"`
150 pub const ELEMENT: Part = Part {
151 category: "list",
152 value: "element",
153 };
154
155 /// The [`Part`] used by [`FormattedList`](super::FormattedList) to mark the part of the string that is a list literal,
156 /// such as ", " or " and ".
157 ///
158 /// * `category`: `"list"`
159 /// * `value`: `"literal"`
160 pub const LITERAL: Part = Part {
161 category: "list",
162 value: "literal",
163 };
164}
165
166/// The [`Writeable`] implementation that is returned by [`ListFormatter::format`]. See
167/// the [`writeable`] crate for how to consume this.
168#[derive(Debug)]
169pub struct FormattedList<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> {
170 formatter: &'a ListFormatter,
171 values: I,
172}
173
174impl<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> Writeable
175 for FormattedList<'a, W, I>
176{
177 fn write_to_parts<V: PartsWrite + ?Sized>(&self, sink: &mut V) -> fmt::Result {
178 macro_rules! literal {
179 ($lit:ident) => {
180 sink.with_part(parts::LITERAL, |l| l.write_str($lit))
181 };
182 }
183 macro_rules! value {
184 ($val:expr) => {
185 sink.with_part(parts::ELEMENT, |e| $val.write_to_parts(e))
186 };
187 }
188
189 let mut values = self.values.clone();
190
191 if let Some(first) = values.next() {
192 if let Some(second) = values.next() {
193 if let Some(third) = values.next() {
194 // Start(values[0], middle(..., middle(values[n-3], End(values[n-2], values[n-1]))...)) =
195 // start_before + values[0] + start_between + (values[1..n-3] + middle_between)* +
196 // values[n-2] + end_between + values[n-1] + end_after
197
198 let (start_before, start_between, _) = self
199 .formatter
200 .data
201 .get()
202 .start(self.formatter.length)
203 .parts(&second);
204
205 literal!(start_before)?;
206 value!(first)?;
207 literal!(start_between)?;
208 value!(second)?;
209
210 let mut next = third;
211
212 for next_next in values {
213 let (_, between, _) = self
214 .formatter
215 .data
216 .get()
217 .middle(self.formatter.length)
218 .parts(&next);
219 literal!(between)?;
220 value!(next)?;
221 next = next_next;
222 }
223
224 let (_, end_between, end_after) = self
225 .formatter
226 .data
227 .get()
228 .end(self.formatter.length)
229 .parts(&next);
230 literal!(end_between)?;
231 value!(next)?;
232 literal!(end_after)
233 } else {
234 // Pair(values[0], values[1]) = pair_before + values[0] + pair_between + values[1] + pair_after
235 let (before, between, after) = self
236 .formatter
237 .data
238 .get()
239 .pair(self.formatter.length)
240 .parts(&second);
241 literal!(before)?;
242 value!(first)?;
243 literal!(between)?;
244 value!(second)?;
245 literal!(after)
246 }
247 } else {
248 value!(first)
249 }
250 } else {
251 Ok(())
252 }
253 }
254
255 fn writeable_length_hint(&self) -> LengthHint {
256 let mut count = 0;
257 let item_length = self
258 .values
259 .clone()
260 .map(|w| {
261 count += 1;
262 w.writeable_length_hint()
263 })
264 .sum::<LengthHint>();
265 item_length
266 + self
267 .formatter
268 .data
269 .get()
270 .size_hint(self.formatter.length, count)
271 }
272}
273
274impl<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a> core::fmt::Display
275 for FormattedList<'a, W, I>
276{
277 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
278 self.write_to(sink:f)
279 }
280}
281
282#[cfg(all(test, feature = "datagen"))]
283mod tests {
284 use super::*;
285 use writeable::{assert_writeable_eq, assert_writeable_parts_eq};
286
287 fn formatter(length: ListLength) -> ListFormatter {
288 ListFormatter {
289 data: DataPayload::from_owned(crate::patterns::test::test_patterns()),
290 length,
291 }
292 }
293
294 #[test]
295 fn test_slices() {
296 let formatter = formatter(ListLength::Wide);
297 let values = ["one", "two", "three", "four", "five"];
298
299 assert_writeable_eq!(formatter.format(values[0..0].iter()), "");
300 assert_writeable_eq!(formatter.format(values[0..1].iter()), "one");
301 assert_writeable_eq!(formatter.format(values[0..2].iter()), "$one;two+");
302 assert_writeable_eq!(formatter.format(values[0..3].iter()), "@one:two.three!");
303 assert_writeable_eq!(
304 formatter.format(values[0..4].iter()),
305 "@one:two,three.four!"
306 );
307
308 assert_writeable_parts_eq!(
309 formatter.format(values.iter()),
310 "@one:two,three,four.five!",
311 [
312 (0, 1, parts::LITERAL),
313 (1, 4, parts::ELEMENT),
314 (4, 5, parts::LITERAL),
315 (5, 8, parts::ELEMENT),
316 (8, 9, parts::LITERAL),
317 (9, 14, parts::ELEMENT),
318 (14, 15, parts::LITERAL),
319 (15, 19, parts::ELEMENT),
320 (19, 20, parts::LITERAL),
321 (20, 24, parts::ELEMENT),
322 (24, 25, parts::LITERAL)
323 ]
324 );
325 }
326
327 #[test]
328 fn test_into_iterator() {
329 let formatter = formatter(ListLength::Wide);
330
331 let mut vecdeque = std::collections::vec_deque::VecDeque::<u8>::new();
332 vecdeque.push_back(10);
333 vecdeque.push_front(48);
334
335 assert_writeable_parts_eq!(
336 formatter.format(vecdeque.iter()),
337 "$48;10+",
338 [
339 (0, 1, parts::LITERAL),
340 (1, 3, parts::ELEMENT),
341 (3, 4, parts::LITERAL),
342 (4, 6, parts::ELEMENT),
343 (6, 7, parts::LITERAL),
344 ]
345 );
346 }
347
348 #[test]
349 fn test_iterator() {
350 let formatter = formatter(ListLength::Wide);
351
352 assert_writeable_parts_eq!(
353 formatter.format(core::iter::repeat(5).take(2)),
354 "$5;5+",
355 [
356 (0, 1, parts::LITERAL),
357 (1, 2, parts::ELEMENT),
358 (2, 3, parts::LITERAL),
359 (3, 4, parts::ELEMENT),
360 (4, 5, parts::LITERAL),
361 ]
362 );
363 }
364
365 #[test]
366 fn test_conditional() {
367 let formatter = formatter(ListLength::Narrow);
368
369 assert_writeable_eq!(formatter.format(["Beta", "Alpha"].iter()), "Beta :o Alpha");
370 }
371
372 macro_rules! test {
373 ($locale:literal, $type:ident, $(($input:expr, $output:literal),)+) => {
374 let f = ListFormatter::$type(
375 &icu::locid::locale!($locale).into(),
376 ListLength::Wide
377 ).unwrap();
378 $(
379 assert_writeable_eq!(f.format($input.iter()), $output);
380 )+
381 };
382 }
383
384 #[test]
385 fn test_basic() {
386 test!("fr", try_new_or_with_length, (["A", "B"], "A ou B"),);
387 }
388
389 #[test]
390 fn test_spanish() {
391 test!(
392 "es",
393 try_new_and_with_length,
394 (["x", "Mallorca"], "x y Mallorca"),
395 (["x", "Ibiza"], "x e Ibiza"),
396 (["x", "Hidalgo"], "x e Hidalgo"),
397 (["x", "Hierva"], "x y Hierva"),
398 );
399
400 test!(
401 "es",
402 try_new_or_with_length,
403 (["x", "Ibiza"], "x o Ibiza"),
404 (["x", "Okinawa"], "x u Okinawa"),
405 (["x", "8 más"], "x u 8 más"),
406 (["x", "8"], "x u 8"),
407 (["x", "87 más"], "x u 87 más"),
408 (["x", "87"], "x u 87"),
409 (["x", "11 más"], "x u 11 más"),
410 (["x", "11"], "x u 11"),
411 (["x", "110 más"], "x o 110 más"),
412 (["x", "110"], "x o 110"),
413 (["x", "11.000 más"], "x u 11.000 más"),
414 (["x", "11.000"], "x u 11.000"),
415 (["x", "11.000,92 más"], "x u 11.000,92 más"),
416 (["x", "11.000,92"], "x u 11.000,92"),
417 );
418
419 test!(
420 "es-AR",
421 try_new_and_with_length,
422 (["x", "Ibiza"], "x e Ibiza"),
423 );
424 }
425
426 #[test]
427 fn test_hebrew() {
428 test!(
429 "he",
430 try_new_and_with_length,
431 (["x", "יפו"], "x ויפו"),
432 (["x", "Ibiza"], "x ו‑Ibiza"),
433 );
434 }
435}
436