use gpui::SharedString; #[derive(Clone, PartialEq, Debug)] pub enum MaskToken { /// 0 Digit, equivalent to `[0]` // Digit0, /// Digit, equivalent to `[0-9]` Digit, /// Letter, equivalent to `[a-zA-Z]` Letter, /// Letter or digit, equivalent to `[a-zA-Z0-9]` LetterOrDigit, /// Separator Sep(char), /// Any character Any, } #[allow(unused)] impl MaskToken { /// Check if the token is any character. pub fn is_any(&self) -> bool { matches!(self, MaskToken::Any) } /// Check if the token is a match for the given character. /// /// The separator is always a match any input character. 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, } } /// Is the token a separator (Can be ignored) fn is_sep(&self) -> bool { matches!(self, MaskToken::Sep(_)) } /// Check if the token is a number. 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 { 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, }, Number { /// Group separator, e.g. "," or " " separator: Option, /// Number of fraction digits, e.g. 2 for 123.45 fraction: Option, }, } impl From<&str> for MaskPattern { fn from(pattern: &str) -> Self { Self::new(pattern) } } impl MaskPattern { /// Create a new mask pattern /// /// - `9` - Digit /// - `A` - Letter /// - `#` - Letter or Digit /// - `*` - Any character /// - other characters - Separator /// /// For example: /// /// - `(999)999-9999` - US phone number: (123)456-7890 /// - `99999-9999` - ZIP code: 12345-6789 /// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4 /// - `*999*` - Custom pattern: (123) or [123] pub fn new(pattern: &str) -> Self { let tokens = pattern .chars() .map(|ch| match ch { // '0' => MaskToken::Digit0, '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> { match self { Self::Pattern { tokens, .. } => Some(tokens), Self::Number { .. } => None, Self::None => None, } } /// Create a new mask pattern with group separator, e.g. "," or " " pub fn number(sep: Option) -> Self { Self::Number { separator: sep, fraction: None, } } pub fn placeholder(&self) -> Option { match self { Self::Pattern { tokens, .. } => { Some(tokens.iter().map(|token| token.placeholder()).collect()) } Self::Number { .. } => None, Self::None => None, } } /// Return true if the mask pattern is None or no any pattern. pub fn is_none(&self) -> bool { match self { Self::Pattern { tokens, .. } => tokens.is_empty(), Self::Number { .. } => false, Self::None => true, } } /// Check is the mask text is valid. /// /// If the mask pattern is None, always return 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 = 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; } // check if the text is valid number 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 = int_part .chars() .enumerate() .filter_map(|(i, ch)| match is_sign(&ch) { true => Some(i), false => None, }) .collect(); // only one sign is valid // sign is only valid at the beginning of the string if sign_positions.len() > 1 || sign_positions.first() > Some(&0) { return false; } // check if the integer part is valid if !int_part.chars().enumerate().all(|(i, ch)| { ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator }) { return false; } // check if the fraction part is valid if let Some(frac) = frac_part { if !frac .chars() .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) { return false; } } true } Self::None => true, } } /// Check if valid input char at the given position. 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 next token is match, it's valid if let Some(next_token) = tokens.get(pos + 1) { if next_token.is_match(ch) { return true; } } } } false } Self::Number { .. } => true, Self::None => true, } } /// Format the text according to the mask pattern /// /// For example: /// /// - pattern: (999)999-999 /// - text: 123456789 /// - mask_text: (123)456-789 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 { // Remove the existing group separator let text = text.replace(sep, ""); let mut parts = text.split('.'); let int_part = parts.next().unwrap_or(""); // Limit the fraction part to the given range, if not enough, pad with 0 let frac_part = parts.next().map(|part| { part.chars() .take(fraction.unwrap_or(usize::MAX)) .collect::() }); // Reverse the integer part for easier grouping let mut chars: Vec = int_part.chars().rev().collect(); // Removing the sign from formatting to avoid cases such as: -,123 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 = text.chars().collect(); for (pos, token) in tokens.iter().enumerate() { if text_index >= text_chars.len() { break; } let ch = text_chars[text_index]; // Break if expected char is not match 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(), } } /// Extract original text from masked text 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 = 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() { // Use comma as 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"); // Use space as group separator 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"); // No group separator 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); // Only one sign is valid assert_eq!(mask.is_valid("+-"), false); assert_eq!(mask.is_valid("-+"), false); assert_eq!(mask.is_valid("+-1234567"), false); // No sign is valid in the middle of the number assert_eq!(mask.is_valid("1,-234,567"), false); assert_eq!(mask.is_valid("12-34567.89"), false); // Signs in fractions are invalid assert_eq!(mask.is_valid("+1234567.-"), false); // The separator does not show up before the sign i.e. -,123 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"); } }