Skip to content

Commit 51fafe5

Browse files
committed
fast coercion date and time
1 parent 35fa7b3 commit 51fafe5

File tree

4 files changed

+142
-2
lines changed

4 files changed

+142
-2
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ group :benchmarks do
2323
gem "fast_attributes"
2424
gem "hotch"
2525
gem "virtus"
26+
gem "activemodel"
2627
end
2728
end

benchmarks/coercions_date.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require "active_model"
4+
require "benchmark/ips"
5+
require "dry/types"
6+
7+
am = ActiveModel::Type::Date.new
8+
dry = Dry::Types["params.date"]
9+
10+
["2020-01-20", "3rd Feb 2001"].each do |d|
11+
Benchmark.ips do |x|
12+
x.report("DRY #{d}") do |n|
13+
while n > 0
14+
dry[d]
15+
n -= 1
16+
end
17+
end
18+
19+
x.report("AM #{d}") do |n|
20+
while n > 0
21+
am.cast(d)
22+
n -= 1
23+
end
24+
end
25+
26+
x.compare!
27+
end
28+
end
29+
30+
# before
31+
#
32+
# Comparison:
33+
# AM 2020-01-20: 712594.2 i/s
34+
# DRY 2020-01-20: 234735.9 i/s - 3.04x (± 0.00) slower
35+
#
36+
# Comparison:
37+
# DRY 3rd Feb 2001: 148000.4 i/s
38+
# AM 3rd Feb 2001: 140262.5 i/s - same-ish: difference falls within error
39+
40+
# after
41+
#
42+
# Comparison:
43+
# AM 2020-01-20: 694511.8 i/s
44+
# DRY 2020-01-20: 692906.6 i/s - same-ish: difference falls within error
45+
#
46+
# Comparison:
47+
# DRY 3rd Feb 2001: 141146.9 i/s
48+
# AM 3rd Feb 2001: 139686.5 i/s - same-ish: difference falls within error

benchmarks/coercions_time.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require "benchmark/ips"
4+
require "dry/types"
5+
require "active_model"
6+
require "active_support/core_ext/time/zones"
7+
require 'pry'
8+
9+
::Time.zone_default = "Moscow"
10+
am = ActiveModel::Type::Time.new
11+
dry = Dry::Types["params.time"]
12+
13+
["2020-01-20 19:40:22", "2021-02-03T00:10:54.597+03:00", "Thu Nov 29 14:33:20 2001"].each do |d|
14+
Benchmark.ips do |x|
15+
x.report("DRY #{d}") do |n|
16+
while n > 0
17+
dry[d]
18+
n -= 1
19+
end
20+
end
21+
22+
x.report("AM #{d}") do |n|
23+
while n > 0
24+
am.cast(d)
25+
n -= 1
26+
end
27+
end
28+
29+
x.compare!
30+
end
31+
end
32+
33+
34+
# before
35+
#
36+
# Comparison:
37+
# AM 2020-01-20 19:40:22: 130660.9 i/s
38+
# DRY 2020-01-20 19:40:22: 58853.9 i/s - 2.22x (± 0.00) slower
39+
#
40+
# Comparison:
41+
# DRY 2021-02-03T00:10:54.597+03:00: 52110.0 i/s
42+
# AM 2021-02-03T00:10:54.597+03:00: 39652.9 i/s - 1.31x (± 0.00) slower
43+
#
44+
# Comparison:
45+
# DRY Thu Nov 29 14:33:20 2001: 44819.1 i/s
46+
# AM Thu Nov 29 14:33:20 2001: 33064.5 i/s - 1.36x (± 0.00) slower
47+
48+
# after
49+
#
50+
# Comparison:
51+
# DRY 2020-01-20 19:40:22: 190951.9 i/s
52+
# AM 2020-01-20 19:40:22: 131920.6 i/s - 1.45x (± 0.00) slower
53+
#
54+
# Comparison:
55+
# DRY 2021-02-03T00:10:54.597+03:00: 157549.5 i/s
56+
# AM 2021-02-03T00:10:54.597+03:00: 40502.8 i/s - 3.89x (± 0.00) slower
57+
#
58+
# Comparison:
59+
# DRY Thu Nov 29 14:33:20 2001: 44376.3 i/s
60+
# AM Thu Nov 29 14:33:20 2001: 33955.3 i/s - 1.31x (± 0.00) slower

lib/dry/types/coercions.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Coercions
1818
def to_date(input, &block)
1919
if input.respond_to?(:to_str)
2020
begin
21-
::Date.parse(input)
21+
fast_string_to_date(input) || ::Date.parse(input)
2222
rescue ArgumentError, RangeError => e
2323
CoercionError.handle(e, &block)
2424
end
@@ -64,7 +64,7 @@ def to_date_time(input, &block)
6464
def to_time(input, &block)
6565
if input.respond_to?(:to_str)
6666
begin
67-
::Time.parse(input)
67+
fast_string_to_time(input) || ::Time.parse(input)
6868
rescue ArgumentError => e
6969
CoercionError.handle(e, &block)
7070
end
@@ -102,6 +102,37 @@ def to_symbol(input, &block)
102102
def empty_str?(value)
103103
EMPTY_STRING.eql?(value)
104104
end
105+
106+
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
107+
def fast_string_to_date(string)
108+
if string =~ ISO_DATE
109+
::Date.new $1.to_i, $2.to_i, $3.to_i
110+
end
111+
end
112+
113+
ISO_DATETIME = /
114+
\A
115+
(\d{4})-(\d\d)-(\d\d)(?:T|\s) # 2020-06-20T
116+
(\d\d):(\d\d):(\d\d)(?:\.(\d{1,6})\d*)? # 10:20:30.123456
117+
(?:(Z(?=\z)|[+-]\d\d)(?::?(\d\d))?)? # +09:00
118+
\z
119+
/x
120+
def fast_string_to_time(string)
121+
return unless ISO_DATETIME =~ string
122+
123+
usec = $7.to_i
124+
usec_len = $7&.length
125+
if usec_len&.< 6
126+
usec *= 10**(6 - usec_len)
127+
end
128+
129+
if $8
130+
offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
131+
end
132+
133+
::Time.local($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
134+
end
135+
105136
end
106137
end
107138
end

0 commit comments

Comments
 (0)