sarcasm_utils/decode.rs
1use crate::StartingCase;
2use itertools::Itertools;
3use log::{debug, info, trace};
4
5#[inline]
6fn different_case((l, r): (char, char)) -> bool {
7 let result = if l.is_lowercase() {
8 r.is_uppercase()
9 } else {
10 r.is_lowercase()
11 };
12 trace!(
13 "Characters {} and {} are {}",
14 l,
15 r,
16 if result { "different cases" } else { "the same case" }
17 );
18
19 result
20}
21
22/// Value returned by [`is_sarcasm`] determining if the input is SaRcAsM.
23#[derive(Copy, Clone, Debug, PartialEq)]
24pub enum IsSarcasm {
25 /// Input text is SaRcAsM. Contains the case of the first letter of the SaRcAsM text.
26 Yes(StartingCase),
27 /// Input text is either normal text or malformed SaRcAsM text.
28 No,
29 /// Input is either empty or contains only non-alphanumeric characters.
30 TooShort,
31}
32
33/// Determines if input is SaRcAsM and what the case of the first letter is.
34///
35/// All non-alphanumeric characters will be ignored in the input. As a result,
36/// the SaRcAsM pattern only has to hold within groups of alphanumeric characters.
37/// This means the starting letter of any "secondary" alphanumeric groups can be any case.
38///
39/// While the encoder is well defined in the capitalization throughout the whole string,
40/// the decoder needs to be able to deal with many possible styles of writing SaRcAsM text.
41///
42/// # Edge Cases
43///
44/// - `AbC DeF` and `AbC dEf` -> SaRcAsM.
45/// - `Ab-Cd` and `Ab-cD` -> SaRcAsM.
46/// - `A` -> SaRcAsM.
47/// - `!!` -> Too Short (no alphanumeric characters to judge).
48/// - `A!A` and `A!a` -> SaRcAsM.
49///
50/// # Arguments
51///
52/// - `input` - String slice to check for SaRcAsM.
53///
54/// # Return
55///
56/// [`IsSarcasm`] dictating if the input text was SaRcAsM.
57///
58/// # Examples
59///
60/// ```edition2018
61/// # use assert_matches::assert_matches;
62/// # use sarcasm_utils::{is_sarcasm, IsSarcasm, StartingCase};
63/// assert_matches!(is_sarcasm("AbC"), IsSarcasm::Yes(StartingCase::Uppercase));
64/// assert_matches!(is_sarcasm("aBc"), IsSarcasm::Yes(StartingCase::Lowercase));
65///
66/// assert_matches!(is_sarcasm("Abc"), IsSarcasm::No);
67/// assert_matches!(is_sarcasm("aBC"), IsSarcasm::No);
68///
69/// assert_matches!(is_sarcasm(""), IsSarcasm::TooShort);
70/// assert_matches!(is_sarcasm("!!"), IsSarcasm::TooShort);
71/// ```
72pub fn is_sarcasm(input: &str) -> IsSarcasm {
73 info!("Checking sarcasm on {} bytes", input.len());
74 trace!("Checking sarcasm on input: {}", input);
75
76 let mut iter = input.chars().skip_while(|c| !c.is_alphabetic()).peekable();
77 match iter.peek() {
78 Some(&c) => {
79 trace!("Iterator has at least one alphanumeric character");
80 let grouped = iter.group_by(|c| c.is_alphabetic());
81 let grouped_filtered = grouped.into_iter().filter(|(key, _group)| *key).map(|(_, group)| group);
82 let sarcasm: bool = grouped_filtered
83 .map(|g| g.tuple_windows().all(different_case))
84 .enumerate()
85 .all(|(i, b)| {
86 if b {
87 debug!("Alphanumeric group {} is sarcasm", i);
88 } else {
89 debug!("Alphanumeric group {} is not sarcasm", i);
90 }
91 b
92 });
93
94 if sarcasm {
95 info!("Input is sarcasm.");
96 IsSarcasm::Yes(StartingCase::from(c))
97 } else {
98 info!("Input is not sarcasm.");
99 IsSarcasm::No
100 }
101 }
102 None => {
103 if input.is_empty() {
104 info!("Cannot determine sarcasm: input string empty");
105 } else {
106 info!("Cannot determine sarcasm: input only contains non-alphanumeric characters");
107 }
108 IsSarcasm::TooShort
109 }
110 }
111}