2020-05-10 01:05:48 +02:00
|
|
|
use crate::prelude::*;
|
2020-05-16 06:00:06 +02:00
|
|
|
use chrono::{Datelike, Local, NaiveDate};
|
2020-05-10 01:05:48 +02:00
|
|
|
use indexmap::IndexMap;
|
2021-01-10 03:50:49 +01:00
|
|
|
use nu_engine::{EvaluatedWholeStreamCommandArgs, WholeStreamCommand};
|
2020-06-07 05:52:42 +02:00
|
|
|
use nu_errors::ShellError;
|
|
|
|
use nu_protocol::{Dictionary, Signature, SyntaxShape, UntaggedValue, Value};
|
2020-05-10 01:05:48 +02:00
|
|
|
|
|
|
|
pub struct Cal;
|
|
|
|
|
2020-05-29 10:22:52 +02:00
|
|
|
#[async_trait]
|
2020-05-10 01:05:48 +02:00
|
|
|
impl WholeStreamCommand for Cal {
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
"cal"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn signature(&self) -> Signature {
|
|
|
|
Signature::build("cal")
|
2020-05-11 02:35:24 +02:00
|
|
|
.switch("year", "Display the year column", Some('y'))
|
|
|
|
.switch("quarter", "Display the quarter column", Some('q'))
|
|
|
|
.switch("month", "Display the month column", Some('m'))
|
2020-05-10 01:05:48 +02:00
|
|
|
.named(
|
2020-05-11 02:35:24 +02:00
|
|
|
"full-year",
|
2020-05-10 01:05:48 +02:00
|
|
|
SyntaxShape::Int,
|
|
|
|
"Display a year-long calendar for the specified year",
|
2020-05-11 02:35:24 +02:00
|
|
|
None,
|
|
|
|
)
|
2020-06-18 19:34:51 +02:00
|
|
|
.named(
|
|
|
|
"week-start",
|
|
|
|
SyntaxShape::String,
|
|
|
|
"Display the calendar with the specified day as the first day of the week",
|
|
|
|
None,
|
|
|
|
)
|
2020-05-11 02:35:24 +02:00
|
|
|
.switch(
|
|
|
|
"month-names",
|
|
|
|
"Display the month names instead of integers",
|
|
|
|
None,
|
2020-05-10 01:05:48 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn usage(&self) -> &str {
|
|
|
|
"Display a calendar."
|
|
|
|
}
|
|
|
|
|
2020-12-18 08:53:49 +01:00
|
|
|
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
|
|
|
|
cal(args).await
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
2020-05-12 03:00:55 +02:00
|
|
|
|
2020-05-18 14:56:01 +02:00
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
|
|
vec![
|
2020-05-12 03:00:55 +02:00
|
|
|
Example {
|
|
|
|
description: "This month's calendar",
|
|
|
|
example: "cal",
|
2020-05-18 14:56:01 +02:00
|
|
|
result: None,
|
2020-05-12 03:00:55 +02:00
|
|
|
},
|
|
|
|
Example {
|
|
|
|
description: "The calendar for all of 2012",
|
|
|
|
example: "cal --full-year 2012",
|
2020-05-18 14:56:01 +02:00
|
|
|
result: None,
|
2020-05-12 03:00:55 +02:00
|
|
|
},
|
2020-06-18 19:34:51 +02:00
|
|
|
Example {
|
|
|
|
description: "This month's calendar with the week starting on monday",
|
|
|
|
example: "cal --week-start monday",
|
|
|
|
result: None,
|
|
|
|
},
|
2020-05-12 03:00:55 +02:00
|
|
|
]
|
|
|
|
}
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
|
|
|
|
2020-12-18 08:53:49 +01:00
|
|
|
pub async fn cal(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
|
|
|
let args = args.evaluate_once().await?;
|
2020-05-30 01:36:04 +02:00
|
|
|
let mut calendar_vec_deque = VecDeque::new();
|
|
|
|
let tag = args.call_info.name_tag.clone();
|
|
|
|
|
|
|
|
let (current_year, current_month, current_day) = get_current_date();
|
|
|
|
|
|
|
|
let mut selected_year: i32 = current_year;
|
|
|
|
let mut current_day_option: Option<u32> = Some(current_day);
|
2020-05-10 01:05:48 +02:00
|
|
|
|
2020-06-28 19:16:10 +02:00
|
|
|
let month_range = if let Some(full_year_value) = args.get("full-year") {
|
|
|
|
if let Ok(year_u64) = full_year_value.as_u64() {
|
|
|
|
selected_year = year_u64 as i32;
|
|
|
|
|
|
|
|
if selected_year != current_year {
|
|
|
|
current_day_option = None
|
2020-05-16 06:00:06 +02:00
|
|
|
}
|
2020-06-28 19:16:10 +02:00
|
|
|
} else {
|
|
|
|
return Err(get_invalid_year_shell_error(&full_year_value.tag()));
|
2020-05-16 05:18:24 +02:00
|
|
|
}
|
2020-05-30 01:36:04 +02:00
|
|
|
|
|
|
|
(1, 12)
|
|
|
|
} else {
|
|
|
|
(current_month, current_month)
|
2020-05-16 05:18:24 +02:00
|
|
|
};
|
2020-05-10 01:05:48 +02:00
|
|
|
|
2020-07-25 14:40:35 +02:00
|
|
|
add_months_of_year_to_table(
|
2020-05-30 01:36:04 +02:00
|
|
|
&args,
|
|
|
|
&mut calendar_vec_deque,
|
|
|
|
&tag,
|
|
|
|
selected_year,
|
|
|
|
month_range,
|
|
|
|
current_month,
|
|
|
|
current_day_option,
|
2020-07-25 14:40:35 +02:00
|
|
|
)?;
|
2020-05-30 01:36:04 +02:00
|
|
|
|
2020-07-25 14:40:35 +02:00
|
|
|
Ok(futures::stream::iter(calendar_vec_deque).to_output_stream())
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
|
|
|
|
2020-05-16 06:00:06 +02:00
|
|
|
fn get_invalid_year_shell_error(year_tag: &Tag) -> ShellError {
|
|
|
|
ShellError::labeled_error("The year is invalid", "invalid year", year_tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
struct MonthHelper {
|
|
|
|
selected_year: i32,
|
|
|
|
selected_month: u32,
|
2020-06-16 22:00:49 +02:00
|
|
|
day_number_of_week_month_starts_on: u32,
|
|
|
|
number_of_days_in_month: u32,
|
|
|
|
quarter_number: u32,
|
|
|
|
month_name: String,
|
2020-05-16 06:00:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MonthHelper {
|
|
|
|
pub fn new(selected_year: i32, selected_month: u32) -> Result<MonthHelper, ()> {
|
2020-06-16 22:00:49 +02:00
|
|
|
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 {
|
2020-05-16 06:00:06 +02:00
|
|
|
selected_year,
|
|
|
|
selected_month,
|
2020-06-16 22:00:49 +02:00
|
|
|
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(),
|
|
|
|
})
|
2020-05-16 06:00:06 +02:00
|
|
|
}
|
|
|
|
|
2020-06-16 22:00:49 +02:00
|
|
|
fn calculate_number_of_days_in_month(
|
|
|
|
mut selected_year: i32,
|
|
|
|
mut selected_month: u32,
|
|
|
|
) -> Result<u32, ()> {
|
2020-05-16 06:00:06 +02:00
|
|
|
// 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
|
2020-06-16 22:00:49 +02:00
|
|
|
if selected_month == 12 {
|
|
|
|
selected_year += 1;
|
|
|
|
selected_month = 1;
|
2020-05-16 06:00:06 +02:00
|
|
|
} else {
|
2020-06-16 22:00:49 +02:00
|
|
|
selected_month += 1;
|
2020-05-16 06:00:06 +02:00
|
|
|
};
|
|
|
|
|
2020-06-16 22:00:49 +02:00
|
|
|
let next_month_naive_date =
|
|
|
|
NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?;
|
2020-05-16 06:00:06 +02:00
|
|
|
|
2020-06-16 22:00:49 +02:00
|
|
|
Ok(next_month_naive_date.pred().day())
|
2020-05-16 06:00:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-10 01:05:48 +02:00
|
|
|
fn get_current_date() -> (i32, u32, u32) {
|
2020-05-16 06:00:06 +02:00
|
|
|
let local_now_date = Local::now().date();
|
2020-05-10 01:05:48 +02:00
|
|
|
|
2020-05-16 06:00:06 +02:00
|
|
|
let current_year: i32 = local_now_date.year();
|
|
|
|
let current_month: u32 = local_now_date.month();
|
|
|
|
let current_day: u32 = local_now_date.day();
|
2020-05-10 01:05:48 +02:00
|
|
|
|
|
|
|
(current_year, current_month, current_day)
|
|
|
|
}
|
|
|
|
|
2020-05-16 06:00:06 +02:00
|
|
|
fn add_months_of_year_to_table(
|
|
|
|
args: &EvaluatedWholeStreamCommandArgs,
|
2020-05-10 01:05:48 +02:00
|
|
|
mut calendar_vec_deque: &mut VecDeque<Value>,
|
|
|
|
tag: &Tag,
|
2020-05-16 06:00:06 +02:00
|
|
|
selected_year: i32,
|
|
|
|
(start_month, end_month): (u32, u32),
|
2020-05-10 01:05:48 +02:00
|
|
|
current_month: u32,
|
|
|
|
current_day_option: Option<u32>,
|
2020-05-16 06:00:06 +02:00
|
|
|
) -> Result<(), ShellError> {
|
|
|
|
for month_number in start_month..=end_month {
|
2020-05-10 01:05:48 +02:00
|
|
|
let mut new_current_day_option: Option<u32> = None;
|
|
|
|
|
|
|
|
if let Some(current_day) = current_day_option {
|
|
|
|
if month_number == current_month {
|
|
|
|
new_current_day_option = Some(current_day)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-16 06:00:06 +02:00
|
|
|
let add_month_to_table_result = add_month_to_table(
|
|
|
|
&args,
|
2020-05-10 01:05:48 +02:00
|
|
|
&mut calendar_vec_deque,
|
|
|
|
&tag,
|
|
|
|
selected_year,
|
|
|
|
month_number,
|
|
|
|
new_current_day_option,
|
|
|
|
);
|
2020-05-16 06:00:06 +02:00
|
|
|
|
|
|
|
add_month_to_table_result?
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
2020-05-16 06:00:06 +02:00
|
|
|
|
|
|
|
Ok(())
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn add_month_to_table(
|
2020-05-16 06:00:06 +02:00
|
|
|
args: &EvaluatedWholeStreamCommandArgs,
|
2020-05-10 01:05:48 +02:00
|
|
|
calendar_vec_deque: &mut VecDeque<Value>,
|
|
|
|
tag: &Tag,
|
2020-05-16 06:00:06 +02:00
|
|
|
selected_year: i32,
|
|
|
|
current_month: u32,
|
2020-06-07 05:52:42 +02:00
|
|
|
current_day_option: Option<u32>,
|
2020-05-16 06:00:06 +02:00
|
|
|
) -> Result<(), ShellError> {
|
|
|
|
let month_helper_result = MonthHelper::new(selected_year, current_month);
|
|
|
|
|
|
|
|
let month_helper = match month_helper_result {
|
|
|
|
Ok(month_helper) => month_helper,
|
|
|
|
Err(()) => match args.get("full-year") {
|
|
|
|
Some(full_year_value) => {
|
|
|
|
return Err(get_invalid_year_shell_error(&full_year_value.tag()))
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
return Err(ShellError::labeled_error(
|
|
|
|
"Issue parsing command",
|
|
|
|
"invalid command",
|
|
|
|
tag,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-06-18 19:34:51 +02:00
|
|
|
let mut days_of_the_week = [
|
2020-05-10 01:05:48 +02:00
|
|
|
"sunday",
|
|
|
|
"monday",
|
|
|
|
"tuesday",
|
|
|
|
"wednesday",
|
2020-05-13 14:06:31 +02:00
|
|
|
"thursday",
|
2020-05-10 01:05:48 +02:00
|
|
|
"friday",
|
|
|
|
"saturday",
|
|
|
|
];
|
|
|
|
|
2020-06-18 19:34:51 +02:00
|
|
|
let mut week_start_day = days_of_the_week[0].to_string();
|
|
|
|
|
|
|
|
if let Some(week_start_value) = args.get("week-start") {
|
|
|
|
if let Ok(day) = week_start_value.as_string() {
|
|
|
|
if days_of_the_week.contains(&day.as_str()) {
|
|
|
|
week_start_day = day;
|
|
|
|
} else {
|
|
|
|
return Err(ShellError::labeled_error(
|
|
|
|
"The specified week start day is invalid",
|
|
|
|
"invalid week start day",
|
|
|
|
week_start_value.tag(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2020-05-11 02:35:24 +02:00
|
|
|
let should_show_year_column = args.has("year");
|
|
|
|
let should_show_quarter_column = args.has("quarter");
|
2020-06-16 22:00:49 +02:00
|
|
|
let should_show_month_column = args.has("month");
|
2020-05-11 02:35:24 +02:00
|
|
|
let should_show_month_names = args.has("month-names");
|
|
|
|
|
2020-06-18 19:34:51 +02:00
|
|
|
while day_number <= day_limit {
|
2020-05-10 01:05:48 +02:00
|
|
|
let mut indexmap = IndexMap::new();
|
|
|
|
|
2020-05-11 02:35:24 +02:00
|
|
|
if should_show_year_column {
|
2020-05-16 06:00:06 +02:00
|
|
|
indexmap.insert(
|
|
|
|
"year".to_string(),
|
|
|
|
UntaggedValue::int(month_helper.selected_year).into_value(tag),
|
|
|
|
);
|
2020-05-11 02:35:24 +02:00
|
|
|
}
|
2020-05-10 01:05:48 +02:00
|
|
|
|
2020-05-11 02:35:24 +02:00
|
|
|
if should_show_quarter_column {
|
|
|
|
indexmap.insert(
|
|
|
|
"quarter".to_string(),
|
2020-06-16 22:00:49 +02:00
|
|
|
UntaggedValue::int(month_helper.quarter_number).into_value(tag),
|
2020-05-11 02:35:24 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-28 19:16:10 +02:00
|
|
|
if should_show_month_column || should_show_month_names {
|
2020-05-11 02:35:24 +02:00
|
|
|
let month_value = if should_show_month_names {
|
2020-06-16 22:00:49 +02:00
|
|
|
UntaggedValue::string(month_helper.month_name.clone()).into_value(tag)
|
2020-05-11 02:35:24 +02:00
|
|
|
} else {
|
2020-05-16 06:00:06 +02:00
|
|
|
UntaggedValue::int(month_helper.selected_month).into_value(tag)
|
2020-05-11 02:35:24 +02:00
|
|
|
};
|
2020-05-10 01:05:48 +02:00
|
|
|
|
2020-05-11 02:35:24 +02:00
|
|
|
indexmap.insert("month".to_string(), month_value);
|
|
|
|
}
|
2020-05-10 01:05:48 +02:00
|
|
|
|
|
|
|
for day in &days_of_the_week {
|
2020-06-18 19:34:51 +02:00
|
|
|
let should_add_day_number_to_table =
|
|
|
|
(day_number > total_start_offset) && (day_number <= day_limit);
|
2020-06-07 05:52:42 +02:00
|
|
|
|
|
|
|
let mut value = UntaggedValue::nothing().into_value(tag);
|
|
|
|
|
|
|
|
if should_add_day_number_to_table {
|
2020-06-18 19:34:51 +02:00
|
|
|
let adjusted_day_number = day_number - total_start_offset;
|
2020-06-07 05:52:42 +02:00
|
|
|
|
2020-06-18 19:34:51 +02:00
|
|
|
value = UntaggedValue::int(adjusted_day_number).into_value(tag);
|
2020-06-07 05:52:42 +02:00
|
|
|
|
|
|
|
if let Some(current_day) = current_day_option {
|
2020-06-18 19:34:51 +02:00
|
|
|
if current_day == adjusted_day_number {
|
2020-06-07 05:52:42 +02:00
|
|
|
// TODO: Update the value here with a color when color support is added
|
|
|
|
// This colors the current day
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-10 01:05:48 +02:00
|
|
|
|
|
|
|
indexmap.insert((*day).to_string(), value);
|
|
|
|
|
2020-06-18 19:34:51 +02:00
|
|
|
day_number += 1;
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
calendar_vec_deque
|
|
|
|
.push_back(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(tag));
|
|
|
|
}
|
|
|
|
|
2020-05-16 06:00:06 +02:00
|
|
|
Ok(())
|
2020-05-10 01:05:48 +02:00
|
|
|
}
|
2020-05-18 14:56:01 +02:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::Cal;
|
2020-10-03 16:06:02 +02:00
|
|
|
use super::ShellError;
|
2020-05-18 14:56:01 +02:00
|
|
|
|
|
|
|
#[test]
|
2020-10-03 16:06:02 +02:00
|
|
|
fn examples_work_as_expected() -> Result<(), ShellError> {
|
2020-05-18 14:56:01 +02:00
|
|
|
use crate::examples::test as test_examples;
|
|
|
|
|
2020-10-03 16:06:02 +02:00
|
|
|
Ok(test_examples(Cal {})?)
|
2020-05-18 14:56:01 +02:00
|
|
|
}
|
|
|
|
}
|