categories.rs 4.6 KB

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