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 { <> 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)) <> 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: <>, 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 { <> 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:, ) } <> 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)) } }