1use chrono::{Datelike, Duration, NaiveDate, Utc, Weekday};
23use date_time_parser::DateParser as NaturalDateParser;
24use freezebox::FreezeBox;
25use lazy_static::lazy_static;
26use regex::Regex;
27use std::collections::HashMap;
28use std::fmt;
29
30lazy_static! {
31 static ref RE_TADA_ITEM: Regex = Regex::new(r##"(?x)
33 ^ # start
34 ( x \s+ )? # capture: optional "x"
35 ( [(] [A-Z] [)] \s+ )? # capture: optional priority letter
36 ( \d{4} - \d{2} - \d{2} \s+ )? # capture: optional date
37 ( \d{4} - \d{2} - \d{2} \s+ )? # capture: optional date
38 ( .+ ) # capture: description
39 $ # end
40 "##)
41 .unwrap();
42
43 static ref RE_KV: Regex = Regex::new(r##"(?x)
45 ([^\s:]+) # capture: key
46 : # colon
47 ([^\s:]+) # capture: value
48 "##)
49 .unwrap();
50
51 static ref RE_TAG: Regex = Regex::new(r##"(?x)
53 (?:^|\s) # whitespace or start of string
54 [+] # plus sign
55 (\S+) # capture: tag
56 "##)
57 .unwrap();
58
59 static ref RE_CONTEXT: Regex = Regex::new(r##"(?x)
61 (?:^|\s) # whitespace or start of string
62 [@] # at sign
63 (\S+) # capture: context
64 "##)
65 .unwrap();
66
67 static ref RE_SMALL: Regex = Regex::new("(?i)^X*S$").unwrap();
69
70 static ref RE_MEDIUM: Regex = Regex::new("(?i)^X*M$").unwrap();
72
73 static ref RE_LARGE: Regex = Regex::new("(?i)^X*L$").unwrap();
75
76 static ref DATE_TODAY: NaiveDate = Utc::now().date_naive();
84
85 static ref DATE_SOON: NaiveDate = Utc::now().date_naive() + Duration::days(2);
89
90 static ref DATE_WEEKEND: NaiveDate = Utc::now().date_naive().week(Weekday::Mon).last_day();
94
95 static ref DATE_NEXT_WEEKEND: NaiveDate = Utc::now().date_naive().week(Weekday::Mon).last_day() + Duration::days(7);
97
98 static ref DATE_NEXT_MONTH: NaiveDate = {
102 let date = Utc::now().date_naive();
103 match date.month() {
104 11 => NaiveDate::from_ymd_opt(date.year() + 1, 1, 1),
105 12 => NaiveDate::from_ymd_opt(date.year() + 1, 2, 1),
106 _ => NaiveDate::from_ymd_opt(date.year(), date.month() + 2, 1),
107 }
108 .unwrap()
109 .pred_opt()
110 .unwrap()
111 };
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
116pub enum Importance {
117 A,
119 B,
121 C,
123 D,
125 E,
127}
128
129impl Importance {
130 pub fn from_char(c: char) -> Option<Self> {
132 match c {
133 'A' => Some(Self::A),
134 'B' => Some(Self::B),
135 'C' => Some(Self::C),
136 'D' => Some(Self::D),
137 'E'..='Z' => Some(Self::E),
138 _ => None,
139 }
140 }
141
142 pub fn to_char(&self) -> char {
144 match self {
145 Self::A => 'A',
146 Self::B => 'B',
147 Self::C => 'C',
148 Self::D => 'D',
149 Self::E => 'E',
150 }
151 }
152
153 pub fn to_string(&self) -> &str {
155 match self {
156 Self::A => "Critical",
157 Self::B => "Important",
158 Self::C => "Semi-important",
159 Self::D => "Normal",
160 Self::E => "Unimportant",
161 }
162 }
163
164 pub fn all() -> Vec<Self> {
166 Vec::from([Self::A, Self::B, Self::C, Self::D, Self::E])
167 }
168}
169
170impl Default for Importance {
171 fn default() -> Self {
173 Self::D
174 }
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
179pub enum Urgency {
180 Overdue,
182 Today,
184 Soon,
186 ThisWeek,
189 NextWeek,
191 NextMonth,
197 Later,
199}
200
201impl Urgency {
202 pub fn from_due_date(due: NaiveDate) -> Self {
204 if due < *DATE_TODAY {
205 Self::Overdue
206 } else if due == *DATE_TODAY {
207 Self::Today
208 } else if due <= *DATE_SOON {
209 Self::Soon
210 } else if due <= *DATE_WEEKEND {
211 Self::ThisWeek
212 } else if due <= *DATE_NEXT_WEEKEND {
213 Self::NextWeek
214 } else if due <= *DATE_NEXT_MONTH {
215 Self::NextMonth
216 } else {
217 Self::Later
218 }
219 }
220
221 pub fn to_string(&self) -> &str {
223 match self {
224 Self::Overdue => "Overdue",
225 Self::Today => "Today",
226 Self::Soon => "Soon",
227 Self::ThisWeek => "This week",
228 Self::NextWeek => "Next week",
229 Self::NextMonth => "Next month",
230 Self::Later => "Later",
231 }
232 }
233
234 pub fn all() -> Vec<Self> {
236 Vec::from([
237 Self::Overdue,
238 Self::Today,
239 Self::Soon,
240 Self::ThisWeek,
241 Self::NextWeek,
242 Self::NextMonth,
243 Self::Later,
244 ])
245 }
246}
247
248impl Default for Urgency {
249 fn default() -> Self {
252 Self::Soon
253 }
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
258pub enum TshirtSize {
259 Small,
260 Medium,
261 Large,
262}
263
264impl TshirtSize {
265 pub fn to_string(&self) -> &str {
267 match self {
268 Self::Small => "Small",
269 Self::Medium => "Medium",
270 Self::Large => "Large",
271 }
272 }
273
274 pub fn all() -> Vec<Self> {
276 Vec::from([Self::Small, Self::Medium, Self::Large])
277 }
278}
279
280impl Default for TshirtSize {
281 fn default() -> Self {
283 Self::Medium
284 }
285}
286
287pub struct Item {
299 line_number: usize,
300 completion: bool,
301 priority: char,
302 completion_date: Option<NaiveDate>,
303 creation_date: Option<NaiveDate>,
304 description: String,
305 _importance: FreezeBox<Option<Importance>>,
306 _due_date: FreezeBox<Option<NaiveDate>>,
307 _start_date: FreezeBox<Option<NaiveDate>>,
308 _urgency: FreezeBox<Option<Urgency>>,
309 _tshirt_size: FreezeBox<Option<TshirtSize>>,
310 _tags: FreezeBox<Vec<String>>,
311 _contexts: FreezeBox<Vec<String>>,
312 _kv: FreezeBox<HashMap<String, String>>,
313}
314
315impl Item {
316 pub fn new() -> Item {
317 Item {
318 line_number: 0,
319 completion: false,
320 priority: '\0',
321 completion_date: None,
322 creation_date: None,
323 description: String::new(),
324 _importance: FreezeBox::default(),
325 _due_date: FreezeBox::default(),
326 _start_date: FreezeBox::default(),
327 _urgency: FreezeBox::default(),
328 _tshirt_size: FreezeBox::default(),
329 _tags: FreezeBox::default(),
330 _contexts: FreezeBox::default(),
331 _kv: FreezeBox::default(),
332 }
333 }
334
335 pub fn parse(text: &str) -> Item {
339 let caps = RE_TADA_ITEM.captures(text).unwrap();
340 let blank = Self::new();
341
342 Item {
343 completion: caps.get(1).is_some(),
344 priority: match caps.get(2) {
345 Some(p) => p.as_str().chars().nth(1).unwrap(),
346 None => '\0',
347 },
348 completion_date: if caps.get(3).is_some() && caps.get(4).is_some() {
349 let cap3 = caps.get(3).unwrap().as_str();
350 NaiveDate::parse_from_str(cap3.trim(), "%Y-%m-%d").ok()
351 } else {
352 None
353 },
354 creation_date: if caps.get(3).is_some() && caps.get(4).is_some() {
355 let cap4 = caps.get(4).unwrap().as_str();
356 NaiveDate::parse_from_str(cap4.trim(), "%Y-%m-%d").ok()
357 } else if caps.get(3).is_some() {
358 let cap3 = caps.get(3).unwrap().as_str();
359 NaiveDate::parse_from_str(cap3.trim(), "%Y-%m-%d").ok()
360 } else {
361 None
362 },
363 description: match caps.get(5) {
364 Some(m) => String::from(m.as_str().trim()),
365 None => String::from(""),
366 },
367 ..blank
368 }
369 }
370
371 pub fn but_done(&self, include_date: bool) -> Item {
373 let mut i = self.clone();
374 i.set_completion(true);
375 if include_date {
376 i.set_completion_date(*DATE_TODAY);
377 if i.creation_date().is_none() {
378 i.set_creation_date(*DATE_TODAY);
379 }
380 }
381 i
382 }
383
384 pub fn zen(&self) -> Item {
386 if self.urgency() == Some(Urgency::Overdue) {
387 let mut new = self.clone();
388 let important = matches!(
389 new.importance(),
390 Some(Importance::A) | Some(Importance::B)
391 );
392 let small = matches!(new.tshirt_size(), Some(TshirtSize::Small));
393 let new_urgency = if important && small {
394 Urgency::Soon
395 } else if important || small {
396 Urgency::NextWeek
397 } else {
398 Urgency::NextMonth
399 };
400 new.set_urgency(new_urgency);
401 return new;
402 }
403 self.clone()
404 }
405
406 pub fn but_pull(&self, new_urgency: Urgency) -> Item {
408 let mut new = self.clone();
409 if new.completion() {
410 return new;
411 }
412 new.set_urgency(new_urgency);
413
414 let re = Regex::new(r"start:(?:[^\s:]+)").unwrap();
415 let new_start = format!("start:{}", DATE_TODAY.format("%Y-%m-%d"));
416 new.set_description(format!(
417 "{}",
418 re.replace(&new.description, new_start)
419 ));
420
421 new
422 }
423
424 pub fn fixup(&self, warnings: bool) -> Item {
426 let maybe_warn = |w| {
427 if warnings {
428 eprintln!("{w}");
429 }
430 };
431 let mut new = self.clone();
432
433 if new.priority() == '\0' {
434 maybe_warn(String::from("Hint: a task can be given an importance be prefixing it with a parenthesized capital letter, like `(A)`."));
435 }
436
437 for slot in ["due", "start"] {
438 match new.kv().get(slot) {
439 Some(given_date) => {
440 if NaiveDate::parse_from_str(given_date, "%Y-%m-%d")
441 .is_err()
442 {
443 let processed_date = given_date.replace('_', " ");
444 if let Some(naive_date) =
445 NaturalDateParser::parse(&processed_date)
446 {
447 new.set_description(new.description().replace(
448 &format!("{slot}:{given_date}"),
449 &format!(
450 "{}:{}",
451 slot,
452 naive_date.format("%Y-%m-%d")
453 ),
454 ));
455 maybe_warn(format!(
456 "Notice: {} date `{}` changed to `{}`.",
457 slot,
458 given_date,
459 naive_date.format("%Y-%m-%d")
460 ));
461 } else {
462 maybe_warn(format!("Notice: {slot} date `{given_date}` should be in YYYY-MM-DD format."));
463 }
464 }
465 }
466 None => {
467 if slot == "due" {
468 maybe_warn(format!("Hint: a task can be given a {slot} date by including `{slot}:YYYY-MM-DD`."));
469 }
470 }
471 }
472 }
473
474 if new.tshirt_size().is_none() {
475 maybe_warn(String::from("Hint: a task can be given a size by including `@S`, `@M`, or `@L`."));
476 }
477
478 if new.description().len() > 120 {
479 maybe_warn(String::from("Hint: long descriptions can make a task list slower to skim read."));
480 } else if new.description().len() < 30 {
481 maybe_warn(String::from("Hint: short descriptions can make it hard to remember what a task means!"));
482 }
483
484 new
485 }
486
487 pub fn completion(&self) -> bool {
489 self.completion
490 }
491
492 pub fn set_completion(&mut self, x: bool) {
494 self.completion = x;
495 }
496
497 pub fn line_number(&self) -> usize {
499 self.line_number
500 }
501
502 pub fn set_line_number(&mut self, x: usize) {
504 self.line_number = x;
505 }
506
507 pub fn priority(&self) -> char {
514 self.priority
515 }
516
517 pub fn set_priority(&mut self, x: char) {
519 self.priority = x;
520 }
521
522 pub fn completion_date(&self) -> Option<NaiveDate> {
526 self.completion_date
527 }
528
529 pub fn set_completion_date(&mut self, x: NaiveDate) {
531 self.completion_date = Some(x);
532 }
533
534 pub fn clear_completion_date(&mut self) {
536 self.completion_date = None;
537 }
538
539 pub fn creation_date(&self) -> Option<NaiveDate> {
543 self.creation_date
544 }
545
546 pub fn set_creation_date(&mut self, x: NaiveDate) {
548 self.creation_date = Some(x);
549 }
550
551 pub fn clear_creation_date(&mut self) {
553 self.creation_date = None;
554 }
555
556 pub fn description(&self) -> String {
558 self.description.clone()
559 }
560
561 pub fn set_description(&mut self, x: String) {
565 self._importance = FreezeBox::default();
566 self._due_date = FreezeBox::default();
567 self._urgency = FreezeBox::default();
568 self._tshirt_size = FreezeBox::default();
569 self._tags = FreezeBox::default();
570 self._contexts = FreezeBox::default();
571 self._kv = FreezeBox::default();
572 self.description = x;
573 }
574
575 pub fn importance(&self) -> Option<Importance> {
580 if !self._importance.is_initialized() {
581 self._importance
582 .lazy_init(self._build_importance());
583 }
584 *self._importance
585 }
586
587 fn _build_importance(&self) -> Option<Importance> {
588 Importance::from_char(self.priority)
589 }
590
591 pub fn set_importance(&mut self, i: Importance) {
593 self.priority = i.to_char();
594 self._importance = FreezeBox::default();
595 }
596
597 pub fn clear_importance(&mut self) {
599 self.priority = '\0';
600 self._importance = FreezeBox::default();
601 }
602
603 pub fn due_date(&self) -> Option<NaiveDate> {
605 if !self._due_date.is_initialized() {
606 self._due_date.lazy_init(self._build_due_date());
607 }
608 *self._due_date
609 }
610
611 fn _build_due_date(&self) -> Option<NaiveDate> {
612 match self.kv().get("due") {
613 Some(dd) => NaiveDate::parse_from_str(dd, "%Y-%m-%d").ok(),
614 None => None,
615 }
616 }
617
618 pub fn start_date(&self) -> Option<NaiveDate> {
620 if !self._start_date.is_initialized() {
621 self._start_date
622 .lazy_init(self._build_start_date());
623 }
624 *self._start_date
625 }
626
627 fn _build_start_date(&self) -> Option<NaiveDate> {
628 match self.kv().get("start") {
629 Some(dd) => NaiveDate::parse_from_str(dd, "%Y-%m-%d").ok(),
630 None => None,
631 }
632 }
633
634 pub fn is_startable(&self) -> bool {
636 match self.start_date() {
637 Some(day) => day <= *DATE_TODAY,
638 None => true,
639 }
640 }
641
642 pub fn urgency(&self) -> Option<Urgency> {
644 if !self._urgency.is_initialized() {
645 self._urgency.lazy_init(self._build_urgency());
646 }
647 *self._urgency
648 }
649
650 fn _build_urgency(&self) -> Option<Urgency> {
651 self.due_date().map(Urgency::from_due_date)
652 }
653
654 pub fn set_urgency(&mut self, urg: Urgency) {
656 let mut d = match urg {
657 Urgency::Overdue => DATE_TODAY.pred_opt().unwrap(),
658 Urgency::Today => *DATE_TODAY,
659 Urgency::Soon => *DATE_SOON,
660 Urgency::ThisWeek => *DATE_WEEKEND,
661 Urgency::NextWeek => *DATE_NEXT_WEEKEND,
662 Urgency::NextMonth => *DATE_NEXT_MONTH,
663 Urgency::Later => *DATE_TODAY + Duration::days(183), };
665 if urg > Urgency::Today
667 && (self.has_context("work") || self.has_context("school"))
668 {
669 d = match format!("{}", d.format("%u")).as_str() {
670 "6" => d.pred_opt().unwrap(),
671 "7" => d.pred_opt().unwrap().pred_opt().unwrap(),
672 _ => d,
673 };
674 }
675
676 let formatted = d.format("%Y-%m-%d");
677
678 match self.kv().get("due") {
679 Some(str) => {
680 self.set_description(self.description().replace(
681 &format!("due:{str}"),
682 &format!("due:{formatted}"),
683 ))
684 }
685 None => self.set_description(format!(
686 "{} due:{formatted}",
687 self.description()
688 )),
689 }
690 }
691
692 pub fn tshirt_size(&self) -> Option<TshirtSize> {
694 if !self._tshirt_size.is_initialized() {
695 self._tshirt_size
696 .lazy_init(self._build_tshirt_size());
697 }
698 *self._tshirt_size
699 }
700
701 fn _build_tshirt_size(&self) -> Option<TshirtSize> {
702 let ctx = self.contexts();
703
704 let mut tmp = ctx.iter().filter(|x| RE_SMALL.is_match(x));
705 if tmp.next().is_some() {
706 return Some(TshirtSize::Small);
707 }
708
709 let mut tmp = ctx.iter().filter(|x| RE_MEDIUM.is_match(x));
710 if tmp.next().is_some() {
711 return Some(TshirtSize::Medium);
712 }
713
714 let mut tmp = ctx.iter().filter(|x| RE_LARGE.is_match(x));
715 if tmp.next().is_some() {
716 return Some(TshirtSize::Large);
717 }
718
719 None
720 }
721
722 #[allow(dead_code)]
724 pub fn tags(&self) -> Vec<String> {
725 if !self._tags.is_initialized() {
726 self._tags.lazy_init(self._build_tags());
727 }
728 (*self._tags).to_vec()
730 }
731
732 fn _build_tags(&self) -> Vec<String> {
733 let mut tags: Vec<String> = Vec::new();
734 for cap in RE_TAG.captures_iter(&self.description) {
735 tags.push(cap[1].to_string());
736 }
737 tags
738 }
739
740 pub fn has_tag(&self, tag: &str) -> bool {
742 let real_tag = match tag.chars().next() {
743 Some('+') => tag.get(1..).unwrap(),
744 _ => tag,
745 };
746 let real_tag = real_tag.to_lowercase();
747 self.tags()
748 .iter()
749 .any(|t| t.to_lowercase().as_str() == real_tag)
750 }
751
752 pub fn contexts(&self) -> Vec<String> {
754 if !self._contexts.is_initialized() {
755 self._contexts.lazy_init(self._build_contexts());
756 }
757 (*self._contexts).to_vec()
759 }
760
761 fn _build_contexts(&self) -> Vec<String> {
762 let mut tags: Vec<String> = Vec::new();
763 for cap in RE_CONTEXT.captures_iter(&self.description) {
764 tags.push(cap[1].to_string());
765 }
766 tags
767 }
768
769 pub fn has_context(&self, ctx: &str) -> bool {
771 let real_ctx = match ctx.chars().next() {
772 Some('@') => ctx.get(1..).unwrap(),
773 _ => ctx,
774 };
775 let real_ctx = real_ctx.to_lowercase();
776 self.contexts()
777 .iter()
778 .any(|c| c.to_lowercase().as_str() == real_ctx)
779 }
780
781 pub fn kv(&self) -> HashMap<String, String> {
783 if !self._kv.is_initialized() {
784 self._kv.lazy_init(self._build_kv());
785 }
786 let mut kv_clone: HashMap<String, String> = HashMap::new();
788 for (k, v) in &*self._kv {
789 kv_clone.insert(k.clone(), v.clone());
790 }
791 kv_clone
792 }
793
794 fn _build_kv(&self) -> HashMap<String, String> {
795 let mut kv: HashMap<String, String> = HashMap::new();
796 for cap in RE_KV.captures_iter(&self.description) {
797 kv.insert(cap[1].to_string(), cap[2].to_string());
798 }
799 kv
800 }
801
802 pub fn smart_key(&self) -> (Urgency, Importance, TshirtSize) {
804 (
805 self.urgency().unwrap_or_default(),
806 self.importance().unwrap_or_default(),
807 self.tshirt_size().unwrap_or_default(),
808 )
809 }
810}
811
812impl Default for Item {
813 fn default() -> Self {
814 Self::new()
815 }
816}
817
818impl Clone for Item {
819 fn clone(&self) -> Self {
820 Item {
821 line_number: self.line_number,
822 completion: self.completion,
823 priority: self.priority,
824 completion_date: self.completion_date,
825 creation_date: self.creation_date,
826 description: self.description.clone(),
827 ..Item::new()
828 }
829 }
830}
831
832impl fmt::Debug for Item {
833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
835 f.debug_struct("Item")
836 .field("completion", &self.completion)
837 .field("priority", &self.priority)
838 .field("completion_date", &self.completion_date)
839 .field("creation_date", &self.creation_date)
840 .field("description", &self.description)
841 .finish()
842 }
843}
844
845impl fmt::Display for Item {
846 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848 if self.completion {
849 write!(f, "x ")?;
850 }
851
852 if self.priority != '\0' {
853 write!(f, "({}) ", self.priority)?;
854 }
855
856 if self.completion {
857 if let Some(d) = self.completion_date {
858 write!(f, "{} ", d.format("%Y-%m-%d"))?;
859 }
860 }
861
862 if let Some(d) = self.creation_date {
863 write!(f, "{} ", d.format("%Y-%m-%d"))?;
864 }
865
866 write!(f, "{}", self.description)
867 }
868}
869
870#[cfg(test)]
871mod tests_item {
872 use super::*;
873 use chrono::NaiveDate;
874
875 #[test]
876 fn test_debug() {
877 let b = Item::new();
878 let i = Item {
879 completion: false,
880 priority: '\0',
881 completion_date: None,
882 creation_date: None,
883 description: "foo bar baz".to_string(),
884 ..b
885 };
886 let dbug = format!("{i:?}");
887 assert!(dbug.len() > 1);
888 }
889
890 #[test]
891 fn test_display() {
892 let b = Item::new();
893 let i = Item {
894 description: "foo bar baz".to_string(),
895 ..b
896 };
897
898 assert_eq!("foo bar baz", format!("{i}"));
899
900 let b = Item::new();
901 let i = Item {
902 completion: true,
903 priority: 'B',
904 completion_date: Some(NaiveDate::from_ymd_opt(2010, 1, 1).unwrap()),
905 creation_date: Some(NaiveDate::from_ymd_opt(2000, 12, 31).unwrap()),
906 description: "foo bar baz".to_string(),
907 ..b
908 };
909
910 assert_eq!("x (B) 2010-01-01 2000-12-31 foo bar baz", format!("{i}"));
911 }
912
913 #[test]
914 fn test_parse() {
915 let i = Item::parse("x (B) 2010-01-01 2000-12-31 foo bar baz");
917
918 assert_eq!(true, i.completion);
919 assert_eq!('B', i.priority);
920 assert_eq!(
921 NaiveDate::from_ymd_opt(2010, 1, 1).unwrap(),
922 i.completion_date.unwrap()
923 );
924 assert_eq!(
925 NaiveDate::from_ymd_opt(2000, 12, 31).unwrap(),
926 i.creation_date.unwrap()
927 );
928 assert_eq!("foo bar baz".to_string(), i.description);
929 assert!(i.urgency().is_none());
930 assert_eq!(Some(Importance::B), i.importance());
931 assert_eq!(None, i.tshirt_size());
932 assert_eq!(Vec::<String>::new(), i.tags());
933 assert_eq!(Vec::<String>::new(), i.contexts());
934 assert_eq!(HashMap::<String, String>::new(), i.kv());
935
936 let i = Item::parse("2010-01-01 (A) foo bar baz");
938
939 assert!(!i.completion);
940 assert_eq!('\0', i.priority);
941 assert!(i.completion_date.is_none());
942 assert_eq!(
943 NaiveDate::from_ymd_opt(2010, 1, 1).unwrap(),
944 i.creation_date.unwrap()
945 );
946 assert_eq!("(A) foo bar baz".to_string(), i.description);
947 }
948
949 #[test]
950 fn test_kv() {
951 let i = Item::parse("(A) foo bar abc:xyz def:123");
952 let expected_kv = HashMap::from([
953 ("abc".to_string(), "xyz".to_string()),
954 ("def".to_string(), "123".to_string()),
955 ]);
956
957 assert_eq!('A', i.priority);
958 assert_eq!("foo bar abc:xyz def:123".to_string(), i.description);
959 assert_eq!(expected_kv, i.kv());
960 assert_eq!(expected_kv, i.kv());
961 }
962
963 #[test]
964 fn test_due_date() {
965 let i = Item::parse("(A) foo bar due:1980-06-01");
966
967 assert_eq!(
968 NaiveDate::from_ymd_opt(1980, 6, 1).unwrap(),
969 i.due_date().unwrap()
970 );
971 }
972
973 #[test]
974 fn test_urgency() {
975 let i = Item::parse("(A) foo bar due:1970-06-01");
976 assert_eq!(Urgency::Overdue, i.urgency().unwrap());
977
978 let i = Item::parse(&format!(
979 "(A) foo bar due:{}",
980 Utc::now().date_naive().format("%Y-%m-%d")
981 ));
982 assert_eq!(Urgency::Today, i.urgency().unwrap());
983
984 let i = Item::parse(&format!(
985 "(A) foo bar due:{}",
986 (Utc::now().date_naive() + Duration::days(1)).format("%Y-%m-%d")
987 ));
988 assert_eq!(Urgency::Soon, i.urgency().unwrap());
989
990 let i = Item::parse(&format!(
991 "(A) foo bar due:{}",
992 (Utc::now().date_naive() + Duration::days(18)).format("%Y-%m-%d")
993 ));
994 assert_eq!(Urgency::NextMonth, i.urgency().unwrap());
995
996 let i = Item::parse("(A) foo bar due:3970-06-01");
997 assert_eq!(Urgency::Later, i.urgency().unwrap());
998 }
999
1000 #[test]
1001 fn test_tags() {
1002 let i = Item::parse("(A) +Foo +foo bar+baz +bam");
1003 let expected_tags = Vec::from([
1004 "Foo".to_string(),
1005 "foo".to_string(),
1006 "bam".to_string(),
1007 ]);
1008 assert_eq!(expected_tags, i.tags());
1009 assert!(i.has_tag("Foo"));
1010 assert!(i.has_tag("fOO"));
1011 assert!(!i.has_tag("Fool"));
1012 }
1013
1014 #[test]
1015 fn test_contexts() {
1016 let i = Item::parse("(A) @Foo @foo bar@baz @bam");
1017 let expected_ctx = Vec::from([
1018 "Foo".to_string(),
1019 "foo".to_string(),
1020 "bam".to_string(),
1021 ]);
1022 assert_eq!(expected_ctx, i.contexts());
1023 assert!(i.has_context("Foo"));
1024 assert!(i.has_context("fOO"));
1025 assert!(!i.has_context("Fool"));
1026 }
1027
1028 #[test]
1029 fn test_tshirt_size() {
1030 let i = Item::parse("@M Barble");
1031 assert_eq!(TshirtSize::Medium, i.tshirt_size().unwrap());
1032
1033 let i = Item::parse("(A) Fooble @XxL Barble");
1034 assert_eq!(TshirtSize::Large, i.tshirt_size().unwrap());
1035
1036 let i = Item::parse("Barble");
1037 assert!(i.tshirt_size().is_none());
1038 }
1039}