///|
/// Convert an ASCII decimal character to a digit. Callers validate the
/// input first; the fallback keeps validation functions total.
pub fn ascii_digit_value(c : Char) -> Int {
if c >= '0' && c <= '9' {
c.to_int() - '0'.to_int()
} else {
-1
}
}
///|
pub fn valid_month(month : Int) -> Bool {
month >= 1 && month <= 12
}
///|
pub fn days_in_month(year : Int, month : Int) -> Int {
match month {
2 => if is_leap_year(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
_ => 0
}
}
///|
pub fn is_leap_year(year : Int) -> Bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
///|
pub fn decimal_value(value : String) -> Int {
let mut result = 0
for c in value {
let digit = ascii_digit_value(c)
if digit >= 0 {
result = result * 10 + digit
}
}
result
}
///|
pub fn valid_calendar_date(year : Int, month : Int, day : Int) -> Bool {
year >= 1 &&
valid_month(month) &&
day >= 1 &&
day <= days_in_month(year, month)
}
///|
pub fn parse_compact_date(value : String) -> (Int, Int, Int)? {
let digits = keep_digits(value)
if digits.length() != 8 {
None
} else {
let year = decimal_value(digits[0:4].to_owned())
let month = decimal_value(digits[4:6].to_owned())
let day = decimal_value(digits[6:8].to_owned())
if valid_calendar_date(year, month, day) {
Some((year, month, day))
} else {
None
}
}
}
///|
pub fn valid_phone_number(value : String) -> Bool {
let digits = keep_digits(value)
let count = digits.length()
if count == 11 {
digits.has_prefix("1") &&
ascii_digit_value(char_at_or(digits, 1, '0')) >= 3 &&
ascii_digit_value(char_at_or(digits, 1, '0')) <= 9
} else if count == 10 {
value.to_array().any(fn(c) { c == '-' || c == ' ' || c == '.' || c == '(' })
} else {
count >= 7 && count <= 15
}
}
///|
pub fn valid_email_address(value : String) -> Bool {
let trimmed = trim_ascii_space(value)
let mut at = -1
for i in 0..= 0 {
let position = at
let local_part = trimmed[:position]
let domain = trimmed[position + 1:]
!local_part.is_empty() &&
!domain.is_empty() &&
domain.contains(".") &&
!domain.contains("@") &&
!domain.has_prefix(".") &&
!domain.has_suffix(".") &&
!local_part.has_prefix(".") &&
!local_part.has_suffix(".")
} else {
false
}
}
///|
pub fn valid_luhn(value : String) -> Bool {
let digits = keep_digits(value)
let allowed = value
.to_array()
.all(fn(c) { c.is_ascii_digit() || c == ' ' || c == '-' })
if digits.is_empty() || !allowed {
false
} else {
let mut sum = 0
let mut double = false
let chars = digits.to_array()
let mut i = chars.length()
while i > 0 {
i -= 1
let digit = ascii_digit_value(chars[i])
let mut add = digit
if double {
add = digit * 2
if add > 9 {
add -= 9
}
}
sum += add
double = !double
}
sum % 10 == 0
}
}
///|
pub fn valid_bank_card(value : String) -> Bool {
let digits = keep_digits(value)
digits.length() >= 13 && digits.length() <= 19 && valid_luhn(value)
}
///|
pub fn valid_ipv4(value : String) -> Bool {
let parts = value.split(".").to_array()
if parts.length() != 4 {
false
} else {
parts.all(fn(part) {
let item = part.to_owned()
if item.length() >= 1 && item.length() <= 3 && all_ascii_digits(item) {
let parsed = decimal_value(item)
parsed >= 0 && parsed <= 255
} else {
false
}
})
}
}
///|
pub fn valid_hex_color(value : String) -> Bool {
let text = if value.has_prefix("#") { value[1:] } else { value }
(text.length() == 3 || text.length() == 6 || text.length() == 8) &&
all_ascii_hex(text.to_owned())
}
///|
pub fn valid_chinese_id(value : String) -> Bool {
let id = trim_ascii_space(value)
if id.length() != 18 {
false
} else {
let chars = id.to_array()
let first_seventeen = id[:17]
if !all_ascii_digits(first_seventeen.to_owned()) {
false
} else {
let year = decimal_value(id[6:10].to_owned())
let month = decimal_value(id[10:12].to_owned())
let day = decimal_value(id[12:14].to_owned())
let final_char = chars[17]
if !valid_calendar_date(year, month, day) ||
!(final_char.is_ascii_digit() || final_char == 'x' || final_char == 'X') {
false
} else {
let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
let checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
let mut total = 0
for i in 0..<17 {
total += ascii_digit_value(chars[i]) * weights[i]
}
let expected = checks[total % 11]
final_char.to_string().to_upper() == expected.to_string()
}
}
}
}
///|
pub fn valid_passport_number(value : String) -> Bool {
let text = trim_ascii_space(value).to_upper()
let chars = text.to_array()
chars.length() >= 6 &&
chars.length() <= 12 &&
chars[0].is_ascii_alphabetic() &&
chars[1:].all(is_ascii_alphanumeric_char)
}
///|
pub fn valid_postal_code(value : String, locale : LocaleHint) -> Bool {
let text = trim_ascii_space(value)
match locale {
Chinese => text.length() == 6 && all_ascii_digits(text)
English =>
text.length() >= 5 &&
text.length() <= 10 &&
text
.to_array()
.all(fn(c) { is_ascii_alphanumeric_char(c) || c == ' ' || c == '-' })
Mixed | Neutral =>
text.length() >= 4 &&
text.length() <= 10 &&
text
.to_array()
.all(fn(c) { is_ascii_alphanumeric_char(c) || c == ' ' || c == '-' })
}
}
///|
pub fn valid_imei(value : String) -> Bool {
let digits = keep_digits(value)
digits.length() == 15 && valid_luhn(digits)
}
///|
pub fn valid_mac_address(value : String) -> Bool {
let normalized = value
.replace_all(old=":", new="")
.replace_all(old="-", new="")
normalized.length() == 12 && all_ascii_hex(normalized)
}
///|
pub fn valid_uuid(value : String) -> Bool {
let parts = value.split("-").to_array()
parts.length() == 5 &&
parts[0].length() == 8 &&
parts[1].length() == 4 &&
parts[2].length() == 4 &&
parts[3].length() == 4 &&
parts[4].length() == 12 &&
parts.all(fn(part) { all_ascii_hex(part.to_owned()) })
}
///|
pub fn valid_accession(value : String) -> Bool {
let text = trim_ascii_space(value).to_upper()
let chars = text.to_array()
chars.length() >= 5 &&
chars.length() <= 20 &&
chars[0].is_ascii_alphabetic() &&
chars.any(fn(c) { c.is_ascii_digit() }) &&
chars.all(fn(c) { is_ascii_alphanumeric_char(c) || c == '_' || c == '.' })
}
///|
pub fn valid_lab_code(value : String) -> Bool {
let text = trim_ascii_space(value).to_upper()
let chars = text.to_array()
chars.length() >= 4 &&
chars.length() <= 24 &&
chars[0].is_ascii_alphabetic() &&
chars.any(fn(c) { c.is_ascii_digit() }) &&
chars.all(fn(c) { is_ascii_alphanumeric_char(c) || c == '-' || c == '_' })
}
///|
pub fn valid_iban_prefix(value : String) -> Bool {
let text = trim_ascii_space(value).replace_all(old=" ", new="").to_upper()
text.length() >= 15 &&
text.length() <= 34 &&
char_at_or(text, 0, ' ').is_ascii_alphabetic() &&
char_at_or(text, 1, ' ').is_ascii_alphabetic() &&
char_at_or(text, 2, ' ').is_ascii_digit() &&
char_at_or(text, 3, ' ').is_ascii_digit() &&
text[4:].to_owned().to_array().all(is_ascii_alphanumeric_char)
}
///|
pub fn checksum_mod11(values : Array[Int], weights : Array[Int]) -> Int? {
if values.length() != weights.length() || values.is_empty() {
None
} else {
let mut sum = 0
for i in 0.. String {
keep_ascii_letters_and_digits(value).to_upper()
}
///|
pub fn identifier_entropy_hint(value : String) -> Int {
let classes = [
value.to_array().any(fn(c) { c.is_ascii_digit() }),
value.to_array().any(fn(c) { c.is_ascii_lowercase() }),
value.to_array().any(fn(c) { c.is_ascii_uppercase() }),
value.to_array().any(is_cjk_char),
value.to_array().any(is_ascii_punctuation_char),
]
classes.filter(fn(item) { item }).length()
}