string_calc/
lib.rs

1//! `string_calc` is a collection of utilities to perform checked arithmetics on
2//! &str, to counter against floating point errors. It supports (checked) addition, 
3//! subtraction, and multiplication. It does not support division due to the 
4//! nature of division's ability to generate infinite amount of decimals. 
5//! 
6//! If your calculations have too many decimal points such that it can't fit i128 
7//! range AFTER calculation, it'll raise an error. 
8//! If you pass in weird values that's not integer or floats/decimals, it'll raise an error. 
9//! Return result will be Ok(String) which you can call .unwrap() or other methods to get
10//! its internal value, if everything goes right. 
11
12use std::cmp;
13
14/// Perform checked_add on floating points. 
15/// Inherits the "checked_add" property of rust in integers. 
16/// When failed, it'll return Err(message). Success, Ok(value). 
17/// 
18/// NOTE: Must use basic numbers UTF-8 or it'll fail. 
19/// 
20/// Example: 
21/// ```
22/// let lhs = "172.28";
23/// let rhs = "192.168";
24/// 
25/// assert_eq!(string_calc::checked_add(lhs, rhs).unwrap(), 
26///   "364.448".to_owned());
27/// ``` 
28pub fn checked_add(lhs: impl Into<String>, rhs: impl Into<String>) -> Result<String, &'static str> {
29  // Check for longest decimals. We find the "dot index"
30  let lhs: String = lhs.into();
31  let rhs: String = rhs.into();
32  let lhs: &str = lhs.as_str();
33  let rhs: &str = rhs.as_str(); 
34  let res = check_str(lhs);
35  let res2 = check_str(rhs);
36  if res.is_err() { return Err("LHS is invalid decimal. Please check."); }
37  if res2.is_err() { return Err("RHS is invalid decimal. Please check."); }
38
39  let (lhs_i128, rhs_i128, max_decimal) = convert_to_i128(lhs, rhs);
40
41  let value = lhs_i128.checked_add(rhs_i128);
42  if value.is_none() { return Err("Sorry, i128 add encounters overflow. Please find other methods to add."); }
43
44  // Add back the decimal points. 
45  let mut value_str: String = value.unwrap().to_string();
46  let insert_loc = value_str.len().checked_sub(max_decimal);
47  if max_decimal != 0 && insert_loc.is_some() { 
48    let insert_loc = insert_loc.unwrap();
49    value_str.insert(insert_loc, '.');
50    if insert_loc == 0 { value_str.insert(insert_loc, '0'); }
51    value_str = value_str.trim_end_matches('0').to_owned();
52  };
53  value_str = value_str.trim_end_matches(".").to_owned();
54
55  return Ok(value_str);
56}
57
58
59/// Performs checked_sub on floating points. 
60/// Inherits the checked_sub property of rust in integers. 
61/// When failed, it'll return Err(message). Success, Ok(value). 
62/// 
63/// NOTE: Must use basic numbers UTF-8 or it'll fail. 
64/// 
65/// Example: 
66/// ```
67/// let lhs = "192.168";
68/// let rhs = "172.28";
69/// 
70/// assert_eq!(string_calc::checked_sub(lhs, rhs).unwrap(),
71///   "19.888".to_owned());
72/// ``` 
73pub fn checked_sub(lhs: impl Into<String>, rhs: impl Into<String>) -> Result<String, &'static str> {
74  let lhs: String = lhs.into();
75  let rhs: String = rhs.into();
76  let lhs: &str = lhs.as_str();
77  let rhs: &str = rhs.as_str(); 
78
79  let res = check_str(lhs);
80  let res2 = check_str(rhs);
81  if res.is_err() { return Err("LHS is invalid decimal. Please check."); }
82  if res2.is_err() { return Err("RHS is invalid decimal. Please check."); }
83
84  let (lhs_i128, rhs_i128, max_decimal) = convert_to_i128(lhs, rhs);
85
86  let value = lhs_i128.checked_sub(rhs_i128);
87  if value.is_none() { return Err("Sorry, i128 add encounters overflow. Please find other methods to add."); }
88
89  // Add back the decimal points. 
90  let mut value_str: String = value.unwrap().to_string();
91  let insert_loc = value_str.len().checked_sub(max_decimal);
92  if max_decimal != 0 && insert_loc.is_some() { 
93    let insert_loc = insert_loc.unwrap();
94    value_str.insert(insert_loc, '.');
95    if insert_loc == 0 { value_str.insert(insert_loc, '0'); }
96    value_str = value_str.trim_end_matches('0').to_owned();
97  };
98  value_str = value_str.trim_end_matches(".").to_owned();
99
100  return Ok(value_str);
101}
102
103/// Performs checked_mul on floating points. 
104/// Inherits the checked_mul property of rust in integers. 
105/// When failed, it'll return Err(message). Success, Ok(value). 
106/// 
107/// NOTE: Must use basic numbers UTF-8 or it'll fail. 
108/// 
109/// Example: 
110/// ```
111/// let lhs = "12.35";
112/// let rhs = "15.6";
113/// 
114/// assert_eq!(string_calc::checked_mul(lhs, rhs).unwrap(),
115///   "192.66".to_owned());
116/// ```
117pub fn checked_mul(lhs: impl Into<String>, rhs: impl Into<String>) -> Result<String, &'static str> {
118  let lhs: String = lhs.into();
119  let rhs: String = rhs.into();
120  let lhs: &str = lhs.as_str();
121  let rhs: &str = rhs.as_str(); 
122
123  let res = check_str(lhs);
124  let res2 = check_str(rhs);
125  if res.is_err() { return Err("LHS is invalid decimal. Please check."); }
126  if res2.is_err() { return Err("RHS is invalid decimal. Please check."); }
127
128  let (lhs_i128, rhs_i128, max_decimal) = convert_to_i128(lhs, rhs);
129  let total_decimal = max_decimal.checked_mul(2);
130  if total_decimal.is_none() { return Err("Please report Bug: Failed to multiply total_decimal. "); }
131  let total_decimal = total_decimal.unwrap();
132  
133  let value = lhs_i128.checked_mul(rhs_i128);
134  if value.is_none() { return Err("Sorry, i128 add encounters overflow. Please find other methods to add."); }
135
136  // Add back decimal points. 
137  let mut value_str: String = value.unwrap().to_string();
138  let insert_loc = value_str.len().checked_sub(total_decimal);
139  if total_decimal != 0 && insert_loc.is_some() { 
140    let insert_loc = insert_loc.unwrap();
141    value_str.insert(insert_loc, '.');
142    if insert_loc == 0 { value_str.insert(insert_loc, '0'); }
143    value_str = value_str.trim_end_matches("0").to_owned();
144  };
145  value_str = value_str.trim_end_matches(".").to_owned();  // if last value is '.', trim. 
146
147  return Ok(value_str)
148}
149
150/// Performs comparison between 2 values. 
151/// When failed, it'll return Err(message). Success, Ok(bool). 
152/// 
153/// Available comparator are: "le", "ge", "lt", "gt", "eq" which 
154/// corresponds to "less than equal", "greater than equal", 
155/// "less than", "greater than", and "equal". 
156/// 
157/// The signs are compared to LHS. For example, less than equal would
158/// check if lhs <= rhs. 
159/// 
160/// Example: 
161/// ```
162/// let lhs = "12.35";
163/// let rhs = "17.5";
164/// 
165/// assert!(string_calc::compare(lhs, rhs, "lt").unwrap());
166/// assert!(!string_calc::compare(lhs, rhs, "ge").unwrap());
167/// ```
168pub fn compare(lhs: impl Into<String>, rhs: impl Into<String>, 
169  comparator: &str
170) -> Result<bool, &'static str> {
171  let lhs: String = lhs.into();
172  let rhs: String = rhs.into();
173  let lhs: &str = lhs.as_str();
174  let rhs: &str = rhs.as_str(); 
175
176  let res = check_str(lhs);
177  let res2 = check_str(rhs);
178  if res.is_err() { return Err("LHS is invalid decimal. Please check."); }
179  if res2.is_err() { return Err("RHS is invalid decimal. Please check."); }
180
181  let (lhs_i128, rhs_i128, _) = convert_to_i128(lhs, rhs);
182  match comparator {
183    "le" => return Ok(lhs_i128 <= rhs_i128),
184    "ge" => return Ok(lhs_i128 >= rhs_i128),
185    "lt" => return Ok(lhs_i128 < rhs_i128),
186    "gt" => return Ok(lhs_i128 > rhs_i128),
187    "eq" => return Ok(lhs_i128 == rhs_i128),
188    _ => return Err("Invalid comparator. Choose between le, ge, lt, gt, and eq.")
189  };
190}
191
192
193/// Sum of all values given a Vec<String> or Vec<str> of valid items; 
194/// 
195/// Example: 
196/// ```
197/// let data: Vec<String> = [100.325, 40.272, 51.23]
198///   .map(|c| c.to_string())
199///   .to_vec();
200/// 
201/// assert_eq!(string_calc::sum(data).unwrap(), "191.827".to_owned());
202/// 
203/// let data2 = vec!["20.0", "17", "28"];
204/// assert_eq!(string_calc::sum(data2).unwrap(), "65".to_owned());
205/// ``` 
206pub fn sum(values: Vec<impl Into<String> + Clone>) -> Result<String, &'static str> {
207  let mut _temp_vals: Vec<String> = Vec::new();
208  // let m = values[3];
209  for i in 0..values.len() {
210    let g: String = values[i].clone().into();
211    _temp_vals.push(g);
212  }
213  // Because g didn't live long enough to convert to .as_str() inside. 
214  let new_values: Vec<&str> = _temp_vals.iter().map(|c| c.as_str()).collect();
215
216  if new_values.iter().any(|c| check_str(*c).is_err()) {
217    // Unfortunately, we didn't check for exact indices where it fails, for now. 
218    return Err("There are values which didn't pass check_str. Please inspect and fix.");
219  }
220
221  let (new_vec, max_decimal) = convert_all_to_i128(new_values);
222
223  let sum_val: i128 = new_vec.iter().sum();
224
225  // Add back the decimal point. 
226  let mut value_str: String = sum_val.to_string();
227  let insert_loc = value_str.len().checked_sub(max_decimal);
228  if max_decimal != 0 && insert_loc.is_some() { 
229    let insert_loc = insert_loc.unwrap();
230    value_str.insert(insert_loc, '.');
231    if insert_loc == 0 { value_str.insert(insert_loc, '0'); }
232    value_str = value_str.trim_end_matches('0').to_owned();
233  };
234  value_str = value_str.trim_end_matches(".").to_owned();
235
236  return Ok(value_str);
237}
238
239
240
241
242// ==========================================================================
243fn check_str(str: &str) -> Result<(), &'static str> {
244  let mut dot_count = 0;
245  for c in str.chars() {
246    if !['0', '1', '2', '3', '4', '5', '6', 
247      '7', '8', '9', '.', '-'].contains(&c) 
248    {
249      return Err("Invalid String. Must be numbers and decimal point only.");
250    }
251
252    if c == '.' { dot_count += 1; }
253    if dot_count > 1 { return Err("Invalid String. Cannot have more than one decimal."); }
254  }
255  return Ok(());
256}
257
258/// Get the decimal point given a string. 
259/// di stands for dot index. 
260/// So 12 will return 0, while 12.1 will return 1. 
261fn get_decimal_points(num: &str) -> usize {
262  let num_len = num.len();
263  let num_di = num.find(".").unwrap_or(num_len - 1);
264  return num_len - (num_di + 1);
265}
266
267/// Preprocess the value by: 
268/// 1. Remove the dot. 
269/// 2. Add appropriate number of zeros at the back. 
270/// 
271/// curr_d = current decimals. NEED TO PASS IN CORRECT VALUE OR FAILED.
272/// Use cache value because it won't refigure it out, which waste computing power.
273fn preprocess_value(num: &str, curr_d: usize, max_d: usize) -> String {
274  let mut num_str = num.to_owned();
275  let num_di = num.find(".");
276  if num_di.is_some() { num_str.remove(num_di.unwrap()); }
277
278  // Add zeros where appropriate
279  let repeat_no = max_d - curr_d;
280  if repeat_no >= 1 {
281    for _ in 0..repeat_no {
282      num_str.push('0');
283    }
284  }
285
286  return num_str;
287}
288
289// Changed to i128 to support negative numbers. 
290fn convert_to_i128(lhs: &str, rhs: &str) -> (i128, i128, usize) {
291  let lhs_decimals = get_decimal_points(lhs);
292  let rhs_decimals = get_decimal_points(rhs);
293  let max_decimal = cmp::max(lhs_decimals, rhs_decimals);
294
295  // Remove the dot. 
296  let lhs_int = preprocess_value(lhs, lhs_decimals, max_decimal);
297  let rhs_int = preprocess_value(rhs, rhs_decimals, max_decimal);
298
299  let lhs_i128: i128 = lhs_int.parse().unwrap();
300  let rhs_i128: i128 = rhs_int.parse().unwrap();
301
302  return (lhs_i128, rhs_i128, max_decimal);
303}
304
305// Convert all to i128
306// Unfortunately, can't reduce the iteration cause we need max_decimal first, before
307// we can continue. 
308fn convert_all_to_i128(our_vec: Vec<&str>) -> (Vec<i128>, usize) {
309  let decimals: Vec<usize> = our_vec.iter()
310    .map(|c| get_decimal_points(*c))
311    .collect();
312  let max_decimal: usize = *decimals.iter().max().unwrap();
313
314  let ret_val: Vec<i128> = our_vec.iter().enumerate().map(|(i, c)| {
315    let value = preprocess_value(c, decimals[i], max_decimal);
316    let value_i128 = value.parse().unwrap();
317    value_i128
318  }).collect();
319
320  return (ret_val, max_decimal);
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_alphabets_wont_work() {
329      let result = check_str("123str");
330      assert!(result.is_err());
331    }
332
333    #[test]
334    fn test_char_wont_work() {
335      let result = check_str("我爱你");
336      assert!(result.is_err());
337    }
338
339    #[test]
340    fn test_floats_work() {
341      let result = check_str("12.376");
342      assert!(result.is_ok());
343    }
344
345    #[test]
346    fn test_ip_addr_not_work() {
347      let result = check_str("192.168.10.1");
348      assert!(result.is_err());
349    }
350
351    #[test]
352    fn correct_decimal() {
353      let decimal = get_decimal_points("12.376");
354      assert_eq!(decimal, 3);
355    }
356
357    #[test]
358    fn correct_integer() {
359      let decimal = get_decimal_points("123");
360      assert_eq!(decimal, 0);
361    }
362
363    #[test]
364    fn preprocess_value_case_1() {
365      let value = preprocess_value("12.237", 3, 3);
366      assert_eq!(value, "12237");
367    }
368
369    #[test]
370    fn preprocess_value_case_2() {
371      let value = preprocess_value("12.237", 3, 5);
372      assert_eq!(value, "1223700");
373    }
374
375    #[test]
376    fn preprocess_value_case_3() {
377      let value = preprocess_value("12", 0, 3);
378      assert_eq!(value, "12000");
379    }
380
381    #[test]
382    fn preprocess_value_case_4() {
383      let value = preprocess_value("12", 0, 0);
384      assert_eq!(value, "12");
385    }
386
387    #[test]
388    fn checked_add_case_1() {
389      let value = checked_add("12.32", "15.6");
390      assert_eq!(value.unwrap(), "27.92".to_owned());
391    }
392
393    #[test]
394    fn checked_add_case_2() {
395      let value = checked_add("12.3", "17");
396      assert_eq!(value.unwrap(), "29.3".to_owned());
397    }
398
399    #[test]
400    fn checked_add_case_3() {
401      let value = checked_add("12", "15");
402      assert_eq!(value.unwrap(), "27".to_owned());
403    }
404
405    #[test]
406    fn checked_add_case_4() {
407      let value = checked_add("meh", "12");
408      assert!(value.is_err());
409    }
410
411    #[test]
412    fn checked_add_case_5() {
413      let value = checked_add("12", "meh");
414      assert!(value.is_err());
415    }
416
417    // not sure if we can test overflow or not for addition; 
418    // but certainly can do for multiplication. 
419
420    #[test]
421    fn checked_sub_case_1() {
422      let value = checked_sub("12.367", "9.3");
423      assert_eq!(value.unwrap(), "3.067".to_owned());
424    }
425
426    #[test]
427    fn checked_sub_case_2() {
428      let value = checked_sub("12", "6.3");
429      assert_eq!(value.unwrap(), "5.7".to_owned());
430    }
431
432    #[test]
433    fn checked_sub_case_3() {
434      let value = checked_sub("12", "6");
435      assert_eq!(value.unwrap(), "6".to_owned());
436    }
437
438    #[test]
439    fn checked_sub_case_4() {
440      let value = checked_sub("meh", "6");
441      assert!(value.is_err());
442    }
443
444    #[test]
445    fn checked_sub_case_5() {
446      let value = checked_sub("6", "meh");
447      assert!(value.is_err());
448    }
449
450    #[test]
451    fn checked_mul_case_1() {
452      let value = checked_mul("19.87", "13.625");
453      assert_eq!(value.unwrap(), "270.72875".to_owned());
454    }
455
456    #[test]
457    fn checked_mul_case_2() {
458      let value = checked_mul("12.3", "6");
459      let value2 = checked_mul("6", "12.3");
460      assert_eq!(value.clone().unwrap(), "73.8".to_owned());
461      assert_eq!(value.unwrap(), value2.unwrap());
462    }
463
464    #[test]
465    fn checked_mul_case_3() {
466      let value = checked_mul("12.5", "6");
467      assert_eq!(value.unwrap(), "75".to_owned());
468    }
469
470    #[test]
471    fn checked_mul_case_4() {
472      let value = checked_mul("meh", "6");
473      assert!(value.is_err());
474    }
475
476    #[test]
477    fn checked_mul_case_5() {
478      let value = checked_mul("6", "meh");
479      assert!(value.is_err());
480    }
481
482    #[test]
483    fn check_comparator_le() {
484      assert!(compare("12.5", "17.6", "le").unwrap());
485      assert!(!compare("17.6", "12.5", "le").unwrap());
486      assert!(compare("12.5", "12.5", "le").unwrap());
487    }
488
489    #[test]
490    fn check_comparator_lt() {
491      assert!(compare("12.5", "17.6", "lt").unwrap());
492      assert!(!compare("17.6", "12.5", "lt").unwrap());
493      assert!(!compare("12.5", "12.5", "lt").unwrap());
494    }
495
496    #[test]
497    fn check_comparator_ge() {
498      assert!(!compare("12.5", "17.6", "ge").unwrap());
499      assert!(compare("17.6", "12.5", "ge").unwrap());
500      assert!(compare("12.5", "12.5", "ge").unwrap());
501    }
502
503    #[test]
504    fn check_comparator_gt() {
505      assert!(!compare("12.5", "17.6", "gt").unwrap());
506      assert!(compare("17.6", "12.5", "gt").unwrap());
507      assert!(!compare("12.5", "12.5", "gt").unwrap());
508    }
509
510    #[test]
511    fn check_comparator_eq() {
512      assert!(!compare("12.5", "17.6", "eq").unwrap());
513      assert!(!compare("17.6", "12.5", "eq").unwrap());
514      assert!(compare("12.5", "12.5", "eq").unwrap());
515    }
516
517    #[test]
518    fn check_comparator_wrong() {
519      assert!(compare("12.5", "17.6", "meh").is_err());
520    }
521
522    #[test]
523    fn check_after_minus_no_decimal_no_cause_error() {
524      assert!(checked_sub("12.70", "12.7").is_ok());
525      assert_eq!(checked_sub("25.400", "12.8").unwrap(), "12.6".to_owned());
526      assert_eq!(checked_sub("12.8", "11.8".to_owned()).unwrap(), "1".to_owned());
527      assert_eq!(checked_sub("12.8", "12.0000").unwrap(), "0.8".to_owned());
528      assert!(checked_mul("12.52", "0").is_ok());
529    }
530
531    #[test]
532    fn check_string_works_not_only_str() {
533      assert_eq!(checked_add("12.5".to_owned(), "13.70".to_owned()).unwrap(), "26.2".to_owned());
534      assert_eq!(checked_sub("12.5".to_owned(), "9.62".to_owned()).unwrap(), "2.88".to_owned());
535      assert_eq!(checked_mul("12.5".to_owned(), "7.2".to_owned()).unwrap(), "90".to_owned());
536    }
537
538    #[test]
539    fn checked_can_run_negative_numbers() {
540      assert_eq!(checked_sub("12.8", "17.5").unwrap(), "-4.7".to_owned());
541    }
542
543    #[test]
544    fn check_special_case_negative_numbers() {
545      assert_eq!(checked_add("-12.800", "12.8").unwrap(), "0".to_owned());
546      assert_eq!(checked_sub("-12.8", "-12.80").unwrap(), "0".to_owned());
547      assert_eq!(checked_sub("-12.8", "-11.0").unwrap(), "-1.8".to_owned());
548      assert_eq!(checked_add("12.8", "-12.0000").unwrap(), "0.8".to_owned());
549    }
550
551    #[test]
552    fn check_convert_all_to_i128_works() {
553      let vec: Vec<&str> = vec!["12.368", "1.14", "28"];
554      let (new_vec, max_decimal) = convert_all_to_i128(vec);
555      assert_eq!(new_vec, vec![12368, 1140, 28000]);
556      assert_eq!(max_decimal, 3);
557    }
558
559    #[test]
560    fn check_sum_all_with_negative_numbers() {
561      let vec = vec!["-12.42", "3.87", "2.323"];
562      assert_eq!(sum(vec).unwrap(), "-6.227".to_owned());
563    }
564}