pub fn extract_number_from_path(path: &std::path::Path) -> Option<u64> {
let filename = path.file_name()?.to_string_lossy();
let numeric: String = filename
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if numeric.is_empty() {
return None;
}
numeric.parse::<u64>().ok()
}
pub fn extract_id_prefix_from_path(path: &std::path::Path) -> Option<String> {
let filename = path.file_name()?.to_string_lossy();
filename.split_once('-').map(|(p, _)| p.to_string())
}
pub fn id_prefix_matches(dir_prefix: &str, id_suffix: &str) -> bool {
if id_suffix.len() == 13 || id_suffix.len() == 26 {
return dir_prefix.eq_ignore_ascii_case(id_suffix);
}
let Ok(id_num) = id_suffix.parse::<u32>() else {
return false;
};
let Ok(dir_num) = dir_prefix.parse::<u32>() else {
return false;
};
id_num == dir_num
}
#[cfg(test)]
pub mod strategy {
use proptest::prelude::*;
use std::path::PathBuf;
pub fn record_path() -> impl Strategy<Value = PathBuf> {
("[a-z]{1,8}", 1u32..10_000, "[a-z]{1,12}")
.prop_map(|(kind, n, slug)| PathBuf::from(format!("docs/{kind}/{n:04}-{slug}.md")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn extract_number_round_trips(path in strategy::record_path()) {
let n = extract_number_from_path(&path);
prop_assert!(n.is_some());
}
#[test]
fn id_prefix_matches_is_reflexive_for_legacy(n in 1u32..10_000) {
let s = format!("{n:04}");
prop_assert!(id_prefix_matches(&s, &s));
}
}
}