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(§ion, 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}