use gpui::SharedString;
#[derive(Clone, PartialEq, Debug)]
pub enum MaskToken {
Digit,
Letter,
LetterOrDigit,
Sep(char),
Any,
}
#[allow(unused)]
impl MaskToken {
pub fn is_any(&self) -> bool {
matches!(self, MaskToken::Any)
}
fn is_match(&self, ch: char) -> bool {
match self {
MaskToken::Digit => ch.is_ascii_digit(),
MaskToken::Letter => ch.is_ascii_alphabetic(),
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
MaskToken::Any => true,
MaskToken::Sep(c) => *c == ch,
}
}
fn is_sep(&self) -> bool {
matches!(self, MaskToken::Sep(_))
}
pub fn is_number(&self) -> bool {
matches!(self, MaskToken::Digit)
}
pub fn placeholder(&self) -> char {
match self {
MaskToken::Sep(c) => *c,
_ => '_',
}
}
fn mask_char(&self, ch: char) -> char {
match self {
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
MaskToken::Sep(c) => *c,
MaskToken::Any => ch,
}
}
fn unmask_char(&self, ch: char) -> Option<char> {
match self {
MaskToken::Digit => Some(ch),
MaskToken::Letter => Some(ch),
MaskToken::LetterOrDigit => Some(ch),
MaskToken::Any => Some(ch),
_ => None,
}
}
}
#[derive(Clone, Default)]
pub enum MaskPattern {
#[default]
None,
Pattern {
pattern: SharedString,
tokens: Vec<MaskToken>,
},
Number {
separator: Option<char>,
fraction: Option<usize>,
},
}
impl From<&str> for MaskPattern {
fn from(pattern: &str) -> Self {
Self::new(pattern)
}
}
impl MaskPattern {
pub fn new(pattern: &str) -> Self {
let tokens = pattern
.chars()
.map(|ch| match ch {
'9' => MaskToken::Digit,
'A' => MaskToken::Letter,
'#' => MaskToken::LetterOrDigit,
'*' => MaskToken::Any,
_ => MaskToken::Sep(ch),
})
.collect();
Self::Pattern {
pattern: pattern.to_owned().into(),
tokens,
}
}
#[allow(unused)]
fn tokens(&self) -> Option<&Vec<MaskToken>> {
match self {
Self::Pattern { tokens, .. } => Some(tokens),
Self::Number { .. } => None,
Self::None => None,
}
}
pub fn number(sep: Option<char>) -> Self {
Self::Number {
separator: sep,
fraction: None,
}
}
pub fn placeholder(&self) -> Option<String> {
match self {
Self::Pattern { tokens, .. } => {
Some(tokens.iter().map(|token| token.placeholder()).collect())
}
Self::Number { .. } => None,
Self::None => None,
}
}
pub fn is_none(&self) -> bool {
match self {
Self::Pattern { tokens, .. } => tokens.is_empty(),
Self::Number { .. } => false,
Self::None => true,
}
}
pub fn is_valid(&self, mask_text: &str) -> bool {
if self.is_none() {
return true;
}
let mut text_index = 0;
let mask_text_chars: Vec<char> = mask_text.chars().collect();
match self {
Self::Pattern { tokens, .. } => {
for token in tokens {
if text_index >= mask_text_chars.len() {
break;
}
let ch = mask_text_chars[text_index];
if token.is_match(ch) {
text_index += 1;
}
}
text_index == mask_text.len()
}
Self::Number { separator, .. } => {
if mask_text.is_empty() {
return true;
}
let mut parts = mask_text.split('.');
let int_part = parts.next().unwrap_or("");
let frac_part = parts.next();
if int_part.is_empty() {
return false;
}
let sign_positions: Vec<usize> = int_part
.chars()
.enumerate()
.filter_map(|(i, ch)| match is_sign(&ch) {
true => Some(i),
false => None,
})
.collect();
if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
return false;
}
if !int_part.chars().enumerate().all(|(i, ch)| {
ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
}) {
return false;
}
if let Some(frac) = frac_part {
if !frac
.chars()
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
{
return false;
}
}
true
}
Self::None => true,
}
}
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
if self.is_none() {
return true;
}
match self {
Self::Pattern { tokens, .. } => {
if let Some(token) = tokens.get(pos) {
if token.is_match(ch) {
return true;
}
if token.is_sep() {
if let Some(next_token) = tokens.get(pos + 1) {
if next_token.is_match(ch) {
return true;
}
}
}
}
false
}
Self::Number { .. } => true,
Self::None => true,
}
}
pub fn mask(&self, text: &str) -> SharedString {
if self.is_none() {
return text.to_owned().into();
}
match self {
Self::Number {
separator,
fraction,
} => {
if let Some(sep) = *separator {
let text = text.replace(sep, "");
let mut parts = text.split('.');
let int_part = parts.next().unwrap_or("");
let frac_part = parts.next().map(|part| {
part.chars()
.take(fraction.unwrap_or(usize::MAX))
.collect::<String>()
});
let mut chars: Vec<char> = int_part.chars().rev().collect();
let maybe_signed = if let Some(pos) = chars.iter().position(is_sign) {
Some(chars.remove(pos))
} else {
None
};
let mut result = String::new();
for (i, ch) in chars.iter().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(sep);
}
result.push(*ch);
}
let int_with_sep: String = result.chars().rev().collect();
let final_str = if let Some(frac) = frac_part {
if fraction == &Some(0) {
int_with_sep
} else {
format!("{}.{}", int_with_sep, frac)
}
} else {
int_with_sep
};
let final_str = if let Some(sign) = maybe_signed {
format!("{}{}", sign, final_str)
} else {
final_str
};
return final_str.into();
}
text.to_owned().into()
}
Self::Pattern { tokens, .. } => {
let mut result = String::new();
let mut text_index = 0;
let text_chars: Vec<char> = text.chars().collect();
for (pos, token) in tokens.iter().enumerate() {
if text_index >= text_chars.len() {
break;
}
let ch = text_chars[text_index];
if !token.is_sep() && !self.is_valid_at(ch, pos) {
break;
}
let mask_ch = token.mask_char(ch);
result.push(mask_ch);
if ch == mask_ch {
text_index += 1;
continue;
}
}
result.into()
}
Self::None => text.to_owned().into(),
}
}
pub fn unmask(&self, mask_text: &str) -> String {
match self {
Self::Number { separator, .. } => {
if let Some(sep) = *separator {
let mut result = String::new();
for ch in mask_text.chars() {
if ch == sep {
continue;
}
result.push(ch);
}
if result.contains('.') {
result = result.trim_end_matches('0').to_string();
}
return result;
}
return mask_text.to_owned();
}
Self::Pattern { tokens, .. } => {
let mut result = String::new();
let mask_text_chars: Vec<char> = mask_text.chars().collect();
for (text_index, token) in tokens.iter().enumerate() {
if text_index >= mask_text_chars.len() {
break;
}
let ch = mask_text_chars[text_index];
let unmask_ch = token.unmask_char(ch);
if let Some(ch) = unmask_ch {
result.push(ch);
}
}
result
}
Self::None => mask_text.to_owned(),
}
}
}
#[inline]
fn is_sign(ch: &char) -> bool {
matches!(ch, '+' | '-')
}
#[cfg(test)]
mod tests {
use crate::input::mask_pattern::{MaskPattern, MaskToken};
#[test]
fn test_is_match() {
assert_eq!(MaskToken::Sep('(').is_match('('), true);
assert_eq!(MaskToken::Sep('-').is_match('('), false);
assert_eq!(MaskToken::Sep('-').is_match('3'), false);
assert_eq!(MaskToken::Digit.is_match('0'), true);
assert_eq!(MaskToken::Digit.is_match('9'), true);
assert_eq!(MaskToken::Digit.is_match('a'), false);
assert_eq!(MaskToken::Digit.is_match('C'), false);
assert_eq!(MaskToken::Letter.is_match('a'), true);
assert_eq!(MaskToken::Letter.is_match('Z'), true);
assert_eq!(MaskToken::Letter.is_match('3'), false);
assert_eq!(MaskToken::Letter.is_match('-'), false);
assert_eq!(MaskToken::LetterOrDigit.is_match('0'), true);
assert_eq!(MaskToken::LetterOrDigit.is_match('9'), true);
assert_eq!(MaskToken::LetterOrDigit.is_match('a'), true);
assert_eq!(MaskToken::LetterOrDigit.is_match('Z'), true);
assert_eq!(MaskToken::LetterOrDigit.is_match('3'), true);
assert_eq!(MaskToken::Any.is_match('a'), true);
assert_eq!(MaskToken::Any.is_match('3'), true);
assert_eq!(MaskToken::Any.is_match('-'), true);
assert_eq!(MaskToken::Any.is_match(' '), true);
}
#[test]
fn test_mask_none() {
let mask = MaskPattern::None;
assert_eq!(mask.is_none(), true);
assert_eq!(mask.is_valid("1124124ASLDJKljk"), true);
assert_eq!(mask.mask("hello-world"), "hello-world");
assert_eq!(mask.unmask("hello-world"), "hello-world");
}
#[test]
fn test_mask_pattern1() {
let mask = MaskPattern::new("(AA)999-999");
assert_eq!(
mask.tokens(),
Some(&vec![
MaskToken::Sep('('),
MaskToken::Letter,
MaskToken::Letter,
MaskToken::Sep(')'),
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Sep('-'),
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Digit,
])
);
assert_eq!(mask.is_valid_at('(', 0), true);
assert_eq!(mask.is_valid_at('H', 0), true);
assert_eq!(mask.is_valid_at('3', 0), false);
assert_eq!(mask.is_valid_at('-', 0), false);
assert_eq!(mask.is_valid_at(')', 1), false);
assert_eq!(mask.is_valid_at('H', 1), true);
assert_eq!(mask.is_valid_at('1', 1), false);
assert_eq!(mask.is_valid_at('e', 2), true);
assert_eq!(mask.is_valid_at(')', 3), true);
assert_eq!(mask.is_valid_at('1', 3), true);
assert_eq!(mask.is_valid_at('2', 4), true);
assert_eq!(mask.is_valid("(AB)123-456"), true);
assert_eq!(mask.mask("AB123456"), "(AB)123-456");
assert_eq!(mask.mask("(AB)123-456"), "(AB)123-456");
assert_eq!(mask.mask("(AB123456"), "(AB)123-456");
assert_eq!(mask.mask("AB123-456"), "(AB)123-456");
assert_eq!(mask.mask("AB123-"), "(AB)123-");
assert_eq!(mask.mask("AB123--"), "(AB)123-");
assert_eq!(mask.mask("AB123-4"), "(AB)123-4");
let unmasked_text = mask.unmask("(AB)123-456");
assert_eq!(unmasked_text, "AB123456");
assert_eq!(mask.is_valid("12AB345"), false);
assert_eq!(mask.is_valid("(11)123-456"), false);
assert_eq!(mask.is_valid("##"), false);
assert_eq!(mask.is_valid("(AB)123456"), true);
}
#[test]
fn test_mask_pattern2() {
let mask = MaskPattern::new("999-999-******");
assert_eq!(
mask.tokens(),
Some(&vec![
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Sep('-'),
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Digit,
MaskToken::Sep('-'),
MaskToken::Any,
MaskToken::Any,
MaskToken::Any,
MaskToken::Any,
MaskToken::Any,
MaskToken::Any,
])
);
let text = "123456A(111)";
let masked_text = mask.mask(text);
assert_eq!(masked_text, "123-456-A(111)");
let unmasked_text = mask.unmask(&masked_text);
assert_eq!(unmasked_text, "123456A(111)");
assert_eq!(mask.is_valid(&masked_text), true);
}
#[test]
fn test_number_with_group_separator() {
let mask = MaskPattern::number(Some(','));
assert_eq!(mask.mask("1234567"), "1,234,567");
assert_eq!(mask.mask("1,234,567"), "1,234,567");
assert_eq!(mask.unmask("1,234,567"), "1234567");
let mask = MaskPattern::number(Some(','));
assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
assert_eq!(mask.unmask("1,234,567.89"), "1234567.89");
let mask = MaskPattern::number(Some(' '));
assert_eq!(mask.mask("1234567"), "1 234 567");
assert_eq!(mask.unmask("1 234 567"), "1234567");
let mask = MaskPattern::number(Some(' '));
assert_eq!(mask.mask("1234567.89"), "1 234 567.89");
assert_eq!(mask.unmask("1 234 567.89"), "1234567.89");
let mask = MaskPattern::number(None);
assert_eq!(mask.mask("1234567"), "1234567");
assert_eq!(mask.unmask("1234567"), "1234567");
let mask = MaskPattern::number(None);
assert_eq!(mask.mask("1234567.89"), "1234567.89");
assert_eq!(mask.unmask("1234567.89"), "1234567.89");
}
#[test]
fn test_number_with_fraction_digits() {
let mask = MaskPattern::Number {
separator: Some(','),
fraction: Some(4),
};
assert_eq!(mask.mask("1234567"), "1,234,567");
assert_eq!(mask.unmask("1,234,567"), "1234567");
assert_eq!(mask.mask("1234567."), "1,234,567.");
assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
assert_eq!(mask.unmask("1,234,567.890"), "1234567.89");
assert_eq!(mask.mask("1234567.891"), "1,234,567.891");
assert_eq!(mask.mask("1234567.891234"), "1,234,567.8912");
let mask = MaskPattern::Number {
separator: Some(','),
fraction: None,
};
assert_eq!(mask.mask("1234567.1234567"), "1,234,567.1234567");
let mask = MaskPattern::Number {
separator: Some(','),
fraction: Some(0),
};
assert_eq!(mask.mask("1234567.1234567"), "1,234,567");
}
#[test]
fn test_signed_number_numbers() {
let mask = MaskPattern::Number {
separator: Some(','),
fraction: Some(2),
};
assert_eq!(mask.is_valid("-"), true);
assert_eq!(mask.is_valid("-1234567"), true);
assert_eq!(mask.is_valid("-1,234,567"), true);
assert_eq!(mask.is_valid("-1234567."), true);
assert_eq!(mask.is_valid("-1234567.89"), true);
assert_eq!(mask.is_valid("+"), true);
assert_eq!(mask.is_valid("+1234567"), true);
assert_eq!(mask.is_valid("+1,234,567"), true);
assert_eq!(mask.is_valid("+1234567."), true);
assert_eq!(mask.is_valid("+1234567.89"), true);
assert_eq!(mask.is_valid("+-"), false);
assert_eq!(mask.is_valid("-+"), false);
assert_eq!(mask.is_valid("+-1234567"), false);
assert_eq!(mask.is_valid("1,-234,567"), false);
assert_eq!(mask.is_valid("12-34567.89"), false);
assert_eq!(mask.is_valid("+1234567.-"), false);
assert_eq!(mask.mask("-123"), "-123");
assert_eq!(mask.mask("-1234567"), "-1,234,567");
assert_eq!(mask.mask("+1234567"), "+1,234,567");
assert_eq!(mask.unmask("-1,234,567"), "-1234567");
assert_eq!(mask.mask("-1234567."), "-1,234,567.");
assert_eq!(mask.mask("-1234567.89"), "-1,234,567.89");
}
}