Hi all, as I’m always trying to improve my code.
Is anyone of you Elixir Pros interested into review my following code?
This module will convert duration measurements into a human easy readable scientific format.
What I mean by this.
I always hated if an uptime / or downtime measurement told me e.g. up since 1 y 21 d 5 h 5 min 5 s
.
In this case 1 y 21 d
would be totally enough for myself.
So the conversion scheme I’m using here is very opinionated.
In case of any unit value below 10, I will additionally add the value of the next unit. Above I don’t.
e.g.
- `1 s _ ms` - `9 s _ ms`
- `10 s ` - `59 s`
I would be very thankful if anybody would give me his opinion to my code below.
If you would do something in Elixir best practice completely different for this use case …
defmodule Tool.Timer do
@moduledoc """
Time measurement tool for performance & uptime testing.
Can convert a duration to a human easy understandable scientific format.
"""
@base_unit :nanosecond
@unit_shift_factor 10
@second 1
@minute @second * 60
@hour @minute * 60
@day @hour * 24
@year @day * 365
@doc """
Returns monotonic system time in @base_unit
"""
@spec now() :: integer()
def now, do: System.monotonic_time(@base_unit)
@doc """
Calculate time difference to a given start time (e.g. from `now/0`).
Return duration in @base_unit.
"""
@spec diff(integer(), integer()) :: integer()
def diff(start_time, end_time \\ now()) do
end_time - start_time
end
@doc """
Calculate time difference to a given start time (e.g. from `now/0`).
Return duration in a human easy understandable scientific format.
In: `-576460349095935368`
Out:
- `1 ns` - `999 ns`
- `1 μs _ ns` - `9 μs _ ns`
- `10 μs` - `999 μs`
- `1 ms _ μs` - `9 ms _ μs`
- `10 ms ` - `999 ms`
- `1 s _ ms` - `9 s _ ms`
- `10 s ` - `59 s`
- `1 min _ s` - `59 min _ s`
- `1 h _ min` - `23 h _ min`
- `1 d _ h` - `364 d _ h`
- `1 y _ d` - `_ y _ d`
"""
@spec diff_human(integer(), integer(), System.time_unit()) :: binary()
def diff_human(start_time, end_time \\ now(), base_unit \\ @base_unit) do
diff_base = diff(start_time, end_time)
diff_nano = System.convert_time_unit(diff_base, base_unit, :nanosecond)
time_conversion(diff_nano)
end
# everything below system precision
defp time_conversion(time) when time === 0,
do: "< #{system_precision()}"
# nanoseconds (ns) only
defp time_conversion(time) when time < 1_000,
do: unit_fraction_string(time, :nanosecond)
# microseconds (μs) + nanoseconds (ns)
defp time_conversion(time) when time < 1_000 * @unit_shift_factor do
list_to_string([
unit_fraction_string(time, :microsecond),
unit_fraction_string(time, :nanosecond)
])
end
# microseconds (μs) only
defp time_conversion(time) when time < 1_000_000,
do: unit_fraction_string(time, :microsecond)
# milliseconds (ms) + microseconds (μs)
defp time_conversion(time) when time < 1_000_000 * @unit_shift_factor do
list_to_string([
unit_fraction_string(time, :millisecond),
unit_fraction_string(time, :microsecond)
])
end
# milliseconds (ms) only
defp time_conversion(time) when time < 1_000_000_000,
do: unit_fraction_string(time, :millisecond)
# seconds (s) + milliseconds (ms)
defp time_conversion(time) when time < 1_000_000_000 * @unit_shift_factor do
list_to_string([
unit_fraction_string(time, :second),
unit_fraction_string(time, :millisecond)
])
end
# seconds (s) only
defp time_conversion(time) when time < @minute * 1_000_000_000,
do: unit_fraction_string(time, :second)
# minutes (min) + seconds (s)
defp time_conversion(time) when time < @hour * 1_000_000_000 do
list_to_string([
unit_subset_fraction_string(time, :minute, @minute, 60),
unit_subset_fraction_string(time, :second, @second, 60)
])
end
# hours (h) + minutes (min)
defp time_conversion(time) when time < @day * 1_000_000_000 do
list_to_string([
unit_subset_fraction_string(time, :hour, @hour, 60),
unit_subset_fraction_string(time, :minute, @minute, 60)
])
end
# days (d) + hours (h)
defp time_conversion(time) when time < @year * 1_000_000_000 do
list_to_string([
unit_subset_fraction_string(time, :day, @day, 365),
unit_subset_fraction_string(time, :hour, @hour, 24)
])
end
# years (y) + days (d)
@spec time_conversion(integer()) :: binary()
defp time_conversion(time) do
list_to_string([
unit_subset_fraction_string(time, :year, @year, 365),
unit_subset_fraction_string(time, :day, @day, 365)
])
end
# will return the human readable system precision
@spec system_precision :: binary
defp system_precision do
parts_per_second = System.convert_time_unit(1, :second, :native)
precision_nano = round(1 / parts_per_second * 1_000_000_000)
# to avoid a infinite loop on value 0
precision =
case precision_nano do
x when x > 0 -> x
_ -> 1
end
time_conversion(precision)
end
# unit fraction for everything below seconds
@spec unit_fraction_string(integer(), atom()) :: binary()
defp unit_fraction_string(time, unit) do
fraction =
time
|> System.convert_time_unit(:nanosecond, unit)
|> Integer.mod(1_000)
human_readable_unit(fraction, unit)
end
# unit fraction for everything above seconds
@spec unit_subset_fraction_string(integer(), atom(), integer(), integer()) :: binary()
defp unit_subset_fraction_string(time, unit, fraction_divisor, subset_divisor) do
fraction =
time
|> System.convert_time_unit(:nanosecond, :second)
|> Integer.floor_div(fraction_divisor)
|> Integer.mod(subset_divisor)
human_readable_unit(fraction, unit)
end
# join value with unit
@spec human_readable_unit(integer(), atom()) :: binary()
defp human_readable_unit(fraction, unit) do
cond do
fraction > 0 -> Integer.to_string(fraction) <> " " <> unit_abbreviation(unit)
true -> nil
end
end
# unit abbreviations
@spec unit_abbreviation(atom()) :: binary()
defp unit_abbreviation(:nanosecond), do: "ns"
defp unit_abbreviation(:microsecond), do: "μs"
defp unit_abbreviation(:millisecond), do: "ms"
defp unit_abbreviation(:second), do: "s"
defp unit_abbreviation(:minute), do: "min"
defp unit_abbreviation(:hour), do: "h"
defp unit_abbreviation(:day), do: "d"
defp unit_abbreviation(:year), do: "y"
# join list to string
@spec list_to_string(list()) :: binary()
defp list_to_string(list) do
list
# remove nil values from list
|> Enum.filter(& &1)
|> Enum.join(" ")
end
end
Edit:
- Updated with the already mentioned changes from
david_ex
.