///|
fn decimal_digit(char : Char) -> Int? {
if char.is_ascii_digit() {
Some(char.to_int() - '0'.to_int())
} else {
None
}
}
///|
fn parse_fixed_decimal(chars : Array[Char], start : Int, length : Int) -> Int? {
if start < 0 || length <= 0 || start + length > chars.length() {
return None
}
let mut value = 0
for index = start; index < start + length; index = index + 1 {
guard decimal_digit(chars[index]) is Some(digit) else { return None }
value = value * 10 + digit
}
Some(value)
}
///|
fn is_leap_year(year : Int) -> Bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
fn is_iso_date_chars(chars : Array[Char], start : Int) -> Bool {
if start < 0 || start + 10 > chars.length() {
return false
}
if chars[start + 4] != '-' || chars[start + 7] != '-' {
return false
}
guard parse_fixed_decimal(chars, start, 4) is Some(year) else { return false }
guard parse_fixed_decimal(chars, start + 5, 2) is Some(month) else {
return false
}
guard parse_fixed_decimal(chars, start + 8, 2) is Some(day) else {
return false
}
year >= 1 &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= days_in_month(year, month)
}
///|
/// Validate an ISO 8601 calendar date in `YYYY-MM-DD` form.
fn is_iso_date_string(text : String) -> Bool {
let chars = text.to_array()
chars.length() == 10 && is_iso_date_chars(chars, 0)
}
///|
fn parse_timezone(chars : Array[Char], start : Int) -> Bool {
if start >= chars.length() {
return false
}
if chars[start] == 'Z' || chars[start] == 'z' {
return start + 1 == chars.length()
}
if chars[start] != '+' && chars[start] != '-' {
return false
}
if start + 6 != chars.length() || chars[start + 3] != ':' {
return false
}
guard parse_fixed_decimal(chars, start + 1, 2) is Some(hours) else {
return false
}
guard parse_fixed_decimal(chars, start + 4, 2) is Some(minutes) else {
return false
}
hours <= 23 && minutes <= 59
}
///|
/// Validate a practical RFC 3339 timestamp with seconds and a timezone.
fn is_rfc3339_datetime_string(text : String) -> Bool {
let chars = text.to_array()
if chars.length() < 20 || !is_iso_date_chars(chars, 0) {
return false
}
if (chars[10] != 'T' && chars[10] != 't') ||
chars[13] != ':' ||
chars[16] != ':' {
return false
}
guard parse_fixed_decimal(chars, 11, 2) is Some(hours) else { return false }
guard parse_fixed_decimal(chars, 14, 2) is Some(minutes) else { return false }
guard parse_fixed_decimal(chars, 17, 2) is Some(seconds) else { return false }
if hours > 23 || minutes > 59 || seconds > 59 {
return false
}
let mut timezone_start = 19
if chars.get(timezone_start) == Some('.') {
timezone_start = timezone_start + 1
let fraction_start = timezone_start
while chars
.get(timezone_start)
.map(fn(ch) { ch.is_ascii_digit() })
.unwrap_or(false) {
timezone_start = timezone_start + 1
}
if timezone_start == fraction_start {
return false
}
}
parse_timezone(chars, timezone_start)
}
///|
fn is_ipv4_string(text : String) -> Bool {
let chars = text.to_array()
if chars.is_empty() {
return false
}
let mut cursor = 0
let mut segments = 0
while cursor < chars.length() {
if segments == 4 {
return false
}
let start = cursor
let mut value = 0
while cursor < chars.length() && chars[cursor].is_ascii_digit() {
value = value * 10 + chars[cursor].to_int() - '0'.to_int()
cursor = cursor + 1
}
let digits = cursor - start
if digits == 0 || digits > 3 || value > 255 {
return false
}
if digits > 1 && chars[start] == '0' {
return false
}
segments = segments + 1
if cursor == chars.length() {
break
}
if chars[cursor] != '.' {
return false
}
cursor = cursor + 1
if cursor == chars.length() {
return false
}
}
segments == 4
}
///|
fn is_email_local_char(char : Char) -> Bool {
char.is_ascii_alphabetic() ||
char.is_ascii_digit() ||
char == '.' ||
char == '!' ||
char == '#' ||
char == '$' ||
char == '%' ||
char == '&' ||
char == '\'' ||
char == '*' ||
char == '+' ||
char == '-' ||
char == '/' ||
char == '=' ||
char == '?' ||
char == '^' ||
char == '_' ||
char == '`' ||
char == '{' ||
char == '|' ||
char == '}' ||
char == '~'
}
///|
fn is_email_domain(chars : Array[Char], start : Int) -> Bool {
if start >= chars.length() || chars.length() - start > 253 {
return false
}
let mut cursor = start
let mut labels = 0
while cursor < chars.length() {
let label_start = cursor
while cursor < chars.length() && chars[cursor] != '.' {
let char = chars[cursor]
if !char.is_ascii_alphabetic() && !char.is_ascii_digit() && char != '-' {
return false
}
cursor = cursor + 1
}
let label_length = cursor - label_start
if label_length == 0 ||
label_length > 63 ||
chars[label_start] == '-' ||
chars[cursor - 1] == '-' {
return false
}
labels = labels + 1
if cursor < chars.length() {
cursor = cursor + 1
if cursor == chars.length() {
return false
}
}
}
labels >= 2
}
///|
/// Validate a conservative ASCII email-address syntax.
fn is_email_string(text : String) -> Bool {
let chars = text.to_array()
if chars.length() < 3 || chars.length() > 254 {
return false
}
let mut at = -1
for index, char in chars {
if char == '@' {
if at >= 0 {
return false
}
at = index
}
}
if at <= 0 || at > 64 || at + 1 >= chars.length() {
return false
}
if chars[0] == '.' || chars[at - 1] == '.' {
return false
}
for index = 0; index < at; index = index + 1 {
if !is_email_local_char(chars[index]) ||
(chars[index] == '.' && index + 1 < at && chars[index + 1] == '.') {
return false
}
}
is_email_domain(chars, at + 1)
}
///|
fn is_uuid_string(text : String) -> Bool {
let chars = text.to_array()
if chars.length() != 36 {
return false
}
for index, char in chars {
if index == 8 || index == 13 || index == 18 || index == 23 {
if char != '-' {
return false
}
} else if !char.is_ascii_hexdigit() {
return false
}
}
true
}
///|
fn is_semver_identifier_char(char : Char) -> Bool {
char.is_ascii_alphabetic() || char.is_ascii_digit() || char == '-'
}
///|
fn scan_semver_identifiers(
chars : Array[Char],
start : Int,
end : Int,
reject_numeric_leading_zero : Bool,
) -> Bool {
if start >= end {
return false
}
let mut cursor = start
while cursor < end {
let identifier_start = cursor
let mut numeric = true
while cursor < end && chars[cursor] != '.' {
if !is_semver_identifier_char(chars[cursor]) {
return false
}
if !chars[cursor].is_ascii_digit() {
numeric = false
}
cursor = cursor + 1
}
if cursor == identifier_start ||
(
reject_numeric_leading_zero &&
numeric &&
cursor - identifier_start > 1 &&
chars[identifier_start] == '0'
) {
return false
}
if cursor < end {
cursor = cursor + 1
if cursor == end {
return false
}
}
}
true
}
///|
fn scan_semver_core(chars : Array[Char], end : Int) -> Bool {
let mut cursor = 0
let mut components = 0
while cursor < end {
let start = cursor
while cursor < end && chars[cursor].is_ascii_digit() {
cursor = cursor + 1
}
if cursor == start || (cursor - start > 1 && chars[start] == '0') {
return false
}
components = components + 1
if cursor == end {
break
}
if chars[cursor] != '.' || components >= 3 {
return false
}
cursor = cursor + 1
}
components == 3
}
///|
fn is_semver_string(text : String) -> Bool {
let chars = text.to_array()
if chars.is_empty() {
return false
}
let mut core_end = chars.length()
let mut prerelease_start = -1
let mut build_start = -1
for index, char in chars {
if char == '-' && prerelease_start < 0 && build_start < 0 {
prerelease_start = index + 1
core_end = index
} else if char == '+' {
if build_start >= 0 {
return false
}
build_start = index + 1
if prerelease_start < 0 {
core_end = index
}
}
}
if !scan_semver_core(chars, core_end) {
return false
}
if prerelease_start >= 0 {
let prerelease_end = if build_start >= 0 {
build_start - 1
} else {
chars.length()
}
if !scan_semver_identifiers(chars, prerelease_start, prerelease_end, true) {
return false
}
}
if build_start >= 0 &&
!scan_semver_identifiers(chars, build_start, chars.length(), false) {
return false
}
true
}
///|
fn validate_literal_for_function(name : String, text : String) -> Bool? {
match name {
"is_date" => Some(is_iso_date_string(text))
"is_datetime" => Some(is_rfc3339_datetime_string(text))
"is_ipv4" => Some(is_ipv4_string(text))
"is_email" => Some(is_email_string(text))
"is_uuid" => Some(is_uuid_string(text))
"is_semver" => Some(is_semver_string(text))
_ => None
}
}