Skip to main content

writing_analysis/
cliche.rs

1use crate::error::{Result, WritingAnalysisError};
2
3/// Result of cliché detection.
4#[derive(Debug, Clone, PartialEq)]
5pub struct ClicheResult {
6    /// All detected cliché instances
7    pub instances: Vec<ClicheInstance>,
8    /// Total number of clichés found
9    pub count: usize,
10}
11
12/// A single cliché occurrence.
13#[derive(Debug, Clone, PartialEq)]
14pub struct ClicheInstance {
15    /// The matched cliché phrase as it appears in the text
16    pub phrase: String,
17    /// Byte offset in the original text
18    pub offset: usize,
19    /// The canonical form from the built-in list
20    pub canonical: String,
21}
22
23static CLICHES: &[&str] = &[
24    "a chip on your shoulder",
25    "a dime a dozen",
26    "a picture is worth a thousand words",
27    "absence makes the heart grow fonder",
28    "actions speak louder than words",
29    "add insult to injury",
30    "against all odds",
31    "all in a day's work",
32    "all that glitters is not gold",
33    "at the drop of a hat",
34    "at the end of the day",
35    "back to the drawing board",
36    "barking up the wrong tree",
37    "beat a dead horse",
38    "beat around the bush",
39    "better late than never",
40    "better safe than sorry",
41    "between a rock and a hard place",
42    "bite the bullet",
43    "bite the dust",
44    "blood is thicker than water",
45    "break the ice",
46    "burning the midnight oil",
47    "by the skin of your teeth",
48    "calm before the storm",
49    "cost an arm and a leg",
50    "cut to the chase",
51    "don't cry over spilled milk",
52    "don't put all your eggs in one basket",
53    "drastic times call for drastic measures",
54    "easier said than done",
55    "easy as pie",
56    "every cloud has a silver lining",
57    "fall head over heels",
58    "few and far between",
59    "fit as a fiddle",
60    "go the extra mile",
61    "good things come to those who wait",
62    "hit the nail on the head",
63    "ignorance is bliss",
64    "in the heat of the moment",
65    "it takes two to tango",
66    "keep your chin up",
67    "kill two birds with one stone",
68    "last but not least",
69    "leave no stone unturned",
70    "let the cat out of the bag",
71    "like riding a bicycle",
72    "live and learn",
73    "look before you leap",
74    "more than meets the eye",
75    "needle in a haystack",
76    "no pain no gain",
77    "once in a blue moon",
78    "only time will tell",
79    "out of the frying pan into the fire",
80    "par for the course",
81    "play it by ear",
82    "pulling someone's leg",
83    "read between the lines",
84    "reinvent the wheel",
85    "see eye to eye",
86    "shoot for the moon",
87    "sleep on it",
88    "speak of the devil",
89    "stand the test of time",
90    "take it with a grain of salt",
91    "the apple doesn't fall far from the tree",
92    "the ball is in your court",
93    "the best of both worlds",
94    "the best thing since sliced bread",
95    "the bigger they are the harder they fall",
96    "the early bird catches the worm",
97    "the elephant in the room",
98    "the last straw",
99    "the tip of the iceberg",
100    "the whole nine yards",
101    "think outside the box",
102    "time flies when you're having fun",
103    "time is money",
104    "to each his own",
105    "under the weather",
106    "when it rains it pours",
107    "you can't judge a book by its cover",
108];
109
110/// Detect clichés in text.
111pub fn detect_cliches(text: &str) -> Result<ClicheResult> {
112    if text.trim().is_empty() {
113        return Err(WritingAnalysisError::EmptyText);
114    }
115
116    let lower = text.to_lowercase();
117    let mut instances = Vec::new();
118
119    for &cliche in CLICHES {
120        let mut start = 0;
121        while let Some(pos) = lower[start..].find(cliche) {
122            let offset = start + pos;
123            instances.push(ClicheInstance {
124                phrase: text[offset..offset + cliche.len()].to_string(),
125                offset,
126                canonical: cliche.to_string(),
127            });
128            start = offset + cliche.len();
129        }
130    }
131
132    // Sort by offset for consistent ordering
133    instances.sort_by_key(|i| i.offset);
134
135    let count = instances.len();
136    Ok(ClicheResult { instances, count })
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn detect_single_cliche() {
145        let result = detect_cliches("At the end of the day, we need results.").unwrap();
146        assert_eq!(result.count, 1);
147        assert_eq!(result.instances[0].canonical, "at the end of the day");
148    }
149
150    #[test]
151    fn detect_multiple_cliches() {
152        let text = "It was easier said than done, but better late than never.";
153        let result = detect_cliches(text).unwrap();
154        assert_eq!(result.count, 2);
155    }
156
157    #[test]
158    fn no_cliches() {
159        let result =
160            detect_cliches("The quantum processor achieved remarkable throughput.").unwrap();
161        assert_eq!(result.count, 0);
162    }
163
164    #[test]
165    fn case_insensitive_match() {
166        let result = detect_cliches("Think Outside The Box to solve this.").unwrap();
167        assert_eq!(result.count, 1);
168    }
169
170    #[test]
171    fn empty_text_error() {
172        let result = detect_cliches("");
173        assert!(result.is_err());
174    }
175}