From 520b5d380c6a5ae2fd062bd87ab41477d9cf1305 Mon Sep 17 00:00:00 2001 From: Lily Rose Date: Wed, 9 Jul 2025 01:52:46 +1000 Subject: [PATCH] Move date and timestamp to separate model files --- src/spacetraders_models/account.gleam | 5 +- src/spacetraders_models/agent_event.gleam | 5 +- src/spacetraders_models/chart.gleam | 8 +- .../chart_transaction.gleam | 5 +- src/spacetraders_models/contract.gleam | 5 +- src/spacetraders_models/contract_terms.gleam | 5 +- src/spacetraders_models/cooldown.gleam | 5 +- src/spacetraders_models/date.gleam | 125 ++++++++++ src/spacetraders_models/internal/jwt.gleam | 11 +- src/spacetraders_models/internal/time.gleam | 219 ------------------ .../market_transaction.gleam | 5 +- .../repair_transaction.gleam | 5 +- .../scrap_transaction.gleam | 5 +- .../ship_fuel_consumed.gleam | 5 +- .../ship_modification_transaction.gleam | 5 +- src/spacetraders_models/ship_nav_route.gleam | 10 +- .../shipyard_transaction.gleam | 5 +- src/spacetraders_models/survey.gleam | 11 +- src/spacetraders_models/timestamp.gleam | 27 +++ 19 files changed, 189 insertions(+), 282 deletions(-) create mode 100644 src/spacetraders_models/date.gleam delete mode 100644 src/spacetraders_models/internal/time.gleam create mode 100644 src/spacetraders_models/timestamp.gleam diff --git a/src/spacetraders_models/account.gleam b/src/spacetraders_models/account.gleam index e2dd366..701af76 100644 --- a/src/spacetraders_models/account.gleam +++ b/src/spacetraders_models/account.gleam @@ -1,8 +1,7 @@ import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/account_id.{type AccountId} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} pub type Account { Account( @@ -25,6 +24,6 @@ pub fn decoder() -> Decoder(Account) { option.None, decode.optional(decode.string), ) - use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder()) + use created_at <- decode.field("createdAt", timestamp.decoder()) decode.success(Account(id:, email:, token:, created_at:)) } diff --git a/src/spacetraders_models/agent_event.gleam b/src/spacetraders_models/agent_event.gleam index 3f6b1ce..bf8b924 100644 --- a/src/spacetraders_models/agent_event.gleam +++ b/src/spacetraders_models/agent_event.gleam @@ -1,9 +1,8 @@ import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/agent_event_id.{type AgentEventId} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} pub type AgentEvent { AgentEvent( @@ -24,6 +23,6 @@ pub fn decoder() -> Decoder(AgentEvent) { option.None, decode.optional(decode.dynamic), ) - use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder()) + use created_at <- decode.field("createdAt", timestamp.decoder()) decode.success(AgentEvent(id:, type_:, message:, data:, created_at:)) } diff --git a/src/spacetraders_models/chart.gleam b/src/spacetraders_models/chart.gleam index 18dd799..957a6f3 100644 --- a/src/spacetraders_models/chart.gleam +++ b/src/spacetraders_models/chart.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/agent_symbol.{type AgentSymbol} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type Chart { @@ -18,9 +17,6 @@ pub fn decoder() -> Decoder(Chart) { waypoint_symbol.decoder(), ) use submitted_by <- decode.field("submittedBy", agent_symbol.decoder()) - use submitted_on <- decode.field( - "submittedOn", - time.rfc3339_timestamp_decoder(), - ) + use submitted_on <- decode.field("submittedOn", timestamp.decoder()) decode.success(Chart(waypoint_symbol:, submitted_by:, submitted_on:)) } diff --git a/src/spacetraders_models/chart_transaction.gleam b/src/spacetraders_models/chart_transaction.gleam index b6b0de6..e8d5975 100644 --- a/src/spacetraders_models/chart_transaction.gleam +++ b/src/spacetraders_models/chart_transaction.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type ChartTransaction { @@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(ChartTransaction) { ) use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder()) use total_price <- decode.field("totalPrice", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(ChartTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/contract.gleam b/src/spacetraders_models/contract.gleam index d332110..f38ea43 100644 --- a/src/spacetraders_models/contract.gleam +++ b/src/spacetraders_models/contract.gleam @@ -1,11 +1,10 @@ import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/contract_id.{type ContractId} import spacetraders_models/contract_terms.{type ContractTerms} import spacetraders_models/contract_type.{type ContractType} import spacetraders_models/faction_symbol.{type FactionSymbol} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} pub type Contract { Contract( @@ -29,7 +28,7 @@ pub fn decoder() -> Decoder(Contract) { use deadline_to_accept <- decode.optional_field( "deadlineToAccept", option.None, - decode.optional(time.rfc3339_timestamp_decoder()), + decode.optional(timestamp.decoder()), ) decode.success(Contract( id:, diff --git a/src/spacetraders_models/contract_terms.gleam b/src/spacetraders_models/contract_terms.gleam index 46752db..c11eda6 100644 --- a/src/spacetraders_models/contract_terms.gleam +++ b/src/spacetraders_models/contract_terms.gleam @@ -1,9 +1,8 @@ import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/contract_deliver_good.{type ContractDeliverGood} import spacetraders_models/contract_payment.{type ContractPayment} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} pub type ContractTerms { ContractTerms( @@ -14,7 +13,7 @@ pub type ContractTerms { } pub fn decoder() -> Decoder(ContractTerms) { - use deadline <- decode.field("deadline", time.rfc3339_timestamp_decoder()) + use deadline <- decode.field("deadline", timestamp.decoder()) use payment <- decode.field("payment", contract_payment.decoder()) use deliver <- decode.optional_field( "deliver", diff --git a/src/spacetraders_models/cooldown.gleam b/src/spacetraders_models/cooldown.gleam index 85793f6..c2db25b 100644 --- a/src/spacetraders_models/cooldown.gleam +++ b/src/spacetraders_models/cooldown.gleam @@ -1,8 +1,7 @@ import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} pub type Cooldown { Cooldown( @@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(Cooldown) { use expiration <- decode.optional_field( "expiration", option.None, - decode.optional(time.rfc3339_timestamp_decoder()), + decode.optional(timestamp.decoder()), ) decode.success(Cooldown( ship_symbol:, diff --git a/src/spacetraders_models/date.gleam b/src/spacetraders_models/date.gleam new file mode 100644 index 0000000..c374cf7 --- /dev/null +++ b/src/spacetraders_models/date.gleam @@ -0,0 +1,125 @@ +import gleam/bit_array +import gleam/dynamic/decode.{type Decoder} +import gleam/int +import gleam/json.{type Json} +import gleam/pair +import gleam/result +import gleam/time/calendar.{ + type Month, April, August, Date, December, February, January, July, June, + March, May, November, October, September, +} + +pub type Date = + calendar.Date + +pub fn parse(value: String) -> Result(Date, Nil) { + let bytes = bit_array.from_string(value) + 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 decoder() -> Decoder(Date) { + use value <- decode.then(decode.string) + case parse(value) { + Ok(date) -> decode.success(date) + Error(Nil) -> decode.failure(Date(1970, calendar.January, 1), "Date") + } +} + +pub fn encode(date: Date) -> Json { + json.string( + int.to_string(date.year) + <> "-" + <> int.to_string(calendar.month_to_int(date.month)) + <> "-" + <> int.to_string(date.day), + ) +} + +const byte_zero: Int = 0x30 + +const byte_nine: Int = 0x39 + +const byte_minus: Int = 0x2D + +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 } +} diff --git a/src/spacetraders_models/internal/jwt.gleam b/src/spacetraders_models/internal/jwt.gleam index aba9d1e..686745a 100644 --- a/src/spacetraders_models/internal/jwt.gleam +++ b/src/spacetraders_models/internal/jwt.gleam @@ -4,9 +4,8 @@ import gleam/json import gleam/option.{type Option} import gleam/result import gleam/string -import gleam/time/calendar.{type Date} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time +import spacetraders_models/date.{type Date} +import spacetraders_models/timestamp.{type Timestamp} pub type JwtDecodeError { MissingHeader @@ -91,10 +90,10 @@ fn jwt_payload_decoder() -> Decoder(JwtPayload) { use reset_date <- decode.optional_field( "reset_date", option.None, - decode.optional(time.iso8601_date_decoder()), + decode.optional(date.decoder()), ) - use issued_at_int <- decode.field("iat", decode.int) - let issued_at = timestamp.from_unix_seconds(issued_at_int) + use issued_at_unix <- decode.field("iat", decode.int) + let issued_at = timestamp.from_unix(issued_at_unix) use subject <- decode.field("sub", decode.string) decode.success(JwtPayload( identifier:, diff --git a/src/spacetraders_models/internal/time.gleam b/src/spacetraders_models/internal/time.gleam deleted file mode 100644 index 6431bb3..0000000 --- a/src/spacetraders_models/internal/time.gleam +++ /dev/null @@ -1,219 +0,0 @@ -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)) - } -} diff --git a/src/spacetraders_models/market_transaction.gleam b/src/spacetraders_models/market_transaction.gleam index aad9510..58bf446 100644 --- a/src/spacetraders_models/market_transaction.gleam +++ b/src/spacetraders_models/market_transaction.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/trade_symbol.{type TradeSymbol} import spacetraders_models/transaction_type.{type TransactionType} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} @@ -30,7 +29,7 @@ pub fn decoder() -> Decoder(MarketTransaction) { use units <- decode.field("units", decode.int) use price_per_unit <- decode.field("pricePerUnit", decode.int) use total_price <- decode.field("totalPrice", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(MarketTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/repair_transaction.gleam b/src/spacetraders_models/repair_transaction.gleam index cbe7a0b..cd1c619 100644 --- a/src/spacetraders_models/repair_transaction.gleam +++ b/src/spacetraders_models/repair_transaction.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type RepairTransaction { @@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(RepairTransaction) { ) use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder()) use total_price <- decode.field("totalPrice", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(RepairTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/scrap_transaction.gleam b/src/spacetraders_models/scrap_transaction.gleam index 003a6f6..7d9ab89 100644 --- a/src/spacetraders_models/scrap_transaction.gleam +++ b/src/spacetraders_models/scrap_transaction.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type ScrapTransaction { @@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(ScrapTransaction) { ) use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder()) use total_price <- decode.field("totalPrice", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(ScrapTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/ship_fuel_consumed.gleam b/src/spacetraders_models/ship_fuel_consumed.gleam index 6b34759..2ba89b0 100644 --- a/src/spacetraders_models/ship_fuel_consumed.gleam +++ b/src/spacetraders_models/ship_fuel_consumed.gleam @@ -1,6 +1,5 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time +import spacetraders_models/timestamp.{type Timestamp} pub type ShipFuelConsumed { ShipFuelConsumed(amount: Int, timestamp: Timestamp) @@ -8,6 +7,6 @@ pub type ShipFuelConsumed { pub fn decoder() -> Decoder(ShipFuelConsumed) { use amount <- decode.field("amount", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(ShipFuelConsumed(amount:, timestamp:)) } diff --git a/src/spacetraders_models/ship_modification_transaction.gleam b/src/spacetraders_models/ship_modification_transaction.gleam index cc03611..894a61d 100644 --- a/src/spacetraders_models/ship_modification_transaction.gleam +++ b/src/spacetraders_models/ship_modification_transaction.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_symbol.{type ShipSymbol} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/trade_symbol.{type TradeSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} @@ -23,7 +22,7 @@ pub fn decoder() -> Decoder(ShipModificationTransaction) { use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder()) use trade_symbol <- decode.field("tradeSymbol", trade_symbol.decoder()) use total_price <- decode.field("totalPrice", decode.int) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(ShipModificationTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/ship_nav_route.gleam b/src/spacetraders_models/ship_nav_route.gleam index 0280a55..68edd85 100644 --- a/src/spacetraders_models/ship_nav_route.gleam +++ b/src/spacetraders_models/ship_nav_route.gleam @@ -1,7 +1,6 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/ship_nav_route_waypoint.{type ShipNavRouteWaypoint} +import spacetraders_models/timestamp.{type Timestamp} pub type ShipNavRoute { ShipNavRoute( @@ -18,10 +17,7 @@ pub fn decoder() -> Decoder(ShipNavRoute) { ship_nav_route_waypoint.decoder(), ) use origin <- decode.field("origin", ship_nav_route_waypoint.decoder()) - use departure_time <- decode.field( - "departureTime", - time.rfc3339_timestamp_decoder(), - ) - use arrival <- decode.field("arrival", time.rfc3339_timestamp_decoder()) + use departure_time <- decode.field("departureTime", timestamp.decoder()) + use arrival <- decode.field("arrival", timestamp.decoder()) decode.success(ShipNavRoute(destination:, origin:, departure_time:, arrival:)) } diff --git a/src/spacetraders_models/shipyard_transaction.gleam b/src/spacetraders_models/shipyard_transaction.gleam index ef35949..3ad3bd4 100644 --- a/src/spacetraders_models/shipyard_transaction.gleam +++ b/src/spacetraders_models/shipyard_transaction.gleam @@ -1,8 +1,7 @@ import gleam/dynamic/decode.{type Decoder} -import gleam/time/timestamp.{type Timestamp} import spacetraders_models/agent_symbol.{type AgentSymbol} -import spacetraders_models/internal/time import spacetraders_models/ship_type.{type ShipType} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type ShipyardTransaction { @@ -23,7 +22,7 @@ pub fn decoder() -> Decoder(ShipyardTransaction) { use ship_type <- decode.field("shipType", ship_type.decoder()) use price <- decode.field("price", decode.int) use agent_symbol <- decode.field("agentSymbol", agent_symbol.decoder()) - use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) + use timestamp <- decode.field("timestamp", timestamp.decoder()) decode.success(ShipyardTransaction( waypoint_symbol:, ship_type:, diff --git a/src/spacetraders_models/survey.gleam b/src/spacetraders_models/survey.gleam index 2a11d5d..a9481a6 100644 --- a/src/spacetraders_models/survey.gleam +++ b/src/spacetraders_models/survey.gleam @@ -1,11 +1,9 @@ import gleam/dynamic/decode.{type Decoder} import gleam/json.{type Json} -import gleam/time/calendar -import gleam/time/timestamp.{type Timestamp} -import spacetraders_models/internal/time import spacetraders_models/survey_deposit.{type SurveyDeposit} import spacetraders_models/survey_signature.{type SurveySignature} import spacetraders_models/survey_size.{type SurveySize} +import spacetraders_models/timestamp.{type Timestamp} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} pub type Survey { @@ -25,7 +23,7 @@ pub fn decoder() -> Decoder(Survey) { "deposits", decode.list(survey_deposit.decoder()), ) - use expiration <- decode.field("expiration", time.rfc3339_timestamp_decoder()) + use expiration <- decode.field("expiration", timestamp.decoder()) use size <- decode.field("size", survey_size.decoder()) decode.success(Survey(signature:, symbol:, deposits:, expiration:, size:)) } @@ -35,10 +33,7 @@ pub fn encode(survey: Survey) -> Json { #("signature", survey_signature.encode(survey.signature)), #("symbol", waypoint_symbol.encode(survey.symbol)), #("deposits", json.array(survey.deposits, survey_deposit.encode)), - #( - "expiration", - json.string(timestamp.to_rfc3339(survey.expiration, calendar.utc_offset)), - ), + #("expiration", timestamp.encode(survey.expiration)), #("size", survey_size.encode(survey.size)), ]) } diff --git a/src/spacetraders_models/timestamp.gleam b/src/spacetraders_models/timestamp.gleam new file mode 100644 index 0000000..2393b09 --- /dev/null +++ b/src/spacetraders_models/timestamp.gleam @@ -0,0 +1,27 @@ +import gleam/dynamic/decode.{type Decoder} +import gleam/json.{type Json} +import gleam/time/calendar +import gleam/time/timestamp + +pub type Timestamp = + timestamp.Timestamp + +pub fn parse(value: String) -> Result(Timestamp, Nil) { + timestamp.parse_rfc3339(value) +} + +pub fn from_unix(seconds: Int) -> Timestamp { + timestamp.from_unix_seconds(seconds) +} + +pub fn decoder() -> Decoder(Timestamp) { + use value <- decode.then(decode.string) + case parse(value) { + Ok(timestamp) -> decode.success(timestamp) + Error(Nil) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp") + } +} + +pub fn encode(timestamp: Timestamp) -> Json { + json.string(timestamp.to_rfc3339(timestamp, calendar.utc_offset)) +}