Skip to main content

ferroui_controls/utils/
masked_text_provider.rs

1// Derived from the .NET runtime's `System.ComponentModel.MaskedTextProvider`
2// and `System.ComponentModel.MaskedTextResultHint`.
3// Copyright (c) .NET Foundation and Contributors. Licensed under the MIT license.
4
5//! A mask-parsing service: the engine behind a masked text box.
6//!
7//! The mask language:
8//!
9//! | Mask char | Meaning |
10//! |-----------|---------|
11//! | `0` | digit, required |
12//! | `9` | digit or space, optional |
13//! | `#` | digit, space, `+` or `-`, optional |
14//! | `L` | letter, required |
15//! | `?` | letter, optional |
16//! | `&` | any character, required |
17//! | `C` | any character, optional |
18//! | `A` | alphanumeric, required |
19//! | `a` | alphanumeric, optional |
20//! | `.` | decimal separator of the culture |
21//! | `,` | thousands separator of the culture |
22//! | `:` | time separator of the culture |
23//! | `/` | date separator of the culture |
24//! | `$` | currency symbol of the culture |
25//! | `<` | converts the characters that follow to lower case |
26//! | `>` | converts the characters that follow to upper case |
27//! | `\|` | stops the case conversion |
28//! | `\` | escapes the next mask character, making it a literal |
29//! | anything else | literal |
30//!
31//! Positions are indices into the formatted ("test") string, a `Vec<char>`
32//! here. The original works on UTF-16 code units, where every character
33//! outside the basic multilingual plane is a surrogate pair and a surrogate is
34//! never a valid mask or input character. The same holds here: a character
35//! outside the BMP is rejected everywhere (mask, prompt, password, input), so
36//! a position is always exactly one UTF-16 code unit and positions can be used
37//! as UTF-16 text indices unchanged.
38
39use ferroui_base::media::text_formatting::unicode::{Codepoint, GeneralCategory};
40use ferroui_base::utilities::CultureInfo;
41use std::fmt;
42
43const SPACE_CHAR: char = ' ';
44const DEFAULT_PROMPT_CHAR: char = '_';
45const NULL_PASSWORD_CHAR: char = '\0';
46const DEFAULT_ALLOW_PROMPT: bool = true;
47const INVALID_INDEX: i32 = -1;
48const EDIT_ANY: u8 = 0;
49const EDIT_UNASSIGNED: u8 = 1;
50const EDIT_ASSIGNED: u8 = 2;
51const FORWARD: bool = true;
52const BACKWARD: bool = false;
53
54/// Specifies the result of a [`MaskedTextProvider`] operation. Positive
55/// values are successes, negative values failures (see
56/// [`MaskedTextProvider::get_operation_result_from_hint`]).
57#[repr(i32)]
58#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub enum MaskedTextResultHint {
60    /// The input position is out of the allowed range. Failure.
61    PositionOutOfRange = -55,
62    /// The position is not editable and the input does not match the literal. Failure.
63    NonEditPosition = -54,
64    /// There are no more edit positions available. Failure.
65    UnavailableEditPosition = -53,
66    /// The prompt character is not valid input. Failure.
67    PromptCharNotAllowed = -52,
68    /// The input character is not valid for the mask language at all. Failure.
69    InvalidInput = -51,
70    /// A digit or sign was expected. Failure.
71    SignedDigitExpected = -5,
72    /// A letter was expected. Failure.
73    LetterExpected = -4,
74    /// A digit was expected. Failure.
75    DigitExpected = -3,
76    /// An alphanumeric character was expected. Failure.
77    AlphanumericCharacterExpected = -2,
78    /// An ASCII character was expected. Failure.
79    AsciiCharacterExpected = -1,
80    /// Unknown; the initial value of a hint.
81    Unknown = 0,
82    /// The character was escaped (a literal that was skipped, or a prompt or
83    /// space over an unassigned position). Success.
84    CharacterEscaped = 1,
85    /// The operation changed nothing. Success.
86    NoEffect = 2,
87    /// The operation had a side effect (characters were shifted, or a
88    /// position was reset). Success.
89    SideEffect = 3,
90    /// The primary operation succeeded. Success.
91    Success = 4,
92}
93
94/// The culture-dependent strings the mask language inserts for its separator
95/// characters, read from the culture when the provider is created.
96#[derive(Clone, Debug)]
97struct MaskSeparators {
98    /// Replaces `.` in the mask.
99    decimal_separator: String,
100    /// Replaces `,` in the mask.
101    group_separator: String,
102    /// Replaces `/` in the mask.
103    date_separator: String,
104    /// Replaces `:` in the mask.
105    time_separator: String,
106    /// Replaces `$` in the mask.
107    currency_symbol: String,
108}
109
110impl MaskSeparators {
111    fn of(culture: &CultureInfo) -> Self {
112        let number_format = culture.number_format();
113        let date_time_format = culture.date_time_format();
114        Self {
115            decimal_separator: number_format.number_decimal_separator().to_owned(),
116            group_separator: number_format.number_group_separator().to_owned(),
117            date_separator: date_time_format.date_separator().to_owned(),
118            time_separator: date_time_format.time_separator().to_owned(),
119            currency_symbol: number_format.currency_symbol().to_owned(),
120        }
121    }
122}
123
124/// A culture named `name` whose separators are the given ones (for tests).
125#[cfg(test)]
126pub(crate) fn culture_with_separators(
127    name: &str,
128    decimal_separator: &str,
129    group_separator: &str,
130    date_separator: &str,
131    time_separator: &str,
132    currency_symbol: &str,
133) -> CultureInfo {
134    use ferroui_base::utilities::{DateTimeFormatInfo, NumberFormatInfo};
135
136    let mut number_format = NumberFormatInfo::new();
137    number_format.set_number_decimal_separator(decimal_separator);
138    number_format.set_number_group_separator(group_separator);
139    number_format.set_currency_symbol(currency_symbol);
140    let mut date_time_format = DateTimeFormatInfo::new();
141    date_time_format.set_date_separator(date_separator);
142    date_time_format.set_time_separator(time_separator);
143    CultureInfo::get_culture_info(name).with_number_format(number_format).with_date_time_format(date_time_format)
144}
145
146/// The argument errors of [`MaskedTextProvider`].
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
148pub enum MaskedTextProviderError {
149    /// The mask is empty.
150    MaskNullOrEmpty,
151    /// The mask contains a character that is not valid in a mask.
152    MaskInvalidChar,
153    /// The prompt or password character is not valid.
154    InvalidChar,
155    /// The prompt and the password character are the same.
156    PasswordAndPromptCharEqual,
157}
158
159impl fmt::Display for MaskedTextProviderError {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.write_str(match self {
162            Self::MaskNullOrEmpty => "The Mask value cannot be null or empty.",
163            Self::MaskInvalidChar => "The specified mask contains invalid characters.",
164            Self::InvalidChar => "The specified character value is not allowed for this property.",
165            Self::PasswordAndPromptCharEqual => "The PasswordChar and PromptChar values cannot be the same.",
166        })
167    }
168}
169
170impl std::error::Error for MaskedTextProviderError {}
171
172/// The case conversion a mask modifier (`<`, `>`, `|`) asks for.
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174enum CaseConversion {
175    None,
176    ToLower,
177    ToUpper,
178}
179
180/// The kind of a position of the test string.
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182enum CharType {
183    EditOptional,
184    EditRequired,
185    Separator,
186    Literal,
187}
188
189/// Describes one position of the test string.
190#[derive(Clone, Copy, Debug)]
191struct CharDescriptor {
192    /// The position of the mask character this position comes from.
193    mask_position: usize,
194    case_conversion: CaseConversion,
195    char_type: CharType,
196    is_assigned: bool,
197}
198
199impl CharDescriptor {
200    #[inline]
201    fn is_edit_position(&self) -> bool {
202        matches!(self.char_type, CharType::EditRequired | CharType::EditOptional)
203    }
204
205    #[inline]
206    fn is_literal_position(&self) -> bool {
207        matches!(self.char_type, CharType::Literal | CharType::Separator)
208    }
209}
210
211/// Provides functionality for formatting a test string against a mask string.
212/// [`MaskedTextProvider::add`], [`MaskedTextProvider::insert_at`],
213/// [`MaskedTextProvider::remove_at`] and the like modify the state of the
214/// test string; `Clone` produces an independent copy with the same state.
215#[derive(Clone, Debug)]
216pub struct MaskedTextProvider {
217    ascii_only: bool,
218    allow_prompt_as_input: bool,
219    include_prompt: bool,
220    include_literals: bool,
221    reset_on_prompt: bool,
222    reset_on_space: bool,
223    skip_literals: bool,
224    culture: CultureInfo,
225    separators: MaskSeparators,
226    /// The formatted string: literals, separators, prompts and input.
227    test_string: Vec<char>,
228    assigned_char_count: i32,
229    required_char_count: i32,
230    required_edit_chars: i32,
231    optional_edit_chars: i32,
232    mask: String,
233    mask_chars: Vec<char>,
234    password_char: char,
235    prompt_char: char,
236    /// One descriptor per position of `test_string`.
237    string_descriptor: Vec<CharDescriptor>,
238}
239
240impl MaskedTextProvider {
241    /// The index returned by the `find_*` methods when nothing is found (-1).
242    pub const INVALID_INDEX: i32 = INVALID_INDEX;
243
244    /// The password character used by [`Self::set_is_password`] (`*`).
245    pub const DEFAULT_PASSWORD_CHAR: char = '*';
246
247    /// Creates a provider for `mask` with the current culture, the prompt
248    /// allowed as input, `_` as prompt, no password character and no ASCII
249    /// restriction.
250    pub fn new(mask: &str) -> Result<Self, MaskedTextProviderError> {
251        Self::new_full(mask, None, DEFAULT_ALLOW_PROMPT, DEFAULT_PROMPT_CHAR, NULL_PASSWORD_CHAR, false)
252    }
253
254    /// Creates a provider for `mask`, optionally restricted to ASCII input.
255    pub fn new_with_ascii(mask: &str, restrict_to_ascii: bool) -> Result<Self, MaskedTextProviderError> {
256        Self::new_full(mask, None, DEFAULT_ALLOW_PROMPT, DEFAULT_PROMPT_CHAR, NULL_PASSWORD_CHAR, restrict_to_ascii)
257    }
258
259    /// Creates a provider for `mask` with a culture.
260    pub fn new_with_culture(mask: &str, culture: Option<CultureInfo>) -> Result<Self, MaskedTextProviderError> {
261        Self::new_full(mask, culture, DEFAULT_ALLOW_PROMPT, DEFAULT_PROMPT_CHAR, NULL_PASSWORD_CHAR, false)
262    }
263
264    /// Creates a provider for `mask` with a culture, optionally restricted to
265    /// ASCII input.
266    pub fn new_with_culture_ascii(
267        mask: &str,
268        culture: Option<CultureInfo>,
269        restrict_to_ascii: bool,
270    ) -> Result<Self, MaskedTextProviderError> {
271        Self::new_full(mask, culture, DEFAULT_ALLOW_PROMPT, DEFAULT_PROMPT_CHAR, NULL_PASSWORD_CHAR, restrict_to_ascii)
272    }
273
274    /// Creates a provider for `mask` with a password character (`'\0'` for
275    /// none).
276    pub fn new_with_password(mask: &str, password_char: char, allow_prompt_as_input: bool) -> Result<Self, MaskedTextProviderError> {
277        Self::new_full(mask, None, allow_prompt_as_input, DEFAULT_PROMPT_CHAR, password_char, false)
278    }
279
280    /// Creates a provider for `mask` with a culture and a password character
281    /// (`'\0'` for none).
282    pub fn new_with_culture_password(
283        mask: &str,
284        culture: Option<CultureInfo>,
285        password_char: char,
286        allow_prompt_as_input: bool,
287    ) -> Result<Self, MaskedTextProviderError> {
288        Self::new_full(mask, culture, allow_prompt_as_input, DEFAULT_PROMPT_CHAR, password_char, false)
289    }
290
291    /// Creates a provider.
292    ///
293    /// * `mask` — the mask; must not be empty and must only contain valid
294    ///   mask characters ([`Self::is_valid_mask_char`]).
295    /// * `culture` — the culture providing the separators; `None` is the
296    ///   current culture.
297    /// * `allow_prompt_as_input` — whether the prompt character is accepted
298    ///   as input.
299    /// * `prompt_char` — the character shown at unassigned edit positions.
300    /// * `password_char` — the character shown instead of the input, `'\0'`
301    ///   for none.
302    /// * `restrict_to_ascii` — whether letters and characters are restricted
303    ///   to ASCII.
304    ///
305    /// As in the original, the prompt and password characters are not
306    /// validated here (only their setters validate).
307    pub fn new_full(
308        mask: &str,
309        culture: Option<CultureInfo>,
310        allow_prompt_as_input: bool,
311        prompt_char: char,
312        password_char: char,
313        restrict_to_ascii: bool,
314    ) -> Result<Self, MaskedTextProviderError> {
315        if mask.is_empty() {
316            return Err(MaskedTextProviderError::MaskNullOrEmpty);
317        }
318
319        if !mask.chars().all(is_printable_char) {
320            return Err(MaskedTextProviderError::MaskInvalidChar);
321        }
322
323        let culture = culture.unwrap_or_else(CultureInfo::current_culture);
324        let mut provider = Self {
325            ascii_only: restrict_to_ascii,
326            allow_prompt_as_input,
327            // Default values of the read/write properties.
328            include_prompt: false,
329            include_literals: true,
330            reset_on_prompt: true,
331            reset_on_space: true,
332            skip_literals: true,
333            separators: MaskSeparators::of(&culture),
334            culture,
335            test_string: Vec::new(),
336            assigned_char_count: 0,
337            required_char_count: 0,
338            required_edit_chars: 0,
339            optional_edit_chars: 0,
340            mask: mask.to_owned(),
341            mask_chars: mask.chars().collect(),
342            password_char,
343            prompt_char,
344            string_descriptor: Vec::new(),
345        };
346
347        provider.initialize();
348        Ok(provider)
349    }
350
351    /// Traverses the mask to generate the test string and the descriptors
352    /// used to validate the input.
353    fn initialize(&mut self) {
354        let mut case_conversion = CaseConversion::None;
355        let mut escaped_char = false;
356        let mut char_type = CharType::Literal;
357        let mut buffer = [0u8; 4];
358
359        for mask_pos in 0..self.mask_chars.len() {
360            let mut ch = self.mask_chars[mask_pos];
361            // The culture string a separator stands for; it can be longer than one character.
362            let mut loc_symbol: Option<&str> = None;
363
364            if !escaped_char {
365                match ch {
366                    // Mask language placeholders.
367                    '.' => {
368                        loc_symbol = Some(&self.separators.decimal_separator);
369                        char_type = CharType::Separator;
370                    }
371                    ',' => {
372                        loc_symbol = Some(&self.separators.group_separator);
373                        char_type = CharType::Separator;
374                    }
375                    ':' => {
376                        loc_symbol = Some(&self.separators.time_separator);
377                        char_type = CharType::Separator;
378                    }
379                    '/' => {
380                        loc_symbol = Some(&self.separators.date_separator);
381                        char_type = CharType::Separator;
382                    }
383                    '$' => {
384                        loc_symbol = Some(&self.separators.currency_symbol);
385                        char_type = CharType::Separator;
386                    }
387
388                    // Mask language modifiers: no position of their own, they
389                    // set the conversion of the edit positions that follow.
390                    '<' => {
391                        case_conversion = CaseConversion::ToLower;
392                        continue;
393                    }
394                    '>' => {
395                        case_conversion = CaseConversion::ToUpper;
396                        continue;
397                    }
398                    '|' => {
399                        case_conversion = CaseConversion::None;
400                        continue;
401                    }
402                    '\\' => {
403                        // The next character is a literal.
404                        escaped_char = true;
405                        char_type = CharType::Literal;
406                        continue;
407                    }
408
409                    // Mask language edit identifiers.
410                    '0' | 'L' | '&' | 'A' => {
411                        self.required_edit_chars += 1;
412                        ch = self.prompt_char;
413                        char_type = CharType::EditRequired;
414                    }
415                    '?' | '9' | '#' | 'C' | 'a' => {
416                        self.optional_edit_chars += 1;
417                        ch = self.prompt_char;
418                        char_type = CharType::EditOptional;
419                    }
420
421                    // Literals are added to the test string as they are.
422                    _ => {
423                        char_type = CharType::Literal;
424                    }
425                }
426            } else {
427                // The escaped character is now added to the test string.
428                escaped_char = false;
429            }
430
431            let mut descriptor = CharDescriptor {
432                mask_position: mask_pos,
433                case_conversion: CaseConversion::None,
434                char_type,
435                is_assigned: false,
436            };
437
438            if descriptor.is_edit_position() {
439                descriptor.case_conversion = case_conversion;
440            }
441
442            let symbol: &str = match loc_symbol {
443                Some(symbol) if char_type == CharType::Separator => symbol,
444                _ => ch.encode_utf8(&mut buffer),
445            };
446
447            for ch_val in symbol.chars() {
448                self.test_string.push(ch_val);
449                self.string_descriptor.push(descriptor);
450            }
451        }
452
453        self.test_string.shrink_to_fit();
454        self.string_descriptor.shrink_to_fit();
455    }
456
457    // ---- Properties ----
458
459    /// Whether the prompt character is accepted as valid input.
460    pub fn allow_prompt_as_input(&self) -> bool {
461        self.allow_prompt_as_input
462    }
463
464    /// The number of edit positions that have a character assigned.
465    pub fn assigned_edit_position_count(&self) -> i32 {
466        self.assigned_char_count
467    }
468
469    /// The number of edit positions that have no character assigned.
470    pub fn available_edit_position_count(&self) -> i32 {
471        self.edit_position_count() - self.assigned_char_count
472    }
473
474    /// The culture that provides the separators.
475    pub fn culture(&self) -> &CultureInfo {
476        &self.culture
477    }
478
479    /// The default password character (`*`).
480    pub fn default_password_char() -> char {
481        Self::DEFAULT_PASSWORD_CHAR
482    }
483
484    /// The number of edit positions in the test string.
485    pub fn edit_position_count(&self) -> i32 {
486        self.optional_edit_chars + self.required_edit_chars
487    }
488
489    /// The edit positions of the test string, in ascending order.
490    pub fn edit_positions(&self) -> impl Iterator<Item = i32> + '_ {
491        self.string_descriptor
492            .iter()
493            .enumerate()
494            .filter(|(_, descriptor)| descriptor.is_edit_position())
495            .map(|(position, _)| position as i32)
496    }
497
498    /// Whether literals are included in the formatted string ([`fmt::Display`]
499    /// and the `to_string_*` methods without an explicit flag). Default `true`.
500    pub fn include_literals(&self) -> bool {
501        self.include_literals
502    }
503
504    /// Sets [`Self::include_literals`].
505    pub fn set_include_literals(&mut self, value: bool) {
506        self.include_literals = value;
507    }
508
509    /// Whether prompt characters are included in the formatted string.
510    /// Default `false`.
511    pub fn include_prompt(&self) -> bool {
512        self.include_prompt
513    }
514
515    /// Sets [`Self::include_prompt`].
516    pub fn set_include_prompt(&mut self, value: bool) {
517        self.include_prompt = value;
518    }
519
520    /// Whether only ASCII characters are valid input for the letter,
521    /// alphanumeric and "any character" mask elements.
522    pub fn ascii_only(&self) -> bool {
523        self.ascii_only
524    }
525
526    /// Whether the input is displayed with a password character.
527    pub fn is_password(&self) -> bool {
528        self.password_char != NULL_PASSWORD_CHAR
529    }
530
531    /// Turns the password display on (with [`Self::DEFAULT_PASSWORD_CHAR`])
532    /// or off; does nothing when it already is in the requested state.
533    pub fn set_is_password(&mut self, value: bool) {
534        if self.is_password() != value {
535            self.password_char = if value { Self::DEFAULT_PASSWORD_CHAR } else { NULL_PASSWORD_CHAR };
536        }
537    }
538
539    /// The index returned by the `find_*` methods when nothing is found (-1).
540    pub fn invalid_index() -> i32 {
541        INVALID_INDEX
542    }
543
544    /// The position of the last assigned edit position, or
545    /// [`Self::INVALID_INDEX`].
546    pub fn last_assigned_position(&self) -> i32 {
547        self.find_assigned_edit_position_from(self.length() - 1, BACKWARD)
548    }
549
550    /// The length of the test string (the mask without modifiers and with the
551    /// separators replaced by the culture's).
552    pub fn length(&self) -> i32 {
553        self.test_string.len() as i32
554    }
555
556    /// The mask.
557    pub fn mask(&self) -> &str {
558        &self.mask
559    }
560
561    /// Whether all required edit positions have been assigned.
562    pub fn mask_completed(&self) -> bool {
563        self.required_char_count == self.required_edit_chars
564    }
565
566    /// Whether all edit positions (required and optional) have been assigned.
567    pub fn mask_full(&self) -> bool {
568        self.assigned_char_count == self.edit_position_count()
569    }
570
571    /// The character displayed instead of the input, `'\0'` for none.
572    pub fn password_char(&self) -> char {
573        self.password_char
574    }
575
576    /// Sets the password character (`'\0'` for none). It must differ from the
577    /// prompt character and be a valid password character.
578    pub fn set_password_char(&mut self, value: char) -> Result<(), MaskedTextProviderError> {
579        if value == self.prompt_char {
580            // Prompt and password chars must be different.
581            return Err(MaskedTextProviderError::PasswordAndPromptCharEqual);
582        }
583
584        if !Self::is_valid_password_char(value) {
585            // Same message as the prompt character.
586            return Err(MaskedTextProviderError::InvalidChar);
587        }
588
589        self.password_char = value;
590        Ok(())
591    }
592
593    /// The character displayed at unassigned edit positions.
594    pub fn prompt_char(&self) -> char {
595        self.prompt_char
596    }
597
598    /// Sets the prompt character. It must differ from the password character
599    /// and be a valid input character. The unassigned edit positions of the
600    /// test string are updated.
601    pub fn set_prompt_char(&mut self, value: char) -> Result<(), MaskedTextProviderError> {
602        if value == self.password_char {
603            // Prompt and password chars must be different.
604            return Err(MaskedTextProviderError::PasswordAndPromptCharEqual);
605        }
606
607        if !is_printable_char(value) {
608            return Err(MaskedTextProviderError::InvalidChar);
609        }
610
611        if value != self.prompt_char {
612            self.prompt_char = value;
613
614            for position in 0..self.test_string.len() {
615                let descriptor = &self.string_descriptor[position];
616                if descriptor.is_edit_position() && !descriptor.is_assigned {
617                    self.test_string[position] = value;
618                }
619            }
620        }
621
622        Ok(())
623    }
624
625    /// Whether a prompt character in the input resets the position it lands
626    /// on instead of being tested against the mask. Default `true`.
627    pub fn reset_on_prompt(&self) -> bool {
628        self.reset_on_prompt
629    }
630
631    /// Sets [`Self::reset_on_prompt`].
632    pub fn set_reset_on_prompt(&mut self, value: bool) {
633        self.reset_on_prompt = value;
634    }
635
636    /// Whether a space in the input resets the position it lands on instead
637    /// of being tested against the mask. Default `true`.
638    pub fn reset_on_space(&self) -> bool {
639        self.reset_on_space
640    }
641
642    /// Sets [`Self::reset_on_space`].
643    pub fn set_reset_on_space(&mut self, value: bool) {
644        self.reset_on_space = value;
645    }
646
647    /// Whether an input character equal to the literal at its position is
648    /// accepted and skipped. Default `true`.
649    pub fn skip_literals(&self) -> bool {
650        self.skip_literals
651    }
652
653    /// Sets [`Self::skip_literals`].
654    pub fn set_skip_literals(&mut self, value: bool) {
655        self.skip_literals = value;
656    }
657
658    /// The character of the test string at `index`.
659    ///
660    /// # Panics
661    ///
662    /// Panics when `index` is out of range.
663    pub fn char_at(&self, index: i32) -> char {
664        if index < 0 || index >= self.length() {
665            panic!("index {index} is out of range of the test string (length {})", self.length());
666        }
667
668        self.test_string[index as usize]
669    }
670
671    // ---- Methods ----
672
673    /// Attempts to add `input` at the first edit position after the last
674    /// assigned one.
675    pub fn add_char(&mut self, input: char) -> bool {
676        self.add_char_with_hint(input).0
677    }
678
679    /// As [`Self::add_char`]; also returns the position the character was
680    /// tested at (the length of the test string when there is none) and the
681    /// result hint.
682    pub fn add_char_with_hint(&mut self, input: char) -> (bool, i32, MaskedTextResultHint) {
683        let last_assigned_pos = self.last_assigned_position();
684
685        if last_assigned_pos == self.length() - 1 {
686            // At the last edit char position.
687            return (false, self.length(), MaskedTextResultHint::UnavailableEditPosition);
688        }
689
690        // Get the position after the last assigned position.
691        let test_position = self.find_edit_position_from(last_assigned_pos + 1, FORWARD);
692
693        if test_position == INVALID_INDEX {
694            return (false, self.length(), MaskedTextResultHint::UnavailableEditPosition);
695        }
696
697        let (result, hint) = self.test_set_char(input, test_position);
698        (result, test_position, hint)
699    }
700
701    /// Attempts to add the characters of `input` after the last assigned
702    /// position. All of them are added, or none.
703    pub fn add(&mut self, input: &str) -> bool {
704        self.add_with_hint(input).0
705    }
706
707    /// As [`Self::add`]; also returns the position of the last character
708    /// tested (on failure: the failing position) and the result hint.
709    pub fn add_with_hint(&mut self, input: &str) -> (bool, i32, MaskedTextResultHint) {
710        let test_position = self.last_assigned_position() + 1;
711
712        if input.is_empty() {
713            // Nothing to add; the position is where the test would be performed.
714            return (true, test_position, MaskedTextResultHint::NoEffect);
715        }
716
717        self.test_set_string(input, test_position)
718    }
719
720    /// Resets all edit positions.
721    pub fn clear(&mut self) {
722        self.clear_with_hint();
723    }
724
725    /// As [`Self::clear`]; returns [`MaskedTextResultHint::NoEffect`] when
726    /// nothing was assigned, [`MaskedTextResultHint::Success`] otherwise.
727    pub fn clear_with_hint(&mut self) -> MaskedTextResultHint {
728        if self.assigned_char_count == 0 {
729            return MaskedTextResultHint::NoEffect;
730        }
731
732        for position in 0..self.length() {
733            self.reset_char(position);
734        }
735
736        MaskedTextResultHint::Success
737    }
738
739    /// The first assigned edit position from `position` in the direction
740    /// (`true` forward, `false` backward), or [`Self::INVALID_INDEX`].
741    pub fn find_assigned_edit_position_from(&self, position: i32, direction: bool) -> i32 {
742        if self.assigned_char_count == 0 {
743            return INVALID_INDEX;
744        }
745
746        let (start_position, end_position) = self.range_from(position, direction);
747        self.find_assigned_edit_position_in_range(start_position, end_position, direction)
748    }
749
750    /// The first assigned edit position in the inclusive range, searching in
751    /// the direction, or [`Self::INVALID_INDEX`].
752    pub fn find_assigned_edit_position_in_range(&self, start_position: i32, end_position: i32, direction: bool) -> i32 {
753        if self.assigned_char_count == 0 {
754            return INVALID_INDEX;
755        }
756
757        self.find_edit_position_in_range_status(start_position, end_position, direction, EDIT_ASSIGNED)
758    }
759
760    /// The first edit position (assigned or not) from `position` in the
761    /// direction, or [`Self::INVALID_INDEX`].
762    pub fn find_edit_position_from(&self, position: i32, direction: bool) -> i32 {
763        let (start_position, end_position) = self.range_from(position, direction);
764        self.find_edit_position_in_range(start_position, end_position, direction)
765    }
766
767    /// The first edit position (assigned or not) in the inclusive range,
768    /// searching in the direction, or [`Self::INVALID_INDEX`].
769    pub fn find_edit_position_in_range(&self, start_position: i32, end_position: i32, direction: bool) -> i32 {
770        self.find_position_in_range(start_position, end_position, direction, true)
771    }
772
773    /// The first edit position in the range with the given assigned status.
774    fn find_edit_position_in_range_status(&self, mut start_position: i32, mut end_position: i32, direction: bool, assigned_status: u8) -> i32 {
775        // Out of range positions are handled in find_edit_position_in_range.
776        loop {
777            let test_position = self.find_edit_position_in_range(start_position, end_position, direction);
778
779            if test_position == INVALID_INDEX {
780                break;
781            }
782
783            let is_assigned = self.string_descriptor[test_position as usize].is_assigned;
784
785            match assigned_status {
786                EDIT_UNASSIGNED => {
787                    if !is_assigned {
788                        return test_position;
789                    }
790                }
791                EDIT_ASSIGNED => {
792                    if is_assigned {
793                        return test_position;
794                    }
795                }
796                _ => return test_position,
797            }
798
799            if direction == FORWARD {
800                start_position += 1;
801            } else {
802                end_position -= 1;
803            }
804
805            if start_position > end_position {
806                break;
807            }
808        }
809
810        INVALID_INDEX
811    }
812
813    /// The first non-edit position (literal or separator) from `position` in
814    /// the direction, or [`Self::INVALID_INDEX`].
815    pub fn find_non_edit_position_from(&self, position: i32, direction: bool) -> i32 {
816        let (start_position, end_position) = self.range_from(position, direction);
817        self.find_non_edit_position_in_range(start_position, end_position, direction)
818    }
819
820    /// The first non-edit position in the inclusive range, searching in the
821    /// direction, or [`Self::INVALID_INDEX`].
822    pub fn find_non_edit_position_in_range(&self, start_position: i32, end_position: i32, direction: bool) -> i32 {
823        self.find_position_in_range(start_position, end_position, direction, false)
824    }
825
826    /// Finds a position in the range whose kind (edit / non-edit) matches.
827    fn find_position_in_range(&self, mut start_position: i32, mut end_position: i32, direction: bool, edit: bool) -> i32 {
828        if start_position < 0 {
829            start_position = 0;
830        }
831
832        if end_position >= self.length() {
833            end_position = self.length() - 1;
834        }
835
836        if start_position > end_position {
837            return INVALID_INDEX;
838        }
839
840        while start_position <= end_position {
841            let test_position = if direction == FORWARD {
842                start_position += 1;
843                start_position - 1
844            } else {
845                end_position -= 1;
846                end_position + 1
847            };
848
849            if self.string_descriptor[test_position as usize].is_edit_position() == edit {
850                return test_position;
851            }
852        }
853
854        INVALID_INDEX
855    }
856
857    /// The first unassigned edit position from `position` in the direction,
858    /// or [`Self::INVALID_INDEX`].
859    pub fn find_unassigned_edit_position_from(&self, position: i32, direction: bool) -> i32 {
860        let (start_position, end_position) = self.range_from(position, direction);
861        self.find_edit_position_in_range_status(start_position, end_position, direction, EDIT_UNASSIGNED)
862    }
863
864    /// The first unassigned edit position in the inclusive range, searching
865    /// in the direction, or [`Self::INVALID_INDEX`].
866    pub fn find_unassigned_edit_position_in_range(&self, mut start_position: i32, mut end_position: i32, direction: bool) -> i32 {
867        loop {
868            let position = self.find_edit_position_in_range_status(start_position, end_position, direction, EDIT_ANY);
869
870            if position == INVALID_INDEX {
871                return INVALID_INDEX;
872            }
873
874            if !self.string_descriptor[position as usize].is_assigned {
875                return position;
876            }
877
878            if direction == FORWARD {
879                start_position += 1;
880            } else {
881                end_position -= 1;
882            }
883        }
884    }
885
886    /// The range a `*_from` search covers.
887    #[inline]
888    fn range_from(&self, position: i32, direction: bool) -> (i32, i32) {
889        if direction == FORWARD {
890            (position, self.length() - 1)
891        } else {
892            (0, position)
893        }
894    }
895
896    /// Whether a hint describes a success (positive) or a failure.
897    pub fn get_operation_result_from_hint(hint: MaskedTextResultHint) -> bool {
898        (hint as i32) > 0
899    }
900
901    /// Attempts to insert `input` at `position` (or the first edit position
902    /// after it), shifting the existing characters right.
903    pub fn insert_at_char(&mut self, input: char, position: i32) -> bool {
904        if position < 0 || position >= self.length() {
905            return false;
906        }
907
908        let mut buffer = [0u8; 4];
909        self.insert_at(input.encode_utf8(&mut buffer), position)
910    }
911
912    /// As [`Self::insert_at_char`]; also returns the position of the inserted
913    /// character (on failure: the failing position) and the result hint.
914    pub fn insert_at_char_with_hint(&mut self, input: char, position: i32) -> (bool, i32, MaskedTextResultHint) {
915        let mut buffer = [0u8; 4];
916        self.insert_at_with_hint(input.encode_utf8(&mut buffer), position)
917    }
918
919    /// Attempts to insert the characters of `input` from `position`, shifting
920    /// the existing characters right. All of them are inserted, or none.
921    pub fn insert_at(&mut self, input: &str, position: i32) -> bool {
922        self.insert_at_with_hint(input, position).0
923    }
924
925    /// As [`Self::insert_at`]; also returns the position of the last inserted
926    /// character (on failure: the failing position) and the result hint.
927    pub fn insert_at_with_hint(&mut self, input: &str, position: i32) -> (bool, i32, MaskedTextResultHint) {
928        if position < 0 || position >= self.length() {
929            return (false, position, MaskedTextResultHint::PositionOutOfRange);
930        }
931
932        self.insert_at_int(input, position, false)
933    }
934
935    /// Inserts (or, with `test_only`, only tests the insertion of) `input` at
936    /// `position`, which must be in range.
937    fn insert_at_int(&mut self, input: &str, position: i32, test_only: bool) -> (bool, i32, MaskedTextResultHint) {
938        debug_assert!(position >= 0 && position < self.length(), "input param out of range.");
939
940        if input.is_empty() {
941            // Nothing to insert.
942            return (true, position, MaskedTextResultHint::NoEffect);
943        }
944
945        // Test the input string first. test_position is the position of the last inserted character.
946        let (result, mut test_position, mut result_hint) = self.test_string(input, position);
947        if !result {
948            return (false, test_position, result_hint);
949        }
950
951        // Now check whether room has to be made for the input characters
952        // (existing characters shift right) and if so test the shifting characters.
953        let mut src_pos = self.find_edit_position_from(position, FORWARD);
954        let shift_needed = self.find_assigned_edit_position_in_range(src_pos, test_position, FORWARD) != INVALID_INDEX;
955        let last_assigned_pos = self.last_assigned_position();
956
957        if shift_needed && test_position == self.length() - 1 {
958            // No room for shifting.
959            return (false, self.length(), MaskedTextResultHint::UnavailableEditPosition);
960        }
961
962        let mut dst_pos = self.find_edit_position_from(test_position + 1, FORWARD);
963
964        if shift_needed {
965            // Temp hint used not to overwrite the primary operation result hint (from test_string).
966            let mut temp_hint = MaskedTextResultHint::Unknown;
967
968            // Test the shifting characters.
969            loop {
970                if dst_pos == INVALID_INDEX {
971                    return (false, self.length(), MaskedTextResultHint::UnavailableEditPosition);
972                }
973
974                // Only assigned positions are tested.
975                if self.string_descriptor[src_pos as usize].is_assigned {
976                    let (ok, hint) = self.test_char(self.test_string[src_pos as usize], dst_pos);
977                    temp_hint = hint;
978                    if !ok {
979                        test_position = dst_pos;
980                        return (false, test_position, temp_hint);
981                    }
982                }
983
984                if src_pos == last_assigned_pos {
985                    // All shifting positions tested.
986                    break;
987                }
988
989                src_pos = self.find_edit_position_from(src_pos + 1, FORWARD);
990                dst_pos = self.find_edit_position_from(dst_pos + 1, FORWARD);
991            }
992
993            if temp_hint > result_hint {
994                result_hint = temp_hint;
995            }
996        }
997
998        if test_only {
999            return (true, test_position, result_hint);
1000        }
1001
1002        // The tests passed: shift the existing characters to make room for
1003        // the new ones, then set those.
1004        if shift_needed {
1005            while src_pos >= position {
1006                if self.string_descriptor[src_pos as usize].is_assigned {
1007                    self.set_char(self.test_string[src_pos as usize], dst_pos);
1008                } else {
1009                    self.reset_char(dst_pos);
1010                }
1011
1012                dst_pos = self.find_edit_position_from(dst_pos - 1, BACKWARD);
1013                src_pos = self.find_edit_position_from(src_pos - 1, BACKWARD);
1014            }
1015        }
1016
1017        // Finally set the input characters.
1018        self.set_string(input, position);
1019
1020        (true, test_position, result_hint)
1021    }
1022
1023    /// Whether `position` is an edit position with no character assigned.
1024    pub fn is_available_position(&self, position: i32) -> bool {
1025        if position < 0 || position >= self.length() {
1026            return false;
1027        }
1028
1029        let descriptor = &self.string_descriptor[position as usize];
1030        descriptor.is_edit_position() && !descriptor.is_assigned
1031    }
1032
1033    /// Whether `position` is an edit position.
1034    pub fn is_edit_position(&self, position: i32) -> bool {
1035        if position < 0 || position >= self.length() {
1036            return false;
1037        }
1038
1039        self.string_descriptor[position as usize].is_edit_position()
1040    }
1041
1042    /// Whether `c` is a valid input character: a letter, digit, punctuation,
1043    /// symbol or the space.
1044    pub fn is_valid_input_char(c: char) -> bool {
1045        is_printable_char(c)
1046    }
1047
1048    /// Whether `c` is a valid mask character (the same set as
1049    /// [`Self::is_valid_input_char`]).
1050    pub fn is_valid_mask_char(c: char) -> bool {
1051        is_printable_char(c)
1052    }
1053
1054    /// Whether `c` is a valid password character: a valid input character, or
1055    /// `'\0'` (no password character).
1056    pub fn is_valid_password_char(c: char) -> bool {
1057        is_printable_char(c) || c == NULL_PASSWORD_CHAR
1058    }
1059
1060    /// Removes the last assigned character. Always succeeds.
1061    pub fn remove(&mut self) -> bool {
1062        self.remove_with_hint().0
1063    }
1064
1065    /// As [`Self::remove`]; also returns the position of the removed
1066    /// character (0 when there was none) and the result hint.
1067    pub fn remove_with_hint(&mut self) -> (bool, i32, MaskedTextResultHint) {
1068        let last_assigned_pos = self.last_assigned_position();
1069
1070        if last_assigned_pos == INVALID_INDEX {
1071            // Nothing to remove.
1072            return (true, 0, MaskedTextResultHint::NoEffect);
1073        }
1074
1075        self.reset_char(last_assigned_pos);
1076        (true, last_assigned_pos, MaskedTextResultHint::Success)
1077    }
1078
1079    /// Removes the character at `position`, shifting the characters after it
1080    /// left.
1081    pub fn remove_at(&mut self, position: i32) -> bool {
1082        self.remove_at_range(position, position)
1083    }
1084
1085    /// Removes the characters in the inclusive range, shifting the characters
1086    /// after it left.
1087    pub fn remove_at_range(&mut self, start_position: i32, end_position: i32) -> bool {
1088        self.remove_at_range_with_hint(start_position, end_position).0
1089    }
1090
1091    /// As [`Self::remove_at_range`]; also returns the position the operation
1092    /// was performed at (on failure: the failing position) and the result
1093    /// hint.
1094    pub fn remove_at_range_with_hint(&mut self, start_position: i32, end_position: i32) -> (bool, i32, MaskedTextResultHint) {
1095        if end_position >= self.length() {
1096            return (false, end_position, MaskedTextResultHint::PositionOutOfRange);
1097        }
1098
1099        if start_position < 0 || start_position > end_position {
1100            return (false, start_position, MaskedTextResultHint::PositionOutOfRange);
1101        }
1102
1103        self.remove_at_int(start_position, end_position, false)
1104    }
1105
1106    /// Removes (or, with `test_only`, only tests the removal of) the
1107    /// characters in the range, which must be valid.
1108    fn remove_at_int(&mut self, mut start_position: i32, end_position: i32, test_only: bool) -> (bool, i32, MaskedTextResultHint) {
1109        debug_assert!(
1110            start_position >= 0 && start_position <= end_position && end_position < self.length(),
1111            "Out of range input."
1112        );
1113
1114        // Check whether characters have to shift left to occupy the positions
1115        // left by the characters being removed.
1116        let last_assigned_pos = self.last_assigned_position();
1117        let mut dst_pos = self.find_edit_position_in_range(start_position, end_position, FORWARD);
1118
1119        let mut result_hint = MaskedTextResultHint::NoEffect;
1120
1121        if dst_pos == INVALID_INDEX || dst_pos > last_assigned_pos {
1122            // Nothing to remove.
1123            return (true, start_position, result_hint);
1124        }
1125
1126        // On remove, the test position remains the same.
1127        let test_position = start_position;
1128
1129        // Does the last assigned position survive the removal?
1130        let shift_needed = end_position < last_assigned_pos;
1131
1132        // If there are assigned characters to be removed (the range may have
1133        // none, in which case characters may just be shifted), the hint is success.
1134        if self.find_assigned_edit_position_in_range(start_position, end_position, FORWARD) != INVALID_INDEX {
1135            result_hint = MaskedTextResultHint::Success;
1136        }
1137
1138        if shift_needed {
1139            // Test the shifting characters.
1140            let mut src_pos = self.find_edit_position_from(end_position + 1, FORWARD);
1141            let shift_start = src_pos;
1142
1143            // The actual start position.
1144            start_position = dst_pos;
1145
1146            loop {
1147                let src_ch = self.test_string[src_pos as usize];
1148                let is_assigned = self.string_descriptor[src_pos as usize].is_assigned;
1149
1150                // A prompt at an unassigned position does not need to be tested.
1151                if src_ch != self.prompt_char || is_assigned {
1152                    let (ok, test_hint) = self.test_char(src_ch, dst_pos);
1153                    if !ok {
1154                        // dst_pos is the failed position.
1155                        return (false, dst_pos, test_hint);
1156                    }
1157                }
1158
1159                if src_pos == last_assigned_pos {
1160                    break;
1161                }
1162
1163                src_pos = self.find_edit_position_from(src_pos + 1, FORWARD);
1164                dst_pos = self.find_edit_position_from(dst_pos + 1, FORWARD);
1165            }
1166
1167            // Shifting characters is a side effect; update the hint if no
1168            // characters are removed (which would be success).
1169            if MaskedTextResultHint::SideEffect > result_hint {
1170                result_hint = MaskedTextResultHint::SideEffect;
1171            }
1172
1173            if test_only {
1174                return (true, test_position, result_hint);
1175            }
1176
1177            // The test passed: shift the characters.
1178            src_pos = shift_start;
1179            dst_pos = start_position;
1180
1181            loop {
1182                let src_ch = self.test_string[src_pos as usize];
1183                let is_assigned = self.string_descriptor[src_pos as usize].is_assigned;
1184
1185                // A prompt at an unassigned position just resets the destination.
1186                if src_ch == self.prompt_char && !is_assigned {
1187                    self.reset_char(dst_pos);
1188                } else {
1189                    self.set_char(src_ch, dst_pos);
1190                    self.reset_char(src_pos);
1191                }
1192
1193                if src_pos == last_assigned_pos {
1194                    break;
1195                }
1196
1197                src_pos = self.find_edit_position_from(src_pos + 1, FORWARD);
1198                dst_pos = self.find_edit_position_from(dst_pos + 1, FORWARD);
1199            }
1200
1201            // Reset the remaining characters from dst_pos + 1 to the end position.
1202            start_position = dst_pos + 1;
1203        }
1204
1205        if start_position <= end_position {
1206            self.reset_string(start_position, end_position);
1207        }
1208
1209        (true, test_position, result_hint)
1210    }
1211
1212    /// Replaces the character at `position` (or the first edit position after
1213    /// it) with `input`.
1214    pub fn replace_char(&mut self, input: char, position: i32) -> bool {
1215        self.replace_char_with_hint(input, position).0
1216    }
1217
1218    /// As [`Self::replace_char`]; also returns the position of the replaced
1219    /// character (on failure: the failing position) and the result hint.
1220    pub fn replace_char_with_hint(&mut self, input: char, position: i32) -> (bool, i32, MaskedTextResultHint) {
1221        if position < 0 || position >= self.length() {
1222            return (false, position, MaskedTextResultHint::PositionOutOfRange);
1223        }
1224
1225        let mut test_position = position;
1226
1227        // If the character is not to be escaped, find the first edit position to test it in.
1228        if !self.test_escape_char(input, test_position) {
1229            test_position = self.find_edit_position_from(test_position, FORWARD);
1230        }
1231
1232        if test_position == INVALID_INDEX {
1233            return (false, position, MaskedTextResultHint::UnavailableEditPosition);
1234        }
1235
1236        let (result, hint) = self.test_set_char(input, test_position);
1237        (result, test_position, hint)
1238    }
1239
1240    /// Replaces the characters in the inclusive range with `input`, removing
1241    /// the rest of the range and shifting the characters after it left.
1242    pub fn replace_char_range_with_hint(&mut self, input: char, start_position: i32, end_position: i32) -> (bool, i32, MaskedTextResultHint) {
1243        if end_position >= self.length() {
1244            return (false, end_position, MaskedTextResultHint::PositionOutOfRange);
1245        }
1246
1247        if start_position < 0 || start_position > end_position {
1248            return (false, start_position, MaskedTextResultHint::PositionOutOfRange);
1249        }
1250
1251        if start_position == end_position {
1252            let (result, hint) = self.test_set_char(input, start_position);
1253            return (result, start_position, hint);
1254        }
1255
1256        let mut buffer = [0u8; 4];
1257        self.replace_range_with_hint(input.encode_utf8(&mut buffer), start_position, end_position)
1258    }
1259
1260    /// Replaces the characters from `position` with the characters of
1261    /// `input`, without shifting. An empty input removes the character at
1262    /// `position`.
1263    pub fn replace(&mut self, input: &str, position: i32) -> bool {
1264        self.replace_with_hint(input, position).0
1265    }
1266
1267    /// As [`Self::replace`]; also returns the position of the last replaced
1268    /// character (on failure: the failing position) and the result hint.
1269    pub fn replace_with_hint(&mut self, input: &str, position: i32) -> (bool, i32, MaskedTextResultHint) {
1270        if position < 0 || position >= self.length() {
1271            return (false, position, MaskedTextResultHint::PositionOutOfRange);
1272        }
1273
1274        if input.is_empty() {
1275            // Remove the character at position.
1276            return self.remove_at_range_with_hint(position, position);
1277        }
1278
1279        // Replace the characters with the ones in the input.
1280        self.test_set_string(input, position)
1281    }
1282
1283    /// Replaces the characters in the inclusive range with the characters of
1284    /// `input`: when the input is shorter than the range the rest of the
1285    /// range is removed, when it is longer the characters after the range
1286    /// shift right. An empty input removes the range.
1287    pub fn replace_range_with_hint(&mut self, input: &str, start_position: i32, end_position: i32) -> (bool, i32, MaskedTextResultHint) {
1288        if end_position >= self.length() {
1289            return (false, end_position, MaskedTextResultHint::PositionOutOfRange);
1290        }
1291
1292        if start_position < 0 || start_position > end_position {
1293            return (false, start_position, MaskedTextResultHint::PositionOutOfRange);
1294        }
1295
1296        if input.is_empty() {
1297            // Remove the characters of the range.
1298            return self.remove_at_range_with_hint(start_position, end_position);
1299        }
1300
1301        // Three cases:
1302        // 1. The input covers exactly the range (or nothing is assigned): just replace.
1303        // 2. The input is shorter: replace, and remove the rest of the range.
1304        // 3. The input is longer: replace the range and insert the rest.
1305
1306        // Test the input string first; the last test position tells which case it is.
1307        let (result, mut test_position, mut result_hint) = self.test_string(input, start_position);
1308        if !result {
1309            return (false, test_position, result_hint);
1310        }
1311
1312        if self.assigned_char_count > 0 {
1313            if test_position < end_position {
1314                // Case 2. Replace + remove the remaining characters.
1315                let (ok, temp_pos, temp_hint) = self.remove_at_int(test_position + 1, end_position, false);
1316                if !ok {
1317                    return (false, temp_pos, temp_hint);
1318                }
1319
1320                // If the current hint is not success and characters were removed, the hint is side effect.
1321                if temp_hint == MaskedTextResultHint::Success && result_hint != temp_hint {
1322                    result_hint = MaskedTextResultHint::SideEffect;
1323                }
1324            } else if test_position > end_position {
1325                // Case 3. Replace + insert: test shifting the existing
1326                // characters to make room for the rest of the input.
1327                let last_assigned_pos = self.last_assigned_position();
1328                let mut dst_pos = test_position + 1;
1329                let mut src_pos = end_position + 1;
1330
1331                loop {
1332                    src_pos = self.find_edit_position_from(src_pos, FORWARD);
1333                    dst_pos = self.find_edit_position_from(dst_pos, FORWARD);
1334
1335                    if dst_pos == INVALID_INDEX {
1336                        test_position = self.length();
1337                        return (false, test_position, MaskedTextResultHint::UnavailableEditPosition);
1338                    }
1339
1340                    let (ok, temp_hint) = self.test_char(self.test_string[src_pos as usize], dst_pos);
1341                    if !ok {
1342                        return (false, dst_pos, temp_hint);
1343                    }
1344
1345                    // If the current hint is not success and a character is actually shifted, the hint is success.
1346                    if temp_hint == MaskedTextResultHint::Success && result_hint != temp_hint {
1347                        result_hint = MaskedTextResultHint::Success;
1348                    }
1349
1350                    if src_pos == last_assigned_pos {
1351                        break;
1352                    }
1353
1354                    src_pos += 1;
1355                    dst_pos += 1;
1356                }
1357
1358                // The shift test passed, now do it.
1359                while dst_pos > test_position {
1360                    self.set_char(self.test_string[src_pos as usize], dst_pos);
1361
1362                    src_pos = self.find_edit_position_from(src_pos - 1, BACKWARD);
1363                    dst_pos = self.find_edit_position_from(dst_pos - 1, BACKWARD);
1364                }
1365            }
1366
1367            // Otherwise end_position == test_position: replacing the range is the same as setting it.
1368        }
1369
1370        // In all cases the input replaces the characters.
1371        self.set_string(input, start_position);
1372
1373        (true, test_position, result_hint)
1374    }
1375
1376    /// Resets the edit position (if it is one and it is assigned).
1377    fn reset_char(&mut self, test_position: i32) {
1378        let descriptor = &mut self.string_descriptor[test_position as usize];
1379
1380        if descriptor.is_edit_position() && descriptor.is_assigned {
1381            descriptor.is_assigned = false;
1382            self.test_string[test_position as usize] = self.prompt_char;
1383            self.assigned_char_count -= 1;
1384
1385            if descriptor.char_type == CharType::EditRequired {
1386                self.required_char_count -= 1;
1387            }
1388
1389            debug_assert!(self.assigned_char_count >= 0, "Invalid count of assigned chars.");
1390        }
1391    }
1392
1393    /// Resets the assigned edit positions in the inclusive range.
1394    fn reset_string(&mut self, mut start_position: i32, mut end_position: i32) {
1395        start_position = self.find_assigned_edit_position_from(start_position, FORWARD);
1396
1397        if start_position != INVALID_INDEX {
1398            end_position = self.find_assigned_edit_position_from(end_position, BACKWARD);
1399
1400            while start_position <= end_position {
1401                start_position = self.find_assigned_edit_position_from(start_position, FORWARD);
1402                self.reset_char(start_position);
1403                start_position += 1;
1404            }
1405        }
1406    }
1407
1408    /// Sets the test string to `input`: the previous content is replaced.
1409    /// Nothing changes when the input does not fit the mask.
1410    pub fn set(&mut self, input: &str) -> bool {
1411        self.set_with_hint(input).0
1412    }
1413
1414    /// As [`Self::set`]; also returns the position of the last character set
1415    /// (on failure: the failing position) and the result hint.
1416    pub fn set_with_hint(&mut self, input: &str) -> (bool, i32, MaskedTextResultHint) {
1417        if input.is_empty() {
1418            // Clearing the input text.
1419            let hint = self.clear_with_hint();
1420            return (true, 0, hint);
1421        }
1422
1423        let (result, test_position, result_hint) = self.test_set_string(input, 0);
1424        if !result {
1425            return (false, test_position, result_hint);
1426        }
1427
1428        // Reset the remaining characters (if any).
1429        let reset_pos = self.find_assigned_edit_position_from(test_position + 1, FORWARD);
1430
1431        if reset_pos != INVALID_INDEX {
1432            self.reset_string(reset_pos, self.length() - 1);
1433        }
1434
1435        (true, test_position, result_hint)
1436    }
1437
1438    /// Sets (or, when the character is to be escaped, resets) the character
1439    /// at `position`. The character must have been tested.
1440    fn set_char(&mut self, mut input: char, position: i32) {
1441        // A space or prompt that is to be escaped resets the position if it
1442        // is assigned; this does not affect literal positions.
1443        if self.test_escape_char(input, position) {
1444            self.reset_char(position);
1445            return;
1446        }
1447
1448        let descriptor = self.string_descriptor[position as usize];
1449        debug_assert!(!descriptor.is_literal_position(), "Setting char in literal position.");
1450
1451        if is_letter(input) {
1452            if is_upper(input) {
1453                if descriptor.case_conversion == CaseConversion::ToLower {
1454                    input = to_lower(input);
1455                }
1456            } else if descriptor.case_conversion == CaseConversion::ToUpper {
1457                // The character is lower case.
1458                input = to_upper(input);
1459            }
1460        }
1461
1462        self.test_string[position as usize] = input;
1463
1464        if !descriptor.is_assigned {
1465            // The position was not counted as assigned yet.
1466            self.string_descriptor[position as usize].is_assigned = true;
1467            self.assigned_char_count += 1;
1468
1469            if descriptor.char_type == CharType::EditRequired {
1470                self.required_char_count += 1;
1471            }
1472        }
1473    }
1474
1475    /// Sets the characters of `input` from `test_position`. The string must
1476    /// have been tested.
1477    fn set_string(&mut self, input: &str, mut test_position: i32) {
1478        for ch in input.chars() {
1479            // If the character is not to be escaped, find the first edit position to set it in.
1480            if !self.test_escape_char(ch, test_position) {
1481                test_position = self.find_edit_position_from(test_position, FORWARD);
1482            }
1483
1484            self.set_char(ch, test_position);
1485            test_position += 1;
1486        }
1487    }
1488
1489    /// Tests `input` against the mask at `position` (which must be in range).
1490    fn test_char(&self, input: char, position: i32) -> (bool, MaskedTextResultHint) {
1491        if !is_printable_char(input) {
1492            return (false, MaskedTextResultHint::InvalidInput);
1493        }
1494
1495        let descriptor = &self.string_descriptor[position as usize];
1496        let current = self.test_string[position as usize];
1497
1498        // Test whether the character should be accepted as a literal.
1499        if descriptor.is_literal_position() {
1500            if self.skip_literals && input == current {
1501                return (true, MaskedTextResultHint::CharacterEscaped);
1502            }
1503
1504            return (false, MaskedTextResultHint::NonEditPosition);
1505        }
1506
1507        if input == self.prompt_char {
1508            if self.reset_on_prompt {
1509                // The test does not fail for the prompt when it is to be escaped.
1510                return if descriptor.is_edit_position() && descriptor.is_assigned {
1511                    // The position would be reset.
1512                    (true, MaskedTextResultHint::SideEffect)
1513                } else {
1514                    (true, MaskedTextResultHint::CharacterEscaped)
1515                };
1516            }
1517
1518            // Escaping precedes allow_prompt_as_input.
1519            if !self.allow_prompt_as_input {
1520                return (false, MaskedTextResultHint::PromptCharNotAllowed);
1521            }
1522        }
1523
1524        if input == SPACE_CHAR && self.reset_on_space {
1525            return if descriptor.is_edit_position() && descriptor.is_assigned {
1526                // The position would be reset.
1527                (true, MaskedTextResultHint::SideEffect)
1528            } else {
1529                (true, MaskedTextResultHint::CharacterEscaped)
1530            };
1531        }
1532
1533        // The character was not escaped: test it against the mask
1534        // constraints. A space passes when the element is optional.
1535        match self.mask_chars[descriptor.mask_position] {
1536            // Digit or plus/minus sign, optional.
1537            '#' => {
1538                if !is_digit(input) && input != '-' && input != '+' && input != SPACE_CHAR {
1539                    return (false, MaskedTextResultHint::DigitExpected);
1540                }
1541            }
1542            // Digit, required.
1543            '0' => {
1544                if !is_digit(input) {
1545                    return (false, MaskedTextResultHint::DigitExpected);
1546                }
1547            }
1548            // Digit, optional.
1549            '9' => {
1550                if !is_digit(input) && input != SPACE_CHAR {
1551                    return (false, MaskedTextResultHint::DigitExpected);
1552                }
1553            }
1554            // Letter, required.
1555            'L' => {
1556                if !is_letter(input) {
1557                    return (false, MaskedTextResultHint::LetterExpected);
1558                }
1559                if !is_ascii_letter(input) && self.ascii_only {
1560                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1561                }
1562            }
1563            // Letter, optional.
1564            '?' => {
1565                if !is_letter(input) && input != SPACE_CHAR {
1566                    return (false, MaskedTextResultHint::LetterExpected);
1567                }
1568                if !is_ascii_letter(input) && self.ascii_only {
1569                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1570                }
1571            }
1572            // Any character, required.
1573            '&' => {
1574                if !is_ascii(input) && self.ascii_only {
1575                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1576                }
1577            }
1578            // Any character, optional.
1579            'C' => {
1580                if (!is_ascii(input) && self.ascii_only) && input != SPACE_CHAR {
1581                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1582                }
1583            }
1584            // Alphanumeric, required.
1585            'A' => {
1586                if !is_alphanumeric(input) {
1587                    return (false, MaskedTextResultHint::AlphanumericCharacterExpected);
1588                }
1589                if !is_ascii_alphanumeric(input) && self.ascii_only {
1590                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1591                }
1592            }
1593            // Alphanumeric, optional.
1594            'a' => {
1595                if !is_alphanumeric(input) && input != SPACE_CHAR {
1596                    return (false, MaskedTextResultHint::AlphanumericCharacterExpected);
1597                }
1598                if !is_ascii_alphanumeric(input) && self.ascii_only {
1599                    return (false, MaskedTextResultHint::AsciiCharacterExpected);
1600                }
1601            }
1602            _ => debug_assert!(false, "Invalid mask language character."),
1603        }
1604
1605        // The test passed.
1606        if input == current && descriptor.is_assigned {
1607            // Setting the character would not make any difference.
1608            (true, MaskedTextResultHint::NoEffect)
1609        } else {
1610            (true, MaskedTextResultHint::Success)
1611        }
1612    }
1613
1614    /// Whether `input` would be escaped at `position` (which must be in
1615    /// range): a literal equal to the input that is skipped, or a prompt or
1616    /// space that resets an edit position.
1617    fn test_escape_char(&self, input: char, position: i32) -> bool {
1618        let descriptor = &self.string_descriptor[position as usize];
1619
1620        if descriptor.is_literal_position() {
1621            // Should the literal be skipped?
1622            return self.skip_literals && input == self.test_string[position as usize];
1623        }
1624
1625        (self.reset_on_prompt && input == self.prompt_char) || (self.reset_on_space && input == SPACE_CHAR)
1626    }
1627
1628    /// Tests the character and sets it when the test passes.
1629    fn test_set_char(&mut self, input: char, position: i32) -> (bool, MaskedTextResultHint) {
1630        let (result, hint) = self.test_char(input, position);
1631
1632        if result {
1633            // The character is not to be escaped.
1634            if hint == MaskedTextResultHint::Success || hint == MaskedTextResultHint::SideEffect {
1635                self.set_char(input, position);
1636            }
1637
1638            return (true, hint);
1639        }
1640
1641        (false, hint)
1642    }
1643
1644    /// Tests the string and sets it when the test passes.
1645    fn test_set_string(&mut self, input: &str, position: i32) -> (bool, i32, MaskedTextResultHint) {
1646        let (result, test_position, hint) = self.test_string(input, position);
1647
1648        if result {
1649            self.set_string(input, position);
1650        }
1651
1652        (result, test_position, hint)
1653    }
1654
1655    /// Tests the characters of `input` from `position`. Returns the position
1656    /// of the last character tested (on failure: the failing position, or the
1657    /// length of the test string when it ran out of edit positions).
1658    fn test_string(&self, input: &str, position: i32) -> (bool, i32, MaskedTextResultHint) {
1659        debug_assert!(position >= 0, "Position out of range.");
1660
1661        let mut result_hint = MaskedTextResultHint::Unknown;
1662        let mut test_position = position;
1663
1664        if input.is_empty() {
1665            // Nothing to test.
1666            return (true, test_position, result_hint);
1667        }
1668
1669        // If any character is actually accepted the hint is success,
1670        // otherwise whatever the result of the last character is.
1671        for ch in input.chars() {
1672            if test_position >= self.length() {
1673                return (false, test_position, MaskedTextResultHint::UnavailableEditPosition);
1674            }
1675
1676            // If the character is not to be escaped, find an edit position to test it in.
1677            if !self.test_escape_char(ch, test_position) {
1678                test_position = self.find_edit_position_from(test_position, FORWARD);
1679
1680                if test_position == INVALID_INDEX {
1681                    return (false, self.length(), MaskedTextResultHint::UnavailableEditPosition);
1682                }
1683            }
1684
1685            // Test the character at the test position.
1686            let (ok, temp_hint) = self.test_char(ch, test_position);
1687            if !ok {
1688                return (false, test_position, temp_hint);
1689            }
1690
1691            // Result precedence: Success, SideEffect, NoEffect, CharacterEscaped.
1692            if temp_hint > result_hint {
1693                result_hint = temp_hint;
1694            }
1695
1696            test_position += 1;
1697        }
1698
1699        // The last tested position.
1700        test_position -= 1;
1701
1702        (true, test_position, result_hint)
1703    }
1704
1705    /// The formatted string as it is to be displayed: prompts and literals
1706    /// included, and the input replaced by the password character if there is
1707    /// one.
1708    pub fn to_display_string(&self) -> String {
1709        if !self.is_password() || self.assigned_char_count == 0 {
1710            // The test string contains the formatted text.
1711            return self.test_string.iter().collect();
1712        }
1713
1714        // Copy the test string, replacing the input with the password character.
1715        self.test_string
1716            .iter()
1717            .zip(&self.string_descriptor)
1718            .map(|(&ch, descriptor)| {
1719                if descriptor.is_edit_position() && descriptor.is_assigned {
1720                    self.password_char
1721                } else {
1722                    ch
1723                }
1724            })
1725            .collect()
1726    }
1727
1728    /// The formatted string, with prompts and literals according to
1729    /// [`Self::include_prompt`] and [`Self::include_literals`]; the password
1730    /// character is used unless `ignore_password_char`.
1731    pub fn to_string_ignore_password(&self, ignore_password_char: bool) -> String {
1732        self.to_string_with(ignore_password_char, self.include_prompt, self.include_literals, 0, self.length())
1733    }
1734
1735    /// A part of the formatted string, with prompts and literals according to
1736    /// [`Self::include_prompt`] and [`Self::include_literals`], ignoring the
1737    /// password character.
1738    pub fn to_string_range(&self, start_position: i32, length: i32) -> String {
1739        self.to_string_with(true, self.include_prompt, self.include_literals, start_position, length)
1740    }
1741
1742    /// A part of the formatted string, with prompts and literals according to
1743    /// [`Self::include_prompt`] and [`Self::include_literals`]; the password
1744    /// character is used unless `ignore_password_char`.
1745    pub fn to_string_ignore_password_range(&self, ignore_password_char: bool, start_position: i32, length: i32) -> String {
1746        self.to_string_with(ignore_password_char, self.include_prompt, self.include_literals, start_position, length)
1747    }
1748
1749    /// The formatted string with or without prompts and literals, ignoring
1750    /// the password character.
1751    pub fn to_string_include(&self, include_prompt: bool, include_literals: bool) -> String {
1752        self.to_string_with(true, include_prompt, include_literals, 0, self.length())
1753    }
1754
1755    /// A part of the formatted string with or without prompts and literals,
1756    /// ignoring the password character.
1757    pub fn to_string_include_range(&self, include_prompt: bool, include_literals: bool, start_position: i32, length: i32) -> String {
1758        self.to_string_with(true, include_prompt, include_literals, start_position, length)
1759    }
1760
1761    /// A part of the formatted string.
1762    ///
1763    /// * `ignore_password_char` — show the input even when there is a
1764    ///   password character.
1765    /// * `include_prompt` — keep the prompt at unassigned positions; without
1766    ///   it they become spaces, and the string ends at the last assigned
1767    ///   position (or the last literal, when literals are included).
1768    /// * `include_literals` — keep the literals and separators.
1769    /// * `start_position`, `length` — the part of the test string; clamped to
1770    ///   it.
1771    pub fn to_string_with(
1772        &self,
1773        ignore_password_char: bool,
1774        include_prompt: bool,
1775        include_literals: bool,
1776        mut start_position: i32,
1777        mut length: i32,
1778    ) -> String {
1779        if length <= 0 {
1780            return String::new();
1781        }
1782
1783        if start_position < 0 {
1784            start_position = 0;
1785        }
1786
1787        if start_position >= self.length() {
1788            return String::new();
1789        }
1790
1791        let max_length = self.length() - start_position;
1792
1793        if length > max_length {
1794            length = max_length;
1795        }
1796
1797        // The text may not need to be formatted.
1798        if (!self.is_password() || ignore_password_char) && include_prompt && include_literals {
1799            // The test string contains just what is wanted.
1800            return self.test_string[start_position as usize..(start_position + length) as usize].iter().collect();
1801        }
1802
1803        // Build the formatted string.
1804        let mut st = String::new();
1805        let mut last_position = start_position + length - 1;
1806
1807        if !include_prompt {
1808            // Without prompts the string ends at the last assigned character
1809            // (or literal, when literals are included).
1810            let last_literal_pos = if include_literals {
1811                self.find_non_edit_position_in_range(start_position, last_position, BACKWARD)
1812            } else {
1813                INVALID_INDEX
1814            };
1815            let last_assigned_pos = self.find_assigned_edit_position_in_range(
1816                if last_literal_pos == INVALID_INDEX { start_position } else { last_literal_pos },
1817                last_position,
1818                BACKWARD,
1819            );
1820
1821            // If there is an assigned position after the last literal, it is the last position to include.
1822            last_position = if last_assigned_pos != INVALID_INDEX { last_assigned_pos } else { last_literal_pos };
1823
1824            if last_position == INVALID_INDEX {
1825                return String::new();
1826            }
1827        }
1828
1829        for position in start_position..=last_position {
1830            let ch = self.test_string[position as usize];
1831            let descriptor = &self.string_descriptor[position as usize];
1832
1833            match descriptor.char_type {
1834                CharType::EditOptional | CharType::EditRequired => {
1835                    if descriptor.is_assigned {
1836                        if self.is_password() && !ignore_password_char {
1837                            // Replace the input with the password character.
1838                            st.push(self.password_char);
1839                            continue;
1840                        }
1841                    } else if !include_prompt {
1842                        // Replace the prompt with a space.
1843                        st.push(SPACE_CHAR);
1844                        continue;
1845                    }
1846
1847                    st.push(ch);
1848                }
1849                CharType::Separator | CharType::Literal => {
1850                    if include_literals {
1851                        st.push(ch);
1852                    }
1853                }
1854            }
1855        }
1856
1857        st
1858    }
1859
1860    /// Tests whether `input` would be accepted at `position`, without
1861    /// changing anything. Returns the result and its hint.
1862    pub fn verify_char(&self, input: char, position: i32) -> (bool, MaskedTextResultHint) {
1863        if position < 0 || position >= self.length() {
1864            return (false, MaskedTextResultHint::PositionOutOfRange);
1865        }
1866
1867        self.test_char(input, position)
1868    }
1869
1870    /// Tests whether `input` would be escaped at `position`: skipped as a
1871    /// matching literal, or taken as a reset (prompt or space).
1872    pub fn verify_escape_char(&self, input: char, position: i32) -> bool {
1873        if position < 0 || position >= self.length() {
1874            return false;
1875        }
1876
1877        self.test_escape_char(input, position)
1878    }
1879
1880    /// Tests whether `input` could be set ([`Self::set`]), without changing
1881    /// anything.
1882    pub fn verify_string(&self, input: &str) -> bool {
1883        self.verify_string_with_hint(input).0
1884    }
1885
1886    /// As [`Self::verify_string`]; also returns the position of the last
1887    /// character tested (on failure: the failing position) and the result
1888    /// hint.
1889    pub fn verify_string_with_hint(&self, input: &str) -> (bool, i32, MaskedTextResultHint) {
1890        if input.is_empty() {
1891            // Nothing to verify.
1892            return (true, 0, MaskedTextResultHint::NoEffect);
1893        }
1894
1895        self.test_string(input, 0)
1896    }
1897}
1898
1899/// The formatted string with prompts and literals according to
1900/// [`MaskedTextProvider::include_prompt`] and
1901/// [`MaskedTextProvider::include_literals`], ignoring the password character.
1902impl fmt::Display for MaskedTextProvider {
1903    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1904        f.write_str(&self.to_string_with(true, self.include_prompt, self.include_literals, 0, self.length()))
1905    }
1906}
1907
1908/// The character of the test string at an index; panics when it is out of
1909/// range.
1910impl std::ops::Index<i32> for MaskedTextProvider {
1911    type Output = char;
1912
1913    fn index(&self, index: i32) -> &char {
1914        if index < 0 || index >= self.length() {
1915            panic!("index {index} is out of range of the test string (length {})", self.length());
1916        }
1917
1918        &self.test_string[index as usize]
1919    }
1920}
1921
1922// ---- Character classification (per UTF-16 code unit in the original) ----
1923
1924/// The general category of a character; `None` outside the BMP, where the
1925/// original sees surrogates, which belong to none of the tested categories.
1926#[inline]
1927fn category(c: char) -> Option<GeneralCategory> {
1928    let value = c as u32;
1929    if value > 0xFFFF {
1930        return None;
1931    }
1932
1933    Some(Codepoint::new(value).general_category())
1934}
1935
1936/// Lu, Ll, Lt, Lm, Lo.
1937fn is_letter(c: char) -> bool {
1938    if c.is_ascii() {
1939        return c.is_ascii_alphabetic();
1940    }
1941
1942    matches!(
1943        category(c),
1944        Some(
1945            GeneralCategory::UppercaseLetter
1946                | GeneralCategory::LowercaseLetter
1947                | GeneralCategory::TitlecaseLetter
1948                | GeneralCategory::ModifierLetter
1949                | GeneralCategory::OtherLetter
1950        )
1951    )
1952}
1953
1954/// Nd.
1955fn is_digit(c: char) -> bool {
1956    if c.is_ascii() {
1957        return c.is_ascii_digit();
1958    }
1959
1960    category(c) == Some(GeneralCategory::DecimalNumber)
1961}
1962
1963/// Lu.
1964fn is_upper(c: char) -> bool {
1965    if c.is_ascii() {
1966        return c.is_ascii_uppercase();
1967    }
1968
1969    category(c) == Some(GeneralCategory::UppercaseLetter)
1970}
1971
1972/// Pc, Pd, Ps, Pe, Pi, Pf, Po.
1973fn is_punctuation(c: char) -> bool {
1974    matches!(
1975        category(c),
1976        Some(
1977            GeneralCategory::ConnectorPunctuation
1978                | GeneralCategory::DashPunctuation
1979                | GeneralCategory::OpenPunctuation
1980                | GeneralCategory::ClosePunctuation
1981                | GeneralCategory::InitialPunctuation
1982                | GeneralCategory::FinalPunctuation
1983                | GeneralCategory::OtherPunctuation
1984        )
1985    )
1986}
1987
1988/// Sm, Sc, Sk, So.
1989fn is_symbol(c: char) -> bool {
1990    matches!(
1991        category(c),
1992        Some(GeneralCategory::MathSymbol | GeneralCategory::CurrencySymbol | GeneralCategory::ModifierSymbol | GeneralCategory::OtherSymbol)
1993    )
1994}
1995
1996/// The characters that are valid in a mask and as input.
1997fn is_printable_char(c: char) -> bool {
1998    is_letter(c) || is_digit(c) || is_punctuation(c) || is_symbol(c) || c == SPACE_CHAR
1999}
2000
2001/// The printable ASCII characters, the space excluded (`!` to `~`).
2002fn is_ascii(c: char) -> bool {
2003    ('!'..='~').contains(&c)
2004}
2005
2006fn is_ascii_alphanumeric(c: char) -> bool {
2007    c.is_ascii_alphanumeric()
2008}
2009
2010fn is_alphanumeric(c: char) -> bool {
2011    is_letter(c) || is_digit(c)
2012}
2013
2014fn is_ascii_letter(c: char) -> bool {
2015    c.is_ascii_alphabetic()
2016}
2017
2018/// The lower case of a character, when it is a single BMP character.
2019fn to_lower(c: char) -> char {
2020    let mut mapped = c.to_lowercase();
2021    match (mapped.next(), mapped.next()) {
2022        (Some(lower), None) if (lower as u32) <= 0xFFFF => lower,
2023        _ => c,
2024    }
2025}
2026
2027/// The upper case of a character, when it is a single BMP character.
2028fn to_upper(c: char) -> char {
2029    // The Greek letters with an iota subscript have no single upper case
2030    // character; the simple case mapping the original uses gives their title
2031    // case form.
2032    match c {
2033        '\u{1F80}'..='\u{1F87}' | '\u{1F90}'..='\u{1F97}' | '\u{1FA0}'..='\u{1FA7}' => {
2034            return char::from_u32(c as u32 + 8).unwrap_or(c);
2035        }
2036        '\u{1FB3}' | '\u{1FC3}' | '\u{1FF3}' => return char::from_u32(c as u32 + 9).unwrap_or(c),
2037        _ => {}
2038    }
2039
2040    let mut mapped = c.to_uppercase();
2041    match (mapped.next(), mapped.next()) {
2042        (Some(upper), None) if (upper as u32) <= 0xFFFF => upper,
2043        _ => c,
2044    }
2045}
2046
2047#[cfg(test)]
2048mod tests {
2049    // The expectations of these tests were checked against the original
2050    // class running on .NET 10.
2051    use super::MaskedTextResultHint as H;
2052    use super::*;
2053
2054    fn provider(mask: &str) -> MaskedTextProvider {
2055        MaskedTextProvider::new(mask).unwrap()
2056    }
2057
2058    fn filled(mask: &str, input: &str) -> MaskedTextProvider {
2059        let mut p = provider(mask);
2060        assert!(p.add(input), "{input:?} does not fit {mask:?}");
2061        p
2062    }
2063
2064    fn en_us() -> CultureInfo {
2065        culture_with_separators("en-US", ".", ",", "/", ":", "$")
2066    }
2067
2068    // ---- Construction, defaults, properties ----
2069
2070    #[test]
2071    fn result_hint_values() {
2072        assert_eq!(H::Unknown as i32, 0);
2073        assert_eq!(H::CharacterEscaped as i32, 1);
2074        assert_eq!(H::NoEffect as i32, 2);
2075        assert_eq!(H::SideEffect as i32, 3);
2076        assert_eq!(H::Success as i32, 4);
2077        assert_eq!(H::AsciiCharacterExpected as i32, -1);
2078        assert_eq!(H::AlphanumericCharacterExpected as i32, -2);
2079        assert_eq!(H::DigitExpected as i32, -3);
2080        assert_eq!(H::LetterExpected as i32, -4);
2081        assert_eq!(H::SignedDigitExpected as i32, -5);
2082        assert_eq!(H::InvalidInput as i32, -51);
2083        assert_eq!(H::PromptCharNotAllowed as i32, -52);
2084        assert_eq!(H::UnavailableEditPosition as i32, -53);
2085        assert_eq!(H::NonEditPosition as i32, -54);
2086        assert_eq!(H::PositionOutOfRange as i32, -55);
2087    }
2088
2089    #[test]
2090    fn get_operation_result_from_hint_is_true_for_positive_hints() {
2091        for hint in [H::CharacterEscaped, H::NoEffect, H::SideEffect, H::Success] {
2092            assert!(MaskedTextProvider::get_operation_result_from_hint(hint));
2093        }
2094        for hint in [H::Unknown, H::AsciiCharacterExpected, H::DigitExpected, H::InvalidInput, H::PositionOutOfRange] {
2095            assert!(!MaskedTextProvider::get_operation_result_from_hint(hint));
2096        }
2097    }
2098
2099    #[test]
2100    fn constructor_defaults() {
2101        let p = provider("00/00");
2102
2103        assert_eq!(p.mask(), "00/00");
2104        assert!(p.allow_prompt_as_input());
2105        assert!(!p.ascii_only());
2106        assert_eq!(p.prompt_char(), '_');
2107        assert_eq!(p.password_char(), '\0');
2108        assert!(!p.is_password());
2109        assert!(p.include_literals());
2110        assert!(!p.include_prompt());
2111        assert!(p.reset_on_prompt());
2112        assert!(p.reset_on_space());
2113        assert!(p.skip_literals());
2114        assert_eq!(p.culture(), &CultureInfo::current_culture());
2115        assert_eq!(p.length(), 5);
2116        assert_eq!(p.edit_position_count(), 4);
2117        assert_eq!(p.assigned_edit_position_count(), 0);
2118        assert_eq!(p.available_edit_position_count(), 4);
2119        assert_eq!(p.last_assigned_position(), -1);
2120        assert!(!p.mask_completed());
2121        assert!(!p.mask_full());
2122        assert_eq!(p.to_display_string(), "__/__");
2123        assert_eq!(p.to_string(), "  /");
2124        assert_eq!(MaskedTextProvider::default_password_char(), '*');
2125        assert_eq!(MaskedTextProvider::DEFAULT_PASSWORD_CHAR, '*');
2126        assert_eq!(MaskedTextProvider::invalid_index(), -1);
2127        assert_eq!(MaskedTextProvider::INVALID_INDEX, -1);
2128    }
2129
2130    #[test]
2131    fn constructor_overloads() {
2132        assert!(MaskedTextProvider::new_with_ascii("LL", true).unwrap().ascii_only());
2133        assert_eq!(MaskedTextProvider::new_with_culture("$0", Some(en_us())).unwrap().to_display_string(), "$_");
2134        let p = MaskedTextProvider::new_with_culture_ascii("$0", Some(en_us()), true).unwrap();
2135        assert!(p.ascii_only());
2136        assert_eq!(p.culture(), &en_us());
2137
2138        let mut p = MaskedTextProvider::new_with_password("00", '*', false).unwrap();
2139        assert!(!p.allow_prompt_as_input());
2140        assert!(p.is_password());
2141        assert!(p.add("12"));
2142        assert_eq!(p.to_display_string(), "**");
2143        assert_eq!(p.to_string(), "12");
2144
2145        let p = MaskedTextProvider::new_with_culture_password("$0", Some(en_us()), '#', true).unwrap();
2146        assert_eq!(p.password_char(), '#');
2147        assert!(p.allow_prompt_as_input());
2148
2149        let p = MaskedTextProvider::new_full("00", None, false, '#', 'x', true).unwrap();
2150        assert_eq!(p.prompt_char(), '#');
2151        assert_eq!(p.password_char(), 'x');
2152        assert!(p.ascii_only());
2153        assert!(!p.allow_prompt_as_input());
2154        assert_eq!(p.to_display_string(), "##");
2155    }
2156
2157    #[test]
2158    fn constructor_rejects_empty_and_invalid_masks() {
2159        assert_eq!(MaskedTextProvider::new("").unwrap_err(), MaskedTextProviderError::MaskNullOrEmpty);
2160        assert_eq!(MaskedTextProvider::new("0\n").unwrap_err(), MaskedTextProviderError::MaskInvalidChar);
2161        assert_eq!(MaskedTextProvider::new("0\t0").unwrap_err(), MaskedTextProviderError::MaskInvalidChar);
2162        // A character outside the BMP is a surrogate pair in the original, which is not valid.
2163        assert_eq!(MaskedTextProvider::new("0\u{1F600}").unwrap_err(), MaskedTextProviderError::MaskInvalidChar);
2164        assert!(!MaskedTextProviderError::MaskNullOrEmpty.to_string().is_empty());
2165    }
2166
2167    #[test]
2168    fn constructor_does_not_validate_prompt_and_password_chars() {
2169        // Only the setters validate, as in the original.
2170        let p = MaskedTextProvider::new_full("00", None, true, '*', '*', false).unwrap();
2171        assert_eq!(p.to_display_string(), "**");
2172        assert!(MaskedTextProvider::new_full("00", None, true, '\n', '\0', false).is_ok());
2173        assert!(MaskedTextProvider::new_full("00", None, true, '_', '\n', false).is_ok());
2174    }
2175
2176    #[test]
2177    fn valid_char_predicates() {
2178        for c in ['a', 'Z', '0', '-', '$', ' ', '_', '*', '\u{434}', '\u{A4}', '+', '(', '~'] {
2179            assert!(MaskedTextProvider::is_valid_input_char(c), "{c:?}");
2180            assert!(MaskedTextProvider::is_valid_mask_char(c), "{c:?}");
2181            assert!(MaskedTextProvider::is_valid_password_char(c), "{c:?}");
2182        }
2183        for c in ['\n', '\t', '\r', '\u{7F}', '\u{A0}', '\u{1F600}'] {
2184            assert!(!MaskedTextProvider::is_valid_input_char(c), "{c:?}");
2185            assert!(!MaskedTextProvider::is_valid_mask_char(c), "{c:?}");
2186            assert!(!MaskedTextProvider::is_valid_password_char(c), "{c:?}");
2187        }
2188        assert!(!MaskedTextProvider::is_valid_input_char('\0'));
2189        assert!(!MaskedTextProvider::is_valid_mask_char('\0'));
2190        assert!(MaskedTextProvider::is_valid_password_char('\0'));
2191    }
2192
2193    #[test]
2194    fn password_char_setter_validates() {
2195        let mut p = provider("00");
2196
2197        assert_eq!(p.set_password_char('_'), Err(MaskedTextProviderError::PasswordAndPromptCharEqual));
2198        assert_eq!(p.set_password_char('\n'), Err(MaskedTextProviderError::InvalidChar));
2199        assert_eq!(p.password_char(), '\0');
2200        assert_eq!(p.set_password_char('#'), Ok(()));
2201        assert!(p.is_password());
2202        assert_eq!(p.set_password_char('\0'), Ok(()));
2203        assert!(!p.is_password());
2204    }
2205
2206    #[test]
2207    fn is_password_setter_uses_default_password_char() {
2208        let mut p = provider("00");
2209
2210        p.set_is_password(true);
2211        assert_eq!(p.password_char(), '*');
2212        p.set_is_password(false);
2213        assert_eq!(p.password_char(), '\0');
2214
2215        // An existing password character is kept.
2216        p.set_password_char('x').unwrap();
2217        p.set_is_password(true);
2218        assert_eq!(p.password_char(), 'x');
2219    }
2220
2221    #[test]
2222    fn prompt_char_setter_validates_and_updates_unassigned_positions() {
2223        let mut p = MaskedTextProvider::new_full("00-00", None, true, '_', '*', false).unwrap();
2224        assert!(p.add("1"));
2225
2226        assert_eq!(p.set_prompt_char('*'), Err(MaskedTextProviderError::PasswordAndPromptCharEqual));
2227        assert_eq!(p.set_prompt_char('\n'), Err(MaskedTextProviderError::InvalidChar));
2228        assert_eq!(p.prompt_char(), '_');
2229
2230        assert_eq!(p.set_prompt_char('#'), Ok(()));
2231        assert_eq!(p.prompt_char(), '#');
2232        assert_eq!(p.to_string_include(true, true), "1#-##");
2233        assert_eq!(p.to_display_string(), "*#-##");
2234    }
2235
2236    #[test]
2237    fn flag_setters() {
2238        let mut p = provider("00");
2239        p.set_include_literals(false);
2240        p.set_include_prompt(true);
2241        p.set_reset_on_prompt(false);
2242        p.set_reset_on_space(false);
2243        p.set_skip_literals(false);
2244        assert!(!p.include_literals());
2245        assert!(p.include_prompt());
2246        assert!(!p.reset_on_prompt());
2247        assert!(!p.reset_on_space());
2248        assert!(!p.skip_literals());
2249    }
2250
2251    #[test]
2252    fn indexer_returns_the_test_string_characters() {
2253        let p = filled("00-00", "12");
2254        assert_eq!(p[0], '1');
2255        assert_eq!(p[2], '-');
2256        assert_eq!(p[4], '_');
2257        assert_eq!(p.char_at(1), '2');
2258    }
2259
2260    #[test]
2261    #[should_panic]
2262    fn indexer_panics_past_the_end() {
2263        let p = provider("00");
2264        let _ = p[2];
2265    }
2266
2267    #[test]
2268    #[should_panic]
2269    fn char_at_panics_for_negative_index() {
2270        let p = provider("00");
2271        let _ = p.char_at(-1);
2272    }
2273
2274    #[test]
2275    fn clone_is_independent_and_keeps_the_state() {
2276        let mut p = MaskedTextProvider::new_full("00-00", Some(en_us()), false, '#', '*', true).unwrap();
2277        p.set_reset_on_prompt(false);
2278        p.set_reset_on_space(false);
2279        p.set_skip_literals(false);
2280        p.set_include_literals(false);
2281        p.set_include_prompt(true);
2282        assert!(p.add("123"));
2283
2284        let mut clone = p.clone();
2285        assert_eq!(clone.mask(), "00-00");
2286        assert_eq!(clone.culture(), &en_us());
2287        assert_eq!(clone.prompt_char(), '#');
2288        assert_eq!(clone.password_char(), '*');
2289        assert!(clone.ascii_only());
2290        assert!(!clone.allow_prompt_as_input());
2291        assert!(!clone.reset_on_prompt());
2292        assert!(!clone.reset_on_space());
2293        assert!(!clone.skip_literals());
2294        assert!(!clone.include_literals());
2295        assert!(clone.include_prompt());
2296        assert_eq!(clone.to_string_include(true, true), "12-3#");
2297        assert_eq!(clone.assigned_edit_position_count(), 3);
2298
2299        assert!(clone.add("4"));
2300        assert_eq!(clone.to_string_include(true, true), "12-34");
2301        assert_eq!(p.to_string_include(true, true), "12-3#");
2302    }
2303
2304    // ---- The mask language ----
2305
2306    #[test]
2307    fn mask_is_translated_into_the_test_string() {
2308        let p = provider("(999) 000-0000");
2309        assert_eq!(p.to_display_string(), "(___) ___-____");
2310        assert_eq!(p.edit_position_count(), 10);
2311        assert_eq!(p.edit_positions().collect::<Vec<_>>(), vec![1, 2, 3, 6, 7, 8, 10, 11, 12, 13]);
2312
2313        // Modifiers do not take a position.
2314        let p = provider(">LL<LL|LL");
2315        assert_eq!(p.length(), 6);
2316        assert_eq!(p.to_display_string(), "______");
2317
2318        // An escaped mask character is a literal.
2319        let p = provider("\\00");
2320        assert_eq!(p.to_display_string(), "0_");
2321        assert_eq!(p.edit_positions().collect::<Vec<_>>(), vec![1]);
2322        let p = provider("0\\\\0\\L\\<");
2323        assert_eq!(p.to_display_string(), "_\\_L<");
2324        assert_eq!(p.edit_position_count(), 2);
2325
2326        // A mask without edit positions is complete and full.
2327        let p = provider("xbc");
2328        assert_eq!(p.to_display_string(), "xbc");
2329        assert_eq!(p.edit_position_count(), 0);
2330        assert!(p.mask_completed());
2331        assert!(p.mask_full());
2332    }
2333
2334    #[test]
2335    fn required_and_optional_elements_drive_mask_completed_and_mask_full() {
2336        // Required: 0 L & A; optional: 9 # ? C a.
2337        let mut p = provider("0L&A9#?Ca");
2338        assert_eq!(p.edit_position_count(), 9);
2339        assert!(!p.mask_completed());
2340
2341        assert!(p.add("1bc"));
2342        assert!(!p.mask_completed());
2343        assert!(p.add("d"));
2344        assert!(p.mask_completed());
2345        assert!(!p.mask_full());
2346        assert_eq!(p.available_edit_position_count(), 5);
2347
2348        assert!(p.add("5+efg"));
2349        assert!(p.mask_full());
2350        assert_eq!(p.to_display_string(), "1bcd5+efg");
2351        assert_eq!(p.available_edit_position_count(), 0);
2352
2353        assert!(p.remove());
2354        assert!(p.mask_completed());
2355        assert!(!p.mask_full());
2356        // Removing would shift a letter into the digit position.
2357        assert!(!p.remove_at(0));
2358        assert!(p.mask_completed());
2359        // Resetting a required position makes the mask incomplete.
2360        assert!(p.replace_char(' ', 0));
2361        assert!(!p.mask_completed());
2362    }
2363
2364    #[test]
2365    fn mask_completed_with_only_optional_positions() {
2366        let p = provider("99");
2367        assert!(p.mask_completed());
2368        assert!(!p.mask_full());
2369    }
2370
2371    /// The mask used to probe every edit element with `verify_char`.
2372    const ELEMENTS: &str = "09#L?&CAa";
2373
2374    fn probe(ascii: bool) -> MaskedTextProvider {
2375        let mut p = MaskedTextProvider::new_with_ascii(ELEMENTS, ascii).unwrap();
2376        // The space and the prompt are tested against the mask instead of resetting.
2377        p.set_reset_on_space(false);
2378        p.set_reset_on_prompt(false);
2379        p
2380    }
2381
2382    #[test]
2383    fn verify_char_digit_elements() {
2384        let p = probe(false);
2385
2386        // 0: digit required.
2387        assert_eq!(p.verify_char('5', 0), (true, H::Success));
2388        assert_eq!(p.verify_char('a', 0), (false, H::DigitExpected));
2389        assert_eq!(p.verify_char(' ', 0), (false, H::DigitExpected));
2390        assert_eq!(p.verify_char('+', 0), (false, H::DigitExpected));
2391        // A non-ASCII decimal digit is a digit.
2392        assert_eq!(p.verify_char('\u{663}', 0), (true, H::Success));
2393
2394        // 9: digit or space.
2395        assert_eq!(p.verify_char('5', 1), (true, H::Success));
2396        assert_eq!(p.verify_char(' ', 1), (true, H::Success));
2397        assert_eq!(p.verify_char('-', 1), (false, H::DigitExpected));
2398
2399        // #: digit, space or sign.
2400        assert_eq!(p.verify_char('5', 2), (true, H::Success));
2401        assert_eq!(p.verify_char(' ', 2), (true, H::Success));
2402        assert_eq!(p.verify_char('+', 2), (true, H::Success));
2403        assert_eq!(p.verify_char('-', 2), (true, H::Success));
2404        assert_eq!(p.verify_char('a', 2), (false, H::DigitExpected));
2405    }
2406
2407    #[test]
2408    fn verify_char_letter_elements() {
2409        let p = probe(false);
2410
2411        // L: letter required.
2412        assert_eq!(p.verify_char('a', 3), (true, H::Success));
2413        assert_eq!(p.verify_char('\u{434}', 3), (true, H::Success));
2414        assert_eq!(p.verify_char('1', 3), (false, H::LetterExpected));
2415        assert_eq!(p.verify_char(' ', 3), (false, H::LetterExpected));
2416
2417        // ?: letter or space.
2418        assert_eq!(p.verify_char('a', 4), (true, H::Success));
2419        assert_eq!(p.verify_char(' ', 4), (true, H::Success));
2420        assert_eq!(p.verify_char('1', 4), (false, H::LetterExpected));
2421    }
2422
2423    #[test]
2424    fn verify_char_any_and_alphanumeric_elements() {
2425        let p = probe(false);
2426
2427        // &: any character required (the space is a character).
2428        assert_eq!(p.verify_char('!', 5), (true, H::Success));
2429        assert_eq!(p.verify_char(' ', 5), (true, H::Success));
2430        assert_eq!(p.verify_char('\u{434}', 5), (true, H::Success));
2431
2432        // C: any character optional.
2433        assert_eq!(p.verify_char('!', 6), (true, H::Success));
2434        assert_eq!(p.verify_char(' ', 6), (true, H::Success));
2435
2436        // A: alphanumeric required.
2437        assert_eq!(p.verify_char('a', 7), (true, H::Success));
2438        assert_eq!(p.verify_char('1', 7), (true, H::Success));
2439        assert_eq!(p.verify_char('\u{DC}', 7), (true, H::Success));
2440        assert_eq!(p.verify_char('!', 7), (false, H::AlphanumericCharacterExpected));
2441        assert_eq!(p.verify_char(' ', 7), (false, H::AlphanumericCharacterExpected));
2442
2443        // a: alphanumeric or space.
2444        assert_eq!(p.verify_char('a', 8), (true, H::Success));
2445        assert_eq!(p.verify_char(' ', 8), (true, H::Success));
2446        assert_eq!(p.verify_char('!', 8), (false, H::AlphanumericCharacterExpected));
2447    }
2448
2449    #[test]
2450    fn verify_char_ascii_only() {
2451        let p = probe(true);
2452
2453        // The digit elements are not affected.
2454        assert_eq!(p.verify_char('\u{663}', 0), (true, H::Success));
2455
2456        assert_eq!(p.verify_char('a', 3), (true, H::Success));
2457        assert_eq!(p.verify_char('\u{434}', 3), (false, H::AsciiCharacterExpected));
2458        assert_eq!(p.verify_char('\u{434}', 4), (false, H::AsciiCharacterExpected));
2459        // The space passes the letter test of `?` but not its ASCII letter test.
2460        assert_eq!(p.verify_char(' ', 4), (false, H::AsciiCharacterExpected));
2461
2462        assert_eq!(p.verify_char('!', 5), (true, H::Success));
2463        assert_eq!(p.verify_char('\u{434}', 5), (false, H::AsciiCharacterExpected));
2464        // "ASCII" is `!` to `~` for `&`: the space is not in it...
2465        assert_eq!(p.verify_char(' ', 5), (false, H::AsciiCharacterExpected));
2466        // ...but `C` accepts it explicitly.
2467        assert_eq!(p.verify_char(' ', 6), (true, H::Success));
2468        assert_eq!(p.verify_char('\u{434}', 6), (false, H::AsciiCharacterExpected));
2469
2470        assert_eq!(p.verify_char('z', 7), (true, H::Success));
2471        assert_eq!(p.verify_char('9', 7), (true, H::Success));
2472        assert_eq!(p.verify_char('\u{DC}', 7), (false, H::AsciiCharacterExpected));
2473        assert_eq!(p.verify_char('\u{DC}', 8), (false, H::AsciiCharacterExpected));
2474        assert_eq!(p.verify_char(' ', 8), (false, H::AsciiCharacterExpected));
2475    }
2476
2477    #[test]
2478    fn verify_char_position_and_input_validation() {
2479        let p = probe(false);
2480
2481        assert_eq!(p.verify_char('1', -1), (false, H::PositionOutOfRange));
2482        assert_eq!(p.verify_char('1', 9), (false, H::PositionOutOfRange));
2483        assert_eq!(p.verify_char('\n', 0), (false, H::InvalidInput));
2484        assert_eq!(p.verify_char('\0', 6), (false, H::InvalidInput));
2485        assert_eq!(p.verify_char('\u{1F600}', 6), (false, H::InvalidInput));
2486    }
2487
2488    #[test]
2489    fn verify_char_reports_no_effect_for_the_same_character() {
2490        let p = filled("00", "1");
2491        assert_eq!(p.verify_char('1', 0), (true, H::NoEffect));
2492        assert_eq!(p.verify_char('2', 0), (true, H::Success));
2493        assert_eq!(p.verify_char('1', 1), (true, H::Success));
2494    }
2495
2496    #[test]
2497    fn verify_char_on_literals() {
2498        let mut p = provider("00-00");
2499        assert_eq!(p.verify_char('-', 2), (true, H::CharacterEscaped));
2500        assert_eq!(p.verify_char('1', 2), (false, H::NonEditPosition));
2501        assert!(p.verify_escape_char('-', 2));
2502        assert!(!p.verify_escape_char('1', 2));
2503
2504        p.set_skip_literals(false);
2505        assert_eq!(p.verify_char('-', 2), (false, H::NonEditPosition));
2506        assert!(!p.verify_escape_char('-', 2));
2507    }
2508
2509    #[test]
2510    fn verify_escape_char_on_edit_positions() {
2511        let mut p = provider("00-00");
2512        assert!(p.verify_escape_char(' ', 0));
2513        assert!(p.verify_escape_char('_', 0));
2514        assert!(!p.verify_escape_char('1', 0));
2515        assert!(!p.verify_escape_char(' ', -1));
2516        assert!(!p.verify_escape_char(' ', 5));
2517
2518        p.set_reset_on_space(false);
2519        assert!(!p.verify_escape_char(' ', 0));
2520        assert!(p.verify_escape_char('_', 0));
2521        p.set_reset_on_prompt(false);
2522        assert!(!p.verify_escape_char('_', 0));
2523    }
2524
2525    #[test]
2526    fn case_conversion_modifiers() {
2527        let mut p = provider(">LL<LL|LL");
2528        assert!(p.add("abCDeF"));
2529        assert_eq!(p.to_display_string(), "ABcdeF");
2530
2531        // Digits and already converted letters are left alone.
2532        let mut p = provider(">AAA");
2533        assert!(p.add("a1B"));
2534        assert_eq!(p.to_display_string(), "A1B");
2535
2536        // Non-ASCII letters are converted too.
2537        let mut p = provider("<L>L");
2538        assert!(p.add("\u{DC}\u{434}"));
2539        assert_eq!(p.to_display_string(), "\u{FC}\u{414}");
2540
2541        // The conversion applies to characters that shift into a position.
2542        let mut p = provider(">L<L");
2543        assert!(p.add("a"));
2544        assert_eq!(p.to_display_string(), "A_");
2545        assert!(p.insert_at("b", 0));
2546        assert_eq!(p.to_display_string(), "Ba");
2547    }
2548
2549    #[test]
2550    fn culture_separators() {
2551        assert_eq!(CultureInfo::current_culture(), CultureInfo::invariant_culture());
2552
2553        let p = MaskedTextProvider::new_with_culture("$0,000.00 00/00 00:00", Some(CultureInfo::invariant_culture())).unwrap();
2554        assert_eq!(p.to_display_string(), "\u{A4}_,___.__ __/__ __:__");
2555        assert_eq!(provider("$0").to_display_string(), "\u{A4}_");
2556
2557        // A German-like culture: the separators are swapped.
2558        let de = culture_with_separators("de-DE", ",", ".", ".", ":", "\u{20AC}");
2559        let p = MaskedTextProvider::new_with_culture("$0.0,0/0:0", Some(de)).unwrap();
2560        assert_eq!(p.to_display_string(), "\u{20AC}_,_._._:_");
2561        assert_eq!(p.length(), 10);
2562
2563        // Separators can be longer than one character; every character is a non-edit position.
2564        let ch = culture_with_separators("de-CH", ".", "'", ".", ":", "CHF");
2565        let mut p = MaskedTextProvider::new_with_culture("$0.0,0/0:0", Some(ch)).unwrap();
2566        assert_eq!(p.to_display_string(), "CHF_._'_._:_");
2567        assert_eq!(p.length(), 12);
2568        assert_eq!(p.edit_positions().collect::<Vec<_>>(), vec![3, 5, 7, 9, 11]);
2569        assert!(!p.is_edit_position(1));
2570        assert_eq!(p.find_non_edit_position_from(3, true), 4);
2571
2572        // The separator characters of the culture are skipped in the input, not the mask's.
2573        assert!(p.add("CHF1.2'3"));
2574        assert_eq!(p.to_display_string(), "CHF1.2'3._:_");
2575        assert_eq!(p.verify_char(',', 6), (false, H::NonEditPosition));
2576    }
2577
2578    #[test]
2579    fn escaped_separator_is_not_localised() {
2580        let de = culture_with_separators("de-DE", ",", ".", ".", ":", "\u{20AC}");
2581        let p = MaskedTextProvider::new_with_culture("0\\.0.0", Some(de)).unwrap();
2582        assert_eq!(p.to_display_string(), "_._,_");
2583    }
2584
2585    // ---- Add ----
2586
2587    #[test]
2588    fn add_char_goes_after_the_last_assigned_position() {
2589        let mut p = provider("00-00");
2590
2591        assert_eq!(p.add_char_with_hint('1'), (true, 0, H::Success));
2592        assert_eq!(p.add_char_with_hint('2'), (true, 1, H::Success));
2593        // The literal is skipped.
2594        assert_eq!(p.add_char_with_hint('3'), (true, 3, H::Success));
2595        assert_eq!(p.add_char_with_hint('a'), (false, 4, H::DigitExpected));
2596        assert_eq!(p.to_display_string(), "12-3_");
2597        assert!(p.add_char('4'));
2598        assert_eq!(p.add_char_with_hint('5'), (false, 5, H::UnavailableEditPosition));
2599        assert!(!p.add_char('5'));
2600        assert_eq!(p.to_display_string(), "12-34");
2601    }
2602
2603    #[test]
2604    fn add_char_without_edit_position_after_the_last_assigned() {
2605        let mut p = filled("00-", "12");
2606        assert_eq!(p.add_char_with_hint('3'), (false, 3, H::UnavailableEditPosition));
2607    }
2608
2609    #[test]
2610    fn add_char_escaped_space_assigns_nothing() {
2611        let mut p = provider("00");
2612        assert_eq!(p.add_char_with_hint(' '), (true, 0, H::CharacterEscaped));
2613        assert_eq!(p.assigned_edit_position_count(), 0);
2614        // The next character still goes to the first position.
2615        assert_eq!(p.add_char_with_hint('1'), (true, 0, H::Success));
2616    }
2617
2618    #[test]
2619    fn add_string() {
2620        let mut p = provider("00-00");
2621
2622        assert_eq!(p.add_with_hint("12-34"), (true, 4, H::Success));
2623        assert_eq!(p.to_display_string(), "12-34");
2624        assert!(p.mask_full());
2625        assert_eq!(p.add_with_hint(""), (true, 5, H::NoEffect));
2626        assert_eq!(p.add_with_hint("1"), (false, 5, H::UnavailableEditPosition));
2627
2628        // Literals do not have to be part of the input.
2629        let mut p = provider("00-00");
2630        assert_eq!(p.add_with_hint("1234"), (true, 4, H::Success));
2631        assert_eq!(p.to_display_string(), "12-34");
2632
2633        // Strings are appended after the last assigned position.
2634        let mut p = provider("00-00");
2635        assert_eq!(p.add_with_hint("12"), (true, 1, H::Success));
2636        assert_eq!(p.add_with_hint("3"), (true, 3, H::Success));
2637        assert_eq!(p.to_display_string(), "12-3_");
2638    }
2639
2640    #[test]
2641    fn add_string_is_all_or_nothing() {
2642        let mut p = provider("00-00");
2643
2644        assert_eq!(p.add_with_hint("12a"), (false, 3, H::DigitExpected));
2645        assert_eq!(p.to_display_string(), "__-__");
2646        assert_eq!(p.add_with_hint("12345"), (false, 5, H::UnavailableEditPosition));
2647        assert_eq!(p.assigned_edit_position_count(), 0);
2648        assert!(!p.add("x"));
2649    }
2650
2651    #[test]
2652    fn add_string_without_skip_literals() {
2653        let mut p = provider("00-00");
2654        p.set_skip_literals(false);
2655
2656        assert_eq!(p.add_with_hint("12-34"), (false, 3, H::DigitExpected));
2657        assert_eq!(p.add_with_hint("1234"), (true, 4, H::Success));
2658        assert_eq!(p.to_display_string(), "12-34");
2659    }
2660
2661    // ---- Clear / Remove ----
2662
2663    #[test]
2664    fn clear_resets_all_positions() {
2665        let mut p = filled("00-00", "123");
2666
2667        assert_eq!(p.clear_with_hint(), H::Success);
2668        assert_eq!(p.to_display_string(), "__-__");
2669        assert_eq!(p.assigned_edit_position_count(), 0);
2670        assert!(!p.mask_completed());
2671        assert_eq!(p.clear_with_hint(), H::NoEffect);
2672        p.clear();
2673        assert_eq!(p.to_display_string(), "__-__");
2674    }
2675
2676    #[test]
2677    fn remove_resets_the_last_assigned_position() {
2678        let mut p = filled("00-00", "123");
2679
2680        assert_eq!(p.remove_with_hint(), (true, 3, H::Success));
2681        assert_eq!(p.to_display_string(), "12-__");
2682        assert!(p.remove());
2683        assert!(p.remove());
2684        assert_eq!(p.remove_with_hint(), (true, 0, H::NoEffect));
2685        assert_eq!(p.to_display_string(), "__-__");
2686    }
2687
2688    #[test]
2689    fn remove_at_shifts_the_following_characters_left() {
2690        let mut p = filled("00-00", "1234");
2691
2692        assert_eq!(p.remove_at_range_with_hint(0, 0), (true, 0, H::Success));
2693        assert_eq!(p.to_display_string(), "23-4_");
2694        assert!(p.remove_at(1));
2695        assert_eq!(p.to_display_string(), "24-__");
2696    }
2697
2698    #[test]
2699    fn remove_at_last_position_does_not_shift() {
2700        // The backspace at the end of a programmatically set text.
2701        let mut p = provider("00:00:00.000");
2702        assert!(p.set("12:34:56.000"));
2703        assert!(p.remove_at(11));
2704        assert_eq!(p.to_display_string(), "12:34:56.00_");
2705    }
2706
2707    #[test]
2708    fn remove_at_range_across_a_literal() {
2709        let mut p = filled("00-00", "1234");
2710        assert_eq!(p.remove_at_range_with_hint(1, 3), (true, 1, H::Success));
2711        assert_eq!(p.to_display_string(), "14-__");
2712
2713        let mut p = filled("00-00", "1234");
2714        assert!(p.remove_at_range(0, 4));
2715        assert_eq!(p.to_display_string(), "__-__");
2716    }
2717
2718    #[test]
2719    fn remove_at_a_literal_has_no_effect() {
2720        let mut p = filled("00-00", "1234");
2721        assert_eq!(p.remove_at_range_with_hint(2, 2), (true, 2, H::NoEffect));
2722        assert_eq!(p.to_display_string(), "12-34");
2723    }
2724
2725    #[test]
2726    fn remove_at_after_the_last_assigned_position_has_no_effect() {
2727        let mut p = filled("00-00", "12");
2728        assert_eq!(p.remove_at_range_with_hint(3, 4), (true, 3, H::NoEffect));
2729        assert_eq!(p.to_display_string(), "12-__");
2730    }
2731
2732    #[test]
2733    fn remove_at_unassigned_position_shifts_as_a_side_effect() {
2734        let mut p = provider("000");
2735        assert!(p.replace_char('5', 2));
2736        assert_eq!(p.to_display_string(), "__5");
2737
2738        assert_eq!(p.remove_at_range_with_hint(0, 0), (true, 0, H::SideEffect));
2739        assert_eq!(p.to_display_string(), "_5_");
2740    }
2741
2742    #[test]
2743    fn remove_at_fails_when_a_shifted_character_does_not_fit() {
2744        let mut p = provider("0L");
2745        assert!(p.replace_char('a', 1));
2746        assert_eq!(p.to_display_string(), "_a");
2747
2748        assert_eq!(p.remove_at_range_with_hint(0, 0), (false, 0, H::DigitExpected));
2749        assert!(!p.remove_at(0));
2750        assert_eq!(p.to_display_string(), "_a");
2751    }
2752
2753    #[test]
2754    fn remove_at_position_validation() {
2755        let mut p = filled("00-00", "1234");
2756
2757        assert_eq!(p.remove_at_range_with_hint(0, 5), (false, 5, H::PositionOutOfRange));
2758        assert_eq!(p.remove_at_range_with_hint(-1, 2), (false, -1, H::PositionOutOfRange));
2759        assert_eq!(p.remove_at_range_with_hint(3, 2), (false, 3, H::PositionOutOfRange));
2760        assert!(!p.remove_at(5));
2761        assert!(!p.remove_at(-1));
2762        assert_eq!(p.to_display_string(), "12-34");
2763    }
2764
2765    // ---- InsertAt ----
2766
2767    #[test]
2768    fn insert_at_shifts_the_following_characters_right() {
2769        let mut p = filled("00000", "123");
2770
2771        assert_eq!(p.insert_at_with_hint("9", 1), (true, 1, H::Success));
2772        assert_eq!(p.to_display_string(), "1923_");
2773        assert_eq!(p.insert_at_char_with_hint('8', 0), (true, 0, H::Success));
2774        assert_eq!(p.to_display_string(), "81923");
2775    }
2776
2777    #[test]
2778    fn insert_at_shifts_across_literals() {
2779        let mut p = filled("00-00", "123");
2780        assert!(p.insert_at("9", 0));
2781        assert_eq!(p.to_display_string(), "91-23");
2782    }
2783
2784    #[test]
2785    fn insert_at_a_literal_uses_the_next_edit_position() {
2786        let mut p = filled("00-00", "123");
2787        assert_eq!(p.insert_at_with_hint("9", 2), (true, 3, H::Success));
2788        assert_eq!(p.to_display_string(), "12-93");
2789    }
2790
2791    #[test]
2792    fn insert_at_unassigned_position_does_not_shift() {
2793        let mut p = provider("000");
2794        assert_eq!(p.insert_at_with_hint("5", 1), (true, 1, H::Success));
2795        assert_eq!(p.to_display_string(), "_5_");
2796        assert!(p.insert_at_char('7', 2));
2797        assert_eq!(p.to_display_string(), "_57");
2798    }
2799
2800    #[test]
2801    fn insert_at_fails_without_room() {
2802        let mut p = filled("000", "123");
2803
2804        assert_eq!(p.insert_at_with_hint("9", 0), (false, 3, H::UnavailableEditPosition));
2805        assert_eq!(p.insert_at_with_hint("9", 2), (false, 3, H::UnavailableEditPosition));
2806        assert!(!p.insert_at_char('9', 1));
2807        assert_eq!(p.to_display_string(), "123");
2808    }
2809
2810    #[test]
2811    fn insert_at_fails_when_a_shifted_character_does_not_fit() {
2812        let mut p = filled("00LL", "12");
2813        assert_eq!(p.insert_at_with_hint("3", 0), (false, 2, H::LetterExpected));
2814        assert_eq!(p.to_display_string(), "12__");
2815    }
2816
2817    #[test]
2818    fn insert_at_fails_for_invalid_input() {
2819        let mut p = provider("00-00");
2820        assert_eq!(p.insert_at_with_hint("1a", 0), (false, 1, H::DigitExpected));
2821        assert_eq!(p.insert_at_with_hint("12345", 0), (false, 5, H::UnavailableEditPosition));
2822        assert_eq!(p.to_display_string(), "__-__");
2823    }
2824
2825    #[test]
2826    fn insert_at_empty_string_and_position_validation() {
2827        let mut p = provider("000");
2828
2829        assert_eq!(p.insert_at_with_hint("", 1), (true, 1, H::NoEffect));
2830        assert_eq!(p.insert_at_with_hint("1", 3), (false, 3, H::PositionOutOfRange));
2831        assert_eq!(p.insert_at_with_hint("1", -1), (false, -1, H::PositionOutOfRange));
2832        assert_eq!(p.insert_at_char_with_hint('1', 3), (false, 3, H::PositionOutOfRange));
2833        assert!(!p.insert_at_char('1', -1));
2834        assert!(!p.insert_at_char('1', 3));
2835        assert!(!p.insert_at("1", 3));
2836    }
2837
2838    #[test]
2839    fn insert_at_space_opens_a_gap() {
2840        // The space key of a masked text box.
2841        let mut p = filled("0000", "123");
2842        assert_eq!(p.insert_at_with_hint(" ", 1), (true, 1, H::Success));
2843        assert_eq!(p.to_display_string(), "1_23");
2844
2845        // There is no room for the gap in a full mask.
2846        let mut p = filled("000", "123");
2847        assert_eq!(p.insert_at_with_hint(" ", 1), (false, 3, H::UnavailableEditPosition));
2848        assert_eq!(p.to_display_string(), "123");
2849    }
2850
2851    // ---- Replace ----
2852
2853    #[test]
2854    fn replace_char_at_position() {
2855        let mut p = filled("00-00", "1234");
2856
2857        assert_eq!(p.replace_char_with_hint('9', 0), (true, 0, H::Success));
2858        assert_eq!(p.replace_char_with_hint('9', 0), (true, 0, H::NoEffect));
2859        // On a literal the next edit position is replaced...
2860        assert_eq!(p.replace_char_with_hint('8', 2), (true, 3, H::Success));
2861        // ...unless the character is the literal.
2862        assert_eq!(p.replace_char_with_hint('-', 2), (true, 2, H::CharacterEscaped));
2863        assert_eq!(p.to_display_string(), "92-84");
2864        assert_eq!(p.replace_char_with_hint('a', 1), (false, 1, H::DigitExpected));
2865        assert_eq!(p.replace_char_with_hint('1', 5), (false, 5, H::PositionOutOfRange));
2866        assert_eq!(p.replace_char_with_hint('1', -1), (false, -1, H::PositionOutOfRange));
2867        assert!(!p.replace_char('a', 1));
2868        assert_eq!(p.to_display_string(), "92-84");
2869    }
2870
2871    #[test]
2872    fn replace_char_without_edit_position() {
2873        let mut p = provider("00-");
2874        assert_eq!(p.replace_char_with_hint('1', 2), (false, 2, H::UnavailableEditPosition));
2875    }
2876
2877    #[test]
2878    fn replace_string_at_position_does_not_shift() {
2879        let mut p = filled("00000", "12345");
2880
2881        assert_eq!(p.replace_with_hint("99", 1), (true, 2, H::Success));
2882        assert_eq!(p.to_display_string(), "19945");
2883        // An empty string removes the character.
2884        assert_eq!(p.replace_with_hint("", 1), (true, 1, H::Success));
2885        assert_eq!(p.to_display_string(), "1945_");
2886        assert_eq!(p.replace_with_hint("1a", 0), (false, 1, H::DigitExpected));
2887        assert_eq!(p.replace_with_hint("1", 5), (false, 5, H::PositionOutOfRange));
2888        assert!(!p.replace("123456", 0));
2889        assert!(p.replace("7", 4));
2890        assert_eq!(p.to_display_string(), "19457");
2891    }
2892
2893    #[test]
2894    fn replace_range_with_a_shorter_string_removes_the_rest() {
2895        let mut p = filled("00000", "12345");
2896        assert_eq!(p.replace_range_with_hint("9", 1, 3), (true, 1, H::Success));
2897        assert_eq!(p.to_display_string(), "195__");
2898
2899        let mut p = filled("00000", "12345");
2900        assert_eq!(p.replace_char_range_with_hint('9', 1, 3), (true, 1, H::Success));
2901        assert_eq!(p.to_display_string(), "195__");
2902    }
2903
2904    #[test]
2905    fn replace_range_with_a_longer_string_inserts_the_rest() {
2906        let mut p = filled("00000", "123");
2907        assert_eq!(p.replace_range_with_hint("789", 0, 0), (true, 2, H::Success));
2908        assert_eq!(p.to_display_string(), "78923");
2909
2910        // No room for the shifted characters.
2911        let mut p = filled("00000", "1234");
2912        assert_eq!(p.replace_range_with_hint("789", 0, 0), (false, 5, H::UnavailableEditPosition));
2913        assert_eq!(p.to_display_string(), "1234_");
2914    }
2915
2916    #[test]
2917    fn replace_range_with_a_string_of_the_same_length() {
2918        let mut p = filled("00-00", "1234");
2919        assert_eq!(p.replace_range_with_hint("98", 1, 3), (true, 3, H::Success));
2920        assert_eq!(p.to_display_string(), "19-84");
2921    }
2922
2923    #[test]
2924    fn replace_range_with_an_empty_string_removes_the_range() {
2925        let mut p = filled("00000", "12345");
2926        assert_eq!(p.replace_range_with_hint("", 1, 2), (true, 1, H::Success));
2927        assert_eq!(p.to_display_string(), "145__");
2928    }
2929
2930    #[test]
2931    fn replace_char_range_of_one_position_sets_that_position() {
2932        let mut p = filled("00-00", "1234");
2933        assert_eq!(p.replace_char_range_with_hint('9', 1, 1), (true, 1, H::Success));
2934        assert_eq!(p.to_display_string(), "19-34");
2935        // Unlike `replace_char`, a literal position is not skipped here.
2936        assert_eq!(p.replace_char_range_with_hint('9', 2, 2), (false, 2, H::NonEditPosition));
2937    }
2938
2939    #[test]
2940    fn replace_range_position_validation() {
2941        let mut p = filled("00000", "12345");
2942
2943        assert_eq!(p.replace_range_with_hint("1", 0, 5), (false, 5, H::PositionOutOfRange));
2944        assert_eq!(p.replace_range_with_hint("1", -1, 2), (false, -1, H::PositionOutOfRange));
2945        assert_eq!(p.replace_range_with_hint("1", 3, 2), (false, 3, H::PositionOutOfRange));
2946        assert_eq!(p.replace_char_range_with_hint('1', 0, 5), (false, 5, H::PositionOutOfRange));
2947        assert_eq!(p.replace_char_range_with_hint('1', 3, 2), (false, 3, H::PositionOutOfRange));
2948        assert_eq!(p.replace_range_with_hint("a", 1, 3), (false, 1, H::DigitExpected));
2949        assert_eq!(p.to_display_string(), "12345");
2950    }
2951
2952    // ---- Set ----
2953
2954    #[test]
2955    fn set_replaces_the_whole_content() {
2956        let mut p = provider("00/00/0000");
2957
2958        assert_eq!(p.set_with_hint("12/10/2000"), (true, 9, H::Success));
2959        assert_eq!(p.to_display_string(), "12/10/2000");
2960        assert!(p.mask_full());
2961
2962        // Setting the same text changes nothing.
2963        assert_eq!(p.set_with_hint("12/10/2000"), (true, 9, H::NoEffect));
2964
2965        // Positions after the new text are reset.
2966        assert_eq!(p.set_with_hint("3"), (true, 0, H::Success));
2967        assert_eq!(p.to_display_string(), "3_/__/____");
2968
2969        assert!(p.set("12102000"));
2970        assert_eq!(p.to_display_string(), "12/10/2000");
2971    }
2972
2973    #[test]
2974    fn set_empty_string_clears() {
2975        let mut p = filled("000", "12");
2976        assert_eq!(p.set_with_hint(""), (true, 0, H::Success));
2977        assert_eq!(p.to_display_string(), "___");
2978        assert_eq!(p.set_with_hint(""), (true, 0, H::NoEffect));
2979    }
2980
2981    #[test]
2982    fn set_invalid_text_keeps_the_previous_content() {
2983        let mut p = provider("000");
2984        assert!(p.set("123"));
2985        assert_eq!(p.set_with_hint("abc"), (false, 0, H::DigitExpected));
2986        assert!(!p.set("1234"));
2987        assert_eq!(p.to_display_string(), "123");
2988
2989        // A text whose literal is replaced by an edit character does not fit.
2990        let mut p = provider("00:00:00.000");
2991        assert_eq!(p.set_with_hint("12:34:560000"), (false, 12, H::UnavailableEditPosition));
2992        assert_eq!(p.to_display_string(), "__:__:__.___");
2993    }
2994
2995    #[test]
2996    fn set_accepts_its_own_display_string() {
2997        // A masked text box sets the provider from the text it displays.
2998        let mut p = provider("00/00/0000");
2999        assert!(p.set("12/1"));
3000        let display = p.to_display_string();
3001        assert_eq!(display, "12/1_/____");
3002        assert_eq!(p.set_with_hint(&display), (true, 9, H::NoEffect));
3003        assert_eq!(p.to_display_string(), "12/1_/____");
3004        assert_eq!(p.assigned_edit_position_count(), 3);
3005    }
3006
3007    // ---- Prompt and space handling ----
3008
3009    #[test]
3010    fn prompt_resets_the_position() {
3011        let mut p = filled("000", "123");
3012
3013        assert_eq!(p.verify_char('_', 1), (true, H::SideEffect));
3014        assert_eq!(p.replace_char_with_hint('_', 1), (true, 1, H::SideEffect));
3015        assert_eq!(p.to_display_string(), "1_3");
3016        assert_eq!(p.assigned_edit_position_count(), 2);
3017        assert!(!p.mask_completed());
3018        assert_eq!(p.replace_char_with_hint('_', 1), (true, 1, H::CharacterEscaped));
3019
3020        assert!(p.set("4_6"));
3021        assert_eq!(p.to_display_string(), "4_6");
3022        assert!(p.is_available_position(1));
3023    }
3024
3025    #[test]
3026    fn prompt_is_input_when_reset_on_prompt_is_off() {
3027        let mut p = provider("0CC");
3028        p.set_reset_on_prompt(false);
3029
3030        // It is tested against the mask...
3031        assert_eq!(p.replace_char_with_hint('_', 0), (false, 0, H::DigitExpected));
3032        // ...and assigned where it fits.
3033        assert_eq!(p.replace_char_with_hint('_', 1), (true, 1, H::Success));
3034        assert_eq!(p.to_display_string(), "___");
3035        assert_eq!(p.assigned_edit_position_count(), 1);
3036        assert!(!p.is_available_position(1));
3037    }
3038
3039    #[test]
3040    fn prompt_is_rejected_when_not_allowed_as_input() {
3041        let mut p = MaskedTextProvider::new_full("CC", None, false, '_', '\0', false).unwrap();
3042
3043        // Escaping precedes the check.
3044        assert_eq!(p.replace_char_with_hint('_', 0), (true, 0, H::CharacterEscaped));
3045        p.set_reset_on_prompt(false);
3046        assert_eq!(p.replace_char_with_hint('_', 0), (false, 0, H::PromptCharNotAllowed));
3047        assert_eq!(p.add_with_hint("a_"), (false, 1, H::PromptCharNotAllowed));
3048        assert_eq!(p.assigned_edit_position_count(), 0);
3049    }
3050
3051    #[test]
3052    fn space_resets_the_position() {
3053        let mut p = filled("000", "123");
3054
3055        assert_eq!(p.replace_char_with_hint(' ', 0), (true, 0, H::SideEffect));
3056        assert_eq!(p.to_display_string(), "_23");
3057        assert_eq!(p.replace_char_with_hint(' ', 0), (true, 0, H::CharacterEscaped));
3058
3059        // A space in a string skips a position.
3060        let mut p = provider("AA#00");
3061        assert_eq!(p.add_with_hint("S2 33"), (true, 4, H::Success));
3062        assert_eq!(p.to_display_string(), "S2_33");
3063        assert_eq!(p.assigned_edit_position_count(), 4);
3064    }
3065
3066    #[test]
3067    fn space_is_input_when_reset_on_space_is_off() {
3068        let mut p = provider("909");
3069        p.set_reset_on_space(false);
3070
3071        assert_eq!(p.replace_char_with_hint(' ', 1), (false, 1, H::DigitExpected));
3072        assert_eq!(p.replace_char_with_hint(' ', 0), (true, 0, H::Success));
3073        assert_eq!(p.to_display_string(), " __");
3074        assert_eq!(p.assigned_edit_position_count(), 1);
3075        assert_eq!(p.last_assigned_position(), 0);
3076    }
3077
3078    // ---- Find ----
3079
3080    fn find_fixture() -> MaskedTextProvider {
3081        // "12-_4": 0, 1 and 4 are assigned.
3082        let mut p = filled("00-00", "12");
3083        assert!(p.replace_char('4', 4));
3084        assert_eq!(p.to_display_string(), "12-_4");
3085        p
3086    }
3087
3088    #[test]
3089    fn find_assigned_edit_position() {
3090        let p = find_fixture();
3091
3092        assert_eq!(p.find_assigned_edit_position_from(0, true), 0);
3093        assert_eq!(p.find_assigned_edit_position_from(2, true), 4);
3094        assert_eq!(p.find_assigned_edit_position_from(3, false), 1);
3095        assert_eq!(p.find_assigned_edit_position_from(5, true), -1);
3096        assert_eq!(p.find_assigned_edit_position_in_range(2, 3, true), -1);
3097        assert_eq!(p.find_assigned_edit_position_in_range(0, 4, false), 4);
3098        assert_eq!(p.find_assigned_edit_position_in_range(3, 1, true), -1);
3099        assert_eq!(p.last_assigned_position(), 4);
3100
3101        let empty = provider("00-00");
3102        assert_eq!(empty.find_assigned_edit_position_from(0, true), -1);
3103        assert_eq!(empty.find_assigned_edit_position_in_range(0, 4, false), -1);
3104    }
3105
3106    #[test]
3107    fn find_edit_position() {
3108        let p = find_fixture();
3109
3110        assert_eq!(p.find_edit_position_from(0, true), 0);
3111        assert_eq!(p.find_edit_position_from(2, true), 3);
3112        assert_eq!(p.find_edit_position_from(2, false), 1);
3113        assert_eq!(p.find_edit_position_from(5, true), -1);
3114        // Out of range bounds are clamped.
3115        assert_eq!(p.find_edit_position_from(-3, true), 0);
3116        assert_eq!(p.find_edit_position_from(10, false), 4);
3117        assert_eq!(p.find_edit_position_from(-1, false), -1);
3118        assert_eq!(p.find_edit_position_in_range(2, 2, true), -1);
3119        assert_eq!(p.find_edit_position_in_range(1, 3, false), 3);
3120        assert_eq!(p.find_edit_position_in_range(-5, 50, true), 0);
3121    }
3122
3123    #[test]
3124    fn find_non_edit_position() {
3125        let p = find_fixture();
3126
3127        assert_eq!(p.find_non_edit_position_from(0, true), 2);
3128        assert_eq!(p.find_non_edit_position_from(4, false), 2);
3129        assert_eq!(p.find_non_edit_position_from(3, true), -1);
3130        assert_eq!(p.find_non_edit_position_from(1, false), -1);
3131        assert_eq!(p.find_non_edit_position_in_range(0, 1, true), -1);
3132        assert_eq!(p.find_non_edit_position_in_range(2, 2, false), 2);
3133    }
3134
3135    #[test]
3136    fn find_unassigned_edit_position() {
3137        let p = find_fixture();
3138
3139        assert_eq!(p.find_unassigned_edit_position_from(0, true), 3);
3140        assert_eq!(p.find_unassigned_edit_position_from(4, false), 3);
3141        assert_eq!(p.find_unassigned_edit_position_from(4, true), -1);
3142        assert_eq!(p.find_unassigned_edit_position_in_range(0, 1, true), -1);
3143        assert_eq!(p.find_unassigned_edit_position_in_range(0, 4, false), 3);
3144
3145        let empty = provider("00-00");
3146        assert_eq!(empty.find_unassigned_edit_position_from(2, true), 3);
3147        assert_eq!(empty.find_unassigned_edit_position_from(2, false), 1);
3148    }
3149
3150    #[test]
3151    fn edit_position_predicates() {
3152        let p = find_fixture();
3153
3154        assert_eq!(p.edit_positions().collect::<Vec<_>>(), vec![0, 1, 3, 4]);
3155        assert!(p.is_edit_position(0));
3156        assert!(!p.is_edit_position(2));
3157        assert!(p.is_edit_position(3));
3158        assert!(!p.is_edit_position(-1));
3159        assert!(!p.is_edit_position(5));
3160        assert!(p.is_available_position(3));
3161        assert!(!p.is_available_position(0));
3162        assert!(!p.is_available_position(2));
3163        assert!(!p.is_available_position(-1));
3164        assert!(!p.is_available_position(5));
3165    }
3166
3167    // ---- Formatting ----
3168
3169    #[test]
3170    fn to_string_overloads() {
3171        let mut p = MaskedTextProvider::new_full("00/00", None, true, '_', '*', false).unwrap();
3172        assert!(p.add("12"));
3173
3174        assert_eq!(p.to_display_string(), "**/__");
3175        // Display: no password char, no prompt, literals.
3176        assert_eq!(p.to_string(), "12/");
3177        assert_eq!(p.to_string_ignore_password(true), "12/");
3178        assert_eq!(p.to_string_ignore_password(false), "**/");
3179        assert_eq!(p.to_string_include(true, true), "12/__");
3180        assert_eq!(p.to_string_include(false, true), "12/");
3181        assert_eq!(p.to_string_include(true, false), "12__");
3182        assert_eq!(p.to_string_include(false, false), "12");
3183        assert_eq!(p.to_string_range(1, 3), "2/");
3184        assert_eq!(p.to_string_ignore_password_range(false, 1, 3), "*/");
3185        assert_eq!(p.to_string_include_range(true, true, 1, 3), "2/_");
3186        assert_eq!(p.to_string_with(false, true, true, 0, 5), "**/__");
3187        assert_eq!(p.to_string_with(false, true, false, 0, 5), "**__");
3188        assert_eq!(p.to_string_with(false, false, false, 0, 5), "**");
3189
3190        p.set_include_prompt(true);
3191        assert_eq!(p.to_string(), "12/__");
3192        p.set_include_literals(false);
3193        assert_eq!(p.to_string(), "12__");
3194        assert_eq!(format!("{p}"), "12__");
3195    }
3196
3197    #[test]
3198    fn to_string_range_is_clamped() {
3199        let p = filled("00/00", "1234");
3200
3201        assert_eq!(p.to_string_include_range(true, true, 0, 0), "");
3202        assert_eq!(p.to_string_include_range(true, true, 0, -1), "");
3203        assert_eq!(p.to_string_include_range(true, true, 5, 1), "");
3204        assert_eq!(p.to_string_include_range(true, true, -2, 2), "12");
3205        assert_eq!(p.to_string_include_range(true, true, 3, 100), "34");
3206        assert_eq!(p.to_string_include_range(false, false, 1, 3), "23");
3207    }
3208
3209    #[test]
3210    fn to_string_without_prompt_replaces_gaps_with_spaces() {
3211        let mut p = provider("000-000");
3212        assert!(p.replace_char('1', 1));
3213        assert!(p.replace_char('2', 5));
3214
3215        assert_eq!(p.to_display_string(), "_1_-_2_");
3216        // The string ends at the last assigned position.
3217        assert_eq!(p.to_string_include(false, true), " 1 - 2");
3218        assert_eq!(p.to_string_include(false, false), " 1  2");
3219        assert_eq!(p.to_string_include(true, false), "_1__2_");
3220    }
3221
3222    #[test]
3223    fn to_string_without_prompt_ends_at_the_last_literal_when_nothing_follows() {
3224        // What a masked text box shows when it hides the prompt on leave.
3225        let p = filled("00/00/0000", "12");
3226        assert_eq!(p.to_string_include(false, true), "12/  /");
3227
3228        let empty = provider("00/00/0000");
3229        assert_eq!(empty.to_string_include(false, true), "  /  /");
3230        assert_eq!(empty.to_string_include(false, false), "");
3231        assert_eq!(provider("000").to_string_include(false, true), "");
3232        assert_eq!(filled("000", "1").to_string_include(false, true), "1");
3233    }
3234
3235    #[test]
3236    fn to_display_string_with_password_char() {
3237        let mut p = MaskedTextProvider::new_full("00/00/0000", None, true, '_', '*', false).unwrap();
3238        assert_eq!(p.to_display_string(), "__/__/____");
3239        assert!(p.insert_at("12102000", 0));
3240        assert_eq!(p.to_display_string(), "**/**/****");
3241        assert_eq!(p.to_string(), "12/10/2000");
3242
3243        let mut p = MaskedTextProvider::new_full("AA#00", None, true, '_', '*', false).unwrap();
3244        assert!(p.insert_at("S2 33", 0));
3245        assert_eq!(p.to_display_string(), "**_**");
3246    }
3247
3248    // ---- VerifyString ----
3249
3250    #[test]
3251    fn verify_string_does_not_change_anything() {
3252        let p = provider("00-00");
3253
3254        assert_eq!(p.verify_string_with_hint("12-34"), (true, 4, H::Success));
3255        assert_eq!(p.verify_string_with_hint("1234"), (true, 4, H::Success));
3256        assert_eq!(p.verify_string_with_hint(""), (true, 0, H::NoEffect));
3257        assert_eq!(p.verify_string_with_hint("1a"), (false, 1, H::DigitExpected));
3258        assert_eq!(p.verify_string_with_hint("123456"), (false, 5, H::UnavailableEditPosition));
3259        assert_eq!(p.verify_string_with_hint(" "), (true, 0, H::CharacterEscaped));
3260        assert_eq!(p.verify_string_with_hint("1\n"), (false, 1, H::InvalidInput));
3261        assert!(p.verify_string("12"));
3262        assert!(!p.verify_string("x"));
3263        assert_eq!(p.assigned_edit_position_count(), 0);
3264
3265        let p = filled("00-00", "12");
3266        assert_eq!(p.verify_string_with_hint("12"), (true, 1, H::NoEffect));
3267        assert_eq!(p.verify_string_with_hint("1 "), (true, 1, H::SideEffect));
3268        assert_eq!(p.to_display_string(), "12-__");
3269    }
3270
3271    // ---- The scenarios of a masked text box ----
3272
3273    /// Text input: the whole text is inserted at the caret.
3274    fn type_text(p: &mut MaskedTextProvider, text: &str) -> String {
3275        let caret = p.find_edit_position_from(0, true);
3276        p.insert_at(text, caret);
3277        p.to_display_string()
3278    }
3279
3280    #[test]
3281    fn typed_text_follows_the_mask() {
3282        assert_eq!(type_text(&mut provider("00/00/0000"), "12102000"), "12/10/2000");
3283        assert_eq!(type_text(&mut provider("LLLL"), "\u{434}\u{431}s"), "\u{434}\u{431}s_");
3284        assert_eq!(type_text(&mut provider("AA#00"), "S2 33"), "S2_33");
3285    }
3286
3287    #[test]
3288    fn typed_text_is_hidden_by_the_password_char() {
3289        let password = |mask: &str| MaskedTextProvider::new_full(mask, None, true, '_', '*', false).unwrap();
3290
3291        assert_eq!(type_text(&mut password("00/00/0000"), "12102000"), "**/**/****");
3292        assert_eq!(type_text(&mut password("LLLL"), "\u{434}\u{431}s"), "***_");
3293        assert_eq!(type_text(&mut password("AA#00"), "S2 33"), "**_**");
3294    }
3295
3296    #[test]
3297    fn typed_non_ascii_text_is_rejected_when_ascii_only() {
3298        let ascii = |mask: &str| MaskedTextProvider::new_full(mask, None, true, '_', '\0', true).unwrap();
3299
3300        assert_eq!(type_text(&mut ascii("00/00/0000"), "12102000"), "12/10/2000");
3301        assert_eq!(type_text(&mut ascii("LLLL"), "\u{434}\u{431}s"), "____");
3302        assert_eq!(type_text(&mut ascii("AA"), "\u{DC}1"), "__");
3303    }
3304
3305    #[test]
3306    fn pasted_text_is_inserted_character_by_character() {
3307        let mut p = provider("00/00/0000");
3308        let mut caret = 0;
3309
3310        for item in "12x10-2000".chars() {
3311            let index = p.find_edit_position_from(caret, true);
3312            if p.insert_at_char(item, index) {
3313                caret = index + 1;
3314            }
3315        }
3316
3317        assert_eq!(p.to_display_string(), "12/10/2000");
3318        // There is no edit position after the end.
3319        assert_eq!(p.find_edit_position_from(caret, true), -1);
3320        assert!(!p.insert_at_char('1', -1));
3321    }
3322
3323    #[test]
3324    fn selection_is_removed_on_reset_input() {
3325        // A space or prompt typed over a selection removes the selected range.
3326        let mut p = filled("00/00/0000", "12102000");
3327        assert!(p.remove_at_range(3, 6));
3328        assert_eq!(p.to_display_string(), "12/00/0___");
3329    }
3330
3331    #[test]
3332    fn upper_case_of_greek_letters_with_iota_subscript() {
3333        let mut p = provider(">LL");
3334        assert!(p.add("\u{1F80}\u{1FF3}"));
3335        assert_eq!(p.to_display_string(), "\u{1F88}\u{1FFC}");
3336    }
3337}