Writing · CitizenDays · 13 min read
Counting days the way the government does
Australia, Canada, the UK, the US and the Schengen area all limit how long you can be away, and no two count a day the same way. How CitizenDays turns trips into days, checks rolling windows without counting day by day, plans trips before they're booked, and is a day stricter than one official calculator on purpose.
If you're on the way to citizenship somewhere, someone is counting your days. Australia wants no more than 90 days away in the twelve months before you apply. Canada wants 1,095 days in the country out of the last five years. The UK's settlement routes break if you spend more than 180 days away in any twelve months, the US presumes that a single trip of more than six months broke your residence, and a visitor to the Schengen area gets 90 days in any 180. Get the count wrong by one day and the cost is an application refused, or an overstay on your record.
CitizenDays keeps that count. You log your trips, where you went and when you left and came back, and for each goal it tells you how many days you've used, the earliest date you can apply, how long you can safely be away and the last day you must be back. The adding up is easy. Knowing what to add up, and doing it fast enough to ask a thousand what-ifs, was the actual work.
1A trip is not a number of days
Take one trip. You leave Canada on 1 July for Spain and fly back on 15 July. How many days were you away?
Canada says 13. Its physical presence calculator is explicit that the day you leave and the day you return are days in Canada, "because you were physically in Canada for a part of the day of departure or arrival". Australia, the UK and the US use the same convention. The Schengen area uses the opposite one: under the Schengen Borders Code, the date of entry is the first day of stay and the date of exit is the last. Spain counts 15.
Canada: days away –Schengen: days of stay –
So the engine never stores "days away". It turns trips into two kinds of day, whole days away and days of stay, and each rule reads the kind its own text describes. Every counting convention has a test, and the test quotes the official sentence it comes from. When someone asks why a number is what it is, the answer is a citation, not my opinion.
The rules are written for people filling in forms, not for software, so they leave gaps. If one trip ends on the day the next one begins, did you come home in between? CitizenDays reads the two as one journey, so the shared date counts as a day away: leave for France on the 1st, go on to Spain on the 5th, come home on the 9th, and that's seven days away however it was logged. If you did come home that day, the count is one day high, never one day low. A trip still in progress counts every day up to today as away. And dates are calendar dates in the traveller's own time, kept as whole numbers of days, so "the 179 days before" is a subtraction and a clock change can never move a day.
// a calendar date as a whole number of days: no hours, no time zones, no daylight saving
const dayNumber = (iso) => Date.UTC(+iso.slice(0, 4), +iso.slice(5, 7) - 1, +iso.slice(8, 10)) / 86_400_000;
// the obvious way to count: days of stay in the window that ends on `day`
function usedOn(day, stayDays) { // stayDays: a Set of day numbers spent in the area
let n = 0;
for (let d = day - 179; d <= day; d++) if (stayDays.has(d)) n++;
return n;
}
2Days as runs, not lists
That loop is correct, and it's exactly what the test suite uses as its reference. It's also the wrong shape for what the app does all day. A rolling window has to be checked on every day of a stay; "how long can I still be away?" asks it again for every candidate return date; and the planner asks all of that once more for every goal. Counted day by day, every one of those questions costs as many steps as the window has days.
So the engine keeps a set of days as sorted runs. A trip from the 11th to the 24th is one run, not fourteen entries, and overlapping or touching runs are merged as they arrive. Next to the runs sits a running total of how many days came before each one. To count the days between two dates, the engine finds where each date falls with a binary search and subtracts the two totals. A decade of travel is a few dozen runs, and any window, however long, is answered in a handful of steps.
/** The number of days in the set that are earlier than `day`. */
private countBefore(day: number) {
// find the first run that ends on or after `day`; every run before it lies wholly earlier
let low = 0, high = this.spans.length;
while (low < high) {
const middle = (low + high) >> 1;
if (this.spans[middle].end < day) low = middle + 1;
else high = middle;
}
const span = this.spans[low];
return this.before[low] + (span && span.start < day ? day - span.start : 0);
}
/** How many days of the set fall from `from` to `to`, both included. */
count(from: number, to: number) {
return to < from ? 0 : this.countBefore(to + 1) - this.countBefore(from);
}
days away in the window –binary-search steps –a day-by-day count reads –
3Windows that move
The second difference between rules is the window. Australia and Canada look back over a fixed period that ends the day before you apply. Move the application date and the period moves with it, but there is only ever one period to check. The UK and Schengen rules roll. "No more than 90 days in any 180-day period" means every single day of a stay has its own window, that day and the 179 before it, and every one of those windows has to hold 90 days or fewer.
Rolling windows give answers that look wrong. The European Commission's manual for its own short-stay calculator has a traveller who was in the Schengen area from 1 to 10 January, 1 to 30 March and 1 May to 9 June 2024. If they come back on 7 August, they can stay 20 days. If they come back on 8 August, one day later, they can stay 50.
in the window on arrival –can stay –leave by –
The figure shows why. Days don't come back on a schedule: each one leaves the window exactly 180 days after it was spent. Arrive on 7 August, and on the 21st day of the stay the window still starts on 1 March and holds all 70 of the earlier days: 91, one too many. Arrive on 8 August, and the 21st day is 28 August, the day 1 March leaves the window. From then on every new day in Spain is matched by a day of March leaving, and the stay can run until March is used up. A day's difference in arrival is thirty days' difference in stay, which is not something anyone should have to work out on a napkin.
4The worst twelve months
The UK's rule is the most demanding kind of rolling window: no more than 180 days away in any 12 months of a qualifying period that is usually five years long. Taken literally, that's every 12-month period starting on every day of those five years, more than 1,800 windows, each one needing a count.
Most of them can't matter. Slide a 12-month window forward from a day you spent at home and nothing leaves it until your next absence begins, while its far end only takes in more days. So the count can only peak in a window that starts on the first day of an absence, and those are the only windows the engine checks: a few dozen instead of eighteen hundred, each answered by the run lookup above.
this window –worst 12 months –windows checked –
The same reasoning answers the question people actually ask: how much longer can I stay away? One more day away can never help, so the answers run yes, yes, yes, then no forever after, and the engine finds the edge with a binary search over the number of extra days, checking each guess against every window it touches.
/** The largest n from 0 to `max` for which `holds(n)` is true, or -1 when it fails even at 0. */
export function lastThatHolds(max: number, holds: (n: number) => boolean): number {
if (max < 0 || !holds(0)) return -1;
let low = 0, high = max;
while (low < high) {
const middle = Math.ceil((low + high) / 2);
if (holds(middle)) low = middle;
else high = middle - 1;
}
return low;
}
5Five rules, side by side
Each rule lives in its own pack: an id and a version, a plain-English summary, the counting conventions, the official sources, the date they were last checked, the dates it needs from the user, and the worked examples from its own guidance. A new country is a new folder, never a new app. The user interface never does day arithmetic. It asks the engine one question, what does this goal look like on this date, and draws the answer. These are the five packs as they stand:
| Rule | Counts | Window | Limit | Earliest date |
|---|---|---|---|---|
| Australian citizenship | whole days away | fixed: 4 years and 12 months before applying | at most 365 days away in 4 years and 90 in 12 months; 4 years' residence, 12 months as a permanent resident | the first day all four hold |
| Canadian citizenship | days present, in half-days | fixed: 5 years before signing | 1,095 days present; days before permanent residence count half, up to 365 | the first day the total reaches 1,095 |
| UK settlement | whole days away | rolling: any 12 months of the qualifying period | at most 180 days away | up to 28 days before the period ends |
| US naturalization | whole days away | fixed: 5 years before filing, 3 for spouses of US citizens | 913 days present (548 for spouses); one absence of more than 180 days is presumed to break residence, a year or more breaks it | 90 days before residence is met |
| Schengen 90/180 | days of stay | rolling: each day and the 179 before it | at most 90 days | none: it's a limit, not an application |
Canada's arithmetic runs in half-days, so 1,095 days is 2,190 half-days and nothing is ever rounded.
6A day stricter, on purpose
Australia publishes a Residence Calculator, and it was the obvious thing to test against. It runs on the Home Affairs server and turns away plain page fetchers, so I wrote a small script that drives the real page in headless Chrome, and ran test trips through it. Permanent residence from 1 January 2020, lawful residence from 1 January 2015, applying on 5 October 2026:
| Trip | Whole days away | Home Affairs calculator | CitizenDays |
|---|---|---|---|
| 1 Jun to 31 Aug 2026 | 90, all in the last 12 months | met | met |
| 1 Jun to 1 Sep 2026 | 91 | not met, reapply 2 Jun 2027 | not met, earliest 3 Jun 2027 |
| 1 Jan 2024 to 1 Jan 2025 | 365 | met | met |
| 1 Jan 2024 to 2 Jan 2025 | 366 | met | not met |
| 1 Jan 2024 to 3 Jan 2025 | 367 | not met, reapply 2 Jan 2028 | not met |
| 4 Oct 2025 to 4 Jan 2026 | 91, the first on 5 Oct 2025 | met | not met |
| no trips, permanent resident from 1 Mar 2026 | not met, reapply 1 Mar 2027 | earliest 1 Mar 2027 | |
| no trips, living in Australia from 1 Feb 2023 | not met, reapply 1 Feb 2027 | earliest 1 Feb 2027 |
Checked on 5 October 2026. The first two rows confirm that travel days count as days in Australia.
In two rows CitizenDays is stricter than the government's own calculator, and that's deliberate. The calculator allows 366 days away in four years, where the written guidance says the limit is 365 days, "12 months". And it ignores a day away that falls on the first day of a look-back period, where the Citizenship Act says "the period of 12 months immediately before". When official sources disagree with each other, CitizenDays takes the reading that counts more days away. The price is that its earliest date can be a day or two later than the calculator's. It is never earlier, and a warning that comes a day early is one nobody regrets.
7Try the trip before you book it
Most people don't need to know where they stand today so much as what a trip would do to them. The planner takes a trip that isn't booked yet, adds it to the log, runs every goal again, and sorts the effect into four levels: nothing to worry about; still within the rule, but now close to a limit; the earliest application date moves later; or a limit is broken, which includes a planned application date that would no longer work. Then it answers the question behind the question: leaving on that day, what's the latest you can come back without delaying or breaking anything? That's one more binary search, over return dates up to two years out.
Australian citizenship –Schengen –
8Five forms, five layouts
An application doesn't ask whether you met the rule. It asks for a list of every absence, and every form wants that list differently. Australia's Form 1300t wants every country of the last ten years, in date order, and says to leave out trips that came back the same day. Canada's calculator wants every absence in the five years before you sign, same-day trips included. The US Form N-400 wants five years of trips, most recent first, without day trips that were over within 24 hours. So the export builds each list from the same log with the form's own rules and column names, and the PDF and the CSV are both drawn from that one list, so they can never disagree with each other or with the counts.
trips listed –left out –
One small thing in the CSV writer: a field that starts with =, +, -, @ or a tab gets a leading apostrophe. Otherwise a reason typed as "=something" would run as a formula when the file is opened in a spreadsheet, and an export is exactly the kind of file people open without a second thought.
9Fast answers, checked the slow way
Runs, running totals and binary searches are fast, and they're also exactly where off-by-one errors live. So the tests come in three layers. One test per counting convention, quoting the sentence it comes from. The worked examples from each rule's own official guidance, including the Commission's traveller above, run against every pack. And a third layer, the one I trust most, which generates 520 messy trip logs from a seeded random generator, with trips back to back, overlapping, same-day and still in progress, and answers every question twice: once with the engine, and once the slow and obvious way, a day at a time, straight from the definition. Every answer has to match. Because the generator is seeded, any disagreement replays exactly.
logs checked –disagreements –
10Rules change
All of this rests on text that governments revise. The UK guidance page I started from turned out to cover only a few older routes, because the main ones now sit under a newer appendix of the Immigration Rules, and a longer "earned settlement" is already proposed. So every rule pack records the date its sources were last checked, and the release check fails once that date is more than 120 days old. Changing a rule means quoting the new text, bumping the pack's version (never its id), and adding the test that names the new convention.
A few choices sit around the engine rather than in it. Trips are typed in by hand: CitizenDays asks for no location permission, and its trip map is drawn from country codes alone. The figures in the articles on its website run the app's own engine, bundled for the browser, so the site can't disagree with the app. And the app says plainly that it isn't legal advice, and means it. But when it says you can stay until 26 September, that date comes from the same arithmetic as the European Commission's own examples, and it has been checked against them.