From 41257f998256a4203da49c6c344045d18431c32d Mon Sep 17 00:00:00 2001 From: Bryan Malyn Date: Sat, 30 Mar 2024 19:34:03 -0500 Subject: [PATCH] Add tally formatter --- src/formatting.rs | 6 + src/formatting/formatter.rs | 3 + src/formatting/formatter/tally.rs | 582 ++++++++++++++++++++++++++++++ 3 files changed, 591 insertions(+) create mode 100644 src/formatting/formatter/tally.rs diff --git a/src/formatting.rs b/src/formatting.rs index 7ad57c402a..32db7d7dc6 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -63,6 +63,12 @@ //! `max_value` | which value is treated as "full". For example, for battery level `100` is full. | `100` //! `vertical` or `v` | whether to render the bar vertically or not | `false` //! +//! ## `tally` - Display numbers as tally marks +//! +//! Argument | Description |Default value +//! ---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------- +//! `style` or `s` | One of [`chinese_counting_rods`/`ccr`](https://en.wikipedia.org/wiki/Counting_rods), [`chinese_tally`/`ct`, `western_tally`/`wt`, `western_tally_ungrouped`/`wtu`](https://en.wikipedia.org/wiki/Tally_marks) | western_tally +//! //! ## `pango-str` - Just display the text without pango markup escaping //! //! No arguments. diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 69ffe8a74b..36887b528c 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -25,6 +25,8 @@ macro_rules! new_fmt { mod bar; pub use bar::BarFormatter; +mod tally; +pub use tally::TallyFormatter; mod datetime; pub use datetime::{DatetimeFormatter, DEFAULT_DATETIME_FORMATTER}; mod eng; @@ -51,6 +53,7 @@ pub fn new_formatter(name: &str, args: &[Arg]) -> Result> { "eng" => Ok(Box::new(EngFormatter::from_args(args)?)), "pango-str" => Ok(Box::new(PangoStrFormatter::from_args(args)?)), "str" => Ok(Box::new(StrFormatter::from_args(args)?)), + "tally" => Ok(Box::new(TallyFormatter::from_args(args)?)), _ => Err(Error::new(format!("Unknown formatter: '{name}'"))), } } diff --git a/src/formatting/formatter/tally.rs b/src/formatting/formatter/tally.rs new file mode 100644 index 0000000000..15bdf614b3 --- /dev/null +++ b/src/formatting/formatter/tally.rs @@ -0,0 +1,582 @@ +use std::str::FromStr; + +use crate::formatting::unit::Unit; + +use super::*; + +#[derive(Debug)] +enum Style { + ChineseCountingRods, + ChineseTally, + WesternTally, + WesternTallyUngrouped, +} + +impl FromStr for Style { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "chinese_counting_rods" | "ccr" => Ok(Style::ChineseCountingRods), + "chinese_tally" | "ct" => Ok(Style::ChineseTally), + "western_tally" | "wt" => Ok(Style::WesternTally), + "western_tally_ungrouped" | "wtu" => Ok(Style::WesternTallyUngrouped), + x => Err(Error::new(format!("Unknown Style: '{x}'"))), + } + } +} + +#[derive(Debug)] +pub struct TallyFormatter { + style: Style, +} + +impl TallyFormatter { + pub(super) fn from_args(args: &[Arg]) -> Result { + let mut style = Style::WesternTally; + for arg in args { + match arg.key { + "style" | "s" => { + style = arg.val.parse()?; + } + other => { + return Err(Error::new(format!( + "Unknown argument for 'tally': '{other}'" + ))); + } + } + } + Ok(Self { style }) + } +} + +const HORIZONTAL_CHINESE_COUNTING_RODS_CHARS: [char; 10] = + ['〇', '𝍠', '𝍡', '𝍢', '𝍣', '𝍤', '𝍥', '𝍦', '𝍧', '𝍨']; + +const VERTICAL_CHINESE_COUNTING_RODS_CHARS: [char; 10] = + ['〇', '𝍩', '𝍪', '𝍫', '𝍬', '𝍭', '𝍮', '𝍯', '𝍰', '𝍱']; + +const CHINESE_TALLY_CHARS: [char; 5] = ['𝍲', '𝍳', '𝍴', '𝍵', '𝍶']; + +impl Formatter for TallyFormatter { + fn format(&self, val: &Value, _config: &SharedConfig) -> Result { + match val { + Value::Number { + val, + unit: Unit::None, + } => { + let is_negative = val.is_sign_negative(); + let mut val = val.abs().round() as u64; + let mut result = String::new(); + match self.style { + Style::ChineseCountingRods => { + if is_negative { + result.push('\u{20E5}'); + } + if val == 0 { + result.insert(0, '〇'); + } else { + let mut horizontal = true; + while val != 0 { + let digit = val % 10; + val /= 10; + let charset = if horizontal { + horizontal = false; + HORIZONTAL_CHINESE_COUNTING_RODS_CHARS + } else { + horizontal = true; + VERTICAL_CHINESE_COUNTING_RODS_CHARS + }; + result.insert(0, charset[digit as usize]); + } + } + } + Style::ChineseTally => { + if is_negative { + return Err(FormatError::Other(Error::new( + "Chinese Tally marks do not support negative numbers", + ))); + } + let (fives, rem) = (val / 5, val % 5); + for _ in 0..fives { + result.push(CHINESE_TALLY_CHARS[4]); + } + if rem != 0 { + result.push(CHINESE_TALLY_CHARS[rem as usize - 1]); + } + } + Style::WesternTally | Style::WesternTallyUngrouped => { + if is_negative { + return Err(FormatError::Other(Error::new( + "Western Tally marks do not support negative numbers", + ))); + } + if matches!(self.style, Style::WesternTally) { + let fives = val / 5; + val %= 5; + for _ in 0..fives { + result.push('𝍸'); + } + } + for _ in 0..val { + result.push('𝍷'); + } + } + } + Ok(result) + } + Value::Number { .. } => Err(FormatError::Other(Error::new( + "Tally can only format Numbers with Unit::None", + ))), + other => Err(FormatError::IncompatibleFormatter { + ty: other.type_name(), + fmt: "tally", + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tally_chinese_counting_rods_negative() { + let fmt = new_fmt!(tally, style: chinese_counting_rods).unwrap(); + let config = SharedConfig::default(); + + let result = fmt + .format( + &Value::Number { + val: -0.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "〇\u{20E5}"); + + for (hundreds, hundreds_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS + .into_iter() + .enumerate() + { + for (tens, tens_char) in VERTICAL_CHINESE_COUNTING_RODS_CHARS.into_iter().enumerate() { + for (ones, ones_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS + .into_iter() + .enumerate() + { + let val = -((hundreds * 100 + tens * 10 + ones) as f64); + if val == 0.0 { + continue; + } + // Contcat characters, excluding leading 〇 + let expected = String::from_iter( + [hundreds_char, tens_char, ones_char, '\u{20E5}'] + .into_iter() + .skip_while(|c| *c == '〇'), + ); + + let result = fmt + .format( + &Value::Number { + val, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, expected); + } + } + } + } + + #[test] + fn tally_chinese_counting_rods_positive() { + let fmt = new_fmt!(tally, style: chinese_counting_rods).unwrap(); + let config = SharedConfig::default(); + + let result = fmt + .format( + &Value::Number { + val: 0.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "〇"); + + for (hundreds, hundreds_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS + .into_iter() + .enumerate() + { + for (tens, tens_char) in VERTICAL_CHINESE_COUNTING_RODS_CHARS.into_iter().enumerate() { + for (ones, ones_char) in HORIZONTAL_CHINESE_COUNTING_RODS_CHARS + .into_iter() + .enumerate() + { + let val = (hundreds * 100 + tens * 10 + ones) as f64; + if val == 0.0 { + continue; + } + // Contcat characters, excluding leading 〇 + let expected = String::from_iter( + [hundreds_char, tens_char, ones_char] + .into_iter() + .skip_while(|c| *c == '〇'), + ); + + let result = fmt + .format( + &Value::Number { + val, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, expected); + } + } + } + } + + #[test] + fn tally_chinese_tally_negative() { + let fmt = new_fmt!(tally, style: chinese_tally).unwrap(); + let config = SharedConfig::default(); + + let result = fmt.format( + &Value::Number { + val: -1.0, + unit: Unit::None, + }, + &config, + ); + assert!(result.is_err()); + } + + #[test] + fn tally_chinese_tally_positive() { + let fmt = new_fmt!(tally, style: chinese_tally).unwrap(); + let config = SharedConfig::default(); + + let result = fmt + .format( + &Value::Number { + val: 0.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, ""); + + let result = fmt + .format( + &Value::Number { + val: 1.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍲"); + + let result = fmt + .format( + &Value::Number { + val: 2.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍳"); + + let result = fmt + .format( + &Value::Number { + val: 3.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍴"); + + let result = fmt + .format( + &Value::Number { + val: 4.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍵"); + + let result = fmt + .format( + &Value::Number { + val: 5.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶"); + + let result = fmt + .format( + &Value::Number { + val: 6.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶𝍲"); + + let result = fmt + .format( + &Value::Number { + val: 7.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶𝍳"); + + let result = fmt + .format( + &Value::Number { + val: 8.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶𝍴"); + + let result = fmt + .format( + &Value::Number { + val: 9.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶𝍵"); + + let result = fmt + .format( + &Value::Number { + val: 10.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍶𝍶"); + } + + #[test] + fn tally_western_tally_negative() { + let fmt = new_fmt!(tally, style: western_tally).unwrap(); + let config = SharedConfig::default(); + + let result = fmt.format( + &Value::Number { + val: -1.0, + unit: Unit::None, + }, + &config, + ); + assert!(result.is_err()); + } + + #[test] + fn tally_western_tally_positive() { + let fmt = new_fmt!(tally, style: western_tally).unwrap(); + let config = SharedConfig::default(); + + let result = fmt + .format( + &Value::Number { + val: 0.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, ""); + + let result = fmt + .format( + &Value::Number { + val: 1.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 2.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 3.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 4.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 5.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍸"); + + let result = fmt + .format( + &Value::Number { + val: 6.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍸𝍷"); + } + + #[test] + fn tally_western_tally_ungrouped_negative() { + let fmt = new_fmt!(tally, style: western_tally_ungrouped).unwrap(); + let config = SharedConfig::default(); + + let result = fmt.format( + &Value::Number { + val: -1.0, + unit: Unit::None, + }, + &config, + ); + assert!(result.is_err()); + } + + #[test] + fn tally_western_tally_ungrouped_positive() { + let fmt = new_fmt!(tally, style: western_tally_ungrouped).unwrap(); + let config = SharedConfig::default(); + + let result = fmt + .format( + &Value::Number { + val: 0.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, ""); + + let result = fmt + .format( + &Value::Number { + val: 1.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 2.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 3.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 4.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 5.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷𝍷𝍷"); + + let result = fmt + .format( + &Value::Number { + val: 6.0, + unit: Unit::None, + }, + &config, + ) + .unwrap(); + assert_eq!(result, "𝍷𝍷𝍷𝍷𝍷𝍷"); + } +}