///|
pub(all) struct MatchQuality {
finding_id : String
lexical : Int
contextual : Int
structural : Int
checksum : Int
total : Int
band : ConfidenceBand
} derive(Debug, Eq)
///|
pub fn lexical_score(finding : Finding) -> Int {
let value = finding.text
let mut score = 0
if !value.is_empty() {
score += 20
}
if value.length() >= 4 {
score += 20
}
if value.length() >= 8 {
score += 20
}
if contains_ascii_letter(value) {
score += 10
}
if all_ascii_digits(value) {
score += 20
}
if contains_cjk(value) {
score += 10
}
if score > 100 {
100
} else {
score
}
}
///|
pub fn contextual_score(input : String, finding : Finding, radius : Int) -> Int {
let window = finding_context(input, finding, radius)
let mut score = 0
let lower = window.text.to_lower()
if lower.contains("name") ||
lower.contains("姓名") ||
lower.contains("patient") {
score += 35
}
if lower.contains("phone") ||
lower.contains("电话") ||
lower.contains("tel") {
score += 30
}
if lower.contains("email") || lower.contains("邮箱") {
score += 25
}
if lower.contains("id") || lower.contains("号") {
score += 20
}
if score > 100 {
100
} else {
score
}
}
///|
pub fn structural_score(finding : Finding) -> Int {
let mut score = 0
if finding.start >= 0 && finding.end > finding.start {
score += 30
}
if finding.end - finding.start == finding.text.length() {
score += 30
}
if finding.replacement.length() > 0 {
score += 20
}
if finding.rule_id.length() > 0 {
score += 20
}
score
}
///|
pub fn checksum_score(finding : Finding) -> Int {
match finding.kind {
IdNumber => if valid_chinese_id(finding.text) { 100 } else { 20 }
Insurance =>
if valid_bank_card(finding.text) || valid_iban_prefix(finding.text) {
100
} else {
35
}
Phone => if valid_phone_number(finding.text) { 90 } else { 30 }
Email => if valid_email_address(finding.text) { 95 } else { 25 }
Custom(name) =>
if name == "device_identifier" && valid_imei(finding.text) {
100
} else {
40
}
_ => 50
}
}
///|
pub fn match_quality(
input : String,
finding : Finding,
radius : Int,
) -> MatchQuality {
let lexical = lexical_score(finding)
let contextual = contextual_score(input, finding, radius)
let structural = structural_score(finding)
let checksum = checksum_score(finding)
let total = (lexical + contextual + structural + checksum) / 4
{
finding_id: finding.id,
lexical,
contextual,
structural,
checksum,
total,
band: confidence_band(total),
}
}
///|
pub fn match_qualities(
input : String,
findings : Array[Finding],
radius : Int,
) -> Array[MatchQuality] {
findings.map(fn(item) { match_quality(input, item, radius) })
}
///|
pub fn quality_by_finding(qualities : Array[MatchQuality]) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for quality in qualities {
result[quality.finding_id] = quality.total
}
result
}
///|
pub fn quality_average(qualities : Array[MatchQuality]) -> Float {
if qualities.is_empty() {
0.0
} else {
Float::from_int(qualities.fold(init=0, (sum, item) => sum + item.total)) /
Float::from_int(qualities.length())
}
}
///|
pub fn quality_minimum(qualities : Array[MatchQuality]) -> Int {
match qualities.get(0) {
None => 0
Some(first) =>
qualities.fold(init=first.total, (current, item) => {
if item.total < current {
item.total
} else {
current
}
})
}
}
///|
pub fn quality_filter_findings(
input : String,
findings : Array[Finding],
threshold : Int,
radius : Int,
) -> Array[Finding] {
let qualities = match_qualities(input, findings, radius)
findings.filter(fn(item) {
let mut total = -1
for quality in qualities {
if quality.finding_id == item.id {
total = quality.total
}
}
total >= threshold
})
}
///|
pub fn quality_adjust_confidence(
input : String,
finding : Finding,
radius : Int,
) -> Finding {
let quality = match_quality(input, finding, radius)
let confidence = (finding.confidence + quality.total) / 2
{ ..finding, confidence: if confidence > 100 { 100 } else { confidence } }
}
///|
pub fn quality_adjust_findings(
input : String,
findings : Array[Finding],
radius : Int,
) -> Array[Finding] {
findings.map(fn(item) { quality_adjust_confidence(input, item, radius) })
}
///|
pub fn match_quality_report(
input : String,
findings : Array[Finding],
radius : Int,
) -> String {
let qualities = match_qualities(input, findings, radius)
let lines = [
"findings=\{findings.length()}",
"average=\{quality_average(qualities)}",
"minimum=\{quality_minimum(qualities)}",
]
for item in qualities {
lines.push(
"\{item.finding_id}: \{item.total} (\{confidence_band_name(item.band)})",
)
}
lines.join("\n")
}
///|
pub fn quality_is_consistent(input : String, finding : Finding) -> Bool {
validate_finding_text(input, finding) &&
finding.confidence >= 0 &&
finding.confidence <= 100
}
///|
pub fn quality_consistency_issues(
input : String,
findings : Array[Finding],
) -> Array[String] {
findings
.filter(fn(item) { !quality_is_consistent(input, item) })
.map(fn(item) { item.id })
}
///|
pub fn quality_by_kind(
input : String,
findings : Array[Finding],
radius : Int,
) -> Map[String, Float] {
let buckets : Map[String, Array[Int]] = Map([])
for item in match_qualities(input, findings, radius) {
let key = item.finding_id
let values = buckets.get_or_default(key, [])
values.push(item.total)
buckets[key] = values
}
let result : Map[String, Float] = Map([])
for key, values in buckets {
result[key] = Float::from_int(
values.fold(init=0, (sum, item) => sum + item),
) /
Float::from_int(values.length())
}
result
}