diff --git a/Gemfile b/Gemfile index 4276aeca..7533e4af 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ end group :benchmarks do platform :mri do + gem "activemodel" gem "attrio" gem "benchmark-ips" gem "fast_attributes" diff --git a/benchmarks/coercions_date.rb b/benchmarks/coercions_date.rb new file mode 100644 index 00000000..2a111ebc --- /dev/null +++ b/benchmarks/coercions_date.rb @@ -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 diff --git a/benchmarks/coercions_time.rb b/benchmarks/coercions_time.rb new file mode 100644 index 00000000..db71ae2a --- /dev/null +++ b/benchmarks/coercions_time.rb @@ -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 diff --git a/lib/dry/types/coercions.rb b/lib/dry/types/coercions.rb index 8f6965fb..6d6def88 100644 --- a/lib/dry/types/coercions.rb +++ b/lib/dry/types/coercions.rb @@ -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 @@ -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 @@ -102,6 +102,36 @@ def to_symbol(input, &block) def empty_str?(value) EMPTY_STRING.eql?(value) end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/.freeze + 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.freeze + 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