tada/
item.rs

1//! Types related to individual tasks.
2//!
3//! # Examples
4//!
5//! ```
6//! use chrono::NaiveDate;
7//! use tada::item::{Importance, Item, TshirtSize, Urgency};
8//!
9//! let mut i = Item::parse("(A) clean my @home @L");
10//!
11//! assert_eq!(Some(Importance::A), i.importance());
12//! assert_eq!("clean my @home @L", i.description());
13//! assert!(i.has_context("home"));
14//! assert!(i.has_context("l"));
15//! assert_eq!(Some(TshirtSize::Large), i.tshirt_size());
16//!
17//! i.set_completion(true);
18//! i.set_completion_date(NaiveDate::from_ymd_opt(2022, 12, 1).unwrap());
19//! println!("{}", i);
20//! ```
21
22use 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	/// Regular expression to capture the parts of a tada list line.
32	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	/// Regular expression to find key-value tags within a description.
44	static ref RE_KV: Regex = Regex::new(r##"(?x)
45		([^\s:]+)                       # capture: key
46		:                               # colon
47		([^\s:]+)                       # capture: value
48	"##)
49	.unwrap();
50
51	/// Regular expression to find tags within a description.
52	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	/// Regular expression to find contexts within a description.
60	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	/// Regular expression to match contexts indicating a small tshirt size.
68	static ref RE_SMALL: Regex  = Regex::new("(?i)^X*S$").unwrap();
69
70	/// Regular expression to match contexts indicating a medium tshirt size.
71	static ref RE_MEDIUM: Regex = Regex::new("(?i)^X*M$").unwrap();
72
73	/// Regular expression to match contexts indicating a large tshirt size.
74	static ref RE_LARGE: Regex  = Regex::new("(?i)^X*L$").unwrap();
75
76	/// Constant for today's date.
77	///
78	/// These date constants are evaluated once to ensure predictable behaviour
79	/// when the application is run at midnight.
80	///
81	/// This may cause issues later on if we want to run a persistent tadalist
82	/// process.
83	static ref DATE_TODAY: NaiveDate = Utc::now().date_naive();
84
85	/// Constant representing "soon".
86	///
87	/// Tomorrow or overmorrow.
88	static ref DATE_SOON: NaiveDate = Utc::now().date_naive() + Duration::days(2);
89
90	/// Constant representing the end of this week.
91	///
92	/// Weeks end on Sunday.
93	static ref DATE_WEEKEND: NaiveDate = Utc::now().date_naive().week(Weekday::Mon).last_day();
94
95	/// Constant representing the end of next week.
96	static ref DATE_NEXT_WEEKEND: NaiveDate = Utc::now().date_naive().week(Weekday::Mon).last_day() + Duration::days(7);
97
98	/// Constant representing the end of next month.
99	///
100	/// Who cares when *this* month ends?!
101	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/// Five levels of importance are defined.
115#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
116pub enum Importance {
117	/// Critical
118	A,
119	/// Important
120	B,
121	/// Semi-important
122	C,
123	/// Normal
124	D,
125	/// Unimportant
126	E,
127}
128
129impl Importance {
130	/// Get an importance from a letter.
131	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	/// Returns a letter representing this importance.
143	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	/// Returns a heading suitable for items of this importance.
154	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	/// Returns a list of known importances, in a sane order.
165	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	/// Default is D.
172	fn default() -> Self {
173		Self::D
174	}
175}
176
177/// Seven levels of urgency are defined.
178#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
179pub enum Urgency {
180	/// A due date earlier than today.
181	Overdue,
182	/// A due date today.
183	Today,
184	/// A due date tomorrow or overmorrow.
185	Soon,
186	/// A due date by the end of this week. Note that if it is Friday or later, any
187	/// tasks due this week will fall into the `Today` or `Soon` urgencies instead.
188	ThisWeek,
189	/// A due date by the end of next week.
190	NextWeek,
191	/// A due date by the end of next month.
192	///
193	/// There is no `ThisMonth` urgency because for almost half the month any tasks
194	/// would fall into the `ThisWeek` or `NextWeek` urgencies instead, making
195	/// `ThisMonth` fairly useless.
196	NextMonth,
197	/// Any due date after the end of next month.
198	Later,
199}
200
201impl Urgency {
202	/// Calculate urgency from a due date.
203	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	/// Returns a heading suitable for items of this urgency.
222	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	/// Returns a list of known urgencies, in a sane order.
235	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	/// Default is soon, but you should rely on the default as little as possible.
250	/// It is useful when sorting tasks by urgency.
251	fn default() -> Self {
252		Self::Soon
253	}
254}
255
256/// Three sizes are defined.
257#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
258pub enum TshirtSize {
259	Small,
260	Medium,
261	Large,
262}
263
264impl TshirtSize {
265	/// Returns a heading suitable for items of this size.
266	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	/// Returns a list of known sizes, in a sane order.
275	pub fn all() -> Vec<Self> {
276		Vec::from([Self::Small, Self::Medium, Self::Large])
277	}
278}
279
280impl Default for TshirtSize {
281	/// Default is medium.
282	fn default() -> Self {
283		Self::Medium
284	}
285}
286
287/// An item in a todo list.
288///
289/// # Examples
290///
291/// ```
292/// use tada::{Importance, Item};
293/// let i = Item::parse("(A) clean my @home");
294/// assert_eq!(Some(Importance::A), i.importance());
295/// assert_eq!("clean my @home", i.description());
296/// assert!(i.has_context("home"));
297/// ```
298pub 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	/// Parse an item from a line of text.
336	///
337	/// Assumes the [todo.txt](https://github.com/todotxt/todo.txt) format.
338	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	/// Create a version of this item but representing a completed task.
372	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	/// Provide zen-like calm by rescheduling an overdue task.
385	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	/// Pull a task forward to being done with a new urgency, also clearing any start date.
407	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	/// Performs a bunch of small fixes on the item syntax.
425	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	/// Whether the task is complete.
488	pub fn completion(&self) -> bool {
489		self.completion
490	}
491
492	/// Set indicator of whether the task is complete.
493	pub fn set_completion(&mut self, x: bool) {
494		self.completion = x;
495	}
496
497	/// Line number indicator (sometimes zero).
498	pub fn line_number(&self) -> usize {
499		self.line_number
500	}
501
502	/// Set line number indicator for the task.
503	pub fn set_line_number(&mut self, x: usize) {
504		self.line_number = x;
505	}
506
507	/// Task priority/importance as given in a todo.txt file.
508	///
509	/// A is highest, then B and C. D should be considered normal. E is low priority.
510	/// Any uppercase letter is valid, but letters after E are not especially meaningful.
511	///
512	/// The importance() method is better.
513	pub fn priority(&self) -> char {
514		self.priority
515	}
516
517	/// Set task priority.
518	pub fn set_priority(&mut self, x: char) {
519		self.priority = x;
520	}
521
522	/// Completion date.
523	///
524	/// Often none.
525	pub fn completion_date(&self) -> Option<NaiveDate> {
526		self.completion_date
527	}
528
529	/// Set the completion date to a given date.
530	pub fn set_completion_date(&mut self, x: NaiveDate) {
531		self.completion_date = Some(x);
532	}
533
534	/// Set the completion date to None.
535	pub fn clear_completion_date(&mut self) {
536		self.completion_date = None;
537	}
538
539	/// Task creation date.
540	///
541	/// Often none.
542	pub fn creation_date(&self) -> Option<NaiveDate> {
543		self.creation_date
544	}
545
546	/// Set the task creation date to a given date.
547	pub fn set_creation_date(&mut self, x: NaiveDate) {
548		self.creation_date = Some(x);
549	}
550
551	/// Set the task creation date to None.
552	pub fn clear_creation_date(&mut self) {
553		self.creation_date = None;
554	}
555
556	/// Task description.
557	pub fn description(&self) -> String {
558		self.description.clone()
559	}
560
561	/// Set the task description.
562	///
563	/// Internally clears cached tags, etc.
564	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	/// Return the importance of this task.
576	///
577	/// Basically the same as priority, except it's an enum and all letters after E
578	/// are treated as being the same as E.
579	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	/// Set the item's importance.
592	pub fn set_importance(&mut self, i: Importance) {
593		self.priority = i.to_char();
594		self._importance = FreezeBox::default();
595	}
596
597	/// Set the item's importance.
598	pub fn clear_importance(&mut self) {
599		self.priority = '\0';
600		self._importance = FreezeBox::default();
601	}
602
603	/// Return the date when this task is due by.
604	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	/// Return the date when this task may be started.
619	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	/// A task is startable if it doesn't have a start date which is in the future.
635	pub fn is_startable(&self) -> bool {
636		match self.start_date() {
637			Some(day) => day <= *DATE_TODAY,
638			None => true,
639		}
640	}
641
642	/// Classify how urgent this task is.
643	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	/// Set task urgency.
655	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), // about 6 months
664		};
665		// Work and school tasks should be rescheduled from Saturday/Sunday.
666		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	/// Return the size of this task.
693	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	/// Tags.
723	#[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		// Need to return a copy
729		(*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	/// Boolean indicating whether a task has a particular tag.
741	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	/// Contexts.
753	pub fn contexts(&self) -> Vec<String> {
754		if !self._contexts.is_initialized() {
755			self._contexts.lazy_init(self._build_contexts());
756		}
757		// Need to return a copy
758		(*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	/// Boolean indicating whether a task has a particular context.
770	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	/// Key-Value Tags.
782	pub fn kv(&self) -> HashMap<String, String> {
783		if !self._kv.is_initialized() {
784			self._kv.lazy_init(self._build_kv());
785		}
786		// Need to return a copy
787		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	/// Key used for smart sorting
803	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	/// Debugging output; used for format!("{:?}")
834	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	/// File-ready output; used for format!("{}")
847	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		// Parse a complex line
916		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		// Parse a misleading line
937		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}