SurveyLoopr
Tips & Tricks
14 min read

XLSForm Date Calculations: Add Days, Months, Due Dates

Mehrab Ali
Mehrab AliPublished November 14, 2026

To add days to a date in XLSForm, write date(decimal-date-time(${lmp_date}) + 280) in the calculation column of a calculate row. To count the days between two dates, subtract them: int(${discharge_date} - ${admission_date}). Calendar months need the year, month and day parts from format-date(), because months are not all the same length. Every example on this page compiles with pyxform and ODK Validate, the same check ODK Central and KoboToolbox run on upload.

Download example form (.xlsx)All seven examples in one XLSForm, one group each. Tested with pyxform and ODK Validate; free to reuse under CC BY 4.0.Download

How do I add days to a date?

Convert the date to a number of days, add to it, and convert back. This works out an expected date of delivery as 280 days after the first day of the last menstrual period:

typenamelabelrequiredconstraintconstraint_messagecalculation
datelmp_dateWhat was the first day of her last menstrual period?yes. <= today() and . > today() - 310The date must be within the last 310 days.
calculateedddate(decimal-date-time(${lmp_date}) + 280)
calculateedd_textformat-date(${edd}, '%e %b %Y')
noteshow_eddExpected date of delivery: ${edd_text}
surveychoicessettings

Two functions do the work, and they are the inverse of each other:

  • decimal-date-time(date) gives the number of days since 1 January 1970.
  • date(days) turns a number of days back into a date, stored as YYYY-MM-DD.

For weeks, add a multiple of 7: a follow-up visit in two weeks is date(decimal-date-time(${visit_date}) + 14). The second calculate here is only for the screen. format-date() shows the date as 21 Aug 2027; the codes are in date format in XLSForm.

How do I subtract days from a date?

Use the same pattern with a minus sign. A common use is the start of a recall period, so the enumerator can read out a real date instead of "in the last 30 days":

typenamelabelrequiredconstraintconstraint_messagecalculation
todaytoday
calculaterecall_startdate(decimal-date-time(${today}) - 30)
calculaterecall_start_textformat-date(${recall_start}, '%e %b')
integerclinic_visitsHow many times has anyone in the household visited a health facility since ${recall_start_text}?yes. >= 0 and . <= 60Enter a number from 0 to 60.
surveychoicessettings

The today row is a metadata question. It records the date the form was first opened and is never shown, so the recall period does not move when the form is edited a week later. today() with brackets is recalculated every time the form is opened.

How do I count the days between two dates?

Subtract the earlier date from the later one. Two date questions subtract directly, and the result is a number of days:

typenamelabelrequiredconstraintconstraint_messagecalculation
dateadmission_dateOn what date was the child admitted to the programme?yes. <= today()The admission date cannot be in the future.
datedischarge_dateOn what date was the child discharged?yes. >= ${admission_date} and . <= today()The discharge date must be on or after the admission date.
calculatestay_daysint(${discharge_date} - ${admission_date})
noteshow_stayLength of stay: ${stay_days} days
surveychoicessettings

A child admitted on 3 March and discharged on 17 April has a stay of 45 days. The subtraction counts nights, not calendar days touched: admitted and discharged on the same day gives 0. If your indicator counts both the first and the last day, add 1.

To count the days from a date until today, use int(${today} - ${visit_date}) with the today metadata row. More ways to restrict the dates themselves are in date constraints in XLSForm.

Warning:

A date plus a number is a number, not a date. ${lmp_date} + 280 in a calculate shows something like 21052 in a note, which is the count of days since 1970. Wrap the sum in date() to get a date back.

How do I get weeks and days between two dates?

Divide the days by 7 with div and keep the whole part with int(). The remainder, mod 7, is the extra days. This shows gestational age the way a midwife reads it:

typenamelabelrequiredconstraintconstraint_messagecalculation
todaytoday
datelmp_dateWhat was the first day of her last menstrual period?yes. <= today() and . > today() - 310The date must be within the last 310 days.
calculatega_daysint(${today} - ${lmp_date})
calculatega_weeksint(${ga_days} div 7)
calculatega_extra_days${ga_days} mod 7
noteshow_gaGestational age: ${ga_weeks} weeks and ${ga_extra_days} days
surveychoicessettings

A last period that started 200 days ago gives 28 weeks and 4 days. Keep ga_days in the data: it is the exact value, and the other two are derived from it.

How do I count the months between two dates?

Do not divide days by 30. Months have 28 to 31 days, so the result drifts by a month every few years. Count calendar months from the year, month and day parts instead. %Y is the year, %n the month number and %e the day of the month:

