One-Day Calendar

Calendar file

Upload calendar.csv click or drop a CSV file here

People

Upload a calendar first.

Meeting duration

minutes

Actions

No calendar loaded โ€” upload a CSV to see the day.

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.

Upload a CSV on the Calendar tab first โ€” the journey is built from your real data.

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.

Upload a CSV on the Calendar tab first โ€” the internals view is built from your real data.

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.

1 ยท Loading โ€” O(events): every event is touched exactly once

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:

for (Event event : events) { // runs exactly |events| times int start = clampToDay(minuteOf(event.getStart())); // O(1) int end = clampToDay(minuteOf(event.getEnd())); // O(1) masks.computeIfAbsent(person, k -> new BitSet(720)) // O(1) hash lookup .set(start, end); // word-fill, โ‰ค 12 longs }

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).

2 ยท Querying โ€” O(people + 720): the event count has left the building

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:

BitSet combined = new BitSet(720); for (String name : people) // โ”€โ”€ part 1: O(people) combined.or(masks.get(name)); // each OR = 12 long-word ops (720/64), not 720 for (start = 0; start + d <= 720; start += d) // โ”€โ”€ part 2: O(720/d) candidates busy.nextSetBit(start) // skips whole 64-minute words of zeros >= start + d ? free : busy // total bits examined across the scan โ‰ค 720

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.

3 ยท Why "linear" is really "constant" here

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:

10 events 10,000 events load (once) 10 ร— set() 10,000 ร— set() โ† the only thing that grows mask size 90 bytes 90 bytes โ† identical query: OR 2 people 12 word ops 12 word ops โ† identical query: scan the day โ‰ค 720 bits โ‰ค 720 bits โ† identical

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.

4 ยท The honest worst cases

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.

W1

Why this folder structure โ€” domain / parsing / repository / scheduling?

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.

W2

Why Apache Commons CSV instead of 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.

W3

Why RFC 4180 as the dialect?

"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).

W4

Why strip a BOM?

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.

W5

Why care about CRLF and blank lines?

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.

W6

Why is a person with no events โ€” or not in the CSV at all โ€” free all day?

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:

W7

Why does 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.

W8

Why a BitSet per person โ€” and why one bit per minute?

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.

W9

Why half-open ranges โ€” [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.

W10

Why do candidate slots step by the meeting duration?

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.

W11

Why fail fast on malformed rows instead of skipping them?

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.

W12

Why clip events to the working day โ€” and why is 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.