feat(rad): preserve airways in mandatory transitions + anchor-on-entry
The transition parser dropped airways, so mandate splices filed all-DCT (MOVUM DCT HAREM) — wrong routing, and the arrival splice self-looped (ROUTE49) when the enroute already crossed the arrival area. - rad: expand_via keeps airway tokens (is_airway_tok); a transition is now a point/airway token sequence (DCT where no airway), e.g. [MOVUM, T109, HAREM, T104, WLD]. - discover: transition_legs() builds legs with the interspersed airways. route_via_arrival_transition anchors on the transition ENTRY (first on-airway point) and flies the transition through once — the enroute leg stops before the arrival area, so no self-loop. route_via_transition and the est/near sorts use the last/first *point* (skip airways). Files the mandate correctly now (… MOVUM T109 HAREM T104 WLD DCT EDDM). Coverage 6/10 held, no regression (LFPG-EDDF still passes); LSZH-EGLL 6→4 PROF204; EGLL-EDDM's fake 1-err self-loop is replaced by its real residuals (CPT dep forbidden, forbidden points on the mandated T104, WLD STAR limit). 49 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-6
@@ -221,18 +221,24 @@ fn via_sequences(up: &str) -> Vec<Vec<String>> {
|
||||
/// `(A, B, …)` alternative group into one sequence per branch (`RANUX DCT VALEK
|
||||
/// DCT (LUTAX, LIPNI)` → `[RANUX,VALEK,LUTAX]`, `[RANUX,VALEK,LIPNI]`).
|
||||
fn expand_via(inner: &str) -> Vec<Vec<String>> {
|
||||
let points = |s: &str| -> Vec<String> {
|
||||
s.split(|c: char| !c.is_ascii_alphanumeric()).filter(|t| is_point(t)).map(str::to_owned).collect()
|
||||
// Keep points AND airway designators (in order); `DCT`/keywords are dropped so
|
||||
// consecutive points with no airway between them are an implicit DCT. This lets
|
||||
// the splice file `MOVUM T109 HAREM T104 WLD` with its airways, not all-DCT.
|
||||
let tokens = |s: &str| -> Vec<String> {
|
||||
s.split(|c: char| !c.is_ascii_alphanumeric())
|
||||
.filter(|t| is_point(t) || is_airway_tok(t))
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
};
|
||||
if let (Some(op), Some(rel)) = (inner.find('('), inner.find('(').and_then(|o| inner[o..].find(')'))) {
|
||||
let cp = inner.find('(').unwrap() + rel;
|
||||
let (before, group, after) = (&inner[..op], &inner[op + 1..cp], &inner[cp + 1..]);
|
||||
let prefix = points(before);
|
||||
let tail = points(after);
|
||||
let prefix = tokens(before);
|
||||
let tail = tokens(after);
|
||||
let mut out = Vec::new();
|
||||
for alt in group.split(',') {
|
||||
let mut seq = prefix.clone();
|
||||
seq.extend(points(alt));
|
||||
seq.extend(tokens(alt));
|
||||
seq.extend(tail.clone());
|
||||
if !seq.is_empty() {
|
||||
out.push(seq);
|
||||
@@ -240,10 +246,18 @@ fn expand_via(inner: &str) -> Vec<Vec<String>> {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let pts = points(inner);
|
||||
let pts = tokens(inner);
|
||||
if pts.is_empty() { Vec::new() } else { vec![pts] }
|
||||
}
|
||||
|
||||
/// An airway designator token: starts with a letter and contains a digit (`N871`,
|
||||
/// `T109`, `UL607`, `Z66`). Distinguishes airways from points in a transition seq.
|
||||
fn is_airway_tok(t: &str) -> bool {
|
||||
(2..=6).contains(&t.len())
|
||||
&& t.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
|
||||
&& t.chars().any(|c| c.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// An enroute point token (2–5 letters, not a RAD keyword or airway designator).
|
||||
fn is_point(t: &str) -> bool {
|
||||
(2..=5).contains(&t.len())
|
||||
|
||||
Reference in New Issue
Block a user