use crate::blocks::{Block, BlockKind};
const DOT_LEADER: usize = 4;
const CURRENCY: [char; 4] = ['$', '\u{20ac}', '\u{a3}', '\u{a5}'];
pub(crate) fn tidy_cells(blocks: &mut [Block]) {
for block in blocks {
if !matches!(block.kind, BlockKind::TableCell { .. }) {
continue;
}
let tidy = tidy(&block.text);
if tidy != block.text {
block.text = tidy;
}
}
}
pub(crate) fn tidy(text: &str) -> String {
join_currency(&strip_leaders(text))
}
fn strip_leaders(text: &str) -> String {
if !text.contains('.') && !text.contains('\u{2026}') {
return text.to_string();
}
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '.' || chars[i] == '\u{2026}' {
let mut j = i;
let mut stops = 0usize;
let mut last_stop = i;
while j < chars.len() {
match chars[j] {
'.' => {
stops += 1;
last_stop = j;
}
'\u{2026}' => {
stops += 3;
last_stop = j;
}
c if c.is_whitespace() => {}
_ => break,
}
j += 1;
}
if stops >= DOT_LEADER {
if !out.ends_with(' ') && !out.is_empty() {
out.push(' ');
}
i = last_stop + 1;
continue;
}
out.extend(&chars[i..=last_stop]);
i = last_stop + 1;
continue;
}
out.push(chars[i]);
i += 1;
}
let collapsed: Vec<&str> = out.split_whitespace().collect();
collapsed.join(" ")
}
fn join_currency(text: &str) -> String {
if !text.chars().any(|c| CURRENCY.contains(&c)) {
return text.to_string();
}
let words: Vec<&str> = text.split(' ').collect();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < words.len() {
let word = words[i];
let is_sign = word.chars().count() == 1 && word.chars().all(|c| CURRENCY.contains(&c));
if is_sign
&& i + 1 < words.len()
&& words[i + 1]
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == '(')
{
if !out.is_empty() {
out.push(' ');
}
out.push_str(word);
out.push_str(words[i + 1]);
i += 2;
continue;
}
if !out.is_empty() {
out.push(' ');
}
out.push_str(word);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_leader_run_is_struck_and_the_words_round_it_kept() {
assert_eq!(strip_leaders("2011 . . . . . . . 65.1"), "2011 65.1");
assert_eq!(strip_leaders("Total........"), "Total");
assert_eq!(strip_leaders("Thereafter . . . . ."), "Thereafter");
assert_eq!(
strip_leaders("Business Overview ....... 21"),
"Business Overview 21"
);
assert_eq!(strip_leaders("Vested \u{2026}\u{2026}"), "Vested");
}
#[test]
fn an_ellipsis_and_a_full_stop_are_not_a_leader() {
assert_eq!(strip_leaders("Net income..."), "Net income...");
assert_eq!(strip_leaders("and so on\u{2026}"), "and so on\u{2026}");
assert_eq!(strip_leaders("U.S. Treasury"), "U.S. Treasury");
assert_eq!(strip_leaders("2.8 %"), "2.8 %");
}
#[test]
fn a_currency_sign_joins_the_figure_after_it() {
assert_eq!(join_currency("$ 336.5"), "$336.5");
assert_eq!(join_currency("$ (1.3)"), "$(1.3)");
assert_eq!(join_currency("\u{20ac} 1,000"), "\u{20ac}1,000");
assert_eq!(join_currency("Total $ 278.6"), "Total $278.6");
}
#[test]
fn a_sign_alone_or_before_a_word_keeps_its_place() {
assert_eq!(join_currency("$"), "$");
assert_eq!(join_currency("$ million"), "$ million");
assert_eq!(join_currency("US$ 5"), "US$ 5");
assert_eq!(join_currency("$336.5"), "$336.5");
}
#[test]
fn tidy_does_both() {
assert_eq!(tidy("2011 . . . . . $ 65.1"), "2011 $65.1");
}
}