Skip to content

Commit

Permalink
Add Time#shift with calendrical arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
straight-shoota committed Sep 14, 2018
1 parent 156b4f5 commit 50a2da8
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 46 deletions.
79 changes: 61 additions & 18 deletions spec/std/time/time_spec.cr
Expand Up @@ -168,6 +168,33 @@ describe Time do
time.shift(0, 0).should eq time
end

describe "irregular calendrical unit ratios" do
it "shifts by a week if one day is left out" do
# The week from 2011-12-25 to 2012-01-01 for example lasted only 6 days in Samoa,
# because it skipped 2011-12-28 due to changing time zone from -11:00 to +13:00.
with_zoneinfo do
samoa = Time::Location.load("Pacific/Apia")
start = Time.new(2011, 12, 25, 0, 0, 0, location: samoa)

plus_one_week = start.shift days: 7
plus_one_week.should eq start + 6.days

plus_one_year = start.shift years: 1
plus_one_year.should eq start + 365.days # 2012 is a leap year so it should've been 366 days, but 2011-12-28 was skipped
end
end

it "shifts by conceptual hour even if elapsed time is less" do
# Venezuela switched from -4:30 to -4:00 on 2016-05-01, the hour between 2:00 and 3:00 lasted only 30 minutes
with_zoneinfo do
venezuela = Time::Location.load("America/Caracas")
start = Time.new(2016, 5, 1, 2, 0, 0, location: venezuela)
plus_one_hour = start.shift hours: 1
plus_one_hour.should eq start + 30.minutes
end
end
end

describe "adds days" do
it "simple" do
time = Time.utc(2002, 2, 25, 15, 25, 13)
Expand All @@ -182,13 +209,13 @@ describe Time do
time.should eq Time.utc(2002, 3, 2, 17, 49, 13)
end

pending "over dst" do
it "over dst" do
with_zoneinfo do
location = Time::Location.load("Europe/Berlin")
reference = Time.new(2017, 10, 28, 13, 37, location: location)
next_day = Time.new(2017, 10, 29, 13, 37, location: location)
next_day = reference.shift days: 1

(reference + 1.day).should eq next_day
next_day.should eq reference + 25.hours
end
end

Expand All @@ -199,44 +226,59 @@ describe Time do
end
end

pending "out of range max (shift days)" do
# this will be fixed with raise on overflow
time = Time.utc(2002, 2, 25, 15, 25, 13)
expect_raises ArgumentError do
time.shift days: 10000000
end
end

it "out of range min" do
time = Time.utc(2002, 2, 25, 15, 25, 13)
expect_raises ArgumentError do
time - 10000000.days
end
end

pending "out of range min (shift days)" do
# this will be fixed with raise on overflow
time = Time.utc(2002, 2, 25, 15, 25, 13)
expect_raises ArgumentError do
time.shift days: -10000000
end
end
end

it "adds months" do
t = Time.utc 2014, 10, 30, 21, 18, 13

t2 = t + 1.month
t2 = t.shift months: 1
t2.should eq Time.utc(2014, 11, 30, 21, 18, 13)

t2 = t + 1.months
t2 = t.shift months: 1
t2.should eq Time.utc(2014, 11, 30, 21, 18, 13)

t = Time.utc 2014, 10, 31, 21, 18, 13
t2 = t + 1.month
t2 = t.shift months: 1
t2.should eq Time.utc(2014, 11, 30, 21, 18, 13)

t = Time.utc 2014, 10, 31, 21, 18, 13
t2 = t - 1.month
t2 = t.shift months: -1
t2.should eq Time.utc(2014, 9, 30, 21, 18, 13)

t = Time.utc 2014, 10, 31, 21, 18, 13
t2 = t + 6.month
t2 = t.shift months: 6
t2.should eq Time.utc(2015, 4, 30, 21, 18, 13)
end

it "adds years" do
t = Time.utc 2014, 10, 30, 21, 18, 13

t2 = t + 1.year
t2 = t.shift years: 1
t2.should eq Time.utc(2015, 10, 30, 21, 18, 13)

t = Time.utc 2014, 10, 30, 21, 18, 13
t2 = t - 2.years
t2 = t.shift years: -2
t2.should eq Time.utc(2012, 10, 30, 21, 18, 13)
end

Expand All @@ -254,15 +296,16 @@ describe Time do
end

it "adds nanoseconds" do
time = Time.utc(2002, 2, 25, 15, 25, 13)
time = time + 1e16.nanoseconds
time.should eq Time.utc(2002, 6, 21, 9, 11, 53)
t1 = Time.utc(2002, 2, 25, 15, 25, 13)
t1 = t1.shift nanoseconds: 10_000_000_000_000_000

t1.should eq Time.utc(2002, 6, 21, 9, 11, 53)

time = time - 19e16.nanoseconds
time.should eq Time.utc(1996, 6, 13, 7, 25, 13)
t1 = t1.shift nanoseconds: -190_000_000_000_000_000
t1.should eq Time.utc(1996, 6, 13, 7, 25, 13)

time = time + 15_623_487.nanoseconds
time.should eq Time.utc(1996, 6, 13, 7, 25, 13, nanosecond: 15_623_487)
t1 = t1.shift nanoseconds: 15_623_000
t1.should eq Time.utc(1996, 6, 13, 7, 25, 13, nanosecond: 15_623_000)
end

it "preserves location when adding" do
Expand Down
124 changes: 96 additions & 28 deletions src/time.cr
Expand Up @@ -562,7 +562,7 @@ struct Time
# 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
add_months span.value
shift months: span.value.to_i
end

# Returns a copy of this `Time` with *span* subtracted.
Expand All @@ -580,37 +580,15 @@ struct Time
# 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
add_months -span.value
shift months: -span.value.to_i
end

private def add_months(months)
day = self.day
month = self.month + months.remainder(12)
year = self.year + months.tdiv(12)

if month < 1
month = 12 + month
year -= 1
elsif month > 12
month = month - 12
year += 1
end

maxday = Time.days_in_month(year, month)
if day > maxday
day = maxday
end

temp = Time.new(year, month, day, location: location)
temp + time_of_day
end

# Returns a copy of this `Time` with the number of *seconds* and
# *nanoseconds* added.
# 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 eqivalent of
# 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.
Expand Down Expand Up @@ -641,6 +619,96 @@ struct Time
Time.new(seconds: seconds, nanoseconds: nanoseconds.to_i, location: location)
end

# Returns a copy of this `Time` shifted by the amount of calendrical 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
# represenation of the result will be apart by the specified amounts, but the
# elapsed time between both instances might not equal to the combined default
# durations
# This is the case for example when adding a day over a daylight-savings time
# change:
#
# ```
# start = Time.new(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-07-31` would result in the invalid
# date `2018-08-31` which will be adjusted to `2018-08-30`:
# ```
# Time.utc(2018, 7, 31).shift(months: 1) # => Time.utc(2018, 8, 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, 7, 31).shift(months: 1, days: -1) # => Time.utc(2018, 8, 29)
# Time.utc(2018, 7, 31).shift(months: 1).shift(days: -1) # => Time.utc(2018, 8, 29)
# Time.utc(2018, 7, 31).shift(days: -1).shift(months: 1) # => Time.utc(2018, 8, 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, _ = to_utc.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*.
Expand Down Expand Up @@ -1246,7 +1314,7 @@ struct Time
@seconds + offset
end

private def year_month_day_day_year
protected def year_month_day_day_year
m = 1

days = DAYS_MONTH
Expand Down

0 comments on commit 50a2da8

Please sign in to comment.