From b5af751508281483845d4323177b0aec98a11880 Mon Sep 17 00:00:00 2001
From: Andrew White <andrew.white@unboxed.co>
Date: Wed, 1 Mar 2017 18:13:15 +0000
Subject: Update `DateTime#change` to support usec and nsec

Adding support for these options now allows us to update the
`DateTime#end_of` methods to match the equivalent `Time#end_of`
methods, e.g:

    datetime = DateTime.now.end_of_day
    datetime.nsec == 999999999 # => true

Fixes #21424.
---
 activerecord/test/cases/date_time_test.rb          |  2 +-
 activesupport/CHANGELOG.md                         | 14 ++++++++++-
 .../core_ext/date_time/calculations.rb             | 18 +++++++++++---
 activesupport/test/core_ext/date_time_ext_test.rb  | 29 ++++++++++++++--------
 4 files changed, 46 insertions(+), 17 deletions(-)

diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 3bc08f80ec..ad7da9de70 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -52,7 +52,7 @@ class DateTimeTest < ActiveRecord::TestCase
   end
 
   def test_assign_in_local_timezone
-    now = DateTime.now
+    now = DateTime.civil(2017, 3, 1, 12, 0, 0)
     with_timezone_config default: :local do
       task = Task.new starting: now
       assert_equal now, task.starting
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 0614cdaabd..2999820c42 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,15 @@
+*   Update `DateTime#change` to support `:usec` and `:nsec` options.
+
+    Adding support for these options now allows us to update the `DateTime#end_of`
+    methods to match the equivalent `Time#end_of` methods, e.g:
+
+        datetime = DateTime.now.end_of_day
+        datetime.nsec == 999999999 # => true
+
+    Fixes #21424.
+
+    *Dan Moore*, *Andrew White*
+
 *   Add `ActiveSupport::Duration#before` and `#after` as aliases for `#until` and `#since`
 
     These read more like English and require less mental gymnastics to read and write.
@@ -20,7 +32,7 @@
     *Robin Dupret* (#28157)
 
 *   In Core Extensions, make `MarshalWithAutoloading#load` pass through the second, optional
-    argument for `Marshal#load( source [, proc] )`. This way we don't have to do 
+    argument for `Marshal#load( source [, proc] )`. This way we don't have to do
     `Marshal.method(:load).super_method.call(source, proc)` just to be able to pass a proc.
 
     *Jeff Latz*
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 70d5c9af8e..7a9eb8c266 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -47,13 +47,23 @@ class DateTime
   #   DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1)  # => DateTime.new(1981, 8, 1, 22, 35, 0)
   #   DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
   def change(options)
+    if new_nsec = options[:nsec]
+      raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
+      new_fraction = Rational(new_nsec, 1000000000)
+    else
+      new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+      new_fraction = Rational(new_usec, 1000000)
+    end
+
+    raise ArgumentError, "argument out of range" if new_fraction >= 1
+
     ::DateTime.civil(
       options.fetch(:year, year),
       options.fetch(:month, month),
       options.fetch(:day, day),
       options.fetch(:hour, hour),
       options.fetch(:min, options[:hour] ? 0 : min),
-      options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction),
+      options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
       options.fetch(:offset, offset),
       options.fetch(:start, start)
     )
@@ -122,7 +132,7 @@ class DateTime
 
   # Returns a new DateTime representing the end of the day (23:59:59).
   def end_of_day
-    change(hour: 23, min: 59, sec: 59)
+    change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
   end
   alias :at_end_of_day :end_of_day
 
@@ -134,7 +144,7 @@ class DateTime
 
   # Returns a new DateTime representing the end of the hour (hh:59:59).
   def end_of_hour
-    change(min: 59, sec: 59)
+    change(min: 59, sec: 59, usec: Rational(999999999, 1000))
   end
   alias :at_end_of_hour :end_of_hour
 
