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}