From 5f4b97d54d421571284011cce19d8b9b453c001b Mon Sep 17 00:00:00 2001 From: Ondrej Novak Date: Tue, 17 Feb 2026 23:42:26 +0100 Subject: [PATCH] refactor calendar interface --- src/calendar.rs | 196 ++++++++++++++++++------------------------------ src/cli.rs | 67 +++++++++++++---- 2 files changed, 122 insertions(+), 141 deletions(-) diff --git a/src/calendar.rs b/src/calendar.rs index 432d5e7..87d7507 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -5,16 +5,12 @@ use std::fmt; #[derive(PartialEq, Debug)] pub enum CalendarError { - UnknownTimezone(String), - WrongTimeFormat, TimezoneConversionFailed(String), } impl fmt::Display for CalendarError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match *self { - Self::UnknownTimezone(ref tz) => format!("Unknown timezone: {}", tz), - Self::WrongTimeFormat => "Wrong time format".to_string(), Self::TimezoneConversionFailed(ref tz) => { format!("Could not convert time to timezone: {}", tz) } @@ -26,80 +22,33 @@ impl fmt::Display for CalendarError { impl Error for CalendarError {} -pub struct SimpleTimezone { - pub name: String, - pub offset: FixedOffset, -} - -fn create_calendar_strings(time: &NaiveTime, padding_hrs: i8) -> Vec { +fn create_timetable_vecs(time: &NaiveTime, padding_hrs: i8) -> Vec { let start = -padding_hrs; let end = padding_hrs; (start..=end) .map(|v| time.overflowing_add_signed(TimeDelta::hours(v.into())).0) - .map(|h| h.format("%H:%M").to_string()) .collect() } -pub fn get_local_timezone_offset( - time: &NaiveTime, - timezone: &String, -) -> Result { - if timezone == "Local" { - Ok(SimpleTimezone { - name: "Local".to_string(), - offset: Local::now() - .with_time(time.clone()) - .single() - .ok_or(CalendarError::WrongTimeFormat)? - .offset() - .clone(), - }) - } else { - let offset = timezone - .parse::() - .map_err(|_| CalendarError::UnknownTimezone(timezone.to_string()))? - .offset_from_utc_datetime(&Utc::now().naive_utc()); - - Ok(SimpleTimezone { - name: timezone.to_string(), - offset: offset.fix(), - }) - } -} - pub fn create_timetable( target_time: &NaiveTime, target_date: &NaiveDate, - local_timezone: &FixedOffset, - in_timezone: &String, + local_timezone: &Tz, + target_timezone: &Tz, padding_hrs: i8, -) -> Result, CalendarError> { - let datetime = local_timezone.from_local_datetime(&NaiveDateTime::new( - target_date.clone(), - target_time.clone(), - )); - let time_in_timezone = if in_timezone == "Local" { - Local::now() - .with_time(target_time.clone()) - .single() - .ok_or(CalendarError::WrongTimeFormat)? - .naive_local() - } else { - let parsed_timezone = in_timezone - .parse::() - .map_err(|_| CalendarError::UnknownTimezone(in_timezone.clone()))?; +) -> Result, CalendarError> { + let datetime = local_timezone + .from_local_datetime(&NaiveDateTime::new( + target_date.clone(), + target_time.clone(), + )) + .single() + .ok_or_else(|| { + CalendarError::TimezoneConversionFailed(local_timezone.name().to_string()) + })?; + let time_in_timezone = datetime.with_timezone(target_timezone); - datetime - .single() - .ok_or(CalendarError::TimezoneConversionFailed(in_timezone.clone()))? - .with_timezone(&parsed_timezone) - .naive_local() - }; - - Ok(create_calendar_strings( - &time_in_timezone.time(), - padding_hrs, - )) + Ok(create_timetable_vecs(&time_in_timezone.time(), padding_hrs)) } pub fn get_todays_date() -> String { @@ -109,33 +58,58 @@ pub fn get_todays_date() -> String { #[cfg(test)] mod tests { use super::*; + use chrono_tz::Europe::Prague; #[test] - fn test_calendar_string() { + fn test_timetable_vecs() { assert_eq!( - create_calendar_strings(&NaiveTime::from_hms_opt(10, 0, 0).unwrap(), 4), + create_timetable_vecs(&NaiveTime::from_hms_opt(10, 0, 0).unwrap(), 4), vec![ - "06:00", "07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00" + NaiveTime::from_hms_opt(06, 0, 0).unwrap(), + NaiveTime::from_hms_opt(07, 0, 0).unwrap(), + NaiveTime::from_hms_opt(08, 0, 0).unwrap(), + NaiveTime::from_hms_opt(09, 0, 0).unwrap(), + NaiveTime::from_hms_opt(10, 0, 0).unwrap(), + NaiveTime::from_hms_opt(11, 0, 0).unwrap(), + NaiveTime::from_hms_opt(12, 0, 0).unwrap(), + NaiveTime::from_hms_opt(13, 0, 0).unwrap(), + NaiveTime::from_hms_opt(14, 0, 0).unwrap(), ] ); } #[test] - fn test_calendar_string_overflow() { + fn test_timetable_vecs_overflow() { assert_eq!( - create_calendar_strings(&NaiveTime::from_hms_opt(3, 0, 0).unwrap(), 4), + create_timetable_vecs(&NaiveTime::from_hms_opt(3, 0, 0).unwrap(), 4), vec![ - "23:00", "00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", "07:00" + NaiveTime::from_hms_opt(23, 0, 0).unwrap(), + NaiveTime::from_hms_opt(00, 0, 0).unwrap(), + NaiveTime::from_hms_opt(01, 0, 0).unwrap(), + NaiveTime::from_hms_opt(02, 0, 0).unwrap(), + NaiveTime::from_hms_opt(03, 0, 0).unwrap(), + NaiveTime::from_hms_opt(04, 0, 0).unwrap(), + NaiveTime::from_hms_opt(05, 0, 0).unwrap(), + NaiveTime::from_hms_opt(06, 0, 0).unwrap(), + NaiveTime::from_hms_opt(07, 0, 0).unwrap(), ] ); } #[test] - fn test_calendar_string_underflow() { + fn test_timetable_vecs_underflow() { assert_eq!( - create_calendar_strings(&NaiveTime::from_hms_opt(22, 0, 0).unwrap(), 4), + create_timetable_vecs(&NaiveTime::from_hms_opt(22, 0, 0).unwrap(), 4), vec![ - "18:00", "19:00", "20:00", "21:00", "22:00", "23:00", "00:00", "01:00", "02:00" + NaiveTime::from_hms_opt(18, 0, 0).unwrap(), + NaiveTime::from_hms_opt(19, 0, 0).unwrap(), + NaiveTime::from_hms_opt(20, 0, 0).unwrap(), + NaiveTime::from_hms_opt(21, 0, 0).unwrap(), + NaiveTime::from_hms_opt(22, 0, 0).unwrap(), + NaiveTime::from_hms_opt(23, 0, 0).unwrap(), + NaiveTime::from_hms_opt(00, 0, 0).unwrap(), + NaiveTime::from_hms_opt(01, 0, 0).unwrap(), + NaiveTime::from_hms_opt(02, 0, 0).unwrap(), ] ); } @@ -144,13 +118,20 @@ mod tests { fn test_create_timezone() { assert_eq!( create_timetable( - &"10".to_string(), - &"1-1-2000".to_string(), - &"Europe/Prague".to_string(), + &NaiveTime::from_hms_opt(10, 0, 0).unwrap(), + &NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), + &Prague, + &Prague, 2 ) .unwrap(), - vec!["08:00", "09:00", "10:00", "11:00", "12:00"] + vec![ + NaiveTime::from_hms_opt(08, 0, 0).unwrap(), + NaiveTime::from_hms_opt(09, 0, 0).unwrap(), + NaiveTime::from_hms_opt(10, 0, 0).unwrap(), + NaiveTime::from_hms_opt(11, 0, 0).unwrap(), + NaiveTime::from_hms_opt(12, 0, 0).unwrap() + ] ); } @@ -158,55 +139,20 @@ mod tests { fn test_create_timezone_nonzero_minutes() { assert_eq!( create_timetable( - &"10:30".to_string(), - &"1-1-2000".to_string(), - &"Europe/Prague".to_string(), + &NaiveTime::from_hms_opt(10, 30, 0).unwrap(), + &NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), + &Prague, + &Prague, 2 ) .unwrap(), - vec!["08:30", "09:30", "10:30", "11:30", "12:30"] - ); - } - - #[test] - fn test_create_timezone_wrong_time() { - assert_eq!( - create_timetable( - &"hello".to_string(), - &"1-1-2000".to_string(), - &"Europe/Prague".to_string(), - 2, - ) - .unwrap_err(), - CalendarError::WrongTimeFormat - ); - } - - #[test] - fn test_create_timezone_wrong_date() { - assert_eq!( - create_timetable( - &"10:00".to_string(), - &"hello".to_string(), - &"Europe/Prague".to_string(), - 2, - ) - .unwrap_err(), - CalendarError::WrongDateFormat - ); - } - - #[test] - fn test_create_timezone_wrong_zone_name() { - assert_eq!( - create_timetable( - &"10:00".to_string(), - &"1-1-2000".to_string(), - &"hello".to_string(), - 2, - ) - .unwrap_err(), - CalendarError::UnknownTimezone("hello".to_string()) + vec![ + NaiveTime::from_hms_opt(08, 30, 0).unwrap(), + NaiveTime::from_hms_opt(09, 30, 0).unwrap(), + NaiveTime::from_hms_opt(10, 30, 0).unwrap(), + NaiveTime::from_hms_opt(11, 30, 0).unwrap(), + NaiveTime::from_hms_opt(12, 30, 0).unwrap() + ] ); } } diff --git a/src/cli.rs b/src/cli.rs index f213e4c..87b5935 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,9 @@ -use crate::calendar::CalendarError; - -use super::calendar::{create_timetable, get_local_timezone_offset, get_todays_date}; +use super::calendar::{create_timetable, get_todays_date}; use chrono::NaiveDate; use chrono::NaiveTime; +use chrono_tz::Etc::UTC; +use chrono_tz::Tz; use clap::Parser; use std::error::Error; use std::fmt; @@ -28,7 +28,8 @@ pub struct Args { pub enum CliError { CouldNotParseTime, CouldNotParseDate, - CouldNoGenerateTimetable(CalendarError), + CouldNotParseTimezone(String), + CouldNoGenerateTimetable(String), } impl fmt::Display for CliError { @@ -36,6 +37,9 @@ impl fmt::Display for CliError { let message = match *self { Self::CouldNotParseTime => "Couldn't parse time".to_string(), Self::CouldNotParseDate => "Couldn't parse date".to_string(), + Self::CouldNotParseTimezone(ref tz) => { + format!("Couldn't parse timezone {}", tz).to_string() + } Self::CouldNoGenerateTimetable(ref er) => { format!("Could not generate timetable: {}", er) } @@ -66,30 +70,61 @@ pub fn print_timezones( .or_else(|_| NaiveDate::parse_from_str(date, "%d-%m")) .map_err(|_| CliError::CouldNotParseDate)?; - let local_timezone_offset = &get_local_timezone_offset(&parsed_time, local_timezone) - .map_err(|e| CliError::CouldNoGenerateTimetable(e))?; + let parsed_local_timezone: Tz = if local_timezone == "Local" { + UTC // FIXME: use local timezone instead + } else { + local_timezone + .parse::() + .map_err(|_| CliError::CouldNoGenerateTimetable(local_timezone.to_string()))? + }; - let all_timezones: Vec<&String> = iter::once(&local_timezone_offset.name) - .chain(timezones) + let parsed_timezones: Vec = timezones + .iter() + .map(|tz| { + tz.parse::() + .map_err(|_| CliError::CouldNotParseTimezone(tz.to_string())) + }) + .collect::, _>>()?; + + let all_timezones: Vec = iter::once(parsed_local_timezone) + .chain(parsed_timezones) .collect(); - let max_len = all_timezones.iter().map(|t| t.len()).max().unwrap_or(0) as u32; + let max_len = all_timezones + .iter() + .map(|t| t.name().len()) + .max() + .unwrap_or(0) as u32; for timezone in all_timezones { - let name_padding = (0..(max_len - timezone.len() as u32)) - .map(|_| " ".to_string()) - .collect::>() - .join(""); + let timezone_name = timezone.name(); + let name_padding_len = max_len - timezone_name.len() as u32; match create_timetable( &parsed_time, &parsed_date, - &local_timezone_offset.offset, - timezone, + &parsed_local_timezone, + &timezone, 5, ) { - Ok(timetable) => println!("{}:{}\t{}", timezone, name_padding, timetable.join("\t")), + Ok(timetable) => { + print_timetable(&timezone_name.to_string(), name_padding_len, &timetable) + } Err(error) => panic!("Failed: {}", error), } } Ok(()) } + +fn print_timetable(name: &String, name_padding_len: u32, timetable: &Vec) { + let formatted: Vec = timetable + .iter() + .enumerate() + .map(|(i, t)| t.format("%H:%M").to_string()) + .collect(); + let name_padding = (0..name_padding_len) + .map(|_| " ".to_string()) + .collect::>() + .join(""); + + println!("{}:{}\t{}", name, name_padding, formatted.join("\t")); +}