require "crystal/system/time" # `Time` represents a date-time instant in # [incremental time](https://www.w3.org/International/articles/definitions-time/#incremental_time) # observed in a specific time zone. # # The calendaric calculations are based on the rules of the proleptic Gregorian # calendar as specified in [ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf). # Leap seconds are ignored. # # Internally, the time is stored as an `Int64` representing seconds from epoch # (`0001-01-01 00:00:00Z`) and an `Int32` representing # nanosecond-of-second with value range `0..999_999_999`. # # The supported date range is `0001-01-01 00:00:00.0` to # `9999-12-31 23:59:59.999_999_999` in any local time zone. # # ### Telling the Time # # There are several methods to retrieve a `Time` instance representing the # current time: # # ``` # Time.utc # returns the current time in UTC # Time.local Time::Location.load("Europe/Berlin") # returns the current time in time zone Europe/Berlin # Time.local # returns the current time in current time zone # ``` # # It is generally recommended to keep instances in UTC and only apply a # local time zone when formatting for user display, unless the domain logic # requires having a specific time zone (for example for calendaric operations). # # ### Creating a Specific Instant # # `Time` instances representing a specific instant can be created by # `.utc` or `.new` with the date-time specified as individual arguments: # # ``` # time = Time.utc(2016, 2, 15, 10, 20, 30) # time.to_s # => "2016-02-15 10:20:30 UTC" # time = Time.local(2016, 2, 15, 10, 20, 30, location: Time::Location.load("Europe/Berlin")) # time.to_s # => "2016-02-15 10:20:30 +01:00" # # The time-of-day can be omitted and defaults to midnight (start of day): # time = Time.utc(2016, 2, 15) # time.to_s # => "2016-02-15 00:00:00 UTC" # ``` # # ### Retrieving Time Information # # Each `Time` instance allows querying calendar data: # # ``` # time = Time.utc(2016, 2, 15, 10, 20, 30) # time.year # => 2016 # time.month # => 2 # time.day # => 15 # time.hour # => 10 # time.minute # => 20 # time.second # => 30 # time.millisecond # => 0 # time.nanosecond # => 0 # time.day_of_week # => Time::DayOfWeek::Monday # time.day_of_year # => 46 # time.monday? # => true # time.time_of_day # => 10:20:30 # ``` # # For querying if a time is at a specific day of week, `Time` offers named # predicate methods, delegating to `#day_of_week`: # # ``` # time.monday? # => true # # ... # time.sunday? # => false # ``` # # ### Time Zones # # Each time is attached to a specific time zone, represented by a `Location` # (see `#location`). # `#zone` returns the time zone observed in this location at the current time # (i.e. the instant represented by this `Time`). # `#offset` returns the offset of the current zone in seconds. # # ``` # time = Time.local(2018, 3, 8, 22, 5, 13, location: Time::Location.load("Europe/Berlin")) # time # => 2018-03-08 22:05:13+01:00[Europe/Berlin] # time.location # => # # time.zone # => # # time.offset # => 3600 # ``` # # Using `.utc`, the location is `Time::Location::UTC`: # # ``` # time = Time.utc(2018, 3, 8, 22, 5, 13) # time # => 2018-03-08 22:05:13Z # time.location # => # # time.zone # => # # time.offset # => 0 # ``` # # A `Time` instance can be transformed to a different time zone while retaining # the same instant using `#in`: # # ``` # time_de = Time.local(2018, 3, 8, 22, 5, 13, location: Time::Location.load("Europe/Berlin")) # time_ar = time_de.in Time::Location.load("America/Argentina/Buenos_Aires") # time_de # => 2018-03-08 22:05:13+01:00[Europe/Berlin] # time_ar # => 2018-03-08 18:05:13-03:00[America/Argentina/Buenos_Aires] # ``` # # Both `Time` instances show a different local date-time, but they represent # the same date-time in the instant time-line, therefore they are considered # equal: # # ``` # time_de.to_utc # => 2018-03-08 21:05:13Z # time_ar.to_utc # => 2018-03-08 21:05:13Z # time_de == time_ar # => true # ``` # # There are also two special methods for converting to UTC and local time zone: # # ``` # time.to_utc # equals time.in(Location::UTC) # time.to_local # equals time.in(Location.local) # ``` # # `#to_local_in` allows changing the time zone while keeping # the same local date-time (wall clock) which results in a different instant # on the time line. # # ### Formatting and Parsing Time # # To make date-time instances exchangeable between different computer systems # or readable to humans, they are usually converted to and from a string # representation. # # The method `#to_s` formats the date-time according to a specified pattern. # # ``` # time = Time.utc(2015, 10, 12, 10, 30, 0) # time.to_s("%Y-%m-%d %H:%M:%S %:z") # => "2015-10-12 10:30:00 +00:00" # ``` # # Similarly, `Time.parse` and `Time.parse!` are used to construct a `Time` instance from date-time # information in a string, according to a specified pattern: # # ``` # Time.parse("2015-10-12 10:30:00 +00:00", "%Y-%m-%d %H:%M:%S %z", Time::Location::UTC) # Time.parse!("2015-10-12 10:30:00 +00:00", "%Y-%m-%d %H:%M:%S %z") # ``` # # See `Time::Format` for all directives. # # ### Calculations # # ``` # Time.utc(2015, 10, 10) - 5.days # => 2015-10-05 00:00:00 +00:00 # # span = Time.utc(2015, 10, 10) - Time.utc(2015, 9, 10) # span.days # => 30 # span.total_hours # => 720 # span.total_minutes # => 43200 # ``` # # ## Measuring Time # # The typical time representation provided by the operating system is based on # a "wall clock" which is subject to changes for clock synchronization. # This can result in discontinuous jumps in the time-line making it not # suitable for accurately measuring elapsed time. # # Instances of `Time` are focused on telling time – using a "wall clock". # When `Time.local` is called multiple times, the difference between the # returned instances is not guaranteed to equal to the time elapsed between # making the calls; even the order of the returned `Time` instances might # not reflect invocation order. # # ``` # t1 = Time.utc # # operation that takes 1 minute # t2 = Time.utc # t2 - t1 # => ? # ``` # # The resulting `Time::Span` could be anything, even negative, if the # computer's wall clock has changed between both calls. # # As an alternative, the operating system also provides a monotonic clock. # Its time-line has no specified starting point but is strictly linearly # increasing. # # This monotonic clock should always be used for measuring elapsed time. # # A reading from this clock can be taken using `.instant`: # # ``` # t1 = Time.instant # # operation that takes 1 minute # t2 = Time.instant # t2.duration_since(t1) # => 1.minute (approximately) # ``` # # The execution time of a block can be measured using `.measure`: # # ``` # elapsed_time = Time.measure do # # operation that takes 20 milliseconds # end # elapsed_time # => 20.milliseconds (approximately) # ``` struct Time # Raised when an error occurs while performing a `Time` based operation. class Error < Exception end class FloatingTimeConversionError < Error end include Comparable(Time) include Steppable # :nodoc: DAYS_MONTH = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} # :nodoc: DAYS_MONTH_LEAP = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} # :nodoc: SECONDS_PER_MINUTE = 60 # :nodoc: SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE # :nodoc: SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR # :nodoc: SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY # :nodoc: NANOSECONDS_PER_MILLISECOND = 1_000_000_i64 # :nodoc: NANOSECONDS_PER_MICROSECOND = 1_000_i64 # :nodoc: NANOSECONDS_PER_SECOND = 1_000_000_000_i64 # :nodoc: NANOSECONDS_PER_MINUTE = NANOSECONDS_PER_SECOND * 60 # :nodoc: NANOSECONDS_PER_HOUR = NANOSECONDS_PER_MINUTE * 60 # :nodoc: NANOSECONDS_PER_DAY = NANOSECONDS_PER_HOUR * 24 # :nodoc: DAYS_PER_400_YEARS = 365*400 + 97 # :nodoc: DAYS_PER_100_YEARS = 365*100 + 24 # :nodoc: DAYS_PER_4_YEARS = 365*4 + 1 # This constant is defined as `1970-01-01 00:00:00Z`". # Can be used to create a `Time::Span` that represents an Unix Epoch time duration. # # ``` # Time.utc - Time::UNIX_EPOCH # ``` UNIX_EPOCH = new(unsafe_utc_seconds: 62135596800) # :nodoc: MAX_SECONDS = 315537897599_i64 # `DayOfWeek` represents a day of the week in the Gregorian calendar. # # ``` # time = Time.local(2016, 2, 15) # time.day_of_week # => Time::DayOfWeek::Monday # ``` # # Each member is identified by its ordinal number starting from `Monday = 1` # according to [ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf). # # `#value` returns this ordinal number. It can easily be converted to the also # common numbering based on `Sunday = 0` using `value % 7`. enum DayOfWeek Monday = 1 Tuesday = 2 Wednesday = 3 Thursday = 4 Friday = 5 Saturday = 6 Sunday = 7 # Returns the day of week that has the given value, or raises if no such member exists. # # This method also accepts `0` to identify `Sunday` in order to be compliant # with the `Sunday = 0` numbering. All other days are equal in both formats. def self.from_value(value : Int32) : self value = 7 if value == 0 super(value) end end @seconds : Int64 @nanoseconds : Int32 # Returns `Location` representing the time-zone observed by this `Time`. getter location : Location # Returns a reading from the monotonic clock to measure elapsed time. # # Values from the monotonic clock and wall clock are not comparable. # This method does not return a `Time` instance but a `Time::Span` amounting # to the number of nanoseconds elapsed since the unspecified starting point # of the monotonic clock. # The returned values are strictly linearly increasing. # # This clock should be independent from discontinuous jumps in the # system time, such as leap seconds, time zone adjustments or manual changes # to the computer's clock. # # Subtracting two results from this method equals to the time elapsed between # both readings: # # ``` # start = Time.monotonic # # operation that takes 20 milliseconds # elapsed = Time.monotonic - start # => 20.milliseconds (approximately) # # operation that takes 50 milliseconds # elapsed_total = Time.monotonic - start # => 70.milliseconds (approximately) # ``` # # The execution time of a block can be measured using `.measure`. @[Deprecated("Use `Time.instant` instead.")] def self.monotonic : Time::Span seconds, nanoseconds = Crystal::System::Time.monotonic Time::Span.new(seconds: seconds, nanoseconds: nanoseconds) end # Measures the execution time of *block*. # # The measurement relies on the monotonic clock and is not # affected by fluctuations of the system clock (see `#monotonic`). # # ``` # elapsed_time = Time.measure do # # operation that takes 20 milliseconds # end # elapsed_time # => 20.milliseconds (approximately) # ``` def self.measure(&block : ->) : Time::Span start = instant yield start.elapsed end # Creates a new `Time` instance representing the current time from the # system clock observed in *location* (defaults to local time zone). def self.local(location : Location = Location.local) : Time seconds, nanoseconds = Crystal::System::Time.compute_utc_seconds_and_nanoseconds new(seconds: seconds, nanoseconds: nanoseconds, location: location) end # Creates a new `Time` instance representing the current time from the # system clock in UTC. def self.utc : Time local(Location::UTC) end # Creates a new `Time` instance representing the given local date-time in # *location* (defaults to local time zone). # # ``` # time = Time.local(2016, 2, 15, 10, 20, 30, location: Time::Location.load("Europe/Berlin")) # time.inspect # => "2016-02-15 10:20:30+01:00[Europe/Berlin]" # ``` # # Valid value ranges for the individual fields: # # * `year`: `1..9999` # * `month`: `1..12` # * `day`: `1` - `28`/`29`/`30`/`31` (depending on month and year) # * `hour`: `0..23` # * `minute`: `0..59` # * `second`: `0..59` # * `nanosecond`: `0..999_999_999` # # The time-of-day can be omitted and defaults to midnight (start of day): # # ``` # time = Time.utc(2016, 2, 15) # time.to_s # => "2016-02-15 00:00:00 UTC" # ``` # # The local date-time representation is resolved to a single instant based on # the offset observed in the *location* at this time. # # This process can sometimes be ambiguous, mostly due skipping or repeating # times at time zone transitions. For example, in `America/New_York` the # date-time `2011-03-13 02:15:00` never occurred, there is a gap between time # zones. In return, `2011-11-06 01:15:00` occurred twice because of overlapping # time zones. # # In such cases, the choice of time zone, and therefore the time, is not # well-defined. This method returns a time that is correct in one of the two # zones involved in the transition, but it does not guarantee which. def self.local(year : Int32, month : Int32, day : Int32, hour : Int32 = 0, minute : Int32 = 0, second : Int32 = 0, *, nanosecond : Int32 = 0, location : Location = Location.local) : Time unless 1 <= year <= 9999 && 1 <= month <= 12 && 1 <= day <= Time.days_in_month(year, month) && ( 0 <= hour <= 23 || (hour == 24 && minute == 0 && second == 0 && nanosecond == 0) ) && 0 <= minute <= 59 && 0 <= second <= 59 && 0 <= nanosecond <= 999_999_999 raise ArgumentError.new "Invalid time" end days = absolute_days(year, month, day) seconds = 1_i64 * SECONDS_PER_DAY * days + SECONDS_PER_HOUR * hour + SECONDS_PER_MINUTE * minute + second # Normalize internal representation to UTC seconds = seconds - zone_offset_at(seconds, location) if !location.utc? new(seconds: seconds, nanoseconds: nanosecond.to_i, location: location) end # Creates a new `Time` instance representing the given date-time in UTC. # # ``` # time = Time.utc(2016, 2, 15, 10, 20, 30) # time.to_s # => "2016-02-15 10:20:30 UTC" # ``` # # Valid value ranges for the individual fields: # # * `year`: `1..9999` # * `month`: `1..12` # * `day`: `1` - `28`/`29`/`30`/`31` (depending on month and year) # * `hour`: `0..23` # * `minute`: `0..59` # * `second`: `0..59` # * `nanosecond`: `0..999_999_999` # # The time-of-day can be omitted and defaults to midnight (start of day): # # ``` # time = Time.utc(2016, 2, 15) # time.to_s # => "2016-02-15 00:00:00 UTC" # ``` # # Since UTC does not have any time zone transitions, each date-time is # unambiguously resolved. def self.utc(year : Int32, month : Int32, day : Int32, hour : Int32 = 0, minute : Int32 = 0, second : Int32 = 0, *, nanosecond : Int32 = 0) : Time local(year, month, day, hour, minute, second, nanosecond: nanosecond, location: Location::UTC) end # Creates a new `Time` instance that corresponds to the number of *seconds* # and *nanoseconds* elapsed from epoch (`0001-01-01 00:00:00.0`) # observed in *location*. # # Valid range for *seconds* is `0..315_537_897_599`. # For *nanoseconds* it is `0..999_999_999`. def initialize(*, @seconds : Int64, @nanoseconds : Int32, @location : Location) unless 0 <= offset_seconds <= MAX_SECONDS raise ArgumentError.new "Invalid time: seconds out of range" end unless 0 <= @nanoseconds < NANOSECONDS_PER_SECOND raise ArgumentError.new "Invalid time: nanoseconds out of range" end end # :nodoc: protected def initialize(*, unsafe_utc_seconds : Int64) @seconds = unsafe_utc_seconds @nanoseconds = 0 @location = Location::UTC end # Creates a new `Time` instance that corresponds to the number of *seconds* # and *nanoseconds* elapsed from epoch (`0001-01-01 00:00:00Z`) # in UTC. # # Valid range for *seconds* is `0..315_537_897_599`. # For *nanoseconds* it is `0..999_999_999`. def self.utc(*, seconds : Int64, nanoseconds : Int32) : Time new(seconds: seconds, nanoseconds: nanoseconds, location: Location::UTC) end {% unless flag?(:win32) %} # :nodoc: def self.new(time : LibC::Timespec, location : Location = Location.local) seconds = UNIX_EPOCH.total_seconds + time.tv_sec nanoseconds = time.tv_nsec.to_i new(seconds: seconds, nanoseconds: nanoseconds, location: location) end {% end %} # Creates a new `Time` instance that corresponds to the number of # *seconds* elapsed since the Unix epoch (`1970-01-01 00:00:00Z`). # # The time zone is always UTC. # # ``` # Time.unix(981173106) # => 2001-02-03 04:05:06Z # ``` def self.unix(seconds : Int) : Time utc(seconds: UNIX_EPOCH.total_seconds + seconds, nanoseconds: 0) end # Creates a new `Time` instance that corresponds to the number of # *milliseconds* elapsed since the Unix epoch (`1970-01-01 00:00:00Z`). # # The time zone is always UTC. # # ``` # time = Time.unix_ms(981173106789) # => 2001-02-03 04:05:06.789Z # time.millisecond # => 789 # ``` def self.unix_ms(milliseconds : Int) : Time milliseconds = milliseconds.to_i64 seconds = UNIX_EPOCH.total_seconds + (milliseconds // 1_000) nanoseconds = (milliseconds % 1000) * NANOSECONDS_PER_MILLISECOND utc(seconds: seconds, nanoseconds: nanoseconds.to_i) end # Creates a new `Time` instance that corresponds to the number of # *nanoseconds* elapsed since the Unix epoch (`1970-01-01 00:00:00Z`). # # The time zone is always UTC. # # ``` # time = Time.unix_ns(981173106789479273) # => 2001-02-03 04:05:06.789479273Z # time.nanosecond # => 789479273 # ``` def self.unix_ns(nanoseconds : Int) : Time seconds = UNIX_EPOCH.total_seconds + (nanoseconds // 1_000_000_000) nanoseconds = nanoseconds % 1_000_000_000 utc(seconds: seconds, nanoseconds: nanoseconds.to_i) end # Creates a new `Time` instance with the same local date-time representation # (wall clock) in a different *location*. # # Unlike `#in`, which always preserves the same instant in time, `#to_local_in` # adjusts the instant such that it results in the same local date-time # representation. Both instants are apart from each other by the difference in # zone offsets. # # ``` # new_year = Time.utc(2019, 1, 1, 0, 0, 0) # tokyo = new_year.to_local_in(Time::Location.load("Asia/Tokyo")) # new_york = new_year.to_local_in(Time::Location.load("America/New_York")) # tokyo.inspect # => "2019-01-01 00:00:00+09:00[Asia/Tokyo]" # new_york.inspect # => "2019-01-01 00:00:00-05:00[America/New_York]" # ``` # # If the given wall clock is a gap or fold in the target location, no # unambiguous equivalent representation exists. # # * In case of a gap, the clock jumps to the smaller offset and shows a time # _before_ the gap. The wall clock is different from `self`. # * In case of a fold, the clock chooses the bigger offset and shows a time # _after_ the fold. The wall clock is identical to `self`. # # ``` # nyc = Time::Location.load("America/New_York") # # # gap on 2025-03-09 local time: 02:00:00 STD -> 03:00:00 DST # Time.utc(2025, 3, 9, 2, 12, 34).to_local_in(nyc) # => 2025-03-09 01:12:34.0-05:00[America/New_York] # # # overlap on 2025-11-02 local time: 02:00:00 DST -> 01:00:00 STD # Time.utc(2025, 11, 2, 1, 12, 34).to_local_in(nyc) # => 2025-11-02 01:12:34.0-04:00[America/New_York] # ``` def to_local_in(location : Location) : Time local_seconds = offset_seconds local_seconds -= Time.zone_offset_at(local_seconds, location) Time.new(seconds: local_seconds, nanoseconds: nanosecond, location: location) end def clone : Time self end # Returns a copy of this `Time` with *span* added. # # See `#shift` for details. def +(span : Time::Span) : Time shift span.to_i, span.nanoseconds end # Returns a copy of this `Time` with *span* subtracted. # # See `#shift` for details. def -(span : Time::Span) : Time shift -span.to_i, -span.nanoseconds end # Returns a copy of this `Time` with *span* added. # # It adds the number of months with overflow increasing the year. # If the resulting day-of-month would be invalid, it is adjusted to the last # valid day of the month. # # For example, adding `1.month` to `2007-03-31` would result in the invalid # date `2007-04-31` which will be adjusted to `2007-04-30`. # # This operates on the local time-line, such that the local date-time # representations of month and year are increased by the specified amount. # # If the resulting date-time is ambiguous due to time zone transitions, # a correct time will be returned, but it does not guarantee which. def +(span : Time::MonthSpan) : Time shift months: span.value.to_i end # Returns a copy of this `Time` with *span* subtracted. # # It adds the number of months with overflow decreasing the year. # If the resulting day-of-month would be invalid, it is adjusted to the last # valid day of the month. # # For example, subtracting `1.month` from `2007-05-31` would result in the invalid # date `2007-04-31` which will be adjusted to `2007-04-30`. # # This operates on the local time-line, such that the local date-time # representations of month and year are decreased by the specified amount. # # If the resulting date-time is ambiguous due to time zone transitions, # a correct time will be returned, but it does not guarantee which. def -(span : Time::MonthSpan) : Time shift months: -span.value.to_i end # Returns a copy of this `Time` shifted by the number of *seconds* and # *nanoseconds*. # # Positive values result in a later time, negative values in an earlier time. # # This operates on the instant time-line, such that adding the equivalent of # one hour will always be a duration of one hour later. # The local date-time representation may change by a different amount, # depending on time zone transitions. # # Overflow in *nanoseconds* will be transferred to *seconds*. # # There is no explicit limit on the input values but the addition must result # in a valid time between `0001-01-01 00:00:00.0` and # `9999-12-31 23:59:59.999_999_999`. Otherwise `ArgumentError` is raised. def shift(seconds : Int, nanoseconds : Int) : Time if seconds == 0 && nanoseconds == 0 return self end seconds = total_seconds + seconds nanoseconds = self.nanosecond.to_i64 + nanoseconds # Nanoseconds might end up outside the min/max nanosecond # range, so take care of that seconds += nanoseconds.tdiv(NANOSECONDS_PER_SECOND) nanoseconds = nanoseconds.remainder(NANOSECONDS_PER_SECOND) if nanoseconds < 0 seconds -= 1 nanoseconds += NANOSECONDS_PER_SECOND end Time.new(seconds: seconds, nanoseconds: nanoseconds.to_i, location: location) end # Returns a copy of this `Time` shifted by the amount of calendaric units # provided as arguments. # # Positive values result in a later time, negative values in an earlier time. # # This operates on the local time-line, such that the local date-time # representation of the result will be apart by the specified amounts, but the # elapsed time between both instances might not equal to the combined default # duration. # This is the case for example when adding a day over a daylight-savings time # change: # # ``` # start = Time.local(2017, 10, 28, 13, 37, location: Time::Location.load("Europe/Berlin")) # one_day_later = start.shift days: 1 # # one_day_later - start # => 25.hours # ``` # # *years* is equivalent to `12` months and *weeks* is equivalent to `7` days. # # If the day-of-month resulting from shifting by *years* and *months* would be # invalid, the date is adjusted to the last valid day of the month. # For example, adding one month to `2018-08-31` would result in the invalid # date `2018-09-31` which will be adjusted to `2018-09-30`: # ``` # Time.utc(2018, 7, 31).shift(months: 1) # => Time.utc(2018, 8, 31) # Time.utc(2018, 8, 31).shift(months: 1) # => Time.utc(2018, 9, 30) # ``` # # Overflow in smaller units is transferred to the next larger unit. # # Changes are applied in the same order as the arguments, sorted by increasing # granularity. This is relevant because the order of operations can change the result: # # ``` # Time.utc(2018, 8, 31).shift(months: 1, days: -1) # => Time.utc(2018, 9, 29) # Time.utc(2018, 8, 31).shift(months: 1).shift(days: -1) # => Time.utc(2018, 9, 29) # Time.utc(2018, 8, 31).shift(days: -1).shift(months: 1) # => Time.utc(2018, 9, 30) # ``` # # There is no explicit limit on the input values but the shift must result # in a valid time between `0001-01-01 00:00:00.0` and # `9999-12-31 23:59:59.999_999_999`. Otherwise `ArgumentError` is raised. # # If the resulting date-time is ambiguous due to time zone transitions, # a correct time will be returned, but it does not guarantee which. def shift(*, years : Int = 0, months : Int = 0, weeks : Int = 0, days : Int = 0, hours : Int = 0, minutes : Int = 0, seconds : Int = 0, nanoseconds : Int = 0) seconds = seconds.to_i64 # Skip the entire month-based calculations if year and month are zero if years.zero? && months.zero? # Using offset_seconds with applied zone offset so that calculations # are applied to the equivalent UTC representation of this local time. seconds += offset_seconds else year, month, day, _ = year_month_day_day_year year += years months += month year += months.tdiv(12) month = months.remainder(12) if month < 1 month = 12 + month year -= 1 end maxday = Time.days_in_month(year, month) if day > maxday day = maxday end seconds += Time.absolute_days(year, month, day).to_i64 * SECONDS_PER_DAY seconds += offset_seconds % SECONDS_PER_DAY end # FIXME: These operations currently don't have overflow checks applied. # This should be fixed when operators by default raise on overflow. seconds += weeks * SECONDS_PER_WEEK seconds += days * SECONDS_PER_DAY seconds += hours * SECONDS_PER_HOUR seconds += minutes * SECONDS_PER_MINUTE # Apply the nanosecond shift (including overflow handling) and transform to # local time zone in `location`: Time.utc(seconds: seconds, nanoseconds: self.nanosecond).shift(0, nanoseconds).to_local_in(location) end # Returns a `Time::Span` amounting to the duration between *other* and `self`. # # The time span is negative if `self` is before *other*. # # The duration amounts to the actual time elapsed between both instances, on # the instant time-line. # The difference between local date-time representations may equal to a # different duration, depending on time zone transitions. def -(other : Time) : Time::Span Span.new( seconds: total_seconds - other.total_seconds, nanoseconds: nanosecond - other.nanosecond, ) end # Returns a `Tuple` with `year`, `month` and `day`. def date : Tuple(Int32, Int32, Int32) year, month, day, _ = year_month_day_day_year {year, month, day} end # Returns the year of the proleptic Georgian Calendar (`0..9999`). def year : Int32 year_month_day_day_year[0] end # Returns the month of the year (`1..12`). def month : Int32 year_month_day_day_year[1] end # Returns the day of the month (`1..31`). def day : Int32 year_month_day_day_year[2] end # Returns the hour of the day (`0..23`). def hour : Int32 ((offset_seconds % SECONDS_PER_DAY) // SECONDS_PER_HOUR).to_i end # Returns the minute of the hour (`0..59`). def minute : Int32 ((offset_seconds % SECONDS_PER_HOUR) // SECONDS_PER_MINUTE).to_i end # Returns the second of the minute (`0..59`). def second : Int32 (offset_seconds % SECONDS_PER_MINUTE).to_i end # Returns the millisecond of the second (`0..999`). def millisecond : Int32 nanosecond // NANOSECONDS_PER_MILLISECOND end # Returns the nanosecond of the second (`0..999_999_999`). def nanosecond : Int32 @nanoseconds end # Returns the ISO calendar year and week in which this instance occurs. # # The ISO calendar year to which the week belongs is not always in the same # as the year of the regular calendar date. The first three days of January # sometimes belong to week 52 (or 53) of the previous year; # equally the last three days of December sometimes are already in week 1 # of the following year. # # For that reason, this method returns a tuple `year, week` consisting of the # calendar year to which the calendar week belongs and the ordinal number of # the week within that year. # # Together with `#day_of_week` this represents a specific day as commercial or # week date format `year, week, day_of_week` in the same way as the typical # format `year, month, day`. # `.week_date` creates a `Time` instance from a week date. def calendar_week : {Int32, Int32} year, month, day, day_year = year_month_day_day_year day_of_week = self.day_of_week # The week number can be calculated as number of Mondays in the year up to # the ordinal date. # The addition by +10 consists of +7 to start the week numbering with 1 # instead of 0 and +3 because the first week has already started in the # previous year and the first Monday is actually in week 2. week_number = (day_year - day_of_week.to_i + 10) // 7 if week_number == 0 # Week number 0 means the date belongs to the last week of the previous year. year -= 1 # The week number depends on whether the previous year has 52 or 53 weeks # which can be determined by the day of week of January 1. # The year has 53 weeks if January 1 is on a Friday or the year was a leap # year and January 1 is on a Saturday. jan1_day_of_week = DayOfWeek.from_value((day_of_week.to_i - day_year + 1) % 7) if jan1_day_of_week == DayOfWeek::Friday || (jan1_day_of_week == DayOfWeek::Saturday && Time.leap_year?(year)) week_number = 53 else week_number = 52 end elsif week_number == 53 # Week number 53 is actually week number 1 of the following year, if # December 31 is on a Monday, Tuesday or Wednesday. dec31_day_of_week = (day_of_week.to_i + 31 - day) % 7 if dec31_day_of_week <= DayOfWeek::Wednesday.to_i year += 1 week_number = 1 end end {year, week_number} end # Creates an instance specified by a commercial week date consisting of ISO # calendar *year*, *week* and a *day_of_week*. # # This equates to the results from `#calendar_week` and `#day_of_week`. # # Valid value ranges for the individual fields: # # * `year`: `1..9999` # * `week`: `1..53` # * `day_of_week`: `1..7` def self.week_date(year : Int32, week : Int32, day_of_week : Int32 | DayOfWeek, hour : Int32 = 0, minute : Int32 = 0, second : Int32 = 0, *, nanosecond : Int32 = 0, location : Location = Location.local) : self # For this calculation we need to know the weekday of January 4. # The number of the day plus a fixed offset of 4 gives a correction value # for this year. jan4_day_of_week = Time.utc(year, 1, 4).day_of_week correction = jan4_day_of_week.to_i + 4 # The number of weeks multiplied by 7 plus the day of week and the calculated # correction value results in the ordinal day of the year. ordinal = week * 7 + day_of_week.to_i - correction # Adjust the year if the year of the week date does not correspond with the calendar year around New Years. if ordinal < 1 # If the ordinal day is zero or negative, the date belongs to the previous # calendar year. year -= 1 ordinal += Time.days_in_year(year) elsif ordinal > (days_in_year = Time.days_in_year(year)) # If the ordinal day is greater than the number of days in the year, the date # belongs to the next year. ordinal -= days_in_year year += 1 end # The ordinal day together with the year fully specifies the date. # A new instance for January 1 plus the ordinal days results in the correct date. # This calculation needs to be in UTC to avoid issues with changes in # the time zone offset (such as daylight savings time). # TODO: Use #shift or #to_local_in instead time = Time.utc(year, 1, 1, hour, minute, second, nanosecond: nanosecond) + ordinal.days # If the location is UTC, we're done return time if location.utc? # otherwise, transfer to the specified location without changing the time of day. time = time.in(location: location) time - time.offset.seconds end # Creates an instance representing the *week*-th *day_of_week* in the given # *year* and *month*. # # If *week* is `5` and there are not enough weeks in the given month (e.g. # February in a non-leap year), the last week of the month is used instead. # # Valid value ranges for the individual fields: # # * `year`: `1..9999` # * `month`: `1..12` # * `week`: `1..5` # * `day_of_week`: `0..7` def self.month_week_date(year : Int32, month : Int32, week : Int32, day_of_week : Int32 | DayOfWeek, hour : Int32 = 0, minute : Int32 = 0, second : Int32 = 0, *, nanosecond : Int32 = 0, location : Location = Location.local) : self raise ArgumentError.new "Invalid week of month" unless week.in?(1..5) day_of_week = DayOfWeek.from_value(day_of_week) if day_of_week.is_a?(Int32) day_of_week = day_of_week.to_i32 first_day_of_week = Time.utc(year, month, 1).day_of_week.to_i32 day = (day_of_week - first_day_of_week) % 7 + 1 max_weeks = (Time.days_in_month(year, month) - day) // 7 day += {week - 1, max_weeks}.min * 7 time = Time.utc(year, month, day, hour, minute, second, nanosecond: nanosecond) return time if location.utc? time = time.in(location) time - time.offset.seconds end # Returns the duration between this `Time` and midnight of the same day. # # This is equivalent to creating a `Time::Span` from the time-of-day fields: # # ``` # time.time_of_day == Time::Span.new(hours: time.hour, minutes: time.minute, seconds: time.second, nanoseconds: time.nanosecond) # ``` def time_of_day : Time::Span Span.new(nanoseconds: NANOSECONDS_PER_SECOND * (offset_seconds % SECONDS_PER_DAY) + nanosecond) end # Returns the day of the week (`Monday..Sunday`). def day_of_week : Time::DayOfWeek days = offset_seconds // SECONDS_PER_DAY DayOfWeek.new days.to_i % 7 + 1 end # Returns the day of the year. # # The value range is `1..365` in normal years and `1..366` in leap years. def day_of_year : Int32 year_month_day_day_year[3] end # Returns the time zone in effect in `location` at this instant. def zone : Time::Location::Zone location.lookup(self) end # Returns the offset from UTC (in seconds) in effect in `location` at # this instant. def offset : Int32 zone.offset end # Returns `true` if `#location` equals to `Location::UTC`. def utc? : Bool location.utc? end # Returns `true` if `#location` equals to the local time zone # (`Time::Location.local`). # # Since the system's settings may change during a program's runtime, # the result may not be identical between different invocations. def local? : Bool location.local? end # Compares this `Time` with *other*. # # The comparison is based on the instant time-line, even if the local # date-time representation (wall clock) would compare differently. # # To ensure the comparison is also true for local wall clock, both date-times # need to be transformed to the same time zone. def <=>(other : Time) : Int32 cmp = total_seconds <=> other.total_seconds cmp = nanosecond <=> other.nanosecond if cmp == 0 cmp end # Compares this `Time` with *other* for equality. # # Two instances are considered equal if they represent the same date-time in # the instant time-line, even if they show a different local date-time. # # ``` # time_de = Time.local(2018, 3, 8, 22, 5, 13, location: Time::Location.load("Europe/Berlin")) # time_ar = Time.local(2018, 3, 8, 18, 5, 13, location: Time::Location.load("America/Argentina/Buenos_Aires")) # time_de == time_ar # => true # # # both times represent the same instant: # time_de.to_utc # => 2018-03-08 21:05:13Z # time_ar.to_utc # => 2018-03-08 21:05:13Z # ``` def ==(other : Time) : Bool total_seconds == other.total_seconds && nanosecond == other.nanosecond end def_hash total_seconds, nanosecond # Returns the number of days in *month* (value range: `1..12`) taking account # of the *year*. # # The returned value is either `28`, `29`, `30` or `31` depending on the # month and whether *year* is leap. # # ``` # Time.days_in_month(2016, 2) # => 29 # Time.days_in_month(1990, 4) # => 30 # ``` def self.days_in_month(year : Int, month : Int) : Int32 unless 1 <= month <= 12 raise ArgumentError.new "Invalid month" end days = leap_year?(year) ? DAYS_MONTH_LEAP : DAYS_MONTH days[month] end # Returns the number of days in *year*. # # A normal year has `365` days, a leap year `366` days. # # ``` # Time.days_in_year(1990) # => 365 # Time.days_in_year(2004) # => 366 # ``` def self.days_in_year(year : Int) : Int32 leap_year?(year) ? 366 : 365 end # Returns `true` if *year* is a leap year in the proleptic Gregorian # calendar. def self.leap_year?(year : Int) : Bool unless 1 <= year <= 9999 raise ArgumentError.new "Invalid year" end year.divisible_by?(4) && (!year.divisible_by?(100) || year.divisible_by?(400)) end # Prints this `Time` to *io*. # # It's formatted according to the Internet Extended Date/Time Format (IXDTF) # as specified in [RFC 9557](https://datatracker.ietf.org/doc/html/rfc9557): # An [RFC 3339](https://tools.ietf.org/html/rfc3339) formatted local date-time # string with nanosecond precision followed by a time zone name suffix. # # It is similar to the format `%FT%T.%N%::z[%Z]`. Some parts may be omitted or # shortened. # # Nanoseconds are omitted if `#nanoseconds` is zero. Zero offset seconds are # omitted. The name of the location is omitted for fixed zone offset. # # ``` # Time.utc(2014, 1, 2, 3, 4, 5) # => 2014-01-02 03:04:05Z # Time.utc(2014, 1, 2, 3, 4, 5, nanosecond: 123_456) # => 2014-01-02 03:04:05.123456000Z # # Time.local(2014, 1, 2, 3, 4, 5, location: Time::Location.load("Europe/Berlin")) # => 2014-01-02 03:04:05+01:00[Europe/Berlin] # Time.local(2014, 1, 2, 3, 4, 5, location: Time::Location.fixed(3600)) # => 2014-01-02 03:04:05+01:00 # Time.local(2014, 1, 2, 3, 4, 5, location: Time::Location.fixed(3601)) # => 2014-01-02 03:04:05+01:00:01 # ``` def inspect(io : IO) : Nil Format::RFC_3339.format(self, io, fraction_digits: nanosecond.zero? ? 0 : 9, preferred_separator: ' ') io << '[' << location.name << ']' unless location.fixed? end # :ditto: # # Nanoseconds are omitted if *with_nanoseconds* is `false` or `nanoseconds` # are zero. @[Deprecated("`with_nanoseconds` is deprecated, please use a custom format if necessary")] def inspect(io : IO, with_nanoseconds) : Nil Format::RFC_3339.format(self, io, fraction_digits: (with_nanoseconds && !nanosecond.zero?) ? 9 : 0, preferred_separator: ' ') io << '[' << location.name << ']' unless location.fixed? end # Prints this `Time` to *io*. # # The local date-time is formatted as date string `YYYY-MM-DD HH:mm:ss +ZZ:ZZ:ZZ`. # Nanoseconds are always omitted. # When the location is `UTC`, the offset is replaced with the string `UTC`. # Offset seconds are omitted if `0`. def to_s(io : IO) : Nil formatter = Format::Formatter.new(self, io) formatter.year_month_day io << " " formatter.twenty_four_hour_time_with_seconds if utc? io << " UTC" else io << " " zone.format(io) end end # Formats this `Time` according to the pattern in *format*. # # See `Time::Format` for details. # # ``` # time = Time.local(2016, 4, 5) # time.to_s("%F") # => "2016-04-05" # ``` def to_s(format : String) : String Format.new(format).format(self) end # Formats this `Time` according to the pattern in *format* to the given *io*. # # See `Time::Format` for details. def to_s(io : IO, format : String) : Nil Format.new(format).format(self, io) end # Format this time using the format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # # ``` # Time.utc(2016, 2, 15).to_rfc3339 # => "2016-02-15 00:00:00Z" # ``` # # ISO 8601 allows some freedom over the syntax and RFC 3339 exercises that # freedom to rigidly define a fixed format intended for use in internet # protocols and standards. # # Number of seconds decimals can be selected with *fraction_digits*. # Values accepted are 0 (the default, no decimals), 3 (milliseconds), 6 (microseconds) or 9 (nanoseconds). def to_rfc3339(*, fraction_digits : Int = 0) Format::RFC_3339.format(to_utc, fraction_digits) end # Format this time using the format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # into the given *io*. # # # Number of seconds decimals can be selected with *fraction_digits*. # Values accepted are 0 (the default, no decimals), 3 (milliseconds), 6 (microseconds) or 9 (nanoseconds). def to_rfc3339(io : IO, *, fraction_digits : Int = 0) : Nil Format::RFC_3339.format(to_utc, io, fraction_digits) end # Parse time format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # # ``` # Time.parse_rfc3339("2016-02-15T04:35:50Z") # => 2016-02-15 04:35:50Z # ``` def self.parse_rfc3339(time : String) : self Format::RFC_3339.parse(time) end # Parse datetime format specified by [ISO 8601](https://web.archive.org/web/20250306154328/http://xml.coverpages.org/ISO-FDIS-8601.pdf). # # This is similar to `.parse_rfc3339` but RFC 3339 defines a more strict format. # In ISO 8601 for examples, field delimiters (`-`, `:`) are optional. # # Use `#to_rfc3339` to format a `Time` according to . # # ``` # Time.parse_iso8601("2016-02-15T04:35:50Z") # => 2016-02-15 04:35:50Z # ``` def self.parse_iso8601(time : String) Format::ISO_8601_DATE_TIME.parse(time) end # Format this time using the format specified by [RFC 2822](https://www.ietf.org/rfc/rfc2822.txt). # # ``` # Time.utc(2016, 2, 15).to_rfc2822 # => "Mon, 15 Feb 2016 00:00:00 +0000" # ``` # # This is also compatible to [RFC 882](https://tools.ietf.org/html/rfc882) and [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55). def to_rfc2822 : String Format::RFC_2822.format(to_utc) end # Format this time using the format specified by [RFC 2822](https://www.ietf.org/rfc/rfc2822.txt) # into the given *io*. # # This is also compatible to [RFC 882](https://tools.ietf.org/html/rfc882) and [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55). def to_rfc2822(io : IO) Format::RFC_2822.format(to_utc, io) end # Parse time format specified by [RFC 2822](https://www.ietf.org/rfc/rfc2822.txt). # # This is also compatible to [RFC 882](https://tools.ietf.org/html/rfc882) and [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55). # # ``` # Time.parse_rfc2822("Mon, 15 Feb 2016 04:35:50 UTC") # => 2016-02-15 04:35:50Z # ``` def self.parse_rfc2822(time : String) : self Format::RFC_2822.parse(time) end # Parses a `Time` from *time* string using the given *pattern*. # # See `Time::Format` for details. # # ``` # Time.parse("2016-04-05", "%F", Time::Location.load("Europe/Berlin")) # => 2016-04-05 00:00:00+02:00[Europe/Berlin] # ``` # # If there is no time zone information in the formatted time, *location* will # be assumed. When *location* is `nil`, in such a case the parser will raise # `Time::Format::Error`. def self.parse(time : String, pattern : String, location : Location) : Time Format.new(pattern, location).parse(time) end # Parses a `Time` from *time* string using the given *pattern*. # # See `Time::Format` for details. # # ``` # Time.parse!("2016-04-05 +00:00", "%F %:z") # => 2016-04-05 00:00:00.0 +00:00 # Time.parse!("2016-04-05", "%F") # raises Time::Format::Error # ``` # # If there is no time zone information in the formatted time, the parser will raise # `Time::Format::Error`. def self.parse!(time : String, pattern : String) : Time Format.new(pattern, nil).parse(time) end # Parses a `Time` from *time* string using the given *pattern* and # `Time::Location::UTC` as default location. # # See `Time::Format` for details. # # ``` # Time.parse_utc("2016-04-05", "%F") # => 2016-04-05 00:00:00.0 +00:00 # ``` # # `Time::Location::UTC` will only be used as `location` if the formatted time # does not contain any time zone information. The return value can't be # assumed to be a UTC time (this can be achieved by calling `#to_utc`). def self.parse_utc(time : String, pattern : String) : Time parse(time, pattern, Location::UTC) end # Parses a `Time` from *time* string using the given *pattern* and # `Time::Location.local` as default location # # See `Time::Format` for details. # # ``` # Time.parse_utc("2016-04-05", "%F") # => 2016-04-05 00:00:00.0 +00:00 # ``` # # `Time::Location.local` will only be used as `location` if the formatted time # does not contain any time zone information. The return value can't be # assumed to be a UTC time (this can be achieved by calling `#to_local`). def self.parse_local(time : String, pattern : String) : Time parse(time, pattern, Location.local) end # Returns the number of seconds since the Unix epoch # (`1970-01-01 00:00:00Z`). # # ``` # time = Time.utc(2016, 1, 12, 3, 4, 5) # time.to_unix # => 1452567845 # ``` def to_unix : Int64 (total_seconds - UNIX_EPOCH.total_seconds).to_i64 end # Returns the number of milliseconds since the Unix epoch # (`1970-01-01 00:00:00Z`). # # ``` # time = Time.utc(2016, 1, 12, 3, 4, 5, nanosecond: 678_000_000) # time.to_unix_ms # => 1452567845678 # ``` def to_unix_ms : Int64 to_unix * 1_000 + (nanosecond // NANOSECONDS_PER_MILLISECOND) end # Returns the number of nanoseconds since the Unix epoch # (`1970-01-01 00:00:00Z`). # # ``` # time = Time.utc(2016, 1, 12, 3, 4, 5, nanosecond: 678_910_123) # time.to_unix_ns # => 1452567845678910123 # ``` def to_unix_ns : Int128 (to_unix.to_i128 * NANOSECONDS_PER_SECOND) + nanosecond end # Returns the number of seconds since the Unix epoch # (`1970-01-01 00:00:00Z`) as `Float64` with nanosecond precision. # # ``` # time = Time.utc(2016, 1, 12, 3, 4, 5, nanosecond: 678_000_000) # time.to_unix_f # => 1452567845.678 # ``` def to_unix_f : Float64 to_unix.to_f + nanosecond.to_f / 1e9 end # Returns a copy of this `Time` representing the same instant observed in # *location*. # # This method changes the time zone and retains the instant, which will # usually result in a different representation of local date-time (unless # both locations have the same offset). # # Ambiguous time zone transitions such as gaps and overlaps have no effect on # the result because it retains the same instant. # # ``` # time_de = Time.local(2018, 3, 8, 22, 5, 13, location: Time::Location.load("Europe/Berlin")) # time_ar = time_de.in Time::Location.load("America/Argentina/Buenos_Aires") # time_de # => 2018-03-08 22:05:13+01:00[Europe/Berlin] # time_ar # => 2018-03-08 18:05:13-03:00[America/Argentina/Buenos_Aires] # ``` # # In contrast, `#to_local_in` changes to a different location while # preserving the same wall time, which results in different instants on the # time line. def in(location : Location) : Time return self if location == self.location Time.new( seconds: total_seconds, nanoseconds: nanosecond, location: location ) end # Returns a copy of this `Time` representing the same instant in UTC # (`Time::Location::UTC`). # # See `#in` for details. def to_utc : Time if utc? self else Time.utc( seconds: total_seconds, nanoseconds: nanosecond ) end end # Returns a copy of this `Time` representing the same instant in the local # time zone (`Time::Location.local`). # # See `#in` for details. def to_local : Time if local? self else self.in(Location.local) end end private macro def_at_beginning(interval) # Returns a copy of this `Time` representing the beginning of the {{interval.id}}. def at_beginning_of_{{interval.id}} : Time year, month, day, day_year = year_month_day_day_year {{yield}} end end private macro def_at_end(interval) # Returns a copy of this `Time` representing the end of the {{interval.id}}. def at_end_of_{{interval.id}} : Time year, month, day, day_year = year_month_day_day_year {{yield}} end end def_at_beginning(year) { Time.local(year, 1, 1, location: location) } def_at_beginning(semester) { Time.local(year, ((month - 1) // 6) * 6 + 1, 1, location: location) } def_at_beginning(quarter) { Time.local(year, ((month - 1) // 3) * 3 + 1, 1, location: location) } def_at_beginning(month) { Time.local(year, month, 1, location: location) } def_at_beginning(day) { Time.local(year, month, day, location: location) } def_at_beginning(hour) { Time.local(year, month, day, hour, location: location) } # Returns a copy of this `Time` representing the beginning of the minute. def at_beginning_of_minute : Time Time.new(seconds: total_seconds - second, nanoseconds: 0, location: location) end # Returns a copy of this `Time` representing the beginning of the seconds. # # This essentially resets `nanoseconds` to 0. def at_beginning_of_second : Time Time.new(seconds: total_seconds, nanoseconds: 0, location: location) end # Returns a copy of this `Time` representing the beginning of the week. # # The week starts on Monday by default, but can be configured by passing a different `start_day` as a `Time::DayOfWeek`. # # ``` # now = Time.utc(2023, 5, 16, 17, 53, 22) # now.at_beginning_of_week # => 2023-05-15 00:00:00Z # now.at_beginning_of_week(:sunday) # => 2023-05-14 00:00:00Z # now.at_beginning_of_week(:wednesday) # => 2023-05-10 00:00:00Z # ``` def at_beginning_of_week(start_day : Time::DayOfWeek = :monday) : Time self.shift(days: -((day_of_week.value - start_day.value) % 7)).at_beginning_of_day end def_at_end(year) { Time.local(year, 12, 31, 23, 59, 59, nanosecond: 999_999_999, location: location) } # Returns a copy of this `Time` representing the end of the semester. def at_end_of_semester : Time year, month, _, _ = year_month_day_day_year if month <= 6 month, day = 6, 30 else month, day = 12, 31 end Time.local(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) end # Returns a copy of this `Time` representing the end of the quarter. def at_end_of_quarter : Time year, month, _, _ = year_month_day_day_year if month <= 3 month, day = 3, 31 elsif month <= 6 month, day = 6, 30 elsif month <= 9 month, day = 9, 30 else month, day = 12, 31 end Time.local(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) end def_at_end(month) { Time.local(year, month, Time.days_in_month(year, month), 23, 59, 59, nanosecond: 999_999_999, location: location) } # Returns a copy of this `Time` representing the end of the week. def at_end_of_week : Time self.shift(days: 7 - day_of_week.value).at_end_of_day end def_at_end(day) { Time.local(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) } def_at_end(hour) { Time.local(year, month, day, hour, 59, 59, nanosecond: 999_999_999, location: location) } # Returns a copy of this `Time` representing the end of the minute. def at_end_of_minute : Time Time.new(seconds: total_seconds - second + 59, nanoseconds: 999_999_999, location: location) end # Returns a copy of this `Time` representing the end of the second. def at_end_of_second : Time Time.new(seconds: total_seconds, nanoseconds: 999_999_999, location: location) end # Returns a copy of this `Time` representing midday (`12:00`) of the same day. def at_midday : Time year, month, day, _ = year_month_day_day_year Time.local(year, month, day, 12, 0, 0, nanosecond: 0, location: location) end {% for name in DayOfWeek.constants %} # Returns `true` if the day of week is {{name.id}}. # # See `#day_of_week` for details. def {{name.id.downcase}}? : Bool day_of_week.{{name.id.downcase}}? end {% end %} # Returns the number of days from `0001-01-01` to the date indicated # by *year*, *month*, *day* in the proleptic Gregorian calendar. # # The valid range for *year* is `1..9999` and for *month* `1..12`. The value # of *day* is not validated and can exceed the number of days in the specified # month or even a year. protected def self.absolute_days(year, month, day) : Int32 days_per_month = leap_year?(year) ? DAYS_MONTH_LEAP : DAYS_MONTH days_in_year = day - 1 month_index = 1 while month_index < month days_in_year += days_per_month[month_index] month_index += 1 end year -= 1 year * 365 + year // 4 - year // 100 + year // 400 + days_in_year end protected def total_seconds @seconds end protected def offset_seconds @seconds + offset end # Returns the calendaric representation of this instance's date. # # The return value is a tuple consisting of year (`1..9999`), month (`1..12`), # day (`1..31`) and ordinal day of the year (`1..366`). protected def year_month_day_day_year : {Int32, Int32, Int32, Int32} total_days = (offset_seconds // SECONDS_PER_DAY).to_i num400 = total_days // DAYS_PER_400_YEARS total_days -= num400 * DAYS_PER_400_YEARS num100 = total_days // DAYS_PER_100_YEARS if num100 == 4 # leap num100 = 3 end total_days -= num100 * DAYS_PER_100_YEARS num4 = total_days // DAYS_PER_4_YEARS total_days -= num4 * DAYS_PER_4_YEARS numyears = total_days // 365 if numyears == 4 # leap numyears = 3 end total_days -= numyears * 365 year = num400 * 400 + num100 * 100 + num4 * 4 + numyears + 1 ordinal_day_in_year = total_days + 1 if (numyears == 3) && ((num100 == 3) || !(num4 == 24)) # 31 dec leap year days_per_month = DAYS_MONTH_LEAP else days_per_month = DAYS_MONTH end month = 1 while true days_in_month = days_per_month[month] break if total_days < days_in_month total_days -= days_in_month month += 1 end day = total_days + 1 {year, month, day, ordinal_day_in_year} end protected def self.zone_offset_at(seconds, location) unix = seconds - UNIX_EPOCH.total_seconds zone, range = location.lookup_with_boundaries(unix) if zone.offset != 0 case unix - zone.offset when .<(range[0]) zone = location.lookup(range[0] - 1) when .>=(range[1]) zone = location.lookup(range[1]) else # in range end end zone.offset end end require "./time/**"