#[derive(Debug, Clone)]
pub struct IshikawaNode {
pub text: String,
pub children: Vec<IshikawaNode>,
}
impl IshikawaNode {
pub fn new(text: impl Into<String>) -> Self {
IshikawaNode {
text: text.into(),
children: Vec::new(),
}
}
}
pub struct IshikawaDiagram {
#[allow(dead_code)]
pub title: Option<String>,
pub root: Option<IshikawaNode>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<IshikawaDiagram> {
let mut title: Option<String> = None;
let mut header_seen = false;
let mut entries: Vec<(usize, String)> = Vec::new();
for raw_line in input.lines() {
let trimmed = raw_line.trim();
if trimmed.is_empty() || trimmed.starts_with("%%") || trimmed.starts_with('%') {
continue;
}
if !header_seen {
let lower = trimmed.to_lowercase();
if lower == "fishbone"
|| lower == "ishikawa"
|| lower.starts_with("fishbone ")
|| lower.starts_with("ishikawa ")
|| lower.starts_with("ishikawa-")
{
header_seen = true;
continue;
}
continue;
}
if trimmed.len() >= 5
&& trimmed[..5].eq_ignore_ascii_case("title")
&& (trimmed.len() == 5
|| trimmed.as_bytes()[5] == b' '
|| trimmed.as_bytes()[5] == b'\t')
{
if trimmed.len() > 5 {
title = Some(trimmed[5..].trim().to_string());
}
continue;
}
let indent = raw_line.len() - raw_line.trim_start_matches([' ', '\t']).len();
entries.push((indent, trimmed.to_string()));
}
if entries.is_empty() {
return crate::error::ParseResult::ok(IshikawaDiagram { title, root: None });
}
let n = entries.len();
let mut nodes: Vec<IshikawaNode> = entries
.iter()
.map(|(_, t)| IshikawaNode::new(t.clone()))
.collect();
let mut parent_index: Vec<usize> = vec![usize::MAX; n];
let mut stack: Vec<(usize, usize)> = Vec::new();
for i in 0..n {
let (level, _) = entries[i];
if i == 0 {
stack.push((level, 0));
continue;
}
while stack.len() > 1 {
let top_level = stack.last().unwrap().0;
if top_level >= level {
stack.pop();
} else {
break;
}
}
let parent_idx = if stack.is_empty() {
0 } else {
let top_level = stack.last().unwrap().0;
if top_level >= level {
0
} else {
stack.last().unwrap().1
}
};
parent_index[i] = parent_idx;
stack.push((level, i));
}
let mut children_lists: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, &p) in parent_index
.iter()
.enumerate()
.skip(1)
.take(n.saturating_sub(1))
{
children_lists[p].push(i);
}
fn assemble(idx: usize, nodes: &mut Vec<IshikawaNode>, children_lists: &[Vec<usize>]) {
let child_indices = children_lists[idx].clone();
for ci in child_indices {
assemble(ci, nodes, children_lists);
let child = std::mem::replace(&mut nodes[ci], IshikawaNode::new(String::new()));
nodes[idx].children.push(child);
}
}
assemble(0, &mut nodes, &children_lists);
crate::error::ParseResult::ok(IshikawaDiagram {
title,
root: Some(nodes.remove(0)),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_fishbone() {
let input = "fishbone\n Equipment failure\n Worn parts\n Bearings\n Calibration\n Human error\n Training\n";
let d = parse(input).diagram;
let root = d.root.unwrap();
assert_eq!(root.text, "Equipment failure");
assert_eq!(root.children.len(), 3);
assert_eq!(root.children[0].text, "Worn parts");
assert_eq!(root.children[0].children.len(), 1);
assert_eq!(root.children[0].children[0].text, "Bearings");
assert_eq!(root.children[1].text, "Calibration");
assert_eq!(root.children[2].text, "Human error");
assert_eq!(root.children[2].children[0].text, "Training");
}
#[test]
fn with_title() {
let input = "ishikawa\n title My Diagram\n Effect\n Cause A\n";
let d = parse(input).diagram;
assert_eq!(d.title.as_deref(), Some("My Diagram"));
let root = d.root.unwrap();
assert_eq!(root.text, "Effect");
assert_eq!(root.children[0].text, "Cause A");
}
#[test]
fn no_children() {
let input = "fishbone\n Effect only\n";
let d = parse(input).diagram;
let root = d.root.unwrap();
assert_eq!(root.text, "Effect only");
assert!(root.children.is_empty());
}
}