ex_trainer/lib/growth/growth.ex

234 lines
6.8 KiB
Elixir
Raw Normal View History

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