use super::evaluator::LineResult;
use crate::lang::ParsedLine;
fn is_blank(line: &ParsedLine) -> bool {
line.raw.trim().is_empty()
}
pub fn section_total(results: &[LineResult], lines: &[ParsedLine], cursor_line: usize) -> f64 {
let n = lines.len();
if cursor_line >= n {
return 0.0;
}
let blank = |i: usize| lines.get(i).is_some_and(is_blank);
if blank(cursor_line)
&& ((cursor_line > 0 && blank(cursor_line - 1))
|| (cursor_line + 1 < n && blank(cursor_line + 1)))
{
return 0.0;
}
let Some(pos) = (0..=cursor_line).rev().take(2).find(|&i| !blank(i)) else {
return 0.0;
};
let mut start = pos;
while start > 0 && !blank(start - 1) {
start -= 1;
}
let mut end = pos;
while end + 1 < n && !blank(end + 1) {
end += 1;
}
if end + 1 < n && (end + 2 == n || !blank(end + 2)) {
end += 1;
}
results
.iter()
.take(end + 1)
.skip(start)
.filter(|r| r.error.is_none() && !r.used_sum)
.filter_map(|r| r.value)
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::Evaluator;
use crate::lang::parse_all;
fn total(lines: &[&str], cursor: usize) -> f64 {
let parsed = parse_all(lines);
let results = Evaluator::new().evaluate_all(&parsed);
section_total(&results, &parsed, cursor)
}
#[test]
fn calculate_total_table() {
let two_sections = [
"# Test One",
"one=5000",
"two=3000",
"",
"# Test Two",
"three=10000",
"four=5000",
];
let cases: Vec<(&str, Vec<&str>, usize, f64)> = vec![
("cursor in section 1", two_sections.to_vec(), 1, 8000.0),
("cursor in section 2", two_sections.to_vec(), 5, 15000.0),
(
"cursor on single trailing blank of section 1",
two_sections.to_vec(),
3,
8000.0,
),
(
"cursor on multiple consecutive blanks",
vec![
"# Test One",
"one=5000",
"two=3000",
"",
"",
"# Test Two",
"three=10000",
],
3,
0.0,
),
(
"cursor on second of multiple blanks",
vec!["# Test One", "one=5000", "", "", "# Test Two"],
3,
0.0,
),
(
"comment-only section",
vec!["# Comment only", "", "value=100"],
0,
0.0,
),
("single line section", vec!["value=100"], 0, 100.0),
(
"section at end without trailing blank",
vec!["one=100", "", "two=200", "three=300"],
3,
500.0,
),
(
"section at start",
vec!["one=100", "two=200", "", "three=300"],
0,
300.0,
),
("empty document", vec![""], 0, 0.0),
(
"section with mixed comments and values",
vec![
"# Header",
"one=100",
"# Middle comment",
"two=200",
"",
"other=999",
],
2,
300.0,
),
];
for (name, lines, cursor, want) in cases {
assert_eq!(total(&lines, cursor), want, "{name}");
}
}
#[test]
fn sum_lines_are_not_double_counted() {
let lines = [
"90 # Internet",
"25 # Mint Mobile",
"85 # Dog food",
"bills=sum",
];
for cursor in 0..lines.len() {
assert_eq!(total(&lines, cursor), 200.0, "cursor {cursor}");
}
assert_eq!(total(&["100", "200", "sum"], 0), 300.0);
assert_eq!(total(&["100", "200", "left=500-sum"], 1), 300.0);
}
#[test]
fn errors_are_skipped() {
assert_eq!(total(&["100", "x/0", "y", "50"], 0), 150.0);
}
#[test]
fn edge_cursors() {
assert_eq!(total(&[], 0), 0.0);
assert_eq!(total(&["1"], 5), 0.0);
assert_eq!(total(&["", "1"], 0), 0.0);
assert_eq!(total(&["1", "2", ""], 2), 3.0);
assert_eq!(total(&["1", " ", "2"], 2), 2.0);
}
}