From 0a1f06dbe5bb125a98c84e6487d5914108180464 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Sun, 1 Dec 2024 00:39:49 +0100 Subject: [PATCH] add date --- gleam.toml | 1 + manifest.toml | 3 ++ src/kmer.gleam | 66 +++++++++++++++++++++++++++++++++++++++----- test/kmer_test.gleam | 27 ++++++++++++++++-- 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/gleam.toml b/gleam.toml index 82a74a7..01ef133 100644 --- a/gleam.toml +++ b/gleam.toml @@ -9,6 +9,7 @@ gleam = ">= 1.6.2" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" ieee_float = ">= 1.0.3 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index bba2637..ecc9757 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,13 +2,16 @@ # You typically do not need to edit this file packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, { name = "gleam_erlang", version = "0.32.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "B18643083A0117AC5CFD0C1AEEBE5469071895ECFA426DCC26517A07F6AD9948" }, { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "ieee_float", version = "1.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "ieee_float", source = "hex", outer_checksum = "0E9B6233343BD8EDBF813B50D5480139A3E318D257EA206D5C500984A829C515" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, ] [requirements] +birl = { version = ">= 1.7.1 and < 2.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } ieee_float = { version = ">= 1.0.3 and < 2.0.0" } diff --git a/src/kmer.gleam b/src/kmer.gleam index 0d2f8ff..7d0e682 100644 --- a/src/kmer.gleam +++ b/src/kmer.gleam @@ -1,8 +1,10 @@ +import birl.{type Time} import gleam/bit_array import gleam/list.{reverse} import gleam/result.{try} import ieee_float.{ - type IEEEFloat, from_bytes_16_be, from_bytes_32_be, from_bytes_64_be, + type IEEEFloat, finite, from_bytes_16_be, from_bytes_32_be, from_bytes_64_be, + multiply, power, round, } pub type DecodedValue { @@ -13,6 +15,7 @@ pub type DecodedValue { String(value: String) Array(List(DecodedValue)) Map(List(#(DecodedValue, DecodedValue))) + DateTime(Time) Null Undefined } @@ -24,9 +27,6 @@ pub type DecodeResult = DecoderResult(DecodedValue) pub opaque type DecodeError { - // More data was expected but file/stream ended prematurely - UnexpectedEnd - // Malformed sequence found Malformed } @@ -51,6 +51,7 @@ pub fn decode(bits: BitArray) -> DecodeResult { <<5:3, rest:bits>> -> decode_map(rest) |> map_decoded(Map) + <<6:3, rest:bits>> -> decode_tag(rest) <<7:3, x:5, rest:bits>> if x < 25 -> decode_simple(<>) <<7:3, x:5, rest:bits>> if x >= 25 -> decode_float(<>) @@ -68,7 +69,7 @@ fn decode_int(bits: BitArray) -> DecoderResult(Int) { <<27:5, val:int-unsigned-size(64), rest:bits>> -> Ok(#(val, rest)) <> if x < 24 -> Ok(#(x, rest)) <> if x >= 24 -> Error(Malformed) - _ -> Error(UnexpectedEnd) + _ -> Error(Malformed) } } @@ -106,6 +107,57 @@ fn decode_map( Ok(#(pairs, remainder)) } +fn decode_tag(bits: BitArray) -> DecodeResult { + use #(tag, rest) <- try(decode_int(bits)) + + case tag { + 0 -> decode_datestring(rest) |> map_decoded(DateTime) + 1 -> decode_unixtime(rest) |> map_decoded(DateTime) + // CBOR magic number (no-op for us) + 55_799 -> decode(rest) + _ -> Error(Malformed) + } +} + +fn decode_datestring(bits: BitArray) -> DecoderResult(Time) { + use string_content <- try(case bits { + <<3:3, remainder:bits>> -> Ok(remainder) + _ -> Error(Malformed) + }) + + use #(datestring, rest) <- try(decode_string(string_content)) + + case birl.parse(datestring) { + Ok(datetime) -> Ok(#(datetime, rest)) + _ -> Error(Malformed) + } +} + +fn decode_unixtime(bits: BitArray) -> DecoderResult(Time) { + case bits { + <<0:3, rest:bits>> -> + decode_int(rest) + |> map_decoded(birl.from_unix) + // This is probably undefined behavior, as I doubt birl supports negative unixtimes + <<1:3, rest:bits>> -> + decode_int(rest) + |> map_decoded(birl.from_unix) + // Float unixtimes is also very iffy and the spec says we should be able to ignore it + // However, lets give it a shot and turn it into a millisecond value + <<7:3, rest:bits>> -> + decode_float(rest) + |> try(fn(x) { + let #(time, remainder) = x + let milli_time = multiply(time, power(finite(10.0), finite(3.0))) + case round(milli_time) { + Ok(milli_int) -> Ok(#(birl.from_unix_milli(milli_int), remainder)) + _ -> Error(Malformed) + } + }) + _ -> Error(Malformed) + } +} + fn decode_float(bits: BitArray) -> DecoderResult(IEEEFloat) { case bits { <<25:5, float16:bytes-size(2), rest:bits>> -> @@ -157,8 +209,8 @@ fn decode_next_n( fn map_decoded( result: DecoderResult(t), - map_fn: fn(t) -> DecodedValue, -) -> DecodeResult { + map_fn: fn(t) -> out, +) -> DecoderResult(out) { use #(value, rest) <- try(result) Ok(#(map_fn(value), rest)) } diff --git a/test/kmer_test.gleam b/test/kmer_test.gleam index 51ab296..c0dbc6b 100644 --- a/test/kmer_test.gleam +++ b/test/kmer_test.gleam @@ -1,10 +1,11 @@ +import birl import gleam/list import gleeunit import gleeunit/should import ieee_float.{finite, nan, positive_infinity} import kmer.{ - type DecodeResult, type DecodedValue, Array, Boolean, Bytes, Float, Integer, - Map, Null, String, Undefined, + type DecodeResult, type DecodedValue, Array, Boolean, Bytes, DateTime, Float, + Integer, Map, Null, String, Undefined, } pub fn main() { @@ -155,6 +156,28 @@ pub fn decode_map_test() { |> table_fn(table_test_decode) } +pub fn decode_time_test() { + let assert Ok(textdate_example) = birl.parse("2024-11-30T21:56:27Z") + + [ + // ISO8601 date + #( + <<6:3, 0:5, 3:3, 20:5, "2024-11-30T21:56:27Z">>, + DateTime(textdate_example), + ), + // Unixtime + #( + <<6:3, 1:5, 1:3, 26:5, 1_733_005_876:32>>, + DateTime(birl.from_unix(1_733_005_876)), + ), + #( + <<6:3, 1:5, 7:3, 27:5, 0x41D9D2E48D200000:64>>, + DateTime(birl.from_unix_milli(1_733_005_876_500)), + ), + ] + |> table_fn(table_test_decode) +} + fn table_fn(tests: List(#(a, b)), test_fn: fn(a, b) -> Nil) { list.map(tests, fn(t) { let #(test_case, expected) = t