From 08e05d4a49c1ba1327e3e6821eba1f0c93361ab2 Mon Sep 17 00:00:00 2001
From: Andrew White <andrew.white@unboxed.co>
Date: Fri, 3 Mar 2017 20:12:47 +0000
Subject: Add `Time.rfc3339` parsing method

The `Time.xmlschema` and consequently its alias `iso8601` accepts
timestamps without a offset in contravention of the RFC 3339
standard. This method enforces that constraint and raises an
`ArgumentError` if it doesn't.
---
 activesupport/CHANGELOG.md                         |  8 ++++++
 .../active_support/core_ext/time/calculations.rb   | 23 ++++++++++++++++
 activesupport/test/core_ext/time_ext_test.rb       | 31 ++++++++++++++++++++++
 3 files changed, 62 insertions(+)

(limited to 'activesupport')

diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 8fc22a614d..3cfca555e3 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,11 @@
+*   Add `Time.rfc3339` parsing method
+
+    The `Time.xmlschema` and consequently its alias `iso8601` accepts timestamps
+    without a offset in contravention of the RFC 3339 standard. This method
+    enforces that constraint and raises an `ArgumentError` if it doesn't.
+
+    *Andrew White*
+
 *   Add `ActiveSupport::TimeZone.rfc3339` parsing method
 
     Previously there was no way to get a RFC 3339 timestamp into a specific
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index cbdcb86d6d..7b7aeef25a 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -53,6 +53,29 @@ class Time
     end
     alias_method :at_without_coercion, :at
     alias_method :at, :at_with_coercion
+
+    # Creates a +Time+ instance from an RFC 3339 string.
+    #
+    #   Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000
+    #
+    # If the time or offset components are missing then an +ArgumentError+ will be raised.
+    #
+    #   Time.rfc3339('1999-12-31') # => ArgumentError: invalid date
+    def rfc3339(str)
+      parts = Date._rfc3339(str)
+
+      raise ArgumentError, "invalid date" if parts.empty?
+
+      Time.new(
+        parts.fetch(:year),
+        parts.fetch(:mon),
+        parts.fetch(:mday),
+        parts.fetch(:hour),
+        parts.fetch(:min),
+        parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
+        parts.fetch(:offset)
+      )
+    end
   end
 
   # Returns the number of seconds since 00:00:00.
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index a399e36dc9..8b60494143 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -910,6 +910,37 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
   def test_all_year
     assert_equal Time.local(2011, 1, 1, 0, 0, 0)..Time.local(2011, 12, 31, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_year
   end
+
+  def test_rfc3339_parse
+    time = Time.rfc3339("1999-12-31T19:00:00.125-05:00")
+
+    assert_equal 1999, time.year
+    assert_equal 12, time.month
+    assert_equal 31, time.day
+    assert_equal 19, time.hour
+    assert_equal 0, time.min
+    assert_equal 0, time.sec
+    assert_equal 125000, time.usec
+    assert_equal(-18000, time.utc_offset)
+
+    exception = assert_raises(ArgumentError) do
+      Time.rfc3339("1999-12-31")
+    end
+
+    assert_equal "invalid date", exception.message
+
+    exception = assert_raises(ArgumentError) do
+      Time.rfc3339("1999-12-31T19:00:00")
+    end
+
+    assert_equal "invalid date", exception.message
+
+    exception = assert_raises(ArgumentError) do
+      Time.rfc3339("foobar")
+    end
+
+    assert_equal "invalid date", exception.message
+  end
 end
 
 class TimeExtMarshalingTest < ActiveSupport::TestCase
-- 
cgit v1.2.3