use super::calendar::days_to_date;
use std::time::{SystemTime, UNIX_EPOCH};
pub(super) struct ScheduledEvent {
pub(super) title: String,
pub(super) year: i32,
pub(super) month: u32,
pub(super) day: u32,
pub(super) hour: u32,
pub(super) minute: u32,
pub(super) time_zone: &'static str,
pub(super) duration_minutes: u32,
}
impl ScheduledEvent {
pub(super) fn iso_date(&self) -> String {
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
fn start_stamp(&self) -> String {
format!(
"{:04}{:02}{:02}T{:02}{:02}00",
self.year, self.month, self.day, self.hour, self.minute
)
}
fn end_stamp(&self) -> String {
let (year, month, day, hour, minute) = add_minutes(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.duration_minutes,
);
format!("{year:04}{month:02}{day:02}T{hour:02}{minute:02}00")
}
fn uid(&self) -> String {
format!("{}-{}@formal-ai", self.start_stamp(), self.time_zone)
}
pub(super) fn to_ics(&self) -> String {
let lines = [
"BEGIN:VCALENDAR".to_owned(),
"VERSION:2.0".to_owned(),
"PRODID:-//formal-ai//calendar//EN".to_owned(),
"CALSCALE:GREGORIAN".to_owned(),
"METHOD:PUBLISH".to_owned(),
"BEGIN:VEVENT".to_owned(),
format!("UID:{}", self.uid()),
format!("DTSTAMP:{}", ics_dtstamp()),
format!("DTSTART;TZID={}:{}", self.time_zone, self.start_stamp()),
format!("DTEND;TZID={}:{}", self.time_zone, self.end_stamp()),
format!("SUMMARY:{}", ics_escape(&self.title)),
"END:VEVENT".to_owned(),
"END:VCALENDAR".to_owned(),
];
let mut out = lines.join("\r\n");
out.push_str("\r\n");
out
}
pub(super) fn to_google_calendar_url(&self) -> String {
format!(
"https://calendar.google.com/calendar/render?action=TEMPLATE&text={}&dates={}/{}&ctz={}",
percent_encode(&self.title),
self.start_stamp(),
self.end_stamp(),
percent_encode(self.time_zone),
)
}
}
const fn add_minutes(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
minutes: u32,
) -> (i32, u32, u32, u32, u32) {
let total = hour * 60 + minute + minutes;
let mut day_carry = total / (24 * 60);
let new_minute = total % 60;
let new_hour = (total / 60) % 24;
let mut y = year;
let mut m = month;
let mut d = day;
while day_carry > 0 {
let max_day = days_in_month(y, m);
if d < max_day {
d += 1;
} else {
d = 1;
m += 1;
if m > 12 {
m = 1;
y += 1;
}
}
day_carry -= 1;
}
(y, m, d, new_hour, new_minute)
}
const fn days_in_month(year: i32, month: u32) -> u32 {
match month {
2 => {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
29
} else {
28
}
}
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
fn ics_dtstamp() -> String {
let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) else {
return "19700101T000000Z".to_owned();
};
let secs = i64::try_from(now.as_secs()).unwrap_or(0);
let days = secs.div_euclid(86_400);
let sod = secs.rem_euclid(86_400);
let (y, m, d) = days_to_date(days);
format!(
"{:04}{:02}{:02}T{:02}{:02}{:02}Z",
y,
m,
d,
sod / 3_600,
(sod % 3_600) / 60,
sod % 60,
)
}
fn ics_escape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
';' => out.push_str("\\;"),
',' => out.push_str("\\,"),
'\n' => out.push_str("\\n"),
_ => out.push(ch),
}
}
out
}
fn percent_encode(value: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
out.push('%');
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0F) as usize] as char);
}
}
}
out
}