diff --git a/core/time/datetime/constants.odin b/core/time/datetime/constants.odin index a2a02838c..5f336ef4a 100644 --- a/core/time/datetime/constants.odin +++ b/core/time/datetime/constants.odin @@ -1,16 +1,46 @@ package datetime -// Ordinal 1 = Midnight Monday, January 1, 1 A.D. (Gregorian) -// | Midnight Monday, January 3, 1 A.D. (Julian) +/* +Type representing a mononotic day number corresponding to a date. + + Ordinal 1 = Midnight Monday, January 1, 1 A.D. (Gregorian) + | Midnight Monday, January 3, 1 A.D. (Julian) +*/ Ordinal :: i64 + +/* +*/ EPOCH :: Ordinal(1) -// Minimum and maximum dates and ordinals. Chosen for safe roundtripping. +/* +Minimum valid value for date. + +The value is chosen such that a conversion `date -> ordinal -> date` is always +safe. +*/ MIN_DATE :: Date{year = -25_252_734_927_766_552, month = 1, day = 1} + +/* +Maximum valid value for date + +The value is chosen such that a conversion `date -> ordinal -> date` is always +safe. +*/ MAX_DATE :: Date{year = 25_252_734_927_766_552, month = 12, day = 31} + +/* +Minimum value for an ordinal +*/ MIN_ORD :: Ordinal(-9_223_372_036_854_775_234) + +/* +Maximum value for an ordinal +*/ MAX_ORD :: Ordinal( 9_223_372_036_854_774_869) +/* +Possible errors returned by datetime functions. +*/ Error :: enum { None, Invalid_Year, @@ -24,12 +54,22 @@ Error :: enum { Invalid_Delta, } +/* +A type representing a date. + +The minimum and maximum values for a year can be found in `MIN_DATE` and +`MAX_DATE` constants. The `month` field can range from 1 to 12, and the day +ranges from 1 to however many days there are in the specified month. +*/ Date :: struct { year: i64, month: i8, day: i8, } +/* +A type representing a time within a single day within a nanosecond precision. +*/ Time :: struct { hour: i8, minute: i8, @@ -37,17 +77,30 @@ Time :: struct { nano: i32, } +/* +A type representing datetime. +*/ DateTime :: struct { using date: Date, using time: Time, } +/* +A type representing a difference between two instances of datetime. + +**Note**: All fields are i64 because we can also use it to add a number of +seconds or nanos to a moment, that are then normalized within their respective +ranges. +*/ Delta :: struct { - days: i64, // These are all i64 because we can also use it to add a number of seconds or nanos to a moment, - seconds: i64, // that are then normalized within their respective ranges. + days: i64, + seconds: i64, nanos: i64, } +/* +Type representing one of the months. +*/ Month :: enum i8 { January = 1, February, @@ -63,6 +116,9 @@ Month :: enum i8 { December, } +/* +Type representing one of the weekdays. +*/ Weekday :: enum i8 { Sunday = 0, Monday, diff --git a/core/time/datetime/datetime.odin b/core/time/datetime/datetime.odin index 938b4a368..fc9780e3b 100644 --- a/core/time/datetime/datetime.odin +++ b/core/time/datetime/datetime.odin @@ -1,56 +1,113 @@ /* - Calendrical conversions using a proleptic Gregorian calendar. +Calendrical conversions using a proleptic Gregorian calendar. - Implemented using formulas from: Calendrical Calculations Ultimate Edition, Reingold & Dershowitz +Implemented using formulas from: Calendrical Calculations Ultimate Edition, +Reingold & Dershowitz */ package datetime import "base:intrinsics" -// Procedures that return an Ordinal +/* +Obtain an ordinal from a date. +This procedure converts the specified date into an ordinal. If the specified +date is not a valid date, an error is returned. +*/ date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal, err: Error) { validate(date) or_return return unsafe_date_to_ordinal(date), .None } +/* +Obtain an ordinal from date components. + +This procedure converts the specified date, provided by its individual +components, into an ordinal. If the specified date is not a valid date, an error +is returned. +*/ components_to_ordinal :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (ordinal: Ordinal, err: Error) { validate(year, month, day) or_return return unsafe_date_to_ordinal({year, i8(month), i8(day)}), .None } -// Procedures that return a Date +/* +Obtain date using an Ordinal. +This provedure converts the specified ordinal into a date. If the ordinal is not +a valid ordinal, an error is returned. +*/ ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date, err: Error) { validate(ordinal) or_return return unsafe_ordinal_to_date(ordinal), .None } +/* +Obtain a date from date components. + +This procedure converts date components, specified by a year, a month and a day, +into a date object. If the provided date components don't represent a valid +date, an error is returned. +*/ components_to_date :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (date: Date, err: Error) { validate(year, month, day) or_return return Date{i64(year), i8(month), i8(day)}, .None } +/* +Obtain time from time components. + +This procedure converts time components, specified by an hour, a minute, a second +and nanoseconds, into a time object. If the provided time components don't +represent a valid time, an error is returned. +*/ components_to_time :: proc "contextless" (#any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (time: Time, err: Error) { validate(hour, minute, second, nanos) or_return return Time{i8(hour), i8(minute), i8(second), i32(nanos)}, .None } +/* +Obtain datetime from components. + +This procedure converts date components and time components into a datetime object. +If the provided date components or time components don't represent a valid +datetime, an error is returned. +*/ components_to_datetime :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (datetime: DateTime, err: Error) { date := components_to_date(year, month, day) or_return time := components_to_time(hour, minute, second, nanos) or_return return {date, time}, .None } +/* +Obtain an datetime from an ordinal. + +This procedure converts the value of an ordinal into a datetime. Since the +ordinal only has the amount of days, the resulting time in the datetime +object will always have the time equal to `00:00:00.000`. +*/ ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) { d := ordinal_to_date(ordinal) or_return return {Date(d), {}}, .None } +/* +Calculate the weekday from an ordinal. + +This procedure takes the value of an ordinal and returns the day of week for +that ordinal. +*/ day_of_week :: proc "contextless" (ordinal: Ordinal) -> (day: Weekday) { return Weekday((ordinal - EPOCH + 1) %% 7) } +/* +Calculate the difference between two dates. + +This procedure calculates the difference between two dates `a - b`, and returns +a delta between the two dates in `days`. If either `a` or `b` is not a valid +date, an error is returned. +*/ subtract_dates :: proc "contextless" (a, b: Date) -> (delta: Delta, err: Error) { ord_a := date_to_ordinal(a) or_return ord_b := date_to_ordinal(b) or_return @@ -59,6 +116,16 @@ subtract_dates :: proc "contextless" (a, b: Date) -> (delta: Delta, err: Error) return } +/* +Calculate the difference between two datetimes. + +This procedure calculates the difference between two datetimes, `a - b`, and +returns a delta between the two dates. The difference is returned in all three +fields of the `Delta` struct: the difference in days, the difference in seconds +and the difference in nanoseconds. + +If either `a` or `b` is not a valid datetime, an error is returned. +*/ subtract_datetimes :: proc "contextless" (a, b: DateTime) -> (delta: Delta, err: Error) { ord_a := date_to_ordinal(a) or_return ord_b := date_to_ordinal(b) or_return @@ -73,19 +140,42 @@ subtract_datetimes :: proc "contextless" (a, b: DateTime) -> (delta: Delta, err: return } +/* +Calculate a difference between two deltas. +*/ subtract_deltas :: proc "contextless" (a, b: Delta) -> (delta: Delta, err: Error) { delta = Delta{a.days - b.days, a.seconds - b.seconds, a.nanos - b.nanos} delta = normalize_delta(delta) or_return return } + +/* +Calculate a difference between two datetimes, dates or deltas. +*/ sub :: proc{subtract_datetimes, subtract_dates, subtract_deltas} +/* +Add certain amount of days to a date. + +This procedure adds the specified amount of days to a date and returns a new +date. The new date would have happened the specified amount of days after the +specified date. +*/ add_days_to_date :: proc "contextless" (a: Date, days: i64) -> (date: Date, err: Error) { ord := date_to_ordinal(a) or_return ord += days return ordinal_to_date(ord) } +/* +Add delta to a date. + +This procedure adds a delta to a date, and returns a new date. The new date +would have happened the time specified by `delta` after the specified date. + +**Note**: The delta is assumed to be normalized. That is, if it contains seconds +or milliseconds, regardless of the amount only the days will be added. +*/ add_delta_to_date :: proc "contextless" (a: Date, delta: Delta) -> (date: Date, err: Error) { ord := date_to_ordinal(a) or_return // Because the input is a Date, we add only the days from the Delta. @@ -93,6 +183,13 @@ add_delta_to_date :: proc "contextless" (a: Date, delta: Delta) -> (date: Date, return ordinal_to_date(ord) } +/* +Add delta to datetime. + +This procedure adds a delta to a datetime, and returns a new datetime. The new +datetime would have happened the time specified by `delta` after the specified +datetime. +*/ add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (datetime: DateTime, err: Error) { days := date_to_ordinal(a) or_return @@ -110,8 +207,18 @@ add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (date datetime.time = components_to_time(hour, minute, second, sum_delta.nanos) or_return return } + +/* +Add days to a date, delta to a date or delta to datetime. +*/ add :: proc{add_days_to_date, add_delta_to_date, add_delta_to_datetime} +/* +Obtain the day number in a year + +This procedure returns the number of the day in a year, starting from 1. If +the date is not a valid date, an error is returned. +*/ day_number :: proc "contextless" (date: Date) -> (day_number: i64, err: Error) { validate(date) or_return @@ -120,6 +227,13 @@ day_number :: proc "contextless" (date: Date) -> (day_number: i64, err: Error) { return } +/* +Obtain the remaining number of days in a year. + +This procedure returns the number of days between the specified date and +December 31 of the same year. If the date is not a valid date, an error is +returned. +*/ days_remaining :: proc "contextless" (date: Date) -> (days_remaining: i64, err: Error) { // Alternative formulation `day_number` subtracted from 365 or 366 depending on leap year validate(date) or_return @@ -127,6 +241,12 @@ days_remaining :: proc "contextless" (date: Date) -> (days_remaining: i64, err: return delta.days, .None } +/* +Obtain the last day of a given month on a given year. + +This procedure returns the amount of days in a specified month on a specified +date. If the specified year or month is not valid, an error is returned. +*/ last_day_of_month :: proc "contextless" (#any_int year: i64, #any_int month: i8) -> (day: i8, err: Error) { // Not using formula 2.27 from the book. This is far simpler and gives the same answer. @@ -140,16 +260,33 @@ last_day_of_month :: proc "contextless" (#any_int year: i64, #any_int month: i8) return } +/* +Obtain the new year date of a given year. + +This procedure returns the January 1st date of the specified year. If the year +is not valid, an error is returned. +*/ new_year :: proc "contextless" (#any_int year: i64) -> (new_year: Date, err: Error) { validate(year, 1, 1) or_return return {year, 1, 1}, .None } +/* +Obtain the end year of a given date. + +This procedure returns the December 31st date of the specified year. If the year +is not valid, an error is returned. +*/ year_end :: proc "contextless" (#any_int year: i64) -> (year_end: Date, err: Error) { validate(year, 12, 31) or_return return {year, 12, 31}, .None } +/* +Obtain the range of dates for a given year. + +This procedure returns dates, for every day of a given year in a slice. +*/ year_range :: proc (#any_int year: i64, allocator := context.allocator) -> (range: []Date) { is_leap := is_leap_year(year) @@ -171,6 +308,15 @@ year_range :: proc (#any_int year: i64, allocator := context.allocator) -> (rang return } +/* +Normalize the delta. + +This procedure normalizes the delta in such a way that the number of seconds +is between 0 and the number of seconds in the day and nanoseconds is between +0 and 10^9. + +If the value for `days` overflows during this operation, an error is returned. +*/ normalize_delta :: proc "contextless" (delta: Delta) -> (normalized: Delta, err: Error) { // Distribute nanos into seconds and remainder seconds, nanos := divmod(delta.nanos, 1e9) @@ -194,6 +340,12 @@ normalize_delta :: proc "contextless" (delta: Delta) -> (normalized: Delta, err: // The following procedures don't check whether their inputs are in a valid range. // They're still exported for those who know their inputs have been validated. +/* +Obtain an ordinal from a date. + +This procedure converts a date into an ordinal. If the date is not a valid date, +the result is unspecified. +*/ unsafe_date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal) { year_minus_one := date.year - 1 @@ -223,6 +375,12 @@ unsafe_date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal) return } +/* +Obtain a year and a day of the year from an ordinal. + +This procedure returns the year and the day of the year of a given ordinal. +Of the ordinal is outside of its valid range, the result is unspecified. +*/ unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: i64, day_ordinal: i64) { // Days after epoch d0 := ordinal - EPOCH @@ -253,6 +411,12 @@ unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: i64, d return year + 1, day_ordinal } +/* +Obtain a date from an ordinal. + +This procedure converts an ordinal into a date. If the ordinal is outside of +its valid range, the result is unspecified. +*/ unsafe_ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date) { year, _ := unsafe_ordinal_to_year(ordinal) diff --git a/core/time/datetime/internal.odin b/core/time/datetime/internal.odin index 45c2b99ab..e7129548e 100644 --- a/core/time/datetime/internal.odin +++ b/core/time/datetime/internal.odin @@ -1,3 +1,4 @@ +//+private package datetime // Internal helper functions for calendrical conversions diff --git a/core/time/datetime/validation.odin b/core/time/datetime/validation.odin index 87d5aa1cd..0a66833b0 100644 --- a/core/time/datetime/validation.odin +++ b/core/time/datetime/validation.odin @@ -1,14 +1,29 @@ package datetime - // Validation helpers + +/* +Check if a year is a leap year. +*/ is_leap_year :: proc "contextless" (#any_int year: i64) -> (leap: bool) { return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } +/* +Check for errors in date formation. + +This procedure validates all fields of a date, and if any of the fields is +outside of allowed range, an error is returned. +*/ validate_date :: proc "contextless" (date: Date) -> (err: Error) { return validate(date.year, date.month, date.day) } +/* +Check for errors in date formation given date components. + +This procedure checks whether a date formed by the specified year month and a +day is a valid date. If not, an error is returned. +*/ validate_year_month_day :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (err: Error) { if year < MIN_DATE.year || year > MAX_DATE.year { return .Invalid_Year @@ -29,6 +44,12 @@ validate_year_month_day :: proc "contextless" (#any_int year, #any_int month, #a return .None } +/* +Check for errors in Ordinal + +This procedure checks if the ordinal is in a valid range for roundtrip +conversions with the dates. If not, an error is returned. +*/ validate_ordinal :: proc "contextless" (ordinal: Ordinal) -> (err: Error) { if ordinal < MIN_ORD || ordinal > MAX_ORD { return .Invalid_Ordinal @@ -36,10 +57,22 @@ validate_ordinal :: proc "contextless" (ordinal: Ordinal) -> (err: Error) { return } +/* +Check for errors in time formation + +This procedure checks whether time has all fields in valid ranges, and if not +an error is returned. +*/ validate_time :: proc "contextless" (time: Time) -> (err: Error) { return validate(time.hour, time.minute, time.second, time.nano) } +/* +Check for errors in time formed by its components. + +This procedure checks whether the time formed by its components is valid, and +if not an error is returned. +*/ validate_hour_minute_second :: proc "contextless" (#any_int hour, #any_int minute, #any_int second, #any_int nano: i64) -> (err: Error) { if hour < 0 || hour > 23 { return .Invalid_Hour @@ -56,12 +89,21 @@ validate_hour_minute_second :: proc "contextless" (#any_int hour, #any_int minut return .None } +/* +Check for errors in datetime formation. + +This procedure checks whether all fields of date and time in the specified +datetime are valid, and if not, an error is returned. +*/ validate_datetime :: proc "contextless" (datetime: DateTime) -> (err: Error) { validate(datetime.date) or_return validate(datetime.time) or_return return .None } +/* +Check for errors in date, time or datetime. +*/ validate :: proc{ validate_date, validate_year_month_day,