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 alias __MODULE__.Score @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`. ## Examples iex> Growth.new( ...> "child a", ...> :male, ...> ~D[2024-01-01], ...> date_of_measurement: ~D[2024-04-01], ...> weight: 8, ...> height: 65.4, ...> arm_circumference: 15.5, ...> head_circumference: 42.8, ...> subscapular_skinfold: 10.9, ...> triceps_skinfold: 13.5 ...> ) %Growth{ name: "child a", gender: :male, date_of_birth: ~D[2024-01-01], date_of_measurement: ~D[2024-04-01], age_in_days: 91, age_in_weeks: 13, age_in_months: 2, weight: 8, height: 65.4, arm_circumference: 15.5, head_circumference: 42.8, subscapular_skinfold: 10.9, triceps_skinfold: 13.5, bmi: 18.703999850368, results: [ %{ head_circumference: [ {"day", {1.945484886994137, 0.9741416765426315}}, {"week", {1.945484886994137, 0.9741416765426315}}, {"month", {3.130859582465616, 0.9991285226182205}} ] }, %{arm_circumference: [{"day", {1.9227031505630465, 0.9727413295221268}}]}, %{subscapular_skinfold: [{"day", {1.9437372448689536, 0.9740364275897885}}]}, %{triceps_skinfold: [{"day", {1.950277062993091, 0.974428447506235}}]}, %{ weight: [ {"day", {1.982458622036091, 0.9762860329545557}}, {"week", {1.982458622036091, 0.9762860329545557}}, {"month", {3.0355951313091745, 0.9987996926038037}} ] }, %{ height: [ {"day", {1.956263992749136, 0.9747829682259178}}, {"week", {1.956263992749136, 0.9747829682259178}}, {"month", {3.4867331002754054, 0.9997555204670452}} ] }, %{ bmi: [ {"day", {1.1977344927294398, 0.8844898016950435}}, {"week", {1.1977344927294398, 0.8844898016950435}}, {"month", {1.5837461190318038, 0.9433742474306444}} ] } ] } """ @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, results: [] }, 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() |> with_results() 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 def with_results(growth) do Score.Scorer.results(growth, [ Score.BMI, Score.Height, Score.Weight, Score.TricepsSkinfold, Score.SubscapularSkinfold, Score.ArmCircumference, Score.HeadCircumference ]) end defp default_opts do Enum.map(@valid_measures, fn :date_of_measurement = key -> {key, Date.utc_today()} key -> {key, nil} end) end end