Skip to content
OneKitly

Date Arithmetic Is Harder Than It Looks

Published 6/6/2025 · 15 min read · Everyday calculators

Lena Hoffmann

Lena HoffmannScience & education writer at OneKitly

Mathematics · Physics

Checked against 5 sources

View profile
In short

Ask what January 31 plus one month is and there is no answer the mathematics forces on you. Most systems clamp to the end of the shorter month and return February 28, 2026 — or February 29 in a leap year like 2024. Naive day arithmetic returns something else entirely: adding 31 days gives March 3, adding 30 days gives March 2. All three are defensible, which is exactly the problem. Clamping has a consequence people rarely notice: month addition is neither invertible nor associative. March 31 minus one month is February 28, and adding a month back gives March 28, not March 31. January 31 plus one month plus one month is March 28, but January 31 plus two months is March 31. Time-of-day arithmetic breaks in a different direction. In Europe/Paris, March 29, 2026 is 23 hours long and October 25 is 25 hours long, so adding 86,400 seconds to a 9:00 appointment on March 28 lands at 10:00 the next day. And age is a calendar comparison, not a division: over every birthday from 1930 to 2020, days ÷ 365 gives the wrong age 3.49% of the time. The rule that resolves all of it: do calendar arithmetic in calendar fields, do instant arithmetic in UTC, and never mix the two.

"One month later" has no single correct answer, and every date library has had to pick one. Month addition is neither associative nor invertible, a day is not always 24 hours, and age is not days divided by 365.25.

There is no arithmetic that forces an answer

Adding one to a number is unambiguous. Adding one month to a date is not, because months are not a unit — they are labels of unequal length, running from 28 to 31 days, and the length of the one you land in depends on which one you started from. January 31 plus one month has to land somewhere in February, and February has no 31st. Something must give. The near-universal choice is to clamp: keep the month, keep the year, and pull the day back to the last valid one. That gives February 28, 2026, and February 29, 2024. Java's java.time, the ECMAScript Temporal API, PostgreSQL's interval addition and Python's dateutil all behave this way, and they behave this way because the alternative is worse.

The alternative is to treat a month as a fixed number of days. Pick 30 and January 31 plus one month becomes March 2, 2026; pick 31 and it becomes March 3. Both skip February entirely, which is exactly what a user asking for "one month later" does not want. The table above runs all three definitions against five starting dates, and they only agree when the day of the month is small enough to exist everywhere. That is the practical takeaway: every method is identical on the 1st through the 28th, and every method differs somewhere on the 29th, 30th and 31st. Roughly one date in ten is in the danger zone, which is why the bug survives testing so easily.

Clamping costs you associativity and invertibility

Run this and watch a property you assumed you had disappear. March 31, 2026 minus one month clamps to February 28. Add one month back and you get March 28 — three days short of where you started. Month addition is not invertible: subtracting then adding is not the identity. The same defect shows up as a failure of associativity. January 31, 2026 plus one month plus one month is March 28, because the intermediate value was clamped to February 28 and the clamp is permanent. January 31 plus two months, computed in one step, is March 31. Two expressions that ought to be the same value differ by three days.

This is not a bug in any particular library — it is a consequence of the calendar, and every library that clamps inherits it. The practical rule that follows is worth writing on a sticky note: never build a monthly series by repeatedly adding one month to the previous result. Always add n months to the original anchor date. A subscription that starts on January 31 and renews by iteration will drift to the 28th and stay there forever; the same subscription anchored to January 31 and computed as start + n months lands on February 28, March 31, April 30, May 31 — which is what the customer expects and what the payment processor will bill. The bug is invisible for eleven months of the year and then arrives all at once.

A day is not 24 hours — Europe/Paris, March and October 2026

Take the real tz database rules rather than an assumption. In 2026 Europe/Paris moves from UTC+1 to UTC+2 on March 29 and back on October 25. Measure the distance between local midnight on March 29 and local midnight on March 30: it is 2026-03-28T23:00Z to 2026-03-29T22:00Z, which is 23 hours. Do the same across October 25 and you get 2026-10-24T22:00Z to 2026-10-25T23:00Z, which is 25 hours. The calendar day and the 86,400-second day are different objects, and twice a year they visibly come apart. America/New_York does the same thing on different dates — 23 hours on March 8, 2026 and 25 hours on November 1.

The consequence lands on real appointments. A 9:00 slot on March 28, 2026 in Paris is the instant 2026-03-28T08:00Z. Add exactly 24 hours of elapsed time and you get 2026-03-29T08:00Z, which in Paris reads 10:00 — the meeting has moved an hour later. Do it in October and it moves an hour earlier: 9:00 on October 24 plus 24 hours is 8:00 on October 25. Neither is what "same time tomorrow" means. "Same time tomorrow" is a calendar operation: increment the date field, keep the wall-clock field, then re-resolve the instant against the zone. Adding a duration is a physics operation. They agree on 363 days of the year, which is precisely enough to make the failure look random.

