Current section

Files

Jump to
aja lib helpers random_helper.ex
Raw

lib/helpers/random_helper.ex

defmodule Aja.RandomHelper do
@moduledoc false
# TODO Remove this module when dropping support for Elixir 1.16
# vendoring Elixir's 1.17 implementation of take_random/2 and shuffle/1 which are:
# - faster
# - necessary to keep CI deterministic across multiple Elixir versions
def shuffle(enumerable) do
randomized =
Enum.reduce(enumerable, [], fn x, acc ->
[{:rand.uniform(), x} | acc]
end)
shuffle_unwrap(:lists.keysort(1, randomized))
end
defp shuffle_unwrap([{_, h} | rest]), do: [h | shuffle_unwrap(rest)]
defp shuffle_unwrap([]), do: []
def take_random(enumerable, count)
def take_random(_enumerable, 0), do: []
def take_random([], _), do: []
def take_random(enumerable, 1) do
enumerable
|> Enum.reduce({0, 0, 1.0, nil}, fn
elem, {idx, idx, w, _current} ->
{jdx, w} = take_jdx_w(idx, w, 1)
{idx + 1, jdx, w, elem}
_elem, {idx, jdx, w, current} ->
{idx + 1, jdx, w, current}
end)
|> case do
{0, 0, 1.0, nil} -> []
{_idx, _jdx, _w, current} -> [current]
end
end
def take_random(enumerable, count) when count in 0..128 do
sample = Tuple.duplicate(nil, count)
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> put_elem(idx, elem(sample, rand)) |> put_elem(rand, elem)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, put_elem(sample, pos, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
end
{size, _, _, sample} = Enum.reduce(enumerable, {0, count - 1, 1.0, sample}, reducer)
if count < size do
Tuple.to_list(sample)
else
take_tupled(sample, size, [])
end
end
def take_random(enumerable, count) when is_integer(count) and count >= 0 do
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> Map.put(idx, Map.get(sample, rand)) |> Map.put(rand, elem)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, %{sample | pos => elem}}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
end
{size, _, _, sample} = Enum.reduce(enumerable, {0, count - 1, 1.0, %{}}, reducer)
take_mapped(sample, Kernel.min(count, size), [])
end
@compile {:inline, take_jdx_w: 3, take_index: 1}
defp take_jdx_w(idx, w, count) do
w = w * :math.exp(:math.log(:rand.uniform()) / count)
jdx = idx + floor(:math.log(:rand.uniform()) / :math.log(1 - w)) + 1
{jdx, w}
end
defp take_index(0), do: 0
defp take_index(idx), do: :rand.uniform(idx + 1) - 1
defp take_tupled(_sample, 0, acc), do: acc
defp take_tupled(sample, position, acc) do
position = position - 1
take_tupled(sample, position, [elem(sample, position) | acc])
end
defp take_mapped(_sample, 0, acc), do: acc
defp take_mapped(sample, position, acc) do
position = position - 1
take_mapped(sample, position, [Map.fetch!(sample, position) | acc])
end
end