gpui_component/input/
mask_pattern.rs

1use gpui::SharedString;
2
3#[derive(Clone, PartialEq, Debug)]
4pub enum MaskToken {
5    /// 0 Digit, equivalent to `[0]`
6    // Digit0,
7    /// Digit, equivalent to `[0-9]`
8    Digit,
9    /// Letter, equivalent to `[a-zA-Z]`
10    Letter,
11    /// Letter or digit, equivalent to `[a-zA-Z0-9]`
12    LetterOrDigit,
13    /// Separator
14    Sep(char),
15    /// Any character
16    Any,
17}
18
19#[allow(unused)]
20impl MaskToken {
21    /// Check if the token is any character.
22    pub fn is_any(&self) -> bool {
23        matches!(self, MaskToken::Any)
24    }
25
26    /// Check if the token is a match for the given character.
27    ///
28    /// The separator is always a match any input character.
29    fn is_match(&self, ch: char) -> bool {
30        match self {
31            MaskToken::Digit => ch.is_ascii_digit(),
32            MaskToken::Letter => ch.is_ascii_alphabetic(),
33            MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
34            MaskToken::Any => true,
35            MaskToken::Sep(c) => *c == ch,
36        }
37    }
38
39    /// Is the token a separator (Can be ignored)
40    fn is_sep(&self) -> bool {
41        matches!(self, MaskToken::Sep(_))
42    }
43
44    /// Check if the token is a number.
45    pub fn is_number(&self) -> bool {
46        matches!(self, MaskToken::Digit)
47    }
48
49    pub fn placeholder(&self) -> char {
50        match self {
51            MaskToken::Sep(c) => *c,
52            _ => '_',
53        }
54    }
55
56    fn mask_char(&self, ch: char) -> char {
57        match self {
58            MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
59            MaskToken::Sep(c) => *c,
60            MaskToken::Any => ch,
61        }
62    }
63
64    fn unmask_char(&self, ch: char) -> Option<char> {
65        match self {
66            MaskToken::Digit => Some(ch),
67            MaskToken::Letter => Some(ch),
68            MaskToken::LetterOrDigit => Some(ch),
69            MaskToken::Any => Some(ch),
70            _ => None,
71        }
72    }
73}
74
75#[derive(Clone, Default)]
76pub enum MaskPattern {
77    #[default]
78    None,
79    Pattern {
80        pattern: SharedString,
81        tokens: Vec<MaskToken>,
82    },
83    Number {
84        /// Group separator, e.g. "," or " "
85        separator: Option<char>,
86        /// Number of fraction digits, e.g. 2 for 123.45
87        fraction: Option<usize>,
88    },
89}
90
91impl From<&str> for MaskPattern {
92    fn from(pattern: &str) -> Self {
93        Self::new(pattern)
94    }
95}
96
97impl MaskPattern {
98    /// Create a new mask pattern
99    ///
100    /// - `9` - Digit
101    /// - `A` - Letter
102    /// - `#` - Letter or Digit
103    /// - `*` - Any character
104    /// - other characters - Separator
105    ///
106    /// For example:
107    ///
108    /// - `(999)999-9999` - US phone number: (123)456-7890
109    /// - `99999-9999` - ZIP code: 12345-6789
110    /// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
111    /// - `*999*` - Custom pattern: (123) or [123]
112    pub fn new(pattern: &str) -> Self {
113        let tokens = pattern
114            .chars()
115            .map(|ch| match ch {
116                // '0' => MaskToken::Digit0,
117                '9' => MaskToken::Digit,
118                'A' => MaskToken::Letter,
119                '#' => MaskToken::LetterOrDigit,
120                '*' => MaskToken::Any,
121                _ => MaskToken::Sep(ch),
122            })
123            .collect();
124
125        Self::Pattern {
126            pattern: pattern.to_owned().into(),
127            tokens,
128        }
129    }
130
131    #[allow(unused)]
132    fn tokens(&self) -> Option<&Vec<MaskToken>> {
133        match self {
134            Self::Pattern { tokens, .. } => Some(tokens),
135            Self::Number { .. } => None,
136            Self::None => None,
137        }
138    }
139
140    /// Create a new mask pattern with group separator, e.g. "," or " "
141    pub fn number(sep: Option<char>) -> Self {
142        Self::Number {
143            separator: sep,
144            fraction: None,
145        }
146    }
147
148    pub fn placeholder(&self) -> Option<String> {
149        match self {
150            Self::Pattern { tokens, .. } => {
151                Some(tokens.iter().map(|token| token.placeholder()).collect())
152            }
153            Self::Number { .. } => None,
154            Self::None => None,
155        }
156    }
157
158    /// Return true if the mask pattern is None or no any pattern.
159    pub fn is_none(&self) -> bool {
160        match self {
161            Self::Pattern { tokens, .. } => tokens.is_empty(),
162            Self::Number { .. } => false,
163            Self::None => true,
164        }
165    }
166
167    /// Check is the mask text is valid.
168    ///
169    /// If the mask pattern is None, always return true.
170    pub fn is_valid(&self, mask_text: &str) -> bool {
171        if self.is_none() {
172            return true;
173        }
174
175        let mut text_index = 0;
176        let mask_text_chars: Vec<char> = mask_text.chars().collect();
177        match self {
178            Self::Pattern { tokens, .. } => {
179                for token in tokens {
180                    if text_index >= mask_text_chars.len() {
181                        break;
182                    }
183
184                    let ch = mask_text_chars[text_index];
185                    if token.is_match(ch) {
186                        text_index += 1;
187                    }
188                }
189                text_index == mask_text.len()
190            }
191            Self::Number { separator, .. } => {
192                if mask_text.is_empty() {
193                    return true;
194                }
195
196                // check if the text is valid number
197                let mut parts = mask_text.split('.');
198                let int_part = parts.next().unwrap_or("");
199                let frac_part = parts.next();
200
201                if int_part.is_empty() {
202                    return false;
203                }
204
205                let sign_positions: Vec<usize> = int_part
206                    .chars()
207                    .enumerate()
208                    .filter_map(|(i, ch)| match is_sign(&ch) {
209                        true => Some(i),
210                        false => None,
211                    })
212                    .collect();
213
214                // only one sign is valid
215                // sign is only valid at the beginning of the string
216                if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
217                    return false;
218                }
219
220                // check if the integer part is valid
221                if !int_part.chars().enumerate().all(|(i, ch)| {
222                    ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
223                }) {
224                    return false;
225                }
226
227                // check if the fraction part is valid
228                if let Some(frac) = frac_part {
229                    if !frac
230                        .chars()
231                        .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
232                    {
233                        return false;
234                    }
235                }
236
237                true
238            }
239            Self::None => true,
240        }
241    }
242
243    /// Check if valid input char at the given position.
244    pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
245        if self.is_none() {
246            return true;
247        }
248
249        match self {
250            Self::Pattern { tokens, .. } => {
251                if let Some(token) = tokens.get(pos) {
252                    if token.is_match(ch) {
253                        return true;
254                    }
255
256                    if token.is_sep() {
257                        // If next token is match, it's valid
258                        if let Some(next_token) = tokens.get(pos + 1) {
259                            if next_token.is_match(ch) {
260                                return true;
261                            }
262                        }
263                    }
264                }
265
266                false
267            }
268            Self::Number { .. } => true,
269            Self::None => true,
270        }
271    }
272
273    /// Format the text according to the mask pattern
274    ///
275    /// For example:
276    ///
277    /// - pattern: (999)999-999
278    /// - text: 123456789
279    /// - mask_text: (123)456-789
280    pub fn mask(&self, text: &str) -> SharedString {
281        if self.is_none() {
282            return text.to_owned().into();
283        }
284
285        match self {
286            Self::Number {
287                separator,
288                fraction,
289            } => {
290                if let Some(sep) = *separator {
291                    // Remove the existing group separator
292                    let text = text.replace(sep, "");
293
294                    let mut parts = text.split('.');
295                    let int_part = parts.next().unwrap_or("");
296
297                    // Limit the fraction part to the given range, if not enough, pad with 0
298                    let frac_part = parts.next().map(|part| {
299                        part.chars()
300                            .take(fraction.unwrap_or(usize::MAX))
301                            .collect::<String>()
302                    });
303
304                    // Reverse the integer part for easier grouping
305                    let mut chars: Vec<char> = int_part.chars().rev().collect();
306
307                    // Removing the sign from formatting to avoid cases such as: -,123
308                    let maybe_signed = if let Some(pos) = chars.iter().position(is_sign) {
309                        Some(chars.remove(pos))
310                    } else {
311                        None
312                    };
313
314                    let mut result = String::new();
315                    for (i, ch) in chars.iter().enumerate() {
316                        if i > 0 && i % 3 == 0 {
317                            result.push(sep);
318                        }
319                        result.push(*ch);
320                    }
321                    let int_with_sep: String = result.chars().rev().collect();
322
323                    let final_str = if let Some(frac) = frac_part {
324                        if fraction == &Some(0) {
325                            int_with_sep
326                        } else {
327                            format!("{}.{}", int_with_sep, frac)
328                        }
329                    } else {
330                        int_with_sep
331                    };
332
333                    let final_str = if let Some(sign) = maybe_signed {
334                        format!("{}{}", sign, final_str)
335                    } else {
336                        final_str
337                    };
338
339                    return final_str.into();
340                }
341
342                text.to_owned().into()
343            }
344            Self::Pattern { tokens, .. } => {
345                let mut result = String::new();
346                let mut text_index = 0;
347                let text_chars: Vec<char> = text.chars().collect();
348                for (pos, token) in tokens.iter().enumerate() {
349                    if text_index >= text_chars.len() {
350                        break;
351                    }
352                    let ch = text_chars[text_index];
353                    // Break if expected char is not match
354                    if !token.is_sep() && !self.is_valid_at(ch, pos) {
355                        break;
356                    }
357                    let mask_ch = token.mask_char(ch);
358                    result.push(mask_ch);
359                    if ch == mask_ch {
360                        text_index += 1;
361                        continue;
362                    }
363                }
364                result.into()
365            }
366            Self::None => text.to_owned().into(),
367        }
368    }
369
370    /// Extract original text from masked text
371    pub fn unmask(&self, mask_text: &str) -> String {
372        match self {
373            Self::Number { separator, .. } => {
374                if let Some(sep) = *separator {
375                    let mut result = String::new();
376                    for ch in mask_text.chars() {
377                        if ch == sep {
378                            continue;
379                        }
380                        result.push(ch);
381                    }
382
383                    if result.contains('.') {
384                        result = result.trim_end_matches('0').to_string();
385                    }
386                    return result;
387                }
388
389                return mask_text.to_owned();
390            }
391            Self::Pattern { tokens, .. } => {
392                let mut result = String::new();
393                let mask_text_chars: Vec<char> = mask_text.chars().collect();
394                for (text_index, token) in tokens.iter().enumerate() {
395                    if text_index >= mask_text_chars.len() {
396                        break;
397                    }
398                    let ch = mask_text_chars[text_index];
399                    let unmask_ch = token.unmask_char(ch);
400                    if let Some(ch) = unmask_ch {
401                        result.push(ch);
402                    }
403                }
404                result
405            }
406            Self::None => mask_text.to_owned(),
407        }
408    }
409}
410
411#[inline]
412fn is_sign(ch: &char) -> bool {
413    matches!(ch, '+' | '-')
414}
415
416#[cfg(test)]
417mod tests {
418    use crate::input::mask_pattern::{MaskPattern, MaskToken};
419
420    #[test]
421    fn test_is_match() {
422        assert_eq!(MaskToken::Sep('(').is_match('('), true);
423        assert_eq!(MaskToken::Sep('-').is_match('('), false);
424        assert_eq!(MaskToken::Sep('-').is_match('3'), false);
425
426        assert_eq!(MaskToken::Digit.is_match('0'), true);
427        assert_eq!(MaskToken::Digit.is_match('9'), true);
428        assert_eq!(MaskToken::Digit.is_match('a'), false);
429        assert_eq!(MaskToken::Digit.is_match('C'), false);
430
431        assert_eq!(MaskToken::Letter.is_match('a'), true);
432        assert_eq!(MaskToken::Letter.is_match('Z'), true);
433        assert_eq!(MaskToken::Letter.is_match('3'), false);
434        assert_eq!(MaskToken::Letter.is_match('-'), false);
435
436        assert_eq!(MaskToken::LetterOrDigit.is_match('0'), true);
437        assert_eq!(MaskToken::LetterOrDigit.is_match('9'), true);
438        assert_eq!(MaskToken::LetterOrDigit.is_match('a'), true);
439        assert_eq!(MaskToken::LetterOrDigit.is_match('Z'), true);
440        assert_eq!(MaskToken::LetterOrDigit.is_match('3'), true);
441
442        assert_eq!(MaskToken::Any.is_match('a'), true);
443        assert_eq!(MaskToken::Any.is_match('3'), true);
444        assert_eq!(MaskToken::Any.is_match('-'), true);
445        assert_eq!(MaskToken::Any.is_match(' '), true);
446    }
447
448    #[test]
449    fn test_mask_none() {
450        let mask = MaskPattern::None;
451        assert_eq!(mask.is_none(), true);
452        assert_eq!(mask.is_valid("1124124ASLDJKljk"), true);
453        assert_eq!(mask.mask("hello-world"), "hello-world");
454        assert_eq!(mask.unmask("hello-world"), "hello-world");
455    }
456
457    #[test]
458    fn test_mask_pattern1() {
459        let mask = MaskPattern::new("(AA)999-999");
460        assert_eq!(
461            mask.tokens(),
462            Some(&vec![
463                MaskToken::Sep('('),
464                MaskToken::Letter,
465                MaskToken::Letter,
466                MaskToken::Sep(')'),
467                MaskToken::Digit,
468                MaskToken::Digit,
469                MaskToken::Digit,
470                MaskToken::Sep('-'),
471                MaskToken::Digit,
472                MaskToken::Digit,
473                MaskToken::Digit,
474            ])
475        );
476
477        assert_eq!(mask.is_valid_at('(', 0), true);
478        assert_eq!(mask.is_valid_at('H', 0), true);
479        assert_eq!(mask.is_valid_at('3', 0), false);
480        assert_eq!(mask.is_valid_at('-', 0), false);
481        assert_eq!(mask.is_valid_at(')', 1), false);
482        assert_eq!(mask.is_valid_at('H', 1), true);
483        assert_eq!(mask.is_valid_at('1', 1), false);
484        assert_eq!(mask.is_valid_at('e', 2), true);
485        assert_eq!(mask.is_valid_at(')', 3), true);
486        assert_eq!(mask.is_valid_at('1', 3), true);
487        assert_eq!(mask.is_valid_at('2', 4), true);
488
489        assert_eq!(mask.is_valid("(AB)123-456"), true);
490
491        assert_eq!(mask.mask("AB123456"), "(AB)123-456");
492        assert_eq!(mask.mask("(AB)123-456"), "(AB)123-456");
493        assert_eq!(mask.mask("(AB123456"), "(AB)123-456");
494        assert_eq!(mask.mask("AB123-456"), "(AB)123-456");
495        assert_eq!(mask.mask("AB123-"), "(AB)123-");
496        assert_eq!(mask.mask("AB123--"), "(AB)123-");
497        assert_eq!(mask.mask("AB123-4"), "(AB)123-4");
498
499        let unmasked_text = mask.unmask("(AB)123-456");
500        assert_eq!(unmasked_text, "AB123456");
501
502        assert_eq!(mask.is_valid("12AB345"), false);
503        assert_eq!(mask.is_valid("(11)123-456"), false);
504        assert_eq!(mask.is_valid("##"), false);
505        assert_eq!(mask.is_valid("(AB)123456"), true);
506    }
507
508    #[test]
509    fn test_mask_pattern2() {
510        let mask = MaskPattern::new("999-999-******");
511        assert_eq!(
512            mask.tokens(),
513            Some(&vec![
514                MaskToken::Digit,
515                MaskToken::Digit,
516                MaskToken::Digit,
517                MaskToken::Sep('-'),
518                MaskToken::Digit,
519                MaskToken::Digit,
520                MaskToken::Digit,
521                MaskToken::Sep('-'),
522                MaskToken::Any,
523                MaskToken::Any,
524                MaskToken::Any,
525                MaskToken::Any,
526                MaskToken::Any,
527                MaskToken::Any,
528            ])
529        );
530
531        let text = "123456A(111)";
532        let masked_text = mask.mask(text);
533        assert_eq!(masked_text, "123-456-A(111)");
534        let unmasked_text = mask.unmask(&masked_text);
535        assert_eq!(unmasked_text, "123456A(111)");
536        assert_eq!(mask.is_valid(&masked_text), true);
537    }
538
539    #[test]
540    fn test_number_with_group_separator() {
541        // Use comma as group separator
542        let mask = MaskPattern::number(Some(','));
543        assert_eq!(mask.mask("1234567"), "1,234,567");
544        assert_eq!(mask.mask("1,234,567"), "1,234,567");
545        assert_eq!(mask.unmask("1,234,567"), "1234567");
546        let mask = MaskPattern::number(Some(','));
547        assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
548        assert_eq!(mask.unmask("1,234,567.89"), "1234567.89");
549
550        // Use space as group separator
551        let mask = MaskPattern::number(Some(' '));
552        assert_eq!(mask.mask("1234567"), "1 234 567");
553        assert_eq!(mask.unmask("1 234 567"), "1234567");
554        let mask = MaskPattern::number(Some(' '));
555        assert_eq!(mask.mask("1234567.89"), "1 234 567.89");
556        assert_eq!(mask.unmask("1 234 567.89"), "1234567.89");
557
558        // No group separator
559        let mask = MaskPattern::number(None);
560        assert_eq!(mask.mask("1234567"), "1234567");
561        assert_eq!(mask.unmask("1234567"), "1234567");
562        let mask = MaskPattern::number(None);
563        assert_eq!(mask.mask("1234567.89"), "1234567.89");
564        assert_eq!(mask.unmask("1234567.89"), "1234567.89");
565    }
566
567    #[test]
568    fn test_number_with_fraction_digits() {
569        let mask = MaskPattern::Number {
570            separator: Some(','),
571            fraction: Some(4),
572        };
573
574        assert_eq!(mask.mask("1234567"), "1,234,567");
575        assert_eq!(mask.unmask("1,234,567"), "1234567");
576        assert_eq!(mask.mask("1234567."), "1,234,567.");
577        assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
578        assert_eq!(mask.unmask("1,234,567.890"), "1234567.89");
579        assert_eq!(mask.mask("1234567.891"), "1,234,567.891");
580        assert_eq!(mask.mask("1234567.891234"), "1,234,567.8912");
581
582        let mask = MaskPattern::Number {
583            separator: Some(','),
584            fraction: None,
585        };
586
587        assert_eq!(mask.mask("1234567.1234567"), "1,234,567.1234567");
588
589        let mask = MaskPattern::Number {
590            separator: Some(','),
591            fraction: Some(0),
592        };
593
594        assert_eq!(mask.mask("1234567.1234567"), "1,234,567");
595    }
596
597    #[test]
598    fn test_signed_number_numbers() {
599        let mask = MaskPattern::Number {
600            separator: Some(','),
601            fraction: Some(2),
602        };
603
604        assert_eq!(mask.is_valid("-"), true);
605        assert_eq!(mask.is_valid("-1234567"), true);
606        assert_eq!(mask.is_valid("-1,234,567"), true);
607        assert_eq!(mask.is_valid("-1234567."), true);
608        assert_eq!(mask.is_valid("-1234567.89"), true);
609
610        assert_eq!(mask.is_valid("+"), true);
611        assert_eq!(mask.is_valid("+1234567"), true);
612        assert_eq!(mask.is_valid("+1,234,567"), true);
613        assert_eq!(mask.is_valid("+1234567."), true);
614        assert_eq!(mask.is_valid("+1234567.89"), true);
615
616        // Only one sign is valid
617        assert_eq!(mask.is_valid("+-"), false);
618        assert_eq!(mask.is_valid("-+"), false);
619        assert_eq!(mask.is_valid("+-1234567"), false);
620
621        // No sign is valid in the middle of the number
622        assert_eq!(mask.is_valid("1,-234,567"), false);
623        assert_eq!(mask.is_valid("12-34567.89"), false);
624
625        // Signs in fractions are invalid
626        assert_eq!(mask.is_valid("+1234567.-"), false);
627
628        // The separator does not show up before the sign i.e. -,123
629        assert_eq!(mask.mask("-123"), "-123");
630
631        assert_eq!(mask.mask("-1234567"), "-1,234,567");
632        assert_eq!(mask.mask("+1234567"), "+1,234,567");
633        assert_eq!(mask.unmask("-1,234,567"), "-1234567");
634        assert_eq!(mask.mask("-1234567."), "-1,234,567.");
635        assert_eq!(mask.mask("-1234567.89"), "-1,234,567.89");
636    }
637}