Skip to content

Commit

Permalink
Timestamp functions
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Dec 27, 2024
1 parent 0dc9da1 commit 716c30f
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 29 deletions.
5 changes: 5 additions & 0 deletions src/gleam/time/duration.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import gleam/string

/// An amount of time, with up to nanosecond precision.
///
/// This type does not represent calendar periods such as "1 month" or "2
/// days". Those periods will be different lengths of time depending on which
/// month or day they apply to. For example, January is longer than February.
/// A different type should be used for calendar periods.
///
pub opaque type Duration {
// When compiling to JavaScript ints have limited precision and size. This
// means that if we were to store the the timestamp in a single int the
Expand Down
118 changes: 89 additions & 29 deletions src/gleam/time/timestamp.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import gleam/int
import gleam/order
import gleam/time/duration.{type Duration}

Expand Down Expand Up @@ -52,39 +53,88 @@ pub opaque type Timestamp {
Timestamp(seconds: Int, nanoseconds: Int)
}

// TODO: test
/// Ensure the time is represented with `nanoseconds` being positive and less
/// than 1 second.
///
/// This function does not change the time that the timestamp refers to, it
/// only adjusts the values used to represent the time.
///
fn normalise(timestamp: Timestamp) -> Timestamp {
todo
let multiplier = 1_000_000_000
let nanoseconds = timestamp.nanoseconds % multiplier
let overflow = timestamp.nanoseconds - nanoseconds
let seconds = timestamp.seconds + overflow / multiplier
Timestamp(seconds, nanoseconds)
}

// TODO: docs
// TODO: test
/// Compare one timestamp to another, indicating whether the first is greater or
/// smaller than the second.
///
/// # Examples
///
/// ```gleam
/// compare(from_unix_seconds(1), from_unix_seconds(2))
/// // -> order.Lt
/// ```
///
pub fn compare(left: Timestamp, right: Timestamp) -> order.Order {
todo
order.break_tie(
int.compare(left.seconds, right.seconds),
int.compare(left.nanoseconds, right.nanoseconds),
)
}

// TODO: docs
// TODO: test
/// Get the current system time.
///
/// Note this time is not unique or monotonic, it could change at any time or
/// even go backwards! The exact behaviour will depend on the runtime used. See
/// the module documentation for more information.
///
/// On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses
/// [`Date.now`][2].
///
/// [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1
/// [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
///
pub fn system_time() -> Timestamp {
todo
let #(seconds, nanoseconds) = get_system_time()
normalise(Timestamp(seconds, nanoseconds))
}

// TODO: docs
// TODO: test
@external(erlang, "gleam_time_ffi", "system_time")
@external(javascript, "../../gleam_time_ffi.mjs", "system_time")
fn get_system_time() -> #(Int, Int)

/// Calculate the difference between two timestamps.
///
/// This is effectively substracting the first timestamp from the second.
///
/// # Examples
///
/// ```gleam
/// difference(from_unix_seconds(1), from_unix_seconds(5))
/// // -> duration.seconds(4)
/// ```
///
pub fn difference(left: Timestamp, right: Timestamp) -> Duration {
todo
let seconds = duration.seconds(right.seconds - left.seconds)
let nanoseconds = duration.nanoseconds(right.nanoseconds - left.nanoseconds)
duration.add(seconds, nanoseconds)
}

// TODO: docs
// TODO: test
pub fn add(timetamp: Timestamp, duration: Duration) -> Duration {
todo
/// Add a duration to a timestamp.
///
/// # Examples
///
/// ```gleam
/// add(from_unix_seconds(1000), duration.seconds(5))
/// // -> from_unix_seconds(1005)
/// ```
///
pub fn add(timestamp: Timestamp, duration: Duration) -> Timestamp {
let #(seconds, nanoseconds) = duration.to_seconds_and_nanoseconds(duration)
Timestamp(timestamp.seconds + seconds, timestamp.nanoseconds + nanoseconds)
|> normalise
}

// TODO: docs
Expand All @@ -101,29 +151,39 @@ pub fn add(timetamp: Timestamp, duration: Duration) -> Duration {
// todo
// }

// TODO: docs
// TODO: test
/// Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January
/// 1970.
///
pub fn from_unix_seconds(seconds: Int) -> Timestamp {
todo
Timestamp(seconds, 0)
}

// TODO: docs
// TODO: test
/// Create a timestamp from a number of seconds and nanoseconds since 00:00:00
/// UTC on 1 January 1970.
///
pub fn from_unix_seconds_and_nanoseconds(
seconds seconds: Int,
nanoseconds nanoseconds: Int,
) -> Timestamp {
todo
Timestamp(seconds, nanoseconds)
|> normalise
}

// TODO: docs
// TODO: test
pub fn to_unix_seconds(input: String) -> Float {
todo
/// Convert the timestamp to a number of seconds since 00:00:00 UTC on 1
/// January 1970.
///
/// There may be some small loss of precision due to `Timestamp` being
/// nanosecond accurate and `Float` not being able to represent this.
///
pub fn to_unix_seconds(timestamp: Timestamp) -> Float {
let seconds = int.to_float(timestamp.seconds)
let nanoseconds = int.to_float(timestamp.nanoseconds)
seconds +. { nanoseconds /. 1_000_000_000.0 }
}

// TODO: docs
// TODO: test
pub fn to_unix_seconds_and_nanoseconds(input: String) -> #(Int, Int) {
todo
/// Convert the timestamp to a number of seconds and nanoseconds since 00:00:00
/// UTC on 1 January 1970. There is no loss of precision with this conversion
/// on any target.
pub fn to_unix_seconds_and_nanoseconds(timestamp: Timestamp) -> #(Int, Int) {
#(timestamp.seconds, timestamp.nanoseconds)
}
5 changes: 5 additions & 0 deletions src/gleam_time_ffi.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-module(gleam_time_ffi).
-export([system_time/0]).

system_time() ->
{0, erlang:system_time(nanosecond)}.
7 changes: 7 additions & 0 deletions src/gleam_time_ffi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function system_time() {
const now = Date.now();
const milliseconds = now % 1_000;
const nanoseconds = milliseconds * 1000_000;
const seconds = (now - milliseconds) / 1_000;
return [seconds, nanoseconds];
}
107 changes: 107 additions & 0 deletions test/gleam/time/timestamp_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import gleam/order
import gleam/time/duration
import gleam/time/timestamp
import gleeunit/should

pub fn compare_0_test() {
timestamp.compare(
timestamp.from_unix_seconds(1),
timestamp.from_unix_seconds(1),
)
|> should.equal(order.Eq)
}

pub fn compare_1_test() {
timestamp.compare(
timestamp.from_unix_seconds(2),
timestamp.from_unix_seconds(1),
)
|> should.equal(order.Gt)
}

pub fn compare_2_test() {
timestamp.compare(
timestamp.from_unix_seconds(2),
timestamp.from_unix_seconds(3),
)
|> should.equal(order.Lt)
}

pub fn difference_0_test() {
timestamp.difference(
timestamp.from_unix_seconds(1),
timestamp.from_unix_seconds(1),
)
|> should.equal(duration.seconds(0))
}

pub fn difference_1_test() {
timestamp.difference(
timestamp.from_unix_seconds(1),
timestamp.from_unix_seconds(5),
)
|> should.equal(duration.seconds(4))
}

pub fn difference_2_test() {
timestamp.difference(
timestamp.from_unix_seconds_and_nanoseconds(1, 10),
timestamp.from_unix_seconds_and_nanoseconds(5, 20),
)
|> should.equal(duration.seconds(4) |> duration.add(duration.nanoseconds(10)))
}

pub fn add_0_test() {
timestamp.from_unix_seconds(0)
|> timestamp.add(duration.seconds(1))
|> should.equal(timestamp.from_unix_seconds(1))
}

pub fn add_1_test() {
timestamp.from_unix_seconds(100)
|> timestamp.add(duration.seconds(-1))
|> should.equal(timestamp.from_unix_seconds(99))
}

pub fn add_2_test() {
timestamp.from_unix_seconds(99)
|> timestamp.add(duration.nanoseconds(100))
|> should.equal(timestamp.from_unix_seconds_and_nanoseconds(99, 100))
}

pub fn to_unix_seconds_0_test() {
timestamp.from_unix_seconds_and_nanoseconds(1, 0)
|> timestamp.to_unix_seconds
|> should.equal(1.0)
}

pub fn to_unix_seconds_1_test() {
timestamp.from_unix_seconds_and_nanoseconds(1, 500_000_000)
|> timestamp.to_unix_seconds
|> should.equal(1.5)
}

pub fn to_unix_seconds_and_nanoseconds_0_test() {
timestamp.from_unix_seconds_and_nanoseconds(1, 0)
|> timestamp.to_unix_seconds_and_nanoseconds
|> should.equal(#(1, 0))
}

pub fn to_unix_seconds_and_nanoseconds_1_test() {
timestamp.from_unix_seconds_and_nanoseconds(1, 2)
|> timestamp.to_unix_seconds_and_nanoseconds
|> should.equal(#(1, 2))
}

pub fn system_time_0_test() {
let #(now, _) =
timestamp.system_time() |> timestamp.to_unix_seconds_and_nanoseconds

// This test will start to fail once enough time has passed.
// When that happens please update these values.
let when_this_test_was_last_updated = 1_735_307_287
let christmas_day_2025 = 1_766_620_800

let assert True = now > when_this_test_was_last_updated
let assert True = now < christmas_day_2025
}

0 comments on commit 716c30f

Please sign in to comment.