Skip to content

Commit

Permalink
Show fractional seconds in to_rfc3339
Browse files Browse the repository at this point in the history
See #8
  • Loading branch information
mooreryan authored and lpil committed Jan 21, 2025
1 parent a910cf2 commit 260e591
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 0 deletions.
66 changes: 66 additions & 0 deletions src/gleam/time/timestamp.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import gleam/bit_array
import gleam/float
import gleam/int
import gleam/list
import gleam/order
import gleam/result
import gleam/string
Expand Down Expand Up @@ -207,6 +208,7 @@ pub fn to_rfc3339(timestamp: Timestamp, offset_minutes offset: Int) -> String {
let out = out <> n4(years) <> "-" <> n2(months) <> "-" <> n2(days)
let out = out <> "T"
let out = out <> n2(hours) <> ":" <> n2(minutes) <> ":" <> n2(seconds)
let out = out <> show_second_fraction(timestamp.nanoseconds)
case int.compare(offset, 0) {
order.Eq -> out <> "Z"
order.Gt -> out <> "+" <> n2(offset_hours) <> ":" <> n2(offset_minutes)
Expand Down Expand Up @@ -263,6 +265,70 @@ fn to_civil(minutes: Int) -> #(Int, Int, Int) {
#(year, month, day)
}

/// Converts nanoseconds into a `String` representation of fractional seconds.
///
/// Assumes that `nanoseconds < 1_000_000_000`, which will be true for any
/// normalised timestamp.
///
fn show_second_fraction(nanoseconds: Int) -> String {
case int.compare(nanoseconds, 0) {
// Zero fractional seconds are not shown.
order.Lt | order.Eq -> ""
order.Gt -> {
let second_fraction_part = {
nanoseconds
|> get_zero_padded_digits
|> remove_trailing_zeros
|> list.map(int.to_string)
|> string.join("")
}

"." <> second_fraction_part
}
}
}

/// Given a list of digits, return new list with any trailing zeros removed.
///
fn remove_trailing_zeros(digits: List(Int)) -> List(Int) {
let reversed_digits = list.reverse(digits)

do_remove_trailing_zeros(reversed_digits)
}

fn do_remove_trailing_zeros(reversed_digits) {
case reversed_digits {
[] -> []
[digit, ..digits] if digit == 0 -> do_remove_trailing_zeros(digits)
reversed_digits -> list.reverse(reversed_digits)
}
}

/// Returns the list of digits of `number`. If the number of digits is less
/// than 9, the result is zero-padded at the front.
///
fn get_zero_padded_digits(number: Int) -> List(Int) {
do_get_zero_padded_digits(number, [], 0)
}

fn do_get_zero_padded_digits(
number: Int,
digits: List(Int),
count: Int,
) -> List(Int) {
case number {
number if number <= 0 && count >= 9 -> digits
number if number <= 0 ->
// Zero-pad the digits at the front until we have at least 9 digits.
do_get_zero_padded_digits(number, [0, ..digits], count + 1)
number -> {
let digit = number % 10
let number = floored_div(number, 10.0)
do_get_zero_padded_digits(number, [digit, ..digits], count + 1)
}
}
}

/// Parses an RFC 3339 formatted time string into a `Timestamp`.
///
/// # Examples
Expand Down
54 changes: 54 additions & 0 deletions test/gleam/time/timestamp_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,60 @@ pub fn to_rfc3339_12_test() {
|> should.equal("0100-01-01T00:00:00Z")
}

pub fn to_rfc3339_13_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 1)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00.000000001Z")
}

pub fn to_rfc3339_14_test() {
timestamp.from_unix_seconds_and_nanoseconds(-1, 12)
|> timestamp.to_rfc3339(0)
|> should.equal("1969-12-31T23:59:59.000000012Z")
}

pub fn to_rfc3339_15_test() {
timestamp.from_unix_seconds_and_nanoseconds(1, 123)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:01.000000123Z")
}

pub fn to_rfc3339_16_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 1230)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00.00000123Z")
}

pub fn to_rfc3339_17_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 500_600_000)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00.5006Z")
}

pub fn to_rfc3339_18_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 500_006)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00.000500006Z")
}

pub fn to_rfc3339_19_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 999_999_999)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00.999999999Z")
}

pub fn to_rfc3339_20_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 0)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:00Z")
}

pub fn to_rfc3339_21_test() {
timestamp.from_unix_seconds_and_nanoseconds(0, 1_000_000_001)
|> timestamp.to_rfc3339(0)
|> should.equal("1970-01-01T00:00:01.000000001Z")
}

// RFC 3339 Parsing

pub fn parse_rfc3339_0_test() {
Expand Down

0 comments on commit 260e591

Please sign in to comment.