From c6aff972dadbcd35aebd3876677f8f8d23b61aa2 Mon Sep 17 00:00:00 2001 From: onthebridgetonowhere <71919805+onthebridgetonowhere@users.noreply.github.com> Date: Sat, 4 Dec 2021 18:15:03 +0100 Subject: [PATCH] Cal command (#429) * Add calendar (cal) command * Move options into arguments to avoid clippy warnings * Remove commented line * Fix formatting issues * Fix clippy warning --- crates/nu-command/src/calendar/cal.rs | 395 +++++++++++++++++++++++ crates/nu-command/src/calendar/mod.rs | 3 + crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/lib.rs | 3 +- 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 crates/nu-command/src/calendar/cal.rs create mode 100644 crates/nu-command/src/calendar/mod.rs diff --git a/crates/nu-command/src/calendar/cal.rs b/crates/nu-command/src/calendar/cal.rs new file mode 100644 index 0000000000..105b1ab1c6 --- /dev/null +++ b/crates/nu-command/src/calendar/cal.rs @@ -0,0 +1,395 @@ +use std::collections::VecDeque; + +use chrono::{Datelike, Local, NaiveDate}; +use indexmap::IndexMap; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Cal; + +struct Arguments { + year: bool, + quarter: bool, + month: bool, + month_names: bool, + full_year: Option>, + week_start: Option>, +} + +impl Command for Cal { + fn name(&self) -> &str { + "cal" + } + + fn signature(&self) -> Signature { + Signature::build("cal") + .switch("year", "Display the year column", Some('y')) + .switch("quarter", "Display the quarter column", Some('q')) + .switch("month", "Display the month column", Some('m')) + .named( + "full-year", + SyntaxShape::Int, + "Display a year-long calendar for the specified year", + None, + ) + .named( + "week-start", + SyntaxShape::String, + "Display the calendar with the specified day as the first day of the week", + None, + ) + .switch( + "month-names", + "Display the month names instead of integers", + None, + ) + } + + fn usage(&self) -> &str { + "Display a calendar." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + cal(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "This month's calendar", + example: "cal", + result: None, + }, + Example { + description: "The calendar for all of 2012", + example: "cal --full-year 2012", + result: None, + }, + Example { + description: "This month's calendar with the week starting on monday", + example: "cal --week-start monday", + result: None, + }, + ] + } +} + +pub fn cal( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let mut calendar_vec_deque = VecDeque::new(); + let tag = call.head; + + let (current_year, current_month, current_day) = get_current_date(); + + let arguments = Arguments { + year: call.has_flag("year"), + month: call.has_flag("month"), + month_names: call.has_flag("month-names"), + quarter: call.has_flag("quarter"), + full_year: call.get_flag(engine_state, stack, "full-year")?, + week_start: call.get_flag(engine_state, stack, "week-start")?, + }; + + let mut selected_year: i32 = current_year; + let mut current_day_option: Option = Some(current_day); + + let full_year_value = &arguments.full_year; + let month_range = if let Some(full_year_value) = full_year_value { + selected_year = full_year_value.item as i32; + + if selected_year != current_year { + current_day_option = None + } + (1, 12) + } else { + (current_month, current_month) + }; + + add_months_of_year_to_table( + &arguments, + &mut calendar_vec_deque, + tag, + selected_year, + month_range, + current_month, + current_day_option, + )?; + + Ok(Value::List { + vals: calendar_vec_deque.into_iter().collect(), + span: tag, + } + .into_pipeline_data()) +} + +fn get_invalid_year_shell_error(head: Span) -> ShellError { + ShellError::UnsupportedInput("The year is invalid".to_string(), head) +} + +struct MonthHelper { + selected_year: i32, + selected_month: u32, + day_number_of_week_month_starts_on: u32, + number_of_days_in_month: u32, + quarter_number: u32, + month_name: String, +} + +impl MonthHelper { + pub fn new(selected_year: i32, selected_month: u32) -> Result { + let naive_date = NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; + let number_of_days_in_month = + MonthHelper::calculate_number_of_days_in_month(selected_year, selected_month)?; + + Ok(MonthHelper { + selected_year, + selected_month, + day_number_of_week_month_starts_on: naive_date.weekday().num_days_from_sunday(), + number_of_days_in_month, + quarter_number: ((selected_month - 1) / 3) + 1, + month_name: naive_date.format("%B").to_string().to_ascii_lowercase(), + }) + } + + fn calculate_number_of_days_in_month( + mut selected_year: i32, + mut selected_month: u32, + ) -> Result { + // Chrono does not provide a method to output the amount of days in a month + // This is a workaround taken from the example code from the Chrono docs here: + // https://docs.rs/chrono/0.3.0/chrono/naive/date/struct.NaiveDate.html#example-30 + if selected_month == 12 { + selected_year += 1; + selected_month = 1; + } else { + selected_month += 1; + }; + + let next_month_naive_date = + NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; + + Ok(next_month_naive_date.pred().day()) + } +} + +fn get_current_date() -> (i32, u32, u32) { + let local_now_date = Local::now().date(); + + let current_year: i32 = local_now_date.year(); + let current_month: u32 = local_now_date.month(); + let current_day: u32 = local_now_date.day(); + + (current_year, current_month, current_day) +} + +fn add_months_of_year_to_table( + arguments: &Arguments, + calendar_vec_deque: &mut VecDeque, + tag: Span, + selected_year: i32, + (start_month, end_month): (u32, u32), + current_month: u32, + current_day_option: Option, +) -> Result<(), ShellError> { + for month_number in start_month..=end_month { + let mut new_current_day_option: Option = None; + + if let Some(current_day) = current_day_option { + if month_number == current_month { + new_current_day_option = Some(current_day) + } + } + + let add_month_to_table_result = add_month_to_table( + arguments, + calendar_vec_deque, + tag, + selected_year, + month_number, + new_current_day_option, + ); + + add_month_to_table_result? + } + + Ok(()) +} + +fn add_month_to_table( + arguments: &Arguments, + calendar_vec_deque: &mut VecDeque, + tag: Span, + selected_year: i32, + current_month: u32, + current_day_option: Option, +) -> Result<(), ShellError> { + let month_helper_result = MonthHelper::new(selected_year, current_month); + + let full_year_value: &Option> = &arguments.full_year; + + let month_helper = match month_helper_result { + Ok(month_helper) => month_helper, + Err(()) => match full_year_value { + Some(x) => return Err(get_invalid_year_shell_error(x.span)), + None => { + return Err(ShellError::UnknownOperator( + "Issue parsing command, invalid command".to_string(), + tag, + )) + } + }, + }; + + let mut days_of_the_week = [ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + ]; + + let mut week_start_day = days_of_the_week[0].to_string(); + if let Some(day) = &arguments.week_start { + let s = &day.item; + if days_of_the_week.contains(&s.as_str()) { + week_start_day = s.to_string(); + } else { + return Err(ShellError::UnsupportedInput( + "The specified week start day is invalid".to_string(), + day.span, + )); + } + } + + let week_start_day_offset = days_of_the_week.len() + - days_of_the_week + .iter() + .position(|day| *day == week_start_day) + .unwrap_or(0); + + days_of_the_week.rotate_right(week_start_day_offset); + + let mut total_start_offset: u32 = + month_helper.day_number_of_week_month_starts_on + week_start_day_offset as u32; + total_start_offset %= days_of_the_week.len() as u32; + + let mut day_number: u32 = 1; + let day_limit: u32 = total_start_offset + month_helper.number_of_days_in_month; + + let should_show_year_column = arguments.year; + let should_show_quarter_column = arguments.quarter; + let should_show_month_column = arguments.month; + let should_show_month_names = arguments.month_names; + + while day_number <= day_limit { + let mut indexmap = IndexMap::new(); + + if should_show_year_column { + indexmap.insert( + "year".to_string(), + Value::Int { + val: month_helper.selected_year as i64, + span: tag, + }, + ); + } + + if should_show_quarter_column { + indexmap.insert( + "quarter".to_string(), + Value::Int { + val: month_helper.quarter_number as i64, + span: tag, + }, + ); + } + + if should_show_month_column || should_show_month_names { + let month_value = if should_show_month_names { + Value::String { + val: month_helper.month_name.clone(), + span: tag, + } + } else { + Value::Int { + val: month_helper.selected_month as i64, + span: tag, + } + }; + + indexmap.insert("month".to_string(), month_value); + } + + for day in &days_of_the_week { + let should_add_day_number_to_table = + (day_number > total_start_offset) && (day_number <= day_limit); + + let mut value = Value::Nothing { span: tag }; + + if should_add_day_number_to_table { + let adjusted_day_number = day_number - total_start_offset; + + value = Value::Int { + val: adjusted_day_number as i64, + span: tag, + }; + + if let Some(current_day) = current_day_option { + if current_day == adjusted_day_number { + // TODO: Update the value here with a color when color support is added + // This colors the current day + } + } + } + + indexmap.insert((*day).to_string(), value); + + day_number += 1; + } + + let cols: Vec = indexmap.keys().map(|f| f.to_string()).collect(); + let mut vals: Vec = Vec::new(); + for c in &cols { + if let Some(x) = indexmap.get(c) { + vals.push(x.to_owned()) + } + } + calendar_vec_deque.push_back(Value::Record { + cols, + vals, + span: tag, + }) + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Cal {}) + } +} diff --git a/crates/nu-command/src/calendar/mod.rs b/crates/nu-command/src/calendar/mod.rs new file mode 100644 index 0000000000..09f61b8ce8 --- /dev/null +++ b/crates/nu-command/src/calendar/mod.rs @@ -0,0 +1,3 @@ +mod cal; + +pub use cal::Cal; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index c962ea194c..3a3ca2470d 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -32,6 +32,7 @@ pub fn create_default_context() -> EngineState { Append, Benchmark, BuildString, + Cal, Cd, Clear, Collect, diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index 12b0d9bb2e..c4823cc93d 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,3 +1,4 @@ +mod calendar; mod conversions; mod core_commands; mod date; @@ -18,7 +19,7 @@ mod viewers; #[cfg(feature = "dataframe")] mod dataframe; - +pub use calendar::*; pub use conversions::*; pub use core_commands::*; pub use date::*;