The silent rollover: February 30 does not raise an error

In JavaScript, new Date(2026, 1, 30) does not throw. It returns March 2, 2026. The constructor accepts any integer and normalises by carrying the excess into the next month, so a day field of 30 in a 28-day February quietly becomes the 2nd of the following month. The same normalisation turns month index 12 into January of the next year and a day of 0 into the last day of the previous month — which is the trick behind the common idiom for "days in this month", new Date(y, m, 0).getDate(). It is a useful behaviour when you want it and a silent data corruption when you do not, and nothing in the return value tells you which case you are in.

This is why validating a date means more than checking that the parser did not complain. A form that accepts 30/02/2026 and stores 2026-03-02 has lost the user's error rather than reported it, and the record now says something the user never typed. The defensive check is one line: construct the date, then verify that the year, month and day you get back are the three you put in. If they are not, the input was not a real date. The other half of the defence is knowing how long each month is before you build the input at all — 31, 28 or 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 — which is what the days-in-month calculator exists to answer for any year you name.

Age is a comparison, not a division

The tempting shortcut is to count the elapsed days and divide by 365.25, on the reasoning that the average year is 365.25 days long. Test it. Someone born on August 23, 2000, asked on August 22, 2026, has lived 9,495 days. Divide by 365 and you get 26.0137, which floors to 26 — but the person is 25 and their birthday is tomorrow. Divide by 365.25 and you get 25.9959, which floors to 25 and happens to be right. So run the whole space rather than one example: every birthday from January 1, 1930 to January 1, 2020, evaluated on August 22, 2026, is 32,873 dates. Days ÷ 365 gives the wrong age on 1,148 of them, or 3.49%. Days ÷ 365.25 is far better but still wrong on 44 of them, or 0.13%.

The residual failures are the cruellest kind: they land on the birthday itself. Someone born on August 22, 1932 has lived 34,333 days by August 22, 2026, and 34,333 ÷ 365.25 = 93.9986, so the division says 93 on the morning they turn 94. No refinement of the divisor fixes this, because no single divisor can, and that is the whole point — the calendar is not a uniform scale. The correct algorithm has no division in it at all. Subtract the birth year from the current year, then subtract one more if the current month and day have not yet reached the birth month and day. Three integer comparisons, exact everywhere, and it never needs to know how long a year is.

The rule: calendar fields for calendar work, UTC for instants

Almost every date bug is one of two mistakes. Either a calendar question was answered with elapsed time — "a month" turned into 30 days, "tomorrow" into 86,400 seconds, "age" into a division — or an instant question was answered in local fields, so a stored timestamp shifted when the offset changed. The fix is to decide, before writing a line, which kind of quantity you are handling. Renewal dates, birthdays, deadlines, invoice periods and opening hours are calendar quantities: keep them as year, month, day and wall-clock time with a named zone, and do the arithmetic on those fields. Timeouts, log ordering, cache expiry, rate limits and durations are instant quantities: keep them as UTC timestamps and add seconds.

Where the two must meet — a reminder at 9:00 local time, sent by a server that only understands instants — convert at the boundary and only at the boundary. Do the calendar step first in the user's named zone, resolve the result to a UTC instant once, and hand that instant to the scheduler. Storing the offset instead of the zone name breaks the moment the political rules change, which they do several times a year; the tz database issues releases precisely because governments keep moving their transitions. And never store a future local appointment as a UTC instant alone, because if the rules for that zone are amended before the date arrives, the instant you saved will no longer correspond to 9:00 in anybody's morning.

The same starting date, three defensible definitions of "a month later" — and the answers diverge
Starting date+ 1 month, clamped+ 30 days+ 31 days
January 31, 2026February 28, 2026March 2, 2026March 3, 2026
January 31, 2024 (leap year)February 29, 2024March 1, 2024March 2, 2024
March 31, 2026April 30, 2026April 30, 2026May 1, 2026
August 31, 2026September 30, 2026September 30, 2026October 1, 2026
November 30, 2026December 30, 2026December 30, 2026December 31, 2026
Days in a Month CalculatorThe number of days in any month of any year from 1 to 9999, the leap rule worked through line by line for February, the weekday of the 1st and of the last day, how many of each weekday the month holds, and the calendar itself.Try the tool

Frequently asked questions

