Dates, DST and Time Zones in Flowo's Planner
By Mykhailo Boichuk · Chief Technology Officer
In short
Flowo, the planning app from BBMM Technologies, treats a schedule block as an instant and a day as a calendar date, and it never moves between days by adding 24 hours. Planning code reads the time through an injected clock, builds day boundaries with the date constructor and counts days at UTC midnight. On the night the clocks go forward, a 23:00 to 07:00 sleep window produces a 7 hour block and the planner reports the missing hour as a shortfall.
The injected clock
Flowo's planning code asks an AppClock for the current time and does not read the system clock itself. The interface has one method, now.
There are two implementations. SystemClock returns the device time and is what the app runs with. FixedClock holds a value that a test sets and can advance by a duration.
More than 40 source files take a clock this way, including the planning engine, the series engine for repeating items, the quota placer, the reminder factory and the streak service. More than 80 test files construct a FixedClock.
The clock returns a date-time value, and the code keeps whatever kind it was given. A test can therefore pass a zoned value for Europe/Kyiv or Europe/Berlin and the planner computes in that zone on any machine.
We learned why that matters from one of our own tests. It used a plain local time for a DST night, and on most machines the local zone has no transition on that date. The assertion passed without ever meeting a short night. The test now uses a named zone.
Instants and calendar days
Flowo keeps two kinds of time value and does not mix them. A schedule block has a start and an end, which are instants. A task stores its deadline and its estimate as integer milliseconds.
A day is a calendar date. Date overrides and quota ledgers look a day up by an eight digit key in the form yyyyMMdd. One helper produces that key. Eleven files used to carry their own copy of it, and we replaced them with the single function so that no two parts of the app can disagree about a day.
Timestamps in our small state documents are written as ISO 8601 strings in UTC. The readers of the migration state and the backup manifest reject a timestamp that has no offset.
Calendar arithmetic
The rule in our planner is that a calendar boundary is built from date parts and never by adding a duration. The next midnight is the date constructor called with the day plus one. On a day when the clocks change, that midnight is 23 or 25 hours away, and adding 24 hours would land on the wrong side of it.
A helper called calendarDate builds these boundaries. It takes a template value and returns a result of the same kind. A zoned template gives a zoned result in the same location, and a UTC template gives a UTC result.
Counting days uses a second rule. Both dates are rebuilt at UTC midnight and subtracted, because UTC has no daylight saving. A 23 hour day then still counts as one day.
- Weekly quotas use the ISO week, Monday 00:00 to the next Monday 00:00.
- Monthly quotas run from the 1st at 00:00 to the 1st of the next month.
- Streaks reuse the same period boundaries, and tests cover three day runs across 29 March and 25 October 2026.
- The schedule screen's day index is tested across 211 calendar days from 28 March to 25 October 2026, a span with both European transitions.
- The day preview bar is scaled to the real length of that day in minutes.
Nights that cross midnight
A night in Flowo belongs to the day the person wakes up. The period for a sleep quota runs from 12:00 on the previous day to 12:00 on the day itself, and both boundaries are built with the constructor so they stay at 12:00 on a transition day.
Other sessions that cross midnight follow the same direction. A session declared as 23:00 to 01:00 belongs to the day it ends on. The planner widens its search just far enough to reach the evening half, and only for that session.
What the planner does on a DST night
On the night of 28 to 29 March 2026 in Europe/Kyiv, a sleep window of 23:00 to 07:00 contains seven real hours. Flowo places one block of 7 hours that starts at 23:00 and ends at 07:00.
The planner does not take the eighth hour from the morning. It reports it as a quota shortfall. This is also the result when the person has set the minimum sleep equal to the ideal 8 hours, because the session is pinned to its own night. A golden scenario locks this behaviour.
Repeating items keep their wall-clock time. A daily item at 18:00 in Europe/Berlin stays at 18:00 across both 2026 transitions, while its UTC offset moves from 1 hour to 2 in spring and back in autumn.
Reminders are computed on instants. The reminder time is the block's start minus the chosen number of minutes. In one test a block starts at 03:00 in Berlin on 29 March with a 30 minute reminder. The reminder fires 30 minutes of real time earlier, at 01:30, and never at the 02:30 that does not exist that night.
Time zones across boundaries
A zone name has to survive every hop, because an offset alone cannot say when the next transition is.
The planning request sent to the worker isolate carries instants as integer microseconds. When the clock supplied a zoned value, the request also carries the zone's name, and the worker rebuilds zoned values in that location. This field was added in version 8 of the message format.
For reminders, the app reads the device's IANA zone name at startup and sets it as the local location in the bundled time zone database. The operating system and the database do not always agree on a name. Europe/Kyiv and Europe/Kiev is the pair we handle explicitly, and a name the database does not contain falls back to UTC.
The onboarding request to our service includes the same IANA name.
Key takeaways
- Flowo's planning code reads time through an AppClock, with SystemClock in the app and FixedClock in tests.
- Day boundaries are built with the date constructor and never by adding 24 hours.
- Day counts are taken between UTC midnights, so a 23 or 25 hour day counts as one day.
- A night belongs to the day the person wakes, in a period that runs from 12:00 to 12:00.
- On a spring-forward night a 23:00 to 07:00 window yields a 7 hour block and a reported shortfall.
- DST tests run in named zones such as Europe/Kyiv, because a local time only meets a transition on some machines.
Frequently asked questions
- Why is adding 24 hours the wrong way to get tomorrow?
- On the two days a year when clocks change, the next local midnight is 23 or 25 hours away. Adding a 24 hour duration lands an hour before or after it. Flowo builds the next day with the date constructor and the day number plus one, which the runtime normalises.
- How should the number of days between two dates be counted?
- Flowo rebuilds both dates at UTC midnight and subtracts them. UTC has no daylight saving, so the difference is always a whole number of 24 hour days. Subtracting two local midnights can give 23 or 25 hours and round to the wrong count.
- What does Flowo do with a sleep window on the night clocks go forward?
- The window keeps its wall-clock hours and loses one real hour. For 23:00 to 07:00 the planner places a 7 hour block inside the window. The missing hour is reported as a shortfall for that night.
- How can date code be tested for daylight saving time on any machine?
- The test has to control the zone as well as the time. Flowo's tests pass a zoned value for a named location, such as Europe/Kyiv or Europe/Berlin, into a FixedClock. A plain local time would only meet a transition on machines set to a zone that has one on that date.
- Does Flowo store a time zone on each task?
- No. Task and block records in Flowo have no time zone field. Blocks are stored as instants, and the planner works in the zone the clock supplies, which on a phone is the device's current zone.
References
- Flowo
- BBMM Technologies
- RFC 3339: Date and Time on the Internet: Timestamps
- RFC 5545: iCalendar, recurrence rules
- IANA Time Zone Database
- Dart API documentation: DateTime class
About the author
Mykhailo Boichuk
Chief Technology Officer
Mykhailo builds the systems behind the apps. He works on macOS and iOS performance, and on the sync problems that come with software that has to run offline.