nu_command/generators/
cal.rs

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) // TODO: supply exhaustive examples
54            .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        // Chrono does not provide a method to output the amount of days in a month
211        // This is a workaround taken from the example code from the Chrono docs here:
212        // https://docs.rs/chrono/0.3.0/chrono/naive/date/struct.NaiveDate.html#example-30
213        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                        // This colors the current day
367                        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}