use super::token::EnglishToken;
pub fn is_wordsign_letter(c: char) -> bool {
c.is_ascii_alphabetic() && !matches!(c.to_ascii_lowercase(), 'a' | 'i' | 'o')
}
fn is_boundary(t: Option<&EnglishToken>) -> bool {
matches!(
t,
None | Some(EnglishToken::Space)
| Some(EnglishToken::Symbol('-' | '\u{2013}' | '\u{2014}'))
)
}
fn is_left_transparent(c: char) -> bool {
matches!(c, '(' | '[' | '{' | '"' | '\u{201C}' | '\u{2018}' | '\'')
}
fn is_right_transparent(c: char) -> bool {
matches!(
c,
')' | ']' | '}' | '"' | '\u{201D}' | '\u{2019}' | '.' | ',' | ':' | ';' | '\u{2026}'
)
}
fn is_contraction_suffix(w: &[char]) -> bool {
let lower: Vec<char> = w.iter().map(|c| c.to_ascii_lowercase()).collect();
matches!(
lower.as_slice(),
['s'] | ['l', 'l'] | ['r', 'e'] | ['v', 'e'] | ['d'] | ['t'] | ['m']
)
}
pub fn needs_grade1_indicator(tokens: &[EnglishToken], i: usize, explicit_english: bool) -> bool {
let Some(EnglishToken::Word(chars)) = tokens.get(i) else {
return false;
};
if chars.len() != 1 || !is_wordsign_letter(chars[0]) {
return false;
}
let prev = i.checked_sub(1).map(|p| &tokens[p]);
let next = tokens.get(i + 1);
if !explicit_english && prev.is_none() && next.is_none() {
return false;
}
let mut l = i;
while l > 0 && matches!(&tokens[l - 1], EnglishToken::Symbol(c) if is_left_transparent(*c)) {
l -= 1;
}
if !is_boundary(l.checked_sub(1).map(|p| &tokens[p])) {
return false;
}
if matches!(next, Some(EnglishToken::Symbol('\''))) {
return match tokens.get(i + 2) {
None => true,
Some(EnglishToken::Word(w)) => is_contraction_suffix(w),
Some(EnglishToken::Space) => true,
Some(EnglishToken::Symbol('.' | ',' | ':' | ';' | '!' | '?' | '\u{2026}')) => true,
_ => false,
};
}
let after_apostrophe = matches!(prev, Some(EnglishToken::Symbol('\'')));
let right_transparent =
|c: char| is_right_transparent(c) || (after_apostrophe && matches!(c, '!' | '?'));
let mut r = i;
while r + 1 < tokens.len()
&& matches!(&tokens[r + 1], EnglishToken::Symbol(c) if right_transparent(*c))
{
r += 1;
}
is_boundary(tokens.get(r + 1))
}
#[cfg(test)]
mod tests {
use super::*;
fn word(s: &str) -> EnglishToken {
EnglishToken::Word(s.chars().collect())
}
fn num(s: &str) -> EnglishToken {
EnglishToken::Number(s.chars().collect())
}
fn sym(c: char) -> EnglishToken {
EnglishToken::Symbol(c)
}
#[rstest::rstest]
#[case::after_hyphen(vec![word("b"), sym('-'), num("1")], 0, true)]
#[case::before_close_paren(vec![word("in"), EnglishToken::Space, word("b"), sym(')')], 2, true)]
#[case::free_paren(vec![sym('('), word("h"), sym(')')], 1, true)]
#[case::period_then_space(vec![word("p"), sym('.'), EnglishToken::Space, num("7")], 0, true)]
#[case::colon_ends_run(vec![word("d"), sym(':')], 0, true)]
#[case::free_quote(vec![sym('"'), word("g"), sym('"')], 1, true)]
#[case::apostrophe_suffix_ll(vec![word("X"), sym('\''), word("ll")], 0, true)]
#[case::apostrophe_suffix_s(vec![word("p"), sym('\''), word("s")], 0, true)]
#[case::apostrophe_at_end(vec![word("d"), sym('\'')], 0, true)]
#[case::apostrophe_before_terminal_punct(vec![word("t"), sym('\''), sym('?')], 0, true)]
#[case::running_text_space_bounded(vec![word("a"), EnglishToken::Space, word("b")], 2, true)]
#[case::bare_alone(vec![word("b")], 0, false)]
#[case::index_on_non_word(vec![sym('!')], 0, false)]
#[case::aio_excluded(vec![sym('('), word("i"), sym(')')], 1, false)]
#[case::factorial(vec![word("x"), sym('!')], 0, false)]
#[case::abbreviation_dot_digit(vec![word("p"), sym('.'), num("7")], 0, false)]
#[case::abbreviation_dot_letter(vec![word("U"), sym('.'), word("S"), sym('.')], 0, false)]
#[case::attached_paren(vec![word("noun"), sym('('), word("s"), sym(')')], 2, false)]
#[case::before_open_paren(vec![word("p"), sym('('), word("en"), sym(')')], 0, false)]
#[case::contraction_prefix(vec![word("d"), sym('\''), word("you")], 0, false)]
#[case::poetic_prefix(vec![word("e"), sym('\''), word("er")], 0, false)]
fn grade1_indicator_matches_2_6(
#[case] tokens: Vec<EnglishToken>,
#[case] index: usize,
#[case] expected: bool,
) {
assert_eq!(needs_grade1_indicator(&tokens, index, false), expected);
}
#[rstest::rstest]
#[case::isolated_x(vec![word("x")], 0, true)]
#[case::isolated_capital_n(vec![word("N")], 0, true)]
#[case::isolated_aio(vec![word("i")], 0, false)]
fn explicit_english_indicates_isolated_letter(
#[case] tokens: Vec<EnglishToken>,
#[case] index: usize,
#[case] expected: bool,
) {
assert_eq!(needs_grade1_indicator(&tokens, index, true), expected);
}
}