From 769c0990a4ffcbd1b27db820f32449cd36e63b45 Mon Sep 17 00:00:00 2001 From: Victor Quiroz Date: Mon, 16 Jun 2025 04:05:37 +0200 Subject: [PATCH] number_input: Allow signed numbers (#957) Allow negative numbers and explicit positive numbers. Added these tests: 1. Only one sign at the start of the number. 2. No signs in the middle of the number. 3. No signs in the fractional part. 4. Sign doesn't break formatting. https://github.com/user-attachments/assets/89c4f4c7-d3bf-4cc1-aeca-7e9bedd02e20 --------- Co-authored-by: Jason Lee --- crates/ui/src/input/mask_pattern.rs | 85 +++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/crates/ui/src/input/mask_pattern.rs b/crates/ui/src/input/mask_pattern.rs index bf0ca800..a1b40a47 100644 --- a/crates/ui/src/input/mask_pattern.rs +++ b/crates/ui/src/input/mask_pattern.rs @@ -202,11 +202,25 @@ impl MaskPattern { return false; } - // check if the integer part is valid - if !int_part + let sign_positions: Vec = int_part .chars() - .all(|ch| ch.is_ascii_digit() || Some(ch) == *separator) - { + .enumerate() + .filter_map(|(i, ch)| match is_sign(&ch) { + true => Some(i), + false => None, + }) + .collect(); + + // only one sign is valid + // sign is only valid at the beginning of the string + if sign_positions.len() > 1 || sign_positions.first() > Some(&0) { + return false; + } + + // check if the integer part is valid + if !int_part.chars().enumerate().all(|(i, ch)| { + ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator + }) { return false; } @@ -288,7 +302,15 @@ impl MaskPattern { }); // Reverse the integer part for easier grouping - let chars: Vec = int_part.chars().rev().collect(); + let mut chars: Vec = int_part.chars().rev().collect(); + + // Removing the sign from formatting to avoid cases such as: -,123 + let maybe_signed = if let Some(pos) = chars.iter().position(is_sign) { + Some(chars.remove(pos)) + } else { + None + }; + let mut result = String::new(); for (i, ch) in chars.iter().enumerate() { if i > 0 && i % 3 == 0 { @@ -307,6 +329,13 @@ impl MaskPattern { } else { int_with_sep }; + + let final_str = if let Some(sign) = maybe_signed { + format!("{}{}", sign, final_str) + } else { + final_str + }; + return final_str.into(); } @@ -379,6 +408,11 @@ impl MaskPattern { } } +#[inline] +fn is_sign(ch: &char) -> bool { + matches!(ch, '+' | '-') +} + #[cfg(test)] mod tests { use crate::input::mask_pattern::{MaskPattern, MaskToken}; @@ -559,4 +593,45 @@ mod tests { assert_eq!(mask.mask("1234567.1234567"), "1,234,567"); } + + #[test] + fn test_signed_number_numbers() { + let mask = MaskPattern::Number { + separator: Some(','), + fraction: Some(2), + }; + + assert_eq!(mask.is_valid("-"), true); + assert_eq!(mask.is_valid("-1234567"), true); + assert_eq!(mask.is_valid("-1,234,567"), true); + assert_eq!(mask.is_valid("-1234567."), true); + assert_eq!(mask.is_valid("-1234567.89"), true); + + assert_eq!(mask.is_valid("+"), true); + assert_eq!(mask.is_valid("+1234567"), true); + assert_eq!(mask.is_valid("+1,234,567"), true); + assert_eq!(mask.is_valid("+1234567."), true); + assert_eq!(mask.is_valid("+1234567.89"), true); + + // Only one sign is valid + assert_eq!(mask.is_valid("+-"), false); + assert_eq!(mask.is_valid("-+"), false); + assert_eq!(mask.is_valid("+-1234567"), false); + + // No sign is valid in the middle of the number + assert_eq!(mask.is_valid("1,-234,567"), false); + assert_eq!(mask.is_valid("12-34567.89"), false); + + // Signs in fractions are invalid + assert_eq!(mask.is_valid("+1234567.-"), false); + + // The separator does not show up before the sign i.e. -,123 + assert_eq!(mask.mask("-123"), "-123"); + + assert_eq!(mask.mask("-1234567"), "-1,234,567"); + assert_eq!(mask.mask("+1234567"), "+1,234,567"); + assert_eq!(mask.unmask("-1,234,567"), "-1234567"); + assert_eq!(mask.mask("-1234567."), "-1,234,567."); + assert_eq!(mask.mask("-1234567.89"), "-1,234,567.89"); + } }