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,
}
}
pub fn instant(&self, local: IcalRecurDateTime) -> Option<i64> {
let offset = match self {
Self::One(offset) => *offset,
Self::Gap { .. } => return None,
Self::Fold { earlier, .. } => *earlier,
};
Some(local.seconds() - i64::from(offset))
}
}
#[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)]
pub struct IcalTzTransition {
pub local: IcalRecurDateTime,
pub from: i32,
pub to: i32,
}
impl IcalTzTransition {
pub fn after_local(&self) -> IcalRecurDateTime {
IcalRecurDateTime::from_seconds(
self.local.seconds() + i64::from(self.to) - i64::from(self.from),
)
}
}
#[derive(Clone, Debug)]
pub struct IcalTzTransitions {
zone: IcalTz,
transitions: Vec<IcalTzTransition>,
through: Option<i32>,
span: i32,
}
impl IcalTzTransitions {
pub fn of_zone(zone: IcalTz) -> Self {
Self {
zone,
transitions: Vec::new(),
through: None,
span: 1,
}
}
pub fn resolve(&mut self, local: IcalRecurDateTime) -> IcalTzOffset {
if self.through.is_none_or(|through| local.year > through) {
let through = local.year.saturating_add(self.span);
self.transitions = self.zone.transitions(through);
self.through = Some(through);
self.span = self.span.saturating_mul(2);
}
IcalTz::offset(&self.transitions, local)
}
pub fn is_gap(&mut self, local: IcalRecurDateTime) -> bool {
matches!(self.resolve(local), IcalTzOffset::Gap { .. })
}
}
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 {
Self::offset(&self.transitions(local.year), local)
}
pub fn is_gap(&self, local: IcalRecurDateTime) -> bool {
matches!(self.resolve(local), IcalTzOffset::Gap { .. })
}
pub fn transitions(&self, through: i32) -> Vec<IcalTzTransition> {
let mut transitions = Vec::new();
for observance in &self.observances {
for onset in observance.onsets.expand() {
let transition = IcalTzTransition {
local: onset.start,
from: observance.from,
to: observance.to,
};
transitions.push(transition);
if transition.local.year > through {
break;
}
}
}
transitions.sort_unstable_by_key(|transition| transition.local);
transitions
}
fn offset(transitions: &[IcalTzTransition], local: IcalRecurDateTime) -> IcalTzOffset {
let index = transitions.partition_point(|transition| transition.local <= local);
let previous = index.checked_sub(1).map(|index| transitions[index]);
let next = transitions.get(index).copied();
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, transitions.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),
})
}
}