1use chrono::{Datelike, Local, NaiveDate};
2use nu_color_config::StyleComputer;
3use nu_engine::command_prelude::*;
4use nu_protocol::ast::{self, Expr, Expression};
5
6use std::collections::VecDeque;
7
8#[derive(Clone)]
9pub struct Cal;
10
11struct Arguments {
12 year: bool,
13 quarter: bool,
14 month: bool,
15 month_names: bool,
16 full_year: Option<Spanned<i64>>,
17 week_start: Option<Spanned<String>>,
18 as_table: bool,
19}
20
21impl Command for Cal {
22 fn name(&self) -> &str {
23 "cal"
24 }
25
26 fn signature(&self) -> Signature {
27 Signature::build("cal")
28 .switch("year", "Display the year column", Some('y'))
29 .switch("quarter", "Display the quarter column", Some('q'))
30 .switch("month", "Display the month column", Some('m'))
31 .switch("as-table", "output as a table", Some('t'))
32 .named(
33 "full-year",
34 SyntaxShape::Int,
35 "Display a year-long calendar for the specified year",
36 None,
37 )
38 .named(
39 "week-start",
40 SyntaxShape::String,
41 "Display the calendar with the specified day as the first day of the week",
42 None,
43 )
44 .switch(
45 "month-names",
46 "Display the month names instead of integers",
47 None,
48 )
49 .input_output_types(vec![
50 (Type::Nothing, Type::String),
51 (Type::Nothing, Type::table()),
52 ])
53 .allow_variants_without_examples(true) .category(Category::Generators)
55 }
56
57 fn description(&self) -> &str {
58 "Display a calendar."
59 }
60
61 fn run(
62 &self,
63 engine_state: &EngineState,
64 stack: &mut Stack,
65 call: &Call,
66 input: PipelineData,
67 ) -> Result<PipelineData, ShellError> {
68 cal(engine_state, stack, call, input)
69 }
70
71 fn examples(&self) -> Vec<Example> {
72 vec![
73 Example {
74 description: "This month's calendar",
75 example: "cal",
76 result: None,
77 },
78 Example {
79 description: "The calendar for all of 2012",
80 example: "cal --full-year 2012",
81 result: None,
82 },
83 Example {
84 description: "This month's calendar with the week starting on Monday",
85 example: "cal --week-start mo",
86 result: None,
87 },
88 Example {
89 description: "How many 'Friday the Thirteenths' occurred in 2015?",
90 example: "cal --as-table --full-year 2015 | where fr == 13 | length",
91 result: None,
92 },
93 ]
94 }
95}
96
97pub fn cal(
98 engine_state: &EngineState,
99 stack: &mut Stack,
100 call: &Call,
101 _input: PipelineData,
102) -> Result<PipelineData, ShellError> {
103 let mut calendar_vec_deque = VecDeque::new();
104 let tag = call.head;
105
106 let (current_year, current_month, current_day) = get_current_date();
107
108 let arguments = Arguments {
109 year: call.has_flag(engine_state, stack, "year")?,
110 month: call.has_flag(engine_state, stack, "month")?,
111 month_names: call.has_flag(engine_state, stack, "month-names")?,
112 quarter: call.has_flag(engine_state, stack, "quarter")?,
113 full_year: call.get_flag(engine_state, stack, "full-year")?,
114 week_start: call.get_flag(engine_state, stack, "week-start")?,
115 as_table: call.has_flag(engine_state, stack, "as-table")?,
116 };
117
118 let style_computer = &StyleComputer::from_config(engine_state, stack);
119
120 let mut selected_year: i32 = current_year;
121 let mut current_day_option: Option<u32> = Some(current_day);
122
123 let full_year_value = &arguments.full_year;
124 let month_range = if let Some(full_year_value) = full_year_value {
125 selected_year = full_year_value.item as i32;
126
127 if selected_year != current_year {
128 current_day_option = None
129 }
130 (1, 12)
131 } else {
132 (current_month, current_month)
133 };
134
135 add_months_of_year_to_table(
136 &arguments,
137 &mut calendar_vec_deque,
138 tag,
139 selected_year,
140 month_range,
141 current_month,
142 current_day_option,
143 style_computer,
144 )?;
145
146 let mut table_no_index = ast::Call::new(Span::unknown());
147 table_no_index.add_named((
148 Spanned {
149 item: "index".to_string(),
150 span: Span::unknown(),
151 },
152 None,
153 Some(Expression::new_unknown(
154 Expr::Bool(false),
155 Span::unknown(),
156 Type::Bool,
157 )),
158 ));
159
160 let cal_table_output =
161 Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data();
162 if !arguments.as_table {
163 crate::Table.run(
164 engine_state,
165 stack,
166 &(&table_no_index).into(),
167 cal_table_output,
168 )
169 } else {
170 Ok(cal_table_output)
171 }
172}
173
174fn get_invalid_year_shell_error(head: Span) -> ShellError {
175 ShellError::TypeMismatch {
176 err_message: "The year is invalid".to_string(),
177 span: head,
178 }
179}
180
181struct MonthHelper {
182 selected_year: i32,
183 selected_month: u32,
184 day_number_of_week_month_starts_on: u32,
185 number_of_days_in_month: u32,
186 quarter_number: u32,
187 month_name: String,
188}
189
190impl MonthHelper {
191 pub fn new(selected_year: i32, selected_month: u32) -> Result<MonthHelper, ()> {
192 let naive_date = NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?;
193 let number_of_days_in_month =
194 MonthHelper::calculate_number_of_days_in_month(selected_year, selected_month)?;
195
196 Ok(MonthHelper {
197 selected_year,
198 selected_month,
199 day_number_of_week_month_starts_on: naive_date.weekday().num_days_from_sunday(),
200 number_of_days_in_month,
201 quarter_number: ((selected_month - 1) / 3) + 1,
202 month_name: naive_date.format("%B").to_string().to_ascii_lowercase(),
203 })
204 }
205
206 fn calculate_number_of_days_in_month(
207 mut selected_year: i32,
208 mut selected_month: u32,
209 ) -> Result<u32, ()> {
210 if selected_month == 12 {
214 selected_year += 1;
215 selected_month = 1;
216 } else {
217 selected_month += 1;
218 };
219
220 let next_month_naive_date =
221 NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?;
222
223 Ok(next_month_naive_date.pred_opt().unwrap_or_default().day())
224 }
225}
226
227fn get_current_date() -> (i32, u32, u32) {
228 let local_now_date = Local::now().date_naive();
229
230 let current_year: i32 = local_now_date.year();
231 let current_month: u32 = local_now_date.month();
232 let current_day: u32 = local_now_date.day();
233
234 (current_year, current_month, current_day)
235}
236
237#[allow(clippy::too_many_arguments)]
238fn add_months_of_year_to_table(
239 arguments: &Arguments,
240 calendar_vec_deque: &mut VecDeque<Value>,
241 tag: Span,
242 selected_year: i32,
243 (start_month, end_month): (u32, u32),
244 current_month: u32,
245 current_day_option: Option<u32>,
246 style_computer: &StyleComputer,
247) -> Result<(), ShellError> {
248 for month_number in start_month..=end_month {
249 let mut new_current_day_option: Option<u32> = None;
250
251 if let Some(current_day) = current_day_option {
252 if month_number == current_month {
253 new_current_day_option = Some(current_day)
254 }
255 }
256
257 let add_month_to_table_result = add_month_to_table(
258 arguments,
259 calendar_vec_deque,
260 tag,
261 selected_year,
262 month_number,
263 new_current_day_option,
264 style_computer,
265 );
266
267 add_month_to_table_result?
268 }
269
270 Ok(())
271}
272
273fn add_month_to_table(
274 arguments: &Arguments,
275 calendar_vec_deque: &mut VecDeque<Value>,
276 tag: Span,
277 selected_year: i32,
278 current_month: u32,
279 current_day_option: Option<u32>,
280 style_computer: &StyleComputer,
281) -> Result<(), ShellError> {
282 let month_helper_result = MonthHelper::new(selected_year, current_month);
283
284 let full_year_value: &Option<Spanned<i64>> = &arguments.full_year;
285
286 let month_helper = match month_helper_result {
287 Ok(month_helper) => month_helper,
288 Err(()) => match full_year_value {
289 Some(x) => return Err(get_invalid_year_shell_error(x.span)),
290 None => {
291 return Err(ShellError::UnknownOperator {
292 op_token: "Issue parsing command, invalid command".to_string(),
293 span: tag,
294 });
295 }
296 },
297 };
298
299 let mut days_of_the_week = ["su", "mo", "tu", "we", "th", "fr", "sa"];
300 let mut total_start_offset: u32 = month_helper.day_number_of_week_month_starts_on;
301
302 if let Some(week_start_day) = &arguments.week_start {
303 if let Some(position) = days_of_the_week
304 .iter()
305 .position(|day| *day == week_start_day.item)
306 {
307 days_of_the_week.rotate_left(position);
308 total_start_offset += (days_of_the_week.len() - position) as u32;
309 total_start_offset %= days_of_the_week.len() as u32;
310 } else {
311 return Err(ShellError::TypeMismatch {
312 err_message: "The specified week start day is invalid, expected one of ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa']".to_string(),
313 span: week_start_day.span,
314 });
315 }
316 };
317
318 let mut day_number: u32 = 1;
319 let day_limit: u32 = total_start_offset + month_helper.number_of_days_in_month;
320
321 let should_show_year_column = arguments.year;
322 let should_show_quarter_column = arguments.quarter;
323 let should_show_month_column = arguments.month;
324 let should_show_month_names = arguments.month_names;
325
326 while day_number <= day_limit {
327 let mut record = Record::new();
328
329 if should_show_year_column {
330 record.insert(
331 "year".to_string(),
332 Value::int(month_helper.selected_year as i64, tag),
333 );
334 }
335
336 if should_show_quarter_column {
337 record.insert(
338 "quarter".to_string(),
339 Value::int(month_helper.quarter_number as i64, tag),
340 );
341 }
342
343 if should_show_month_column || should_show_month_names {
344 let month_value = if should_show_month_names {
345 Value::string(month_helper.month_name.clone(), tag)
346 } else {
347 Value::int(month_helper.selected_month as i64, tag)
348 };
349
350 record.insert("month".to_string(), month_value);
351 }
352
353 for day in &days_of_the_week {
354 let should_add_day_number_to_table =
355 (day_number > total_start_offset) && (day_number <= day_limit);
356
357 let mut value = Value::nothing(tag);
358
359 if should_add_day_number_to_table {
360 let adjusted_day_number = day_number - total_start_offset;
361
362 value = Value::int(adjusted_day_number as i64, tag);
363
364 if let Some(current_day) = current_day_option {
365 if current_day == adjusted_day_number {
366 let header_style =
368 style_computer.compute("header", &Value::nothing(Span::unknown()));
369
370 value = Value::string(
371 header_style
372 .paint(adjusted_day_number.to_string())
373 .to_string(),
374 tag,
375 );
376 }
377 }
378 }
379
380 record.insert((*day).to_string(), value);
381
382 day_number += 1;
383 }
384
385 calendar_vec_deque.push_back(Value::record(record, tag))
386 }
387
388 Ok(())
389}
390
391#[cfg(test)]
392mod test {
393 use super::*;
394
395 #[test]
396 fn test_examples() {
397 use crate::test_examples;
398
399 test_examples(Cal {})
400 }
401}