Allow negative numbers and explicit positive numbers. Added these tests: 1. Only one sign at the start of the number. 2. No signs in the middle of the number. 3. No signs in the fractional part. 4. Sign doesn't break formatting. https://github.com/user-attachments/assets/89c4f4c7-d3bf-4cc1-aeca-7e9bedd02e20 --------- Co-authored-by: Jason Lee <huacnlee@gmail.com>
637 lines
21 KiB
Rust
637 lines
21 KiB
Rust
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<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 {
|
|
/// Group separator, e.g. "," or " "
|
|
separator: Option<char>,
|
|
/// Number of fraction digits, e.g. 2 for 123.45
|
|
fraction: Option<usize>,
|
|
},
|
|
}
|
|
|
|
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<MaskToken>> {
|
|
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<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,
|
|
}
|
|
}
|
|
|
|
/// 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<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;
|
|
}
|
|
|
|
// 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<usize> = 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::<String>()
|
|
});
|
|
|
|
// Reverse the integer part for easier grouping
|
|
let mut chars: Vec<char> = 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<char> = 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<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() {
|
|
// 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");
|
|
}
|
|
}
|