typenamelabelrequiredconstraintconstraint_messagecalculation
todaytoday
datearrival_dateOn what date did the household arrive in this camp?yes. <= today()The arrival date cannot be in the future.
calculatemonths_in_camp(int(format-date(${today}, '%Y')) - int(format-date(${arrival_date}, '%Y'))) * 12 + int(format-date(${today}, '%n')) - int(format-date(${arrival_date}, '%n')) - if(int(format-date(${today}, '%e')) < int(format-date(${arrival_date}, '%e')), 1, 0)
noteshow_monthsThe household has been in the camp for ${months_in_camp} completed months.
surveychoicessettings

The formula reads: whole years times 12, plus the difference in months, minus one if the day of the month has not been reached yet. A household that arrived on 20 August 2025 and is interviewed on 14 November 2026 has been there 14 completed months (12 + 3 - 1). For completed years, take int(${months_in_camp} div 12). The same formula with a date of birth gives age, covered in calculate age from date of birth.

How do I add months to a date?

There is no EDATE() in XLSForm. Adding 30 days per month lands on the wrong day, so add to the month number and build the new date as text. This works out when a loan falls due, from the disbursement date and the term in months:

typenamelabelrequiredconstraintconstraint_messagecalculation
dateloan_dateOn what date was the loan disbursed?yes. <= today()The disbursement date cannot be in the future.
integerloan_termWhat is the loan term in months?yes. >= 1 and . <= 60The term must be between 1 and 60 months.
calculatemonth_indexint(format-date(${loan_date}, '%Y')) * 12 + int(format-date(${loan_date}, '%n')) - 1 + ${loan_term}
calculatedue_yearint(${month_index} div 12)
calculatedue_month${month_index} mod 12 + 1
calculatedue_month_daysif(${due_month} = 2, if(${due_year} mod 4 = 0 and (${due_year} mod 100 != 0 or ${due_year} mod 400 = 0), 29, 28), if(${due_month} = 4 or ${due_month} = 6 or ${due_month} = 9 or ${due_month} = 11, 30, 31))
calculatedue_dayif(int(format-date(${loan_date}, '%e')) > ${due_month_days}, ${due_month_days}, int(format-date(${loan_date}, '%e')))
calculatedue_dateif(${loan_date} = '' or ${loan_term} = '', '', concat(${due_year}, '-', if(${due_month} < 10, '0', ''), ${due_month}, '-', if(${due_day} < 10, '0', ''), ${due_day}))
noteshow_dueThe loan falls due on ${due_date}.
surveychoicessettings

Row by row:

  • month_index counts months from year 0 and adds the term, so a term that crosses a new year needs no special case.
  • due_year and due_month split that count back into a year and a month from 1 to 12.
  • due_month_days is the length of the target month, with the leap year rule for February.
  • due_day keeps the original day of the month, or the last day of the target month when it is shorter.
  • due_date joins the parts as YYYY-MM-DD. The if(... < 10, '0', '') parts add the leading zero that a date needs.

A loan disbursed on 30 November 2026 with a 3-month term falls due on 2027-02-28, not on 2 March. To go back in time, subtract from month_index instead of adding. The text in due_date has the same form as a date question's value, so decimal-date-time(${due_date}) reads it for further arithmetic.

How do I check whether a due date has passed?

Keep the due date as a number of days, and compare it with today's number. This flags a late second vaccine dose, due 28 days after the first:

typenamelabelrequiredconstraintconstraint_messagerelevantcalculation
todaytoday
datedose1_dateOn what date did the child receive the first dose?yes. <= today()The date cannot be in the future.
calculatedose2_due_daysdecimal-date-time(${dose1_date}) + 28
calculatedose2_due_textformat-date(date(${dose2_due_days}), '%e %b %Y')
calculatedays_overdueint(decimal-date-time(${today}) - ${dose2_due_days})
select_one yes_nodose2_givenThe second dose was due on ${dose2_due_text}. Has the child received it?yes
select_one late_reasonlate_reasonThe second dose is ${days_overdue} days late. What is the main reason?yes${dose2_given} = '0' and ${days_overdue} > 0
surveychoicessettings
list_namenamelabel
yes_no1Yes
yes_no0No
late_reason1Did not know the date
late_reason2The clinic is too far
late_reason3The vaccine was out of stock
late_reason4The child was sick
late_reason-99Don't know
surveychoicessettings

days_overdue is negative before the due date, 0 on the day and positive after it, so the reason question only appears for a child who is late and has not had the dose. How relevant works is covered in skip logic in XLSForm.

Note:

Use date questions, not dateTime, when you count days. A dateTime carries the time of day, so the difference between two of them has a fraction, and ODK forum answers report results one day off around time zones and daylight saving changes. If your country changes its clocks, test dates either side of the change.

Common date calculation mistakes and how to fix them

These are the messages pyxform and ODK Validate return, which is what you see when ODK Central or KoboToolbox rejects a form:

