harper_core/linting/
spelled_numbers.rs1use crate::linting::{LintKind, Linter, Suggestion};
2use crate::{Document, Lint, Number, TokenStringExt};
3
4#[derive(Default, Clone, Copy)]
7pub struct SpelledNumbers;
8
9impl Linter for SpelledNumbers {
10 fn lint(&mut self, document: &Document) -> Vec<crate::Lint> {
11 let mut lints = Vec::new();
12
13 for number_tok in document.iter_numbers() {
14 let Number {
15 value,
16 suffix: None,
17 ..
18 } = number_tok.kind.as_number().unwrap()
19 else {
20 continue;
21 };
22 let value: f64 = (*value).into();
23
24 if (value - value.floor()).abs() < f64::EPSILON && value < 10. {
25 lints.push(Lint {
26 span: number_tok.span,
27 lint_kind: LintKind::Readability,
28 suggestions: vec![Suggestion::ReplaceWith(
29 spell_out_number(value as u64).unwrap().chars().collect(),
30 )],
31 message: "Try to spell out numbers less than ten.".to_string(),
32 priority: 63,
33 })
34 }
35 }
36
37 lints
38 }
39
40 fn description(&self) -> &'static str {
41 "Most style guides recommend that you spell out numbers less than ten."
42 }
43}
44
45fn spell_out_number(num: u64) -> Option<String> {
51 if num > 999 {
52 return None;
53 }
54
55 Some(match num {
56 0 => "zero".to_string(),
57 1 => "one".to_string(),
58 2 => "two".to_string(),
59 3 => "three".to_string(),
60 4 => "four".to_string(),
61 5 => "five".to_string(),
62 6 => "six".to_string(),
63 7 => "seven".to_string(),
64 8 => "eight".to_string(),
65 9 => "nine".to_string(),
66 10 => "ten".to_string(),
67 11 => "eleven".to_string(),
68 12 => "twelve".to_string(),
69 13 => "thirteen".to_string(),
70 14 => "fourteen".to_string(),
71 15 => "fifteen".to_string(),
72 16 => "sixteen".to_string(),
73 17 => "seventeen".to_string(),
74 18 => "eighteen".to_string(),
75 19 => "nineteen".to_string(),
76 20 => "twenty".to_string(),
77 30 => "thirty".to_string(),
78 40 => "forty".to_string(),
79 50 => "fifty".to_string(),
80 60 => "sixty".to_string(),
81 70 => "seventy".to_string(),
82 80 => "eighty".to_string(),
83 90 => "ninety".to_string(),
84 hundred if hundred % 100 == 0 => {
85 format!("{} hundred", spell_out_number(hundred / 100).unwrap())
86 }
87 _ => {
88 let n = 10u64.pow((num as f32).log10() as u32);
89 let parent = (num / n) * n; let child = num % n;
91
92 format!(
93 "{}{}{}",
94 spell_out_number(parent).unwrap(),
95 if num <= 99 { '-' } else { ' ' },
96 spell_out_number(child).unwrap()
97 )
98 }
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use crate::linting::tests::assert_suggestion_result;
105
106 use super::{SpelledNumbers, spell_out_number};
107
108 #[test]
109 fn produces_zero() {
110 assert_eq!(spell_out_number(0), Some("zero".to_string()))
111 }
112
113 #[test]
114 fn produces_eighty_two() {
115 assert_eq!(spell_out_number(82), Some("eighty-two".to_string()))
116 }
117
118 #[test]
119 fn produces_nine_hundred_ninety_nine() {
120 assert_eq!(
121 spell_out_number(999),
122 Some("nine hundred ninety-nine".to_string())
123 )
124 }
125
126 #[test]
127 fn corrects_nine() {
128 assert_suggestion_result("There are 9 pigs.", SpelledNumbers, "There are nine pigs.");
129 }
130
131 #[test]
132 fn does_not_correct_ten() {
133 assert_suggestion_result("There are 10 pigs.", SpelledNumbers, "There are 10 pigs.");
134 }
135
136 #[test]
138 fn services_range() {
139 for i in 0..1000 {
140 spell_out_number(i).unwrap();
141 }
142 }
143}