categories.rs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #[cfg(feature = "telegram")]
  2. use crate::telegram::bot_is_running;
  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. /*
  73. #[cfg(feature = "telegram")]
  74. pub fn get_category_from_tg(
  75. item: &str,
  76. storage: &mut CatStats,
  77. accounts: &[String],
  78. ctx: &UpdateWithCx<AutoSend<Bot>, Message>,
  79. ) -> String {
  80. if bot_is_running() {
  81. let future = async move { input_category_from_tg(item, storage, accounts, ctx).await };
  82. if let Ok(handle) = Handle::try_current() {
  83. tokio::task::block_in_place(move || handle.block_on(future))
  84. } else {
  85. String::new()
  86. }
  87. } else {
  88. String::new()
  89. }
  90. }
  91. */
  92. /// Choose proper category or ask user
  93. pub fn get_category(item: &str, storage: &mut CatStats, accounts: &[String]) -> String {
  94. let istty = unsafe { isatty(libc::STDOUT_FILENO as i32) } != 0;
  95. if istty {
  96. let topcat = match get_top_category(item, storage) {
  97. Some(cat) => String::from(cat),
  98. None => String::new(),
  99. };
  100. let cats: Vec<&String> = accounts
  101. .iter()
  102. .filter(|acc| acc.contains("Expenses:"))
  103. .collect();
  104. let cat = input_category(item, &topcat, &cats);
  105. if cat.is_empty() {
  106. topcat
  107. } else {
  108. assign_category(item, &cat, storage);
  109. cat
  110. }
  111. } else {
  112. match get_top_category(item, storage) {
  113. Some(cat) => String::from(cat),
  114. None => String::new(),
  115. }
  116. }
  117. }
  118. #[cfg(test)]
  119. mod tests {
  120. use super::*;
  121. #[test]
  122. fn test_update_stat() {
  123. let mut stat: Vec<CatStat> = Vec::new();
  124. update_stat("test", &mut stat);
  125. assert_eq!(stat[0].hits, 1);
  126. assert_eq!(stat[0].category, "test");
  127. update_stat("test", &mut stat);
  128. assert_eq!(stat[0].hits, 2);
  129. update_stat("test2", &mut stat);
  130. assert_eq!(stat[1].category, "test2");
  131. assert_eq!(stat[1].hits, 1);
  132. update_stat("test2", &mut stat);
  133. update_stat("test2", &mut stat);
  134. assert_eq!(stat[0].category, "test2");
  135. assert_eq!(stat[0].hits, 3);
  136. assert_eq!(stat[1].hits, 2);
  137. assert_eq!(stat[1].category, "test");
  138. }
  139. #[test]
  140. fn test_assign_category() {
  141. let mut cm: Trie<String, Vec<CatStat>> = Trie::new();
  142. assign_category("item", "category", &mut cm);
  143. let stats = cm.get("item").unwrap();
  144. assert_eq!(stats[0].category, "category");
  145. assert_eq!(stats[0].hits, 1);
  146. let topcat = get_top_category("item", &mut cm).unwrap();
  147. assert_eq!(topcat, "category");
  148. }
  149. }