defmodule Growth.Score.Scorer do @moduledoc """ Behaviour defining common interface to calculate z-score and centile for a given measurement. """ alias Growth.Calc.Centile alias Growth.Calc.ZScore @callback measure_name() :: atom() @spec results(Growth.t(), [module()]) :: Growth.t() @doc """ Add z-score and centile values in growth measurements `results` for each indicator. """ def results(growth, indicators) do Enum.reduce(indicators, growth, &result/2) end @spec result(module(), Growth.t()) :: Growth.t() @doc """ Calculate z-score and centile values for the given indicator and add them to the growth measurement `results`. """ def result(indicator, growth) do result = growth |> lms(indicator) |> Enum.map(fn {precision, {l, m, s}} -> {precision, scores(indicator, growth, l, m, s)} end) %{growth | results: [Map.new([{indicator.measure_name(), result}]) | growth.results]} end @spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}] @doc """ Get the indicaator fitted values of Box-Cox transformation: * power (`l`) * median (`m`) * coefficient of variation (`s`) """ def lms(growth, indicator) do [ {growth.gender, "day", growth.age_in_days}, {growth.gender, "week", growth.age_in_weeks}, {growth.gender, "month", growth.age_in_months} ] |> Enum.map(fn {_, precision, _} = key -> case :ets.lookup(indicator, key) do [{^key, %{l: l, m: m, s: s}} | _] -> {precision, {l, m, s}} _ -> nil end end) |> Enum.reject(&is_nil/1) end @spec scores(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: {Growth.measure(), number() | :na, nil} @doc """ Calculate the z-score and centile of an indicator measurement. """ def scores(indicator, growth, l, m, s) do measure = Map.get(growth, indicator.measure_name()) { z_score(measure, l, m, s), centile(measure, l, m, s) } end @spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure() @doc """ Check `Growth.Calc.ZScore.compute/4`. """ def z_score(nil, _, _, _) do nil end def z_score(value, l, m, s) do ZScore.compute(value, l, m, s) end @spec centile(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na | nil @doc """ Check `Growth.Calc.Centile.compute/4`. """ def centile(nil, _, _, _) do nil end def centile(value, l, m, s) do Centile.compute(value, l, m, s) end end