///|
pub(all) enum TimestampPrecision {
Year
Month
Day
Minute
Second
Fractional
} derive(Eq, Debug)
///|
pub(all) struct IonTimestamp {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
fraction : IonDecimal
offset_minutes : Int
offset_unknown : Bool
precision : TimestampPrecision
} derive(Eq, Debug)
///|
pub fn timestamp_year(year : Int) -> Result[IonTimestamp, IonError] {
if year < 1 || year > 9999 {
Err(InvalidTimestamp(year.to_string()))
} else {
Ok({
year,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
fraction: decimal_from_int(0),
offset_minutes: 0,
offset_unknown: false,
precision: Year,
})
}
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
if month == 1 ||
month == 3 ||
month == 5 ||
month == 7 ||
month == 8 ||
month == 10 ||
month == 12 {
31
} else if month == 4 || month == 6 || month == 9 || month == 11 {
30
} else if month == 2 {
if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
29
} else {
28
}
} else {
0
}
}
///|
pub fn timestamp_of(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
second : Int,
fraction : IonDecimal,
offset_minutes : Int,
offset_unknown : Bool,
precision : TimestampPrecision,
) -> Result[IonTimestamp, IonError] {
if year < 1 || year > 9999 {
return Err(InvalidTimestamp("year"))
}
match precision {
Year => ()
Month =>
if month < 1 || month > 12 {
return Err(InvalidTimestamp("month"))
}
Day => {
if month < 1 || month > 12 {
return Err(InvalidTimestamp("month"))
}
if day < 1 || day > days_in_month(year, month) {
return Err(InvalidTimestamp("day"))
}
}
Minute => {
if month < 1 || month > 12 {
return Err(InvalidTimestamp("month"))
}
if day < 1 || day > days_in_month(year, month) {
return Err(InvalidTimestamp("day"))
}
if hour < 0 || hour > 23 {
return Err(InvalidTimestamp("hour"))
}
if minute < 0 || minute > 59 {
return Err(InvalidTimestamp("minute"))
}
}
Second => {
if month < 1 || month > 12 {
return Err(InvalidTimestamp("month"))
}
if day < 1 || day > days_in_month(year, month) {
return Err(InvalidTimestamp("day"))
}
if hour < 0 || hour > 23 {
return Err(InvalidTimestamp("hour"))
}
if minute < 0 || minute > 59 {
return Err(InvalidTimestamp("minute"))
}
if second < 0 || second > 59 {
return Err(InvalidTimestamp("second"))
}
}
Fractional => {
if month < 1 || month > 12 {
return Err(InvalidTimestamp("month"))
}
if day < 1 || day > days_in_month(year, month) {
return Err(InvalidTimestamp("day"))
}
if hour < 0 || hour > 23 {
return Err(InvalidTimestamp("hour"))
}
if minute < 0 || minute > 59 {
return Err(InvalidTimestamp("minute"))
}
if (match precision {
Second => true
Fractional => true
_ => false
}) {
if second < 0 || second > 59 {
return Err(InvalidTimestamp("second"))
}
}
}
}
if !offset_unknown && (offset_minutes < -24 * 60 || offset_minutes > 24 * 60) {
return Err(InvalidTimestamp("offset"))
}
Ok({
year,
month,
day,
hour,
minute,
second,
fraction,
offset_minutes,
offset_unknown,
precision,
})
}
///|
fn pad2(n : Int) -> String {
if n < 10 {
"0" + n.to_string()
} else {
n.to_string()
}
}
///|
fn pad4(n : Int) -> String {
if n < 10 {
"000" + n.to_string()
} else if n < 100 {
"00" + n.to_string()
} else if n < 1000 {
"0" + n.to_string()
} else {
n.to_string()
}
}
///|
fn offset_text(ts : IonTimestamp) -> String {
if ts.offset_unknown {
"-00:00"
} else if ts.offset_minutes == 0 {
"Z"
} else {
let mut m = ts.offset_minutes
let mut sign = "+"
if m < 0 {
sign = "-"
m = 0 - m
}
sign + pad2(m / 60) + ":" + pad2(m % 60)
}
}
///|
pub fn IonTimestamp::to_ion_text(self : IonTimestamp) -> String {
match self.precision {
Year => pad4(self.year) + "T"
Month => pad4(self.year) + "-" + pad2(self.month) + "T"
Day => pad4(self.year) + "-" + pad2(self.month) + "-" + pad2(self.day) + "T"
Minute =>
pad4(self.year) +
"-" +
pad2(self.month) +
"-" +
pad2(self.day) +
"T" +
pad2(self.hour) +
":" +
pad2(self.minute) +
offset_text(self)
Second =>
pad4(self.year) +
"-" +
pad2(self.month) +
"-" +
pad2(self.day) +
"T" +
pad2(self.hour) +
":" +
pad2(self.minute) +
":" +
pad2(self.second) +
offset_text(self)
Fractional =>
pad4(self.year) +
"-" +
pad2(self.month) +
"-" +
pad2(self.day) +
"T" +
pad2(self.hour) +
":" +
pad2(self.minute) +
":" +
pad2(self.second) +
"." +
fraction_digits(self.fraction) +
offset_text(self)
}
}
///|
fn fraction_digits(frac : IonDecimal) -> String {
let s = frac.coefficient.to_decimal_string()
if s == "0" {
"0"
} else if s[0].to_int() == 45 {
slice_text(s, 1, s.length())
} else {
s
}
}
///|
pub fn parse_timestamp_text(text : String) -> Result[IonTimestamp, IonError] {
if text.length() < 5 {
()
}
if text.length() < 4 {
return Err(InvalidTimestamp(text))
}
let year = match parse_fixed_digits(text, 0, 4) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
if text.length() == 5 && text[4].to_int() == 84 {
return timestamp_year(year)
}
if text.length() == 4 {
return timestamp_year(year)
}
if text.length() < 7 || text[4].to_int() != 45 {
return Err(InvalidTimestamp(text))
}
let month = match parse_fixed_digits(text, 5, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
if text.length() == 8 && text[7].to_int() == 84 {
return timestamp_of(
year,
month,
1,
0,
0,
0,
decimal_from_int(0),
0,
false,
Month,
)
}
if text.length() < 10 || text[7].to_int() != 45 {
return Err(InvalidTimestamp(text))
}
let day = match parse_fixed_digits(text, 8, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
if text.length() == 11 && text[10].to_int() == 84 {
return timestamp_of(
year,
month,
day,
0,
0,
0,
decimal_from_int(0),
0,
false,
Day,
)
}
if text.length() < 16 || text[10].to_int() != 84 {
return Err(InvalidTimestamp(text))
}
let hour = match parse_fixed_digits(text, 11, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
if text[13].to_int() != 58 {
return Err(InvalidTimestamp(text))
}
let minute = match parse_fixed_digits(text, 14, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
let mut idx = 16
let mut second = 0
let mut frac = decimal_from_int(0)
let mut precision = Minute
if idx < text.length() && text[idx].to_int() == 58 {
second = match parse_fixed_digits(text, 17, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
idx = 19
precision = Second
if idx < text.length() && text[idx].to_int() == 46 {
idx += 1
let start = idx
while idx < text.length() && is_digit(text[idx].to_int()) {
idx += 1
}
if idx == start {
return Err(InvalidTimestamp(text))
}
let digits = slice_text(text, start, idx)
match parse_decimal_text("0." + digits + "d0") {
Ok(v) => frac = v
Err(_) => return Err(InvalidTimestamp(text))
}
precision = Fractional
}
}
if idx >= text.length() {
return Err(InvalidTimestamp(text))
}
let rest = slice_text(text, idx, text.length())
let (offset_minutes, offset_unknown) = match parse_offset(rest) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
timestamp_of(
year, month, day, hour, minute, second, frac, offset_minutes, offset_unknown,
precision,
)
}
///|
fn parse_fixed_digits(
text : String,
start : Int,
len : Int,
) -> Result[Int, IonError] {
if start + len > text.length() {
return Err(InvalidNumber(text))
}
let mut n = 0
let mut i = 0
while i < len {
let c = text[start + i].to_int()
if !is_digit(c) {
return Err(InvalidNumber(text))
}
n = n * 10 + (c - 48)
i += 1
}
Ok(n)
}
///|
fn parse_offset(text : String) -> Result[(Int, Bool), IonError] {
if text == "Z" || text == "z" {
Ok((0, false))
} else if text == "-00:00" {
Ok((0, true))
} else if text.length() == 6 &&
(text[0].to_int() == 43 || text[0].to_int() == 45) &&
text[3].to_int() == 58 {
let hh = match parse_fixed_digits(text, 1, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
let mm = match parse_fixed_digits(text, 4, 2) {
Ok(v) => v
Err(_) => return Err(InvalidTimestamp(text))
}
let mut total = hh * 60 + mm
if text[0].to_int() == 45 {
total = 0 - total
}
Ok((total, false))
} else {
Err(InvalidTimestamp(text))
}
}