Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(434) strptime_with_mock_date :: Fix year boundary #437

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/timecop/time_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ def strptime_with_mock_date(str = '-4712-01-01', fmt = '%F', start = Date::ITALY

d = Date._strptime(str, fmt)
now = Time.now.to_date
year = d[:year] || d[:cwyear] || now.year

# If "current" time falls near a year boundary, with a year-ambiguous str, we need to explicitly handle it
cwday = d[:cwday] && (now.to_date + (d[:cwday] - now.to_date.wday))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the to_date calls on now are not necessary. it will be easier to read without those.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

wday = d[:wday] && (now.to_date + (d[:wday] - now.to_date.wday))

year = d[:year] || d[:cwyear] || (cwday || wday || now).year
mon = d[:mon] || now.mon
if d.keys == [:year]
Date.new(year, 1, 1, start)
Expand All @@ -59,7 +64,7 @@ def strptime_with_mock_date(str = '-4712-01-01', fmt = '%F', start = Date::ITALY
elsif d[:yday]
Date.new(year, 1, 1, start).next_day(d[:yday] - 1)
elsif d[:cwyear] || d[:cweek] || d[:wnum0] || d[:wnum1] || d[:wday] || d[:cwday]
week = d[:cweek] || d[:wnum1] || d[:wnum0] || now.strftime('%W').to_i
week = d[:cweek] || d[:wnum1] || d[:wnum0] || (cwday || wday || now).strftime('%W').to_i
if d[:wnum0] #Week of year where week starts on sunday
if d[:cwday] #monday based day of week
Date.strptime_without_mock_date("#{year} #{week} #{d[:cwday]}", '%Y %U %u', start)
Expand Down
36 changes: 36 additions & 0 deletions test/date_strptime_year_boundary_scenarios.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module DateStrptimeYearBoundaryScenarios
#calling freeze and travel tests are making the date Time.local(1992,1,1)

# Test for wday
def test_date_strptime_year_boundary_with_day_of_week
assert_equal Date.strptime('Thursday', '%A'), Date.new(1992, 1, 2)
assert_equal Date.strptime('Monday', '%A'), Date.new(1991, 12, 30)
end

# Test for cwday
def test_date_strptime_year_boundary_with_iso_day_of_week
assert_equal Date.strptime('4', '%u'), Date.new(1992, 1, 2)
assert_equal Date.strptime('1', '%u'), Date.new(1991, 12, 30)
end

# Test for wnum1
def test_date_strptime_year_boundary_with_wnum1
assert_equal Date.strptime('52', '%W'), Date.new(1992, 12, 28)
assert_equal Date.strptime('1', '%W'), Date.new(1992, 01, 6)
end

# Test for wnum0
def test_date_strptime_year_boundary_with_wnum0
assert_equal Date.strptime('52', '%U'), Date.new(1992, 12, 27)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date.new(1992,12,27) is sort of a magic number, I had to do research to understand why it was the right date. What do you think of using Date.strptime('1992-52', '%Y-%U') instead of newing up that date? This comment is general to all the magic dates in here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW if you implement this idea then for the cweek tests you have to use %G not %Y like Date.strptime('1992-52', '%G-%V'). I spent a bit of time scratching my head at this

3.2.2 :016 > Date.strptime('1992-53', '%Y-%V')
 => #<Date: 1992-01-01 ((2448623j,0s,0n),+0s,2299161j)>

before i realized that the iso8601 commercial week has its own year concept too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date.new(1992,12,27) is sort of a magic number, I had to do research to understand why it was the right date. What do you think of using Date.strptime('1992-52', '%Y-%U') instead of newing up that date? This comment is general to all the magic dates in here.

I like this idea. 2 caveats:

  • We would lose some coverage if we compared the output of this function to another output of this function. e.g. what if a future change breaks how the year is computed more broadly in strptime which impacts both sides of the assertion? In that scenario, we'd be blind to other types of changes. we could cover those with addl tests, but I think there's some advantage to using explicit constants in these tests.
  • We use magic numbers in our other strptime tests. We could update these though, but would also introduce the same risk.

For these specific tests, if I don't change the other strptime tests, then I think we could safely improve readability making the change you suggested.

assert_equal Date.strptime('1', '%U'), Date.new(1992, 01, 5)
end

# Test for cweek
def test_date_strptime_year_boundary_with_cweek
# TODO: year-ambiguous cweek is not handled correctly during year boundaries:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not understand this TODO.

Copy link
Author

@jchapa jchapa Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you referenced this in your other comment, but I think in general we're just not handling cweek well in this library. I'm not sure if that's something we want to tackle in this PR.

Some examples (from master branch):

irb(main):003> Date.strptime('1992-52', '%G-%V')
=> #<Date: 1992-12-28 ((2448985j,0s,0n),+0s,2299161j)>
irb(main):004> Date.strptime_without_mock_date('1992-52', '%G-%V')
=> #<Date: 1992-12-21 ((2448978j,0s,0n),+0s,2299161j)>
irb(main):001> Date.strptime('52', '%V')
=> #<Date: 2025-12-29 ((2461039j,0s,0n),+0s,2299161j)>
irb(main):002> Date.strptime_without_mock_date('52', '%V')
=> #<Date: 2025-12-22 ((2461032j,0s,0n),+0s,2299161j)>

Going to poke at this a little more and hopefully find a cleaner solve

# assert_equal Date.strptime('52', '%V'), Date.new(1992, 12, 21)
# assert_equal Date.strptime('53', '%V'), Date.new(1991, 12, 28)
assert_equal Date.strptime('52', '%V'), Date.new(1992, 12, 28)
assert_equal Date.strptime('1', '%V'), Date.new(1992, 01, 6)
end
end
17 changes: 17 additions & 0 deletions test/timecop_date_strptime_freeze_year_boundary_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require_relative "test_helper"
require 'timecop'
require_relative 'date_strptime_year_boundary_scenarios'

class TestTimecop < Minitest::Test
def setup
t = Time.local(1992,1,1)
Timecop.freeze(t)
end

def teardown
Timecop.return
end

# Test for Date
include DateStrptimeYearBoundaryScenarios
end
18 changes: 18 additions & 0 deletions test/timecop_date_strptime_travel_year_boundary_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require_relative "test_helper"
require 'timecop'
require_relative 'date_strptime_year_boundary_scenarios'

class TestTimecop < Minitest::Test
def setup
t = Time.local(1992,1,1)
Timecop.travel(t)
end

def teardown
Timecop.return
end

# Test for Date
include DateStrptimeYearBoundaryScenarios

end
Loading