diff --git a/lib/growth/growth.ex b/lib/growth/growth.ex new file mode 100644 index 0000000..641e049 --- /dev/null +++ b/lib/growth/growth.ex @@ -0,0 +1,154 @@ +defmodule Growth do + @moduledoc """ + Follow up child growth from 0 to 19 years, calculating z-scores for the following measurements: + + * weight between ages 0 and 10 years + * height between ages 0 and 19 years + * body mass index (bmi) between ages 0 and 19 years + * head circumference between ages 0 and 5 years + * arm circumference between ages 3 months and 5 years + * subscapular skinfold between ages 3 months and 5 years + * triceps skinfold between ages 3 months and 5 years + """ + + alias __MODULE__.Calc.Age + alias __MODULE__.Calc.BMI + + @type gender :: :male | :female + + @type measure :: number() | nil + + @type opts :: [ + date_of_measurement: Date.t(), + weight: measure(), + height: measure(), + arm_circumference: measure(), + head_circumference: measure(), + subscapular_skinfold: measure(), + triceps_skinfold: measure() + ] + + @type t :: %__MODULE__{ + name: String.t(), + gender: gender(), + date_of_birth: Date.t(), + date_of_measurement: Date.t(), + age_in_days: pos_integer(), + age_in_weeks: pos_integer(), + age_in_months: pos_integer(), + weight: measure(), + height: measure(), + arm_circumference: measure(), + head_circumference: measure(), + subscapular_skinfold: measure(), + triceps_skinfold: measure(), + bmi: measure(), + results: list() + } + + @enforce_keys [:name, :gender, :date_of_birth] + defstruct [ + :name, + :gender, + :date_of_birth, + :date_of_measurement, + :age_in_days, + :age_in_weeks, + :age_in_months, + :weight, + :height, + :arm_circumference, + :head_circumference, + :subscapular_skinfold, + :triceps_skinfold, + :bmi, + :results + ] + + @valid_measures [ + :date_of_measurement, + :weight, + :height, + :arm_circumference, + :head_circumference, + :subscapular_skinfold, + :triceps_skinfold + ] + + @doc """ + Create a new growth measurement for a children with name, gender, date of birth, and the following optional arguments: + + * `:date_of_measurement`: date when the measures were collected, defaults to today. + * `:weight`: weight in kilograms, defaults to `nil`. + * `:height`: height in centimeters, defaults to `nil`. + * `:arm_circumference`: arm circumference in centimeters, defaults to `nil`. + * `:head_circumference`: head circumference in centimeters, defaults to `nil`. + * `:subscapular_skinfold`: subscapular skinfold in milimeters, defaults to `nil`. + * `:triceps_skinfold`: triceps skinfold in milimeters, defaults to `nil`. + + """ + @spec new(String.t(), gender(), Date.t(), opts()) :: t() + def new(name, gender, date_of_birth, opts \\ []) do + default_opts() + |> Enum.reduce( + %__MODULE__{ + name: name, + gender: gender, + date_of_birth: date_of_birth + }, + fn {key, default_value}, measurement -> + opts + |> Keyword.get(key, default_value) + |> then(&Map.put(measurement, key, &1)) + end + ) + |> with_age_in_days() + |> with_age_in_weeks() + |> with_age_in_months() + |> with_bmi() + end + + for precision <- [:day, :week, :month] do + @doc """ + Add age with given precision in growth measurement. + """ + @spec unquote(:"with_age_in_#{precision}s")(t()) :: t() + + def unquote(:"with_age_in_#{precision}s")( + %__MODULE__{date_of_birth: date_of_birth, date_of_measurement: date_of_measurement} = + growth + ) + when not is_nil(date_of_birth) and not is_nil(date_of_measurement) do + age = Age.calculate(unquote(precision), date_of_birth, date_of_measurement) + Map.put(growth, unquote(:"age_in_#{precision}s"), age) + end + + def unquote(:"with_age_in_#{precision}s")(%__MODULE__{date_of_birth: date_of_birth} = growth) + when not is_nil(date_of_birth) do + unquote(:"with_age_in_#{precision}s")(%{growth | date_of_measurement: Date.utc_today()}) + end + + def unquote(:"with_age_in_#{precision}s")(growth) do + growth + end + end + + def with_bmi(%__MODULE__{weight: weight, height: height} = growth) + when is_number(weight) and is_number(height) do + %{growth | bmi: BMI.calculate(:metric, weight, height)} + end + + def with_bmi(growth) do + growth + end + + defp default_opts do + Enum.map(@valid_measures, fn + :date_of_measurement = key -> + {key, Date.utc_today()} + + key -> + {key, nil} + end) + end +end