Skip to content

Commit 767de3f

Browse files
ulissesalmeidabitwalker
authored andcommitted
🐎 Make Timex.Duration.parse ~2x faster
It's very important for us at Duffel speedup the duration parsing. We're were investigating this function and we noticed a big number of calls to String.contains? that is a O(n) order algorithm in a loop. So we did a quick experiment on putting a flag on the number type while reading the chars. So it assumes that is a integer, until find a dot, where it changes the type to float. Then, it parses using Float or Integer depending on the flag. Avoiding the String.contains calls. The results were quite interesting: Before Timex.Duration.parse 500000 3322687 ~6.65µs/op Timex.Duration.parse 500000 1710993 ~3.42µs/op
1 parent ed146ee commit 767de3f

File tree

3 files changed

+31
-29
lines changed

3 files changed

+31
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77

88
### Added/Changed
99

10+
- Changed `Timex.Duration.Parse` to be 2x faster
11+
1012
### Fixed
1113

1214
---

bench/dateformat_bench.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Timex.Timex.Bench do
66

77
@datetime "2014-07-22T12:30:05Z"
88
@datetime_zoned "2014-07-22T12:30:05+02:00"
9+
@duration "P15Y3M2DT1H14M37.25S"
910

1011
setup_all do
1112
Application.ensure_all_started(:tzdata)
@@ -52,4 +53,8 @@ defmodule Timex.Timex.Bench do
5253
_ = Timex.local
5354
:ok
5455
end
56+
57+
bench "Timex.Duration.parse" do
58+
{:ok, _} = Timex.Duration.parse(@duration)
59+
end
5560
end

lib/parse/duration/parsers/iso8601.ex

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ defmodule Timex.Parse.Duration.Parsers.ISO8601Parser do
110110
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}
111111

112112
defp parse_components(<<c::utf8, rest::binary>>, acc) when c in @numeric do
113-
case parse_component(rest, <<c::utf8>>) do
113+
case parse_component(rest, {:integer, <<c::utf8>>}) do
114114
{:error, _} = err -> err
115115
{u, n, rest} -> parse_components(rest, [{u, n} | acc])
116116
end
@@ -122,48 +122,43 @@ defmodule Timex.Parse.Duration.Parsers.ISO8601Parser do
122122
defp parse_components(<<c::utf8, _::binary>>, _acc),
123123
do: {:error, "expected numeric, but got #{<<c::utf8>>}"}
124124

125-
@spec parse_component(binary, binary) :: {integer, number, binary}
125+
@spec parse_component(binary, {:float | :integer, binary}) ::
126+
{integer, number, binary} | {:error, msg :: binary()}
126127
defp parse_component(<<c::utf8>>, _acc) when c in @numeric,
127128
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}
128129

129-
defp parse_component(<<c::utf8>>, acc) when c in 'WYMDHS' do
130-
cond do
131-
String.contains?(acc, ".") ->
132-
case Float.parse(acc) do
133-
{n, _} -> {c, n, <<>>}
134-
:error -> {:error, "invalid number `#{acc}`"}
135-
end
136-
137-
:else ->
138-
case Integer.parse(acc) do
139-
{n, _} -> {c, n, <<>>}
140-
:error -> {:error, "invalid number `#{acc}`"}
141-
end
130+
defp parse_component(<<c::utf8>>, {type, acc}) when c in 'WYMDHS' do
131+
case cast_number(type, acc) do
132+
{n, _} -> {c, n, <<>>}
133+
:error -> {:error, "invalid number `#{acc}`"}
142134
end
143135
end
144136

145-
defp parse_component(<<c::utf8, rest::binary>>, acc) when c in @numeric do
146-
parse_component(rest, <<acc::binary, c::utf8>>)
137+
defp parse_component(<<".", rest::binary>>, {:integer, acc}) do
138+
parse_component(rest, {:float, <<acc::binary, ".">>})
147139
end
148140

149-
defp parse_component(<<c::utf8, rest::binary>>, acc) when c in 'WYMDHS' do
150-
cond do
151-
String.contains?(acc, ".") ->
152-
case Float.parse(acc) do
153-
{n, _} -> {c, n, rest}
154-
:error -> {:error, "invalid number `#{acc}`"}
155-
end
141+
defp parse_component(<<c::utf8, rest::binary>>, {:integer, acc}) when c in @numeric do
142+
parse_component(rest, {:integer, <<acc::binary, c::utf8>>})
143+
end
156144

157-
:else ->
158-
case Integer.parse(acc) do
159-
{n, _} -> {c, n, rest}
160-
:error -> {:error, "invalid number `#{acc}`"}
161-
end
145+
defp parse_component(<<c::utf8, rest::binary>>, {:float, acc}) when c in @numeric do
146+
parse_component(rest, {:float, <<acc::binary, c::utf8>>})
147+
end
148+
149+
defp parse_component(<<c::utf8, rest::binary>>, {type, acc}) when c in 'WYMDHS' do
150+
case cast_number(type, acc) do
151+
{n, _} -> {c, n, rest}
152+
:error -> {:error, "invalid number `#{acc}`"}
162153
end
163154
end
164155

165156
defp parse_component(<<c::utf8>>, _acc), do: {:error, "unexpected token #{<<c::utf8>>}"}
166157

167158
defp parse_component(<<c::utf8, _::binary>>, _acc),
168159
do: {:error, "unexpected token #{<<c::utf8>>}"}
160+
161+
@spec cast_number(:float | :integer, binary) :: {number(), binary()} | :error
162+
defp cast_number(:integer, binary), do: Integer.parse(binary)
163+
defp cast_number(:float, binary), do: Float.parse(binary)
169164
end

0 commit comments

Comments
 (0)