A lesson that moved 4.5 hours and a clash warning you could book past. One was a bug, one was a feature. What testing a booking calendar taught one of our software engineers about edge cases.

A booking calendar looks like the simplest screen in an app. You pick a learner, pick a time, tap save, and the lesson appears in a grid. Testing one properly says otherwise.
I’m Sadeepa, a software engineer at Lunova, and I build and test Drivio, a platform for driving instructors and schools. Two findings from testing its lesson diary stood out. One was a real bug. The other was a design decision I’d misread, and telling the two apart taught me as much as finding them.
I booked a lesson for 09:00 to 11:00 and it appeared at 13:30 to 15:30. My phone was set to Sri Lanka time (UTC+5:30), and the instructor’s account was set to London, which in September was on British Summer Time (UTC+1). The gap was exactly four and a half hours. I’ve told the story of finding it in my first three months as a remote software engineer. Here I want to look at why it’s such an easy bug to write.
In JavaScript, the obvious way to read the hour from a timestamp uses the phone’s own clock. MDN’s reference for getHours() says it returns the hour “according to local time”, so the same moment gives a different answer in each zone. This is a simplified example, not our production code:
// Simplified example, not production code.
// Reads the hour in whatever time zone the phone is set to.
function hourOnDevice(iso: string): number {
return new Date(iso).getHours()
}
hourOnDevice('2026-09-14T08:00:00Z') // 9 in London, 13 in Colombo, 4 in New YorkThe cure for this class of bug is to name the zone every time you read a time. Intl.DateTimeFormat takes a timeZone option for exactly that, so the answer no longer depends on where the phone is:
// Reads the hour in a named time zone, wherever the phone is.
function hourIn(iso: string, timeZone: string): number {
const parts = new Intl.DateTimeFormat('en-GB', {
timeZone,
hour: 'numeric',
hourCycle: 'h23',
}).formatToParts(new Date(iso))
return Number(parts.find((part) => part.type === 'hour')?.value)
}
hourIn('2026-09-14T08:00:00Z', 'Europe/London') // 9 on every phoneThe part that stuck with me is why the automated tests hadn’t caught it. They all ran on London time, the same zone as the account, so both sides agreed by accident. The tests weren’t wrong; they could never see the difference. We now run part of the tests in a second time zone, so code that quietly relies on the phone’s clock fails a test instead of reaching a phone.
When I booked a lesson that partly overlapped another, the diary showed a red warning. But I could still book it, and at first I took that for a bug.

It’s a deliberate design, and a sensible one:
Instructors sometimes need to double-book on purpose. The soft block allows that without making it easy to do by accident.
You can try it yourself: the diary and its clash guard are part of Drivio’s free plan, for one instructor with no card needed. Book two lessons over each other and see what happens.
The lesson for me as a tester was simple: read the intended behaviour before deciding something is broken.
Boundary value analysis means testing at the edges of each group of inputs that should behave the same way. For two time ranges, the groups are easy to list. Against an existing lesson from 09:00 to 11:00:
The two “No” rows matter as much as the “Yes” rows. Back-to-back lessons are normal for instructors, and a check that blocked them would be as frustrating as one that missed clashes.
The rule that gets every row right treats each lesson as including its start but not its end. Two lessons then clash when each starts before the other ends. Edsger Dijkstra made the case for this convention in “Why numbering should start at zero”: with the lower bound included and the upper bound excluded, two adjacent ranges meet where “the upper bound of the one equals the lower bound of the other”.
// Start included, end excluded, so back-to-back lessons never clash.
type Slot = { start: number; end: number } // minutes since midnight
function clashes(a: Slot, b: Slot): boolean {
return a.start < b.end && b.start < a.end
}Alongside those seven cases, I now:

Manual testing found these issues, but it won’t stop them coming back. The Practical Test Pyramid, by Ham Vocke on Martin Fowler’s site, puts it plainly: write “lots of small and fast unit tests” and “very few” tests that run end to end. The table above is a ready-made set of unit tests for an overlap function, and they run in milliseconds.
The time zone case is automated in the same spirit: a second run of the tests with the time zone set to somewhere other than where you build. On POSIX systems that is the TZ environment variable. It costs one small extra test config and catches a whole class of bug.
Before this, I treated a test case as a check that a feature works. Now I treat it as a question about where the feature’s edges are, and every finding as a question about what the code intended. One of my findings was a real bug and the other was the product working as designed. Both were worth writing up.
Calendars and bookings carry more edge cases than most screens in an app. Lunova Labs designs and builds web and mobile apps (iOS and Android), and tests them properly before your customers find the edges for you.
Ready when you are