Current section
Files
Jump to
Current section
Files
src/calendar/time.gleam
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
// SPDX-FileCopyrightText: 2012 Plataformatec
import calendar/duration as calendar_duration
import gleam/int
import gleam/string
@external(erlang, "gleam_time_ffi", "system_time")
@external(javascript, "../gleam_time_ffi.mjs", "system_time")
fn system_time() -> #(Int, Int)
/// A Time struct and functions.
///
/// The Time struct contains the fields hour, minute, second and microseconds.
/// New times can be built with the `new` function.
///
/// The functions on this module work with the `Time` struct as well
/// as any struct that contains the same fields as the `Time` struct,
/// such as `NaiveDateTime` and `DateTime`.
pub type Time {
Time(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
)
}
pub type TimeError {
InvalidTime
InvalidHour
InvalidMinute
InvalidSecond
InvalidMicrosecond
}
const seconds_per_day = 86_400
/// Creates a new Time struct.
///
/// Returns an error if the time is invalid.
pub fn new(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
) -> Result(Time, TimeError) {
case is_valid_time(hour, minute, second, microsecond) {
True ->
Ok(Time(
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: calendar,
))
False -> Error(InvalidTime)
}
}
/// Creates a new Time struct with default values.
/// Uses microsecond #(0, 0) and Calendar.ISO as defaults.
pub fn new_simple(
hour: Int,
minute: Int,
second: Int,
) -> Result(Time, TimeError) {
new(hour, minute, second, #(0, 0), "Calendar.ISO")
}
/// Validates if the given time components are valid.
fn is_valid_time(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
) -> Bool {
let #(ms, precision) = microsecond
hour >= 0
&& hour <= 23
&& minute >= 0
&& minute <= 59
&& second >= 0
&& second <= 59
&& ms >= 0
&& ms < 1_000_000
&& precision >= 0
&& precision <= 6
}
/// Converts a Time to a string in ISO8601 format.
pub fn to_string(time: Time) -> String {
let hour_str = pad_zero(time.hour)
let minute_str = pad_zero(time.minute)
let second_str = pad_zero(time.second)
let #(ms, precision) = time.microsecond
case ms == 0 || precision == 0 {
True -> hour_str <> ":" <> minute_str <> ":" <> second_str
False -> {
let ms_str = pad_microseconds(ms, precision)
hour_str <> ":" <> minute_str <> ":" <> second_str <> "." <> ms_str
}
}
}
/// Converts a Time to ISO8601 format.
pub fn to_iso8601(time: Time) -> String {
to_iso8601_with_format(time, Extended)
}
pub type TimeFormat {
Extended
Basic
}
/// Converts a Time to ISO8601 format with specified format.
pub fn to_iso8601_with_format(time: Time, format: TimeFormat) -> String {
case format {
Extended -> to_string(time)
Basic -> {
let hour_str = pad_zero(time.hour)
let minute_str = pad_zero(time.minute)
let second_str = pad_zero(time.second)
let #(ms, precision) = time.microsecond
case ms == 0 || precision == 0 {
True -> hour_str <> minute_str <> second_str
False -> {
let ms_str = pad_microseconds(ms, precision)
hour_str <> minute_str <> second_str <> "." <> ms_str
}
}
}
}
}
/// Converts time to total seconds since midnight.
pub fn to_seconds_after_midnight(time: Time) -> Int {
time.hour * 3600 + time.minute * 60 + time.second
}
/// Converts time to total seconds since midnight as a tuple with microseconds.
pub fn to_seconds_after_midnight_tuple(time: Time) -> #(Int, Int) {
let #(ms, _precision) = time.microsecond
let total_seconds = time.hour * 3600 + time.minute * 60 + time.second
#(total_seconds, ms)
}
/// Converts time to total microseconds since midnight.
pub fn to_microseconds_after_midnight(time: Time) -> Int {
let #(ms, _precision) = time.microsecond
let total_seconds = time.hour * 3600 + time.minute * 60 + time.second
total_seconds * 1_000_000 + ms
}
/// Creates a Time from seconds since midnight.
pub fn from_seconds_after_midnight(seconds: Int) -> Result(Time, TimeError) {
from_seconds_after_midnight_with_microsecond(seconds, #(0, 0), "Calendar.ISO")
}
/// Creates a Time from seconds since midnight with microsecond precision.
pub fn from_seconds_after_midnight_with_microsecond(
seconds: Int,
microsecond: #(Int, Int),
calendar: String,
) -> Result(Time, TimeError) {
case seconds >= 0 && seconds < seconds_per_day {
True -> {
let hours = seconds / 3600
let remaining_seconds = seconds % 3600
let minutes = remaining_seconds / 60
let secs = remaining_seconds % 60
new(hours, minutes, secs, microsecond, calendar)
}
False -> Error(InvalidTime)
}
}
/// Converts a Time to a Unix timestamp (seconds since epoch).
/// Note: This requires a date to be meaningful. This function assumes
/// the Unix epoch date (1970-01-01) for demonstration purposes.
pub fn to_timestamp(time: Time) -> Int {
to_seconds_after_midnight(time)
}
/// Creates a Time from a Unix timestamp.
/// Note: This extracts only the time portion, ignoring the date.
pub fn from_timestamp(timestamp: Int) -> Result(Time, TimeError) {
let seconds_in_day = timestamp % seconds_per_day
from_seconds_after_midnight(seconds_in_day)
}
/// Returns the current time in UTC.
pub fn utc_now() -> Result(Time, TimeError) {
utc_now_with_precision(Microsecond, "Calendar.ISO")
}
/// Returns the current time in UTC with specified precision and calendar.
pub fn utc_now_with_precision(
precision: TimeUnit,
calendar: String,
) -> Result(Time, TimeError) {
let #(_seconds, nanoseconds) = system_time()
let total_seconds = nanoseconds / 1_000_000_000
let remaining_nanos = nanoseconds % 1_000_000_000
let microseconds = remaining_nanos / 1000
let hours = { total_seconds / 3600 } % 24
let remaining = total_seconds % 3600
let minutes = remaining / 60
let seconds = remaining % 60
let adjusted_precision = case precision {
Second -> #(0, 0)
Millisecond -> #(microseconds / 1000 * 1000, 3)
Microsecond | Native -> #(microseconds, 6)
}
new(hours, minutes, seconds, adjusted_precision, calendar)
}
pub type TimeUnit {
Second
Millisecond
Microsecond
Native
}
/// Creates a new Time, panicking if invalid.
pub fn new_unchecked_bang(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
) -> Time {
case new(hour, minute, second, microsecond, calendar) {
Ok(time) -> time
Error(_) -> panic as "Invalid time"
}
}
/// Parses an ISO8601 time string.
pub fn from_iso8601(time_string: String) -> Result(Time, TimeError) {
case parse_iso8601_time(time_string) {
Ok(#(hour, minute, second, microsecond)) ->
new(hour, minute, second, microsecond, "Calendar.ISO")
Error(_) -> Error(InvalidTime)
}
}
/// Parses an ISO8601 time string, panicking on error.
pub fn from_iso8601_unchecked(time_string: String) -> Time {
case from_iso8601(time_string) {
Ok(time) -> time
Error(_) -> panic as "Invalid ISO8601 time format"
}
}
/// Converts time to Erlang time tuple.
pub fn to_erl(time: Time) -> #(Int, Int, Int) {
#(time.hour, time.minute, time.second)
}
/// Creates time from Erlang time tuple.
pub fn from_erl(
erl_time: #(Int, Int, Int),
microsecond: #(Int, Int),
calendar: String,
) -> Result(Time, TimeError) {
let #(hour, minute, second) = erl_time
new(hour, minute, second, microsecond, calendar)
}
/// Creates time from Erlang time tuple, panicking on error.
pub fn from_erl_unchecked(
erl_time: #(Int, Int, Int),
microsecond: #(Int, Int),
calendar: String,
) -> Time {
case from_erl(erl_time, microsecond, calendar) {
Ok(time) -> time
Error(_) -> panic as "Invalid Erlang time tuple"
}
}
/// Adds a duration to a time.
pub fn add(time: Time, amount: Int, unit: TimeUnit) -> Result(Time, TimeError) {
let total_microseconds = to_microseconds_after_midnight(time)
let microseconds_to_add = case unit {
Second -> amount * 1_000_000
Millisecond -> amount * 1000
Microsecond -> amount
Native -> amount
// Assume microseconds for native
}
let new_total = total_microseconds + microseconds_to_add
let new_seconds = new_total / 1_000_000
let new_microseconds = new_total % 1_000_000
case from_seconds_after_midnight(new_seconds % seconds_per_day) {
Ok(base_time) -> {
let #(_, precision) = time.microsecond
case
new(
base_time.hour,
base_time.minute,
base_time.second,
#(new_microseconds, precision),
time.calendar,
)
{
Ok(t) -> Ok(t)
Error(e) -> Error(e)
}
}
Error(err) -> Error(err)
}
}
/// Compare two times.
pub type TimeComparison {
Lt
Eq
Gt
}
pub fn compare(time1: Time, time2: Time) -> TimeComparison {
let ms1 = to_microseconds_after_midnight(time1)
let ms2 = to_microseconds_after_midnight(time2)
case ms1 < ms2 {
True -> Lt
False ->
case ms1 > ms2 {
True -> Gt
False -> Eq
}
}
}
/// Check if first time is before second time.
pub fn before(time1: Time, time2: Time) -> Bool {
case compare(time1, time2) {
Lt -> True
_ -> False
}
}
/// Check if first time is after second time.
pub fn after(time1: Time, time2: Time) -> Bool {
case compare(time1, time2) {
Gt -> True
_ -> False
}
}
/// Convert time between calendars.
pub fn convert(time: Time, target_calendar: String) -> Result(Time, TimeError) {
case time.calendar == target_calendar {
True -> Ok(time)
False -> {
// Simplified conversion - would need proper calendar implementation
case
new(
time.hour,
time.minute,
time.second,
time.microsecond,
target_calendar,
)
{
Ok(t) -> Ok(t)
Error(e) -> Error(e)
}
}
}
}
/// Convert time between calendars, panicking on error.
pub fn convert_unchecked(time: Time, target_calendar: String) -> Time {
case convert(time, target_calendar) {
Ok(converted) -> converted
Error(_) -> panic as "Calendar conversion failed"
}
}
/// Calculate difference between two times.
pub fn diff(time1: Time, time2: Time, unit: TimeUnit) -> Int {
let ms1 = to_microseconds_after_midnight(time1)
let ms2 = to_microseconds_after_midnight(time2)
let diff_microseconds = ms1 - ms2
case unit {
Microsecond | Native -> diff_microseconds
Millisecond -> diff_microseconds / 1000
Second -> diff_microseconds / 1_000_000
}
}
/// Shift time by a duration.
pub fn shift(
time: Time,
duration: calendar_duration.Duration,
) -> Result(Time, TimeError) {
let total_seconds =
duration.hour * 3600 + duration.minute * 60 + duration.second
let #(duration_ms, _) = duration.microsecond
let total_microseconds = total_seconds * 1_000_000 + duration_ms
add(time, total_microseconds, Microsecond)
}
/// Truncate time precision.
pub fn truncate(time: Time, precision: Int) -> Time {
let #(ms, _) = time.microsecond
let truncated_ms = case precision {
0 -> #(0, 0)
p if p >= 6 -> #(ms, 6)
p -> {
let divisor = case p {
1 -> 100_000
2 -> 10_000
3 -> 1000
4 -> 100
5 -> 10
_ -> 1
}
#(ms / divisor * divisor, p)
}
}
case new(time.hour, time.minute, time.second, truncated_ms, time.calendar) {
Ok(t) -> t
Error(_) -> time
// Return original time if truncation fails
}
}
/// Get string representation for inspection.
pub fn inspect(time: Time) -> String {
"~T[" <> to_string(time) <> "]"
}
/// Check if a time is valid.
pub fn is_valid(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
) -> Bool {
is_valid_time(hour, minute, second, microsecond)
}
/// Get the maximum precision for a time unit.
pub fn max_precision(unit: TimeUnit) -> Int {
case unit {
Second -> 0
Millisecond -> 3
Microsecond | Native -> 6
}
}
/// Normalize microseconds to a specific precision.
pub fn normalize_precision(
microsecond: #(Int, Int),
target_precision: Int,
) -> #(Int, Int) {
let #(ms, current_precision) = microsecond
case target_precision >= current_precision {
True -> #(
ms * int_pow(10, target_precision - current_precision),
target_precision,
)
False -> #(
ms / int_pow(10, current_precision - target_precision),
target_precision,
)
}
}
/// Convert between time units.
pub fn convert_unit(amount: Int, from_unit: TimeUnit, to_unit: TimeUnit) -> Int {
let microseconds = case from_unit {
Second -> amount * 1_000_000
Millisecond -> amount * 1000
Microsecond | Native -> amount
}
case to_unit {
Second -> microseconds / 1_000_000
Millisecond -> microseconds / 1000
Microsecond | Native -> microseconds
}
}
/// Get the time at midnight (00:00:00).
pub fn midnight() -> Result(Time, TimeError) {
new(0, 0, 0, #(0, 0), "Calendar.ISO")
}
/// Get the time at noon (12:00:00).
pub fn noon() -> Result(Time, TimeError) {
new(12, 0, 0, #(0, 0), "Calendar.ISO")
}
/// Check if a time is at midnight.
pub fn is_midnight(time: Time) -> Bool {
time.hour == 0
&& time.minute == 0
&& time.second == 0
&& time.microsecond == #(0, 0)
}
/// Get the second of the day (0-86399).
pub fn second_of_day(time: Time) -> Int {
time.hour * 3600 + time.minute * 60 + time.second
}
// Helper functions
fn parse_iso8601_time(
time_string: String,
) -> Result(#(Int, Int, Int, #(Int, Int)), Nil) {
// Simplified parsing - would need full ISO8601 implementation
case string.split(time_string, ":") {
[hour_str, minute_str, second_part] -> {
case int.parse(hour_str), int.parse(minute_str) {
Ok(hour), Ok(minute) -> {
case string.split(second_part, ".") {
[second_str] -> {
case int.parse(second_str) {
Ok(second) -> Ok(#(hour, minute, second, #(0, 0)))
Error(_) -> Error(Nil)
}
}
[second_str, ms_str] -> {
case int.parse(second_str), int.parse(ms_str) {
Ok(second), Ok(ms) -> {
let precision = string.length(ms_str)
let normalized_ms = normalize_microseconds(ms, precision)
Ok(#(hour, minute, second, #(normalized_ms, precision)))
}
_, _ -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
_, _ -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
fn normalize_microseconds(ms: Int, precision: Int) -> Int {
case precision {
p if p >= 6 -> ms
p -> ms * int_pow(10, 6 - p)
}
}
fn int_pow(base: Int, exp: Int) -> Int {
case exp {
0 -> 1
n if n > 0 -> base * int_pow(base, n - 1)
_ -> 1
}
}
/// Helper function to pad single digits with leading zero.
fn pad_zero(n: Int) -> String {
case n < 10 {
True -> "0" <> int.to_string(n)
False -> int.to_string(n)
}
}
/// Helper function to format microseconds with proper padding.
fn pad_microseconds(microseconds: Int, precision: Int) -> String {
let ms_str = int.to_string(microseconds)
// Pad with leading zeros to ensure we have at least precision digits
let padded = case string.length(ms_str) < precision {
True -> string.repeat("0", precision - string.length(ms_str)) <> ms_str
False -> ms_str
}
// Take only the first precision digits
string.slice(padded, 0, precision)
}