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