harper_core/linting/
spelled_numbers.rs

1use crate::linting::{LintKind, Linter, Suggestion};
2use crate::{Document, Lint, Number, TokenStringExt};
3
4/// Linter that checks to make sure small integers (< 10) are spelled
5/// out.
6#[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
45/// Converts a number to its spelled-out variant.
46///
47/// For example: 100 -> one hundred.
48///
49/// Works for numbers up to 999, but can be expanded to include more powers of 10.
50fn 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; // truncate
90            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    /// Check that the algorithm won't stack overflow or return `None` for any numbers within the specified range.
137    #[test]
138    fn services_range() {
139        for i in 0..1000 {
140            spell_out_number(i).unwrap();
141        }
142    }
143}