use mf2_runtime::{DateTime, DateTimeOptions, ErrorSink, FnContext, FormatError, Host, ZoneOption};
const DAY_MS: i64 = 86_400_000;
pub(crate) fn wall_ms(d: &DateTime<'_>) -> i64 {
d.date.days_since_epoch() * DAY_MS + d.time.ms_of_day()
}
fn offset_at(host: &dyn Host, zone: &str, t: i64) -> Option<i32> {
host.zone_offset(zone, t)
.filter(|o| o.unsigned_abs() < 86_400)
}
pub(crate) fn search(host: &dyn Host, zone: &str, wall: i64) -> Option<(i64, i32)> {
let fits = |o: i32| offset_at(host, zone, wall - i64::from(o) * 1000) == Some(o);
let before = offset_at(host, zone, wall - DAY_MS)?;
let after = offset_at(host, zone, wall + DAY_MS)?;
let (hi, lo) = if before >= after {
(before, after)
} else {
(after, before)
};
let chosen = if fits(hi) {
hi
} else if hi != lo && fits(lo) {
lo
} else if hi == lo {
let mid = offset_at(host, zone, wall - i64::from(before) * 1000)?;
if fits(mid) { mid } else { before }
} else {
before
};
let instant = wall - i64::from(chosen) * 1000;
Some((instant, offset_at(host, zone, instant)?))
}
enum At<'s> {
Floating,
Offset(i32),
Named(&'s str),
}
fn in_context(d: &DateTime<'_>) -> bool {
d.options.is_resolved() && d.options.time_zone.is_none()
}
fn at<'s>(d: &DateTime<'s>, context: ZoneOption<'s>) -> At<'s> {
match (d.offset, d.zone) {
(Some(o), _) => At::Offset(o),
(None, Some(z)) => At::Named(z),
(None, None) if in_context(d) => match context {
ZoneOption::Offset(o) => At::Offset(o),
ZoneOption::Named(n) => At::Named(n),
_ => At::Offset(0),
},
(None, None) => At::Floating,
}
}
fn zone_name<'s>(d: &DateTime<'s>, context: ZoneOption<'s>) -> Option<&'s str> {
match (d.zone, context) {
(Some(z), _) => Some(z),
(None, ZoneOption::Named(n)) if in_context(d) => Some(n),
_ => None,
}
}
enum Input<'a> {
Zone(ZoneOption<'a>),
Context,
Floating,
}
fn own_zone<'a>(d: &DateTime<'a>) -> Input<'a> {
if d.options.is_resolved() {
match d.options.time_zone {
None => return Input::Context,
Some(ZoneOption::Input) => {}
Some(z) => return Input::Zone(z),
}
}
match (d.zone, d.offset) {
(Some(z), _) => Input::Zone(ZoneOption::Named(z)),
(None, Some(0)) => Input::Zone(ZoneOption::Utc),
(None, Some(o)) => Input::Zone(ZoneOption::Offset(o)),
(None, None) => Input::Floating,
}
}
fn moved<'a>(
t: i64,
offset: i32,
zone: Option<&'a str>,
options: DateTimeOptions<'a>,
) -> Result<DateTime<'a>, FormatError> {
let mut d = DateTime::from_epoch_ms(t + i64::from(offset) * 1000)
.ok_or(FormatError::BadOperand)?
.with_offset(offset)
.ok_or(FormatError::BadOption)?;
d.zone = zone;
d.options = options;
Ok(d)
}
pub(crate) fn place<'a>(
cx: &FnContext<'_>,
d: &mut DateTime<'a>,
option: Option<ZoneOption<'a>>,
errs: &mut dyn ErrorSink,
) -> Result<Option<ZoneOption<'a>>, FormatError> {
let host = cx.host();
let context = cx.time_zone().as_option();
let (target, stored) = match option {
Some(ZoneOption::Input) => match own_zone(d) {
Input::Zone(z) => (z, Some(z)),
Input::Context => (context, None),
Input::Floating => {
errs.error(FormatError::BadOperand);
(context, None)
}
},
Some(z) => (z, Some(z)),
None => (context, None),
};
let keep = match stored {
Some(ZoneOption::Named(n)) => Some(n),
_ => None,
};
let wall = wall_ms(d);
if let ZoneOption::Named(n) = target
&& zone_name(d, context) == Some(n)
{
match d.offset {
None => {
if let Some((t, o)) = search(host, n, wall) {
*d = moved(t, o, d.zone, d.options)?;
}
}
Some(o) => {
let t = wall - i64::from(o) * 1000;
if let Some(at) = offset_at(host, n, t)
&& at != o
{
*d = moved(t, at, d.zone, d.options)?;
}
}
}
if keep.is_some() {
d.zone = keep;
}
return Ok(stored);
}
let instant = match at(d, context) {
At::Floating => {
match target {
ZoneOption::Offset(o) => d.offset = Some(o),
ZoneOption::Named(n) => match search(host, n, wall) {
Some((t, o)) => *d = moved(t, o, None, d.options)?,
None => d.offset = None,
},
ZoneOption::Utc | ZoneOption::Input => d.offset = Some(0),
_ => return Err(FormatError::UnsupportedOperation),
}
d.zone = keep;
return Ok(stored);
}
At::Offset(o) => wall - i64::from(o) * 1000,
At::Named(z) => search(host, z, wall).ok_or(FormatError::BadOption)?.0,
};
let offset = match target {
ZoneOption::Offset(o) => o,
ZoneOption::Named(n) => offset_at(host, n, instant).ok_or(FormatError::BadOption)?,
ZoneOption::Utc | ZoneOption::Input => 0,
_ => return Err(FormatError::UnsupportedOperation),
};
*d = moved(instant, offset, keep, d.options)?;
Ok(stored)
}