You wroteWrite insteadError you will see
DATEDIF(${admission_date}, ${discharge_date}, 'd')int(${discharge_date} - ${admission_date})cannot handle function 'DATEDIF'
EDATE(${loan_date}, 3)The add-months rows abovecannot handle function 'EDATE'
decimal_date_time(${lmp_date})decimal-date-time(...) with hyphenscannot handle function 'decimal_date_time'
(${discharge_date} - ${admission_date}) / 7(...) div 7Unexpected beginning of path in expression
date(decimal-date-time(${lmp_date}) + 280Close the bracketMismatched brackets or parentheses in expression
decimal-date-time()Put the date inside the bracketsRequires 1 arguments but 0 provided
date(decimal-date-time(${lmp_dt}) + 280) with no lmp_dt rowUse the question's exact nameCould not find the name 'lmp_dt'
date(concat(...)) around the parts of a built dateLeave out date(); the YYYY-MM-DD text is already a dateThe value "--01" can't be converted to a date
${lmp_date} + 280date(decimal-date-time(${lmp_date}) + 280)No error, but the note shows a number such as 21052
int((${today} - ${arrival_date}) div 30) for monthsThe calendar month formula aboveNo error, but the count drifts by a month over time

The last two upload without complaint, so preview the form with a few real dates before you deploy. For other messages, see common XLSForm errors, or paste your form into the free XLSForm validator.

Does this work the same in ODK, KoboToolbox and SurveyCTO?

The functions on this page exist on all three. What differs:

PlatformWhat to know
ODK (XLSForm)decimal-date-time() and date() are documented as inverses of each other. Everything goes in the calculation column of a calculate row.
KoboToolboxSame XLSForm syntax. In the Formbuilder, add a Calculate question and type the expression.
SurveyCTOIts documentation lists decimal-date-time(), date() for turning a string into a date, and format-date-time() for display, so use format-date-time() where this page uses format-date(). Skip logic goes in a column named relevance.

On every platform the calculated date exports as text in YYYY-MM-DD form, which Stata, R and Excel all read as a date. Test the add-months rows on the platform you will collect with before fieldwork, using the last day of a month and 29 February.

Why calculate dates in the form

A due date shown on the screen is one the enumerator can tell the respondent before leaving: "your next visit is on 12 December". A length of stay or a gestational age worked out during the interview can drive skip logic and catch a mistyped year while the respondent is still there. The same numbers computed afterwards in Stata or R arrive weeks too late to correct anything.

Tip:

You can describe it in plain language instead: "work out the expected date of delivery as 280 days after the last period, and show gestational age in weeks and days". LooprAI writes the calculate rows, then compiles the form with pyxform before anything is saved, so a missing date() or an Excel function never reaches your form.

Build my first form free

Next: XLSForm calculation examples →

Frequently Asked Questions

How do I add days to a date in XLSForm?

Add a calculate row with date(decimal-date-time(${visit_date}) + 14) in the calculation column. decimal-date-time() turns the date into a number of days, you add to it, and date() turns the result back into a date. Use a minus sign to go back in time.

How do I calculate the number of days between two dates in XLSForm?

Subtract the earlier date question from the later one in a calculate row: int(${discharge_date} - ${admission_date}). The result is a number of days. Add 1 if your indicator counts both the first and the last day, and divide with div 7 for weeks.

How do I add months to a date in ODK or KoboToolbox?

There is no EDATE function. Read the year and month with format-date(), add the months to a running month count, split it back into year and month, and join the parts into YYYY-MM-DD text with concat(), adding a leading zero to months and days below 10.

Why does my calculated date show a number like 21052?

Adding a number to a date gives a number: the count of days since 1 January 1970. Wrap the sum in date(), as in date(decimal-date-time(${lmp_date}) + 280), and the calculate stores a date in YYYY-MM-DD form that format-date() can show in any layout.

How do I calculate an expected date of delivery in XLSForm?

Ask the first day of the last menstrual period as a date question, then add 280 days in a calculate row: date(decimal-date-time(${lmp_date}) + 280). Show it in a note with format-date(${edd}, '%e %b %Y') so the enumerator can read the date to the woman.

Do date calculations work the same in ODK, KoboToolbox and SurveyCTO?

ODK and KoboToolbox use the same XLSForm functions: decimal-date-time(), date() and format-date(). SurveyCTO documents decimal-date-time() and date() as well, and uses format-date-time() for display. Test month arithmetic on your own platform with month-end dates before fieldwork.

No Credit Card Required

Build the form first. Add hosting when you need it.

Start free with LooprAI, then deploy a managed ODK Central server or run DataSnap checks when your project is ready.

Automation handles the mechanical work.

Researchers decide what the evidence means.