Skip to content

Commit

Permalink
fast coercion date and time
Browse files Browse the repository at this point in the history
  • Loading branch information
ermolaev committed Feb 2, 2021
1 parent 35fa7b3 commit 51fafe5
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 2 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ group :benchmarks do
gem "fast_attributes"
gem "hotch"
gem "virtus"
gem "activemodel"
end
end
48 changes: 48 additions & 0 deletions benchmarks/coercions_date.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require "active_model"
require "benchmark/ips"
require "dry/types"

am = ActiveModel::Type::Date.new
dry = Dry::Types["params.date"]

["2020-01-20", "3rd Feb 2001"].each do |d|
Benchmark.ips do |x|
x.report("DRY #{d}") do |n|
while n > 0
dry[d]
n -= 1
end
end

x.report("AM #{d}") do |n|
while n > 0
am.cast(d)
n -= 1
end
end

x.compare!
end
end

# before
#
# Comparison:
# AM 2020-01-20: 712594.2 i/s
# DRY 2020-01-20: 234735.9 i/s - 3.04x (± 0.00) slower
#
# Comparison:
# DRY 3rd Feb 2001: 148000.4 i/s
# AM 3rd Feb 2001: 140262.5 i/s - same-ish: difference falls within error

# after
#
# Comparison:
# AM 2020-01-20: 694511.8 i/s
# DRY 2020-01-20: 692906.6 i/s - same-ish: difference falls within error
#
# Comparison:
# DRY 3rd Feb 2001: 141146.9 i/s
# AM 3rd Feb 2001: 139686.5 i/s - same-ish: difference falls within error
60 changes: 60 additions & 0 deletions benchmarks/coercions_time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require "benchmark/ips"
require "dry/types"
require "active_model"
require "active_support/core_ext/time/zones"
require 'pry'

::Time.zone_default = "Moscow"
am = ActiveModel::Type::Time.new
dry = Dry::Types["params.time"]

["2020-01-20 19:40:22", "2021-02-03T00:10:54.597+03:00", "Thu Nov 29 14:33:20 2001"].each do |d|
Benchmark.ips do |x|
x.report("DRY #{d}") do |n|
while n > 0
dry[d]
n -= 1
end
end

x.report("AM #{d}") do |n|
while n > 0
am.cast(d)
n -= 1
end
end

x.compare!
end
end


# before
#
# Comparison:
# AM 2020-01-20 19:40:22: 130660.9 i/s
# DRY 2020-01-20 19:40:22: 58853.9 i/s - 2.22x (± 0.00) slower
#
# Comparison:
# DRY 2021-02-03T00:10:54.597+03:00: 52110.0 i/s
# AM 2021-02-03T00:10:54.597+03:00: 39652.9 i/s - 1.31x (± 0.00) slower
#
# Comparison:
# DRY Thu Nov 29 14:33:20 2001: 44819.1 i/s
# AM Thu Nov 29 14:33:20 2001: 33064.5 i/s - 1.36x (± 0.00) slower

# after
#
# Comparison:
# DRY 2020-01-20 19:40:22: 190951.9 i/s
# AM 2020-01-20 19:40:22: 131920.6 i/s - 1.45x (± 0.00) slower
#
# Comparison:
# DRY 2021-02-03T00:10:54.597+03:00: 157549.5 i/s
# AM 2021-02-03T00:10:54.597+03:00: 40502.8 i/s - 3.89x (± 0.00) slower
#
# Comparison:
# DRY Thu Nov 29 14:33:20 2001: 44376.3 i/s
# AM Thu Nov 29 14:33:20 2001: 33955.3 i/s - 1.31x (± 0.00) slower
35 changes: 33 additions & 2 deletions lib/dry/types/coercions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module Coercions
def to_date(input, &block)
if input.respond_to?(:to_str)
begin
::Date.parse(input)
fast_string_to_date(input) || ::Date.parse(input)
rescue ArgumentError, RangeError => e
CoercionError.handle(e, &block)
end
Expand Down Expand Up @@ -64,7 +64,7 @@ def to_date_time(input, &block)
def to_time(input, &block)
if input.respond_to?(:to_str)
begin
::Time.parse(input)
fast_string_to_time(input) || ::Time.parse(input)
rescue ArgumentError => e
CoercionError.handle(e, &block)
end
Expand Down Expand Up @@ -102,6 +102,37 @@ def to_symbol(input, &block)
def empty_str?(value)
EMPTY_STRING.eql?(value)
end

ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
def fast_string_to_date(string)
if string =~ ISO_DATE
::Date.new $1.to_i, $2.to_i, $3.to_i
end
end

ISO_DATETIME = /
\A
(\d{4})-(\d\d)-(\d\d)(?:T|\s) # 2020-06-20T
(\d\d):(\d\d):(\d\d)(?:\.(\d{1,6})\d*)? # 10:20:30.123456
(?:(Z(?=\z)|[+-]\d\d)(?::?(\d\d))?)? # +09:00
\z
/x
def fast_string_to_time(string)
return unless ISO_DATETIME =~ string

usec = $7.to_i
usec_len = $7&.length
if usec_len&.< 6
usec *= 10**(6 - usec_len)
end

if $8
offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
end

::Time.local($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
end

end
end
end

0 comments on commit 51fafe5

Please sign in to comment.