use crate::Segment;
use smallvec::SmallVec;
use std::ops::Range;
#[derive(Debug, Clone, Copy)]
pub struct GroupDef<'a> {
pub name: &'a str,
pub trigger: &'a str,
pub children: &'a [GroupDef<'a>],
}
impl<'a> GroupDef<'a> {
#[must_use]
pub const fn new(name: &'a str, trigger: &'a str) -> Self {
Self {
name,
trigger,
children: &[],
}
}
#[must_use]
pub const fn with_children(
name: &'a str,
trigger: &'a str,
children: &'a [GroupDef<'a>],
) -> Self {
Self {
name,
trigger,
children,
}
}
}
#[derive(Debug)]
pub struct SegmentGroupIndexed<'a> {
pub definition: &'a str,
pub total_span: Range<usize>,
pub children: Vec<SegmentGroupIndexed<'a>>,
pub occurrence_index: usize,
}
impl<'a> SegmentGroupIndexed<'a> {
pub fn direct_segment_indices(&self) -> impl Iterator<Item = usize> + '_ {
self.total_span.clone().filter(|i| {
!self
.children
.iter()
.any(|child| child.total_span.contains(i))
})
}
#[must_use]
pub fn segments<'s, 'd>(&self, all: &'s [Segment<'d>]) -> &'s [Segment<'d>] {
all.get(self.total_span.clone()).unwrap_or(&[])
}
pub fn descendants(&self) -> Descendants<'_, 'a> {
Descendants { stack: vec![self] }
}
pub fn find<'q>(
&'q self,
name: &'q str,
) -> impl Iterator<Item = &'q SegmentGroupIndexed<'a>> + 'q {
self.descendants().filter(move |g| g.definition == name)
}
}
pub struct Descendants<'t, 'a> {
stack: Vec<&'t SegmentGroupIndexed<'a>>,
}
impl<'t, 'a> Iterator for Descendants<'t, 'a> {
type Item = &'t SegmentGroupIndexed<'a>;
fn next(&mut self) -> Option<Self::Item> {
let node = self.stack.pop()?;
self.stack.extend(node.children.iter().rev());
Some(node)
}
}
pub fn group_segments_indexed<'g>(
segments: &[Segment<'_>],
schema: &'g [GroupDef<'g>],
root_name: &'g str,
) -> SegmentGroupIndexed<'g> {
let mut root = SegmentGroupIndexed {
definition: root_name,
total_span: 0..0,
children: Vec::new(),
occurrence_index: 0,
};
group_recursive_indexed(segments, &mut root, schema, &[], 0);
root
}
fn group_recursive_indexed<'g>(
segments: &[Segment<'_>],
parent: &mut SegmentGroupIndexed<'g>,
schema: &'g [GroupDef<'g>],
stop_triggers: &[&'g str],
offset: usize,
) -> usize {
let combined_stop: SmallVec<[&'g str; 16]> = {
let mut v: SmallVec<[&'g str; 16]> = SmallVec::from_slice(stop_triggers);
for d in schema {
if !v.contains(&d.trigger) {
v.push(d.trigger);
}
}
v
};
let span_start = if !parent.total_span.is_empty() {
parent.total_span.start } else {
offset
};
let mut i = 0;
let mut occ_counts: std::collections::HashMap<&'g str, usize> =
std::collections::HashMap::new();
while i < segments.len() {
let tag = segments[i].tag();
let matched = schema.iter().find(|d| d.trigger == tag);
if matched.is_none() && stop_triggers.iter().copied().any(|t| t == tag) {
break;
}
if let Some(def) = matched {
let child_offset = offset + i;
let occ_idx = {
let c = occ_counts.entry(def.name).or_insert(0);
let idx = *c;
*c += 1;
idx
};
let mut child = SegmentGroupIndexed {
definition: def.name,
total_span: child_offset..child_offset + 1,
children: Vec::new(),
occurrence_index: occ_idx,
};
i += 1;
let consumed = group_recursive_indexed(
&segments[i..],
&mut child,
def.children,
&combined_stop,
offset + i,
);
i += consumed;
parent.children.push(child);
} else {
i += 1;
}
}
parent.total_span = span_start..(offset + i);
i
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Element;
fn seg(tag: &'static str) -> Segment<'static> {
Segment::new(tag, vec![Element::of(&["x"])])
}
static SCHEMA: &[GroupDef] = &[
GroupDef {
name: "SG1",
trigger: "NAD",
children: &[GroupDef {
name: "SG2",
trigger: "CTA",
children: &[],
}],
},
GroupDef {
name: "SG3",
trigger: "LIN",
children: &[],
},
];
static NESTED_SAME_TRIGGER: &[GroupDef] = &[
GroupDef {
name: "SG2",
trigger: "NAD",
children: &[],
},
GroupDef {
name: "SG4",
trigger: "IDE",
children: &[GroupDef {
name: "SG12",
trigger: "NAD",
children: &[],
}],
},
];
#[test]
fn a_nested_group_wins_over_an_ancestors_sibling_with_the_same_trigger() {
let segs = vec![
seg("UNH"),
seg("BGM"),
seg("NAD"),
seg("NAD"),
seg("IDE"),
seg("NAD"),
seg("DTM"),
seg("NAD"),
];
let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
let top: Vec<&str> = tree.children.iter().map(|c| c.definition).collect();
assert_eq!(
top,
["SG2", "SG2", "SG4"],
"the two message-level NADs are SG2"
);
let sg4 = tree
.children
.iter()
.find(|c| c.definition == "SG4")
.expect("SG4 opens on IDE");
assert_eq!(sg4.total_span, 4..8);
let nested: Vec<&str> = sg4.children.iter().map(|c| c.definition).collect();
assert_eq!(
nested,
["SG12", "SG12"],
"NAD inside SG4 nests as SG12 rather than reopening SG2",
);
assert_eq!(sg4.children[0].total_span, 5..7); assert_eq!(sg4.children[1].total_span, 7..8); assert_eq!(sg4.children[0].occurrence_index, 0);
assert_eq!(sg4.children[1].occurrence_index, 1);
}
#[test]
fn descendants_walk_the_whole_subtree_in_document_order() {
let segs = vec![
seg("NAD"),
seg("IDE"),
seg("NAD"),
seg("DTM"),
seg("IDE"),
seg("NAD"),
];
let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
let walked: Vec<&str> = tree.descendants().map(|g| g.definition).collect();
assert_eq!(
walked,
["ROOT", "SG2", "SG4", "SG12", "SG4", "SG12"],
"pre-order: a parent before its children, siblings in wire order",
);
let sg12: Vec<usize> = tree.find("SG12").map(|g| g.total_span.start).collect();
assert_eq!(sg12, [2, 5]);
let first = tree.find("SG12").next().unwrap();
assert_eq!(
first
.segments(&segs)
.iter()
.map(Segment::tag)
.collect::<Vec<_>>(),
["NAD", "DTM"],
);
}
#[test]
fn a_repeated_trigger_still_reopens_a_sibling_rather_than_nesting_forever() {
let segs = vec![seg("IDE"), seg("NAD"), seg("NAD"), seg("NAD")];
let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
let sg4 = &tree.children[0];
assert_eq!(sg4.definition, "SG4");
assert_eq!(
sg4.children.len(),
3,
"three sibling SG12s, not one nest of three"
);
assert!(
sg4.children.iter().all(|c| c.children.is_empty()),
"SG12 has no children, so nothing may nest inside it",
);
}
#[test]
fn a_tag_no_nested_definition_accepts_still_closes_the_group() {
let segs = vec![seg("IDE"), seg("NAD"), seg("IDE"), seg("NAD")];
let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
let top: Vec<&str> = tree.children.iter().map(|c| c.definition).collect();
assert_eq!(top, ["SG4", "SG4"]);
assert_eq!(tree.children[0].total_span, 0..2);
assert_eq!(tree.children[1].total_span, 2..4);
}
#[test]
fn nesting_is_preferred_at_every_depth() {
static DEEP: &[GroupDef] = &[
GroupDef {
name: "L1",
trigger: "NAD",
children: &[],
},
GroupDef {
name: "A",
trigger: "IDE",
children: &[GroupDef {
name: "L2",
trigger: "NAD",
children: &[GroupDef {
name: "L3",
trigger: "CTA",
children: &[],
}],
}],
},
];
let segs = vec![seg("NAD"), seg("IDE"), seg("NAD"), seg("CTA")];
let tree = group_segments_indexed(&segs, DEEP, "ROOT");
assert_eq!(tree.children[0].definition, "L1");
let a = &tree.children[1];
assert_eq!(a.definition, "A");
assert_eq!(a.children[0].definition, "L2");
assert_eq!(a.children[0].children[0].definition, "L3");
}
#[test]
fn root_segments_before_first_trigger() {
let segs = vec![seg("UNH"), seg("BGM"), seg("NAD")];
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
let direct: Vec<_> = tree.direct_segment_indices().collect();
assert_eq!(direct, vec![0, 1], "UNH + BGM should be direct in root");
assert_eq!(tree.children.len(), 1);
assert_eq!(tree.children[0].definition, "SG1");
}
#[test]
fn repeated_trigger_creates_multiple_children() {
let segs = vec![seg("UNH"), seg("NAD"), seg("NAD"), seg("UNT")];
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
assert_eq!(
tree.children
.iter()
.filter(|c| c.definition == "SG1")
.count(),
2
);
}
#[test]
fn repeated_trigger_occurrence_index_is_stamped() {
let segs = vec![seg("NAD"), seg("NAD"), seg("NAD")];
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
let indices: Vec<_> = tree.children.iter().map(|c| c.occurrence_index).collect();
assert_eq!(indices, vec![0, 1, 2]);
}
#[test]
fn nested_child_groups() {
let segs = vec![seg("NAD"), seg("CTA"), seg("CTA")];
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
let sg1 = &tree.children[0];
assert_eq!(sg1.definition, "SG1");
assert_eq!(sg1.children.len(), 2);
assert!(sg1.children.iter().all(|c| c.definition == "SG2"));
}
#[test]
fn total_span_covers_all_segments() {
let segs = vec![seg("UNH"), seg("NAD"), seg("CTA")];
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
let all_tags: Vec<_> = segs[tree.total_span.clone()]
.iter()
.map(|s| s.tag())
.collect();
assert!(all_tags.contains(&"UNH"));
assert!(all_tags.contains(&"NAD"));
assert!(all_tags.contains(&"CTA"));
}
}