const MAX_RUN: usize = 999;
const MARKER: usize = 3;
pub fn key(value: &str) -> String {
let bytes = value.as_bytes();
let mut out = String::with_capacity(value.len() + MARKER);
let mut at = 0;
while at < bytes.len() {
let start = at;
if bytes[at].is_ascii_digit() {
while at < bytes.len() && bytes[at].is_ascii_digit() {
at += 1;
}
let run = &value[start..at];
let zeros = run.bytes().take_while(|b| *b == b'0').count();
let significant = (run.len() - zeros).min(MAX_RUN);
out.push_str(&format!("{significant:0MARKER$}"));
out.push_str(run);
} else {
while at < bytes.len() && !bytes[at].is_ascii_digit() {
at += 1;
}
out.push_str(&value[start..at]);
}
}
out
}
pub fn worth_keying<'a>(values: impl IntoIterator<Item = &'a str>) -> bool {
let mut seen: Vec<usize> = Vec::new();
for value in values {
for (nth, run) in runs(value).enumerate() {
match seen.get(nth) {
None => seen.push(run),
Some(&first) if first != run => return true,
Some(_) => {}
}
}
}
false
}
fn runs(value: &str) -> impl Iterator<Item = usize> + '_ {
let bytes = value.as_bytes();
let mut at = 0;
std::iter::from_fn(move || {
while at < bytes.len() && !bytes[at].is_ascii_digit() {
at += 1;
}
if at == bytes.len() {
return None;
}
let start = at;
while at < bytes.len() && bytes[at].is_ascii_digit() {
at += 1;
}
Some((at - start).min(MAX_RUN))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ordered<'a>(values: &[&'a str]) -> Vec<&'a str> {
let mut keyed: Vec<(String, &str)> = values.iter().map(|v| (key(v), *v)).collect();
keyed.sort();
keyed.into_iter().map(|(_, v)| v).collect()
}
#[test]
fn percentages_order_by_what_they_count() {
assert_eq!(
ordered(&["100%", "9%", "0%", "23%", "50%"]),
["0%", "9%", "23%", "50%", "100%"]
);
}
#[test]
fn a_number_inside_a_name_counts_as_one() {
assert_eq!(
ordered(&["file10", "file2", "file1"]),
["file1", "file2", "file10"]
);
}
#[test]
fn every_run_is_compared_in_turn() {
assert_eq!(
ordered(&["v1.10", "v1.9", "v1.2"]),
["v1.2", "v1.9", "v1.10"]
);
}
#[test]
fn equal_length_runs_keep_the_order_they_had() {
let dates = ["2024-01-09", "2023-12-31", "2024-01-10"];
let mut byte_order = dates;
byte_order.sort();
assert_eq!(ordered(&dates), byte_order);
}
#[test]
fn text_with_no_digits_keys_to_itself() {
assert_eq!(key("close"), "close");
assert_eq!(key(""), "");
}
#[test]
fn leading_zeros_are_ordered_rather_than_tied() {
assert_ne!(key("03"), key("3"));
assert_eq!(ordered(&["3", "03"]), ["03", "3"]);
}
#[test]
fn a_run_of_zeros_is_a_number_like_any_other() {
assert_eq!(ordered(&["000", "0", "1"]), ["0", "000", "1"]);
}
#[test]
fn a_marker_is_not_read_as_part_of_the_value() {
assert_eq!(ordered(&["a1b", "a11"]), ["a1b", "a11"]);
assert_eq!(ordered(&["1a", "11a", "2a"]), ["1a", "2a", "11a"]);
}
#[test]
fn multi_byte_characters_survive() {
assert_eq!(key("café2"), "café2".replace('2', "0012"));
assert_eq!(ordered(&["é10", "é9"]), ["é9", "é10"]);
}
#[test]
fn a_column_of_equal_length_runs_is_not_worth_keying() {
assert!(!worth_keying(["2024-01-09", "2023-12-31"]));
assert!(!worth_keying(["close", "open"]));
assert!(!worth_keying(Vec::<&str>::new()));
assert!(!worth_keying(["01", "09", "12"]));
}
#[test]
fn saying_no_means_the_key_would_have_changed_nothing() {
for column in [
["2024-01-09", "2023-12-31", "2024-12-01"],
["01", "09", "12"],
["b", "a", "c"],
["x1y", "x9y", "x5y"],
] {
if worth_keying(column) {
continue;
}
let mut natural = column;
natural.sort_by_key(|v| key(v));
let mut bytes = column;
bytes.sort();
assert_eq!(natural, bytes, "{column:?}");
}
}
#[test]
fn runs_are_compared_by_position_and_not_against_each_other() {
assert!(!worth_keying(["2024-01-09"]));
assert!(worth_keying(["2024-01-09", "999-01-09"]));
}
#[test]
fn a_column_whose_runs_differ_in_length_is() {
assert!(worth_keying(["9%", "100%"]));
assert!(worth_keying(["file2", "file10"]));
}
}