harper_core/linting/
currency_placement.rs1use 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
38fn 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}