Current section
Files
Jump to
Current section
Files
src/calendar/naive_datetime.gleam
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
// SPDX-FileCopyrightText: 2012 Plataformatec
import calendar/date
import calendar/duration as calendar_duration
import calendar/iso
import calendar/time
import gleam/int
import gleam/list
import gleam/order
import gleam/string
/// A NaiveDateTime struct (without a time zone) and functions.
///
/// The NaiveDateTime struct contains the fields year, month, day, hour,
/// minute, second, microsecond and calendar. New naive datetimes can be
/// built with the `new` functions.
///
/// We call them "naive" because this datetime representation does not
/// have a time zone. This means the datetime may not actually exist in
/// certain areas in the world even though it is valid.
pub type NaiveDateTime {
NaiveDateTime(
year: Int,
month: Int,
day: Int,
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
)
}
pub type NaiveDateTimeError {
InvalidNaiveDateTime
InvalidDate
InvalidTime
}
const seconds_per_day = 86_400
/// Creates a new NaiveDateTime struct.
pub fn new(
year: Int,
month: Int,
day: Int,
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
// Validate date component
case date.new(year, month, day, calendar) {
Error(_) -> Error(InvalidDate)
Ok(_) -> {
// Validate time component
case time.new(hour, minute, second, microsecond, calendar) {
Error(_) -> Error(InvalidTime)
Ok(_) ->
Ok(NaiveDateTime(
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: calendar,
))
}
}
}
}
/// Creates a new NaiveDateTime with simple parameters.
pub fn new_simple(
year: Int,
month: Int,
day: Int,
hour: Int,
minute: Int,
second: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
new(year, month, day, hour, minute, second, #(0, 0), "Calendar.ISO")
}
/// Creates a NaiveDateTime from separate Date and Time structs.
pub fn from_date_and_time(
date_val: date.Date,
time_val: time.Time,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
case date_val.calendar == time_val.calendar {
True ->
Ok(NaiveDateTime(
year: date_val.year,
month: date_val.month,
day: date_val.day,
hour: time_val.hour,
minute: time_val.minute,
second: time_val.second,
microsecond: time_val.microsecond,
calendar: date_val.calendar,
))
False -> Error(InvalidNaiveDateTime)
}
}
/// Converts a NaiveDateTime to a string in ISO8601 format.
pub fn to_string(ndt: NaiveDateTime) -> String {
let date_part =
pad_left(int.to_string(ndt.year), 4, "0")
<> "-"
<> pad_left(int.to_string(ndt.month), 2, "0")
<> "-"
<> pad_left(int.to_string(ndt.day), 2, "0")
let time_part =
pad_left(int.to_string(ndt.hour), 2, "0")
<> ":"
<> pad_left(int.to_string(ndt.minute), 2, "0")
<> ":"
<> pad_left(int.to_string(ndt.second), 2, "0")
let #(ms, precision) = ndt.microsecond
let microsecond_part = case ms == 0 || precision == 0 {
True -> ""
False -> "." <> pad_microseconds(ms, precision)
}
date_part <> " " <> time_part <> microsecond_part
}
/// Converts a NaiveDateTime to ISO8601 extended format.
pub fn to_iso8601(ndt: NaiveDateTime) -> String {
to_iso8601_with_format(ndt, iso.Extended)
}
/// Converts a NaiveDateTime to ISO8601 with format option.
pub fn to_iso8601_with_format(ndt: NaiveDateTime, format: iso.Format) -> String {
let #(ms, precision) = ndt.microsecond
let microsecond_part = case ms == 0 || precision == 0 {
True -> ""
False -> "." <> pad_microseconds(ms, precision)
}
case format {
iso.Extended -> {
let date_part =
pad_left(int.to_string(ndt.year), 4, "0")
<> "-"
<> pad_left(int.to_string(ndt.month), 2, "0")
<> "-"
<> pad_left(int.to_string(ndt.day), 2, "0")
let time_part =
pad_left(int.to_string(ndt.hour), 2, "0")
<> ":"
<> pad_left(int.to_string(ndt.minute), 2, "0")
<> ":"
<> pad_left(int.to_string(ndt.second), 2, "0")
date_part <> "T" <> time_part <> microsecond_part
}
iso.Basic -> {
let date_part =
pad_left(int.to_string(ndt.year), 4, "0")
<> pad_left(int.to_string(ndt.month), 2, "0")
<> pad_left(int.to_string(ndt.day), 2, "0")
let time_part =
pad_left(int.to_string(ndt.hour), 2, "0")
<> pad_left(int.to_string(ndt.minute), 2, "0")
<> pad_left(int.to_string(ndt.second), 2, "0")
date_part <> "T" <> time_part <> microsecond_part
}
}
}
/// Extracts the Date part from a NaiveDateTime.
pub fn to_date(ndt: NaiveDateTime) -> date.Date {
case date.new(ndt.year, ndt.month, ndt.day, ndt.calendar) {
Ok(d) -> d
Error(_) -> panic as "Invalid date in valid NaiveDateTime struct"
}
}
/// Extracts the Time part from a NaiveDateTime.
pub fn to_time(ndt: NaiveDateTime) -> time.Time {
case
time.new(ndt.hour, ndt.minute, ndt.second, ndt.microsecond, ndt.calendar)
{
Ok(t) -> t
Error(_) -> panic as "Invalid time in valid NaiveDateTime struct"
}
}
/// Compare two NaiveDateTime structs.
pub fn compare(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> order.Order {
let ts1 = to_timestamp(ndt1)
let ts2 = to_timestamp(ndt2)
int.compare(ts1, ts2)
}
/// Add seconds to a NaiveDateTime.
pub fn add_seconds(
ndt: NaiveDateTime,
seconds: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_timestamp = to_timestamp(ndt)
from_timestamp(current_timestamp + seconds)
}
/// Convert NaiveDateTime to Unix timestamp (seconds since epoch).
pub fn to_timestamp(ndt: NaiveDateTime) -> Int {
let date_part = to_date(ndt)
let date_timestamp = date.to_timestamp(date_part)
let time_seconds = ndt.hour * 3600 + ndt.minute * 60 + ndt.second
date_timestamp + time_seconds
}
/// Create NaiveDateTime from Unix timestamp.
pub fn from_timestamp(
timestamp: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let days_since_epoch = timestamp / seconds_per_day
let seconds_in_day = timestamp % seconds_per_day
case date.from_timestamp(days_since_epoch * seconds_per_day) {
Error(_) -> Error(InvalidDate)
Ok(date_val) -> {
case time.from_seconds_after_midnight(seconds_in_day) {
Error(_) -> Error(InvalidTime)
Ok(time_val) -> from_date_and_time(date_val, time_val)
}
}
}
}
/// Helper function to pad string with leading characters.
fn pad_left(str: String, width: Int, pad_char: String) -> String {
let current_length = string.length(str)
case current_length >= width {
True -> str
False -> string.repeat(pad_char, width - current_length) <> str
}
}
/// Returns the current naive datetime in UTC.
pub fn utc_now() -> Result(NaiveDateTime, NaiveDateTimeError) {
utc_now_with_precision(time.Microsecond, "Calendar.ISO")
}
/// Returns the current naive datetime in UTC with precision.
pub fn utc_now_with_precision(
precision: time.TimeUnit,
calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_date = date.utc_today_with_calendar(calendar)
case time.utc_now_with_precision(precision, calendar) {
Ok(current_time) -> {
new(
current_date.year,
current_date.month,
current_date.day,
current_time.hour,
current_time.minute,
current_time.second,
current_time.microsecond,
calendar,
)
}
Error(_) -> Error(InvalidTime)
}
}
/// Returns the current local naive datetime.
pub fn local_now() -> Result(NaiveDateTime, NaiveDateTimeError) {
local_now_with_calendar("Calendar.ISO")
}
/// Returns the current local naive datetime with calendar.
pub fn local_now_with_calendar(
calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
// For simplicity, returns UTC time - real implementation would use local timezone
utc_now_with_precision(time.Microsecond, calendar)
}
/// Add days to a naive datetime.
pub fn add_days(
ndt: NaiveDateTime,
days: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_timestamp = to_timestamp(ndt)
from_timestamp(current_timestamp + days * 86_400)
}
/// Add hours to a naive datetime.
pub fn add_hours(
ndt: NaiveDateTime,
hours: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_timestamp = to_timestamp(ndt)
from_timestamp(current_timestamp + hours * 3600)
}
/// Add minutes to a naive datetime.
pub fn add_minutes(
ndt: NaiveDateTime,
minutes: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_timestamp = to_timestamp(ndt)
from_timestamp(current_timestamp + minutes * 60)
}
/// Calculate difference in days between two naive datetimes.
pub fn diff_days(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Int {
diff(ndt1, ndt2, time.Second) / 86_400
}
/// Calculate difference in hours between two naive datetimes.
pub fn diff_hours(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Int {
diff(ndt1, ndt2, time.Second) / 3600
}
/// Calculate difference in minutes between two naive datetimes.
pub fn diff_minutes(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Int {
diff(ndt1, ndt2, time.Second) / 60
}
/// Add amount to a naive datetime with specified unit.
pub fn add(
ndt: NaiveDateTime,
amount: Int,
unit: time.TimeUnit,
) -> NaiveDateTime {
case add_with_validation(ndt, amount, unit) {
Ok(new_ndt) -> new_ndt
Error(_) -> ndt
// Return original if invalid
}
}
/// Add amount to naive datetime with validation.
pub fn add_with_validation(
ndt: NaiveDateTime,
amount: Int,
unit: time.TimeUnit,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let microseconds_to_add = case unit {
time.Second -> amount * 1_000_000
time.Millisecond -> amount * 1000
time.Microsecond -> amount
time.Native -> amount
}
let current_timestamp = to_timestamp(ndt) * 1_000_000
let #(ms, _precision) = ndt.microsecond
let total_us = current_timestamp + ms + microseconds_to_add
let total_seconds = total_us / 1_000_000
let remaining_us = total_us % 1_000_000
let precision = case unit {
time.Second -> 0
time.Millisecond -> 3
time.Microsecond | time.Native -> 6
}
let #(_, old_precision) = ndt.microsecond
let final_precision = case precision > old_precision {
True -> precision
False -> old_precision
}
let days_since_epoch = total_seconds / seconds_per_day
let seconds_in_day = total_seconds % seconds_per_day
case date.from_days_since_unix_epoch(days_since_epoch, ndt.calendar) {
Error(_) -> Error(InvalidDate)
Ok(date_val) -> {
case
time.from_seconds_after_midnight_with_microsecond(
seconds_in_day,
#(remaining_us, final_precision),
ndt.calendar,
)
{
Error(_) -> Error(InvalidTime)
Ok(time_val) -> from_date_and_time(date_val, time_val)
}
}
}
}
/// Calculate difference between two naive datetimes.
pub fn diff(
ndt1: NaiveDateTime,
ndt2: NaiveDateTime,
unit: time.TimeUnit,
) -> Int {
let ts1 = to_timestamp(ndt1)
let ts2 = to_timestamp(ndt2)
let diff_seconds = ts1 - ts2
case unit {
time.Second -> diff_seconds
time.Microsecond -> diff_seconds * 1_000_000
time.Millisecond -> diff_seconds * 1000
_ -> diff_seconds
}
}
/// Shift naive datetime by duration.
pub fn shift(
ndt: NaiveDateTime,
duration: calendar_duration.Duration,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let month_shift = duration.year * 12 + duration.month
let day_shift = duration.week * 7 + duration.day
let #(us, _) = duration.microsecond
let #(y, m, d, h, min, s, microsecond) =
iso.shift_naive_datetime(
ndt.year,
ndt.month,
ndt.day,
ndt.hour,
ndt.minute,
ndt.second,
ndt.microsecond,
month_shift,
day_shift,
duration.hour * 3600 + duration.minute * 60 + duration.second,
us,
)
new(y, m, d, h, min, s, microsecond, ndt.calendar)
}
/// Truncate naive datetime to specified precision.
pub fn truncate(
ndt: NaiveDateTime,
precision: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let current_time = to_time(ndt)
let truncated_time = time.truncate(current_time, precision)
new(
ndt.year,
ndt.month,
ndt.day,
truncated_time.hour,
truncated_time.minute,
truncated_time.second,
truncated_time.microsecond,
ndt.calendar,
)
}
/// Check if first datetime is before second.
pub fn before(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Bool {
case compare(ndt1, ndt2) {
order.Lt -> True
_ -> False
}
}
/// Check if first datetime is after second.
pub fn after(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Bool {
case compare(ndt1, ndt2) {
order.Gt -> True
_ -> False
}
}
/// Parse ISO8601 string to NaiveDateTime.
pub fn from_iso8601(
iso_string: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
case parse_iso8601_naive_datetime(iso_string) {
Ok(#(year, month, day, hour, minute, second, microsecond)) ->
new(year, month, day, hour, minute, second, microsecond, "Calendar.ISO")
Error(_) -> Error(InvalidNaiveDateTime)
}
}
/// Parse ISO8601 string to NaiveDateTime, panicking on error.
pub fn from_iso8601_unchecked(iso_string: String) -> NaiveDateTime {
case from_iso8601(iso_string) {
Ok(ndt) -> ndt
Error(_) -> panic as "Invalid ISO8601 naive datetime format"
}
}
/// Convert NaiveDateTime to Erlang datetime tuple.
pub fn to_erl(ndt: NaiveDateTime) -> #(#(Int, Int, Int), #(Int, Int, Int)) {
#(#(ndt.year, ndt.month, ndt.day), #(ndt.hour, ndt.minute, ndt.second))
}
/// Create NaiveDateTime from Erlang datetime tuple.
pub fn from_erl(
erl_datetime: #(#(Int, Int, Int), #(Int, Int, Int)),
microsecond: #(Int, Int),
calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let #(#(year, month, day), #(hour, minute, second)) = erl_datetime
new(year, month, day, hour, minute, second, microsecond, calendar)
}
/// Create NaiveDateTime from Erlang tuple, panicking on error.
pub fn from_erl_unchecked(
erl_datetime: #(#(Int, Int, Int), #(Int, Int, Int)),
microsecond: #(Int, Int),
calendar: String,
) -> NaiveDateTime {
case from_erl(erl_datetime, microsecond, calendar) {
Ok(ndt) -> ndt
Error(_) -> panic as "Invalid Erlang datetime tuple"
}
}
/// Convert between calendars.
pub fn convert(
ndt: NaiveDateTime,
target_calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
case ndt.calendar == target_calendar {
True -> Ok(ndt)
False -> {
// Simplified conversion - would need proper calendar implementation
case
new(
ndt.year,
ndt.month,
ndt.day,
ndt.hour,
ndt.minute,
ndt.second,
ndt.microsecond,
target_calendar,
)
{
Ok(result) -> Ok(result)
Error(e) -> Error(e)
}
}
}
}
/// Convert between calendars, panicking on error.
pub fn convert_unchecked(
ndt: NaiveDateTime,
target_calendar: String,
) -> NaiveDateTime {
case convert(ndt, target_calendar) {
Ok(converted) -> converted
Error(_) -> panic as "Calendar conversion failed"
}
}
/// Get beginning of day (00:00:00).
pub fn beginning_of_day(ndt: NaiveDateTime) -> NaiveDateTime {
case new(ndt.year, ndt.month, ndt.day, 0, 0, 0, #(0, 0), ndt.calendar) {
Ok(result) -> result
Error(_) -> panic as "Invalid beginning of day"
}
}
/// Get end of day (23:59:59.999999).
pub fn end_of_day(ndt: NaiveDateTime) -> NaiveDateTime {
case
new(ndt.year, ndt.month, ndt.day, 23, 59, 59, #(999_999, 6), ndt.calendar)
{
Ok(result) -> result
Error(_) -> panic as "Invalid end of day"
}
}
/// Get string representation for inspection.
pub fn inspect(ndt: NaiveDateTime) -> String {
"~N[" <> to_string(ndt) <> "]"
}
/// Create from Gregorian seconds since epoch 0000-01-01 00:00:00.
pub fn from_gregorian_seconds(
seconds: Int,
_microsecond: #(Int, Int),
_calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
// Simplified - would need proper Gregorian calendar implementation
from_timestamp(seconds - 62_167_219_200)
// Approximate offset to Unix epoch
}
/// Convert to Gregorian seconds since epoch 0000-01-01 00:00:00.
pub fn to_gregorian_seconds(ndt: NaiveDateTime) -> #(Int, Int) {
let timestamp = to_timestamp(ndt)
let gregorian_seconds = timestamp + 62_167_219_200
// Approximate offset from Unix epoch
let #(ms, _) = ndt.microsecond
#(gregorian_seconds, ms)
}
/// Create NaiveDateTime with current date and specified time.
pub fn today_with_time(
hour: Int,
minute: Int,
second: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
today_with_time_and_calendar(hour, minute, second, #(0, 0), "Calendar.ISO")
}
/// Create NaiveDateTime with current date and specified time and calendar.
pub fn today_with_time_and_calendar(
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
calendar: String,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
let today = date.utc_today_with_calendar(calendar)
new(
today.year,
today.month,
today.day,
hour,
minute,
second,
microsecond,
calendar,
)
}
/// Check if two naive datetimes are equal.
pub fn equal(ndt1: NaiveDateTime, ndt2: NaiveDateTime) -> Bool {
case compare(ndt1, ndt2) {
order.Eq -> True
_ -> False
}
}
/// Get the day of year (1-366).
pub fn day_of_year(ndt: NaiveDateTime) -> Int {
iso.day_of_year(ndt.year, ndt.month, ndt.day)
}
/// Get the day of week (1=Monday, 7=Sunday).
pub fn day_of_week(ndt: NaiveDateTime) -> Int {
iso.day_of_week(ndt.year, ndt.month, ndt.day, iso.Monday)
}
/// Get the week of year (1-53).
pub fn week_of_year(ndt: NaiveDateTime) -> Int {
day_of_year(ndt) / 7 + 1
}
/// Get year from naive datetime.
pub fn year(ndt: NaiveDateTime) -> Int {
ndt.year
}
/// Get month from naive datetime.
pub fn month(ndt: NaiveDateTime) -> Int {
ndt.month
}
/// Get day from naive datetime.
pub fn day(ndt: NaiveDateTime) -> Int {
ndt.day
}
/// Get hour from naive datetime.
pub fn hour(ndt: NaiveDateTime) -> Int {
ndt.hour
}
/// Get minute from naive datetime.
pub fn minute(ndt: NaiveDateTime) -> Int {
ndt.minute
}
/// Get second from naive datetime.
pub fn second(ndt: NaiveDateTime) -> Int {
ndt.second
}
/// Get microsecond from naive datetime.
pub fn microsecond(ndt: NaiveDateTime) -> #(Int, Int) {
ndt.microsecond
}
/// Get calendar from naive datetime.
pub fn calendar(ndt: NaiveDateTime) -> String {
ndt.calendar
}
/// Validate if components make a valid naive datetime.
pub fn is_valid(
year: Int,
month: Int,
day: Int,
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
) -> Bool {
case
new(year, month, day, hour, minute, second, microsecond, "Calendar.ISO")
{
Ok(_) -> True
Error(_) -> False
}
}
/// Create naive datetime range.
pub fn range(
start: NaiveDateTime,
end: NaiveDateTime,
step_seconds: Int,
) -> List(NaiveDateTime) {
case step_seconds <= 0 {
True -> []
False -> {
let start_ts = to_timestamp(start)
let end_ts = to_timestamp(end)
case start_ts >= end_ts {
True -> []
False -> range_helper(start_ts, end_ts, step_seconds, [])
}
}
}
}
/// Create a new naive datetime with specified components replaced.
pub fn replace(
ndt: NaiveDateTime,
year: Int,
month: Int,
day: Int,
hour: Int,
minute: Int,
second: Int,
microsecond: #(Int, Int),
) -> Result(NaiveDateTime, NaiveDateTimeError) {
new(year, month, day, hour, minute, second, microsecond, ndt.calendar)
}
/// Create a new naive datetime with only some components replaced.
pub fn replace_partial(
ndt: NaiveDateTime,
year: Int,
month: Int,
day: Int,
) -> Result(NaiveDateTime, NaiveDateTimeError) {
new(
year,
month,
day,
ndt.hour,
ndt.minute,
ndt.second,
ndt.microsecond,
ndt.calendar,
)
}
// Helper functions
fn range_helper(
current_ts: Int,
end_ts: Int,
step: Int,
acc: List(NaiveDateTime),
) -> List(NaiveDateTime) {
case current_ts >= end_ts {
True -> acc |> list.reverse
False -> {
case from_timestamp(current_ts) {
Ok(ndt) -> range_helper(current_ts + step, end_ts, step, [ndt, ..acc])
Error(_) -> acc |> list.reverse
}
}
}
}
// Helper functions
/// Parse ISO8601 naive datetime string.
fn parse_iso8601_naive_datetime(
iso_string: String,
) -> Result(#(Int, Int, Int, Int, Int, Int, #(Int, Int)), Nil) {
// Simplified parsing - would need full ISO8601 implementation
case string.split(iso_string, "T") {
[date_part, time_part] -> {
case string.split(date_part, "-"), string.split(time_part, ":") {
[year_str, month_str, day_str], [hour_str, minute_str, second_part] -> {
case
int.parse(year_str),
int.parse(month_str),
int.parse(day_str),
int.parse(hour_str),
int.parse(minute_str)
{
Ok(year), Ok(month), Ok(day), Ok(hour), Ok(minute) -> {
case string.split(second_part, ".") {
[second_str] -> {
case int.parse(second_str) {
Ok(second) ->
Ok(#(year, month, day, 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)
Ok(
#(year, month, day, hour, minute, second, #(
ms,
precision,
)),
)
}
_, _ -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
_, _, _, _, _ -> Error(Nil)
}
}
_, _ -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
/// Helper function to format microseconds with proper padding.
fn pad_microseconds(microseconds: Int, precision: Int) -> String {
let ms_str = int.to_string(microseconds)
let padded = case string.length(ms_str) < precision {
True -> string.repeat("0", precision - string.length(ms_str)) <> ms_str
False -> ms_str
}
string.slice(padded, 0, precision)
}