///|
pub(all) enum DatePrecision {
Year
Month
Day
DateTime
Unknown
} derive(Debug, Eq, ToJson)
///|
fn date_digit(value : String, index : Int) -> Int? {
match value.get(index) {
None => None
Some(ch) =>
if ch >= ('0' : UInt16) && ch <= ('9' : UInt16) {
Some(ch.to_int() - ('0' : UInt16).to_int())
} else {
None
}
}
}
///|
fn date_number(value : String, start : Int, end : Int) -> Int? {
if start >= end || start < 0 || end > value.length() {
None
} else {
for total = 0, index = start; index < end; {
match date_digit(value, index) {
None => continue -1, end
Some(digit) => continue total * 10 + digit, index + 1
}
} nobreak {
if total < 0 {
None
} else {
Some(total)
}
}
}
}
///|
fn date_leap_year(year : Int) -> Bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
///|
fn date_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 date_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
pub fn is_valid_fhir_date(value : String) -> Bool {
if value.length() == 4 {
date_number(value, 0, 4) is Some(_)
} else if value.length() == 7 {
value[4] == '-' &&
date_number(value, 0, 4) is Some(_) &&
date_number(value, 5, 7) is Some(month) &&
month >= 1 &&
month <= 12
} else if value.length() == 10 {
value[4] == '-' &&
value[7] == '-' &&
date_number(value, 0, 4) is Some(year) &&
date_number(value, 5, 7) is Some(month) &&
date_number(value, 8, 10) is Some(day) &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= date_days_in_month(year, month)
} else {
false
}
}
///|
fn is_valid_timezone(value : String) -> Bool {
if value == "Z" {
true
} else if value.length() == 6 &&
(value[0] == '+' || value[0] == '-') &&
value[3] == ':' {
date_number(value, 1, 3) is Some(hour) &&
date_number(value, 4, 6) is Some(minute) &&
hour <= 23 &&
minute <= 59
} else {
false
}
}
///|
fn is_valid_time(value : String) -> Bool {
if value.length() < 8 {
false
} else {
let basic = match date_number(value, 0, 2) {
None => false
Some(hour) =>
match date_number(value, 3, 5) {
None => false
Some(minute) =>
match date_number(value, 6, 8) {
None => false
Some(second) =>
value[2] == ':' &&
value[5] == ':' &&
hour <= 23 &&
minute <= 59 &&
second <= 59
}
}
}
let suffix = if value.length() > 8 { value[8:].to_owned() } else { "" }
basic &&
(if suffix == "" || suffix == "Z" {
true
} else if suffix.length() == 6 && (suffix[0] == '+' || suffix[0] == '-') {
is_valid_timezone(suffix)
} else if suffix[0] == '.' {
match suffix.find("Z") {
Some(index) => index > 1 && is_valid_timezone(suffix[index:].to_owned())
None =>
match suffix.find("+") {
Some(index) =>
index > 1 && is_valid_timezone(suffix[index:].to_owned())
None =>
match suffix.find("-") {
Some(index) =>
index > 1 && is_valid_timezone(suffix[index:].to_owned())
None => false
}
}
}
} else {
false
})
}
}
///|
pub fn is_valid_fhir_datetime(value : String) -> Bool {
if is_valid_fhir_date(value) {
true
} else {
match value.find("T") {
None => false
Some(index) =>
is_valid_fhir_date(value[0:index].to_owned()) &&
is_valid_time(value[index + 1:].to_owned())
}
}
}
///|
pub fn date_precision(value : String) -> DatePrecision {
if value.contains("T") {
DateTime
} else {
match value.length() {
4 => Year
7 => Month
10 => Day
_ => Unknown
}
}
}
///|
pub fn date_precision_name(value : String) -> String {
match date_precision(value) {
Year => "year"
Month => "month"
Day => "day"
DateTime => "datetime"
Unknown => "unknown"
}
}
///|
fn date_order_key(value : String) -> String {
match date_precision(value) {
Year => value + "-01-01"
Month => value + "-01"
Day | DateTime => value
Unknown => ""
}
}
///|
pub fn compare_fhir_dates(left : String, right : String) -> Int? {
if !is_valid_fhir_datetime(left) || !is_valid_fhir_datetime(right) {
None
} else {
Some(String::compare(date_order_key(left), date_order_key(right)))
}
}
///|
pub fn period_contains(start : String, end : String, point : String) -> Bool {
let lower = start == "" || compare_fhir_dates(start, point).unwrap_or(1) <= 0
let upper = end == "" || compare_fhir_dates(point, end).unwrap_or(1) <= 0
lower && upper
}
///|
pub fn period_overlap(
left_start : String,
left_end : String,
right_start : String,
right_end : String,
) -> Bool {
let left_before_right = left_end == "" ||
right_start == "" ||
period_contains(left_start, left_end, right_start)
let right_before_left = right_end == "" ||
left_start == "" ||
period_contains(right_start, right_end, left_start)
left_before_right || right_before_left
}
///|
pub fn period_is_valid(start : String, end : String) -> Bool {
(start == "" || is_valid_fhir_datetime(start)) &&
(end == "" || is_valid_fhir_datetime(end)) &&
(start == "" || end == "" || compare_fhir_dates(start, end).unwrap_or(1) <= 0)
}
///|
pub fn date_year(value : String) -> Int? {
if is_valid_fhir_datetime(value) {
date_number(value, 0, 4)
} else {
None
}
}
///|
pub fn date_month(value : String) -> Int? {
if is_valid_fhir_datetime(value) && value.length() >= 7 {
date_number(value, 5, 7)
} else {
None
}
}
///|
pub fn date_day(value : String) -> Int? {
if is_valid_fhir_datetime(value) && value.length() >= 10 {
date_number(value, 8, 10)
} else {
None
}
}
///|
pub fn date_is_before(left : String, right : String) -> Bool {
compare_fhir_dates(left, right).unwrap_or(1) < 0
}
///|
pub fn date_is_after(left : String, right : String) -> Bool {
compare_fhir_dates(left, right).unwrap_or(-1) > 0
}
///|
pub fn date_is_equal(left : String, right : String) -> Bool {
compare_fhir_dates(left, right).unwrap_or(1) == 0
}
///|
pub fn date_range_label(start : String, end : String) -> String {
if start == "" && end == "" {
"unspecified"
} else if start == "" {
"until \{end}"
} else if end == "" {
"from \{start}"
} else if start == end {
start
} else {
"\{start} to \{end}"
}
}
///|
pub fn date_inputs_are_valid(values : Array[String]) -> Bool {
values.all(is_valid_fhir_datetime)
}
///|
pub fn invalid_date_inputs(values : Array[String]) -> Array[String] {
values.filter(fn(value) { !is_valid_fhir_datetime(value) })
}