Recurring Workspace Calendar
Problem
A team shares rooms and desks through one calendar. Design RecurringWorkspaceCalendar to register those resources, create finite recurring bookings, preserve each occurrence when it is moved or cancelled, and report occupied and free time on an integer-minute timeline. Calls are sequential, and each calendar instance owns independent state.
Requirements
- Resource registry.
addResourcepermanently registers a valid, previously unused resource ID. - Bounded recurrence.
bookcreates one occurrence forONCE, or a finite daily or weekly series whose duration and zero-based occurrence indices follow the declared expansion. - Atomic booking.
bookvalidates the complete expansion before changing state; invalid input, an unknown resource, self-overlap, or overlap with any active occurrence creates nothing and leaves the booking ID reusable. - Half-open conflicts. Active occurrences on the same resource conflict exactly when their half-open intervals overlap; touching endpoints do not conflict.
- Stable identity. An occurrence keeps its booking ID, occurrence index, resource, and siblings when moved or cancelled.
- Persistent exceptions. Moving an active occurrence replaces only its interval, while cancelling it preserves its current interval for inspection but removes it from occupancy.
- Series cancellation.
cancelBookingcancels every active occurrence in the booking and preserves all occurrence history. - Failure atomicity. A rejected cancellation or move leaves all state unchanged.
- Deterministic availability. Availability contains every active occurrence that overlaps the query window, in the declared order, plus the maximal free intervals inside the window.
API
| Signature | Returns | Behavior |
|---|---|---|
RecurringWorkspaceCalendar() | Not applicable | Creates an empty calendar. |
addResource(resourceId: string) | string | Returns INVALID_ARGUMENT, DUPLICATE_RESOURCE, or OK, using the first applicable result. A successful ID is permanent. |
book(bookingId: string, resourceId: string, startMinute: int64, endMinute: int64, frequency: string, occurrenceCount: integer) | string | Returns INVALID_ARGUMENT, DUPLICATE_BOOKING, UNKNOWN_RESOURCE, CONFLICT, or OK, using the first applicable result. Successful booking IDs are permanent. |
cancelBooking(bookingId: string) | string | Returns INVALID_ARGUMENT, UNKNOWN_BOOKING, INVALID_STATE, or OK, using the first applicable result. OK requires at least one active child. |
cancelOccurrence(bookingId: string, occurrenceIndex: integer) | string | Returns INVALID_ARGUMENT, UNKNOWN_OCCURRENCE, INVALID_STATE, or OK, using the first applicable result. |
moveOccurrence(bookingId: string, occurrenceIndex: integer, newStartMinute: int64, newEndMinute: int64) | string | Returns INVALID_ARGUMENT, UNKNOWN_OCCURRENCE, INVALID_STATE, CONFLICT, or OK, using the first applicable result. Conflict checking excludes the occurrence's current interval. |
getOccurrence(bookingId: string, occurrenceIndex: integer) | OccurrenceView | Returns the current detached occurrence snapshot. A malformed or missing identity returns the sentinel described below. |
queryAvailability(resourceId: string, windowStartMinute: int64, windowEndMinute: int64) | AvailabilityView | Returns INVALID_ARGUMENT, UNKNOWN_RESOURCE, or OK, using the first applicable result. Non-OK results contain empty arrays. |
TimeInterval has startMinute: int64 and endMinute: int64. OccurrenceView has found: boolean, bookingId: string, occurrenceIndex: integer, resourceId: string, startMinute: int64, endMinute: int64, and status: string. Existing occurrences have status ACTIVE or CANCELLED. A missing occurrence has found = false, empty strings, index -1, zero times, and status NOT_FOUND. AvailabilityView has status: string, occurrences: OccurrenceView[], and freeIntervals: TimeInterval[]. All returned views and arrays are detached.
For ONCE, the count must be 1. For DAILY and WEEKLY, the count must be from 2 through 100. Occurrence i starts at startMinute + i * 1440 for DAILY and startMinute + i * 10080 for WEEKLY; every occurrence keeps the original duration. Any generated interval outside the valid time range is invalid.
Examples
| Step | Operation | Result |
|---|---|---|
| 1 | addResource("room-a") | OK |
| 2 | book("standup", "room-a", 100, 160, "DAILY", 2) | OK; occurrences are [100,160) and [1540,1600) |
| 3 | book("review", "room-a", 160, 200, "ONCE", 1) | OK; touching endpoints are allowed |
| 4 | moveOccurrence("standup", 1, 180, 240) | CONFLICT; occurrence 1 remains [1540,1600) |
| 5 | moveOccurrence("standup", 1, 200, 260) | OK; occurrence 1 keeps its identity |
| 6 | queryAvailability("room-a", 90, 270) | Active intervals [100,160), [160,200), [200,260); free intervals [90,100), [260,270) |
| 7 | cancelOccurrence("review", 0) | OK |
| 8 | queryAvailability("room-a", 90, 270) | Active intervals [100,160), [200,260); free intervals [90,100), [160,200), [260,270) |
| 9 | getOccurrence("review", 0) | Found, CANCELLED, interval [160,200) |
Constraints
- IDs are non-null, case-sensitive ASCII strings matching
[A-Za-z0-9_-]{1,40}. - Intervals are half-open and valid only when
0 <= startMinute < endMinute <= 1_000_000_000. - A calendar contains at most
100resources,5,000successful bookings, and20,000retained occurrences. - A testcase contains at most
20,000actions. - Time calculations use signed 64-bit arithmetic.
- Conflict checks and availability queries should avoid scanning unrelated resources; producing a query result costs at least linear time in its output size.
Notes
Active intervals [aStart,aEnd) and [bStart,bEnd) overlap exactly when aStart < bEnd and bStart < aEnd. Availability returns complete stored occurrence intervals, even when they extend beyond the query window. It sorts occurrences by startMinute, endMinute, ASCII bookingId, then occurrenceIndex. Free intervals are maximal, nonempty, clipped to the query window, and sorted by start. Calls on one instance are serialized, and separate instances share no state. Time zones, clocks, persistence, unbounded recurrence, reactivation, deletion, and whole-booking or resource movement are out of scope.