ledger.rs

4.4 kB · rust · 173 lines

1use std::collections::HashMap;23#[derive(Clone, Copy)]4pub enum Tag {5    Proved,6    Verified,7    Conjecture,8    Refuted,9}1011pub struct Row {12    pub key: String,13    pub tag: Tag,14    pub witnesses: Vec<String>,15}1617pub struct Book {18    pub rows: Vec<Row>,19    pub parts: usize,20    pub untagged: usize,21}2223const TAGS: [(&str, Tag); 4] = [24    ("[Proved] ", Tag::Proved),25    ("[Verified] ", Tag::Verified),26    ("[Conjecture] ", Tag::Conjecture),27    ("[Refuted] ", Tag::Refuted),28];2930const SEED: usize = 6;3132struct Draft {33    section: String,34    words: Vec<String>,35    tag: Tag,36    witnesses: Vec<String>,37    line: usize,38    text: String,39}4041// PARSE4243pub fn parse(text: &str) -> Result<Book, String> {44    let mut parts = 0;45    let mut untagged = 0;46    let mut section = String::new();47    let mut drafts = Vec::new();48    for (index, line) in text.lines().enumerate() {49        if let Some(heading) = line.strip_prefix("# ") {50            parts += 1;51            section = slug(heading);52        } else if let Some(body) = line.strip_prefix("- ") {53            if section.is_empty() {54                continue;55            }56            match tagged(body) {57                Some((tag, rest)) => drafts.push(draft(&section, tag, rest, index + 1, line)),58                None => untagged += 1,59            }60        }61    }62    Ok(Book {63        rows: key(drafts)?,64        parts,65        untagged,66    })67}6869fn tagged(body: &str) -> Option<(Tag, &str)> {70    let body = dated(body)?;71    TAGS.iter()72        .find_map(|(mark, tag)| body.strip_prefix(mark).map(|rest| (*tag, rest)))73}7475fn dated(body: &str) -> Option<&str> {76    let (date, rest) = body.split_once(' ')?;77    let digits = date.bytes().filter(u8::is_ascii_digit).count();78    (date.len() == 10 && digits == 8 && date.matches('-').count() == 2).then_some(rest)79}8081fn draft(section: &str, tag: Tag, rest: &str, line: usize, text: &str) -> Draft {82    let (claim, witnesses) = match rest.split_once(" Witness: ") {83        Some((claim, field)) => (claim, witnesses(field)),84        None => (rest, Vec::new()),85    };86    Draft {87        section: section.to_string(),88        words: claim.split_whitespace().map(str::to_string).collect(),89        tag,90        witnesses,91        line,92        text: text.to_string(),93    }94}9596fn witnesses(field: &str) -> Vec<String> {97    field98        .trim_end_matches('.')99        .split([',', ';'])100        .map(|token| token.trim().trim_matches('`').to_string())101        .filter(|token| !token.is_empty())102        .collect()103}104105// KEY106107fn key(drafts: Vec<Draft>) -> Result<Vec<Row>, String> {108    let mut widths: Vec<usize> = drafts109        .iter()110        .map(|draft| SEED.min(draft.words.len()))111        .collect();112    loop {113        let mut seen: HashMap<String, Vec<usize>> = HashMap::new();114        for (index, draft) in drafts.iter().enumerate() {115            seen.entry(name(draft, widths[index]))116                .or_default()117                .push(index);118        }119        let mut clash = false;120        for (name, group) in seen {121            if group.len() < 2 {122                continue;123            }124            clash = true;125            let mut room = false;126            for &index in &group {127                if widths[index] < drafts[index].words.len() {128                    widths[index] += 1;129                    room = true;130                }131            }132            if !room {133                let lines: Vec<String> = group134                    .iter()135                    .map(|&index| format!("{}: {}", drafts[index].line, drafts[index].text))136                    .collect();137                return Err(format!("duplicate key {name}\n{}", lines.join("\n")));138            }139        }140        if !clash {141            break;142        }143    }144    Ok(drafts145        .into_iter()146        .zip(widths)147        .map(|(draft, width)| Row {148            key: name(&draft, width),149            tag: draft.tag,150            witnesses: draft.witnesses,151        })152        .collect())153}154155fn name(draft: &Draft, width: usize) -> String {156    format!(157        "{}/{}",158        draft.section,159        slug(&draft.words[..width].join(" "))160    )161}162163fn slug(text: &str) -> String {164    let mut out = String::new();165    for letter in text.chars() {166        if letter.is_ascii_alphanumeric() {167            out.push(letter.to_ascii_lowercase());168        } else if !out.ends_with('-') {169            out.push('-');170        }171    }172    out.trim_matches('-').to_string()173}