i3status_rs/formatting/formatter/
tally.rs

1use std::str::FromStr;
2
3use crate::formatting::unit::Unit;
4
5use super::*;
6
7#[derive(Debug)]
8enum Style {
9    ChineseCountingRods,
10    ChineseTally,
11    WesternTally,
12    WesternTallyUngrouped,
13}
14
15impl FromStr for Style {
16    type Err = Error;
17
18    fn from_str(s: &str) -> Result<Self> {
19        match s {
20            "chinese_counting_rods" | "ccr" => Ok(Style::ChineseCountingRods),
21            "chinese_tally" | "ct" => Ok(Style::ChineseTally),
22            "western_tally" | "wt" => Ok(Style::WesternTally),
23            "western_tally_ungrouped" | "wtu" => Ok(Style::WesternTallyUngrouped),
24            x => Err(Error::new(format!("Unknown Style: '{x}'"))),
25        }
26    }
27}
28
29#[derive(Debug)]
30pub struct TallyFormatter {
31    style: Style,
32}
33
34impl TallyFormatter {
35    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
36        let mut style = Style::WesternTally;
37        for arg in args {
38            match arg.key {
39                "style" | "s" => {
40                    style = arg.parse_value()?;
41                }
42                other => {
43                    return Err(Error::new(format!(
44                        "Unknown argument for 'tally': '{other}'"
45                    )));
46                }
47            }
48        }
49        Ok(Self { style })
50    }
51}
52
53const HORIZONTAL_CHINESE_COUNTING_RODS_CHARS: [char; 10] =
54    ['〇', '𝍠', '𝍡', '𝍢', '𝍣', '𝍤', '𝍥', '𝍦', '𝍧', '𝍨'];
55
56const VERTICAL_CHINESE_COUNTING_RODS_CHARS: [char; 10] =
57    ['〇', '𝍩', '𝍪', '𝍫', '𝍬', '𝍭', '𝍮', '𝍯', '𝍰', '𝍱'];
58
59const CHINESE_TALLY_CHARS: [char; 5] = ['𝍲', '𝍳', '𝍴', '𝍵', '𝍶'];
60
61impl Formatter for TallyFormatter {
62    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
63        match val {
64            Value::Number {
65                val,
66                unit: Unit::None,
67            } => {
68                let is_negative = val.is_sign_negative();
69                let mut val = val.abs().round() as u64;
70                let mut result = String::new();
71                match self.style {
72                    Style::ChineseCountingRods => {
73                        if is_negative {
74                            result.push('\u{20E5}');
75                        }
76                        if val == 0 {
77                            result.insert(0, '〇');
78                        } else {
79                            let mut horizontal = true;
80                            while val != 0 {
81                                let digit = val % 10;
82                                val /= 10;
83                                let charset = if horizontal {
84                                    horizontal = false;
85                                    HORIZONTAL_CHINESE_COUNTING_RODS_CHARS
86                                } else {
87                                    horizontal = true;
88                                    VERTICAL_CHINESE_COUNTING_RODS_CHARS
89                                };
90                                result.insert(0, charset[digit as usize]);
91                            }
92                        }
93                    }
94                    Style::ChineseTally => {
95                        if is_negative {
96                            return Err(FormatError::Other(Error::new(
97                                "Chinese Tally marks do not support negative numbers",
98                            )));
99                        }
100                        let (fives, rem) = (val / 5, val % 5);
101                        for _ in 0..fives {
102                            result.push(CHINESE_TALLY_CHARS[4]);
103                        }
104                        if rem != 0 {
105                            result.push(CHINESE_TALLY_CHARS[rem as usize - 1]);
106                        }
107                    }
108                    Style::WesternTally | Style::WesternTallyUngrouped => {
109                        if is_negative {
110                            return Err(FormatError::Other(Error::new(
111                                "Western Tally marks do not support negative numbers",
112                            )));
113                        }
114                        if matches!(self.style, Style::WesternTally) {
115                            let fives = val / 5;
116                            val %= 5;
117                            for _ in 0..fives {
118                                result.push('𝍸');
119                            }
120                        }
121                        for _ in 0..val {
122                            result.push('𝍷');
123                        }
124                    }
125                }
126                Ok(result)
127            }
128            Value::Number { .. } => Err(FormatError::Other(Error::new(
129                "Tally can only format Numbers with Unit::None",
130            ))),
131            other => Err(FormatError::IncompatibleFormatter {
132                ty: other.type_name(),
133                fmt: "tally",
134            }),
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn tally_chinese_counting_rods_negative() {
145        let fmt = new_fmt!(tally, style: chinese_counting_rods).unwrap();
146        let config = SharedConfig::default();
147
148        let result = fmt
149            .format(
150                &Value::Number {
151                    val: -0.0,
152                    unit: Unit::None,
153                },
154                &config,
155            )
156            .unwrap();
157        assert_eq!(result, "〇\u{20E5}");
158
159        for (hundreds, hundreds_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS
160            .into_iter()
161            .enumerate()
162        {
163            for (tens, tens_char) in VERTICAL_CHINESE_COUNTING_RODS_CHARS.into_iter().enumerate() {
164                for (ones, ones_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS
165                    .into_iter()
166                    .enumerate()
167                {
168                    let val = -((hundreds * 100 + tens * 10 + ones) as f64);
169                    if val == 0.0 {
170                        continue;
171                    }
172                    // Contcat characters, excluding leading 〇
173                    let expected = String::from_iter(
174                        [hundreds_char, tens_char, ones_char, '\u{20E5}']
175                            .into_iter()
176                            .skip_while(|c| *c == '〇'),
177                    );
178
179                    let result = fmt
180                        .format(
181                            &Value::Number {
182                                val,
183                                unit: Unit::None,
184                            },
185                            &config,
186                        )
187                        .unwrap();
188                    assert_eq!(result, expected);
189                }
190            }
191        }
192    }
193
194    #[test]
195    fn tally_chinese_counting_rods_positive() {
196        let fmt = new_fmt!(tally, style: chinese_counting_rods).unwrap();
197        let config = SharedConfig::default();
198
199        let result = fmt
200            .format(
201                &Value::Number {
202                    val: 0.0,
203                    unit: Unit::None,
204                },
205                &config,
206            )
207            .unwrap();
208        assert_eq!(result, "〇");
209
210        for (hundreds, hundreds_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS
211            .into_iter()
212            .enumerate()
213        {
214            for (tens, tens_char) in VERTICAL_CHINESE_COUNTING_RODS_CHARS.into_iter().enumerate() {
215                for (ones, ones_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS
216                    .into_iter()
217                    .enumerate()
218                {
219                    let val = (hundreds * 100 + tens * 10 + ones) as f64;
220                    if val == 0.0 {
221                        continue;
222                    }
223                    // Contcat characters, excluding leading 〇
224                    let expected = String::from_iter(
225                        [hundreds_char, tens_char, ones_char]
226                            .into_iter()
227                            .skip_while(|c| *c == '〇'),
228                    );
229
230                    let result = fmt
231                        .format(
232                            &Value::Number {
233                                val,
234                                unit: Unit::None,
235                            },
236                            &config,
237                        )
238                        .unwrap();
239                    assert_eq!(result, expected);
240                }
241            }
242        }
243    }
244
245    #[test]
246    fn tally_chinese_tally_negative() {
247        let fmt = new_fmt!(tally, style: chinese_tally).unwrap();
248        let config = SharedConfig::default();
249
250        let result = fmt.format(
251            &Value::Number {
252                val: -1.0,
253                unit: Unit::None,
254            },
255            &config,
256        );
257        assert!(result.is_err());
258    }
259
260    #[test]
261    fn tally_chinese_tally_positive() {
262        let fmt = new_fmt!(tally, style: chinese_tally).unwrap();
263        let config = SharedConfig::default();
264
265        let result = fmt
266            .format(
267                &Value::Number {
268                    val: 0.0,
269                    unit: Unit::None,
270                },
271                &config,
272            )
273            .unwrap();
274        assert_eq!(result, "");
275
276        let result = fmt
277            .format(
278                &Value::Number {
279                    val: 1.0,
280                    unit: Unit::None,
281                },
282                &config,
283            )
284            .unwrap();
285        assert_eq!(result, "𝍲");
286
287        let result = fmt
288            .format(
289                &Value::Number {
290                    val: 2.0,
291                    unit: Unit::None,
292                },
293                &config,
294            )
295            .unwrap();
296        assert_eq!(result, "𝍳");
297
298        let result = fmt
299            .format(
300                &Value::Number {
301                    val: 3.0,
302                    unit: Unit::None,
303                },
304                &config,
305            )
306            .unwrap();
307        assert_eq!(result, "𝍴");
308
309        let result = fmt
310            .format(
311                &Value::Number {
312                    val: 4.0,
313                    unit: Unit::None,
314                },
315                &config,
316            )
317            .unwrap();
318        assert_eq!(result, "𝍵");
319
320        let result = fmt
321            .format(
322                &Value::Number {
323                    val: 5.0,
324                    unit: Unit::None,
325                },
326                &config,
327            )
328            .unwrap();
329        assert_eq!(result, "𝍶");
330
331        let result = fmt
332            .format(
333                &Value::Number {
334                    val: 6.0,
335                    unit: Unit::None,
336                },
337                &config,
338            )
339            .unwrap();
340        assert_eq!(result, "𝍶𝍲");
341
342        let result = fmt
343            .format(
344                &Value::Number {
345                    val: 7.0,
346                    unit: Unit::None,
347                },
348                &config,
349            )
350            .unwrap();
351        assert_eq!(result, "𝍶𝍳");
352
353        let result = fmt
354            .format(
355                &Value::Number {
356                    val: 8.0,
357                    unit: Unit::None,
358                },
359                &config,
360            )
361            .unwrap();
362        assert_eq!(result, "𝍶𝍴");
363
364        let result = fmt
365            .format(
366                &Value::Number {
367                    val: 9.0,
368                    unit: Unit::None,
369                },
370                &config,
371            )
372            .unwrap();
373        assert_eq!(result, "𝍶𝍵");
374
375        let result = fmt
376            .format(
377                &Value::Number {
378                    val: 10.0,
379                    unit: Unit::None,
380                },
381                &config,
382            )
383            .unwrap();
384        assert_eq!(result, "𝍶𝍶");
385    }
386
387    #[test]
388    fn tally_western_tally_negative() {
389        let fmt = new_fmt!(tally, style: western_tally).unwrap();
390        let config = SharedConfig::default();
391
392        let result = fmt.format(
393            &Value::Number {
394                val: -1.0,
395                unit: Unit::None,
396            },
397            &config,
398        );
399        assert!(result.is_err());
400    }
401
402    #[test]
403    fn tally_western_tally_positive() {
404        let fmt = new_fmt!(tally, style: western_tally).unwrap();
405        let config = SharedConfig::default();
406
407        let result = fmt
408            .format(
409                &Value::Number {
410                    val: 0.0,
411                    unit: Unit::None,
412                },
413                &config,
414            )
415            .unwrap();
416        assert_eq!(result, "");
417
418        let result = fmt
419            .format(
420                &Value::Number {
421                    val: 1.0,
422                    unit: Unit::None,
423                },
424                &config,
425            )
426            .unwrap();
427        assert_eq!(result, "𝍷");
428
429        let result = fmt
430            .format(
431                &Value::Number {
432                    val: 2.0,
433                    unit: Unit::None,
434                },
435                &config,
436            )
437            .unwrap();
438        assert_eq!(result, "𝍷𝍷");
439
440        let result = fmt
441            .format(
442                &Value::Number {
443                    val: 3.0,
444                    unit: Unit::None,
445                },
446                &config,
447            )
448            .unwrap();
449        assert_eq!(result, "𝍷𝍷𝍷");
450
451        let result = fmt
452            .format(
453                &Value::Number {
454                    val: 4.0,
455                    unit: Unit::None,
456                },
457                &config,
458            )
459            .unwrap();
460        assert_eq!(result, "𝍷𝍷𝍷𝍷");
461
462        let result = fmt
463            .format(
464                &Value::Number {
465                    val: 5.0,
466                    unit: Unit::None,
467                },
468                &config,
469            )
470            .unwrap();
471        assert_eq!(result, "𝍸");
472
473        let result = fmt
474            .format(
475                &Value::Number {
476                    val: 6.0,
477                    unit: Unit::None,
478                },
479                &config,
480            )
481            .unwrap();
482        assert_eq!(result, "𝍸𝍷");
483    }
484
485    #[test]
486    fn tally_western_tally_ungrouped_negative() {
487        let fmt = new_fmt!(tally, style: western_tally_ungrouped).unwrap();
488        let config = SharedConfig::default();
489
490        let result = fmt.format(
491            &Value::Number {
492                val: -1.0,
493                unit: Unit::None,
494            },
495            &config,
496        );
497        assert!(result.is_err());
498    }
499
500    #[test]
501    fn tally_western_tally_ungrouped_positive() {
502        let fmt = new_fmt!(tally, style: western_tally_ungrouped).unwrap();
503        let config = SharedConfig::default();
504
505        let result = fmt
506            .format(
507                &Value::Number {
508                    val: 0.0,
509                    unit: Unit::None,
510                },
511                &config,
512            )
513            .unwrap();
514        assert_eq!(result, "");
515
516        let result = fmt
517            .format(
518                &Value::Number {
519                    val: 1.0,
520                    unit: Unit::None,
521                },
522                &config,
523            )
524            .unwrap();
525        assert_eq!(result, "𝍷");
526
527        let result = fmt
528            .format(
529                &Value::Number {
530                    val: 2.0,
531                    unit: Unit::None,
532                },
533                &config,
534            )
535            .unwrap();
536        assert_eq!(result, "𝍷𝍷");
537
538        let result = fmt
539            .format(
540                &Value::Number {
541                    val: 3.0,
542                    unit: Unit::None,
543                },
544                &config,
545            )
546            .unwrap();
547        assert_eq!(result, "𝍷𝍷𝍷");
548
549        let result = fmt
550            .format(
551                &Value::Number {
552                    val: 4.0,
553                    unit: Unit::None,
554                },
555                &config,
556            )
557            .unwrap();
558        assert_eq!(result, "𝍷𝍷𝍷𝍷");
559
560        let result = fmt
561            .format(
562                &Value::Number {
563                    val: 5.0,
564                    unit: Unit::None,
565                },
566                &config,
567            )
568            .unwrap();
569        assert_eq!(result, "𝍷𝍷𝍷𝍷𝍷");
570
571        let result = fmt
572            .format(
573                &Value::Number {
574                    val: 6.0,
575                    unit: Unit::None,
576                },
577                &config,
578            )
579            .unwrap();
580        assert_eq!(result, "𝍷𝍷𝍷𝍷𝍷𝍷");
581    }
582}