use super::*;
pub(crate) const NUMERIC_ATTRACTORS: &[&str] = &["geography.address.postal_code"];
pub(crate) const TEXT_ATTRACTORS: &[&str] = &[
"identity.person.first_name",
"identity.person.phone_number",
"identity.person.username",
"geography.address.street_name",
];
pub(crate) const CODE_ATTRACTORS: &[&str] = &[
"geography.transportation.icao_code",
"identity.medical.ndc",
"finance.securities.cusip",
"technology.internet.top_level_domain",
];
pub(crate) fn value_sharpen(
values: &[String],
result_label: &str,
result_confidence: f32,
taxonomy: Option<&Taxonomy>,
) -> Option<(String, String)> {
if result_label == DATE_SLASH_PAIR.0 || result_label == DATE_SLASH_PAIR.1 {
if let Some(label) = disambiguate_slash_dates(values) {
return Some((label, "date_slash_disambiguation".to_string()));
}
}
if result_label == SHORT_DATE_PAIR.0 || result_label == SHORT_DATE_PAIR.1 {
if let Some(label) = disambiguate_short_dates(values) {
return Some((label, "short_date_disambiguation".to_string()));
}
}
if result_label == COORDINATE_PAIR.0 || result_label == COORDINATE_PAIR.1 {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let mut parseable = 0usize;
let mut out_of_range = 0usize;
for v in &non_empty {
if let Ok(val) = v.parse::<f64>() {
parseable += 1;
if val.abs() > 180.0 {
out_of_range += 1;
}
}
}
if parseable >= 3 && (out_of_range as f32 / parseable as f32) > 0.1 {
return Some((
"representation.numeric.decimal_number".to_string(),
format!(
"coordinate_plausibility_gate:out_of_range={}/{}",
out_of_range, parseable
),
));
}
}
}
if result_label == COORDINATE_PAIR.0 || result_label == COORDINATE_PAIR.1 {
if let Some(label) = disambiguate_coordinates(values) {
return Some((label, "coordinate_disambiguation".to_string()));
}
}
if let Some(label) = disambiguate_ipv4(values) {
return Some((label, "ipv4_detection".to_string()));
}
if let Some(label) = disambiguate_day_of_week(values) {
return Some((label, "day_of_week_name_detection".to_string()));
}
if let Some(label) = disambiguate_month_name(values) {
return Some((label, "month_name_detection".to_string()));
}
let top_labels_single = [result_label];
if let Some((label, rule)) = disambiguate_boolean_subtype(values, &top_labels_single) {
return Some((label, rule));
}
if let Some(label) = disambiguate_gender(values) {
return Some((label, "gender_detection".to_string()));
}
if let Some((label, rule)) = disambiguate_boolean_override(values, &top_labels_single) {
return Some((label, rule));
}
let numeric_types = [
"representation.identifier.increment",
"representation.numeric.integer_number",
"representation.numeric.decimal_number",
"geography.address.postal_code",
"datetime.component.year",
"representation.identifier.numeric_code",
];
if numeric_types.contains(&result_label) {
if let Some((label, rule)) = disambiguate_numeric(values, &[], &top_labels_single) {
return Some((label, rule));
}
}
if result_label == "representation.numeric.si_number" {
if let Some((label, rule)) = disambiguate_si_number(values) {
return Some((label, rule));
}
}
if result_label == "representation.numeric.percentage" {
let has_pct_sign = values.iter().any(|v| v.contains('%'));
if !has_pct_sign {
return Some((
"representation.numeric.decimal_number".to_string(),
"percentage_no_sign".to_string(),
));
}
}
if result_label == "geography.transportation.hs_code" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let match_count = non_empty.iter().filter(|v| is_hs_code_format(v)).count();
let match_rate = match_count as f32 / non_empty.len() as f32;
if match_rate < 0.5 {
return Some((
"representation.numeric.decimal_number".to_string(),
format!("hs_code_validation_gate:match_rate={:.2}", match_rate),
));
}
}
}
if result_label == "identity.commerce.upc" {
let digit_only: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
.collect();
if digit_only.len() >= 3 {
let twelve = digit_only.iter().filter(|v| v.len() == 12).count();
let thirteen = digit_only.iter().filter(|v| v.len() == 13).count();
let eight = digit_only.iter().filter(|v| v.len() == 8).count();
let total = digit_only.len();
let twelve_rate = twelve as f32 / total as f32;
if twelve_rate < 0.5 {
let ean_rate = (thirteen + eight) as f32 / total as f32;
if ean_rate > 0.5 {
return Some((
"identity.commerce.ean".to_string(),
format!("upc_digit_count_gate:ean_rate={:.2}", ean_rate),
));
}
return Some((
"representation.identifier.numeric_code".to_string(),
format!("upc_digit_count_gate:twelve_rate={:.2}", twelve_rate),
));
}
}
}
if result_label == "identity.commerce.isrc" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let isin_count = non_empty.iter().filter(|v| is_isin_format(v)).count();
let isin_rate = isin_count as f32 / non_empty.len() as f32;
if isin_rate > 0.5 {
return Some((
"finance.securities.isin".to_string(),
format!("isin_format_gate:match_rate={:.2}", isin_rate),
));
}
}
}
if result_label == "identity.government.ein" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let issn_count = non_empty.iter().filter(|v| is_issn_format(v)).count();
let issn_rate = issn_count as f32 / non_empty.len() as f32;
if issn_rate > 0.5 {
return Some((
"identity.commerce.issn".to_string(),
format!("issn_format_gate:match_rate={:.2}", issn_rate),
));
}
}
}
if result_label == "finance.securities.sedol" {
if let Some((label, rule)) = disambiguate_duration_override(values) {
return Some((label, rule));
}
}
if let Some((label, rule)) = disambiguate_utc_offset_override(values) {
return Some((label, rule));
}
if let Some((label, rule)) =
sharpen_attractor_demotion(values, result_label, result_confidence, taxonomy)
{
return Some((label, rule));
}
if matches!(
result_label,
"geography.address.full_address"
| "representation.text.entity_name"
| "representation.text.word"
) {
if let Some((label, rule)) = disambiguate_text_length_demotion(
values,
&[(result_label.to_string(), 1)], ) {
return Some((label, rule));
}
}
if result_label == "geography.address.full_address" {
if let Some((label, rule)) = disambiguate_full_address_whitespace_guard(values) {
return Some((label, rule));
}
}
if result_label == "datetime.date.dmy_short_dot" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let invalid_date_count = non_empty
.iter()
.filter(|v| {
let parts: Vec<&str> = v.split('.').collect();
if parts.len() != 3 {
return false;
}
let day = parts[0].parse::<u32>().unwrap_or(1);
let month = parts[1].parse::<u32>().unwrap_or(1);
let year = parts[2].parse::<u32>().unwrap_or(0);
day == 0 || day > 31 || month == 0 || month > 12 || year > 99
})
.count();
let invalid_rate = invalid_date_count as f32 / non_empty.len() as f32;
if invalid_rate >= 0.30 {
return Some((
"technology.development.version".to_string(),
format!(
"version_dmy_short_dot_gate:invalid_date={:.2}",
invalid_rate
),
));
}
}
}
if result_label == "datetime.date.compact_ym" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
let four_digit_count = non_empty
.iter()
.filter(|v| v.len() == 4 && v.chars().all(|c| c.is_ascii_digit()))
.count();
let four_digit_rate = four_digit_count as f32 / non_empty.len() as f32;
if four_digit_rate >= 0.90 {
let year_count = non_empty
.iter()
.filter(|v| {
v.len() == 4
&& v.parse::<u16>()
.map(|n| (1900..=2100).contains(&n))
.unwrap_or(false)
})
.count();
let year_rate = year_count as f32 / non_empty.len() as f32;
if year_rate >= 0.80 {
return Some((
"datetime.component.year".to_string(),
format!(
"year_compact_ym_gate:four_digit={:.2},year_range={:.2}",
four_digit_rate, year_rate
),
));
}
}
}
}
if result_label == "representation.scientific.measurement_unit"
|| result_label == "geography.coordinate.geohash"
|| result_label == "datetime.offset.utc"
|| result_label == "technology.internet.url"
|| result_label == "geography.index.h3"
|| result_label == "technology.cloud.aws_arn"
|| result_label == "finance.crypto.ethereum_address"
|| result_label == "identity.academic.orcid"
|| result_label == "identity.medical.cpt"
|| result_label == "technology.internet.http_method"
|| result_label == "representation.boolean.terms"
|| result_label == "geography.format.wkt"
|| result_label == "technology.internet.user_agent"
|| result_label == "geography.coordinate.mgrs"
|| result_label == "geography.coordinate.plus_code"
|| result_label == "geography.coordinate.dms"
|| result_label == "geography.transportation.iso6346"
|| result_label == "representation.scientific.inchi"
|| result_label == "finance.banking.swift_bic"
{
if let Some(taxonomy) = taxonomy {
if let Some((label, rule)) = schema_fail_demotion(values, result_label, taxonomy) {
return Some((label, rule));
}
}
}
if result_label == "representation.text.entity_name" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 4 {
let is_prose = |v: &str| {
v.split_whitespace()
.filter(|w| {
w.len() >= 4 && w.chars().all(|c| c.is_alphabetic() && c.is_lowercase())
})
.count()
>= 2
};
let mut tok_counts: Vec<usize> = non_empty
.iter()
.map(|v| v.split_whitespace().count())
.collect();
tok_counts.sort_unstable();
let median_tokens = tok_counts[tok_counts.len() / 2];
let prose_n = non_empty.iter().filter(|v| is_prose(v)).count();
if median_tokens >= 3 && prose_n * 2 >= non_empty.len() {
return Some((
"representation.text.plain_text".to_string(),
format!(
"entity_prose_override:prose={}/{}",
prose_n,
non_empty.len()
),
));
}
}
}
if result_label == "representation.text.word" {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
let n = non_empty.len();
if n >= 4 {
let distinct: std::collections::HashSet<&str> = non_empty.iter().copied().collect();
let d = distinct.len();
if (2..=12).contains(&d) && (d as f32 / n as f32) <= 0.6 {
return Some((
"representation.text.word".to_string(),
format!("text_vocab_override:distinct={}/{}", d, n),
));
}
}
}
None
}
pub(crate) fn schema_fail_demotion(
values: &[String],
result_label: &str,
taxonomy: &Taxonomy,
) -> Option<(String, String)> {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let fail_count = if let Some(compiled) = taxonomy.get_validator(result_label) {
non_empty.iter().filter(|v| !compiled.is_valid(v)).count()
} else {
let validation = taxonomy.get(result_label)?.validation.as_ref()?;
non_empty
.iter()
.filter(|v| {
finetype_core::validate_value(v, validation)
.map(|r| !r.is_valid)
.unwrap_or(false)
})
.count()
};
let fail_rate = fail_count as f32 / non_empty.len() as f32;
if fail_rate <= 0.5 {
return None;
}
let mut unique: Vec<&str> = non_empty.clone();
unique.sort_unstable();
unique.dedup();
let fallback = if (1..=20).contains(&unique.len()) {
"representation.text.word"
} else {
"representation.identifier.alphanumeric_id"
};
Some((
fallback.to_string(),
format!(
"schema_fail_demotion:{}:fail_rate={:.2}",
result_label, fail_rate
),
))
}
pub(crate) fn sharpen_attractor_demotion(
values: &[String],
result_label: &str,
result_confidence: f32,
taxonomy: Option<&Taxonomy>,
) -> Option<(String, String)> {
let is_numeric = NUMERIC_ATTRACTORS.contains(&result_label);
let is_text = TEXT_ATTRACTORS.contains(&result_label);
let is_code = CODE_ATTRACTORS.contains(&result_label);
if !is_numeric && !is_text && !is_code {
return None;
}
let mut locale_confirmed = false;
let mut validation_confirmed = false;
let mut has_locale_validators = false;
if let Some(taxonomy) = taxonomy {
let has_precise_validation = taxonomy
.get(result_label)
.and_then(|d| d.validation.as_ref())
.map(|v| v.is_precise())
.unwrap_or(false);
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
if let Some(locale_validators) = taxonomy.get_locale_validators(result_label) {
has_locale_validators = true;
let mut best_pass_rate: f32 = 0.0;
for compiled in locale_validators.values() {
let pass_count = non_empty.iter().filter(|v| compiled.is_valid(v)).count();
let pass_rate = pass_count as f32 / non_empty.len() as f32;
if pass_rate > best_pass_rate {
best_pass_rate = pass_rate;
}
}
if best_pass_rate > 0.5 {
locale_confirmed = true;
}
}
if !locale_confirmed {
let fail_count = if let Some(compiled) = taxonomy.get_validator(result_label) {
non_empty.iter().filter(|v| !compiled.is_valid(v)).count()
} else if let Some(def) = taxonomy.get(result_label) {
if let Some(validation) = &def.validation {
non_empty
.iter()
.filter(|v| {
finetype_core::validate_value(v, validation)
.map(|r| !r.is_valid)
.unwrap_or(false)
})
.count()
} else {
0
}
} else {
0
};
let fail_rate = fail_count as f32 / non_empty.len() as f32;
if fail_rate > 0.5 {
let fallback = sharpen_select_fallback(is_numeric, is_text, is_code, values);
return Some((
fallback,
format!("attractor_demotion_validation:{}", result_label),
));
}
if has_precise_validation && fail_rate <= 0.3 {
validation_confirmed = true;
}
}
}
}
let confirmed = locale_confirmed || (!has_locale_validators && validation_confirmed);
if !confirmed && result_confidence < 0.85 {
let fallback = sharpen_select_fallback(is_numeric, is_text, is_code, values);
return Some((
fallback,
format!("attractor_demotion_confidence:{}", result_label),
));
}
if is_text && !locale_confirmed {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
let mut unique: Vec<&str> = non_empty.clone();
unique.sort();
unique.dedup();
if (1..=20).contains(&unique.len()) {
return Some((
"representation.text.word".to_string(),
format!("attractor_demotion_cardinality:{}", result_label),
));
}
}
None
}
pub(crate) fn sharpen_select_fallback(
is_numeric: bool,
is_text: bool,
is_code: bool,
values: &[String],
) -> String {
if is_numeric {
let has_decimal = values.iter().any(|v| v.contains('.'));
if has_decimal {
"representation.numeric.decimal_number".to_string()
} else {
"representation.numeric.integer_number".to_string()
}
} else if is_text {
"representation.text.word".to_string()
} else if is_code {
"representation.identifier.alphanumeric_id".to_string()
} else {
"representation.text.word".to_string()
}
}
pub(crate) fn disambiguate(
values: &[String],
results: &[ClassificationResult],
votes: &[(String, usize)],
n_samples: usize,
taxonomy: Option<&Taxonomy>,
) -> Option<(String, String)> {
let top_labels: Vec<&str> = votes.iter().take(3).map(|(l, _)| l.as_str()).collect();
if contains_pair(&top_labels, DATE_SLASH_PAIR.0, DATE_SLASH_PAIR.1) {
if let Some(label) = disambiguate_slash_dates(values) {
return Some((label, "date_slash_disambiguation".to_string()));
}
}
if contains_pair(&top_labels, SHORT_DATE_PAIR.0, SHORT_DATE_PAIR.1) {
if let Some(label) = disambiguate_short_dates(values) {
return Some((label, "short_date_disambiguation".to_string()));
}
}
if contains_pair(&top_labels, COORDINATE_PAIR.0, COORDINATE_PAIR.1) {
let coord_votes: usize = votes
.iter()
.filter(|(l, _)| l == COORDINATE_PAIR.0 || l == COORDINATE_PAIR.1)
.map(|(_, c)| c)
.sum();
let top_votes = votes.first().map(|(_, c)| *c).unwrap_or(0);
let top_is_coord = votes
.first()
.map(|(l, _)| l == COORDINATE_PAIR.0 || l == COORDINATE_PAIR.1)
.unwrap_or(false);
if top_is_coord || coord_votes * 3 >= top_votes {
if let Some(label) = disambiguate_coordinates(values) {
return Some((label, "coordinate_disambiguation".to_string()));
}
}
}
if let Some(label) = disambiguate_ipv4(values) {
return Some((label, "ipv4_detection".to_string()));
}
if let Some(label) = disambiguate_day_of_week(values) {
return Some((label, "day_of_week_name_detection".to_string()));
}
if let Some(label) = disambiguate_month_name(values) {
return Some((label, "month_name_detection".to_string()));
}
if let Some((label, rule)) = disambiguate_boolean_subtype(values, &top_labels) {
return Some((label, rule));
}
if let Some(label) = disambiguate_gender(values) {
return Some((label, "gender_detection".to_string()));
}
if let Some((label, rule)) = disambiguate_boolean_override(values, &top_labels) {
return Some((label, rule));
}
if let Some((label, rule)) = disambiguate_numeric(values, results, &top_labels) {
return Some((label, rule));
}
if top_labels
.first()
.is_some_and(|l| *l == "representation.numeric.si_number")
{
if let Some((label, rule)) = disambiguate_si_number(values) {
return Some((label, rule));
}
}
if top_labels
.first()
.is_some_and(|l| *l == "representation.numeric.percentage")
{
let has_pct_sign = values.iter().any(|v| v.contains('%'));
if !has_pct_sign {
return Some((
"representation.numeric.decimal_number".to_string(),
"percentage_no_sign".to_string(),
));
}
}
if top_labels
.first()
.is_some_and(|l| *l == "finance.securities.sedol")
{
if let Some((label, rule)) = disambiguate_duration_override(values) {
return Some((label, rule));
}
}
if let Some((label, rule)) = disambiguate_utc_offset_override(values) {
return Some((label, rule));
}
if let Some((label, rule)) = disambiguate_attractor_demotion(values, votes, n_samples, taxonomy)
{
return Some((label, rule));
}
if let Some((label, rule)) = disambiguate_text_length_demotion(values, votes) {
return Some((label, rule));
}
None
}
pub(crate) fn disambiguate_day_of_week(values: &[String]) -> Option<String> {
const DAY_NAMES: &[&str] = &[
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun",
"mo",
"tu",
"we",
"th",
"fr",
"sa",
"su",
];
let non_empty: Vec<String> = values
.iter()
.map(|v| v.trim().to_lowercase())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let matching = non_empty
.iter()
.filter(|v| DAY_NAMES.contains(&v.as_str()))
.count();
let fraction = matching as f64 / non_empty.len() as f64;
if fraction >= 0.8 {
Some("datetime.component.day_of_week".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_month_name(values: &[String]) -> Option<String> {
const MONTH_NAMES: &[&str] = &[
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
"jan",
"feb",
"mar",
"apr",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
];
let non_empty: Vec<String> = values
.iter()
.map(|v| v.trim().to_lowercase())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let matching = non_empty
.iter()
.filter(|v| MONTH_NAMES.contains(&v.as_str()))
.count();
let fraction = matching as f64 / non_empty.len() as f64;
if fraction >= 0.8 {
Some("datetime.component.month_name".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_boolean_subtype(
values: &[String],
top_labels: &[&str],
) -> Option<(String, String)> {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let binary_values: &[&str] = &["0", "1"];
let terms_values: &[&str] = &[
"true", "false", "True", "False", "TRUE", "FALSE", "yes", "no", "Yes", "No", "YES", "NO",
"on", "off", "On", "Off", "ON", "OFF",
];
let initials_values: &[&str] = &["T", "F", "t", "f", "Y", "N", "y", "n"];
let binary_count = non_empty
.iter()
.filter(|v| binary_values.contains(v))
.count();
let terms_count = non_empty
.iter()
.filter(|v| terms_values.contains(v))
.count();
let initials_count = non_empty
.iter()
.filter(|v| initials_values.contains(v))
.count();
let n = non_empty.len() as f64;
let binary_frac = binary_count as f64 / n;
let terms_frac = terms_count as f64 / n;
let initials_frac = initials_count as f64 / n;
let has_boolean_vote = top_labels.iter().any(|l| BOOLEAN_LABELS.contains(l));
let max_frac = binary_frac.max(terms_frac).max(initials_frac);
if !has_boolean_vote && max_frac < 0.8 {
return None;
}
if terms_frac >= 0.8 {
Some((
"representation.boolean.terms".to_string(),
"boolean_subtype_terms".to_string(),
))
} else if initials_frac >= 0.8 {
Some((
"representation.boolean.initials".to_string(),
"boolean_subtype_initials".to_string(),
))
} else if binary_frac >= 0.8 {
let unique_values: std::collections::HashSet<&str> = non_empty.iter().copied().collect();
if unique_values.len() == 2 {
let dominant_frac = non_empty
.iter()
.filter(|&&v| v == "0")
.count()
.max(non_empty.iter().filter(|&&v| v == "1").count())
as f64
/ n;
if dominant_frac <= 0.95 {
Some((
"representation.boolean.binary".to_string(),
"boolean_subtype_binary".to_string(),
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub(crate) fn disambiguate_gender(values: &[String]) -> Option<String> {
const GENDER_VALUES: &[&str] = &[
"male",
"female",
"m",
"f",
"Male",
"Female",
"M",
"F",
"MALE",
"FEMALE",
"man",
"woman",
"Man",
"Woman",
"MAN",
"WOMAN",
"boy",
"girl",
"Boy",
"Girl",
"non-binary",
"Non-binary",
"Non-Binary",
"NON-BINARY",
"nonbinary",
"Nonbinary",
"other",
"Other",
"OTHER",
"prefer not to say",
"Prefer not to say",
"unknown",
"Unknown",
"UNKNOWN",
"x",
"X",
"genderqueer",
"Genderqueer",
"agender",
"Agender",
"transgender",
"Transgender",
];
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let all_code = non_empty
.iter()
.all(|v| matches!(v.to_ascii_uppercase().as_str(), "M" | "F" | "X"));
if all_code {
return Some("identity.person.gender_code".to_string());
}
let all_gender = non_empty.iter().all(|v| GENDER_VALUES.contains(v));
if all_gender {
Some("identity.person.gender".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_boolean_override(
values: &[String],
top_labels: &[&str],
) -> Option<(String, String)> {
let has_boolean = top_labels.iter().any(|l| BOOLEAN_LABELS.contains(l));
if !has_boolean {
return None;
}
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let all_single_char = non_empty.iter().all(|v| v.chars().count() == 1);
let all_digits = non_empty
.iter()
.all(|v| v.chars().all(|c| c.is_ascii_digit()));
if all_single_char && !all_digits {
let mut unique_chars: Vec<&str> = non_empty.clone();
unique_chars.sort();
unique_chars.dedup();
if unique_chars.len() >= 2 {
let is_boolean_set = unique_chars.len() == 2 && {
let set: std::collections::HashSet<&str> = unique_chars.iter().copied().collect();
set.contains("0") && set.contains("1")
|| set.contains("T") && set.contains("F")
|| set.contains("t") && set.contains("f")
|| set.contains("Y") && set.contains("N")
|| set.contains("y") && set.contains("n")
};
if !is_boolean_set {
return Some((
"representation.text.word".to_string(),
"boolean_override_single_char_categorical".to_string(),
));
}
}
}
let parsed: Vec<i64> = values
.iter()
.filter_map(|v| v.trim().parse::<i64>().ok())
.collect();
if parsed.len() >= 3 {
let mut unique: Vec<i64> = parsed.clone();
unique.sort();
unique.dedup();
let n_unique = unique.len();
let min = *unique.first().unwrap();
let max = *unique.last().unwrap();
if n_unique < 2 {
return Some((
"representation.numeric.integer_number".to_string(),
"boolean_override_single_value".to_string(),
));
}
let dominant_count = parsed
.iter()
.filter(|&&v| v == parsed[0])
.count()
.max(parsed.iter().filter(|&&v| v != parsed[0]).count());
let dominant_frac = dominant_count as f64 / parsed.len() as f64;
if n_unique == 2 && dominant_frac > 0.95 {
return Some((
"representation.numeric.integer_number".to_string(),
"boolean_override_skewed".to_string(),
));
}
if n_unique > 2 && (max - min) > 1 {
return Some((
"representation.numeric.integer_number".to_string(),
"boolean_override_integer_spread".to_string(),
));
}
}
None
}
pub(crate) fn detect_epoch_seconds(values: &[String]) -> Option<String> {
const EPOCH_MIN: i64 = 946_684_800; const EPOCH_MAX: i64 = 2_524_608_000; const EPOCH_MS_MIN: i64 = EPOCH_MIN * 1000;
const EPOCH_MS_MAX: i64 = EPOCH_MAX * 1000;
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let mut epoch_sec_count = 0usize;
let mut epoch_ms_count = 0usize;
let mut parseable_count = 0usize;
for val in &non_empty {
let num: Option<i64> = val.parse::<i64>().ok().or_else(|| {
val.parse::<f64>().ok().and_then(|f| {
if f.fract() == 0.0 {
Some(f as i64)
} else {
None
}
})
});
if let Some(n) = num {
parseable_count += 1;
if (EPOCH_MIN..=EPOCH_MAX).contains(&n) {
epoch_sec_count += 1;
} else if (EPOCH_MS_MIN..=EPOCH_MS_MAX).contains(&n) {
epoch_ms_count += 1;
}
}
}
let n = non_empty.len();
if parseable_count < n * 80 / 100 {
return None;
}
if epoch_sec_count >= parseable_count * 80 / 100 {
Some("datetime.epoch.unix_seconds".to_string())
} else if epoch_ms_count >= parseable_count * 80 / 100 {
Some("datetime.epoch.unix_milliseconds".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_slash_dates(values: &[String]) -> Option<String> {
let mut first_over_12 = false;
let mut second_over_12 = false;
for val in values {
let parts: Vec<&str> = val.split('/').collect();
if parts.len() >= 2 {
if let Ok(first) = parts[0].parse::<u32>() {
if first > 12 {
first_over_12 = true;
}
}
if let Ok(second) = parts[1].parse::<u32>() {
if second > 12 {
second_over_12 = true;
}
}
}
}
if first_over_12 && !second_over_12 {
Some("datetime.date.dmy_slash".to_string())
} else if second_over_12 && !first_over_12 {
Some("datetime.date.mdy_slash".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_short_dates(values: &[String]) -> Option<String> {
let mut first_over_12 = false;
let mut second_over_12 = false;
for val in values {
let parts: Vec<&str> = val.split('-').collect();
if parts.len() >= 2 {
if let Ok(first) = parts[0].parse::<u32>() {
if first > 12 {
first_over_12 = true;
}
}
if let Ok(second) = parts[1].parse::<u32>() {
if second > 12 {
second_over_12 = true;
}
}
}
}
if first_over_12 && !second_over_12 {
Some("datetime.date.short_dmy".to_string())
} else if second_over_12 && !first_over_12 {
Some("datetime.date.short_mdy".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_coordinates(values: &[String]) -> Option<String> {
let mut any_over_90 = false;
let mut all_parseable = true;
let mut parsed_count = 0;
for val in values {
if let Ok(v) = val.trim().parse::<f64>() {
parsed_count += 1;
if v.abs() > 90.0 {
any_over_90 = true;
}
} else {
all_parseable = false;
}
}
if parsed_count < 3 {
return None;
}
if any_over_90 {
Some("geography.coordinate.longitude".to_string())
} else if all_parseable {
Some("geography.coordinate.latitude".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_ipv4(values: &[String]) -> Option<String> {
let non_empty: Vec<&str> = values
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let mut ipv4_count = 0;
for val in &non_empty {
let parts: Vec<&str> = val.split('.').collect();
if parts.len() == 4 {
let all_valid = parts.iter().all(|p| {
p.parse::<u16>()
.map(|n| n <= 255 && !p.is_empty())
.unwrap_or(false)
});
if all_valid {
ipv4_count += 1;
}
}
}
let fraction = ipv4_count as f64 / non_empty.len() as f64;
if fraction >= 0.8 {
Some("technology.internet.ip_v4".to_string())
} else {
None
}
}
pub(crate) fn disambiguate_numeric(
values: &[String],
results: &[ClassificationResult],
top_labels: &[&str],
) -> Option<(String, String)> {
let numeric_types = [
"representation.identifier.increment",
"representation.numeric.integer_number",
"representation.numeric.decimal_number",
"geography.address.postal_code",
"datetime.component.year",
];
let has_numeric_confusion = top_labels.iter().any(|l| numeric_types.contains(l));
if !has_numeric_confusion {
return None;
}
let parsed: Vec<i64> = values
.iter()
.filter_map(|v| v.trim().parse::<i64>().ok())
.collect();
if parsed.len() < 3 {
return None;
}
let min = *parsed.iter().min().unwrap();
let max = *parsed.iter().max().unwrap();
let range = max - min;
let mut sorted = parsed.clone();
sorted.sort();
sorted.dedup();
let is_sequential = if sorted.len() >= 3 {
let diffs: Vec<i64> = sorted.windows(2).map(|w| w[1] - w[0]).collect();
let avg_diff = diffs.iter().sum::<i64>() as f64 / diffs.len() as f64;
let variance = diffs
.iter()
.map(|d| (*d as f64 - avg_diff).powi(2))
.sum::<f64>()
/ diffs.len() as f64;
variance < (avg_diff * 0.5).powi(2) && avg_diff > 0.0
} else {
false
};
let all_positive = min > 0;
let typical_postal_range = all_positive && max <= 99999 && min >= 100;
let digit_lengths: Vec<usize> = values
.iter()
.filter_map(|v| {
let trimmed = v.trim();
if trimmed.chars().all(|c| c.is_ascii_digit()) {
Some(trimmed.len())
} else {
None
}
})
.collect();
let consistent_digits = if !digit_lengths.is_empty() {
let first_len = digit_lengths[0];
digit_lengths.iter().all(|&l| l == first_len)
} else {
false
};
let year_candidates: Vec<i64> = parsed
.iter()
.filter(|&&v| (1900..=2100).contains(&v))
.copied()
.collect();
let count_trimmed_4digit = values
.iter()
.filter(|v| {
let t = v.trim();
t.len() == 4 && t.chars().all(|c| c.is_ascii_digit())
})
.count();
let fraction_4digit = if values.is_empty() {
0.0
} else {
count_trimmed_4digit as f64 / values.len() as f64
};
let mostly_4digit = fraction_4digit >= 0.8;
let year_fraction = if parsed.is_empty() {
0.0
} else {
year_candidates.len() as f64 / parsed.len() as f64
};
let is_year_column = year_fraction >= 0.8 && parsed.len() >= 3 && mostly_4digit;
if is_year_column {
return Some((
"datetime.component.year".to_string(),
"numeric_year_detection".to_string(),
));
}
if is_sequential && min >= 0 && range > 0 {
return Some((
"representation.identifier.increment".to_string(),
"numeric_sequential_detection".to_string(),
));
}
if consistent_digits && typical_postal_range && !is_sequential {
if mostly_4digit && year_fraction >= 0.8 {
return Some((
"datetime.component.year".to_string(),
"numeric_year_detection".to_string(),
));
}
if digit_lengths.iter().all(|&l| l == 3) {
let status_count = parsed.iter().filter(|&&v| (100..=599).contains(&v)).count();
let status_rate = status_count as f64 / parsed.len() as f64;
if status_rate >= 0.90 {
return None;
}
}
return Some((
"geography.address.postal_code".to_string(),
"numeric_postal_code_detection".to_string(),
));
}
let _ = results; None
}
pub(crate) fn disambiguate_si_number(values: &[String]) -> Option<(String, String)> {
const SI_SUFFIXES: &[char] = &['K', 'k', 'M', 'm', 'B', 'b', 'T', 't', 'G', 'g'];
let has_si_suffix = values.iter().any(|v| {
let trimmed = v.trim();
if trimmed.is_empty() {
return false;
}
trimmed
.chars()
.last()
.is_some_and(|c| SI_SUFFIXES.contains(&c))
});
if !has_si_suffix {
Some((
"representation.numeric.decimal_number".to_string(),
"si_number_override_no_suffix".to_string(),
))
} else {
None
}
}
pub(crate) fn disambiguate_duration_override(values: &[String]) -> Option<(String, String)> {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let duration_count = non_empty
.iter()
.filter(|v| {
let s = v.as_bytes();
if s.is_empty() || s[0] != b'P' {
return false;
}
let after_p = &s[1..];
after_p
.iter()
.any(|&b| matches!(b, b'Y' | b'M' | b'W' | b'D' | b'T' | b'H' | b'S'))
})
.count();
let fraction = duration_count as f64 / non_empty.len() as f64;
if fraction >= 0.5 {
Some((
"datetime.duration.iso_8601".to_string(),
"duration_override_sedol".to_string(),
))
} else {
None
}
}
pub(crate) fn disambiguate_utc_offset_override(values: &[String]) -> Option<(String, String)> {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return None;
}
let offset_count = non_empty
.iter()
.filter(|v| {
let bytes = v.as_bytes();
if bytes.len() != 6 {
return false;
}
if bytes[0] != b'+' && bytes[0] != b'-' {
return false;
}
bytes[1].is_ascii_digit()
&& bytes[2].is_ascii_digit()
&& bytes[3] == b':'
&& bytes[4].is_ascii_digit()
&& bytes[5].is_ascii_digit()
})
.count();
let fraction = offset_count as f64 / non_empty.len() as f64;
if fraction >= 0.8 {
Some((
"datetime.offset.utc".to_string(),
"utc_offset_override_time".to_string(),
))
} else {
None
}
}
pub(crate) fn disambiguate_text_length_demotion(
values: &[String],
votes: &[(String, usize)],
) -> Option<(String, String)> {
let top_label = votes.first().map(|(l, _)| l.as_str())?;
const LONG_PROSE_SOURCES: &[&str] = &[
"geography.address.full_address",
"representation.text.entity_name",
"representation.text.word",
];
if !LONG_PROSE_SOURCES.contains(&top_label) {
return None;
}
let mut lengths: Vec<usize> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.map(|v| v.len())
.collect();
if lengths.len() < 3 {
return None;
}
lengths.sort_unstable();
let median = lengths[lengths.len() / 2];
if median > 100 {
let rule = if top_label == "geography.address.full_address" {
"text_length_demotion_full_address".to_string()
} else {
format!("text_length_demotion_long_prose:{top_label}")
};
Some(("representation.text.plain_text".to_string(), rule))
} else {
None
}
}
pub(crate) fn disambiguate_full_address_whitespace_guard(
values: &[String],
) -> Option<(String, String)> {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 4 {
return None;
}
let with_space = non_empty
.iter()
.filter(|v| v.chars().any(char::is_whitespace))
.count();
let space_frac = with_space as f32 / non_empty.len() as f32;
if space_frac > 0.15 {
return None;
}
let alnum = non_empty
.iter()
.filter(|v| {
v.chars().any(|c| c.is_ascii_alphabetic()) && v.chars().any(|c| c.is_ascii_digit())
})
.count();
if (alnum as f32 / non_empty.len() as f32) < 0.80 {
return None;
}
Some((
"representation.identifier.alphanumeric_id".to_string(),
format!("full_address_whitespace_guard:space_frac={space_frac:.2}"),
))
}
pub(crate) fn disambiguate_attractor_demotion(
values: &[String],
votes: &[(String, usize)],
n_samples: usize,
taxonomy: Option<&Taxonomy>,
) -> Option<(String, String)> {
let (top_label, top_count) = votes.first()?;
let majority_fraction = *top_count as f32 / n_samples as f32;
let is_numeric = NUMERIC_ATTRACTORS.contains(&top_label.as_str());
let is_text = TEXT_ATTRACTORS.contains(&top_label.as_str());
let is_code = CODE_ATTRACTORS.contains(&top_label.as_str());
if !is_numeric && !is_text && !is_code {
return None;
}
let mut locale_confirmed = false;
let mut validation_confirmed = false;
let mut has_locale_validators = false;
if let Some(taxonomy) = taxonomy {
let has_precise_validation = taxonomy
.get(top_label)
.and_then(|d| d.validation.as_ref())
.map(|v| v.is_precise())
.unwrap_or(false);
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() >= 3 {
if let Some(locale_validators) = taxonomy.get_locale_validators(top_label) {
has_locale_validators = true;
let mut best_pass_rate: f32 = 0.0;
for compiled in locale_validators.values() {
let pass_count = non_empty.iter().filter(|v| compiled.is_valid(v)).count();
let pass_rate = pass_count as f32 / non_empty.len() as f32;
if pass_rate > best_pass_rate {
best_pass_rate = pass_rate;
}
}
if best_pass_rate > 0.5 {
locale_confirmed = true;
}
}
if !locale_confirmed {
let fail_count = if let Some(compiled) = taxonomy.get_validator(top_label) {
non_empty.iter().filter(|v| !compiled.is_valid(v)).count()
} else if let Some(def) = taxonomy.get(top_label) {
if let Some(validation) = &def.validation {
non_empty
.iter()
.filter(|v| {
finetype_core::validate_value(v, validation)
.map(|r| !r.is_valid)
.unwrap_or(false)
})
.count()
} else {
0
}
} else {
0
};
let fail_rate = fail_count as f32 / non_empty.len() as f32;
if fail_rate > 0.5 {
let fallback = select_fallback(votes, is_numeric, is_text, is_code, values);
return Some((
fallback,
format!("attractor_demotion_validation:{}", top_label),
));
}
if has_precise_validation && fail_rate <= 0.3 {
validation_confirmed = true;
}
}
}
}
let confirmed = locale_confirmed || (!has_locale_validators && validation_confirmed);
if !confirmed && majority_fraction < 0.85 {
let fallback = select_fallback(votes, is_numeric, is_text, is_code, values);
return Some((
fallback,
format!("attractor_demotion_confidence:{}", top_label),
));
}
if is_text && !locale_confirmed {
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
let mut unique: Vec<&str> = non_empty.clone();
unique.sort();
unique.dedup();
if (1..=20).contains(&unique.len()) {
return Some((
"representation.text.word".to_string(),
format!("attractor_demotion_cardinality:{}", top_label),
));
}
}
None
}
pub(crate) fn schema_validation_gate(
result: &mut ColumnResult,
values: &[String],
votes: &[(String, usize)],
taxonomy: &Taxonomy,
) {
if result.label.starts_with("representation.") {
return;
}
if taxonomy.get_locale_validators(&result.label).is_some() {
return;
}
let Some(validator) = taxonomy.get_validator(&result.label) else {
return;
};
let non_empty: Vec<&str> = values
.iter()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.collect();
if non_empty.len() < 3 {
return;
}
let fail_count = non_empty.iter().filter(|v| !validator.is_valid(v)).count();
let fail_rate = fail_count as f32 / non_empty.len() as f32;
if fail_rate <= 0.5 {
return;
}
let demoted = votes
.iter()
.find(|(label, _)| {
label != &result.label
&& taxonomy
.get_validator(label)
.map(|alt| {
let pass = non_empty.iter().filter(|v| alt.is_valid(v)).count();
pass as f32 / non_empty.len() as f32 > 0.5
})
.unwrap_or(false)
})
.map(|(label, _)| label.clone())
.unwrap_or_else(|| {
let is_numeric = non_empty.iter().all(|v| v.parse::<f64>().is_ok());
select_fallback(votes, is_numeric, !is_numeric, false, values)
});
if demoted != result.label {
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!("schema_validation_gate:{}", result.label));
result.label = demoted;
result.confidence = result.confidence.min(0.6);
result.detected_locale = None;
}
}
pub(crate) fn select_fallback(
votes: &[(String, usize)],
is_numeric: bool,
is_text: bool,
is_code: bool,
values: &[String],
) -> String {
for (label, _) in votes.iter().skip(1) {
if label.starts_with("representation.") {
return label.clone();
}
}
if is_numeric {
let has_decimal = values.iter().any(|v| v.contains('.'));
if has_decimal {
"representation.numeric.decimal_number".to_string()
} else {
"representation.numeric.integer_number".to_string()
}
} else if is_text {
"representation.text.word".to_string()
} else if is_code {
"representation.identifier.alphanumeric_id".to_string()
} else {
"representation.text.word".to_string()
}
}