use mig_types::segment::OwnedSegment;
use super::timezone::{dst_transitions_between, parse_dtm303};
use super::{ConditionResult, EvaluationContext};
const SG10_PATH: [&str; 4] = ["SG5", "SG6", "SG9", "SG10"];
pub fn condition_130(ctx: &EvaluationContext) -> ConditionResult {
evaluate(ctx, |gap| gap.holds_130())
}
pub fn condition_133(ctx: &EvaluationContext) -> ConditionResult {
evaluate(ctx, |gap| gap.holds_133())
}
fn evaluate(ctx: &EvaluationContext, scoped: impl Fn(&ReadingDates) -> bool) -> ConditionResult {
let in_sg10 = ctx
.scope
.is_some_and(|scope| scope.path.iter().any(|(id, _)| *id == "SG10"));
if in_sg10 && ctx.navigator.is_some() {
let dtms = ctx.scoped_find_segments_in("SG10", "DTM");
return ConditionResult::from(scoped(&ReadingDates::of(&dtms)));
}
let Some(nav) = ctx.navigator() else {
return ConditionResult::Unknown;
};
let count = nav.group_instance_count(&SG10_PATH);
if count == 0 {
return ConditionResult::Unknown;
}
let all_hold = (0..count).all(|i| {
let gap = ReadingDates::of(&nav.find_segments_in_group("DTM", &SG10_PATH, i));
gap.holds_130() || gap.holds_133()
});
ConditionResult::from(all_hold)
}
struct ReadingDates {
dtm7: Option<String>,
partners_303: Vec<String>,
dtm9_102: Option<String>,
}
impl ReadingDates {
fn of(dtms: &[OwnedSegment]) -> Self {
let mut dates = ReadingDates {
dtm7: None,
partners_303: Vec::new(),
dtm9_102: None,
};
for dtm in dtms {
let Some(c507) = dtm.elements.first() else {
continue;
};
let part = |i: usize| c507.get(i).map(String::as_str).unwrap_or("");
let (qualifier, value, format) = (part(0), part(1), part(2));
if value.is_empty() {
continue;
}
match (qualifier, format) {
("7", _) => dates.dtm7 = dates.dtm7.take().or(Some(value.to_string())),
("9", "102") => dates.dtm9_102 = Some(value.to_string()),
("60" | "9", _) => dates.partners_303.push(value.to_string()),
_ => {}
}
}
dates
}
fn partners_303_hold(&self) -> bool {
let Some(dtm7) = self.dtm7.as_deref().and_then(parse_dtm303) else {
return true;
};
self.partners_303.iter().all(|partner| {
let Some(partner) = parse_dtm303(partner) else {
return true;
};
let bound = match dst_transitions_between(dtm7, partner).signum() {
-1 => 25 * 60,
1 => 23 * 60,
_ => 24 * 60,
};
(dtm7 - partner).abs() < bound
})
}
fn holds_130(&self) -> bool {
self.dtm9_102.is_none() && self.partners_303_hold()
}
fn holds_133(&self) -> bool {
let (Some(dtm7), Some(dtm9)) = (self.dtm7.as_deref(), self.dtm9_102.as_deref()) else {
return false;
};
let days_apart = match (day_number(dtm7), day_number(dtm9)) {
(Some(a), Some(b)) => (a - b).abs(),
_ => 0,
};
days_apart <= 1 && self.partners_303_hold()
}
}
fn day_number(value: &str) -> Option<i64> {
let date = value.get(..8)?;
if !date.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let (y, m, d): (i64, i64, i64) = (
date[..4].parse().ok()?,
date[4..6].parse().ok()?,
date[6..8].parse().ok()?,
);
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
let a = (14 - m) / 12;
let (y, m) = (y + 4800 - a, m + 12 * a - 3);
Some(d + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045)
}
#[cfg(test)]
mod tests {
use super::*;
fn dtm(qualifier: &str, value: &str, format: &str) -> OwnedSegment {
OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![qualifier.into(), value.into(), format.into()]],
segment_number: 0,
}
}
fn dates(dtms: &[OwnedSegment]) -> (bool, bool) {
let gap = ReadingDates::of(dtms);
(gap.holds_130(), gap.holds_133())
}
#[test]
fn a_lone_reading_holds_130() {
assert_eq!(dates(&[dtm("7", "202411142300", "303")]), (true, false));
}
#[test]
fn a_dtm60_two_days_off_holds_neither() {
let dtms = [
dtm("7", "202609302200", "303"),
dtm("60", "202609282200", "303"),
];
assert_eq!(dates(&dtms), (false, false));
}
#[test]
fn a_dtm9_of_format_102_is_left_to_133() {
let next_day = [dtm("9", "20241115", "102"), dtm("7", "202411142300", "303")];
assert_eq!(dates(&next_day), (false, true));
let three_days = [dtm("9", "20241111", "102"), dtm("7", "202411142300", "303")];
assert_eq!(dates(&three_days), (false, false));
}
#[test]
fn the_bound_follows_the_dst_switch() {
let autumn = [
dtm("7", "202410272300", "303"),
dtm("60", "202410262230", "303"),
];
assert!(dates(&autumn).0);
let spring = [
dtm("7", "202403312200", "303"),
dtm("60", "202403302230", "303"),
];
assert!(!dates(&spring).0);
}
}