diff --git a/CHANGELOG.MD b/CHANGELOG.MD index f8006bb..778e70c 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,13 @@ +## Subroutine 4.1.4 + +Fields using the time/timestamp/datetime caster will now default back to the old behavior, and use a `precision:` option to opt-in to the new behavior introduced in `v4.1.1`. + +`precision: :seconds` will retain the old behavior of always parsing to a new Time object +with floored sub-second precision, but applied more forcefully than before as it would have parsed whatever you passed to it. (This is the default, now.) + +`precision: :high` will now use the new functionality of re-using Time objects when they +are passed in, or if not parsing exactly the provided string as to a Time object. + ## Subroutine 4.1.1 Fields using the time/timestamp/datetime caster will now return exactly the passed in value diff --git a/lib/subroutine/type_caster.rb b/lib/subroutine/type_caster.rb index c8ea847..50a2130 100644 --- a/lib/subroutine/type_caster.rb +++ b/lib/subroutine/type_caster.rb @@ -4,11 +4,14 @@ require 'time' require 'bigdecimal' require 'securerandom' +require 'active_support/core_ext/date_time/acts_like' +require 'active_support/core_ext/date_time/calculations' require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/time/acts_like' +require 'active_support/core_ext/time/calculations' module Subroutine module TypeCaster @@ -117,13 +120,23 @@ def self.cast(value, options = {}) ::Date.parse(String(value)) end -::Subroutine::TypeCaster.register :time, :timestamp, :datetime do |value, _options = {}| +::Subroutine::TypeCaster.register :time, :timestamp, :datetime do |value, options = {}| next nil unless value.present? - if value.try(:acts_like?, :time) - value - else - ::Time.parse(String(value)) + if options[:precision] == :high + if value.try(:acts_like?, :time) + value.to_time + else + ::Time.parse(String(value)) + end + else # precision == :seconds + time = if value.try(:acts_like?, :time) + value.to_time + else + ::Time.parse(String(value)) + end + + time.change(usec: 0) end end diff --git a/lib/subroutine/version.rb b/lib/subroutine/version.rb index 0eb2a84..5d5c4a6 100644 --- a/lib/subroutine/version.rb +++ b/lib/subroutine/version.rb @@ -4,7 +4,7 @@ module Subroutine MAJOR = 4 MINOR = 1 - PATCH = 3 + PATCH = 4 PRE = nil VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".") diff --git a/test/subroutine/type_caster_test.rb b/test/subroutine/type_caster_test.rb index 692e0cc..2369323 100644 --- a/test/subroutine/type_caster_test.rb +++ b/test/subroutine/type_caster_test.rb @@ -269,7 +269,7 @@ def test_date_inputs assert_nil op.date_input end - def test_time_inputs + def test_time_inputs__with_seconds_precision op.time_input = nil assert_nil op.time_input @@ -284,22 +284,139 @@ def test_time_inputs assert_equal 0, op.time_input.min assert_equal 0, op.time_input.sec + op.time_input = ::DateTime.new(2022, 12, 22) + assert_equal ::Time, op.time_input.class + refute_equal ::DateTime, op.time_input.class + + assert_equal 0, op.time_input.utc_offset + assert_equal 2022, op.time_input.year + assert_equal 12, op.time_input.month + assert_equal 22, op.time_input.day + assert_equal 0, op.time_input.hour + assert_equal 0, op.time_input.min + assert_equal 0, op.time_input.sec + op.time_input = '2023-05-05T10:00:30.123456Z' assert_equal ::Time, op.time_input.class refute_equal ::DateTime, op.time_input.class + assert_equal 0, op.time_input.utc_offset + assert_equal 2023, op.time_input.year + assert_equal 5, op.time_input.month + assert_equal 5, op.time_input.day + assert_equal 10, op.time_input.hour + assert_equal 0, op.time_input.min + assert_equal 30, op.time_input.sec + assert_equal 0, op.time_input.usec + + op.time_input = '2023-05-05T10:00:30Z' + assert_equal ::Time, op.time_input.class + assert_equal 0, op.time_input.utc_offset assert_equal 2023, op.time_input.year assert_equal 5, op.time_input.month assert_equal 5, op.time_input.day assert_equal 10, op.time_input.hour assert_equal 0, op.time_input.min assert_equal 30, op.time_input.sec - assert_equal 123456, op.time_input.usec + assert_equal 0, op.time_input.usec - time = Time.at(1678741605.123456) + op.time_input = '2024-11-11T16:42:23.246+0100' + assert_equal ::Time, op.time_input.class + assert_equal 3600, op.time_input.utc_offset + assert_equal 2024, op.time_input.year + assert_equal 11, op.time_input.month + assert_equal 11, op.time_input.day + assert_equal 16, op.time_input.hour + assert_equal 42, op.time_input.min + assert_equal 23, op.time_input.sec + assert_equal 0, op.time_input.usec + + time = Time.at(1678741605.123456).utc op.time_input = time - assert_equal time, op.time_input - assert_equal time.object_id, op.time_input.object_id + refute_equal time, op.time_input + refute_equal time.object_id, op.time_input.object_id + assert_equal 2023, op.time_input.year + assert_equal 3, op.time_input.month + assert_equal 13, op.time_input.day + assert_equal 21, op.time_input.hour + assert_equal 6, op.time_input.min + assert_equal 45, op.time_input.sec + assert_equal 0, op.time_input.usec + end + + def test_time_inputs__with_high_precision + op.precise_time_input = nil + assert_nil op.precise_time_input + + op.precise_time_input = '2022-12-22' + assert_equal ::Time, op.precise_time_input.class + refute_equal ::DateTime, op.precise_time_input.class + + assert_equal 2022, op.precise_time_input.year + assert_equal 12, op.precise_time_input.month + assert_equal 22, op.precise_time_input.day + assert_equal 0, op.precise_time_input.hour + assert_equal 0, op.precise_time_input.min + assert_equal 0, op.precise_time_input.sec + + op.precise_time_input = ::DateTime.new(2022, 12, 22) + assert_equal ::Time, op.precise_time_input.class + refute_equal ::DateTime, op.precise_time_input.class + + assert_equal 0, op.precise_time_input.utc_offset + assert_equal 2022, op.precise_time_input.year + assert_equal 12, op.precise_time_input.month + assert_equal 22, op.precise_time_input.day + assert_equal 0, op.precise_time_input.hour + assert_equal 0, op.precise_time_input.min + assert_equal 0, op.precise_time_input.sec + + op.precise_time_input = '2023-05-05T10:00:30.123456Z' + assert_equal ::Time, op.precise_time_input.class + refute_equal ::DateTime, op.precise_time_input.class + + assert_equal 0, op.precise_time_input.utc_offset + assert_equal 2023, op.precise_time_input.year + assert_equal 5, op.precise_time_input.month + assert_equal 5, op.precise_time_input.day + assert_equal 10, op.precise_time_input.hour + assert_equal 0, op.precise_time_input.min + assert_equal 30, op.precise_time_input.sec + assert_equal 123456, op.precise_time_input.usec + + op.precise_time_input = '2023-05-05T10:00:30Z' + assert_equal ::Time, op.precise_time_input.class + assert_equal 0, op.precise_time_input.utc_offset + assert_equal 2023, op.precise_time_input.year + assert_equal 5, op.precise_time_input.month + assert_equal 5, op.precise_time_input.day + assert_equal 10, op.precise_time_input.hour + assert_equal 0, op.precise_time_input.min + assert_equal 30, op.precise_time_input.sec + assert_equal 0, op.precise_time_input.usec + + op.precise_time_input = '2024-11-11T16:42:23.246+0100' + assert_equal ::Time, op.precise_time_input.class + assert_equal 3600, op.precise_time_input.utc_offset + assert_equal 2024, op.precise_time_input.year + assert_equal 11, op.precise_time_input.month + assert_equal 11, op.precise_time_input.day + assert_equal 16, op.precise_time_input.hour + assert_equal 42, op.precise_time_input.min + assert_equal 23, op.precise_time_input.sec + assert_equal 246000, op.precise_time_input.usec + + time = Time.at(1678741605.123456).utc + op.precise_time_input = time + assert_equal time, op.precise_time_input + assert_equal time.object_id, op.precise_time_input.object_id + assert_equal 2023, op.precise_time_input.year + assert_equal 3, op.precise_time_input.month + assert_equal 13, op.precise_time_input.day + assert_equal 21, op.precise_time_input.hour + assert_equal 6, op.precise_time_input.min + assert_equal 45, op.precise_time_input.sec + assert_equal 123456, op.precise_time_input.usec end def test_iso_date_inputs diff --git a/test/support/ops.rb b/test/support/ops.rb index 73acd6a..f5a2bc4 100644 --- a/test/support/ops.rb +++ b/test/support/ops.rb @@ -173,6 +173,7 @@ class TypeCastOp < ::Subroutine::Op boolean :boolean_input date :date_input time :time_input, default: -> { Time.now } + time :precise_time_input, precision: :high iso_date :iso_date_input iso_time :iso_time_input object :object_input