From 586ba69cc920d17f1acf6cb39941186af68cf24c Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 17 Dec 2024 22:22:41 +0100 Subject: [PATCH] Split DateTime into Date and Time types The DateTime type is now an inline type built on top of two newly introduced types: std.time.Date and std.time.Time. The Date type stores the date components without a timezone, while the Time type stores the time components, also without a timezone. The UTC offset in turn is stored in the DateTime type. In addition, DateTime.local replaces the old DateTime.new method and DateTime.new is now used to construct a DateTime from a Date, Time and UTC offset. DateTime is _not_ a `copy` type as we may need to store additional non-copy data in the future, such as more detailed timezone related objects. This fixes https://github.com/inko-lang/inko/issues/309. Changelog: changed --- std/src/std/fs.inko | 20 +- std/src/std/time.inko | 559 ++++++++++++++++++++++++--------- std/test/std/fs/test_path.inko | 6 +- std/test/std/test_time.inko | 299 ++++++++++++------ 4 files changed, 638 insertions(+), 246 deletions(-) diff --git a/std/src/std/fs.inko b/std/src/std/fs.inko index 5aee015a..d1566ba6 100644 --- a/std/src/std/fs.inko +++ b/std/src/std/fs.inko @@ -107,7 +107,7 @@ class copy Time { fn to_date_time -> DateTime { let time = @secs.to_float + (@nanos.to_float / 1_000_000_000.0) - DateTime.from_timestamp(time, utc_offset: 0) + DateTime.from_timestamp(time, utc_offset: 0).get } } @@ -147,6 +147,12 @@ class pub inline Metadata { # 1970-01-01 00:00:00), an `Option[DateTime]` is returned. If the creation # time is available, the value is a `Option.Some(DateTime)`, otherwise it's a # `None`. + # + # # Panics + # + # This method may panic if the time can't be expressed in a `DateTime`. This + # can only happen if the system clock is returning nonsensical values (e.g. a + # year outside of the 32-bits signed integer range). fn pub created_at -> Option[DateTime] { match @created_at { case Some(v) -> Option.Some(v.to_date_time) @@ -157,6 +163,12 @@ class pub inline Metadata { # Returns the time at which the file was last modified. # # This corresponds to the "mtime" field of `stat()` on Unix systems. + # + # # Panics + # + # This method may panic if the time can't be expressed in a `DateTime`. This + # can only happen if the system clock is returning nonsensical values (e.g. a + # year outside of the 32-bits signed integer range). fn pub modified_at -> DateTime { @modified_at.to_date_time } @@ -164,6 +176,12 @@ class pub inline Metadata { # Returns the time at which the file was last accessed. # # This corresponds to the `atime` field of `stat()` on Unix systems. + # + # # Panics + # + # This method may panic if the time can't be expressed in a `DateTime`. This + # can only happen if the system clock is returning nonsensical values (e.g. a + # year outside of the 32-bits signed integer range). fn pub accessed_at -> DateTime { @accessed_at.to_date_time } diff --git a/std/src/std/time.inko b/std/src/std/time.inko index a1ad1a6f..82f31cba 100644 --- a/std/src/std/time.inko +++ b/std/src/std/time.inko @@ -225,7 +225,269 @@ impl Format for Duration { } } -# An object representing the current system time. +# A type representing a date in the Gregorian calendar, without a time and +# timezone. +class pub copy Date { + # The year, month and day packed into a single set of bits. + # + # The layout is as follows (starting with the least significant bits): + # + # 1. 5 bits for the day + # 1. 4 bits for the month + # 1. Up to 32 bits for the year + # + # For example, the date 1992-08-21 is encoded as follows: + # + # 11111001000 1000 10101 + let @bits: Int + + # Returns a new `Date`, provided the given components are valid. + # + # The arguments must be values in the following ranges: + # + # - `year`: -2147483648 up to and including 2147483647 + # - `month`: 1 up to and including 12 + # - `day`: 1 up to and including 31 + # + # If any of the arguments are out of bounds, an `Option.None` is returned. + # + # # Examples + # + # ```inko + # import std.time (Date) + # + # let date = Date + # .new(year: 2024, month: 12, day: 17) + # .or_panic('the Date is invalid') + # + # date.year # => 2024 + # ``` + fn pub inline static new(year: Int, month: Int, day: Int) -> Option[Date] { + if + year >= -2147483648 + and year <= 2147483647 + and month >= 1 + and month <= 12 + and day >= 1 + and day <= 31 + { + Option.Some(new_unchecked(year, month, day)) + } else { + Option.None + } + } + + fn inline static new_unchecked(year: Int, month: Int, day: Int) -> Date { + Date((year << 9) | (month << 5) | day) + } + + # Returns the year. + fn pub inline year -> Int { + @bits >> 9 + } + + # Returns the month in the range `1` to `12`. + fn pub inline month -> Int { + @bits >> 5 & 0xF + } + + # Returns the day in the range `1` to `31`. + fn pub inline day -> Int { + @bits & 0x1F + } + + # Returns the number of days between `self` and the Unix epoch. + # + # The returned `Int` is negative if `self` is before the Unix epoch, and + # positive for a value that is on or after the Unix epoch. + fn pub days_since_unix_epoch -> Int { + let year = if month <= 2 { year - 1 } else { year } + let month = month + let era = if year >= 0 { year } else { year - 399 } / 400 + let yoe = year - (era * 400) + let doy = (((153 * if month > 2 { month - 3 } else { month + 9 }) + 2) / 5) + + day + - 1 + let doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy + + (era * 146_097) + doe - 719_468 + } + + # Returns the day of the week from 1 to 7. + # + # Per ISO 8601 the first day of the week starts on Monday, not Sunday. + fn pub day_of_week -> Int { + # January 1st, 1970 (our anchor date) was on a Thursday. We add 3 so that + # Monday (3 days before Thursday) becomes the anchor date. + # + # We later on add 1 since the % operator will return 0 for Monday (since its + # the first value in the range), but week days range from 1 to 7; not 0 to + # 6. + # + # The following table should help illustrate this: + # + # | Date | Day of week | days_since_unix_epoch + # |:-----------|:------------|:---------------------- + # | 1969-12-29 | Monday | -3 + # | 1969-12-30 | Tuesday | -2 + # | 1969-12-31 | Wednesday | -1 + # | 1970-01-01 | Thursday | 0 + # | 1970-01-02 | Friday | 1 + # | 1970-01-03 | Saturday | 2 + # | 1970-01-04 | Sunday | 3 + # + # For these dates, the calculations would be as follows: + # + # | Date | Calculation | Simplified | Return value + # |:-----------|:-------------------|:------------|:------------ + # | 1969-12-29 | ((-3 + 3) % 7) + 1 | (0 % 7) + 1 | 1 + # | 1969-12-30 | ((-2 + 3) % 7) + 1 | (1 % 7) + 1 | 2 + # | 1969-12-31 | ((-1 + 3) % 7) + 1 | (2 % 7) + 1 | 3 + # | 1970-01-01 | ((0 + 3) % 7) + 1 | (3 % 7) + 1 | 4 + # | 1970-01-02 | ((1 + 3) % 7) + 1 | (4 % 7) + 1 | 5 + # | 1970-01-03 | ((2 + 3) % 7) + 1 | (5 % 7) + 1 | 6 + # | 1970-01-04 | ((3 + 3) % 7) + 1 | (6 % 7) + 1 | 7 + days_since_unix_epoch + 3 % DAYS_PER_WEEK + 1 + } + + # Returns the day of the year from 1 to 366 for leap years, and from 1 to 365 + # for regular years. + fn pub day_of_year -> Int { + let days = if leap_year? { LEAP_DAYS } else { NORMAL_DAYS } + + days.get(month - 1) + day + } + + # Returns `true` if the current year is a leap year. + fn pub leap_year? -> Bool { + let year = year + + (year % 4) == 0 and ((year % 100) > 0 or (year % 400) == 0) + } +} + +impl Equal[Date] for Date { + fn pub ==(other: Date) -> Bool { + @bits == other.bits + } +} + +impl Format for Date { + fn pub fmt(formatter: mut Formatter) { + let year = year.to_string.pad_start(with: '0', chars: 4) + let month = month.to_string.pad_start(with: '0', chars: 2) + let day = day.to_string.pad_start(with: '0', chars: 2) + + formatter.write('${year}-${month}-${day}') + } +} + +# A type that represents a point in time (hours, minutes and seconds) without a +# specific associated date. +class pub copy Time { + # The hour, minute and second packed into a single set of bits. + # + # The layout is as follows (starting with the least significant bits): + # + # 1. 6 bits for the seconds + # 1. 6 bits for the minutes + # 1. 5 bits for the hour + # + # For example, the time 12:45:30 is encoded as follows: + # + # 1100 101101 011110 + let @bits: Int + let @sub_second: Float + + # Returns a new `Time`, provided the given components are valid. + # + # The arguments must be values in the following ranges: + # + # - `hour`: 0 up to and including 24 + # - `minute`: 0 up to and including 59 + # - `second`: 0 up to and including 60 + # + # If any of the arguments are out of bounds, an `Option.None` is returned. + # + # # Examples + # + # ```inko + # import std.time (Time) + # + # let time = Time + # .new(hour: 12, minute: 30, second: 0, sub_second: 0.0) + # .or_panic('the Time is invalid') + # + # time.hour # => 12 + # ``` + fn pub inline static new( + hour: Int, + minute: Int, + second: Int, + sub_second: Float, + ) -> Option[Time] { + if + hour >= 0 + and hour <= 24 + and minute >= 0 + and minute <= 59 + and second >= 0 + and second <= 60 + and sub_second >= 0.0 + { + Option.Some(new_unchecked(hour, minute, second, sub_second)) + } else { + Option.None + } + } + + fn inline static new_unchecked( + hour: Int, + minute: Int, + second: Int, + sub_second: Float, + ) -> Time { + Time(bits: (hour << 12) | (minute << 6) | second, sub_second: sub_second) + } + + # Returns the hour of the day in the range `0` to `23`. + fn pub inline hour -> Int { + @bits >> 12 + } + + # Returns the minute of the hour in the range `0` to `59`. + fn pub inline minute -> Int { + @bits >> 6 & 0x3F + } + + # Returns the number of seconds in the range `0` to `59`. + fn pub inline second -> Int { + @bits & 0x3F + } + + # Returns the number of sub seconds. + fn pub inline sub_second -> Float { + @sub_second + } +} + +impl Equal[Time] for Time { + fn pub ==(other: Time) -> Bool { + @bits == other.bits and @sub_second == other.sub_second + } +} + +impl Format for Time { + fn pub fmt(formatter: mut Formatter) { + let hour = hour.to_string.pad_start(with: '0', chars: 2) + let min = minute.to_string.pad_start(with: '0', chars: 2) + let secs = second.to_string.pad_start(with: '0', chars: 2) + + formatter.write('${hour}:${min}:${secs}') + } +} + +# A date and time based on the Gregorian calendar. # # Internally the time is represented as the number of seconds since the Unix # epoch, excluding leap seconds. @@ -236,58 +498,53 @@ impl Format for Duration { # If you want to measure the duration between two events, it's best to use the # `Instant` type, as it's not affected by external factors such as clock # adjustments and leap seconds. -class pub DateTime { - # The year. - let pub @year: Int - - # The month, from `1` to `12`. - let pub @month: Int - - # The day, from `1` to `31`. - let pub @day: Int - - # The hour, from `0` to `23`. - let pub @hour: Int - - # The minute, from `0` to `59`. - let pub @minute: Int +class pub inline DateTime { + let @date: Date + let @time: Time + let @utc_offset: Int - # The second, from `0` to `59`. - let pub @second: Int - - # The sub seconds. - let pub @sub_second: Float - - # The UTC offset (in seconds). - let pub @utc_offset: Int + # Returns a new `DateTime` using the given components. + # + # # Examples + # + # ```inko + # import std.time (Date, DateTime, Time) + # + # let date = Date.new(year: 1992, month: 8, day: 21).get + # let time = Time.new(hour: 12, minute: 15, second: 30, sub_second: 0.0).get + # + # DateTime.new(date, time, utc_offset: 3600) + # ``` + fn pub inline static new( + date: Date, + time: Time, + utc_offset: Int, + ) -> DateTime { + DateTime(date: date, time: time, utc_offset: utc_offset) + } - # Returns a new `DateTime` representing the current time using the local - # timezone. + # Returns a new `DateTime` representing the current time using the system's + # local time. # # # Examples # # ```inko # import std.time (DateTime) # - # DateTime.new + # DateTime.local # ``` - fn pub static new -> DateTime { + fn pub static local -> DateTime { let raw = sys.local_time DateTime( - year: raw.year, - month: raw.month, - day: raw.day, - hour: raw.hour, - minute: raw.minute, - second: raw.second, - sub_second: raw.sub_second, + date: Date.new_unchecked(raw.year, raw.month, raw.day), + time: Time.new_unchecked(raw.hour, raw.minute, raw.second, raw.sub_second), utc_offset: raw.offset, ) } - # Returns a new `DateTime` representing the current time using UTC as the - # timezone. + # Returns a new `DateTime` representing the current system time using UTC as + # the timezone. # # # Examples # @@ -300,13 +557,8 @@ class pub DateTime { let raw = sys.utc_time DateTime( - year: raw.year, - month: raw.month, - day: raw.day, - hour: raw.hour, - minute: raw.minute, - second: raw.second, - sub_second: raw.sub_second, + date: Date.new_unchecked(raw.year, raw.month, raw.day), + time: Time.new_unchecked(raw.hour, raw.minute, raw.second, raw.sub_second), utc_offset: 0, ) } @@ -315,6 +567,8 @@ class pub DateTime { # # The `time` argument is the number of seconds since or before the Unix epoch. # + # If the timestamp is invalid, an `Option.None` is returned. + # # # Example # # ```inko @@ -325,7 +579,7 @@ class pub DateTime { fn pub static from_timestamp[T: ToFloat]( time: ref T, utc_offset: Int, - ) -> DateTime { + ) -> Option[DateTime] { # This implementation is based on the algorithms as described on # http://howardhinnant.github.io/date_algorithms.html, specifically the # `civil_from_days()` algorithm. @@ -358,120 +612,132 @@ class pub DateTime { let minute = (day_secs % SECS_PER_HOUR) / 60 let hour = day_secs / SECS_PER_HOUR - DateTime( - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - sub_second: time.fractional, - utc_offset: utc_offset, + Option.Some( + DateTime( + date: try Date.new(year, month, day), + time: try Time.new(hour, minute, second, time.fractional), + utc_offset: utc_offset, + ), ) } - # Returns the day of the week from 1 to 7. + # Returns the `Date` component of `self`. + fn pub inline date -> Date { + @date + } + + # Returns the `Time` component of `self`. + fn pub inline time -> Time { + @time + } + + # Returns the UTC offset in seconds. + fn pub inline utc_offset -> Int { + @utc_offset + } + + # Returns `true` if UTC is used as the timezone. + fn pub inline utc? -> Bool { + @utc_offset == 0 + } + + # Returns the year. # - # Per ISO 8601 the first day of the week starts on Monday, not Sunday. - fn pub day_of_week -> Int { - # January 1st, 1970 (our anchor date) was on a Thursday. We add 3 so that - # Monday (3 days before Thursday) becomes the anchor date. - # - # We later on add 1 since the % operator will return 0 for Monday (since its - # the first value in the range), but week days range from 1 to 7; not 0 to - # 6. - # - # The following table should help illustrate this: - # - # | Date | Day of week | days_since_unix_epoch - # |:-----------|:------------|:---------------------- - # | 1969-12-29 | Monday | -3 - # | 1969-12-30 | Tuesday | -2 - # | 1969-12-31 | Wednesday | -1 - # | 1970-01-01 | Thursday | 0 - # | 1970-01-02 | Friday | 1 - # | 1970-01-03 | Saturday | 2 - # | 1970-01-04 | Sunday | 3 - # - # For these dates, the calculations would be as follows: - # - # | Date | Calculation | Simplified | Return value - # |:-----------|:-------------------|:------------|:------------ - # | 1969-12-29 | ((-3 + 3) % 7) + 1 | (0 % 7) + 1 | 1 - # | 1969-12-30 | ((-2 + 3) % 7) + 1 | (1 % 7) + 1 | 2 - # | 1969-12-31 | ((-1 + 3) % 7) + 1 | (2 % 7) + 1 | 3 - # | 1970-01-01 | ((0 + 3) % 7) + 1 | (3 % 7) + 1 | 4 - # | 1970-01-02 | ((1 + 3) % 7) + 1 | (4 % 7) + 1 | 5 - # | 1970-01-03 | ((2 + 3) % 7) + 1 | (5 % 7) + 1 | 6 - # | 1970-01-04 | ((3 + 3) % 7) + 1 | (6 % 7) + 1 | 7 - days_since_unix_epoch + 3 % DAYS_PER_WEEK + 1 + # Refer to the documentation of `Date.year` for more details. + fn pub inline year -> Int { + @date.year } - # Returns the day of the year from 1 to 366 for leap years, and from 1 to 365 - # for regular years. - fn pub day_of_year -> Int { - let days = if leap_year? { LEAP_DAYS } else { NORMAL_DAYS } + # Returns the month. + # + # Refer to the documentation of `Date.month` for more details. + fn pub inline month -> Int { + @date.month + } + + # Returns the day. + # + # Refer to the documentation of `Date.day` for more details. + fn pub inline day -> Int { + @date.day + } + + # Returns the hour of the day. + # + # Refer to the documentation of `Time.hour` for more details. + fn pub inline hour -> Int { + @time.hour + } + + # Returns the minute of the hour. + # + # Refer to the documentation of `Time.minute` for more details. + fn pub inline minute -> Int { + @time.minute + } - days.get(@month - 1) + @day + # Returns the number of seconds. + # + # Refer to the documentation of `Time.second` for more details. + fn pub inline second -> Int { + @time.second + } + + # Returns the number of sub seconds. + # + # Refer to the documentation of `Time.sub_second` for more details. + fn pub inline sub_second -> Float { + @time.sub_second } # Returns the number of days between `self` and the Unix epoch. # - # The returned `Int` is negative if `self` is before the Unix epoch, and - # positive for a value that is on or after the Unix epoch. + # Refer to the documentation of `Date.days_since_unix_epoch` for more details. fn pub days_since_unix_epoch -> Int { - let year = if @month <= 2 { @year - 1 } else { @year } - let month = @month - let era = if year >= 0 { year } else { year - 399 } / 400 - let yoe = year - (era * 400) - let doy = (((153 * if month > 2 { month - 3 } else { month + 9 }) + 2) / 5) - + @day - - 1 - let doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy - - (era * 146_097) + doe - 719_468 + @date.days_since_unix_epoch } - # Returns `true` if the current year is a leap year. - fn pub leap_year? -> Bool { - let year = @year + # Returns the day of the week from 1 to 7. + # + # Refer to the documentation of `Date.day_of_week` for more details. + fn pub day_of_week -> Int { + @date.day_of_week + } - (year % 4) == 0 and ((year % 100) > 0 or (year % 400) == 0) + # Returns the day of the year. + # + # Refer to the documentation of `Date.day_of_year` for more details. + fn pub day_of_year -> Int { + @date.day_of_year } - # Returns `true` if UTC is used. - fn pub utc? -> Bool { - @utc_offset == 0 + # Returns `true` if the current year is a leap year. + # + # Refer to the documentation of `Date.leap_year?` for more details. + fn pub leap_year? -> Bool { + @date.leap_year? } # Converts the `DateTime` to another `DateTime` that uses UTC as the # timezone. fn pub to_utc -> DateTime { - DateTime.from_timestamp(time: to_float, utc_offset: 0) + # Since our input is already valid at this point, this can never fail. + DateTime.from_timestamp(time: to_float, utc_offset: 0).get } } impl Clone[DateTime] for DateTime { fn pub inline clone -> DateTime { - DateTime( - year: @year, - month: @month, - day: @day, - hour: @hour, - minute: @minute, - second: @second, - sub_second: @sub_second, - utc_offset: @utc_offset, - ) + DateTime(date: @date, time: @time, utc_offset: @utc_offset) } } impl Format for DateTime { fn pub fmt(formatter: mut Formatter) { - let sign = if @utc_offset > 0 { '+' } else { '-' } let offset = if @utc_offset == 0 { ' UTC' } else { + let sign = if @utc_offset > 0 { '+' } else { '-' } let off = @utc_offset.absolute let hh = (off / SECS_PER_HOUR).to_string.pad_start(with: '0', chars: 2) let mm = (off % SECS_PER_HOUR / SECS_PER_MIN).to_string.pad_start( @@ -482,14 +748,10 @@ impl Format for DateTime { ' ${sign}${hh}${mm}' } - let year = @year.to_string.pad_start(with: '0', chars: 4) - let month = @month.to_string.pad_start(with: '0', chars: 2) - let day = @day.to_string.pad_start(with: '0', chars: 2) - let hour = @hour.to_string.pad_start(with: '0', chars: 2) - let min = @minute.to_string.pad_start(with: '0', chars: 2) - let secs = @second.to_string.pad_start(with: '0', chars: 2) - - formatter.write('${year}-${month}-${day} ${hour}:${min}:${secs}${offset}') + @date.fmt(formatter) + formatter.write(' ') + @time.fmt(formatter) + formatter.write(offset) } } @@ -501,7 +763,9 @@ impl ToInt for DateTime { fn pub to_int -> Int { let days = days_since_unix_epoch let days_sec = days.absolute * SECS_PER_DAY - let time_sec = (@hour * SECS_PER_HOUR) + (@minute * SECS_PER_MIN) + @second + let time_sec = (@time.hour * SECS_PER_HOUR) + + (@time.minute * SECS_PER_MIN) + + @time.second let timestamp = if days < 0 { 0 - (days_sec - time_sec) } else { @@ -517,23 +781,37 @@ impl ToFloat for DateTime { # Returns the timestamp since the Unix epoch, the including fractional # seconds. fn pub to_float -> Float { - to_int.to_float + @sub_second + to_int.to_float + @time.sub_second } } impl Add[Duration, DateTime] for DateTime { + # Adds the given `Duration` to `self`, returning the result as a new + # `DateTime`. + # + # # Panics + # + # This method may panic if the result can't be expressed as a `DateTime` (e.g. + # the year is too great). fn pub +(other: ref Duration) -> DateTime { let timestamp = to_float + other.to_secs - DateTime.from_timestamp(timestamp, utc_offset: @utc_offset) + DateTime.from_timestamp(timestamp, utc_offset: @utc_offset).get } } impl Subtract[Duration, DateTime] for DateTime { + # Subtracts the given `Duration` from `self`, returning the result as a new + # `DateTime`. + # + # # Panics + # + # This method may panic if the result can't be expressed as a `DateTime` (e.g. + # the year is too great). fn pub -(other: ref Duration) -> DateTime { let timestamp = to_float - other.to_secs - DateTime.from_timestamp(timestamp, utc_offset: @utc_offset) + DateTime.from_timestamp(timestamp, utc_offset: @utc_offset).get } } @@ -545,13 +823,8 @@ impl Compare[DateTime] for DateTime { impl Equal[ref DateTime] for DateTime { fn pub inline ==(other: ref DateTime) -> Bool { - @year == other.year - and @month == other.month - and @day == other.day - and @hour == other.hour - and @minute == other.minute - and @second == other.second - and @sub_second == other.sub_second + @date == other.date + and @time == other.time and @utc_offset == other.utc_offset } } diff --git a/std/test/std/fs/test_path.inko b/std/test/std/fs/test_path.inko index 86cac56d..1331a8c5 100644 --- a/std/test/std/fs/test_path.inko +++ b/std/test/std/fs/test_path.inko @@ -12,9 +12,9 @@ import std.time (DateTime, Duration) fn valid_date?(time: Result[DateTime, Error]) -> Bool { match time { case Ok(t) -> { - 1970.to(2100).contains?(t.year) - and 1.to(12).contains?(t.month) - and 1.to(31).contains?(t.day) + 1970.to(2100).contains?(t.date.year) + and 1.to(12).contains?(t.date.month) + and 1.to(31).contains?(t.date.day) } case _ -> false } diff --git a/std/test/std/test_time.inko b/std/test/std/test_time.inko index a9c55a4e..cb144dc5 100644 --- a/std/test/std/test_time.inko +++ b/std/test/std/test_time.inko @@ -2,17 +2,12 @@ import std.cmp (Ordering) import std.fmt (fmt) import std.process (sleep) import std.test (Tests) -import std.time (DateTime, Duration, Instant) +import std.time (Date, DateTime, Duration, Instant, Time) fn ymd(year: Int, month: Int, day: Int) -> DateTime { DateTime( - year: year, - month: month, - day: day, - hour: 12, - minute: 0, - second: 0, - sub_second: 0.0, + date: Date.new(year, month, day).get, + time: Time.new(hour: 12, minute: 0, second: 0, sub_second: 0.0).get, utc_offset: 0, ) } @@ -124,126 +119,232 @@ fn pub tests(t: mut Tests) { t.true(rem.to_secs <= 5.0) }) - t.test('DateTime.new', fn (t) { - let time = DateTime.new + t.ok('Date.new with a valid date', fn (t) { + let valid1 = try Date.new(2024, 12, 31).ok_or(nil) + let valid2 = try Date.new(0, 1, 1).ok_or(nil) - t.true(time.year > 0) - t.true(time.month > 0) - t.true(time.day > 0) + t.equal(valid1.year, 2024) + t.equal(valid1.month, 12) + t.equal(valid1.day, 31) + + t.equal(valid2.year, 0) + t.equal(valid2.month, 1) + t.equal(valid2.day, 1) + + Result.Ok(nil) + }) + + t.test('Date.new with an invalid date', fn (t) { + t.true(Date.new(-2147483649, 1, 1).none?) + t.true(Date.new(2147483648, 1, 1).none?) + t.true(Date.new(2024, 0, 1).none?) + t.true(Date.new(2024, 13, 1).none?) + t.true(Date.new(2024, 1, 0).none?) + t.true(Date.new(2024, 1, 32).none?) + }) + + t.test('Date.==', fn (t) { + t.equal(Date.new_unchecked(2024, 1, 1), Date.new_unchecked(2024, 1, 1)) + t.not_equal(Date.new_unchecked(2024, 1, 1), Date.new_unchecked(2024, 1, 2)) + }) + + t.test('Date.fmt', fn (t) { + t.equal(fmt(Date.new_unchecked(1, 1, 1)), '0001-01-01') + t.equal(fmt(Date.new_unchecked(2024, 12, 17)), '2024-12-17') + }) + + t.test('Date.days_since_unix_epoch', fn (t) { + let d1 = Date.new_unchecked(2022, 8, 26) + let d2 = Date.new_unchecked(1970, 1, 1) + let d3 = Date.new_unchecked(1969, 12, 31) + let d4 = Date.new_unchecked(2024, 2, 3) + + t.equal(d1.days_since_unix_epoch, 19_230) + t.equal(d2.days_since_unix_epoch, 0) + t.equal(d3.days_since_unix_epoch, -1) + t.equal(d4.days_since_unix_epoch, 19_756) + }) + + t.test('Date.day_of_week', fn (t) { + let d1 = Date.new_unchecked(2022, 8, 26) + let d2 = Date.new_unchecked(1970, 1, 1) + let d3 = Date.new_unchecked(1969, 12, 31) + let d4 = Date.new_unchecked(1960, 4, 5) + let d5 = Date.new_unchecked(1969, 12, 27) + + t.equal(d1.day_of_week, 5) + t.equal(d2.day_of_week, 4) + t.equal(d3.day_of_week, 3) + t.equal(d4.day_of_week, 2) + t.equal(d5.day_of_week, 6) + }) + + t.test('Date.day_of_year', fn (t) { + let d1 = Date.new_unchecked(2022, 8, 26) + let d2 = Date.new_unchecked(1970, 1, 1) + let d3 = Date.new_unchecked(1969, 12, 31) + let d4 = Date.new_unchecked(2016, 2, 3) + + t.equal(d1.day_of_year, 238) + t.equal(d2.day_of_year, 1) + t.equal(d3.day_of_year, 365) + t.equal(d4.day_of_year, 34) + }) + + t.test('Date.leap_year?', fn (t) { + let d1 = Date.new_unchecked(2016, 1, 1) + let d2 = Date.new_unchecked(2017, 1, 1) + + t.true(d1.leap_year?) + t.false(d2.leap_year?) + }) + + t.ok('Time.new with a valid time', fn (t) { + let time = try Time + .new(hour: 12, minute: 15, second: 30, sub_second: 1.2) + .ok_or(nil) + + t.equal(time.hour, 12) + t.equal(time.minute, 15) + t.equal(time.second, 30) + t.equal(time.sub_second, 1.2) + Result.Ok(nil) + }) + + t.test('Time.new with an invalid time', fn (t) { + t.true(Time.new(hour: 25, minute: 1, second: 1, sub_second: 0.0).none?) + t.true(Time.new(hour: 1, minute: -1, second: 1, sub_second: 0.0).none?) + t.true(Time.new(hour: 1, minute: 1, second: -1, sub_second: 0.0).none?) + t.true(Time.new(hour: 1, minute: 1, second: 1, sub_second: -1.0).none?) + t.true(Time.new(hour: 1, minute: 60, second: 1, sub_second: 0.0).none?) + t.true(Time.new(hour: 1, minute: 0, second: 61, sub_second: 0.0).none?) + }) + + t.test('Time.==', fn (t) { + t.equal( + Time.new_unchecked(hour: 12, minute: 15, second: 10, sub_second: 0.0), + Time.new_unchecked(hour: 12, minute: 15, second: 10, sub_second: 0.0), + ) + t.not_equal( + Time.new_unchecked(hour: 12, minute: 15, second: 10, sub_second: 0.0), + Time.new_unchecked(hour: 12, minute: 15, second: 10, sub_second: 1.0), + ) + }) + + t.test('Time.fmt', fn (t) { + t.equal( + fmt(Time.new_unchecked(hour: 12, minute: 15, second: 15, sub_second: 0.0)), + '12:15:15', + ) + t.equal( + fmt(Time.new_unchecked(hour: 1, minute: 1, second: 1, sub_second: 0.0)), + '01:01:01', + ) + }) + + t.test('DateTime.local', fn (t) { + let time = DateTime.local + + t.true(time.date.year > 0) + t.true(time.date.month > 0) + t.true(time.date.day > 0) }) t.test('DateTime.utc', fn (t) { t.equal(DateTime.utc.utc_offset, 0) }) t.test('DateTime.from_timestamp', fn (t) { - let t1 = DateTime.from_timestamp(time: 0.0, utc_offset: 0) - let t2 = DateTime.from_timestamp(time: 0.0, utc_offset: 3_600) - let t3 = DateTime.from_timestamp(time: 1661538868.123, utc_offset: 7200) - let t4 = DateTime.from_timestamp(time: -3600, utc_offset: 0) - - t.equal(t1.year, 1970) - t.equal(t1.month, 1) - t.equal(t1.day, 1) - t.equal(t1.hour, 0) - t.equal(t1.minute, 0) - t.equal(t1.second, 0) - t.equal(t1.sub_second, 0.0) + let t1 = DateTime.from_timestamp(time: 0.0, utc_offset: 0).get + let t2 = DateTime.from_timestamp(time: 0.0, utc_offset: 3_600).get + let t3 = DateTime.from_timestamp(time: 1661538868.123, utc_offset: 7200).get + let t4 = DateTime.from_timestamp(time: -3600, utc_offset: 0).get + + t.equal(t1.date.year, 1970) + t.equal(t1.date.month, 1) + t.equal(t1.date.day, 1) + t.equal(t1.time.hour, 0) + t.equal(t1.time.minute, 0) + t.equal(t1.time.second, 0) + t.equal(t1.time.sub_second, 0.0) t.equal(t1.utc_offset, 0) - t.equal(t2.year, 1970) - t.equal(t2.month, 1) - t.equal(t2.day, 1) - t.equal(t2.hour, 1) - t.equal(t2.minute, 0) - t.equal(t2.second, 0) - t.equal(t2.sub_second, 0.0) + t.equal(t2.date.year, 1970) + t.equal(t2.date.month, 1) + t.equal(t2.date.day, 1) + t.equal(t2.time.hour, 1) + t.equal(t2.time.minute, 0) + t.equal(t2.time.second, 0) + t.equal(t2.time.sub_second, 0.0) t.equal(t2.utc_offset, 3_600) - t.equal(t3.year, 2022) - t.equal(t3.month, 8) - t.equal(t3.day, 26) - t.equal(t3.hour, 20) - t.equal(t3.minute, 34) - t.equal(t3.second, 28) - t.true(t3.sub_second >= 0.12 and t3.sub_second <= 0.123) + t.equal(t3.date.year, 2022) + t.equal(t3.date.month, 8) + t.equal(t3.date.day, 26) + t.equal(t3.time.hour, 20) + t.equal(t3.time.minute, 34) + t.equal(t3.time.second, 28) + t.true(t3.time.sub_second >= 0.12 and t3.time.sub_second <= 0.123) t.equal(t3.utc_offset, 7200) - t.equal(t4.year, 1969) - t.equal(t4.month, 12) - t.equal(t4.day, 31) - t.equal(t4.hour, 23) - t.equal(t4.minute, 0) - t.equal(t4.second, 0) + t.equal(t4.date.year, 1969) + t.equal(t4.date.month, 12) + t.equal(t4.date.day, 31) + t.equal(t4.time.hour, 23) + t.equal(t4.time.minute, 0) + t.equal(t4.time.second, 0) }) - t.test('DateTime.day_of_week', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0) - let t4 = DateTime.from_timestamp(time: -307411200, utc_offset: 0) - let t5 = DateTime.from_timestamp(time: -432000, utc_offset: 0) + t.test('DateTime.days_since_unix_epoch', fn (t) { + let dt = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get - t.equal(t1.day_of_week, 5) - t.equal(t2.day_of_week, 4) - t.equal(t3.day_of_week, 3) - t.equal(t4.day_of_week, 2) - t.equal(t5.day_of_week, 6) + t.equal(dt.days_since_unix_epoch, 19_230) }) - t.test('DateTime.day_of_year', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0) - let t4 = DateTime.from_timestamp(time: 1454457600, utc_offset: 0) + t.test('DateTime.day_of_week', fn (t) { + let dt = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get - t.equal(t1.day_of_year, 238) - t.equal(t2.day_of_year, 1) - t.equal(t3.day_of_year, 365) - t.equal(t4.day_of_year, 34) + t.equal(dt.day_of_week, 5) }) - t.test('DateTime.days_since_unix_epoch', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0) - let t4 = ymd(2024, 2, 3) + t.test('DateTime.day_of_year', fn (t) { + let dt = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get - t.equal(t1.days_since_unix_epoch, 19_230) - t.equal(t2.days_since_unix_epoch, 0) - t.equal(t3.days_since_unix_epoch, -1) - t.equal(t4.days_since_unix_epoch, 19_756) + t.equal(dt.day_of_year, 238) }) t.test('DateTime.leap_year?', fn (t) { - let t1 = DateTime.from_timestamp(time: 1451606400, utc_offset: 0) # 2016 - let t2 = DateTime.from_timestamp(time: 1483228800, utc_offset: 0) # 2017 + let t1 = DateTime.from_timestamp(time: 1451606400, utc_offset: 0).get # 2016 + let t2 = DateTime.from_timestamp(time: 1483228800, utc_offset: 0).get # 2017 t.true(t1.leap_year?) t.false(t2.leap_year?) }) t.test('DateTime.utc?', fn (t) { - let t1 = DateTime.from_timestamp(time: 1, utc_offset: 3600) - let t2 = DateTime.from_timestamp(time: 1, utc_offset: 0) + let t1 = DateTime.from_timestamp(time: 1, utc_offset: 3600).get + let t2 = DateTime.from_timestamp(time: 1, utc_offset: 0).get t.false(t1.utc?) t.true(t2.utc?) }) t.test('DateTime.to_utc', fn (t) { - let t1 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t2 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t3 = DateTime.from_timestamp(time: 0, utc_offset: 7200) + let t1 = DateTime.from_timestamp(time: 0, utc_offset: 0).get + let t2 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get + let t3 = DateTime.from_timestamp(time: 0, utc_offset: 7200).get t.equal(t1.to_utc, t1) - t.equal(t2.to_utc, DateTime.from_timestamp(time: 1661538868, utc_offset: 0)) - t.equal(t3.to_utc, DateTime.from_timestamp(time: 0, utc_offset: 0)) + t.equal( + t2.to_utc, + DateTime.from_timestamp(time: 1661538868, utc_offset: 0).get, + ) + t.equal(t3.to_utc, DateTime.from_timestamp(time: 0, utc_offset: 0).get) }) t.test('DateTime.fmt', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0) - let t4 = DateTime.from_timestamp(time: 1661538868, utc_offset: -7200) + let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0).get + let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0).get + let t4 = DateTime.from_timestamp(time: 1661538868, utc_offset: -7200).get t.equal(fmt(t1), '2022-08-26 20:34:28 +0200') t.equal(fmt(t2), '1970-01-01 00:00:00 UTC') @@ -252,9 +353,9 @@ fn pub tests(t: mut Tests) { }) t.test('DateTime.to_int', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0) + let t1 = DateTime.from_timestamp(time: 1661538868, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 0, utc_offset: 0).get + let t3 = DateTime.from_timestamp(time: -3600, utc_offset: 0).get let t4 = ymd(2024, 2, 3) t.equal(t1.to_int, 1661538868) @@ -264,9 +365,9 @@ fn pub tests(t: mut Tests) { }) t.test('DateTime.to_float', fn (t) { - let t1 = DateTime.from_timestamp(time: 1661538868.123, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0.123, utc_offset: 0) - let t3 = DateTime.from_timestamp(time: -3600.123, utc_offset: 0) + let t1 = DateTime.from_timestamp(time: 1661538868.123, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 0.123, utc_offset: 0).get + let t3 = DateTime.from_timestamp(time: -3600.123, utc_offset: 0).get t.true(t1.to_float.fractional >= 0.0) t.true(t2.to_float.fractional >= 0.0) @@ -274,22 +375,22 @@ fn pub tests(t: mut Tests) { }) t.test('DateTime.+', fn (t) { - let t1 = DateTime.from_timestamp(time: 0, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 3600, utc_offset: 7200) + let t1 = DateTime.from_timestamp(time: 0, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 3600, utc_offset: 7200).get t.equal(t1 + Duration.from_secs(3600), t2) }) t.test('DateTime.-', fn (t) { - let t1 = DateTime.from_timestamp(time: 3600, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0, utc_offset: 7200) + let t1 = DateTime.from_timestamp(time: 3600, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 0, utc_offset: 7200).get t.equal(t1 - Duration.from_secs(3600), t2) }) t.test('DateTime.cmp', fn (t) { - let t1 = DateTime.from_timestamp(time: 3600.123, utc_offset: 7200) - let t2 = DateTime.from_timestamp(time: 0.123, utc_offset: 7200) + let t1 = DateTime.from_timestamp(time: 3600.123, utc_offset: 7200).get + let t2 = DateTime.from_timestamp(time: 0.123, utc_offset: 7200).get t.equal(t1.cmp(t2), Ordering.Greater) t.equal(t2.cmp(t1), Ordering.Less)