Skip to main content

frazaro_core/reflect/
histogram.rs

1//! `frazaro reflect --functions` (KERNEL.6's instrument, built with
2//! KERNEL.7, 2026-10-08): a histogram of the functions a workbook's formulas
3//! call, counts alone.
4//!
5//! The roadmap's `KERNEL.6` asks that the declared subset be chosen by
6//! measurement over real workbooks, the Enron corpus and the house's own
7//! models, and never by taste; this sink is the instrument. One `function`
8//! row per name, the number of formula cells calling it (a cell calling
9//! `SUM` twice counts once), sorted by count then by name; nothing of the
10//! workbook's contents leaves the machine, AXM.1's discipline, so it can be
11//! run over anyone's models. A script sums the rows over a folder.
12//!
13//! ```text
14//! (function "SUM" 1365)
15//! (function "IF" 912)
16//! ```
17
18use std::collections::BTreeMap;
19
20use vla_lang::calc::calls;
21use vla_lang::rows::{quoted, Row, Sink};
22
23use super::open;
24use crate::messages::Refusal;
25
26/// The functions called, by name, each the number of formula cells.
27#[derive(Debug, Default)]
28pub struct Histogram {
29    pub counts: BTreeMap<String, u64>,
30    pub formulas: u64,
31}
32
33impl Sink for Histogram {
34    fn row(&mut self, row: &Row<'_>) {
35        if let Row::Formula { text, .. } = row {
36            self.formulas += 1;
37            let mut names = calls(text);
38            names.sort_unstable();
39            names.dedup();
40            for name in names {
41                *self.counts.entry(name).or_insert(0) += 1;
42            }
43        }
44    }
45}
46
47impl Histogram {
48    /// The rows, most called first, then by name.
49    pub fn text(&self) -> String {
50        let mut rows: Vec<(&String, &u64)> = self.counts.iter().collect();
51        rows.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
52        let mut out = String::new();
53        for (name, count) in rows {
54            out.push_str(&format!("(function {} {count})\n", quoted(name)));
55        }
56        out
57    }
58}
59
60/// The histogram of a workbook's file, every sheet walked.
61pub fn histogram(bytes: &[u8], label: &str) -> Result<Histogram, Refusal> {
62    let package = open(bytes, label)?;
63    let mut h = Histogram::default();
64    for i in 0..package.sheets().len() {
65        package.walk_sheet(i, &mut h)?;
66    }
67    Ok(h)
68}
69
70/// The histogram as text.
71pub fn histogram_text(bytes: &[u8], label: &str) -> Result<String, Refusal> {
72    Ok(histogram(bytes, label)?.text())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    const FIXTURE: &[u8] = include_bytes!("../../../scripts/reflect/fixture.xlsx");
80
81    #[test]
82    fn the_fixture_s_functions_are_counted_once_per_cell() {
83        let h = histogram(FIXTURE, "fixture.xlsx").unwrap();
84        assert_eq!(h.counts.get("SUM"), Some(&3), "{:?}", h.counts);
85        assert_eq!(h.counts.get("INDIRECT"), Some(&1));
86        assert_eq!(h.counts.get("OFFSET"), Some(&1));
87        assert_eq!(h.counts.get("SEQUENCE"), Some(&1));
88        let text = h.text();
89        assert!(text.starts_with("(function \"SUM\" 3)\n"), "{text}");
90        assert!(h.formulas >= 25);
91    }
92}