validation_core/rules/
display_mask.rs1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
7pub enum MaskDisplayMode {
8 #[default]
11 Dynamic,
12
13 Template {
16 placeholder: char,
18 },
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct DisplayMask {
23 pattern: String,
25 input_char: char,
27 display_mode: MaskDisplayMode,
29}
30
31impl DisplayMask {
32 pub fn new(pattern: impl Into<String>, input_char: char) -> Self {
52 Self {
53 pattern: pattern.into(),
54 input_char,
55 display_mode: MaskDisplayMode::Dynamic,
56 }
57 }
58
59 pub fn with_mode(mut self, mode: MaskDisplayMode) -> Self {
72 self.display_mode = mode;
73 self
74 }
75
76 pub fn with_template(self, placeholder: char) -> Self {
89 self.with_mode(MaskDisplayMode::Template { placeholder })
90 }
91
92 pub fn apply_to_display(&self, raw_input: &str) -> String {
94 match &self.display_mode {
95 MaskDisplayMode::Dynamic => self.apply_dynamic(raw_input),
96 MaskDisplayMode::Template { placeholder } => {
97 self.apply_template(raw_input, *placeholder)
98 }
99 }
100 }
101
102 fn apply_dynamic(&self, raw_input: &str) -> String {
104 if raw_input.is_empty() {
105 return String::new();
106 }
107
108 let mut result = String::new();
109 let mut raw_chars = raw_input.chars();
110
111 for pattern_char in self.pattern.chars() {
112 if pattern_char == self.input_char {
113 if let Some(input_char) = raw_chars.next() {
115 result.push(input_char);
116 } else {
117 break;
119 }
120 } else {
121 result.push(pattern_char);
123 }
124 }
125
126 for remaining_char in raw_chars {
128 result.push(remaining_char);
129 }
130
131 result
132 }
133
134 fn apply_template(&self, raw_input: &str, placeholder: char) -> String {
136 let mut result = String::new();
137 let mut raw_chars = raw_input.chars().peekable();
138
139 for pattern_char in self.pattern.chars() {
140 if pattern_char == self.input_char {
141 if let Some(input_char) = raw_chars.next() {
143 result.push(input_char);
144 } else {
145 result.push(placeholder);
147 }
148 } else {
149 result.push(pattern_char);
151 }
152 }
153
154 result
157 }
158
159 pub fn is_input_position(&self, display_position: usize) -> bool {
161 self.pattern
162 .chars()
163 .nth(display_position)
164 .map(|c| c == self.input_char)
165 .unwrap_or(true) }
167
168 pub fn display_pos_to_raw_pos(&self, display_pos: usize) -> usize {
170 let mut raw_pos = 0;
171
172 for (i, pattern_char) in self.pattern.chars().enumerate() {
173 if i >= display_pos {
174 break;
175 }
176 if pattern_char == self.input_char {
177 raw_pos += 1;
178 }
179 }
180
181 raw_pos
182 }
183
184 pub fn raw_pos_to_display_pos(&self, raw_pos: usize) -> usize {
186 let mut input_positions_seen = 0;
187
188 for (display_pos, pattern_char) in self.pattern.chars().enumerate() {
189 if pattern_char == self.input_char {
190 if input_positions_seen == raw_pos {
191 return display_pos;
192 }
193 input_positions_seen += 1;
194 }
195 }
196
197 self.pattern.len() + (raw_pos - input_positions_seen)
199 }
200
201 pub fn next_input_position(&self, display_pos: usize) -> usize {
203 for (i, pattern_char) in self.pattern.chars().enumerate().skip(display_pos) {
204 if pattern_char == self.input_char {
205 return i;
206 }
207 }
208 display_pos.max(self.pattern.len())
210 }
211
212 pub fn prev_input_position(&self, display_pos: usize) -> Option<usize> {
214 let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect();
216
217 for &(i, pattern_char) in pattern_chars.iter().rev() {
219 if i <= display_pos && pattern_char == self.input_char {
220 return Some(i);
221 }
222 }
223 None
224 }
225
226 pub fn display_mode(&self) -> &MaskDisplayMode {
228 &self.display_mode
229 }
230
231 pub fn is_template_mode(&self) -> bool {
233 matches!(self.display_mode, MaskDisplayMode::Template { .. })
234 }
235
236 pub fn pattern(&self) -> &str {
238 &self.pattern
239 }
240
241 pub fn input_char(&self) -> char {
243 self.input_char
244 }
245
246 pub fn first_input_position(&self) -> usize {
248 for (pos, ch) in self.pattern.chars().enumerate() {
249 if ch == self.input_char {
250 return pos;
251 }
252 }
253 0
254 }
255}
256
257impl Default for DisplayMask {
258 fn default() -> Self {
259 Self::new("", '#')
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_user_defined_phone_mask() {
269 let dynamic = DisplayMask::new("(###) ###-####", '#');
271 let template = DisplayMask::new("(###) ###-####", '#').with_template('_');
272
273 assert_eq!(dynamic.apply_to_display(""), "");
275 assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890");
276
277 assert_eq!(template.apply_to_display(""), "(___) ___-____");
279 assert_eq!(template.apply_to_display("123"), "(123) ___-____");
280 }
281
282 #[test]
283 fn test_user_defined_date_mask() {
284 let us_date = DisplayMask::new("##/##/####", '#');
286 let eu_date = DisplayMask::new("##.##.####", '#');
287 let iso_date = DisplayMask::new("####-##-##", '#');
288
289 assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024");
290 assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024");
291 assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25");
292 }
293
294 #[test]
295 fn test_user_defined_business_formats() {
296 let employee_id = DisplayMask::new("EMP-####-##", '#');
298 let product_code = DisplayMask::new("###-###-###", '#');
299 let invoice = DisplayMask::new("INV####/##", '#');
300
301 assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56");
302 assert_eq!(product_code.apply_to_display("123456789"), "123-456-789");
303 assert_eq!(invoice.apply_to_display("123456"), "INV1234/56");
304 }
305
306 #[test]
307 fn test_custom_input_characters() {
308 let mask_with_x = DisplayMask::new("XXX-XX-XXXX", 'X');
310 let mask_with_hash = DisplayMask::new("###-##-####", '#');
311 let mask_with_n = DisplayMask::new("NNN-NN-NNNN", 'N');
312
313 assert_eq!(mask_with_x.apply_to_display("123456789"), "123-45-6789");
314 assert_eq!(mask_with_hash.apply_to_display("123456789"), "123-45-6789");
315 assert_eq!(mask_with_n.apply_to_display("123456789"), "123-45-6789");
316 }
317
318 #[test]
319 fn test_custom_placeholders() {
320 let underscores = DisplayMask::new("##-##", '#').with_template('_');
322 let dots = DisplayMask::new("##-##", '#').with_template('•');
323 let dashes = DisplayMask::new("##-##", '#').with_template('-');
324
325 assert_eq!(underscores.apply_to_display(""), "__-__");
326 assert_eq!(dots.apply_to_display(""), "••-••");
327 assert_eq!(dashes.apply_to_display(""), "-----"); }
329
330 #[test]
331 fn test_position_mapping_user_patterns() {
332 let custom = DisplayMask::new("ABC-###-XYZ", '#');
333
334 assert_eq!(custom.raw_pos_to_display_pos(0), 4); assert_eq!(custom.raw_pos_to_display_pos(1), 5); assert_eq!(custom.raw_pos_to_display_pos(2), 6); assert_eq!(custom.display_pos_to_raw_pos(4), 0); assert_eq!(custom.display_pos_to_raw_pos(5), 1); assert_eq!(custom.display_pos_to_raw_pos(6), 2); assert!(!custom.is_input_position(0)); assert!(!custom.is_input_position(3)); assert!(custom.is_input_position(4)); assert!(!custom.is_input_position(8)); }
348}