219 lines
6.5 KiB
Gleam
219 lines
6.5 KiB
Gleam
import gleam/bit_array
|
|
import gleam/dynamic/decode.{type Decoder}
|
|
import gleam/pair
|
|
import gleam/result
|
|
import gleam/time/calendar.{
|
|
type Date, type Month, type TimeOfDay, April, August, Date, December, February,
|
|
January, July, June, March, May, November, October, September, TimeOfDay,
|
|
}
|
|
import gleam/time/timestamp.{type Timestamp}
|
|
|
|
pub fn rfc3339_timestamp_decoder() -> Decoder(Timestamp) {
|
|
use value <- decode.then(decode.string)
|
|
case timestamp.parse_rfc3339(value) {
|
|
Ok(timestamp) -> decode.success(timestamp)
|
|
Error(Nil) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp")
|
|
}
|
|
}
|
|
|
|
pub fn parse_iso8601_date(input: String) -> Result(Date, Nil) {
|
|
let bytes = bit_array.from_string(input)
|
|
use #(year, bytes) <- result.try(parse_year(from: bytes))
|
|
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
|
|
use #(month, bytes) <- result.try(parse_month_calendar(from: bytes))
|
|
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
|
|
use #(day, bytes) <- result.try(parse_day_calendar(from: bytes, year:, month:))
|
|
use Nil <- result.try(accept_empty(bytes))
|
|
Ok(Date(year:, month:, day:))
|
|
}
|
|
|
|
pub fn iso8601_date_decoder() -> Decoder(Date) {
|
|
use value <- decode.then(decode.string)
|
|
case parse_iso8601_date(value) {
|
|
Ok(date) -> decode.success(date)
|
|
Error(Nil) -> decode.failure(Date(1970, January, 1), "Date")
|
|
}
|
|
}
|
|
|
|
pub fn parse_iso8601_time_of_day(input: String) -> Result(TimeOfDay, Nil) {
|
|
let bytes = bit_array.from_string(input)
|
|
use #(hours, bytes) <- result.try(parse_hours(from: bytes))
|
|
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
|
|
use #(minutes, bytes) <- result.try(parse_minutes(from: bytes))
|
|
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
|
|
use #(seconds, bytes) <- result.try(parse_seconds(from: bytes))
|
|
use #(nanoseconds, bytes) <- result.try(parse_second_fraction_as_nanoseconds(
|
|
from: bytes,
|
|
))
|
|
use Nil <- result.try(accept_empty(bytes))
|
|
Ok(TimeOfDay(hours:, minutes:, seconds:, nanoseconds:))
|
|
}
|
|
|
|
pub fn iso8601_time_of_day_decoder() -> Decoder(TimeOfDay) {
|
|
use value <- decode.then(decode.string)
|
|
case parse_iso8601_time_of_day(value) {
|
|
Ok(date) -> decode.success(date)
|
|
Error(Nil) -> decode.failure(TimeOfDay(0, 0, 0, 0), "TimeOfDay")
|
|
}
|
|
}
|
|
|
|
const byte_zero: Int = 0x30
|
|
|
|
const byte_nine: Int = 0x39
|
|
|
|
const byte_colon: Int = 0x3A
|
|
|
|
const byte_minus: Int = 0x2D
|
|
|
|
const nanoseconds_per_second: Int = 1_000_000_000
|
|
|
|
fn accept_byte(from bytes: BitArray, value value: Int) -> Result(BitArray, Nil) {
|
|
case bytes {
|
|
<<byte, remaining_bytes:bytes>> if byte == value -> Ok(remaining_bytes)
|
|
_ -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn accept_empty(from bytes: BitArray) -> Result(Nil, Nil) {
|
|
case bytes {
|
|
<<>> -> Ok(Nil)
|
|
_ -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn parse_digits(
|
|
from bytes: BitArray,
|
|
count count: Int,
|
|
) -> Result(#(Int, BitArray), Nil) {
|
|
do_parse_digits(from: bytes, count:, acc: 0, k: 0)
|
|
}
|
|
|
|
fn do_parse_digits(
|
|
from bytes: BitArray,
|
|
count count: Int,
|
|
acc acc: Int,
|
|
k k: Int,
|
|
) -> Result(#(Int, BitArray), Nil) {
|
|
case bytes {
|
|
_ if k >= count -> Ok(#(acc, bytes))
|
|
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine ->
|
|
do_parse_digits(
|
|
from: remaining_bytes,
|
|
count:,
|
|
acc: acc * 10 + { byte - 0x30 },
|
|
k: k + 1,
|
|
)
|
|
_ -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn parse_year(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
|
|
parse_digits(from: bytes, count: 4)
|
|
}
|
|
|
|
// slightly modified version of parse_month that returns calendar.Month instead of Int
|
|
fn parse_month_calendar(from bytes: BitArray) -> Result(#(Month, BitArray), Nil) {
|
|
use #(month, bytes) <- result.try(parse_digits(from: bytes, count: 2))
|
|
calendar.month_from_int(month) |> result.map(pair.new(_, bytes))
|
|
}
|
|
|
|
// slightly modified version of parse_day that takes calendar.Month instead of Int
|
|
fn parse_day_calendar(
|
|
from bytes: BitArray,
|
|
year year: Int,
|
|
month month: Month,
|
|
) -> Result(#(Int, BitArray), Nil) {
|
|
use #(day, bytes) <- result.try(parse_digits(from: bytes, count: 2))
|
|
let max_day = case month {
|
|
January | March | May | July | August | October | December -> 31
|
|
April | June | September | November -> 30
|
|
February -> {
|
|
case is_leap_year(year) {
|
|
True -> 29
|
|
False -> 28
|
|
}
|
|
}
|
|
}
|
|
case 1 <= day && day <= max_day {
|
|
True -> Ok(#(day, bytes))
|
|
False -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn is_leap_year(year: Int) -> Bool {
|
|
year % 4 == 0 && { year % 100 != 0 || year % 400 == 0 }
|
|
}
|
|
|
|
fn parse_hours(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
|
|
use #(hours, bytes) <- result.try(parse_digits(from: bytes, count: 2))
|
|
case 0 <= hours && hours <= 23 {
|
|
True -> Ok(#(hours, bytes))
|
|
False -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn parse_minutes(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
|
|
use #(minutes, bytes) <- result.try(parse_digits(from: bytes, count: 2))
|
|
case 0 <= minutes && minutes <= 59 {
|
|
True -> Ok(#(minutes, bytes))
|
|
False -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn parse_seconds(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
|
|
use #(seconds, bytes) <- result.try(parse_digits(from: bytes, count: 2))
|
|
case 0 <= seconds && seconds <= 60 {
|
|
True -> Ok(#(seconds, bytes))
|
|
False -> Error(Nil)
|
|
}
|
|
}
|
|
|
|
fn parse_second_fraction_as_nanoseconds(from bytes: BitArray) {
|
|
case bytes {
|
|
<<".", byte, remaining_bytes:bytes>>
|
|
if byte_zero <= byte && byte <= byte_nine
|
|
-> {
|
|
do_parse_second_fraction_as_nanoseconds(
|
|
from: <<byte, remaining_bytes:bits>>,
|
|
acc: 0,
|
|
power: nanoseconds_per_second,
|
|
)
|
|
}
|
|
<<".", _:bytes>> -> Error(Nil)
|
|
_ -> Ok(#(0, bytes))
|
|
}
|
|
}
|
|
|
|
fn do_parse_second_fraction_as_nanoseconds(
|
|
from bytes: BitArray,
|
|
acc acc: Int,
|
|
power power: Int,
|
|
) -> Result(#(Int, BitArray), a) {
|
|
// Each digit place to the left in the fractional second is 10x fewer
|
|
// nanoseconds.
|
|
let power = power / 10
|
|
|
|
case bytes {
|
|
<<byte, remaining_bytes:bytes>>
|
|
if byte_zero <= byte && byte <= byte_nine && power < 1
|
|
-> {
|
|
// We already have the max precision for nanoseconds. Truncate any
|
|
// remaining digits.
|
|
do_parse_second_fraction_as_nanoseconds(
|
|
from: remaining_bytes,
|
|
acc:,
|
|
power:,
|
|
)
|
|
}
|
|
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine -> {
|
|
// We have not yet reached the precision limit. Parse the next digit.
|
|
let digit = byte - 0x30
|
|
do_parse_second_fraction_as_nanoseconds(
|
|
from: remaining_bytes,
|
|
acc: acc + digit * power,
|
|
power:,
|
|
)
|
|
}
|
|
_ -> Ok(#(acc, bytes))
|
|
}
|
|
}
|