pub fn match_key(vin: &str) -> String {
let mut key = String::with_capacity(14);
if vin.len() > 3 {
key.push_str(&vin[3..vin.len().min(8)]);
}
if vin.len() > 9 {
key.push('|');
key.push_str(&vin[9..vin.len().min(17)]);
}
key
}
pub fn literal_len(keys: &str) -> usize {
keys.chars().filter(|c| *c != '*').count()
}
pub fn keys_match(keys: &str, key: &str) -> bool {
let underscore_is_wildcard = !keys.contains('[');
let mut input = key.chars();
let mut pattern = keys.chars().peekable();
while let Some(p) = pattern.next() {
let Some(i) = input.next() else {
return false;
};
let ok = match p {
'*' => true,
'_' if underscore_is_wildcard => true,
'#' => i.is_ascii_digit(),
'[' => {
let mut class = String::new();
let mut closed = false;
for c in pattern.by_ref() {
if c == ']' {
closed = true;
break;
}
class.push(c);
}
closed && class_contains(&class, i)
}
_ => p == i,
};
if !ok {
return false;
}
}
true
}
fn class_contains(class: &str, ch: char) -> bool {
let chars: Vec<char> = class.chars().collect();
let mut i = 0;
while i < chars.len() {
if i + 2 < chars.len() && chars[i + 1] == '-' {
let (start, end) = (chars[i], chars[i + 2]);
if start <= ch && ch <= end {
return true;
}
i += 3;
} else {
if chars[i] == ch {
return true;
}
i += 1;
}
}
false
}
pub fn is_formula(keys: &str) -> bool {
keys.contains('#')
}
pub fn formula_value(keys: &str, key: &str) -> Option<String> {
let first = keys.find('#')?;
let last = keys.rfind('#')?;
if !keys_match(keys, key) {
return None;
}
let value: String = key.chars().skip(first).take(last - first + 1).collect();
if value.is_empty() { None } else { Some(value) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn match_key_drops_the_check_digit() {
assert_eq!(match_key("1HGCP26739A060971"), "CP267|9A060971");
}
#[test]
fn match_key_tolerates_short_input() {
assert_eq!(match_key("1HG"), "");
assert_eq!(match_key("1HGCP267"), "CP267");
}
#[test]
fn literal_len_counts_bracket_punctuation_but_not_wildcards() {
assert_eq!(literal_len("RR7LT"), 5);
assert_eq!(literal_len("*****|*S"), 2);
assert_eq!(literal_len("[FWX]G"), 6);
}
#[test]
fn keys_match_is_a_prefix_match() {
let key = "RR7LT|JS179571";
assert!(keys_match("R", key));
assert!(keys_match("RR7LT|J", key));
assert!(!keys_match("RR7LX", key));
}
#[test]
fn keys_match_requires_the_separator_to_line_up() {
assert!(keys_match("*****|*S", "RR7LT|JS179571"));
assert!(!keys_match("****|*C", "RJFBG|FC123456"));
}
#[test]
fn keys_match_handles_classes_and_ranges() {
let key = "RR7LT|JS179571";
assert!(keys_match("[A-Z]R7", key));
assert!(keys_match("[QRS]R7", key));
assert!(!keys_match("[A-Q]R7", key));
assert!(keys_match("[0-9A-Z]R7", key));
}
#[test]
fn keys_match_rejects_an_unterminated_class() {
assert!(!keys_match("[ABR", "RR7LT|JS179571"));
}
#[test]
fn underscore_is_a_wildcard_only_without_brackets() {
assert!(keys_match("_R7LT", "RR7LT|JS179571"));
assert!(!keys_match("_R7L[LT]", "RR7LT|JS179571"));
}
#[test]
fn formula_keys_only_match_digits() {
assert_eq!(
formula_value("**##", "RR12T|JS179571").as_deref(),
Some("12")
);
assert_eq!(formula_value("**##", "RJ1BG|FC123456"), None);
assert_eq!(formula_value("*#", "R1FBG|FC123456").as_deref(), Some("1"));
}
}