rex_app/ui_helper/
verifier.rs

1use std::cmp::Ordering;
2use std::collections::HashSet;
3
4use chrono::NaiveDate;
5use rex_db::ConnCache;
6use rex_db::models::TxType;
7use strum::IntoEnumIterator;
8
9use crate::conn::MutDbConn;
10use crate::ui_helper::{DateType, Field, Output, VerifierError, get_best_match};
11
12pub struct Verifier<'a> {
13    conn: MutDbConn<'a>,
14}
15
16impl<'a> Verifier<'a> {
17    pub(crate) fn new(conn: MutDbConn<'a>) -> Self {
18        Self { conn }
19    }
20
21    /// Checks if:
22    ///
23    /// - The inputted year is between 2022 to 2037.
24    /// - The inputted month is between 01 to 12.
25    /// - The inputted date is between 01 to 31.
26    /// - The inputted date is empty.
27    /// - Contains any extra spaces.
28    /// - The date actually exists.
29    /// - Removes any extra spaces and non-numeric characters.
30    /// - Ensures proper char length for each part of the date.
31    ///
32    /// Finally, tries to correct the date if it was not accepted by
33    /// adding 0 if the beginning if the length is smaller than necessary
34    /// or restores to the smallest or the largest date if date is beyond the
35    /// accepted value.
36    pub fn date(
37        self,
38        user_date: &mut String,
39        date_type: DateType,
40    ) -> Result<Output, VerifierError> {
41        if user_date.is_empty() {
42            return Ok(Output::Nothing(Field::Date));
43        }
44
45        *user_date = user_date
46            .chars()
47            .filter(|c| c.is_numeric() || *c == '-')
48            .collect();
49
50        let split_date = user_date
51            .split('-')
52            .map(ToString::to_string)
53            .collect::<Vec<String>>();
54
55        // If one part of the date is missing/extra, return unknown date
56        match date_type {
57            DateType::Exact => {
58                if split_date.len() != 3 {
59                    *user_date = "2022-01-01".to_string();
60                    return Err(VerifierError::InvalidDate);
61                }
62            }
63            DateType::Monthly => {
64                if split_date.len() != 2 {
65                    *user_date = "2022-01".to_string();
66                    return Err(VerifierError::InvalidDate);
67                }
68            }
69            DateType::Yearly => {
70                if split_date.len() != 1 {
71                    *user_date = "2022".to_string();
72                    return Err(VerifierError::InvalidDate);
73                }
74            }
75        }
76
77        let (int_month, int_day): (Option<u16>, Option<u16>) = match date_type {
78            DateType::Exact => {
79                let month = split_date[1]
80                    .parse()
81                    .map_err(|_| VerifierError::ParsingError(Field::Date))?;
82
83                let day = split_date[2]
84                    .parse()
85                    .map_err(|_| VerifierError::ParsingError(Field::Date))?;
86
87                (Some(month), Some(day))
88            }
89            DateType::Monthly => {
90                let month = split_date[1]
91                    .parse()
92                    .map_err(|_| VerifierError::ParsingError(Field::Date))?;
93
94                (Some(month), None)
95            }
96            DateType::Yearly => (None, None),
97        };
98
99        // Checks if the year part length is 4. If not 4, turn the year to 2022 + the other character entered by the user
100        // and return the new date
101        if split_date[0].len() != 4 {
102            match split_date[0].len().cmp(&4) {
103                Ordering::Less => match date_type {
104                    DateType::Exact => {
105                        *user_date = format!("2022-{}-{}", split_date[1], split_date[2]);
106                    }
107                    DateType::Monthly => *user_date = format!("2022-{}", split_date[1]),
108                    DateType::Yearly => *user_date = "2022".to_string(),
109                },
110                Ordering::Greater => match date_type {
111                    DateType::Exact => {
112                        *user_date = format!(
113                            "{}-{}-{}",
114                            &split_date[0][..4],
115                            split_date[1],
116                            split_date[2]
117                        );
118                    }
119                    DateType::Monthly => {
120                        *user_date = format!("{}-{}", &split_date[0][..4], split_date[1]);
121                    }
122                    DateType::Yearly => *user_date = split_date[0][..4].to_string(),
123                },
124                Ordering::Equal => {}
125            }
126            return Err(VerifierError::InvalidYear);
127        }
128
129        // Checks if the month part length is 2. If not 2, turn the month to 0 + whatever month was entered + the other character entered by the user
130        // and return the new date
131        match date_type {
132            DateType::Exact => {
133                if split_date[1].len() != 2 {
134                    let unwrapped_month = int_month.unwrap();
135                    if unwrapped_month < 10 {
136                        *user_date =
137                            format!("{}-0{unwrapped_month}-{}", split_date[0], split_date[2]);
138                    } else if unwrapped_month > 12 {
139                        *user_date = format!("{}-12-{}", split_date[0], split_date[2]);
140                    }
141
142                    return Err(VerifierError::InvalidMonth);
143                }
144            }
145            DateType::Monthly => {
146                let unwrapped_month = int_month.unwrap();
147                if split_date[1].len() != 2 {
148                    if unwrapped_month < 10 {
149                        *user_date = format!("{}-0{unwrapped_month}", split_date[0]);
150                    } else if unwrapped_month > 12 {
151                        *user_date = format!("{}-12", split_date[0]);
152                    }
153
154                    return Err(VerifierError::InvalidMonth);
155                }
156            }
157            DateType::Yearly => {}
158        }
159
160        // Checks if the day part length is 2. If not 2, turn the day to 0 + whatever day was entered + the other character entered by the user
161        // and return the new date
162        if let DateType::Exact = date_type {
163            let unwrapped_day = int_day.unwrap();
164            if split_date[2].len() != 2 {
165                if unwrapped_day < 10 {
166                    *user_date = format!("{}-{}-0{unwrapped_day}", split_date[0], split_date[1]);
167                } else if unwrapped_day > 31 {
168                    *user_date = format!("{}-{}-31", split_date[0], split_date[1]);
169                }
170
171                return Err(VerifierError::InvalidDay);
172            }
173        }
174
175        // Checks if the month value is between 1 and 12
176        match date_type {
177            DateType::Exact => {
178                let unwrapped_month = int_month.unwrap();
179                if !(1..=12).contains(&unwrapped_month) {
180                    if unwrapped_month < 1 {
181                        *user_date = format!("{}-01-{}", split_date[0], split_date[2]);
182                    } else if unwrapped_month > 12 {
183                        *user_date = format!("{}-12-{}", split_date[0], split_date[2]);
184                    }
185
186                    return Err(VerifierError::MonthTooBig);
187                }
188            }
189            DateType::Monthly => {
190                let unwrapped_month = int_month.unwrap();
191                if !(1..=12).contains(&unwrapped_month) {
192                    if unwrapped_month < 1 {
193                        *user_date = format!("{}-01", split_date[0]);
194                    } else if unwrapped_month > 12 {
195                        *user_date = format!("{}-12", split_date[0]);
196                    }
197
198                    return Err(VerifierError::MonthTooBig);
199                }
200            }
201            DateType::Yearly => {}
202        }
203
204        // Checks if the day value is between 1 and 31
205        if let DateType::Exact = date_type {
206            let unwrapped_day = int_day.unwrap();
207            if !(1..=31).contains(&unwrapped_day) {
208                if unwrapped_day < 1 {
209                    *user_date = format!("{}-{}-01", split_date[0], split_date[1]);
210                } else if unwrapped_day > 31 {
211                    *user_date = format!("{}-{}-31", split_date[0], split_date[1]);
212                }
213
214                return Err(VerifierError::DayTooBig);
215            }
216        }
217
218        // We will check if the date actually exists otherwise return error
219        // Some months have more or less days than 31 so the date needs to be validated
220        if let DateType::Exact = date_type {
221            NaiveDate::parse_from_str(user_date, "%Y-%m-%d")
222                .map_err(|_| VerifierError::NonExistingDate)?;
223        }
224
225        Ok(Output::Accepted(Field::Date))
226    }
227
228    /// Checks if:
229    ///
230    /// - Amount is empty
231    /// - Amount is zero or below
232    /// - Amount text contains a calculation symbol
233    /// - contains any extra spaces
234    /// - removes any extra spaces and non-numeric characters
235    ///
236    /// If the value is not float, tries to make it float ending with double zero
237    pub fn amount(&self, user_amount: &mut String) -> Result<Output, VerifierError> {
238        // Cancel all verification if the amount is empty
239        if user_amount.is_empty() {
240            return Ok(Output::Nothing(Field::Amount));
241        }
242
243        let calc_symbols = vec!['*', '/', '+', '-'];
244
245        *user_amount = user_amount
246            .chars()
247            .filter(|c| c.is_numeric() || *c == '.' || calc_symbols.contains(c))
248            .collect();
249
250        // Already checked if the initial amount is empty.
251        // If it becomes empty after the filtering was done, there no number inside so return error
252        if user_amount.is_empty() {
253            return Err(VerifierError::ParsingError(Field::Amount));
254        }
255
256        // Check if any of the symbols are present
257        if calc_symbols.iter().any(|s| user_amount.contains(*s)) {
258            // How it works:
259            // The calc_symbol intentionally starts with * and / so these calculations are done first.
260            // Start a main loop which will only run for the amount of times anyone of them from calc_symbols is present.
261            // Loop over the symbols and check if the symbol is present in the string
262            // find the index of where the symbol is then take the number values from both side of the symbol.
263            // Example: 1+5*10. We start with *, we initially, we will work with 5*10.
264            // Isolate the numbers => do the calculation => replace the part of the string we are working with, with the result which is 50
265            // result: 1+50 => break the symbol checking loop and continue the main loop again so we start working with 1+50.
266
267            // Get the amount of time the symbols were found in the amount string
268            let count = user_amount
269                .chars()
270                .filter(|c| calc_symbols.contains(c))
271                .count();
272
273            // Remove all spaces for easier indexing
274            let mut working_value = user_amount.to_owned();
275
276            for _i in 0..count {
277                for symbol in &calc_symbols {
278                    if let Some(location) = working_value.find(*symbol) {
279                        // If a symbol is found, we want to store the values to its side to these variables.
280                        // Example: 1+5 first_value = 1 last_value = 5
281                        let mut first_value = String::new();
282                        let mut last_value = String::new();
283
284                        // Skip to symbol location + 1 index value and start taking chars from here until the end
285                        // of the string or until another cal symbol is encountered
286                        for char in working_value.chars().skip(location + 1) {
287                            if calc_symbols.contains(&char) {
288                                break;
289                            }
290                            last_value.push(char);
291                        }
292
293                        // Do the same thing as before but this time, reverse the string
294                        for char in working_value
295                            .chars()
296                            .rev()
297                            .skip(working_value.len() - location)
298                        {
299                            if calc_symbols.contains(&char) {
300                                break;
301                            }
302                            first_value.push(char);
303                        }
304                        // Un-reverse the string
305                        first_value = first_value.chars().rev().collect();
306
307                        // If either of them is empty, the one that is not empty is the value we want to use for using in replacement
308                        let final_value = if first_value.is_empty() || last_value.is_empty() {
309                            if first_value.is_empty() {
310                                last_value.clone()
311                            } else {
312                                first_value.clone()
313                            }
314                        } else {
315                            // If both value is intact, do the calculation and the result is for replacement
316                            let first_num: f64 = match first_value.parse() {
317                                Ok(v) => v,
318                                Err(_) => {
319                                    return Err(VerifierError::ParsingError(Field::Amount));
320                                }
321                            };
322
323                            let last_num: f64 = match last_value.parse() {
324                                Ok(v) => v,
325                                Err(_) => {
326                                    return Err(VerifierError::ParsingError(Field::Amount));
327                                }
328                            };
329
330                            match *symbol {
331                                '*' => format!("{:.2}", (first_num * last_num)),
332                                '/' => format!("{:.2}", (first_num / last_num)),
333                                '+' => format!("{:.2}", (first_num + last_num)),
334                                '-' => format!("{:.2}", (first_num - last_num)),
335                                _ => String::new(),
336                            }
337                        };
338
339                        // Example: 1+5*10
340                        // if everything goes alright, first_value is 5, last_value is 10 and the symbol is *
341                        // replace 5*10 with the earlier result we got which is 50. Continue with 1+50 in the next loop
342                        working_value = working_value
343                            .replace(&format!("{first_value}{symbol}{last_value}"), &final_value);
344
345                        break;
346                    }
347                }
348            }
349            *user_amount = working_value;
350        }
351
352        // If dot is present but nothing after that, add 2 zero
353        // if no dot, add dot + 2 zero
354        if user_amount.contains('.') {
355            let state = user_amount.split('.').collect::<Vec<&str>>();
356            if state[1].is_empty() {
357                *user_amount += "00";
358            }
359        } else {
360            *user_amount = format!("{user_amount}.00");
361        }
362
363        let float_amount: f64 = user_amount
364            .parse()
365            .map_err(|_| VerifierError::ParsingError(Field::Amount))?;
366
367        if float_amount <= 0.0 {
368            *user_amount = format!("{:.2}", (float_amount - (float_amount * 2.0)));
369            return Err(VerifierError::AmountBelowZero);
370        }
371
372        // Checks if there is 2 number after the dot else add zero/s
373        if user_amount.contains('.') {
374            let split_amount = user_amount.split('.').collect::<Vec<&str>>();
375
376            match split_amount[1].len().cmp(&2) {
377                Ordering::Less => *user_amount = format!("{user_amount}0"),
378                Ordering::Greater => {
379                    *user_amount = format!("{}.{}", split_amount[0], &split_amount[1][..2]);
380                }
381                Ordering::Equal => (),
382            }
383        }
384
385        // We can safely split now as previously we just added a dot + 2 numbers with the amount
386        // and create the final value for the amount
387        let split_amount = user_amount.split('.').collect::<Vec<&str>>();
388
389        // limit max character to 10
390        if split_amount[0].len() > 10 {
391            *user_amount = format!("{}.{}", &split_amount[0][..10], split_amount[1]);
392        }
393
394        Ok(Output::Accepted(Field::Amount))
395    }
396
397    /// Checks if:
398    ///
399    /// - The Transaction method exists on the database.
400    /// - The Transaction method is empty
401    /// - contains any extra spaces
402    ///
403    /// If the Transaction is not found, matches each character with the available
404    /// Transaction Methods and corrects to the best matching one.
405    pub fn tx_method(&self, user_method: &mut String) -> Result<Output, VerifierError> {
406        // Get all currently added tx methods
407
408        *user_method = user_method.trim().to_string();
409
410        // Cancel all verification if the text is empty
411        if user_method.is_empty() {
412            return Ok(Output::Nothing(Field::TxMethod));
413        }
414
415        let all_tx_methods = self.conn.cache().get_methods();
416
417        for method in &all_tx_methods {
418            let method_name = &method.name;
419
420            if method_name.to_lowercase() == user_method.to_lowercase() {
421                *user_method = method_name.clone();
422                return Ok(Output::Accepted(Field::Amount));
423            }
424        }
425
426        let user_method_names = all_tx_methods
427            .iter()
428            .map(|m| m.name.clone())
429            .collect::<Vec<String>>();
430
431        let best_match = get_best_match(user_method, &user_method_names);
432
433        *user_method = best_match;
434
435        Err(VerifierError::InvalidTxMethod)
436    }
437
438    /// Checks if:
439    ///
440    /// - The transaction method starts with E, I, or T
441    ///
442    /// Auto expands E to Expense, I to Income and T to transfer.
443    pub fn tx_type(&self, user_type: &mut String) -> Result<Output, VerifierError> {
444        let trimmed_input = user_type.trim();
445
446        if user_type.is_empty() {
447            return Ok(Output::Nothing(Field::TxType));
448        }
449
450        let tx_types = TxType::iter()
451            .map(|s| s.to_string())
452            .collect::<Vec<String>>();
453
454        let return_best_match = || {
455            let best_match = get_best_match(user_type, &tx_types);
456
457            if best_match == trimmed_input {
458                String::new()
459            } else {
460                best_match
461            }
462        };
463
464        let lowercase = user_type.to_lowercase();
465
466        if lowercase.len() <= 2 {
467            if lowercase.starts_with('e') {
468                *user_type = TxType::Expense.to_string();
469            } else if lowercase.starts_with('i') {
470                *user_type = TxType::Income.to_string();
471            } else if lowercase.starts_with('t') {
472                *user_type = TxType::Transfer.to_string();
473            } else if lowercase.starts_with("br") {
474                *user_type = TxType::BorrowRepay.to_string();
475            } else if lowercase.starts_with("lr") {
476                *user_type = TxType::LendRepay.to_string();
477            } else if lowercase.starts_with('b') {
478                *user_type = TxType::Borrow.to_string();
479            } else if lowercase.starts_with('l') {
480                *user_type = TxType::Lend.to_string();
481            } else {
482                *user_type = return_best_match();
483                return Err(VerifierError::InvalidTxType);
484            }
485        } else {
486            if tx_types.contains(user_type) {
487                return Ok(Output::Accepted(Field::TxType));
488            }
489            *user_type = return_best_match();
490            return Err(VerifierError::InvalidTxType);
491        }
492
493        Ok(Output::Accepted(Field::TxType))
494    }
495
496    /// Checks if:
497    ///
498    /// - All tags inserted is unique and is properly separated by commas
499    pub fn tags(&self, user_tag: &mut String) {
500        let mut split_tags = user_tag.split(',').map(str::trim).collect::<Vec<&str>>();
501        split_tags.retain(|s| !s.is_empty());
502
503        let mut seen = HashSet::new();
504
505        // Vec for keeping the original order
506        let mut unique = Vec::new();
507
508        for item in split_tags {
509            if seen.insert(item) {
510                unique.push(item);
511            }
512        }
513
514        *user_tag = unique.join(", ");
515    }
516
517    /// Checks if:
518    ///
519    /// - All tags inserted is unique and is properly separated by commas
520    /// - There is no non-existing tags
521    pub fn tags_forced(&self, user_tag: &mut String) -> Result<Output, VerifierError> {
522        if user_tag.is_empty() {
523            return Ok(Output::Nothing(Field::Tags));
524        }
525
526        let all_tags = self.conn.cache().get_tags_set();
527
528        let mut split_tags = user_tag.split(',').map(str::trim).collect::<Vec<&str>>();
529        split_tags.retain(|s| !s.is_empty());
530
531        let mut seen = HashSet::new();
532        let mut unique = Vec::new();
533
534        for item in split_tags {
535            if seen.insert(item) {
536                unique.push(item);
537            }
538        }
539
540        let old_tags_len = unique.len();
541
542        unique.retain(|&tag| all_tags.contains(tag));
543
544        let new_tags_len = unique.len();
545
546        *user_tag = unique.join(", ");
547
548        if old_tags_len == new_tags_len {
549            Ok(Output::Accepted(Field::Tags))
550        } else {
551            Err(VerifierError::NonExistingTag)
552        }
553    }
554}