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