Calendar file
People
People without events count as free all day โ add anyone to include them in the query.
People without events count as free all day โ add anyone to include them in the query.
Pick a query and follow your data down the road, station by station. Inside each station: plain-English explanations on the left, the actual Java source with real line numbers on the right โ hover any explanation to light up its line. Every number in the notes (loop counts, gates fired, events clipped) is computed from the file you uploaded. The rail on the far right is the full call chain; it tracks your scroll.
Every rule the system applies to odd input โ what the code does, where it does it, how the test suite proves it, and (when a file is loaded) what that rule actually did to your data.
Same journey as the Data journey tab, but at memory level: for
every stage, what the code does step by step โ and on the dark panels, what is actually held on
the heap at that moment, computed from your real file: the raw bytes as a hexdump, every Event
object, each person's 720-bit mask down to the twelve long words inside the BitSet,
and the scan table the scheduler walks.
Why the pure solution is linear โ load is O(events), every query is O(people + day-minutes) โ and why the day being fixed at 720 minutes quietly turns "linear" into "effectively constant". Shown on the actual data structures, not with letters.
Building the calendar is one pass over the parsed list. Each event does a constant amount of work โ clamp two ints, set one bit range in its owner's mask โ and is never looked at again:
No sorting, no comparing events to each other, no merge step โ overlaps cost nothing because
set() on an already-set bit is a no-op. Double the events, double the work, exactly:
O(events), with memory O(people ร 90 bytes).
Here's the trick worth saying out loud in an interview: after loading, queries never see events again. A person with 3 meetings and a person with 300 are the same object โ 720 bits. The query cost has two independent parts:
Part 1 grows with the people you ask about (not the people in the file, not the events). Part 2 is bounded by the day itself: however the candidates fall, the scan pointer only moves forward through 720 minutes. Together: O(people + dayMinutes) per query.
Big-O keeps the day as a variable, but the assignment pins it: the day is 720 minutes. So the 720 term is a constant โ about 12 longs of OR per person and one short scan โ and the whole query is, in practice, a few microseconds regardless of input size:
That's the whole performance story of the design: pay O(events) once, then answer every question from a structure whose size depends only on the clock. It's also exactly the property that makes the masks so easy to cache or ship around โ a value that never grows is a value you can put anywhere.
nextSetBit isn't magic โ a pathological mask alternating busy/free every
minute makes the scan examine every bit. That worst case is still โค 720 bit-reads: the bound
holds, only the constant moves.Every choice in the pure task solution โ the plain Java build with no Spring, no web page, just the assignment โ defended one at a time. Not what the code does (the other tabs cover that), but why it's built that way and not another way. Click a card to open it.
Package-by-concern, one package per reason to change:
domain โ Event, WorkingDay. Changes only if the business rules change.parsing โ EventParser, CsvEventParser, CalendarParseException. Changes only if the input format changes.repository โ EventCalendar. Changes only if how events are stored/indexed changes.scheduling โ MeetingScheduler. Changes only if the availability algorithm changes.App โ CLI wiring and printing, nothing else.The payoff is that the assignment's one required method, findAvailableSlots, lives in a class
whose only job is answering it โ a reviewer finds the core logic in seconds. The test tree mirrors the
main tree one-to-one, so every package's behavior has an obvious home. And it's deliberately not deeper:
~670 lines of code doesn't earn service/impl/util nesting. Structure should match the size of the problem.
String.split(",")?Because the very first data file breaks split. The README's own example contains
Alice,"Lunch with Jack",13:00,14:00 โ and the moment any subject legitimately contains a comma
("Lunch, then coffee"), split(",") shreds the row into five columns and books a
meeting at a time that doesn't parse. Correct CSV needs a state machine: quoted fields, escaped quotes
("" inside quotes), fields spanning newlines. That's a solved problem, and hand-rolling a solved
problem in an interview exercise signals the wrong instinct.
Commons CSV is the smallest battle-tested answer: one small dependency, zero transitive baggage, an
RFC 4180 preset, and a streaming record iterator that reports the record number โ which is what lets
CalendarParseException say which line was malformed. The trade-off (a dependency for a
12-row file) is priced in: the parser sits behind an interface, so the library never leaks past the
parsing package.
"CSV" isn't one format โ delimiters, quoting and line endings all vary by tool and locale. RFC 4180 is the
closest thing to a written standard, and crucially it's the dialect Excel and Google Sheets produce on export โ
which is exactly where a real calendar CSV would come from. Pinning the format to
CSVFormat.RFC4180 makes the accepted grammar explicit and documented instead of
"whatever my parser happens to tolerate":
private static final CSVFormat FORMAT = CSVFormat.RFC4180.builder()
.setIgnoreEmptyLines(true)
.setTrim(true)
.build();
The two builder tweaks are the two ways real files deviate from the RFC: trailing blank lines, and spaces
after commas (Alice, "Yoga", 16:00 โ the provided calendar.csv has them in the header row).
Save a CSV from Excel as "CSV UTF-8" or touch it in Windows Notepad, and the file silently starts with the
bytes EF BB BF โ a byte-order mark that decodes to the invisible character U+FEFF.
To a parser the first cell is then "๏ปฟPerson name", not "Person name": header
detection fails, the header row parses as data, and 07:00-style time parsing throws on the word
"Event start time". An invisible character producing a confusing crash is the worst kind of bug report.
private static Reader stripBom(Reader reader) throws IOException {
PushbackReader pushback = new PushbackReader(reader, 1);
int first = pushback.read();
if (first != -1 && first != BOM) {
pushback.unread(first);
}
return pushback;
}
Five lines: peek one character, push it back unless it's the BOM. Cheap insurance against the single most common real-world file corruption โ and it's covered by a parser test.
RFC 4180 line endings are CRLF (\r\n) โ Windows style. A file written on
Windows and read naรฏvely on Linux keeps a stray \r glued to the last column, so the end time
becomes "14:00\r" and LocalTime.parse throws. Commons CSV's reader treats
\r\n, \n and \r all as record separators, so the same file parses
identically on any OS โ one less environment-dependent bug class.
Blank lines get the same treatment from both ends: setIgnoreEmptyLines(true) plus an explicit
isBlank(record) guard. Editors love leaving a trailing newline; a trailing newline must not
become a phantom "row 13 has 1 column, expected 4" error.
Because the alternative makes the feature useless exactly when it's most needed. The query is "when can these people meet?" โ a new employee with an empty calendar, or a name that simply never appears in the file, has no known commitments. No events is the answer: free from 07:00 to 19:00. Rejecting the query would mean you can't schedule with anyone whose calendar hasn't filled up yet.
The honest risk is typos: Alcie is also "not in the file" and would silently read as
always-free. That's why the unknown-person case logs a warning naming the person โ the query still
answers, but the oddity is visible. Documented as a key assumption, and demonstrated below:
EventParser exist as an interface โ and why is it the only one?Parsing is the one natural seam in this program: everything downstream
(EventCalendar, MeetingScheduler) consumes List<Event> and could
not care less whether it came from CSV, JSON, an ICS export or a test literal. The interface records that
boundary in the type system. Tests exploit it directly โ scheduler tests construct events in memory and never
touch a file, which keeps them fast and focused.
Just as deliberate: there is no SchedulerInterface, no CalendarFactory,
no dependency-injection framework. One concrete scheduler exists and no second implementation is plausible;
an interface there would be ceremony pretending to be flexibility. Abstractions have to earn their place โ
EventParser earns it (a second format is genuinely plausible), the rest don't.
The day is fixed: 07:00โ19:00, 720 minutes. That single fact collapses the whole problem. Instead of
sorting and merging time intervals โ with all the edge cases that invites (overlaps, containment, adjacency,
duplicates) โ each person is a 720-bit mask (~90 bytes) where bit i means "busy during minute
i". Loading an event sets its minutes; overlapping and duplicate events merge for free
because setting a set bit is a no-op. "When are all of them busy?" is one OR per person, and
"is this range free?" is one nextSetBit call. There is no interval arithmetic anywhere in the code.
Minute resolution isn't over-engineering โ the README's own data demands it: Jack's sales call runs
09:00โ09:40 and the morning meeting ends at 08:50. Any coarser grid (15-minute
blocks, say) either rounds those wrong or needs special cases. 720 bits costs nothing; correctness at the
finest granularity the input can express is the cheapest option on the table.
[start, end)?An event ending at 09:00 and a meeting starting at 09:00 must not conflict โ that's how humans book
back-to-back meetings, and the README's expected output depends on it (Bob's 09:30 meeting ends exactly where
"Morning meeting 2" begins). Half-open ranges encode that with zero special cases: minute 09:00 belongs to
the starting event only. Every boundary minute has exactly one owner, adjacency never reads as
overlap, and range lengths are simply end โ start. Closed ranges would double-count every
boundary and need a ยฑ1 somewhere โ the classic off-by-one factory.
It also makes zero-length events (start == end) fall out naturally: the range
[x, x) is empty, covers no minutes, blocks nothing โ no special case needed.
Because that's the grid the README's expected output sits on. For a 60-minute meeting it lists
07:00, 10:00, 11:00, 12:00, โฆ โ hour-aligned starts, no 09:40 even though Alice and
Jack are both free 09:40โ10:40. The specified behavior is duration stepping, so the code does
duration stepping, and the pinned regression test proves the example never breaks.
It's documented as the solution's biggest known trade-off rather than hidden: a real product wants a
sliding scan (step 5โ15 minutes, or jump straight to nextClearBit) so a 45-minute meeting can
start at 08:30. The fix is a few lines and the README example survives as a special case โ but changing
specified behavior speculatively is how take-homes fail their own acceptance test.
Think about what "skip the bad row" actually does here: a meeting that exists in the file but not in the
mask reads as free time. The scheduler then confidently offers a slot that double-books someone โ
a silent wrong answer, discovered by two people standing in the same room. A crash at load time with
Malformed calendar entry at line 3: "Alice,Lunch,25:00,26:00" โ invalid time value "25:00"
is strictly better: loud, immediate, and it tells the user the exact line and reason so they can fix the file.
That's why CalendarParseException carries the line number and raw line as first-class data,
and why validation happens at the edge โ five gates (column count, empty name, two time parses, start<end)
run in the parser, so every object past the parsing package is known good and the domain code never
re-checks its inputs.
WorkingDay a class?An event from 06:00 to 08:00 is perfectly valid input โ people have commitments outside office hours โ but only its 07:00โ08:00 portion can affect a slot search that lives inside the day. Clipping at load time keeps the mask's domain fixed at exactly 720 bits and means the scheduler never has to reason about out-of-range minutes. Rejecting such events would be wrong (they're real), and storing them unclipped would force bounds checks everywhere else.
WorkingDay is a small class rather than two constants for the same reason the parser is an
interface: it's where the day's rules live. It validates start < end at construction, converts
times to minute offsets in one place, and can be constructed with different bounds โ which the tests use to
probe boundaries without redefining the universe. The 07:00/19:00 defaults stay named constants inside it.