use alloc::{
string::{String, ToString},
vec::Vec,
};
use crate::{
component::{IcalComponent, IcalComponentKind, IcalComponentName},
ical::Ical,
prop::{IcalPropKind, IcalPropName},
recur::{IcalRecurDateTime, set::IcalRecurSet},
value::IcalValue,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IcalTzOffset {
One(i32),
Gap {
before: i32,
after: i32,
},
Fold {
earlier: i32,
later: i32,
},
}
impl IcalTzOffset {
pub fn unambiguous(&self) -> Option<i32> {
match self {
Self::One(offset) => Some(*offset),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcalTzObservance {
pub daylight: bool,
pub from: i32,
pub to: i32,
pub onsets: IcalRecurSet,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcalTz {
pub id: String,
pub observances: Vec<IcalTzObservance>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Transition {
before_local: IcalRecurDateTime,
from: i32,
to: i32,
}
impl Transition {
fn after_local(&self) -> IcalRecurDateTime {
IcalRecurDateTime::from_seconds(
self.before_local.seconds() + i64::from(self.to) - i64::from(self.from),
)
}
}
impl IcalTz {
pub fn of_component(component: &IcalComponent<'_>) -> Option<Self> {
if !matches!(
component.name,
IcalComponentName::Kind(IcalComponentKind::VTimezone)
) {
return None;
}
let id = component
.props
.iter()
.find_map(|prop| match (&prop.name, &prop.value) {
(IcalPropName::Kind(IcalPropKind::TzId), IcalValue::Text(text)) => {
Some(text.0.to_string())
}
_ => None,
})?;
let observances = component
.components
.iter()
.filter_map(IcalTzObservance::of_component)
.collect();
Some(Self { id, observances })
}
pub fn of_calendar(ical: &Ical<'_>, tzid: &str) -> Option<Self> {
ical.components
.iter()
.filter_map(Self::of_component)
.find(|zone| zone.id == tzid)
}
pub fn resolve(&self, local: IcalRecurDateTime) -> IcalTzOffset {
let mut previous: Option<Transition> = None;
let mut next: Option<Transition> = None;
let mut first: Option<Transition> = None;
for observance in &self.observances {
for onset in observance.onsets.expand() {
let transition = Transition {
before_local: onset.start,
from: observance.from,
to: observance.to,
};
if first.is_none_or(|held| transition.before_local < held.before_local) {
first = Some(transition);
}
if transition.before_local <= local {
if previous.is_none_or(|held| held.before_local < transition.before_local) {
previous = Some(transition);
}
} else {
if next.is_none_or(|held| transition.before_local < held.before_local) {
next = Some(transition);
}
break;
}
}
}
if let Some(transition) = previous
&& transition.to > transition.from
&& local < transition.after_local()
{
return IcalTzOffset::Gap {
before: transition.from,
after: transition.to,
};
}
if let Some(transition) = next
&& transition.to < transition.from
&& local >= transition.after_local()
{
return IcalTzOffset::Fold {
earlier: transition.from,
later: transition.to,
};
}
match (previous, first) {
(Some(transition), _) => IcalTzOffset::One(transition.to),
(None, Some(transition)) => IcalTzOffset::One(transition.from),
(None, None) => IcalTzOffset::One(0),
}
}
}
impl IcalTzObservance {
pub fn of_component(component: &IcalComponent<'_>) -> Option<Self> {
let daylight = match component.name {
IcalComponentName::Kind(IcalComponentKind::Daylight) => true,
IcalComponentName::Kind(IcalComponentKind::Standard) => false,
_ => return None,
};
let mut from = None;
let mut to = None;
for prop in &component.props {
let IcalPropName::Kind(kind) = prop.name else {
continue;
};
let IcalValue::UtcOffset(offset) = &prop.value else {
continue;
};
match kind {
IcalPropKind::TzOffsetFrom => from = offset.seconds(),
IcalPropKind::TzOffsetTo => to = offset.seconds(),
_ => {}
}
}
Some(Self {
daylight,
from: from?,
to: to?,
onsets: IcalRecurSet::of_component(component),
})
}
}