defmodule Benchee.Conversion.Scale do
@moduledoc """
Functions for scaling values to other units. Different domains handle
this task differently, for example durations and counts.
See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples
"""
alias Benchee.Conversion.Unit
@type unit :: Unit.t()
@type unit_atom :: atom
@type any_unit :: unit | unit_atom
@type scaled_number :: {number, unit}
@type scaling_strategy :: :best | :largest | :smallest | :none
@doc """
Scales a number in a domain's base unit to an equivalent value in the best
fit unit. Results are a `{number, unit}` tuple. See `Benchee.Conversion.Count` and
`Benchee.Conversion.Duration` for examples.
"""
@callback scale(number) :: scaled_number
@doc """
Scales a number in a domain's base unit to an equivalent value in the
specified unit.
See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples.
"""
@callback scale(number, any_unit) :: number
@doc """
Finds the best fit unit for a list of numbers in a domain's base unit.
"Best fit" is the most common unit, or (in case of tie) the largest of the
most common units.
"""
@callback best(list, keyword) :: unit
@doc """
Returns the base_unit in which Benchee takes its measurements, which in
general is the smallest supported unit.
"""
@callback base_unit :: unit
@doc """
Given the atom representation of a unit (`:hour`) return the appropriate
`Benchee.Conversion.Unit` struct.
"""
@callback unit_for(any_unit) :: unit
@doc """
List of all the units supported by this type.
"""
@callback units :: [unit]
@doc """
Takes a tuple of a number and a unit and a unit to be converted to, returning
the the number scaled to the new unit and the new unit.
"""
@callback convert({number, any_unit}, any_unit) :: scaled_number
# Generic scaling functions
@doc """
Used internally by implemented units to handle their scaling with units and
without.
## Examples
iex> scale(12345, :thousand, Benchee.Conversion.Count)
12.345
"""
def scale(value, unit = %Unit{}, _module) do
scale(value, unit)
end
def scale(value, unit_atom, module) do
scale(value, module.unit_for(unit_atom))
end
@doc """
Used internally for scaling but only supports scaling with actual units.
## Examples
iex> unit = %Benchee.Conversion.Unit{magnitude: 1000}
iex> scale 12345, unit
12.345
"""
def scale(value, %Unit{magnitude: magnitude}) do
value / magnitude
end
@doc """
Lookup a unit by its `atom` presentation for the representation of supported
units. Used by `Benchee.Conversion.Duration` and `Benchee.Conversion.Count`.
"""
def unit_for(_units, unit = %Unit{}), do: unit
def unit_for(units, unit), do: Map.fetch!(units, unit)
@doc """
Used internally to implement scaling in the modules without duplication.
"""
def convert({value, current_unit}, desired_unit, module) do
current_unit = module.unit_for(current_unit)
desired_unit = module.unit_for(desired_unit)
do_convert({value, current_unit}, desired_unit)
end
defp do_convert(
{value, %Unit{magnitude: current_magnitude}},
desired_unit = %Unit{magnitude: desired_magnitude}
) do
multiplier = current_magnitude / desired_magnitude
{value * multiplier, desired_unit}
end
@doc """
Given a `list` of number values and a `module` describing the domain of the
values (e.g. Duration, Count), finds the "best fit" unit for the list as a
whole.
The best fit unit for a given value is the smallest unit in the domain for
which the scaled value is at least 1. For example, the best fit unit for a
count of 1_000_000 would be `:million`.
The best fit unit for the list as a whole depends on the `:strategy` passed
in `opts`:
* `:best` - the most frequent best fit unit. In case of tie, the
largest of the most frequent units
* `:largest` - the largest best fit unit
* `:smallest` - the smallest best fit unit
* `:none` - the domain's base (unscaled) unit
## Examples
iex> list = [1, 101, 1_001, 10_001, 100_001, 1_000_001]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :best).name
:thousand
iex> list = [1, 101, 1_001, 10_001, 100_001, 1_000_001]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :smallest).name
:one
iex> list = [1, 101, 1_001, 10_001, 100_001, 1_000_001]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :largest).name
:million
iex> list = []
iex> best_unit(list, Benchee.Conversion.Count, strategy: :best).name
:one
iex> list = [nil]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :best).name
:one
iex> list = [nil, nil, nil, nil]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :best).name
:one
iex> list = [nil, nil, nil, nil, 2_000]
iex> best_unit(list, Benchee.Conversion.Count, strategy: :best).name
:thousand
"""
def best_unit(measurements, module, options) do
do_best_unit(Enum.reject(measurements, &is_nil/1), module, options)
end
defp do_best_unit([], module, _) do
module.base_unit
end
defp do_best_unit(list, module, opts) do
case Keyword.get(opts, :strategy, :best) do
:best -> best_unit(list, module)
:largest -> largest_unit(list, module)
:smallest -> smallest_unit(list, module)
:none -> module.base_unit
end
end
# Finds the most common unit in the list. In case of tie, chooses the
# largest of the most common
defp best_unit(list, module) do
list
|> Enum.map(fn n -> scale_unit(n, module) end)
|> Enum.group_by(fn unit -> unit end)
|> Enum.map(fn {unit, occurrences} -> {unit, length(occurrences)} end)
|> Enum.sort(fn unit, freq -> by_frequency_and_magnitude(unit, freq) end)
|> hd
|> elem(0)
end
# Finds the smallest unit in the list
defp smallest_unit(list, module) do
list
|> Enum.map(fn n -> scale_unit(n, module) end)
|> Enum.min_by(fn unit -> magnitude(unit) end)
end
# Finds the largest unit in the list
defp largest_unit(list, module) do
list
|> Enum.map(fn n -> scale_unit(n, module) end)
|> Enum.max_by(fn unit -> magnitude(unit) end)
end
defp scale_unit(count, module) do
{_, unit} = module.scale(count)
unit
end
# Fetches the magnitude for the given unit
defp magnitude(%Unit{magnitude: magnitude}) do
magnitude
end
# Sorts two elements first by total, then by magnitude of the unit in case
# of tie
defp by_frequency_and_magnitude({unit_a, frequency}, {unit_b, frequency}) do
magnitude(unit_a) > magnitude(unit_b)
end
defp by_frequency_and_magnitude({_, frequency_a}, {_, frequency_b}) do
frequency_a > frequency_b
end
end