@@ -146,7 +156,7 @@ class DateTime
 
   # Returns a new DateTime representing the end of the minute (hh:mm:59).
   def end_of_minute
-    change(sec: 59)
+    change(sec: 59, usec: Rational(999999999, 1000))
   end
   alias :at_end_of_minute :end_of_minute
 
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index e3b31c10f5..36f0ee22b8 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -4,8 +4,8 @@ require "core_ext/date_and_time_behavior"
 require "time_zone_test_helpers"
 
 class DateTimeExtCalculationsTest < ActiveSupport::TestCase
-  def date_time_init(year, month, day, hour, minute, second, *args)
-    DateTime.civil(year, month, day, hour, minute, second)
+  def date_time_init(year, month, day, hour, minute, second, usec = 0)
+    DateTime.civil(year, month, day, hour, minute, second + (usec / 1000000))
   end
 
   include DateAndTimeBehavior
@@ -113,7 +113,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
   end
 
   def test_end_of_day
-    assert_equal DateTime.civil(2005, 2, 4, 23, 59, 59), DateTime.civil(2005, 2, 4, 10, 10, 10).end_of_day
+    assert_equal DateTime.civil(2005, 2, 4, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 10, 10, 10).end_of_day
   end
 
   def test_beginning_of_hour
@@ -121,7 +121,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
   end
 
   def test_end_of_hour
-    assert_equal DateTime.civil(2005, 2, 4, 19, 59, 59), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_hour
+    assert_equal DateTime.civil(2005, 2, 4, 19, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_hour
   end
 
   def test_beginning_of_minute
@@ -129,13 +129,13 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
   end
 
   def test_end_of_minute
-    assert_equal DateTime.civil(2005, 2, 4, 19, 30, 59), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_minute
+    assert_equal DateTime.civil(2005, 2, 4, 19, 30, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_minute
   end
 
   def test_end_of_month
-    assert_equal DateTime.civil(2005, 3, 31, 23, 59, 59), DateTime.civil(2005, 3, 20, 10, 10, 10).end_of_month
-    assert_equal DateTime.civil(2005, 2, 28, 23, 59, 59), DateTime.civil(2005, 2, 20, 10, 10, 10).end_of_month
-    assert_equal DateTime.civil(2005, 4, 30, 23, 59, 59), DateTime.civil(2005, 4, 20, 10, 10, 10).end_of_month
+    assert_equal DateTime.civil(2005, 3, 31, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 3, 20, 10, 10, 10).end_of_month
+    assert_equal DateTime.civil(2005, 2, 28, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 20, 10, 10, 10).end_of_month
+    assert_equal DateTime.civil(2005, 4, 30, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 4, 20, 10, 10, 10).end_of_month
   end
 
   def test_last_year
@@ -162,12 +162,19 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
     assert_equal DateTime.civil(2006, 2, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2006)
     assert_equal DateTime.civil(2005, 6, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(month: 6)
     assert_equal DateTime.civil(2012, 9, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9)
-    assert_equal DateTime.civil(2005, 2, 22, 16),       DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16)
-    assert_equal DateTime.civil(2005, 2, 22, 16, 45),    DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
-    assert_equal DateTime.civil(2005, 2, 22, 15, 45),    DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45)
+    assert_equal DateTime.civil(2005, 2, 22, 16),         DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16)
+    assert_equal DateTime.civil(2005, 2, 22, 16, 45),     DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
+    assert_equal DateTime.civil(2005, 2, 22, 15, 45),     DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45)
 
     # datetime with fractions of a second
     assert_equal DateTime.civil(2005, 2, 1, 15, 15, 10.7), DateTime.civil(2005, 2, 22, 15, 15, 10.7).change(day: 1)
+    assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(usec: 8)
+    assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(nsec: 8000)
+    assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1, nsec: 1) }
+    assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1000000) }
+    assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 1000000000) }
+    assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 999999) }
+    assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 999999999) }
   end
 
   def test_advance
-- 
cgit v1.2.3