///|
/// Switches for common Pinyin input confusions.
pub struct FuzzyRules {
zh_z_enabled : Bool
ch_c_enabled : Bool
sh_s_enabled : Bool
n_l_enabled : Bool
} derive(Eq, Debug)
///|
/// Validated edit costs and equivalence rules.
pub struct FuzzyProfile {
insertion_value : Double
deletion_value : Double
substitution_value : Double
equivalence_value : Double
rule_values : FuzzyRules
} derive(Eq, Debug)
///|
priv struct DistanceCell {
total : Double
insertions : Int
deletions : Int
substitutions : Int
equivalences : Int
}
///|
/// Weighted distance and operation counts for auditability.
pub struct DistanceResult {
total_value : Double
insertion_count : Int
deletion_count : Int
substitution_count : Int
equivalence_count : Int
} derive(Eq, Debug)
///|
pub fn fuzzy_rules(
zh_z : Bool,
ch_c : Bool,
sh_s : Bool,
n_l : Bool,
) -> FuzzyRules {
{
zh_z_enabled: zh_z,
ch_c_enabled: ch_c,
sh_s_enabled: sh_s,
n_l_enabled: n_l,
}
}
///|
pub fn default_fuzzy_rules() -> FuzzyRules {
fuzzy_rules(true, true, true, true)
}
///|
fn validate_edit_cost(
name : String,
value : Double,
) -> Result[Unit, PinyinError] {
if value.is_nan() || value.is_inf() || value <= 0.0 || value > 10.0 {
Err(InvalidWeight(name, value))
} else {
Ok(())
}
}
///|
/// Builds a profile with the standard Pinyin equivalence groups enabled.
pub fn fuzzy_profile(
insertion : Double,
deletion : Double,
substitution : Double,
equivalence : Double,
) -> Result[FuzzyProfile, PinyinError] {
fuzzy_profile_with_rules(
insertion,
deletion,
substitution,
equivalence,
default_fuzzy_rules(),
)
}
///|
/// Builds a profile with caller-selected Pinyin equivalence groups.
pub fn fuzzy_profile_with_rules(
insertion : Double,
deletion : Double,
substitution : Double,
equivalence : Double,
rules : FuzzyRules,
) -> Result[FuzzyProfile, PinyinError] {
match validate_edit_cost("insertion", insertion) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_edit_cost("deletion", deletion) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_edit_cost("substitution", substitution) {
Err(error) => return Err(error)
Ok(_) => ()
}
if equivalence.is_nan() ||
equivalence.is_inf() ||
equivalence < 0.0 ||
equivalence > 1.0 {
return Err(InvalidWeight("equivalence", equivalence))
}
Ok({
insertion_value: insertion,
deletion_value: deletion,
substitution_value: substitution,
equivalence_value: equivalence,
rule_values: rules,
})
}
///|
pub fn default_fuzzy_profile() -> FuzzyProfile {
fuzzy_profile(1.0, 1.0, 1.0, 0.25).unwrap()
}
///|
pub fn FuzzyProfile::insertion_cost(self : FuzzyProfile) -> Double {
self.insertion_value
}
///|
pub fn FuzzyProfile::deletion_cost(self : FuzzyProfile) -> Double {
self.deletion_value
}
///|
pub fn FuzzyProfile::substitution_cost(self : FuzzyProfile) -> Double {
self.substitution_value
}
///|
pub fn FuzzyProfile::equivalence_cost(self : FuzzyProfile) -> Double {
self.equivalence_value
}
///|
pub fn FuzzyProfile::rules(self : FuzzyProfile) -> FuzzyRules {
self.rule_values
}
///|
pub fn FuzzyRules::zh_z_enabled(self : FuzzyRules) -> Bool {
self.zh_z_enabled
}
///|
pub fn FuzzyRules::ch_c_enabled(self : FuzzyRules) -> Bool {
self.ch_c_enabled
}
///|
pub fn FuzzyRules::sh_s_enabled(self : FuzzyRules) -> Bool {
self.sh_s_enabled
}
///|
pub fn FuzzyRules::n_l_enabled(self : FuzzyRules) -> Bool {
self.n_l_enabled
}
///|
fn empty_distance_cell(total : Double) -> DistanceCell {
{ total, insertions: 0, deletions: 0, substitutions: 0, equivalences: 0 }
}
///|
fn distance_operation_count(cell : DistanceCell) -> Int {
cell.insertions + cell.deletions + cell.substitutions + cell.equivalences
}
///|
fn distance_cell_better(
candidate : DistanceCell,
current : DistanceCell,
) -> Bool {
if candidate.total < current.total {
true
} else if candidate.total > current.total {
false
} else {
distance_operation_count(candidate) < distance_operation_count(current)
}
}
///|
fn relax_distance(
cells : Array[DistanceCell],
index : Int,
candidate : DistanceCell,
) -> Unit {
if distance_cell_better(candidate, cells[index]) {
cells[index] = candidate
}
}
///|
fn add_insertion(cell : DistanceCell, cost : Double) -> DistanceCell {
{
total: cell.total + cost,
insertions: cell.insertions + 1,
deletions: cell.deletions,
substitutions: cell.substitutions,
equivalences: cell.equivalences,
}
}
///|
fn add_deletion(cell : DistanceCell, cost : Double) -> DistanceCell {
{
total: cell.total + cost,
insertions: cell.insertions,
deletions: cell.deletions + 1,
substitutions: cell.substitutions,
equivalences: cell.equivalences,
}
}
///|
fn add_substitution(cell : DistanceCell, cost : Double) -> DistanceCell {
{
total: cell.total + cost,
insertions: cell.insertions,
deletions: cell.deletions,
substitutions: cell.substitutions + 1,
equivalences: cell.equivalences,
}
}
///|
fn add_equivalence(cell : DistanceCell, cost : Double) -> DistanceCell {
{
total: cell.total + cost,
insertions: cell.insertions,
deletions: cell.deletions,
substitutions: cell.substitutions,
equivalences: cell.equivalences + 1,
}
}
///|
fn is_initial_pair(single : Int, pair_first : Int, pair_second : Int) -> Int {
if single == 122 && pair_first == 122 && pair_second == 104 {
1
} else if single == 99 && pair_first == 99 && pair_second == 104 {
2
} else if single == 115 && pair_first == 115 && pair_second == 104 {
3
} else {
0
}
}
///|
fn pair_rule_enabled(kind : Int, rules : FuzzyRules) -> Bool {
match kind {
1 => rules.zh_z_enabled
2 => rules.ch_c_enabled
3 => rules.sh_s_enabled
_ => false
}
}
///|
fn can_single_pair(
single_text : String,
single_index : Int,
pair_text : String,
pair_index : Int,
rules : FuzzyRules,
) -> Bool {
if single_index >= single_text.length() ||
pair_index + 1 >= pair_text.length() {
return false
}
pair_rule_enabled(
is_initial_pair(
single_text[single_index].to_int(),
pair_text[pair_index].to_int(),
pair_text[pair_index + 1].to_int(),
),
rules,
)
}
///|
fn can_n_l(
left : String,
left_index : Int,
right : String,
right_index : Int,
rules : FuzzyRules,
) -> Bool {
if !rules.n_l_enabled {
return false
}
let left_code = left[left_index].to_int()
let right_code = right[right_index].to_int()
(left_code == 110 && right_code == 108) ||
(left_code == 108 && right_code == 110)
}
///|
/// Computes weighted Levenshtein distance with optional Pinyin initial rules.
pub fn weighted_pinyin_distance(
left_input : String,
right_input : String,
profile : FuzzyProfile,
) -> DistanceResult {
let left = normalize_literal(left_input)
let right = normalize_literal(right_input)
let rows = left.length() + 1
let columns = right.length() + 1
let cells = Array::make(rows * columns, empty_distance_cell(1000000000.0))
cells[0] = empty_distance_cell(0.0)
for left_index = 0; left_index < rows; left_index = left_index + 1 {
for right_index = 0; right_index < columns; right_index = right_index + 1 {
let cell_index = left_index * columns + right_index
let current = cells[cell_index]
if left_index < left.length() {
relax_distance(
cells,
(left_index + 1) * columns + right_index,
add_deletion(current, profile.deletion_value),
)
}
if right_index < right.length() {
relax_distance(
cells,
left_index * columns + right_index + 1,
add_insertion(current, profile.insertion_value),
)
}
if left_index < left.length() && right_index < right.length() {
let diagonal = if left[left_index] == right[right_index] {
current
} else if can_n_l(
left,
left_index,
right,
right_index,
profile.rule_values,
) {
add_equivalence(current, profile.equivalence_value)
} else {
add_substitution(current, profile.substitution_value)
}
relax_distance(
cells,
(left_index + 1) * columns + right_index + 1,
diagonal,
)
}
if can_single_pair(
left,
left_index,
right,
right_index,
profile.rule_values,
) {
relax_distance(
cells,
(left_index + 1) * columns + right_index + 2,
add_equivalence(current, profile.equivalence_value),
)
}
if can_single_pair(
right,
right_index,
left,
left_index,
profile.rule_values,
) {
relax_distance(
cells,
(left_index + 2) * columns + right_index + 1,
add_equivalence(current, profile.equivalence_value),
)
}
}
}
let result = cells[rows * columns - 1]
{
total_value: result.total,
insertion_count: result.insertions,
deletion_count: result.deletions,
substitution_count: result.substitutions,
equivalence_count: result.equivalences,
}
}
///|
pub fn DistanceResult::cost(self : DistanceResult) -> Double {
self.total_value
}
///|
pub fn DistanceResult::insertions(self : DistanceResult) -> Int {
self.insertion_count
}
///|
pub fn DistanceResult::deletions(self : DistanceResult) -> Int {
self.deletion_count
}
///|
pub fn DistanceResult::substitutions(self : DistanceResult) -> Int {
self.substitution_count
}
///|
pub fn DistanceResult::equivalences(self : DistanceResult) -> Int {
self.equivalence_count
}
///|
pub fn DistanceResult::operations(self : DistanceResult) -> Int {
self.insertion_count +
self.deletion_count +
self.substitution_count +
self.equivalence_count
}