add an option to specify local timezone

This commit is contained in:
Ondrej Novak 2026-02-12 23:52:24 +01:00
parent 85cf3e546e
commit 3cec3953c0
3 changed files with 131 additions and 30 deletions

View File

@ -7,7 +7,6 @@ use std::fmt;
pub enum CalendarError { pub enum CalendarError {
UnknownTimezone(String), UnknownTimezone(String),
WrongTimeFormat, WrongTimeFormat,
WrongDateFormat,
TimezoneConversionFailed(String), TimezoneConversionFailed(String),
} }
@ -16,7 +15,6 @@ impl fmt::Display for CalendarError {
let message = match *self { let message = match *self {
Self::UnknownTimezone(ref tz) => format!("Unknown timezone: {}", tz), Self::UnknownTimezone(ref tz) => format!("Unknown timezone: {}", tz),
Self::WrongTimeFormat => "Wrong time format".to_string(), Self::WrongTimeFormat => "Wrong time format".to_string(),
Self::WrongDateFormat => "Wrong date format".to_string(),
Self::TimezoneConversionFailed(ref tz) => { Self::TimezoneConversionFailed(ref tz) => {
format!("Could not convert time to timezone: {}", tz) format!("Could not convert time to timezone: {}", tz)
} }
@ -28,6 +26,11 @@ impl fmt::Display for CalendarError {
impl Error 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<String> { fn create_calendar_strings(time: &NaiveTime, padding_hrs: i8) -> Vec<String> {
let start = -padding_hrs; let start = -padding_hrs;
let end = padding_hrs; let end = padding_hrs;
@ -37,33 +40,62 @@ fn create_calendar_strings(time: &NaiveTime, padding_hrs: i8) -> Vec<String> {
.collect() .collect()
} }
pub fn get_local_timezone_offset(
time: &NaiveTime,
timezone: &String,
) -> Result<SimpleTimezone, CalendarError> {
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::<Tz>()
.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( pub fn create_timetable(
target_time: &String, target_time: &NaiveTime,
target_date: &String, target_date: &NaiveDate,
local_timezone: &FixedOffset,
in_timezone: &String, in_timezone: &String,
padding_hrs: i8, padding_hrs: i8,
) -> Result<Vec<String>, CalendarError> { ) -> Result<Vec<String>, CalendarError> {
let parsed_timezone = in_timezone let datetime = local_timezone.from_local_datetime(&NaiveDateTime::new(
.parse::<Tz>() target_date.clone(),
.map_err(|_| CalendarError::UnknownTimezone(in_timezone.clone()))?; target_time.clone(),
));
let parsed_time = if let Ok(hour) = target_time.parse::<u32>() { let time_in_timezone = if in_timezone == "Local" {
NaiveTime::from_hms_opt(hour, 0, 0).ok_or(CalendarError::WrongTimeFormat)? Local::now()
.with_time(target_time.clone())
.single()
.ok_or(CalendarError::WrongTimeFormat)?
.naive_local()
} else { } else {
NaiveTime::parse_from_str(target_time, "%H:%M") let parsed_timezone = in_timezone
.map_err(|_| CalendarError::WrongTimeFormat)? .parse::<Tz>()
.map_err(|_| CalendarError::UnknownTimezone(in_timezone.clone()))?;
datetime
.single()
.ok_or(CalendarError::TimezoneConversionFailed(in_timezone.clone()))?
.with_timezone(&parsed_timezone)
.naive_local()
}; };
let parsed_date = NaiveDate::parse_from_str(target_date, "%d-%m-%Y")
.or_else(|_| NaiveDate::parse_from_str(target_date, "%d-%m"))
.map_err(|_| CalendarError::WrongDateFormat)?;
let datetime = Local.from_local_datetime(&NaiveDateTime::new(parsed_date, parsed_time));
let time_in_timezone = datetime
.single()
.ok_or(CalendarError::TimezoneConversionFailed(in_timezone.clone()))?
.with_timezone(&parsed_timezone);
Ok(create_calendar_strings( Ok(create_calendar_strings(
&time_in_timezone.time(), &time_in_timezone.time(),
padding_hrs, padding_hrs,

View File

@ -1,5 +1,13 @@
use super::calendar::create_timetable; use crate::calendar::CalendarError;
use super::calendar::{create_timetable, get_local_timezone_offset, get_todays_date};
use chrono::NaiveDate;
use chrono::NaiveTime;
use clap::Parser; use clap::Parser;
use std::error::Error;
use std::fmt;
use std::iter;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -10,19 +18,78 @@ pub struct Args {
#[arg(short, long)] #[arg(short, long)]
pub date: Option<String>, pub date: Option<String>,
#[arg(short, long, default_value = "Local")]
pub local: String,
pub time: String, pub time: String,
} }
pub fn print_timezones(timezones: &Vec<String>, time: &String, date: &String) { #[derive(PartialEq, Debug)]
let max_len = timezones.iter().map(|t| t.len()).max().unwrap_or(0) as u32; pub enum CliError {
for timezone in timezones { CouldNotParseTime,
CouldNotParseDate,
CouldNoGenerateTimetable(CalendarError),
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let message = match *self {
Self::CouldNotParseTime => "Couldn't parse time".to_string(),
Self::CouldNotParseDate => "Couldn't parse date".to_string(),
Self::CouldNoGenerateTimetable(ref er) => {
format!("Could not generate timetable: {}", er)
}
};
f.write_str(&message)
}
}
impl Error for CliError {}
pub fn print_timezones(
timezones: &Vec<String>,
time: &String,
date: Option<&String>,
local_timezone: &String,
) -> Result<(), CliError> {
let todays_date = &get_todays_date();
let date = date.unwrap_or(todays_date);
let parsed_time = if let Ok(hour) = time.parse::<u32>() {
NaiveTime::from_hms_opt(hour, 0, 0).ok_or(CliError::CouldNotParseTime)?
} else {
NaiveTime::parse_from_str(time, "%H:%M").map_err(|_| CliError::CouldNotParseTime)?
};
let parsed_date = NaiveDate::parse_from_str(date, "%d-%m-%Y")
.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 all_timezones: Vec<&String> = iter::once(&local_timezone_offset.name)
.chain(timezones)
.collect();
let max_len = all_timezones.iter().map(|t| t.len()).max().unwrap_or(0) as u32;
for timezone in all_timezones {
let name_padding = (0..(max_len - timezone.len() as u32)) let name_padding = (0..(max_len - timezone.len() as u32))
.map(|_| " ".to_string()) .map(|_| " ".to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(""); .join("");
match create_timetable(time, date, timezone, 5) { match create_timetable(
&parsed_time,
&parsed_date,
&local_timezone_offset.offset,
timezone,
5,
) {
Ok(timetable) => println!("{}:{}\t{}", timezone, name_padding, timetable.join("\t")), Ok(timetable) => println!("{}:{}\t{}", timezone, name_padding, timetable.join("\t")),
Err(error) => panic!("Failed: {}", error), Err(error) => panic!("Failed: {}", error),
} }
} }
Ok(())
} }

View File

@ -1,7 +1,6 @@
mod calendar; mod calendar;
mod cli; mod cli;
use calendar::get_todays_date;
use clap::Parser; use clap::Parser;
use cli::{Args, print_timezones}; use cli::{Args, print_timezones};
@ -9,7 +8,10 @@ fn main() {
let args = Args::parse(); let args = Args::parse();
if args.timezone.len() > 0 { if args.timezone.len() > 0 {
let date = args.date.unwrap_or(get_todays_date()); let res = print_timezones(&args.timezone, &args.time, args.date.as_ref(), &args.local);
print_timezones(&args.timezone, &args.time, &date);
if let Err(error) = res {
println!("Error: {}", error);
}
} }
} }