1use clap::Args;
2use doing_config::SortOrder;
3use doing_ops::filter::filter_entries;
4use doing_time::{chronify, parse_range};
5
6use crate::{
7 Result,
8 cli::{
9 AppContext,
10 args::{DisplayArgs, FilterArgs},
11 pager,
12 },
13};
14
15#[derive(Args, Clone, Debug)]
32pub struct Command {
33 #[arg(index = 1, required = true, value_name = "DATE")]
35 date: String,
36
37 #[command(flatten)]
38 display: DisplayArgs,
39
40 #[command(flatten)]
41 filter: FilterArgs,
42
43 #[arg(short, long)]
45 pager: bool,
46}
47
48impl Command {
49 pub fn call(&self, ctx: &mut AppContext) -> Result<()> {
50 let section_name = self.filter.section.as_deref().unwrap_or("all");
51
52 let all_entries: Vec<_> = ctx
53 .document
54 .entries_in_section(section_name)
55 .into_iter()
56 .cloned()
57 .collect();
58
59 let mut options = self
60 .filter
61 .clone()
62 .into_filter_options(&ctx.config, ctx.include_notes)?;
63 options.section = Some(section_name.to_string());
64
65 match parse_range(&self.date) {
66 Ok((start, end)) => {
67 if options.after.is_none() {
68 options.after = Some(start);
69 }
70 if options.before.is_none() {
71 options.before = Some(end);
72 }
73 }
74 Err(_) => {
75 let date = chronify(&self.date)?;
76 let day_start = date
77 .date_naive()
78 .and_hms_opt(0, 0, 0)
79 .and_then(|dt| dt.and_local_timezone(chrono::Local).single());
80 let day_end = date
81 .date_naive()
82 .and_hms_opt(23, 59, 59)
83 .and_then(|dt| dt.and_local_timezone(chrono::Local).single());
84
85 if options.after.is_none() {
86 options.after = day_start;
87 }
88 if options.before.is_none() {
89 options.before = day_end;
90 }
91 }
92 }
93
94 let sort_order = self.display.sort.map(SortOrder::from).or(Some(ctx.config.order));
95 options.sort = sort_order;
96
97 let filtered = filter_entries(all_entries, &options);
98
99 let output = self
100 .display
101 .render_entries(&filtered, &ctx.config, "default", ctx.include_notes)?;
102
103 if !output.is_empty() {
104 pager::output(&output, &ctx.config, self.pager || ctx.use_pager)?;
105 }
106
107 Ok(())
108 }
109}
110
111#[cfg(test)]
112mod test {
113 use chrono::{Duration, Local, TimeZone};
114 use doing_taskpaper::{Document, Entry, Note, Section, Tag, Tags};
115
116 use super::*;
117
118 fn default_cmd(date: &str) -> Command {
119 Command {
120 date: date.into(),
121 display: DisplayArgs::default(),
122 filter: FilterArgs::default(),
123 pager: false,
124 }
125 }
126
127 fn sample_ctx() -> AppContext {
128 let yesterday = Local::now() - Duration::days(1);
129 let mut doc = Document::new();
130 let mut section = Section::new("Currently");
131 section.add_entry(Entry::new(
132 yesterday,
133 "Yesterday's work",
134 Tags::from_iter(vec![Tag::new("coding", None::<String>)]),
135 Note::new(),
136 "Currently",
137 None::<String>,
138 ));
139 section.add_entry(Entry::new(
140 Local.with_ymd_and_hms(2024, 3, 15, 10, 0, 0).unwrap(),
141 "Old entry",
142 Tags::new(),
143 Note::new(),
144 "Currently",
145 None::<String>,
146 ));
147 doc.add_section(section);
148
149 AppContext {
150 config: doing_config::Config::default(),
151 default_answer: false,
152 document: doc,
153 doing_file: std::path::PathBuf::from("/tmp/test_doing.md"),
154 include_notes: true,
155 no: false,
156 noauto: false,
157 quiet: false,
158 stdout: false,
159 use_color: false,
160 use_pager: false,
161 yes: false,
162 }
163 }
164
165 mod call {
166 use super::*;
167
168 #[test]
169 fn it_returns_ok_with_single_date() {
170 let mut ctx = sample_ctx();
171 let cmd = default_cmd("yesterday");
172
173 let result = cmd.call(&mut ctx);
174
175 assert!(result.is_ok());
176 }
177
178 #[test]
179 fn it_returns_ok_with_date_range() {
180 let mut ctx = sample_ctx();
181 let cmd = default_cmd("2024-03-01 to 2024-03-31");
182
183 let result = cmd.call(&mut ctx);
184
185 assert!(result.is_ok());
186 }
187
188 #[test]
189 fn it_filters_by_section() {
190 let mut ctx = sample_ctx();
191 let cmd = Command {
192 filter: FilterArgs {
193 section: Some("Currently".into()),
194 ..FilterArgs::default()
195 },
196 ..default_cmd("yesterday")
197 };
198
199 let result = cmd.call(&mut ctx);
200
201 assert!(result.is_ok());
202 }
203
204 #[test]
205 fn it_handles_empty_document() {
206 let mut ctx = AppContext {
207 config: doing_config::Config::default(),
208 default_answer: false,
209 document: Document::new(),
210 doing_file: std::path::PathBuf::from("/tmp/test_doing.md"),
211 include_notes: true,
212 no: false,
213 noauto: false,
214 quiet: false,
215 stdout: false,
216 use_color: false,
217 use_pager: false,
218 yes: false,
219 };
220 let cmd = default_cmd("yesterday");
221
222 let result = cmd.call(&mut ctx);
223
224 assert!(result.is_ok());
225 }
226
227 #[test]
228 fn it_rejects_invalid_date() {
229 let mut ctx = sample_ctx();
230 let cmd = default_cmd("not a real date");
231
232 let result = cmd.call(&mut ctx);
233
234 assert!(result.is_err());
235 }
236 }
237}