A friend's tracking sheet had two columns of dates. One moved the last logged day forward by one calendar month, the other added 28 days to it. Starting from 2026-01-31, both columns wrote 2026-02-28 for February; by the end of the year one read 2027-01-31 and the other 2027-01-02, 29 days apart. Each of them was sure of their own arithmetic, so we took the sheet and checked it against what the page really does: the same day every month and every 28 days are two different sums.
This page only works in days
The period-calculator template is 244 lines and the script sits between L87 and L243. I lifted the script out unchanged and ran it on a DOM stub (Node v24, today frozen at 2026-10-22): 0 calls to setMonth in the whole file, and setDate appears in exactly 1 place, the addDays helper at L125-129 whose body is d.setDate(d.getDate() + n). There is no calendar month anywhere on this page. Every prediction is the start date plus a whole number of cycle lengths.
The "next 3 periods" list is therefore 1, 2 and 3 cycle lengths, not 3 calendar months. With start 2026-01-31 and cycle 28 it returns 2026-02-28, 2026-03-28 and 2026-04-25, so the third entry is neither the 31st nor the end of a month. The promo line that promises periods for the next 3 months would land on 2026-04-30 if it really walked calendar months, 5 days off. The other direction needs guarding too: a bare JS setMonth pushes 2026-01-31 to 2026-03-03, because February has no 31st and the date just overflows. Only a hand-written end-of-month clamp returns 2026-02-28, and no such clamp exists on this page; that one came from my own outside comparison.
How many rounds before the two tracks fork
Calendar months swing between 28 and 31 days, while a 28-day cycle is a fixed length. Line them up and the gap shows:
| Round | Same day each month | Every 28 days | Gap |
|---|---|---|---|
| Round 1 | 2026-02-28 | 2026-02-28 | 0 |
| Round 3 | 2026-04-30 | 2026-04-25 | -6 |
| Round 6 | 2026-07-31 | 2026-07-18 | -13 |
| Round 12 | 2027-01-31 | 2027-01-02 | -29 |
| Round 24 | 2028-01-31 | 2027-12-04 | -58 |
Figure 1: how many days earlier the 28-day track on this page runs than the same calendar day. The green bar is the 29 days from the title, and the same-day column comes from a clamp that lives outside the page.
Round 2 is already -3 days apart, 2026-03-31 against 2026-03-28. Trying to smooth it over with "a year is just 12 months of 30 days" is wrong on both sides: from the same start, 360 days gives 2027-01-26, 372 days gives 2027-02-07, and 12 cycles of 28 days only reach 2027-01-02. Crossing a leap year adds one more layer: 2026-01-31 plus 365 days is exactly 2027-01-31, while 2024-01-31 needs 366 days to come back to 2025-01-31, and with only 365 days it stops at 2025-01-30. How many days February has is handed entirely to JS Date, and the template carries 0 lines of its own leap-year test: 2024 has 29 days in February, 2000 has 29, 1900 has 28, 2100 has 28. What counts as the anniversary of a birthday on the 29th is argued through in the leap-day birthday post.
The output is fixed, and what is missing really is missing
The panel always shows 4 badges, next period, ovulation day, fertile window and safe period, plus 3 prediction rows and 5 rows of date notes. There is only 1 date input and no end date, so the page cannot say how many days separate two dates, and there is no year, month, week, hour or minute breakdown either. The only division in the code is at L122, Math.round((target - today) / 86400000), so the granularity is days, and the only signed day count is the note that reads "-236 days from today". Ovulation is always 14 days before the next period; the fertile window is always 5 days before it to 4 days after, 10 days in total. The safe period is measured two ways at once: between 2026-02-19 and 2026-02-27 the badge gives only 9 days, while the timeline paints 13 green cells for the very same input. The disclaimer is blunt about it and says the result cannot be used as contraception. To watch how much time is left before one particular date you need a page that takes a target date, something in the style of the holiday countdown.
The timeline is 35 cells wide, and what does not fit is not the same as what has not arrived
var totalDays = 35 nails the window down to a grid of 7 columns. In the scan that starts 2026-01-31 with a period of 5 days, cycle 21 leaves the next period 5 cells, cycle 27 still leaves 5, cycle 33 leaves only 2, and cycles 39 and 45 leave 0: with cycle 45 the next period falls on day 46, outside the window, and the whole tail gets topped up with green. The group starting 2026-03-08 with a period of 10 days and cycle 45 comes out as period 10 cells, fertile 9 cells, safe 16 cells, next period 0 and not-yet 0, and the title of the last cell only reaches day 35. A missing red cell means the window was cut short, not that the day has not come. The fill order is also first-come-first-served: with a period of 10 days and cycle 21, the 10-day fertile window loses 8 days to the period and keeps only 2 cells.
Figure 2: the same 35 cells filled under two cycle lengths. Dark is the period, light grey the fertile window, green the safe period, translucent the next period, empty the days that never arrived.
The input side hardly ever complains
The whole page carries only 3 validation messages: pick a start date for the last period, period length should be between 2-10 days, cycle length should be between 21-45 days. The date field has no min and no max, parseDate only does split('-') and then parseInt and never asks whether the date is real. 2026-02-31 is swallowed and read as 2026-03-03, and the next period still comes back as 2026-03-31. 2026-13-01 is swallowed into 2027-01-01. A value with one segment missing, 2026-10, and one that sits at the JS Date ceiling, 275761-01-01, do not even raise a message; the badge just prints NaN-NaN-NaN. Decimals are cut off inside the functions, so a period of 5.9 and a cycle of 28.9 still produce output. The real ceiling is 21-45, which means the claim that a normal cycle is 21-35 days stops nothing at all: enter 40 as the cycle and the page returns 2026-03-12. So when you check the work, read the date string that is echoed back first instead of waiting for a red warning.
Three questions that came up afterwards
Q: Which rule should a log follow?
The cycle length in days, which is what this page implements: the start plus whole multiples of that length. Do not run half a sheet on calendar same-day stepping and the other half on fixed day counts. By round 12 they are 29 days apart and by round 24 they are 58 days apart. Both tracks stay internally sound and simply move further away from each other.
Q: Does it shift the 31st to the end of February by itself?
It does not. There are 0 setMonth calls and no clamp branch anywhere. 2026-01-31 plus 28 days is 2026-02-28, and plus 30 days is 2026-03-02; landing on a month end is only because the cycle length happens to line up with one.
Q: Can that safe period row be relied on?
No. The disclaimer says outright that it cannot be used as contraception, and for one and the same input the 9 days from the badge and the 13 cells from the timeline do not even agree on what length means.
Back to the sheet with two columns: the 2026-02-28 cell is the same day under both rules, while at the end of the year one column reads 2027-01-31 and the other 2027-01-02. Nobody miscalculated; the two sentences were never about one thing. If the log has to hold for years, write the rule into the column header first and then pick a tool, because a tool only carries the track you chose all the way to the end. Put those 3 inputs into the period calculator and it advances the cycle multiplied by a count of rounds, not a calendar month.