use super::{
find_header, first_word, parse_number, read_quoted, Envelope, ParseError, Preamble, Source,
TitleSyntax,
};
pub const KEYWORD: &str = "pie";
#[derive(Debug, Clone, PartialEq)]
pub struct Slice {
pub label: String,
pub value: f64,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Pie {
pub preamble: Preamble,
pub slices: Vec<Slice>,
pub show_data: bool,
}
impl Pie {
pub fn total(&self) -> f64 {
self.slices.iter().map(|s| s.value).sum()
}
}
pub fn is_pie(src: &str) -> bool {
find_header(src, &[KEYWORD]).is_some()
}
pub fn parse(src: &str) -> Result<Pie, ParseError> {
let Some(Source {
lines,
header_index,
header_rest,
front_matter_title,
}) = find_header(src, &[KEYWORD])
else {
return Err(ParseError::NotThisChart {
expected: KEYWORD,
header: first_word(src),
});
};
let mut pie = Pie::default();
let mut env = Envelope::new(front_matter_title);
let mut rest = header_rest.as_str();
if let Some(after) = super::strip_ci(rest, "showData") {
if after.trim().is_empty() || after.starts_with([' ', '\t']) {
pie.show_data = true;
rest = after.trim();
}
}
if !rest.is_empty() {
read_line(&mut pie, &mut env, rest, header_index + 1)?;
}
for (i, line) in lines.iter().enumerate().skip(header_index + 1) {
read_line(&mut pie, &mut env, line, i + 1)?;
}
pie.preamble = env.preamble;
if pie.slices.is_empty() {
return Err(ParseError::NoData {
kind: KEYWORD,
wanted: "slice",
});
}
Ok(pie)
}
fn read_line(
pie: &mut Pie,
env: &mut Envelope,
line: &str,
number: usize,
) -> Result<(), ParseError> {
if env.read(line, TitleSyntax::Terminal) {
return Ok(());
}
let t = line.trim();
if t.is_empty() {
return Ok(());
}
let chars: Vec<char> = t.chars().collect();
let Some((label, after)) = read_quoted(&chars, 0) else {
return Err(ParseError::Unexpected {
kind: KEYWORD,
line: number,
text: t.to_string(),
});
};
let tail: String = chars[after..].iter().collect();
let Some(value_text) = tail.trim_start().strip_prefix(':') else {
return Err(ParseError::Unexpected {
kind: KEYWORD,
line: number,
text: t.to_string(),
});
};
let value_text = value_text.trim();
let Some(value) = parse_number(value_text, true).filter(|_| !value_text.starts_with('+'))
else {
return Err(ParseError::BadNumber {
line: number,
text: value_text.to_string(),
why: "a slice's value must be a number",
});
};
if value < 0.0 {
return Err(ParseError::BadNumber {
line: number,
text: value_text.to_string(),
why: "a slice's value must not be negative",
});
}
if !pie.slices.iter().any(|s| s.label == label) {
pie.slices.push(Slice { label, value });
}
Ok(())
}