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