With the goal of transitioning away from PHP-based web tools, I’ve started writing functions to improve user interface and experience when working with newborn gestational ages.
Goal: given input of any set of LMP, EDC, DOB, date, GA, age, DOL, PMA, calculate as many of the others as possible. Returns NULL if there is a conflict in the input data.
- lmp: date last menstrual period
- edc: due date
- dob: date of birth
- date: date of interest
- ga: GA in ## #/7 format
- age: age in days (zero on the day of birth)
- dol: day of life (one on the day of birth)
- pma: PMA in ## #/7 format
Previously released tools that perform these calculations include:
Previously described code to parse gestational ages
extract_weeks(ga_string)
extract_days(ga_string)
weeks_to_days(ga_string)
days_to_weeks(days)
Code
library(dplyr)
library(stringr)
<- function(ga_string) {
extract_weeks as.integer(str_extract(ga_string, "\\d+")) # First consecutive string of digits
}<- function(ga_string) {
extract_days # imperfect logic: 18/7 will return 8
<- str_extract(ga_string, "\\d(?=(/7|d))") # Extract single digit before "/7" or "d"
days is.na(days)] <- "0"
days[as.integer(days)
}<- function(ga_string) {
weeks_to_days # cleaned string: paste0(extract_weeks(ga_string), " ", extract_days(ga_string), "/7")
as.integer(extract_weeks(ga_string)*7 + extract_days(ga_string))
}<- function(days) {
days_to_weeks # Convert days to weeks as string in ## #/7 format
<- floor(days / 7)
weeks <- days %% 7
days <- ifelse(is.na(weeks), NA, paste0(weeks, " ", days, "/7"))
result return(result)
}
Code for gestational age calculations
ga_calc(lmp, edc, dob, date, ga, age, dol, pma)
The logic is really brute force and not very elegant.
Basically, every possible relationship is described. Starting with the given inputs, keep looping around and try to calculate another output field, and repeat until either nothing more can be calculated, or a conflict is detected representing a contradictory set of inputs.
It’s ugly, but it works.
For a generalizable solution, another option might be to convert the equalities into error terms and try to use some sort of gradient descent to settle on close approximation of solutions (assuming there are no local minima). You’d have to deal with inadequate inputs resulting in some terms that can not be calculated – avoiding including those in the gradient descents. It seems not worth the effort when there are a pretty limited number of fields.
Interestingly, way back in the 1990’s, there was a Palm Pilot application called Mathpad which functioned as a general equation solver. I’ve always wondered how it worked. Still do.
Code
<- function(lmp = NA, edc = NA, dob = NA, date = NA, ga = NA, age = NA, dol = NA, pma = NA) {
ga_calc # Given available inputs, provide all outputs that can be calculated
# - returns NULL if there is a conflict
#
# lmp # date last menstrual period
# edc # due date
# dob # date of birth
# date # date of interest
# ga # GA in ## #/7 format
# age # age in days (zero on the day of birth)
# dol # day of life (one on the day of birth)
# pma # PMA in ## #/7 format
<- days_to_weeks(weeks_to_days(ga)) # normalize input dates to ## #/7
ga <- days_to_weeks(weeks_to_days(pma))
pma <- as.Date(lmp)
lmp <- as.Date(edc)
edc <- as.Date(dob)
dob <- as.Date(date)
date
repeat {
<- TRUE # will keep on looping through calculations until nothing has been changed
no_changes <- FALSE # set this to TRUE if the submitted data is self-contradictory
data_conflict ########## If know EDC, calculate LMP
if (!is.na(edc)) {
<- edc - 40*7
lmp_calc if (!is.na(lmp)) {
if (lmp_calc != lmp) {
<- TRUE
data_conflict
}else {
} <- lmp_calc
lmp <- FALSE
no_changes
}
}########## If know LMP, calculate EDC
if (!is.na(lmp)) {
<- lmp + 40*7
edc_calc if (!is.na(edc)) {
if (edc_calc != edc) {
<- TRUE
data_conflict
}else {
} <- edc_calc
edc <- FALSE
no_changes
}
}########## If know DOL, calculate age
if (!is.na(dol)) {
<- dol - 1
age_calc if (!is.na(age)) {
if (age_calc != age) {
<- TRUE
data_conflict
}else {
} <- age_calc
age <- FALSE
no_changes
}
}########## If know age, calculate DOL
if (!is.na(age)) {
<- age + 1
dol_calc if (!is.na(dol)) {
if (dol_calc != dol) {
<- TRUE
data_conflict
}else {
} <- dol_calc
dol <- FALSE
no_changes
}
}########## If know DOB and LMP, calculate GA
if ( (!is.na(dob)) && (!is.na(lmp)) ) {
<- days_to_weeks(as.integer(dob - lmp))
ga_calc if (!is.na(ga)) {
if (ga_calc != ga) {
<- TRUE
data_conflict
}else {
} <- ga_calc
ga <- FALSE
no_changes
}
}########## If know GA and LMP, calculate DOB
if ( (!is.na(ga)) && (!is.na(lmp)) ) {
<- weeks_to_days(ga) + lmp
dob_calc if (!is.na(dob)) {
if (dob_calc != dob) {
<- TRUE
data_conflict
}else {
} <- dob_calc
dob <- FALSE
no_changes
}
}########## If know DOB and GA, calculate LMP
if ( (!is.na(dob)) && (!is.na(ga)) ) {
<- dob - weeks_to_days(ga)
lmp_calc if (!is.na(lmp)) {
if (lmp_calc != lmp) {
<- TRUE
data_conflict
}else {
} <- lmp_calc
lmp <- FALSE
no_changes
}
}########## If know date and DOB, calculate age
if ( (!is.na(date)) && (!is.na(dob)) ) {
<- as.integer(date - dob)
age_calc if (!is.na(age)) {
if (age_calc != age) {
<- TRUE
data_conflict
}else {
} <- age_calc
age <- FALSE
no_changes
}
}########## If know age and DOB, calculate date
if ( (!is.na(age)) && (!is.na(dob)) ) {
<- dob + age
date_calc if (!is.na(date)) {
if (date_calc != date) {
<- TRUE
data_conflict
}else {
} <- date_calc
date <- FALSE
no_changes
}
}########## If know age and date, calculate DOB
if ( (!is.na(age)) && (!is.na(date)) ) {
<- date - age
dob_calc if (!is.na(dob)) {
if (dob_calc != dob) {
<- TRUE
data_conflict
}else {
} <- dob_calc
dob <- FALSE
no_changes
}
}########## If know GA and age, calculate PMA
if ( (!is.na(ga)) && (!is.na(age)) ) {
<- days_to_weeks( weeks_to_days(ga) + age )
pma_calc if (!is.na(pma)) {
if (pma_calc != pma) {
<- TRUE
data_conflict
}else {
} <- pma_calc
pma <- FALSE
no_changes
}
}########## If know PMA and age, calculate GA
if ( (!is.na(pma)) && (!is.na(age)) ) {
<- days_to_weeks( weeks_to_days(pma) - age )
ga_calc if (!is.na(ga)) {
if (ga_calc != ga) {
<- TRUE
data_conflict
}else {
} <- ga_calc
ga <- FALSE
no_changes
}
}########## If know GA and PMA, calculate age
if ( (!is.na(ga)) && (!is.na(pma)) ) {
<- weeks_to_days(pma) - weeks_to_days(ga)
age_calc if (!is.na(age)) {
if (age_calc != age) {
<- TRUE
data_conflict
}else {
} <- age_calc
age <- FALSE
no_changes
}
}########## If know EDC and date, calculate PMA
if ( (!is.na(edc)) && (!is.na(date)) ) {
<- days_to_weeks(280 - as.integer(edc - date)) # 280 days = 40 weeks
pma_calc if (!is.na(pma)) {
if (pma_calc != pma) {
<- TRUE
data_conflict
}else {
} <- pma_calc
pma <- FALSE
no_changes
}
}########## If know PMA and date, calculate EDC
if ( (!is.na(pma)) && (!is.na(date)) ) {
<- date + 40*7 - weeks_to_days(pma)
edc_calc if (!is.na(edc)) {
if (edc_calc != edc) {
<- TRUE
data_conflict
}else {
} <- edc_calc
edc <- FALSE
no_changes
}
}########## If know PMA and EDC, calculate date
if ( (!is.na(pma)) && (!is.na(edc)) ) {
<- edc - 40*7 + weeks_to_days(pma)
date_calc if (!is.na(date)) {
if (date_calc != date) {
<- TRUE
data_conflict
}else {
} <- date_calc
date <- FALSE
no_changes
}
}if (no_changes || data_conflict) { break }
}if (data_conflict) {
warning("Input data inconsistency")
return (NULL)
else {
} return(list(
lmp = as.character(lmp),
edc = as.character(edc),
dob = as.character(dob),
ga = ga,
date = as.character(date),
age = age,
dol = dol,
pma = pma)
)
} }
Helper function to display ga_calc() results in table
Code
<- function(...) {
test_ga_calc # display ga_calc(lmp, edc, dob, date, ga, age, dol, pma) results in table
<- ga_calc(...)
l data.frame(
field = names(l),
value = unlist(l, use.names = FALSE)
|>
) ::kable()
knitr }
Example usage
If you only have a date of birth and age, there’s a fairly limited number of things that can be calculated.
test_ga_calc(
dob = Sys.Date(),
age = 10
)
field | value |
---|---|
lmp | NA |
edc | NA |
dob | 2025-02-12 |
ga | NA |
date | 2025-02-22 |
age | 10 |
dol | 11 |
pma | NA |
With more starting inputs, more things can be calculated.
test_ga_calc(
date = "2025-02-04",
dob = "2025-01-01",
ga = "26 0/7"
)
field | value |
---|---|
lmp | 2024-07-03 |
edc | 2025-04-09 |
dob | 2025-01-01 |
ga | 26 0/7 |
date | 2025-02-04 |
age | 34 |
dol | 35 |
pma | 30 6/7 |
If contradictory input is submitted, the function fails with a warning.
test_ga_calc(
date = "2025-01-03",
dob = "2025-01-01",
age = 10
)
Warning in ga_calc(...): Input data inconsistency