use crate::{symbol_shortcut, utils};
pub(crate) fn should_skip_terminator_for_symbol(symbol: char) -> bool {
matches!(
symbol,
'.' | '?'
| '!'
| '…'
| '⋯'
| '"'
| '\''
| '”'
| '’'
| '」'
| '』'
| '〉'
| '》'
| '('
| ')'
| ']'
| '}'
| ','
| ':'
| ';'
| '―'
| '·'
)
}
pub(crate) fn should_request_continuation(symbol: char) -> bool {
matches!(
symbol,
'.' | '?'
| '!'
| '…'
| '⋯'
| '"'
| '\''
| '”'
| '’'
| '」'
| '』'
| '〉'
| '》'
| ')'
| ']'
| '}'
| ','
| ':'
| ';'
| '―'
)
}
pub(crate) fn should_force_terminator_before_symbol(symbol: char) -> bool {
matches!(symbol, '/' | '~' | '∼')
}
pub(crate) fn is_english_symbol(symbol: char) -> bool {
symbol_shortcut::is_english_symbol_char(symbol)
}
pub(crate) fn requires_single_letter_continuation(letter: char) -> bool {
letter.is_ascii_alphabetic() && !matches!(letter.to_ascii_lowercase(), 'a' | 'i' | 'o')
}
fn is_ascii_letter_or_digit(ch: Option<char>) -> bool {
ch.is_some_and(is_roman_section_letter_or_digit)
}
pub(crate) fn is_roman_section_letter_or_digit(ch: char) -> bool {
ch.is_ascii_alphanumeric() || crate::rules::korean::rule_31::is_greek_letter(ch)
}
pub(crate) fn begins_korean_mode_number(chars: impl Iterator<Item = char>) -> bool {
let mut chars = chars.peekable();
if !chars.peek().is_some_and(char::is_ascii_digit) {
return false;
}
while let Some(ch) = chars.next() {
if ch.is_ascii_digit()
|| (matches!(ch, ',' | '.') && chars.peek().is_some_and(char::is_ascii_digit))
{
continue;
}
return !(ch.is_ascii_alphabetic()
|| crate::rules::korean::rule_69::is_compatibility_unit_presentation(ch));
}
true
}
pub(crate) fn is_attached_ascii_roman_ampersand(word_chars: &[char], index: usize) -> bool {
if word_chars.get(index) != Some(&'&')
|| index == 0
|| index + 1 >= word_chars.len()
|| !word_chars[index - 1].is_ascii_alphabetic()
|| !word_chars[index + 1].is_ascii_alphabetic()
{
return false;
}
let mut start = index;
while start > 0 && (word_chars[start - 1].is_ascii_alphabetic() || word_chars[start - 1] == '&')
{
start -= 1;
}
let mut end = index + 1;
while end < word_chars.len()
&& (word_chars[end].is_ascii_alphanumeric() || word_chars[end] == '&')
{
end += 1;
}
let segment = &word_chars[start..end];
segment.first().is_some_and(|ch| ch.is_ascii_alphabetic())
&& segment.last().is_some_and(|ch| ch.is_ascii_alphanumeric())
&& segment.iter().enumerate().all(|(offset, ch)| {
*ch != '&'
|| (offset > 0
&& offset + 1 < segment.len()
&& segment[offset - 1].is_ascii_alphabetic()
&& segment[offset + 1].is_ascii_alphabetic())
})
&& (start == 0 || !word_chars[start - 1].is_ascii_alphanumeric())
&& (end == word_chars.len() || !word_chars[end].is_ascii_alphanumeric())
}
pub(crate) fn is_attached_ascii_roman_asterisk(word_chars: &[char], index: usize) -> bool {
if word_chars.get(index) != Some(&'*') || index == 0 || index + 1 >= word_chars.len() {
return false;
}
let mut start = index;
while start > 0
&& (word_chars[start - 1].is_ascii_alphanumeric() || word_chars[start - 1] == '*')
{
start -= 1;
}
let mut end = index + 1;
while end < word_chars.len()
&& (word_chars[end].is_ascii_alphanumeric() || word_chars[end] == '*')
{
end += 1;
}
let segment = &word_chars[start..end];
segment
.split(|ch| *ch == '*')
.all(|part| !part.is_empty() && part.iter().any(|ch| ch.is_ascii_alphabetic()))
&& segment.first().is_some_and(|ch| ch.is_ascii_alphabetic())
&& (start == 0 || !word_chars[start - 1].is_ascii_alphanumeric())
&& (end == word_chars.len() || !word_chars[end].is_ascii_alphanumeric())
}
pub(crate) fn is_ampersand_before_attached_ascii_roman_segment(
word_chars: &[char],
index: usize,
) -> bool {
if word_chars.get(index) != Some(&'&')
|| !word_chars
.get(index + 1)
.is_some_and(|ch| ch.is_ascii_alphabetic())
|| index
.checked_sub(1)
.and_then(|i| word_chars.get(i))
.is_some_and(|previous| previous.is_ascii_alphanumeric() || *previous == '&')
{
return false;
}
let mut end = index + 1;
while word_chars
.get(end)
.is_some_and(|ch| ch.is_ascii_alphanumeric())
{
end += 1;
}
word_chars
.get(end)
.is_none_or(|next| !next.is_ascii_alphanumeric())
}
fn is_digital_notation_symbol(symbol: char) -> bool {
matches!(symbol, '/' | '@' | '#' | '.' | '_' | ':')
}
fn has_digital_notation_signature(word_chars: &[char]) -> bool {
let text: String = word_chars.iter().collect();
if text.contains("//") || text.contains('@') || text.contains('#') {
return true;
}
text.contains('_') && (text.contains('.') || text.contains('/') || text.contains(':'))
}
pub(crate) fn prev_ascii_letter_or_digit(word_chars: &[char], index: usize) -> bool {
let mut j = index;
while j > 0 {
let ch = word_chars[j - 1];
if is_roman_section_letter_or_digit(ch) {
return true;
}
if symbol_shortcut::is_english_symbol_char(ch) {
j -= 1;
continue;
}
break;
}
false
}
pub(crate) fn next_ascii_letter_or_digit(
word_chars: &[char],
index: usize,
remaining_words: &[&str],
) -> bool {
let mut j = index + 1;
while j < word_chars.len() {
let ch = word_chars[j];
if is_roman_section_letter_or_digit(ch) {
return true;
}
if symbol_shortcut::is_english_symbol_char(ch) {
j += 1;
continue;
}
return false;
}
for word in remaining_words {
for ch in word.chars() {
if is_roman_section_letter_or_digit(ch) {
return true;
}
if symbol_shortcut::is_english_symbol_char(ch) {
continue;
}
return false;
}
}
false
}
fn closed_parenthesis_is_korean_punctuation(
word_chars: &[char],
index: usize,
remaining_words: &[&str],
) -> bool {
let mut depth = 1usize;
let mut contains_korean = false;
let mut contains_roman_letter = false;
let tail = word_chars
.iter()
.skip(index + 1)
.copied()
.chain(remaining_words.iter().flat_map(|word| word.chars()));
for ch in tail {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return contains_korean || !contains_roman_letter;
}
}
_ if utils::is_korean_char(ch) => contains_korean = true,
_ if ch.is_ascii_alphabetic() => contains_roman_letter = true,
_ => {}
}
}
false
}
pub(crate) fn closed_parenthesis_encloses_roman_word(
word_chars: &[char],
index: usize,
remaining_words: &[&str],
) -> bool {
let mut depth = 1usize;
let mut roman_letters = 0usize;
let tail = word_chars
.iter()
.skip(index + 1)
.copied()
.chain(remaining_words.iter().flat_map(|word| word.chars()));
for ch in tail {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return roman_letters >= 2;
}
}
_ if ch.is_ascii_alphabetic() => roman_letters += 1,
_ => {}
}
}
false
}
pub(crate) fn closed_parenthesis_continues_into_roman(word_chars: &[char], index: usize) -> bool {
let mut depth = 1usize;
for (offset, ch) in word_chars.iter().enumerate().skip(index + 1) {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return word_chars
.get(offset + 1)
.is_some_and(char::is_ascii_alphabetic);
}
}
_ => {}
}
}
false
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn should_render_symbol_as_english(
english_indicator: bool,
is_english: bool,
is_english_majority: bool,
parenthesis_stack: &[bool],
symbol: char,
word_chars: &[char],
index: usize,
remaining_words: &[&str],
) -> bool {
if !english_indicator {
return false;
}
let prev_char = if index > 0 {
Some(word_chars[index - 1])
} else {
None
};
let next_char = if index + 1 < word_chars.len() {
Some(word_chars[index + 1])
} else {
remaining_words.first().and_then(|w| w.chars().next())
};
if !is_english && prev_char.is_some_and(|ch| matches!(ch, ')' | ']' | '}')) {
return false;
}
match symbol {
'(' => {
let after_closing_quote = !is_english_majority
&& prev_char.is_some_and(|ch| matches!(ch, '\u{2019}' | '\u{201d}'));
(is_english_majority
|| !closed_parenthesis_is_korean_punctuation(word_chars, index, remaining_words))
&& is_ascii_letter_or_digit(next_char)
&& !after_closing_quote
&& (!prev_char.is_some_and(utils::is_korean_char)
|| closed_parenthesis_continues_into_roman(word_chars, index))
}
')' => parenthesis_stack.last().copied().unwrap_or(false),
'&' => {
is_attached_ascii_roman_ampersand(word_chars, index)
|| (is_english
&& prev_char.is_some_and(|ch| ch.is_ascii_alphabetic())
&& word_chars
.get(index + 1)
.is_some_and(|ch| utils::is_korean_char(*ch)))
}
'*' => is_attached_ascii_roman_asterisk(word_chars, index),
'®' | '™' => is_english && prev_char.is_some_and(|ch| ch.is_ascii_alphanumeric()),
'\'' | '\u{2019}' => {
prev_char.is_some_and(|ch| ch.is_ascii_alphabetic())
&& word_chars
.get(index + 1)
.is_some_and(|ch| ch.is_ascii_alphabetic())
}
'…' => {
is_english
&& (next_ascii_letter_or_digit(word_chars, index, remaining_words)
|| matches!(next_char, Some(')' | ']' | '}' | '”' | '’' | '」' | '』')))
}
',' => {
if !is_english {
return false;
}
let next_item_is_korean_mode_number = if index + 1 < word_chars.len() {
begins_korean_mode_number(word_chars[index + 1..].iter().copied())
} else {
remaining_words
.first()
.is_some_and(|word| begins_korean_mode_number(word.chars()))
};
if next_item_is_korean_mode_number {
return false;
}
let prev_roman = prev_ascii_letter_or_digit(word_chars, index)
|| prev_char
.is_some_and(crate::rules::korean::rule_69::is_compatibility_unit_presentation);
let next_roman = next_ascii_letter_or_digit(word_chars, index, remaining_words)
|| next_char
.is_some_and(crate::rules::korean::rule_69::is_compatibility_unit_presentation);
prev_roman && next_roman
}
'-' => {
let prev_ascii = prev_ascii_letter_or_digit(word_chars, index);
let next_ascii = next_ascii_letter_or_digit(word_chars, index, remaining_words);
let roman_started_before_hyphen = word_chars[..index]
.iter()
.rev()
.take_while(|ch| ch.is_ascii_alphanumeric() || **ch == '-')
.any(|ch| ch.is_ascii_alphabetic());
(prev_ascii && next_ascii && (is_english || roman_started_before_hyphen))
|| (is_english
&& crate::rules::token_rules::math_expression::is_roman_minus_grade(word_chars))
}
'.' if word_chars[..index]
.last()
.is_some_and(|ch| ch.is_ascii_digit())
&& word_chars
.get(index + 1)
.is_some_and(|ch| ch.is_ascii_uppercase()) =>
{
false
}
'/' | '@' | '#' | '.' | '_' | ':' => {
let prev_ascii = prev_ascii_letter_or_digit(word_chars, index);
let next_ascii = next_ascii_letter_or_digit(word_chars, index, remaining_words);
if symbol == '/'
&& !is_english
&& !word_chars[..index].iter().any(char::is_ascii_alphabetic)
{
return false;
}
(prev_ascii && next_ascii)
|| (symbol == ':' && is_english && word_chars == [':'] && next_ascii)
|| (symbol == ':'
&& is_english
&& prev_ascii
&& matches!(next_char, Some(')' | ']' | '}')))
|| (symbol == '/' && prev_char == Some('/') && next_ascii)
|| (symbol == '/' && next_char == Some('/') && prev_ascii)
}
_ => false,
}
}
pub(crate) fn should_keep_english_mode_for_symbol(
symbol: char,
word_chars: &[char],
index: usize,
remaining_words: &[&str],
) -> bool {
if !is_digital_notation_symbol(symbol) || !has_digital_notation_signature(word_chars) {
return false;
}
if word_chars[index + 1..]
.iter()
.any(|ch| ch.is_ascii_alphanumeric())
{
return true;
}
should_render_symbol_as_english(
true,
true,
false,
&[],
symbol,
word_chars,
index,
remaining_words,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case::lowercase_b_requires('b', true)]
#[case::lowercase_a_excluded('a', false)]
#[case::uppercase_excluded('A', false)]
fn requires_single_letter_continuation_distinguishes_letters(
#[case] ch: char,
#[case] expected: bool,
) {
assert_eq!(requires_single_letter_continuation(ch), expected);
}
#[test]
fn skip_and_force_terminator_sets_are_separate() {
for symbol in ['.', '?', '!', ')', ']', ','] {
assert!(should_skip_terminator_for_symbol(symbol));
}
for symbol in ['/', '~'] {
assert!(should_force_terminator_before_symbol(symbol));
assert!(!should_skip_terminator_for_symbol(symbol));
}
assert!(!should_force_terminator_before_symbol('-'));
assert!(should_request_continuation('.'));
assert!(!should_request_continuation('('));
}
#[rstest::rstest]
#[case('(', true)]
#[case(')', true)]
#[case(',', true)]
#[case('?', false)]
fn english_symbol_detection_matches_lookup_table(#[case] ch: char, #[case] expected: bool) {
assert_eq!(is_english_symbol(ch), expected);
}
#[rstest::rstest]
#[case::skip_english_symbol_to_ascii("A(,B", 2, true)]
#[case::korean_neighbor_blocks("가,", 1, false)]
fn prev_ascii_letter_or_digit_skips_english_symbols(
#[case] input: &str,
#[case] idx: usize,
#[case] expected: bool,
) {
let word: Vec<char> = input.chars().collect();
assert_eq!(prev_ascii_letter_or_digit(&word, idx), expected);
}
#[rstest::rstest]
#[case::contiguous_ascii("A,B", 1, &[], true)]
#[case::skip_english_symbol("A,(B", 1, &[], true)]
#[case::remaining_word_ascii("A,", 1, &["B"], true)]
#[case::hangul_following("A,가", 1, &[], false)]
#[case::remaining_word_with_symbol_then_ascii("A,", 1, &["(B"], true)]
#[case::remaining_word_only_symbols("A,", 1, &["()"], false)]
fn next_ascii_letter_or_digit_checks_future_ascii(
#[case] input: &str,
#[case] idx: usize,
#[case] remaining: &[&str],
#[case] expected: bool,
) {
let word: Vec<char> = input.chars().collect();
assert_eq!(next_ascii_letter_or_digit(&word, idx, remaining), expected);
}
#[rstest::rstest]
#[case::pure_roman("(Hello)", 0, &[], true, false, false, true)]
#[case::korean_before("가(", 1, &["A)"], true, false, false, false)]
#[case::indicator_disabled("(Hello)", 0, &[], false, false, false, false)]
#[case::official_rule_46_shape("BMI(체질량", 3, &["지수)"], true, true, false, false)]
#[case::roman_then_korean_body(
"SDV(Software",
3,
&["Defined", "Vehicle,", "소프트웨어", "중심)"],
true,
true,
false,
false
)]
#[case::pure_roman_body("ABC(def)", 3, &[], true, true, false, true)]
#[case::pure_number_body("BSI(73)", 3, &[], true, true, false, false)]
#[case::unclosed_body("ABC(def", 3, &["한글"], true, true, false, true)]
#[case::korean_head_roman_continues("폐쇄회로(CC)TV와", 4, &[], true, false, false, true)]
#[case::korean_head_korean_follows("링컨(Lincoln)은", 2, &[], true, false, false, false)]
#[case::nested_korean_body(
"BIT(BT(바이오)+IT(정보))",
3,
&[],
true,
true,
false,
false
)]
#[case::rule_39_english_majority("(Korean:", 0, &["반찬)"], true, true, true, true)]
#[case::after_closing_quote("’(Motor", 1, &[], true, true, false, false)]
#[case::after_closing_quote_english_majority("’(Motor", 1, &[], true, true, true, true)]
fn should_render_symbol_as_english_for_opening_parenthesis(
#[case] input: &str,
#[case] index: usize,
#[case] remaining_words: &[&str],
#[case] english_indicator: bool,
#[case] is_english: bool,
#[case] is_english_majority: bool,
#[case] expected: bool,
) {
let word = input.chars().collect::<Vec<_>>();
assert_eq!(
should_render_symbol_as_english(
english_indicator,
is_english,
is_english_majority,
&[],
'(',
&word,
index,
remaining_words,
),
expected,
);
}
#[rstest::rstest]
#[case::stack_top_true(true, true)]
#[case::stack_top_false(false, false)]
fn should_render_symbol_as_english_for_closing_parenthesis(
#[case] stack_top: bool,
#[case] expected: bool,
) {
let closer: Vec<char> = ")".chars().collect();
assert_eq!(
should_render_symbol_as_english(true, true, false, &[stack_top], ')', &closer, 0, &[],),
expected,
);
}
#[rstest::rstest]
#[case::both_ascii_in_english_mode("A,B", true, true)]
#[case::compatibility_unit_in_english_mode("㎿,30㎿", true, true)]
#[case::not_in_english_mode("A,B", false, false)]
#[case::korean_neighbor("가,B", true, false)]
fn should_render_symbol_as_english_for_comma_requires_ascii_neighbors(
#[case] input: &str,
#[case] is_english: bool,
#[case] expected: bool,
) {
let word: Vec<char> = input.chars().collect();
assert_eq!(
should_render_symbol_as_english(true, is_english, false, &[], ',', &word, 1, &[],),
expected
);
}
#[rstest::rstest]
#[case::roman_led_chain("CV3-AD685", 3, false, true)]
#[case::roman_led_numeric_chain("N-79-20", 4, false, true)]
#[case::number_led_word("0-Zone", 1, false, false)]
#[case::number_led_suffix("777-300ER", 3, false, false)]
#[case::after_closed_enclosure("(GTX)-C", 5, false, false)]
fn hyphen_enters_roman_punctuation_only_after_a_roman_run(
#[case] input: &str,
#[case] index: usize,
#[case] is_english: bool,
#[case] expected: bool,
) {
let chars = input.chars().collect::<Vec<_>>();
assert_eq!(
should_render_symbol_as_english(true, is_english, false, &[], '-', &chars, index, &[],),
expected
);
}
#[test]
fn punctuation_after_closed_korean_enclosure_does_not_reenter_roman_mode() {
let chars = "(XBB).1.5".chars().collect::<Vec<_>>();
assert!(!should_render_symbol_as_english(
true,
false,
false,
&[false],
'.',
&chars,
5,
&[],
));
}
#[rstest::rstest]
#[case::digit_led_korean("2000년대", false)]
#[case::grouped_digit_led_korean("2,000년대", false)]
#[case::decimal_digit_led_korean("3.5년", false)]
#[case::pure_number("2000", false)]
#[case::number_led_roman_item("1998b", true)]
#[case::roman_word("Beta", true)]
#[case::roman_led_mixed_word("LG유플러스", true)]
#[case::numeric_roman_unit_before_korean_particle("68kg의", true)]
fn comma_before_next_word_uses_narrow_digit_led_korean_context(
#[case] next_word: &str,
#[case] expected: bool,
) {
let word = ['A', ','];
assert_eq!(
should_render_symbol_as_english(true, true, false, &[], ',', &word, 1, &[next_word],),
expected
);
}
#[rstest::rstest]
#[case::official_name("O'Hara", true)]
#[case::official_contraction("DON'T", true)]
#[case::official_possessive("THAT'S", true)]
#[case::detached_open("'word", false)]
#[case::detached_close("word'", false)]
#[case::measurement("6'2", false)]
fn internal_apostrophe_requires_ascii_letters_on_both_sides(
#[case] input: &str,
#[case] expected: bool,
) {
let word = input.chars().collect::<Vec<_>>();
let index = word.iter().position(|ch| *ch == '\'').unwrap();
assert_eq!(
should_render_symbol_as_english(true, true, false, &[], '\'', &word, index, &[],),
expected,
);
}
#[test]
fn apostrophe_does_not_join_the_next_whitespace_delimited_word() {
let word = "Guitar'".chars().collect::<Vec<_>>();
assert!(!should_render_symbol_as_english(
true,
true,
false,
&[],
'\'',
&word,
word.len() - 1,
&["Listening"],
));
}
#[rstest::rstest]
#[case::official_at_and_t("AT&T", true, true)]
#[case::official_b_and_b("B&B", true, true)]
#[case::spaced("A & B", true, false)]
#[case::hangul_left("가&B", true, false)]
#[case::hangul_right("A&나", true, true)]
#[case::digit_neighbor("3&B", true, false)]
#[case::digit_outer_left("3A&B", true, false)]
#[case::rule35_digit_suffix("A&B3", true, true)]
#[case::rule35_digit_then_roman_suffix("A&B3C", true, true)]
#[case::ampersand_after_digit("A&B3&C", true, false)]
#[case::multiple_ampersands("A&B&C", true, true)]
#[case::empty_segment("A&&B", true, false)]
#[case::no_roman_indicator("AT&T", false, false)]
fn attached_ampersand_requires_complete_ascii_roman_run(
#[case] input: &str,
#[case] english_indicator: bool,
#[case] expected: bool,
) {
let word: Vec<char> = input.chars().collect();
let index = word.iter().position(|ch| *ch == '&').unwrap();
assert_eq!(
should_render_symbol_as_english(
english_indicator,
true,
false,
&[],
'&',
&word,
index,
&[],
),
expected,
);
}
#[rstest::rstest]
#[case::official_mash_first("M*A*S*H", 1, true, true)]
#[case::official_mash_middle("M*A*S*H", 3, true, true)]
#[case::official_mash_last("M*A*S*H", 5, true, true)]
#[case::roman_number_chain("A1*B2", 2, true, true)]
#[case::number_led("2*A", 1, true, false)]
#[case::digit_only_right_segment("A*2", 1, true, false)]
#[case::empty_segment("A**B", 1, true, false)]
#[case::hangul_segment("가*A", 1, true, false)]
#[case::detached("A * B", 2, true, false)]
#[case::no_roman_indicator("M*A*S*H", 1, false, false)]
fn attached_asterisk_requires_complete_ascii_roman_segments(
#[case] input: &str,
#[case] index: usize,
#[case] english_indicator: bool,
#[case] expected: bool,
) {
let word = input.chars().collect::<Vec<_>>();
assert_eq!(
should_render_symbol_as_english(
english_indicator,
true,
false,
&[],
'*',
&word,
index,
&[],
),
expected,
);
}
#[rstest::rstest]
#[case::official_and_c("&c", true)]
#[case::official_at_and_t("AT&T", false)]
#[case::official_b_and_b("B&B", false)]
#[case::official_spaced("Marks & Spencer", false)]
#[case::rule35_digit_suffix("&P500", true)]
#[case::digit_without_roman_segment("&500", false)]
fn one_sided_ampersand_requires_complete_right_roman_segment(
#[case] input: &str,
#[case] expected: bool,
) {
let word = input.chars().collect::<Vec<_>>();
let index = word.iter().rposition(|ch| *ch == '&').unwrap();
assert_eq!(
is_ampersand_before_attached_ascii_roman_segment(&word, index),
expected,
);
}
#[rstest::rstest]
#[case::double_slash("http://example.com", true)]
#[case::at_sign("user@host", true)]
#[case::hash("tag#name", true)]
#[case::underscore_plus_dot("a_b.c", true)]
#[case::pure_underscore("a_b", false)]
fn digital_notation_signature_strong_markers(#[case] input: &str, #[case] expected: bool) {
let chars: Vec<char> = input.chars().collect();
assert_eq!(
super::has_digital_notation_signature(&chars),
expected,
"input={input:?}"
);
}
#[test]
fn should_keep_english_mode_for_symbol_passes_through() {
let chars: Vec<char> = "user@host.com".chars().collect();
let _ = super::should_keep_english_mode_for_symbol('@', &chars, 4, &[]);
}
}
#[cfg(test)]
mod enclosure_route_coverage {
use super::*;
#[rstest::rstest]
#[case::korean_body(&['(', '체', '질', '량', ')'], true)]
#[case::digits_only(&['(', '7', '3', ')'], true)]
#[case::roman_body(&['(', 'd', 'e', 'f', ')'], false)]
#[case::nested_roman(&['(', '(', 'd', ')', 'e', ')'], false)]
#[case::never_closes(&['(', 'd', 'e', 'f'], false)]
#[case::nothing_follows(&['('], false)]
fn closed_enclosure_is_korean_unless_its_body_is_roman(
#[case] word: &[char],
#[case] expected: bool,
) {
assert_eq!(
closed_parenthesis_is_korean_punctuation(word, 0, &[]),
expected
);
}
#[test]
fn an_enclosure_closing_in_a_later_word_is_still_scanned() {
assert!(closed_parenthesis_is_korean_punctuation(
&['(', 'A'],
0,
&["체질량)"]
));
}
}
#[cfg(test)]
mod symbol_route_coverage {
use super::*;
#[rstest::rstest]
#[case::korean_body(&['(', '체', '질', '량', ')'], true)]
#[case::digits_only(&['(', '7', '3', ')'], true)]
#[case::roman_body(&['(', 'd', 'e', 'f', ')'], false)]
#[case::nested_roman(&['(', '(', 'd', ')', 'e', ')'], false)]
#[case::never_closes(&['(', 'd', 'e', 'f'], false)]
#[case::nothing_follows(&['('], false)]
fn a_closed_enclosure_is_korean_unless_its_body_is_roman(
#[case] word: &[char],
#[case] expected: bool,
) {
assert_eq!(
closed_parenthesis_is_korean_punctuation(word, 0, &[]),
expected
);
}
#[test]
fn an_enclosure_closing_in_a_later_word_is_still_scanned() {
assert!(closed_parenthesis_is_korean_punctuation(
&['(', 'A'],
0,
&["체질량)"]
));
}
#[rstest::rstest]
#[case::after_roman_letter("그는 Jeep\u{00AE} 를")]
#[case::after_roman_letter_tm("그는 Line\u{2122} 을")]
#[case::after_korean("그는 지프\u{00AE} 를")]
#[case::standalone("그는 \u{00AE} 를")]
fn a_trademark_sign_encodes(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod digital_notation_coverage {
#[rstest::rstest]
#[case::digits_then_unit("가나 17.1/km, 다라", "⠼⠁⠛⠲⠁⠸⠌⠴⠅⠍⠐")]
#[case::digit_groups("가나 16/32/64GB 다라", "⠼⠁⠋⠸⠌⠼⠉⠃⠸⠌⠼⠋⠙⠴⠠⠠⠛⠃⠲")]
#[case::roman_on_the_left("가나 A/B 다라", "⠴⠠⠁⠸⠌⠠⠃⠲")]
#[case::web_address("가나 www.a.kr 다라", "⠴⠺⠺⠺⠲⠁⠲⠅⠗⠲")]
fn a_slash_after_digits_opens_no_roman_section(
#[case] input: &str,
#[case] expected_segment: &str,
) {
let actual = crate::encode_to_unicode(input).unwrap();
assert!(
actual.contains(expected_segment),
"missing slash run {expected_segment:?} in {actual:?}"
);
}
#[rstest::rstest]
#[case::address_continues("그는 https://a.b 를")]
#[case::separator_ends_the_word("그는 https:// 를")]
#[case::mail_address("그는 a@b.c 를")]
fn a_digital_address_encodes(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod roman_word_enclosure_coverage {
use super::*;
#[rstest::rstest]
#[case::two_letters(&['(', 'P', 'F', ')'], true)]
#[case::one_letter(&['(', 'a', ')'], false)]
#[case::nested(&['(', '(', 'A', 'B', ')', ')'], true)]
#[case::never_closes(&['(', 'A', 'B'], false)]
#[case::digits_only(&['(', '7', '3', ')'], false)]
fn an_enclosure_holds_a_roman_word_only_with_two_letters(
#[case] word: &[char],
#[case] expected: bool,
) {
assert_eq!(
closed_parenthesis_encloses_roman_word(word, 0, &[]),
expected
);
}
#[rstest::rstest]
#[case::address_continues(&['h','t','t','p','s',':','/','/','a','.','b'], 6, true)]
#[case::separator_ends_the_word(&['h','t','t','p','s',':','/','/'], 7, false)]
#[case::not_a_notation_symbol(&['a','b','c'], 1, false)]
fn a_digital_address_separator_keeps_english_mode(
#[case] word: &[char],
#[case] index: usize,
#[case] expected: bool,
) {
assert_eq!(
should_keep_english_mode_for_symbol(word[index], word, index, &[]),
expected
);
}
}