Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::text_normalise_name::normalise_name; ← from text.normalise-name ^1.0.0 · built alongside by fune
const COMPANY_FORMS: [&str; 6] = ["ltd", "limited", "plc", "llp", "llc", "inc"];
const MAX_KEY: usize = 500;
fn lower_one(ch: char) -> char {
// One-to-one case mappings only, as text.normalise-name, so all three languages agree.
let mapped: Vec<char> = ch.to_lowercase().collect();
if mapped.len() == 1 {
mapped[0]
} else {
ch
}
}
fn is_ascii_punctuation(cp: u32) -> bool {
(0x21..=0x2f).contains(&cp) || (0x3a..=0x40).contains(&cp) || (0x5b..=0x60).contains(&cp) || (0x7b..=0x7e).contains(&cp)
}
/// The normalised, punctuation-free, company-form-free, token-sorted key.
fn match_key(value: &str) -> String {
let mut text = String::new();
for ch in normalise_name(value).chars() {
if ch == '\'' || ch == '\u{2019}' || ch == '.' {
continue;
}
if ch == '&' {
text.push_str(" and ");
} else if is_ascii_punctuation(ch as u32) || ch == ' ' {
text.push(' ');
} else {
text.push(lower_one(ch));
}
}
let mut tokens: Vec<&str> = text.split(' ').filter(|t| !t.is_empty() && !COMPANY_FORMS.contains(t)).collect();
// Byte order of UTF-8 is code point order.
tokens.sort();
let key = tokens.join(" ");
if key.chars().count() > MAX_KEY {
panic!("names must be at most {} characters after normalising", MAX_KEY);
}
key
}
/// Jaro-Winkler similarity in basis points, from exact integer fractions.
fn jaro_winkler(s1: &[char], s2: &[char]) -> i64 {
let a = s1.len();
let b = s2.len();
if a == 0 || b == 0 {
return 0;
}
let window = (a.max(b) / 2).saturating_sub(1);
let mut used = vec![false; b];
let mut order1: Vec<char> = Vec::new();
for i in 0..a {
let lo = i.saturating_sub(window);
let hi = (b - 1).min(i + window);
for j in lo..=hi {
if !used[j] && s1[i] == s2[j] {
used[j] = true;
order1.push(s1[i]);
break;
}
}
}
let m = order1.len() as i64;
if m == 0 {
return 0;
}
let mut k = 0;
let mut out_of_order: i64 = 0;
for j in 0..b {
if !used[j] {
continue;
}
if s2[j] != order1[k] {
out_of_order += 1;
}
k += 1;
}
let (a, b) = (a as i64, b as i64);
// Jaro = n / d exactly, with t = out_of_order / 2.
let n = 2 * m * m * (a + b) + a * b * (2 * m - out_of_order);
let d = 6 * a * b * m;
let mut prefix: i64 = 0;
while prefix < 4 && prefix < a && prefix < b && s1[prefix as usize] == s2[prefix as usize] {
prefix += 1;
}
if 10 * n <= 7 * d {
return round_div(10000 * n, d, "half-up");
}
round_div(10000 * (n * (10 - prefix) + prefix * d), 10 * d, "half-up")
}
/// Candidates similar to a name, best first, for a conflict check.
///
/// # Panics
/// Panics on a threshold outside 0..=10000, a name that normalises to nothing,
/// or a key longer than 500 characters.
pub fn conflict_name_match(name: &str, candidates: &[String], threshold_basis_points: i64) -> Vec<NameMatch> {
if !(0..=10000).contains(&threshold_basis_points) {
panic!("thresholdBasisPoints must be between 0 and 10000, received {}", threshold_basis_points);
}
let key = match_key(name);
if key.is_empty() {
panic!("name is empty after normalising");
}
let s1: Vec<char> = key.chars().collect();
let mut matches: Vec<NameMatch> = Vec::new();
for (index, candidate) in candidates.iter().enumerate() {
let other = match_key(candidate);
let s2: Vec<char> = other.chars().collect();
let score = jaro_winkler(&s1, &s2);
if score >= threshold_basis_points {
matches.push(NameMatch { index: index as i64, candidate: candidate.clone(), match_key: other, score });
}
}
matches.sort_by(|x, y| y.score.cmp(&x.score).then(x.index.cmp(&y.index)));
matches
}
pub fn name_match_to_value(m: &NameMatch) -> Value {
Value::obj(vec![
("index", Value::Int(m.index)),
("candidate", Value::str(&m.candidate)),
("matchKey", Value::str(&m.match_key)),
("score", Value::Int(m.score)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let candidates: Vec<String> = args[1].as_arr().iter().map(|v| v.as_str().to_string()).collect();
Value::Arr(conflict_name_match(args[0].as_str(), &candidates, args[2].as_i64()).iter().map(name_match_to_value).collect())
}