What is January 31 plus one month?
It depends on the definition your system uses, and there is no answer mathematics forces. Clamping — the behaviour of java.time, the Temporal API, PostgreSQL intervals and Python's dateutil — keeps the month and year and pulls the day back to the last valid one, giving February 28, 2026 or February 29, 2024. Fixed-length day arithmetic gives something else: plus 30 days is March 2, 2026 and plus 31 days is March 3. Clamping is almost always the right choice for human-facing dates, because a user asking for "one month later" means the corresponding date in the next month, not a fixed number of days. If you are billing, contracting or scheduling, state which rule you use in the terms, because customers do notice when a January 31 subscription renews on February 28.
Why does subtracting a month and adding it back not return the original date?
Because the clamp destroys information and nothing can restore it. March 31, 2026 minus one month has to land in February, February has no 31st, so the result is clamped to February 28. That result no longer remembers it came from a 31st. Adding one month to February 28 therefore gives March 28, and you are three days short of where you began. The same mechanism costs you associativity: January 31 plus one month plus one month is March 28, while January 31 plus two months in a single step is March 31. The practical consequence is a rule you should apply everywhere: build recurring schedules by adding n months to the original anchor date, never by iterating one month at a time from the previous result. Iteration lets a single clamp propagate forever.
Is a day always 24 hours?
No, not as a local calendar day. In Europe/Paris in 2026, March 29 lasts 23 hours and October 25 lasts 25 hours, because the zone moves from UTC+1 to UTC+2 and back. Measured as instants, local midnight on March 29 is 2026-03-28T23:00Z and local midnight on March 30 is 2026-03-29T22:00Z — 23 hours apart. America/New_York does the same on March 8 and November 1, 2026. Some transitions are not even whole hours; Lord Howe Island shifts by 30 minutes. Zones that observe no daylight saving have 24-hour days year-round, but you cannot assume your users are in one. The safe habit is to treat "a day" in a calendar sense as an increment of the date field, resolved against a named zone, and to reserve 86,400 seconds for genuine elapsed-time work done in UTC.
How should I calculate someone's age?
With comparisons, never with a division. Subtract the birth year from the current year, then subtract one more if the current month is before the birth month, or the months match and the current day is before the birth day. That is exact for every date. The divisions fail measurably: over every birthday from 1930 to 2020 evaluated on August 22, 2026 — 32,873 dates — elapsed days ÷ 365 gives the wrong age 3.49% of the time and ÷ 365.25 gets it wrong 0.13% of the time. Worse, the residual failures cluster on the birthday itself, which is the one day people check. Someone born August 22, 1932 has lived 34,333 days by August 22, 2026, and 34,333 ÷ 365.25 = 93.9986, so the division reports 93 on the morning they turn 94. February 29 birthdays need a separate policy decision, since jurisdictions differ on whether the legal birthday in a common year is February 28 or March 1.
Why does my form accept February 30 without complaining?
Because most date constructors normalise rather than validate. In JavaScript, new Date(2026, 1, 30) returns March 2, 2026 without an error: the day field overflows and the excess is carried into the next month. The same rule turns a month index of 12 into January of the following year and a day of 0 into the last day of the previous month, which is why new Date(y, m, 0).getDate() is the common idiom for the length of a month. Nothing in the returned value distinguishes a deliberate overflow from a typo. The defence is a round-trip check: build the date, then assert that the year, month and day you read back are the three you wrote. If they differ, reject the input. Doing this at the boundary is much cheaper than discovering later that a database column contains a date nobody ever entered.
Should I store dates in UTC or in local time?
It depends on what the value means, and answering "always UTC" causes as many bugs as it prevents. Anything that records when something happened — a log line, a payment, a cache expiry, a rate-limit window — is an instant, and instants belong in UTC. Anything that records when something is meant to happen in someone's day — a 9:00 reminder, a delivery window, a store's opening hours, a recurring meeting — is a calendar value, and storing it as a bare UTC instant is a bug waiting for a rule change. Governments amend daylight-saving rules several times a year, and the tz database ships releases to track them; a future appointment frozen as an instant will drift off the intended wall-clock time when its zone is amended. Store those as local date, local time and the IANA zone name, and resolve to an instant only at the moment you need to act.

Articles you may find interesting

All guides
ExplainerLeap Years: The Rule, the Exception, and the Exception to the ExceptionDivisible by four, except centuries, except centuries divisible by four hundred. The three-line rule exists because the tropical year is not 365.25 days, and the arithmetic behind it explains ten deleted days in 1582 and a calendar that repeats exactly every 400 years.ExplainerWorking Out an AnniversaryCounting back and counting forward are two different sums, and they disagree by a year on the day itself. What this calculator does with 29 February, what other date libraries do, and where the traditional and modern gift lists actually come from.How-toHow Many Weeks Until a Date? (With Worked Examples)Count the weeks between today and any future date. Learn the simple divide-by-seven method, when to round, and the difference between inclusive and exclusive counting.How-toHow to Add or Subtract Days from a DateCount forward to add, backward to subtract, rolling over month ends. Here's how, why month lengths and leap years trip you up, and when weekends count.How-toHow Many Days Until a Date? (With Worked Examples)Count the days until any future date, understand inclusive vs. exclusive counting, and build an accurate countdown you can trust.ExplainerReading and Writing Military Time0800 is not “eight o'clock” and not “08:00” — it is “zero eight hundred”, with a zone letter on the end. The four digits, the spoken form, midnight at 0000, the argument about 2400, and the reason 12-hour notation cannot settle noon.

Related tools

Sources

Spotted a mistake in this article?