From fa022b959269f174cebcbdf0e1c5369c2ef80714 Mon Sep 17 00:00:00 2001
From: Joao P Dubas <joao.dubas+gitea@gmail.com>
Date: Tue, 11 Jun 2024 11:38:18 +0000
Subject: [PATCH] feat(growth): calculate z-score/centile for all indicators

The `Growth.Score.Scorer` behaviour implements the methods needed to
calculate z-score and centile for any indicator.
---
 lib/growth/score/scorer.ex | 80 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 80 insertions(+)
 create mode 100644 lib/growth/score/scorer.ex

diff --git a/lib/growth/score/scorer.ex b/lib/growth/score/scorer.ex
new file mode 100644
index 0000000..26ab1cf
--- /dev/null
+++ b/lib/growth/score/scorer.ex
@@ -0,0 +1,80 @@
+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, {z_score(indicator, growth, l, m, s), centile(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 z_score(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number()
+  @doc """
+  Check `Growth.Calc.ZScore.compute/4`.
+  """
+  def z_score(indicator, growth, l, m, s) do
+    growth
+    |> Map.get(indicator.measure_name())
+    |> ZScore.compute(l, m, s)
+  end
+
+  @spec centile(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na
+  @doc """
+  Check `Growth.Calc.Centile.compute/4`.
+  """
+  def centile(indicator, growth, l, m, s) do
+    growth
+    |> Map.get(indicator.measure_name())
+    |> Centile.compute(l, m, s)
+  end
+end