use super::{AfFormat, Keystroke, KeystrokeResult};
use crate::error::AlertMessage;
use crate::parse::{is_ascii_alnum, is_ascii_alpha, is_decimal_digit};
use crate::printx::util_printx;
const CALLER: &str = "AFSpecial_KeystrokeEx";
#[must_use]
pub fn mask_satisfied(change: char, mask: char) -> bool {
match mask {
'9' => is_decimal_digit(change),
'A' => is_ascii_alpha(change),
'O' => is_ascii_alnum(change),
'X' => true,
_ => change == mask,
}
}
#[must_use]
pub fn is_reserved_mask_char(ch: char) -> bool {
matches!(ch, '9' | 'A' | 'O' | 'X')
}
#[must_use]
pub fn af_special_format(value: &str, kind: i32) -> AfFormat {
const PHONE_PROBE: &str = "9999999999";
let format = match kind {
0 => "99999",
1 => "99999-9999",
2 => {
if util_printx(PHONE_PROBE, value).chars().count() >= 10 {
"(999) 999-9999"
} else {
"999-9999"
}
}
3 => "999-99-9999",
_ => "",
};
AfFormat::formatted(util_printx(format, value))
}
#[must_use]
pub fn af_special_keystroke_ex(event: &Keystroke, mask: &str) -> KeystrokeResult {
if mask.is_empty() {
return KeystrokeResult::accept();
}
let mask_chars: Vec<char> = mask.chars().collect();
let val_chars: Vec<char> = event.value.chars().collect();
if event.will_commit {
if val_chars.is_empty() {
return KeystrokeResult::accept();
}
if val_chars.len() > mask_chars.len() {
return KeystrokeResult::reject_with(CALLER, AlertMessage::TooLong);
}
let matched = val_chars
.iter()
.zip(mask_chars.iter())
.take_while(|(v, m)| mask_satisfied(**v, **m))
.count();
if matched == mask_chars.len() {
return KeystrokeResult::accept();
}
return KeystrokeResult::reject_with(CALLER, AlertMessage::InvalidInput);
}
let mut change_chars: Vec<char> = event.change.chars().collect();
if change_chars.is_empty() {
return KeystrokeResult::accept();
}
let Ok(mut mask_index) = usize::try_from(event.sel_start) else {
return KeystrokeResult::reject_with(CALLER, AlertMessage::TooLong);
};
let replaced = usize::try_from(event.sel_end.saturating_sub(event.sel_start)).unwrap_or(0);
let combined_len = val_chars
.len()
.saturating_add(change_chars.len())
.saturating_sub(replaced);
if combined_len > mask_chars.len() || mask_index >= mask_chars.len() {
return KeystrokeResult::reject_with(CALLER, AlertMessage::TooLong);
}
for slot in &mut change_chars {
let Some(w_mask) = mask_chars.get(mask_index).copied() else {
return KeystrokeResult::reject_with(CALLER, AlertMessage::TooLong);
};
if !is_reserved_mask_char(w_mask) {
*slot = w_mask;
}
if !mask_satisfied(*slot, w_mask) {
return KeystrokeResult::reject();
}
mask_index = mask_index.saturating_add(1);
}
KeystrokeResult::accept_change(change_chars.into_iter().collect::<String>())
}
#[must_use]
pub fn af_special_keystroke(event: &Keystroke, kind: i32) -> KeystrokeResult {
const LOCAL_PHONE_LEN: usize = 7;
let format = match kind {
0 => "99999",
1 | 3 => "999999999",
2 => {
let typed = event.value.chars().count() + event.change.chars().count();
if typed > LOCAL_PHONE_LEN {
"9999999999"
} else {
"9999999"
}
}
_ => "",
};
af_special_keystroke_ex(event, format)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::af::AfOutcome;
fn fmt(value: &str, kind: i32) -> String {
match af_special_format(value, kind).outcome {
AfOutcome::Formatted(s) => s,
other => unreachable!("AFSpecial_Format answered {other:?}"),
}
}
#[test]
fn special_format_transcript_lines() {
assert_eq!(fmt("", 0), "");
assert_eq!(fmt("", 1), "-");
assert_eq!(fmt("", 2), "-");
assert_eq!(fmt("", 3), "--");
assert_eq!(fmt("0123456789", 0), "01234");
assert_eq!(fmt("0123456789", 1), "01234-5678");
assert_eq!(fmt("0123456789", 2), "(012) 345-6789");
assert_eq!(fmt("0123456789", 3), "012-34-5678");
}
#[test]
fn the_phone_mask_is_chosen_by_a_digit_probe() {
assert_eq!(fmt("5551212", 2), "555-1212");
assert_eq!(fmt("5551212345", 2), "(555) 121-2345");
assert_eq!(fmt("(555) 121-2345", 2), "(555) 121-2345");
}
#[test]
fn an_unknown_selector_blanks_the_value() {
assert_eq!(fmt("0123456789", 9), "");
assert_eq!(fmt("0123456789", -1), "");
}
#[test]
fn the_four_placeholders() {
assert!(mask_satisfied('5', '9'));
assert!(!mask_satisfied('a', '9'));
assert!(mask_satisfied('a', 'A'));
assert!(mask_satisfied('Z', 'A'));
assert!(!mask_satisfied('1', 'A'));
assert!(mask_satisfied('1', 'O'));
assert!(mask_satisfied('a', 'O'));
assert!(!mask_satisfied('-', 'O'));
for anything in ['-', ' ', '5', 'q', '\u{e9}'] {
assert!(mask_satisfied(anything, 'X'), "{anything:?}");
}
assert!(mask_satisfied('-', '-'));
assert!(!mask_satisfied('x', '-'));
for ch in ['9', 'A', 'O', 'X'] {
assert!(is_reserved_mask_char(ch), "{ch}");
}
for ch in ['-', '(', '8', 'B'] {
assert!(!is_reserved_mask_char(ch), "{ch}");
}
}
#[test]
fn keystroke_ex_commit_transcript_lines() {
let cases: &[(&str, &str, Option<&str>)] = &[
("12345", "", None),
(
"123",
"9999",
Some("AFSpecial_KeystrokeEx[icon=3,type=0]: The input value is invalid."),
),
(
"12345",
"9999",
Some("AFSpecial_KeystrokeEx[icon=3,type=0]: The input value is too long."),
),
(
"abcd",
"9999",
Some("AFSpecial_KeystrokeEx[icon=3,type=0]: The input value is invalid."),
),
("1234", "9999", None),
("abcd", "XXXX", None),
];
for &(value, mask, expected) in cases {
let out = af_special_keystroke_ex(&Keystroke::commit(value), mask);
assert_eq!(
out.alert().map(ToString::to_string).as_deref(),
expected,
"{value:?} against {mask:?}"
);
assert_eq!(out.is_reject(), expected.is_some(), "{value:?}");
}
}
#[test]
fn a_short_value_fails_on_commit() {
let out = af_special_keystroke_ex(&Keystroke::commit("1"), "9999");
assert!(out.is_reject());
assert!(!af_special_keystroke_ex(&Keystroke::commit(""), "9999").is_reject());
}
#[test]
fn a_literal_in_the_mask_overwrites_the_keystroke() {
let out = af_special_keystroke_ex(&Keystroke::insert("01234", "5", 5), "99999-9999");
assert_eq!(out.change(), Some("-"));
let out = af_special_keystroke_ex(&Keystroke::insert("0123", "4", 4), "99999-9999");
assert_eq!(out.change(), Some("4"));
}
#[test]
fn too_long_notifies_and_a_bad_character_does_not() {
let out = af_special_keystroke_ex(&Keystroke::insert("1234", "5", 4), "9999");
assert!(out.is_reject());
assert_eq!(
out.alert().map(ToString::to_string).as_deref(),
Some("AFSpecial_KeystrokeEx[icon=3,type=0]: The input value is too long.")
);
let out = af_special_keystroke_ex(&Keystroke::insert("12", "a", 2), "9999");
assert!(out.is_reject());
assert!(out.effects.is_empty());
}
#[test]
fn a_selection_makes_room_for_the_insertion() {
let out = af_special_keystroke_ex(&Keystroke::replace("1234", "99", 0, 2), "9999");
assert_eq!(out.change(), Some("99"));
}
#[test]
fn an_empty_mask_accepts_everything() {
for event in [
Keystroke::commit("anything at all"),
Keystroke::insert("x", "y", 1),
] {
assert!(!af_special_keystroke_ex(&event, "").is_reject());
}
}
#[test]
fn the_keystroke_masks_carry_no_punctuation() {
assert!(!af_special_keystroke(&Keystroke::commit("123456789"), 1).is_reject());
assert!(af_special_keystroke(&Keystroke::commit("01234-5678"), 1).is_reject());
assert!(!af_special_keystroke(&Keystroke::commit("12345"), 0).is_reject());
assert!(!af_special_keystroke(&Keystroke::commit("123456789"), 3).is_reject());
}
#[test]
fn the_phone_keystroke_mask_grows_with_the_value() {
assert!(!af_special_keystroke(&Keystroke::commit("5551212"), 2).is_reject());
assert!(!af_special_keystroke(&Keystroke::commit("5551212345"), 2).is_reject());
}
#[test]
fn an_unknown_keystroke_selector_accepts_everything() {
assert!(!af_special_keystroke(&Keystroke::commit("abc"), 65).is_reject());
}
}