escpos_rust/
formatter.rs

1/// Options to print tables
2#[derive(Clone, Debug)]
3pub struct TableOptions {
4    /// Indicates the header/row division character
5    pub header_division_pattern: Option<String>,
6    /// Inicates if a pattern should be used to bridge between columns
7    pub join_columns_pattern: Option<String>
8}
9
10/// Helper structure to format text
11///
12/// The Formatter structure helps create some simple shapes through text, like tables and just formatted text. By default, for tables a header division pattern of `-` and no pattern for bridging columns will be used. This can be modified by using either [set_table_options](Formatter::set_table_options) or by [modify_table_options](Formatter::modify_table_options).
13pub struct Formatter {
14    /// Inner table options
15    table_options: TableOptions,
16    /// Width to use for formatting
17    width: u8
18}
19
20impl Formatter {
21    /// Creates a new formatter with a default width
22    pub fn new(width: u8) -> Formatter {
23        Formatter{
24            table_options: TableOptions {
25                header_division_pattern: Some("-".into()),
26                join_columns_pattern: None
27            },
28            width
29        }
30    }
31
32    /// Sets a new set of table options
33    ///
34    /// To modify just one parameter in a simpler way, check the [modify_table_options](self::Formatter::modify_table_options) method.
35    ///
36    /// ```rust
37    /// # use escpos_rs::{Formatter, TableOptions};
38    /// let mut formatter = Formatter::new(20);
39    /// formatter.set_table_options(TableOptions {
40    ///     header_division_pattern: Some(".-".into()),
41    ///     join_columns_pattern: Some(".".into())
42    /// });
43    /// ```
44    pub fn set_table_options(&mut self, table_options: TableOptions) {
45        self.table_options = table_options
46    }
47
48    /// Gives back a reference to the active table options
49    ///
50    /// ```rust
51    /// # use escpos_rs::Formatter;
52    /// let mut formatter = Formatter::new(20);
53    /// assert_eq!(Some("-".to_string()), formatter.get_table_options().header_division_pattern);
54    /// ```
55    pub fn get_table_options(&self) -> &TableOptions {
56        &self.table_options
57    }
58
59    /// Modify the table options through a callback
60    ///
61    /// Allows table options modification with a function or closure. Sometimes it may come in hand.
62    ///
63    /// ```rust
64    /// # use escpos_rs::Formatter;
65    /// let mut formatter = Formatter::new(20);
66    /// formatter.modify_table_options(|table_options| {
67    ///     table_options.header_division_pattern = Some("=".to_string());
68    /// });
69    /// assert_eq!(Some("=".to_string()), formatter.get_table_options().header_division_pattern);
70    /// ```
71    pub fn modify_table_options<F: Fn(&mut TableOptions)>(&mut self, modifier: F) {
72        modifier(&mut self.table_options);
73    }
74
75    /// Splits a string by whitespaces, according to the given width
76    ///
77    /// Notice that the final line will not contain a new line at the end.
78    ///
79    /// ```rust
80    /// use escpos_rs::Formatter;
81    /// 
82    /// let formatter = Formatter::new(16);
83    /// let res = formatter.space_split("Sentence with two lines.");
84    /// assert_eq!("Sentence with\ntwo lines.", res.as_str());
85    /// ```
86    pub fn space_split<A: AsRef<str>>(&self, source: A) -> String {
87        let mut result = source.as_ref().split("\n").map(|line| {
88            // Now, for each line, we split it into words.
89            let mut current_line = String::new();
90            let mut broken_lines = Vec::new();
91            for word in line.split_whitespace() {
92                let num_chars = word.chars().count();
93                // The one being added marks the space
94                if current_line.len() + num_chars + 1 < self.width.into() {
95                    // Easy to add to the current line, the conditional if is for the first word of them all.
96                    current_line += &format!("{}{}", if current_line.len() == 0 {""} else {" "}, word);
97                } else {
98                    // We have to terminate the current line, in case it contains something
99                    if !current_line.is_empty() {
100                        broken_lines.push(current_line.clone());
101                    }
102                    if num_chars < self.width.into() {
103                        // We start the next line with the current word
104                        current_line = word.to_string();
105                    } else {
106                        // We use a char iterator to split this into lines
107                        let mut chars = word.chars();
108                        let mut word_fragment: String = chars.by_ref().take(self.width.into()).collect();
109                        broken_lines.push(format!("{}",word_fragment));
110                        while !word_fragment.is_empty() {
111                            word_fragment = chars.by_ref().take(self.width.into()).collect();
112                            broken_lines.push(format!("{}",word_fragment));
113                        }
114                    }
115                }
116            }
117            if !current_line.is_empty() {
118                broken_lines.push(current_line);
119            }
120            broken_lines.join("\n")
121        }).collect::<Vec<_>>().join("");
122        // If the last character is a new line, we need to add it back in
123        if let Some(last_char) = source.as_ref().chars().last() {
124            if last_char == '\n' {
125                result += "\n";
126            }
127        }
128        result
129    }
130
131    /// Creates a table with two columns
132    ///
133    /// In case the headers do not fit with at least one space between, priority will be given to the second header, and the last remaining character from the first header will be replaced by a dot. If the second header would need to be shortened to less than 3 characters, then the first header will now also be truncated, with the same dot replacing the last charcater from the remaining part of the first header.
134    ///
135    /// ```rust
136    /// # use escpos_rs::Formatter;
137    /// let formatter = Formatter::new(20);
138    /// let header = ("Product", "Price");
139    /// let rows = vec![
140    ///     ("Milk", "5.00"),
141    ///     ("Cereal", "10.00")
142    /// ];
143    ///
144    /// // We use trim_start just to show the table nicer in this example.
145    /// let target = r#"
146    /// Product        Price
147    /// --------------------
148    /// Milk            5.00
149    /// Cereal         10.00
150    /// "#.trim_start();
151    /// 
152    /// assert_eq!(target, formatter.duo_table(header, rows));
153    /// ```
154    pub fn duo_table<A: Into<String>, B: Into<String>, C: IntoIterator<Item = (D, E)>, D: Into<String>, E: Into<String>>(&self, header: (A, B), rows: C) -> String {
155        // Aux closure to create each row.
156        let aux_duo_table = |mut first: String, mut second: String, width: u8, replace_last: Option<char>| -> String {
157            let row_width = first.len() + second.len();
158            let (column_1, column_2) = if row_width < width as usize {
159                (first, second)
160            } else {
161                // If the second column requires all the space, we give it
162                if second.len() + 4 > (width as usize) {
163                    if let Some(replacement) = replace_last {
164                        second.truncate((width as usize) - 5);
165                        second += &replacement.to_string();
166                    } else {
167                        second.truncate((width as usize) - 4);
168                    }
169                }
170
171                // We calculate the remaining space for the second word now.
172                let remaining = (width as usize) - second.len();
173                // We just need to shorten the second word. We need to include the separating whitespace
174                if first.len() > remaining {
175                    if let Some(replacement) = replace_last {
176                        first.truncate(remaining - 2);
177                        first += &replacement.to_string();
178                    } else {
179                        first.truncate(remaining - 1);
180                    }
181                }
182
183                (first, second)
184            };
185
186            format!("{} {:>2$}\n",
187                column_1,
188                column_2,
189                (width as usize) - (column_1.len() + 1)
190            )
191        };
192
193        let mut content = aux_duo_table(header.0.into(), header.1.into(), self.width, Some('.'));
194
195        if let Some(hdp) = self.print_header_division_pattern() {
196            content += &hdp;
197        }
198
199        for row in rows {
200            let (first, second) = (row.0.into(), row.1.into());
201            content += &aux_duo_table(first, second, self.width, None);
202        }
203        content
204    }
205
206    /// Creates a table with three columns
207    ///
208    /// In case the headers do not fit with at least one space between, priority will be given to the first header, and the last remaining character from the second header will be replaced by a dot. If the second header would need to be shortened to less than 3 characters, then the first header will now also be truncated, with the same dot replacing the last charcater from the remaining part of the first header.
209    ///
210    /// ```rust
211    /// # use escpos_rs::Formatter;
212    /// let formatter = Formatter::new(20);
213    /// let header = ("Product", "Price", "Qty.");
214    /// let rows = vec![
215    ///     ("Milk", "5.00", "3"),
216    ///     ("Cereal", "10.00", "1")
217    /// ];
218    ///
219    /// // We use trim_start just to show the table nicer in this example.
220    /// let target = r#"
221    /// Product  Price  Qty.
222    /// --------------------
223    /// Milk     5.00      3
224    /// Cereal   10.00     1
225    /// "#.trim_start();
226    /// 
227    /// assert_eq!(target, formatter.trio_table(header, rows));
228    /// ```
229    pub fn trio_table<A: Into<String>, B: Into<String>, C: Into<String>, D: IntoIterator<Item = (E, F, G)>, E: Into<String>, F: Into<String>, G: Into<String>>(&self, header: (A, B, C), rows: D) -> String {
230        // Auxiliary closure for printing
231        let aux_trio_table = |mut first: String, mut second: String, mut third: String, width: u8, limits: (u8, u8), replace_last: Option<char>| -> String {
232            if first.len() > limits.0 as usize {
233                let max_width = (limits.0 as usize) - 1;
234                if let Some(replacement) = replace_last {
235                    first.truncate(max_width);
236                    first += &replacement.to_string();
237                } else {
238                    first.truncate(max_width);
239                }
240            }
241            if second.len() > (limits.1 - limits.0) as usize {
242                let max_width = (limits.1 - limits.0) as usize;
243                if let Some(replacement) = replace_last {
244                    second.truncate(max_width);
245                    second += &replacement.to_string();
246                } else {
247                    second.truncate(max_width);
248                }
249            }
250            if third.len() - 1 > (width - limits.1) as usize {
251                let max_width = (width - limits.1) as usize;
252                if let Some(replacement) = replace_last {
253                    third.truncate(max_width);
254                    third += &replacement.to_string();
255                } else {
256                    third.truncate(max_width);
257                }
258            }
259            format!("{:<3$} {:^4$} {:>5$}\n",
260                first,
261                second,
262                third,
263                (limits.0 - 1) as usize,
264                (limits.1 - limits.0) as usize,
265                (width - limits.1 - 1) as usize
266            )
267        };
268
269        // First step, is to find the maximum desirable width of a column.
270        let header: (String, String, String) = (header.0.into(), header.1.into(), header.2.into());
271        let mut max_left = header.0.len();
272        let mut max_middle = header.1.len();
273        let mut max_right = header.2.len();
274
275        // I was not able to do 2 for loops with the IntoIterator trait with borrowed items :(
276        let rows: Vec<(String, String, String)> = rows.into_iter().map(|(a, b, c)| (a.into(), b.into(), c.into())).collect();
277        
278        // Now we compare to all rows
279        for row in &rows {
280            if row.0.len() > max_left {
281                max_left = row.0.len();
282            }
283            if row.1.len() > max_middle {
284                max_middle = row.1.len();
285            }
286            if row.2.len() > max_right {
287                max_right = row.2.len();
288            }
289        }
290
291        let limits = if max_left + max_middle + max_right + 2 < self.width as usize {
292            // Nothing to do, easy peasy
293            ((max_left + 1) as u8, (self.width as usize - max_right - 1) as u8)
294        } else {
295            let mut limits = (0u8, self.width as u8);
296            // The left-most column must be at least 4 characters wide, with the lowest priority
297            if max_middle + max_right + 4 > (self.width as usize) {
298                limits.0 = 4;
299            } else {
300                limits.0 = ((self.width as usize) - max_middle - max_right) as u8;
301            }
302
303            // Ahora para el segundo lĂ­mite
304            let remaining = self.width - limits.0;
305
306            if (max_right as u8) + 4 > remaining {
307                limits.1 = limits.0 + 4;
308            } else {
309                limits.1 = limits.0 + remaining - (max_right as u8);
310            }
311            limits
312        };
313
314        let mut content = aux_trio_table(header.0, header.1, header.2, self.width, limits, None);
315
316        if let Some(hdp) = self.print_header_division_pattern() {
317            content += &hdp;
318        }
319
320        for row in rows {
321            content += &aux_trio_table(row.0, row.1, row.2, self.width, limits, None);
322        }
323        content
324    }
325
326    fn print_header_division_pattern(&self) -> Option<String> {
327        if let Some(header_division_pattern) = &self.table_options.header_division_pattern {
328            let mut line = header_division_pattern.repeat((self.width as usize) / header_division_pattern.len() + 1);
329            line.truncate(self.width as usize);
330            Some(line + "\n")
331        } else {
332            None
333        }
334    }
335}