///|
/// Additional deterministic rules for outpatient notes, laboratory records,
/// insurance forms, and mixed Chinese-English exports.
pub fn extended_rules() -> Array[Rule] {
[
rule(
"bank_card_context",
"Bank card number",
Insurance,
"(银行卡|借记卡|信用卡|Card)[:: ]{1,3}[0-9 -]{13,23}",
"[CARD]",
88,
contextual=true,
),
rule(
"birth_date_context",
"Date of birth",
Date,
"(出生日期|出生|DOB|Date of Birth)[:: ]{1,3}(19|20)\\d{2}[-/.年](0?[1-9]|1[0-2])[-/.月]([0-2]?\\d|3[0-1])日?",
"[DOB]",
94,
contextual=true,
),
rule(
"time_context",
"Clinical time",
Date,
"(就诊时间|采样时间|手术时间|Time)[:: ]{1,3}(19|20)\\d{2}[-/.年](0?[1-9]|1[0-2])[-/.月]([0-2]?\\d|3[0-1])([日T ]|$)",
"[TIME]",
78,
contextual=true,
),
rule(
"ipv4",
"IPv4 address",
Custom("network_address"),
"\\b([0-9]{1,3}\\.){3}[0-9]{1,3}\\b",
"[IP]",
84,
),
rule(
"url",
"URL",
Custom("url"),
"https?://[A-Za-z0-9./_?=&%:#-]{6,160}",
"[URL]",
82,
),
rule(
"uuid",
"UUID",
Custom("uuid"),
"\\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\b",
"[UUID]",
86,
),
rule(
"imei_context",
"Device IMEI",
Custom("device_identifier"),
"(IMEI|设备号|终端号)[:: ]{1,3}[0-9 -]{14,20}",
"[DEVICE]",
92,
contextual=true,
),
rule(
"mac_address",
"MAC address",
Custom("device_identifier"),
"\\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b",
"[DEVICE]",
90,
),
rule(
"license_plate",
"Vehicle license plate",
Custom("vehicle_identifier"),
"(车牌|车号)[:: ]{1,3}[A-Z\\u4e00-\\u9fff][A-Z][A-Z0-9]{5,6}",
"[PLATE]",
86,
contextual=true,
),
rule(
"postal_code_context",
"Postal code",
Address,
"(邮编|邮政编码|ZIP|Postal Code)[:: ]{1,3}[0-9A-Za-z -]{4,10}",
"[POSTAL]",
84,
contextual=true,
),
rule(
"lab_accession_context",
"Laboratory accession",
MedicalRecord,
"(检验号|样本号|标本号|Accession|Lab ID)[:: ]{1,3}[A-Za-z0-9_-]{4,24}",
"[LAB]",
91,
contextual=true,
),
rule(
"visit_id_context",
"Visit identifier",
MedicalRecord,
"(就诊号|门诊ID|Visit ID|Encounter)[:: ]{1,3}[A-Za-z0-9_-]{4,24}",
"[VISIT]",
90,
contextual=true,
),
rule(
"prescription_context",
"Prescription identifier",
MedicalRecord,
"(处方号|处方编号|Prescription)[:: ]{1,3}[A-Za-z0-9_-]{4,24}",
"[RX]",
88,
contextual=true,
),
rule(
"diagnosis_code_context",
"Diagnosis code",
MedicalRecord,
"(诊断编码|ICD-?10|Diagnosis Code)[:: ]{1,3}[A-Z][0-9]{2,4}(\\.[0-9A-Z]{1,4})?",
"[DIAGNOSIS]",
83,
contextual=true,
),
rule(
"fax_context",
"Fax number",
Phone,
"(传真|Fax)[:: ]{1,3}[+0-9() .-]{7,20}",
"[FAX]",
80,
contextual=true,
),
rule(
"wechat_context",
"WeChat identifier",
Custom("online_identifier"),
"(微信|WeChat|Vx)[:: ]{1,3}[A-Za-z][A-Za-z0-9_-]{5,30}",
"[ACCOUNT]",
87,
contextual=true,
),
rule(
"qq_context",
"QQ identifier",
Custom("online_identifier"),
"(QQ)[:: ]{1,3}[0-9]{5,12}",
"[ACCOUNT]",
85,
contextual=true,
),
rule(
"tax_identifier_context",
"Tax identifier",
Insurance,
"(统一社会信用代码|税号|Tax ID)[:: ]{1,3}[0-9A-Z-]{8,24}",
"[TAX_ID]",
88,
contextual=true,
),
rule(
"iban_context",
"IBAN",
Insurance,
"(IBAN|国际银行账号)[:: ]{1,3}[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}",
"[IBAN]",
90,
contextual=true,
),
rule(
"emergency_contact_context",
"Emergency contact",
PersonName,
"(紧急联系人|Emergency Contact)[:: ]{1,3}[\\u4e00-\\u9fff]{2,6}",
"[CONTACT]",
90,
contextual=true,
),
rule(
"operator_context",
"Operator or staff name",
PersonName,
"(医生|护士|经办人|Operator|Clinician)[:: ]{1,3}[\\u4e00-\\u9fffA-Z][\\u4e00-\\u9fffA-Za-z .'-]{1,24}",
"[STAFF]",
82,
contextual=true,
),
rule(
"street_address_en",
"English street address",
Address,
"\\b[0-9]{1,6} [A-Z][A-Za-z0-9 .'-]{3,48}(Street|St\\.|Road|Rd\\.|Avenue|Ave\\.|Lane|Ln\\.)\\b",
"[ADDRESS]",
80,
),
rule(
"passport_context",
"Passport identifier in context",
IdNumber,
"(护照号|Passport)[:: ]{1,3}[A-Z][A-Z0-9]{5,14}",
"[PASSPORT]",
92,
contextual=true,
),
rule(
"national_health_context",
"National health identifier",
Insurance,
"(健康卡号|医保卡号|Health ID)[:: ]{1,3}[A-Za-z0-9-]{6,24}",
"[HEALTH_ID]",
91,
contextual=true,
),
rule(
"email_context",
"Contextual email",
Email,
"(邮箱|Email|E-mail)[:: ]{1,3}[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}",
"[EMAIL]",
97,
contextual=true,
),
]
}
///|
pub fn comprehensive_rules() -> Array[Rule] {
let all = builtin_rules()
all.append(extended_rules())
all
}
///|
pub fn rules_for_kind(rules : Array[Rule], kind : PhiKind) -> Array[Rule] {
rules.filter(fn(item) { item.kind == kind })
}
///|
pub fn rules_above_confidence(
rules : Array[Rule],
threshold : Int,
) -> Array[Rule] {
rules.filter(fn(item) { item.confidence >= threshold })
}
///|
pub fn rule_ids(rules : Array[Rule]) -> Array[String] {
rules.map(fn(item) { item.id })
}
///|
pub fn rule_count_by_kind(rules : Array[Rule]) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in rules {
let key = phi_kind_name(item.kind)
result[key] = result.get_or_default(key, 0) + 1
}
result
}
///|
pub fn rule_confidence_histogram(rules : Array[Rule]) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in rules {
let key = "\{confidence_band(item.confidence)}"
result[key] = result.get_or_default(key, 0) + 1
}
result
}
///|
pub fn rule_ids_with_prefix(
rules : Array[Rule],
prefix : String,
) -> Array[String] {
rules
.filter(fn(item) { item.id.has_prefix(prefix) })
.map(fn(item) { item.id })
}
///|
pub fn rules_for_locale(
rules : Array[Rule],
locale : LocaleHint,
) -> Array[Rule] {
match locale {
Chinese =>
rules.filter(fn(item) {
item.id.has_prefix("cn_") ||
item.id.has_prefix("insurance") ||
item.id.contains("context") ||
item.id == "email"
})
English =>
rules.filter(fn(item) {
item.id.has_prefix("en_") ||
item.id == "us_phone" ||
item.id.contains("context") ||
item.id == "email"
})
Mixed | Neutral => rules
}
}
///|
pub fn disabled_rule_ids(rules : Array[Rule]) -> Array[String] {
rules.filter(fn(item) { !item.enabled }).map(fn(item) { item.id })
}
///|
pub fn rule_surface_report(rules : Array[Rule]) -> String {
let by_kind = rule_count_by_kind(rules)
let by_band = rule_confidence_histogram(rules)
let lines = [
"rules=\{rules.length()}",
"enabled=\{enabled_rules(rules).length()}",
"disabled=\{disabled_rule_ids(rules).length()}",
"kinds=\{by_kind.length()}",
"confidence_bands=\{by_band.length()}",
]
lines.join("\n")
}