harper_core/linting/
currency_placement.rs

1use itertools::Itertools;
2
3use crate::{Document, Span, Token, TokenStringExt, remove_overlaps};
4
5use super::{Lint, LintKind, Linter, Suggestion};
6
7#[derive(Debug, Default)]
8pub struct CurrencyPlacement {}
9
10impl Linter for CurrencyPlacement {
11    fn lint(&mut self, document: &Document) -> Vec<Lint> {
12        let mut lints = Vec::new();
13
14        for chunk in document.iter_chunks() {
15            for (a, b) in chunk.iter().tuple_windows() {
16                lints.extend(generate_lint_for_tokens(a, b, document));
17            }
18
19            for (p, a, b, c) in chunk.iter().tuple_windows() {
20                if !b.kind.is_whitespace() || p.kind.is_currency() {
21                    continue;
22                }
23
24                lints.extend(generate_lint_for_tokens(a, c, document));
25            }
26        }
27
28        remove_overlaps(&mut lints);
29
30        lints
31    }
32
33    fn description(&self) -> &str {
34        "The location of currency symbols varies by country. The rule looks for and corrects improper positioning."
35    }
36}
37
38// Given two tokens that may have an error, check if they do and create a [`Lint`].
39fn generate_lint_for_tokens(a: &Token, b: &Token, document: &Document) -> Option<Lint> {
40    let punct = a.kind.as_punctuation().or(b.kind.as_punctuation())?;
41    let currency = punct.as_currency()?;
42
43    let number = a.kind.as_number().or(b.kind.as_number())?;
44
45    let span = Span::new(a.span.start, b.span.end);
46
47    let correct: Vec<_> = currency.format_amount(number).chars().collect();
48    let actual = document.get_span_content(&span);
49
50    if correct != actual {
51        Some(Lint {
52            span,
53            lint_kind: LintKind::Formatting,
54            suggestions: vec![Suggestion::ReplaceWith(correct)],
55            message: "The position of the currency symbol matters.".to_string(),
56            priority: 63,
57        })
58    } else {
59        None
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
66
67    use super::CurrencyPlacement;
68
69    #[test]
70    fn eof() {
71        assert_suggestion_result(
72            "It was my last bill worth more than 4$.",
73            CurrencyPlacement::default(),
74            "It was my last bill worth more than $4.",
75        );
76    }
77
78    #[test]
79    fn blog_title_allows_correct() {
80        assert_lint_count("The Best $25 I Ever Spent", CurrencyPlacement::default(), 0);
81    }
82
83    #[test]
84    fn blog_title() {
85        assert_suggestion_result(
86            "The Best 25$ I Ever Spent",
87            CurrencyPlacement::default(),
88            "The Best $25 I Ever Spent",
89        );
90    }
91
92    #[test]
93    fn blog_title_cents() {
94        assert_suggestion_result(
95            "The Best ¢25 I Ever Spent",
96            CurrencyPlacement::default(),
97            "The Best 25¢ I Ever Spent",
98        );
99    }
100
101    #[test]
102    fn blog_title_with_space() {
103        assert_suggestion_result(
104            "The Best 25   $ I Ever Spent",
105            CurrencyPlacement::default(),
106            "The Best $25 I Ever Spent",
107        );
108    }
109
110    #[test]
111    fn multiple_dollar() {
112        assert_suggestion_result(
113            "They were either 25$ 24$ or 23$.",
114            CurrencyPlacement::default(),
115            "They were either $25 $24 or $23.",
116        );
117    }
118
119    #[test]
120    fn multiple_pound() {
121        assert_suggestion_result(
122            "They were either 25£ 24£ or 23£.",
123            CurrencyPlacement::default(),
124            "They were either £25 £24 or £23.",
125        );
126    }
127
128    #[test]
129    fn suffix() {
130        assert_suggestion_result(
131            "It was my 20th$.",
132            CurrencyPlacement::default(),
133            "It was my $20th.",
134        );
135    }
136
137    #[test]
138    fn seven_even_two_decimal_clean() {
139        assert_lint_count("$7.00", CurrencyPlacement::default(), 0);
140    }
141}