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 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 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}