categories.rs 4.4 KB

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