2026-02-12 21:10:24 +01:00
|
|
|
use chrono::{TimeDelta, prelude::*};
|
|
|
|
|
use chrono_tz::Tz;
|
|
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fmt;
|
|
|
|
|
|
|
|
|
|
#[derive(PartialEq, Debug)]
|
2026-02-12 21:41:18 +01:00
|
|
|
pub enum CalendarError {
|
2026-02-12 21:10:24 +01:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
f.write_str(&message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Error for CalendarError {}
|
|
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
pub struct SimpleTimezone {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub offset: FixedOffset,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 21:10:24 +01:00
|
|
|
fn create_calendar_strings(time: &NaiveTime, padding_hrs: i8) -> Vec<String> {
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
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(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 21:10:24 +01:00
|
|
|
pub fn create_timetable(
|
2026-02-12 23:52:24 +01:00
|
|
|
target_time: &NaiveTime,
|
|
|
|
|
target_date: &NaiveDate,
|
|
|
|
|
local_timezone: &FixedOffset,
|
2026-02-12 21:10:24 +01:00
|
|
|
in_timezone: &String,
|
|
|
|
|
padding_hrs: i8,
|
|
|
|
|
) -> Result<Vec<String>, CalendarError> {
|
2026-02-12 23:52:24 +01:00
|
|
|
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()
|
2026-02-12 21:10:24 +01:00
|
|
|
} else {
|
2026-02-12 23:52:24 +01:00
|
|
|
let parsed_timezone = in_timezone
|
|
|
|
|
.parse::<Tz>()
|
|
|
|
|
.map_err(|_| CalendarError::UnknownTimezone(in_timezone.clone()))?;
|
2026-02-12 21:10:24 +01:00
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
datetime
|
|
|
|
|
.single()
|
|
|
|
|
.ok_or(CalendarError::TimezoneConversionFailed(in_timezone.clone()))?
|
|
|
|
|
.with_timezone(&parsed_timezone)
|
|
|
|
|
.naive_local()
|
|
|
|
|
};
|
2026-02-12 21:10:24 +01:00
|
|
|
|
|
|
|
|
Ok(create_calendar_strings(
|
|
|
|
|
&time_in_timezone.time(),
|
|
|
|
|
padding_hrs,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 21:41:18 +01:00
|
|
|
pub fn get_todays_date() -> String {
|
|
|
|
|
Local::now().format("%d-%m-%Y").to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 21:10:24 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_calendar_string() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
create_calendar_strings(&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"
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_calendar_string_overflow() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
create_calendar_strings(&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"
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_calendar_string_underflow() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
create_calendar_strings(&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"
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_create_timezone() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
create_timetable(
|
|
|
|
|
&"10".to_string(),
|
|
|
|
|
&"1-1-2000".to_string(),
|
|
|
|
|
&"Europe/Prague".to_string(),
|
|
|
|
|
2
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
vec!["08:00", "09:00", "10:00", "11:00", "12:00"]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_create_timezone_nonzero_minutes() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
create_timetable(
|
|
|
|
|
&"10:30".to_string(),
|
|
|
|
|
&"1-1-2000".to_string(),
|
|
|
|
|
&"Europe/Prague".to_string(),
|
|
|
|
|
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())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|