///|
pub enum IdentifierKind {
PhoneIdentifier
EmailIdentifier
GovernmentIdentifier
AccountIdentifier
DeviceIdentifier
LocationIdentifier
UnknownIdentifier
} derive(Debug, Eq)
///|
pub fn identifier_kind_values() -> Array[IdentifierKind] {
[
PhoneIdentifier,
EmailIdentifier,
GovernmentIdentifier,
AccountIdentifier,
DeviceIdentifier,
LocationIdentifier,
UnknownIdentifier,
]
}
///|
pub fn identifier_kind_name(kind : IdentifierKind) -> String {
match kind {
PhoneIdentifier => "phone"
EmailIdentifier => "email"
GovernmentIdentifier => "government"
AccountIdentifier => "account"
DeviceIdentifier => "device"
LocationIdentifier => "location"
UnknownIdentifier => "unknown"
}
}
///|
pub fn normalize_phone(value : String) -> String {
let digits = keep_digits(value)
if digits.has_prefix("0086") {
"+86" + digits[4:].to_owned()
} else if digits.has_prefix("86") && digits.length() == 13 {
"+86" + digits[2:].to_owned()
} else {
digits
}
}
///|
pub fn phone_region_hint(value : String) -> String {
let normalized = normalize_phone(value)
if normalized.has_prefix("+86") {
"CN"
} else if normalized.has_prefix("+1") {
"US/CA"
} else if normalized.has_prefix("+44") {
"GB"
} else {
"UNKNOWN"
}
}
///|
pub fn normalize_email(value : String) -> String {
trim_ascii_space(value).to_lower()
}
///|
pub fn email_domain(value : String) -> String {
match normalize_email(value).split_once("@") {
Some((_, domain)) => domain.to_owned()
None => ""
}
}
///|
pub fn email_local_part(value : String) -> String {
match normalize_email(value).split_once("@") {
Some((local_part, _)) => local_part.to_owned()
None => ""
}
}
///|
pub fn normalize_identifier(value : String, kind : IdentifierKind) -> String {
match kind {
PhoneIdentifier => normalize_phone(value)
EmailIdentifier => normalize_email(value)
GovernmentIdentifier | AccountIdentifier | DeviceIdentifier =>
normalized_identifier(value)
LocationIdentifier | UnknownIdentifier => trim_ascii_space(value)
}
}
///|
pub fn identifier_is_reversible(value : String, kind : IdentifierKind) -> Bool {
match kind {
PhoneIdentifier
| EmailIdentifier
| GovernmentIdentifier
| AccountIdentifier
| DeviceIdentifier => !normalize_identifier(value, kind).is_empty()
LocationIdentifier | UnknownIdentifier => false
}
}
///|
pub fn mask_identifier(value : String, kind : IdentifierKind) -> String {
let normalized = normalize_identifier(value, kind)
match kind {
PhoneIdentifier => {
let digits = keep_digits(normalized)
if digits.length() <= 4 {
"****"
} else {
digits[:3].to_owned() + "****" + digits[digits.length() - 4:].to_owned()
}
}
EmailIdentifier => {
let local_part = email_local_part(normalized)
let domain = email_domain(normalized)
if local_part.length() <= 1 {
"*" + "@" + domain
} else {
local_part[:1].to_owned() + "***@" + domain
}
}
_ => edge_mask(normalized, 1, 1, false)
}
}
///|
pub fn probable_chinese_name(value : String) -> Bool {
let text = trim_ascii_space(value)
let chars = text.to_array()
chars.length() >= 2 && chars.length() <= 6 && chars.all(is_cjk_char)
}
///|
pub fn probable_english_name(value : String) -> Bool {
let words = trim_ascii_space(value).split(" ").to_array()
words.length() >= 1 &&
words.length() <= 4 &&
words.all(fn(word) {
let item = word.to_owned()
!item.is_empty() &&
item
.to_array()
.all(fn(c) { c.is_ascii_alphabetic() || c == '-' || c == '\'' })
})
}
///|
pub fn name_initials(value : String) -> String {
let words = trim_ascii_space(value).split(" ").to_array()
let builder = StringBuilder()
for word in words {
match word.to_owned().to_array().get(0) {
Some(c) => builder.write_string(c.to_string().to_upper())
None => ()
}
}
builder.to_string()
}
///|
pub fn normalize_address(value : String) -> String {
collapse_spaces(normalize_punctuation(value)).to_lower()
}
///|
pub fn address_components(value : String) -> Array[String] {
let normalized = normalize_address(value)
normalized.split(",").map(fn(item) { item.to_owned() }).to_array()
}
///|
pub fn probable_address(value : String) -> Bool {
let text = normalize_address(value)
text.length() >= 6 &&
(
text.contains("路") ||
text.contains("街") ||
text.contains("号") ||
text.contains("street") ||
text.contains("road") ||
text.contains("avenue")
)
}
///|
pub fn date_digits(value : String) -> String {
keep_digits(value)
}
///|
pub fn normalize_date(value : String) -> String? {
let digits = date_digits(value)
if digits.length() == 8 {
match parse_compact_date(digits) {
Some((year, month, day)) =>
Some(
pad_left(year.to_string(), 4, '0') +
"-" +
pad_left(month.to_string(), 2, '0') +
"-" +
pad_left(day.to_string(), 2, '0'),
)
None => None
}
} else {
None
}
}
///|
pub fn date_is_future(value : String, current_year : Int) -> Bool {
match normalize_date(value) {
Some(normalized) => decimal_value(normalized[0:4].to_owned()) > current_year
None => false
}
}
///|
pub fn identifier_class(value : String) -> String {
if valid_chinese_id(value) {
"chinese-id"
} else if valid_bank_card(value) {
"bank-card"
} else if valid_email_address(value) {
"email"
} else if valid_phone_number(value) {
"phone"
} else if valid_uuid(value) {
"uuid"
} else if valid_ipv4(value) {
"ipv4"
} else {
"unknown"
}
}
///|
pub fn identifier_quality(value : String) -> Int {
let normalized = normalized_identifier(value)
let mut score = 0
if normalized.length() >= 4 {
score += 20
}
if normalized.length() >= 8 {
score += 20
}
if valid_chinese_id(value) {
score += 40
}
if valid_bank_card(value) {
score += 30
}
if valid_email_address(value) {
score += 25
}
if valid_phone_number(value) {
score += 25
}
if score > 100 {
100
} else {
score
}
}
///|
pub fn pseudonym_for(
value : String,
kind : IdentifierKind,
salt : String,
) -> String {
let digest = stable_hash(salt + ":" + normalize_identifier(value, kind))
match kind {
PhoneIdentifier => "PHONE-" + digest[:6].to_owned()
EmailIdentifier => "user-" + digest[:8].to_owned() + "@example.invalid"
GovernmentIdentifier => "ID-" + digest
AccountIdentifier => "ACCOUNT-" + digest[:10].to_owned()
DeviceIdentifier => "DEVICE-" + digest[:10].to_owned()
LocationIdentifier => "LOCATION-" + digest[:8].to_owned()
UnknownIdentifier => "TOKEN-" + digest[:8].to_owned()
}
}
///|
pub fn pseudonym_map(
values : Array[String],
kind : IdentifierKind,
salt : String,
) -> Map[String, String] {
let result : Map[String, String] = Map([])
for value in values {
result[value] = pseudonym_for(value, kind, salt)
}
result
}