///|
fn invalid_match_number(value : Double) -> Bool {
value.is_nan() || value.is_inf()
}
///|
/// Returns a copy with a different decision threshold.
pub fn MatchConfig::with_threshold(
self : MatchConfig,
threshold : Double,
) -> MatchConfig {
{
normalization: self.normalization,
encoders: self.encoders.copy(),
metric: self.metric,
string_weight: self.string_weight,
threshold,
token_policy: self.token_policy,
unmatched_token_penalty: self.unmatched_token_penalty,
}
}
///|
/// Returns a copy with a different string-similarity metric.
pub fn MatchConfig::with_metric(
self : MatchConfig,
metric : SimilarityMetric,
) -> MatchConfig {
{
normalization: self.normalization,
encoders: self.encoders.copy(),
metric,
string_weight: self.string_weight,
threshold: self.threshold,
token_policy: self.token_policy,
unmatched_token_penalty: self.unmatched_token_penalty,
}
}
///|
/// Returns a copy with a different string component weight.
pub fn MatchConfig::with_string_weight(
self : MatchConfig,
string_weight : Double,
) -> MatchConfig {
{
normalization: self.normalization,
encoders: self.encoders.copy(),
metric: self.metric,
string_weight,
threshold: self.threshold,
token_policy: self.token_policy,
unmatched_token_penalty: self.unmatched_token_penalty,
}
}
///|
/// Returns a copy with different token assignment and penalty settings.
pub fn MatchConfig::with_token_settings(
self : MatchConfig,
token_policy : TokenPolicy,
unmatched_token_penalty : Double,
) -> MatchConfig {
{
normalization: self.normalization,
encoders: self.encoders.copy(),
metric: self.metric,
string_weight: self.string_weight,
threshold: self.threshold,
token_policy,
unmatched_token_penalty,
}
}
///|
/// Returns a copy with a separate encoder array owned by the new config.
pub fn MatchConfig::with_encoders(
self : MatchConfig,
encoders : Array[EncoderWeight],
) -> MatchConfig {
{
normalization: self.normalization,
encoders: encoders.copy(),
metric: self.metric,
string_weight: self.string_weight,
threshold: self.threshold,
token_policy: self.token_policy,
unmatched_token_penalty: self.unmatched_token_penalty,
}
}
///|
/// Returns a copy with a different normalization policy.
pub fn MatchConfig::with_normalization(
self : MatchConfig,
normalization : NormalizationConfig,
) -> MatchConfig {
{
normalization,
encoders: self.encoders.copy(),
metric: self.metric,
string_weight: self.string_weight,
threshold: self.threshold,
token_policy: self.token_policy,
unmatched_token_penalty: self.unmatched_token_penalty,
}
}
///|
fn validate_match_metric(metric : SimilarityMetric) -> Result[Unit, MatchError] {
match metric {
JaroWinkler(scaling) =>
if invalid_match_number(scaling) || scaling < 0.0 || scaling > 0.25 {
Err(InvalidSimilarityMetric(InvalidWinklerScaling(scaling)))
} else {
Ok(())
}
Dice(size) =>
if size < 1 || size > 64 {
Err(InvalidSimilarityMetric(InvalidNGramSize(size)))
} else {
Ok(())
}
Levenshtein | Jaro => Ok(())
}
}
///|
/// Validates every nested policy and score parameter before matching.
pub fn validate_match_config(config : MatchConfig) -> Result[Unit, MatchError] {
match validate_normalization_config(config.normalization) {
Err(error) => return Err(InvalidNormalizationConfig(error))
Ok(_) => ()
}
if invalid_match_number(config.threshold) ||
config.threshold < 0.0 ||
config.threshold > 1.0 {
return Err(InvalidThreshold(config.threshold))
}
if invalid_match_number(config.string_weight) || config.string_weight < 0.0 {
return Err(InvalidStringWeight(config.string_weight))
}
if invalid_match_number(config.unmatched_token_penalty) ||
config.unmatched_token_penalty < 0.0 ||
config.unmatched_token_penalty > 1.0 {
return Err(InvalidUnmatchedTokenPenalty(config.unmatched_token_penalty))
}
match validate_match_metric(config.metric) {
Err(error) => return Err(error)
Ok(_) => ()
}
let mut positive_components = if config.string_weight > 0.0 { 1 } else { 0 }
for index = 0; index < config.encoders.length(); index = index + 1 {
let encoder = config.encoders[index]
if invalid_match_number(encoder.weight) || encoder.weight < 0.0 {
return Err(InvalidEncoderWeight(encoder.algorithm, encoder.weight))
}
if encoder.weight > 0.0 {
positive_components = positive_components + 1
}
for earlier = 0; earlier < index; earlier = earlier + 1 {
if config.encoders[earlier].algorithm == encoder.algorithm {
return Err(DuplicateEncoder(encoder.algorithm))
}
}
}
if positive_components == 0 {
Err(NoPositiveComponents)
} else {
Ok(())
}
}
///|
/// Returns a high-threshold whole-name strategy.
pub fn conservative_match_config() -> MatchConfig {
{
normalization: name_normalization_config(),
encoders: [
{ algorithm: DoubleMetaphone, weight: 0.45 },
{ algorithm: Nysiis, weight: 0.20 },
],
metric: JaroWinkler(0.1),
string_weight: 0.35,
threshold: 0.88,
token_policy: WholeInput,
unmatched_token_penalty: 0.5,
}
}
///|
/// Returns the default mixed phonetic and token-aware strategy.
pub fn balanced_match_config() -> MatchConfig {
{
normalization: name_normalization_config(),
encoders: [
{ algorithm: DoubleMetaphone, weight: 0.30 },
{ algorithm: Metaphone, weight: 0.15 },
{ algorithm: Nysiis, weight: 0.15 },
],
metric: JaroWinkler(0.1),
string_weight: 0.40,
threshold: 0.78,
token_policy: BestTokenPairs,
unmatched_token_penalty: 0.5,
}
}
///|
/// Returns a lower-threshold strategy intended to retain more candidates.
pub fn recall_match_config() -> MatchConfig {
{
normalization: name_normalization_config(),
encoders: [
{ algorithm: DoubleMetaphone, weight: 0.25 },
{ algorithm: Soundex, weight: 0.15 },
{ algorithm: Caverphone2, weight: 0.15 },
],
metric: Dice(2),
string_weight: 0.45,
threshold: 0.67,
token_policy: BestTokenPairs,
unmatched_token_penalty: 0.35,
}
}