categories.rs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #[cfg(feature = "telegram")]
  2. use crate::ui::input_category;
  3. use libc::isatty;
  4. use radix_trie::Trie;
  5. use serde::{Deserialize, Serialize};
  6. use std::cmp::Ordering;
  7. use std::collections::HashSet;
  8. /// Category statistics for single item
  9. #[derive(Serialize, Deserialize, Debug)]
  10. pub struct CatStat {
  11. /// Category name
  12. category: String,
  13. /// How many times did the item hit this category
  14. hits: i64,
  15. }
  16. impl Ord for CatStat {
  17. fn cmp(&self, other: &Self) -> Ordering {
  18. self.hits.cmp(&other.hits)
  19. }
  20. }
  21. impl PartialOrd for CatStat {
  22. fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
  23. Some(self.cmp(other))
  24. }
  25. }
  26. impl PartialEq for CatStat {
  27. fn eq(&self, other: &Self) -> bool {
  28. self.hits == other.hits
  29. }
  30. }
  31. impl Eq for CatStat {}
  32. pub type CatStats = Trie<String, Vec<CatStat>>;
  33. /// Insert new `cat` into statistics vector or add single usage to existing cat
  34. fn update_stat(cat: &str, stat: &mut Vec<CatStat>) {
  35. let existing = stat.iter_mut().find(|stat| stat.category == cat);
  36. match existing {
  37. Some(e) => e.hits += 1,
  38. None => stat.push(CatStat {
  39. category: String::from(cat),
  40. hits: 1,
  41. }),
  42. }
  43. stat.sort_by(|a, b| b.cmp(a));
  44. }
  45. /// Set up `cat` as category for `item`: update statistics or create new item
  46. /// in `storage`
  47. pub fn assign_category(item: &str, cat: &str, storage: &mut CatStats) {
  48. if cat.is_empty() {
  49. panic!("Do not assign empty category!")
  50. }
  51. let existing = storage.get_mut(item);
  52. match existing {
  53. Some(stat) => update_stat(cat, stat),
  54. None => {
  55. let newstat = CatStat {
  56. category: String::from(cat),
  57. hits: 1,
  58. };
  59. storage.insert(String::from(item), vec![newstat]);
  60. }
  61. }
  62. }
  63. /// Return most probable category for provided `item`
  64. pub fn get_top_category<'a>(item: &str, storage: &'a CatStats) -> Option<&'a str> {
  65. storage.get(item).map(|s| -> &'a str { &s[0].category })
  66. }
  67. /// Choose proper category or ask user
  68. pub fn get_category(item: &str, storage: &mut CatStats, accounts: &HashSet<String>) -> String {
  69. let istty = unsafe { isatty(libc::STDOUT_FILENO) } != 0;
  70. if istty {
  71. let topcat = match get_top_category(item, storage) {
  72. Some(cat) => String::from(cat),
  73. None => String::new(),
  74. };
  75. let cats: Vec<&String> = accounts
  76. .iter()
  77. .filter(|acc| acc.contains("Expenses:"))
  78. .collect();
  79. let cat = input_category(item, &topcat, &cats);
  80. if cat.is_empty() {
  81. topcat
  82. } else {
  83. assign_category(item, &cat, storage);
  84. cat
  85. }
  86. } else {
  87. match get_top_category(item, storage) {
  88. Some(cat) => String::from(cat),
  89. None => String::new(),
  90. }
  91. }
  92. }
  93. #[cfg(test)]
  94. mod tests {
  95. use super::*;
  96. #[test]
  97. fn test_update_stat() {
  98. let mut stat: Vec<CatStat> = Vec::new();
  99. update_stat("test", &mut stat);
  100. assert_eq!(stat[0].hits, 1);
  101. assert_eq!(stat[0].category, "test");
  102. update_stat("test", &mut stat);
  103. assert_eq!(stat[0].hits, 2);
  104. update_stat("test2", &mut stat);
  105. assert_eq!(stat[1].category, "test2");
  106. assert_eq!(stat[1].hits, 1);
  107. update_stat("test2", &mut stat);
  108. update_stat("test2", &mut stat);
  109. assert_eq!(stat[0].category, "test2");
  110. assert_eq!(stat[0].hits, 3);
  111. assert_eq!(stat[1].hits, 2);
  112. assert_eq!(stat[1].category, "test");
  113. }
  114. #[test]
  115. fn test_assign_category() {
  116. let mut cm: Trie<String, Vec<CatStat>> = Trie::new();
  117. assign_category("item", "category", &mut cm);
  118. let stats = cm.get("item").unwrap();
  119. assert_eq!(stats[0].category, "category");
  120. assert_eq!(stats[0].hits, 1);
  121. let topcat = get_top_category("item", &cm).unwrap();
  122. assert_eq!(topcat, "category");
  123. }
  124. }