Cars that are free on some days and not others. The problem where the hard part is a calendar: saying yes to a booking is a question about every hour of a date range, not about a single moment.
Car rental is a favourite medium-difficulty prompt because it looks like a CRUD exercise and is not. A booking is a claim on a date range, so the interesting question is "is there a car for every hour of this range?", and the usual first answer (count the bookings that overlap and compare with the fleet) is quietly wrong. The second half of the prompt is money: daily rates, weekend surcharges, long-rental discounts, one-way fees, late fees, damage. Candidates who write one big calculate_price with nested ifs cannot add the next rule in a follow-up without editing it.
The hotel round (LLD-13) already sells a pool of interchangeable rooms by counting each night. What this round adds: availability over continuous time, where bookings start and end at any hour, so there are no nights to count and the test is the peak overlap across the range, not an overlap test against each booking; pricing as a pipeline of rules that each emit labelled lines so a customer can see why the total is what it is; and what happens at the return, where a late fee, a damage fee and the car's own status all change at once.
- Strategy:
PricingRuleandLatePolicyare swappable per market - Chain of Responsibility (as a pipeline): each rule sees the running subtotal and adds lines
- State (as an enum):
ReservationStateandVehicleStatusgate what is allowed - Facade:
RentalServiceis the one entry point for quote, reserve, pickup and return
How the round runs#
"Design a car rental company with several branches. Customers reserve a class of car (economy, SUV) at a branch for a date range, pick it up, and return it. Price it with a daily rate, a weekend surcharge, a discount for long rentals and a fee for one-way trips. Charge for late returns and damage. Show a week running in code. 60 minutes."
- 0–8 min — clarify: class or specific car, one-way trips, how time is measured, what "late" means, what damage does
- 8–20 min — the classes, and the two decisions: is a range available, and how is a price built
- 20–35 min — availability: the overlap test done properly; reserve and cancel
- 35–47 min — pricing as rules; quote before booking
- 47–55 min — pickup and return: late fee, damage fee, the car's next state
- 55–60 min — a scripted week and the follow-ups: upgrades, no-shows, dynamic pricing
Step 1: Clarify the requirements#
| You ask | They answer | What it changes |
|---|---|---|
| Does a customer book a specific car or a class? | A class at a branch. A specific car is assigned at pickup. | Availability counts a pool; Vehicle is chosen late |
| How is time measured? | Whole hours; a rental is a half-open range [start, end). A started day is a day. | Time is an int parameter, never a clock; rental_days rounds up |
| Can a car be returned at another branch? | Yes, if booked that way, for a one-way fee. | return_branch on the request; a fee rule; the car moves |
| What is late? | Past the end time plus one grace hour. | LatePolicy with a grace period |
| What happens with damage? | Minor damage is a fee; major damage is a bigger fee and the car goes to the workshop. | Damage enum and a fee table; fleet shrinks for later bookings |
| Money? | Whole cents. | Integer cents everywhere, one rounding helper |
| Payments, insurance, loyalty tiers? | Out of scope; mention them. | Nothing now |
Two cars. A is booked hours 0–48, B hours 48–96. Can someone book 24–72?
An economy fleet has two cars at one branch. One reservation holds a car for hours 0–48, another for 48–96. A new customer asks for hours 24–72. Counting reservations that overlap the new range gives 2, which equals the fleet. Is there a car?
One strong answer
Yes. The two existing reservations never overlap each other: at hour 40 only the first is active, at hour 60 only the second. So at every moment of 24–72 at most one car is taken, and there is always a free one. The right test is the peak number of simultaneously held cars inside the requested range, which is 1 here, compared with the fleet of 2. Counting overlapping bookings (2) turns away a customer a car was available for.
Step 2: Identify the entities#
Step 3: Class diagram#
%% title: Car rental: the classes
classDiagram
class RentalService {
+quote(req) Quote
+reserve(customer, req) Reservation
+cancel(rid)
+pickup(rid, now) str
+return_car(rid, now, branch, damage) Quote
}
class Inventory {
+fleet_size(branch, car_class) int
+ready_car(branch, car_class) Vehicle
}
class Vehicle {
+vid str
+car_class CarClass
+branch str
+status VehicleStatus
}
class RentalRequest {
+car_class CarClass
+start int
+end int
+pickup_branch str
+return_branch str
}
class Reservation {
+rid int
+state ReservationState
+vehicle_id str
}
class Quote {
+lines Line[]
+total int
}
class PricingRule {
<<interface>>
+lines(req, so_far) Line[]
}
class PricingEngine {
+quote(req) Quote
}
class LatePolicy {
+fee(car_class, late_hours) int
}
RentalService --> Inventory
RentalService --> PricingEngine
RentalService --> LatePolicy
RentalService "1" *-- "many" Reservation
Inventory "1" *-- "many" Vehicle
Reservation --> RentalRequest
Reservation --> Quote
PricingEngine "1" o-- "many" PricingRule
PricingRule <|.. DailyRate
PricingRule <|.. WeekendSurcharge
PricingRule <|.. LongRentalDiscount
PricingRule <|.. OneWayFeeTwo decisions, two homes. Is this range available is answered from the fleet and the reservations: a function of the pool and a list of ranges, with no price in sight. What does it cost is a pipeline of rules over the request: no reservations in sight. RentalService is the facade that asks the first, then the second, and owns the state changes.
A reservation holds a class, not a car. The concrete car is chosen at pickup from the cars that are ready at that branch. That is how real rentals work, and it is what makes the overlap arithmetic exact: cars of a class are interchangeable, so the question is only "how many at the same time".
Step 4: Patterns applied#
| Pattern | Where | Why it earns its place |
|---|---|---|
| Strategy | PricingRule, LatePolicy |
Markets differ: a new city has a different weekend rule or late fee. Each is a class swapped in, not an edit. |
| Chain of Responsibility (as a pipeline) | PricingEngine runs PricingRules in order, passing the subtotal |
Rules compose and each explains itself with labelled lines. Order lives in one list. |
| State (as an enum) | ReservationState, VehicleStatus |
pickup needs BOOKED, return_car needs ACTIVE; a car in MAINTENANCE leaves the fleet. Two small enums beat a class per state. |
| Facade | RentalService |
The caller sees quote, reserve, cancel, pickup and return, not the inventory, the sweep or the fee tables. |
Step 5: Code#
The interfaces#
# carrental.py, part 1: money in cents, car classes, vehicles, requests, reservations
from __future__ import annotations
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
HOURS_PER_DAY = 24
def money(cents: int) -> str: # money is an integer count of cents, never a float
sign = "-" if cents < 0 else ""
return f"{sign}{abs(cents) // 100}.{abs(cents) % 100:02d}"
def pct_of(cents: int, pct: int) -> int: # an integer percentage, rounded half up: the only rounding rule
return (cents * pct + 50) // 100
def rental_days(start: int, end: int) -> int: # a started day is a day
return -(-(end - start) // HOURS_PER_DAY)
class CarClass(Enum):
ECONOMY = "economy"
SUV = "suv"
class VehicleStatus(Enum):
AVAILABLE = 1
RENTED = 2
MAINTENANCE = 3
class ReservationState(Enum):
BOOKED = 1
ACTIVE = 2
RETURNED = 3
CANCELLED = 4
class Damage(Enum):
NONE = 0
MINOR = 1
MAJOR = 2
@dataclass
class Vehicle:
vid: str
car_class: CarClass
branch: str
status: VehicleStatus = VehicleStatus.AVAILABLE
@dataclass(frozen=True)
class Line: # one explained line of a quote or an invoice
label: str
cents: int
@dataclass(frozen=True)
class Quote:
lines: tuple[Line, ...]
@property
def total(self) -> int:
return sum(line.cents for line in self.lines)
@dataclass(frozen=True)
class RentalRequest: # hours since Monday 00:00 of week 0; [start, end) is half-open
car_class: CarClass
start: int
end: int
pickup_branch: str
return_branch: str
@dataclass
class Reservation:
rid: int
customer: str
req: RentalRequest
quote: Quote
state: ReservationState = ReservationState.BOOKED
vehicle_id: str | None = None
invoice: Quote | None = None
class PricingRule(ABC): # each rule sees the running subtotal and adds zero or more lines
@abstractmethod
def lines(self, req: RentalRequest, so_far: int) -> list[Line]:
"""The lines this rule contributes."""// CarRentalDemo.java: a fleet by branch, reservations over date ranges, pricing rules, pickup and return
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantLock;
class Money {
static String format(int cents) { // money is an integer count of cents, never a float
String sign = cents < 0 ? "-" : "";
int a = Math.abs(cents);
return sign + a / 100 + "." + String.format("%02d", a % 100);
}
static int pctOf(int cents, int pct) { return (cents * pct + 50) / 100; } // rounded half up: the only rounding rule
}
class Hours {
static final int PER_DAY = 24;
static int rentalDays(int start, int end) { return (end - start + PER_DAY - 1) / PER_DAY; } // a started day is a day
}
enum CarClass { ECONOMY, SUV }
enum VehicleStatus { AVAILABLE, RENTED, MAINTENANCE }
enum ReservationState { BOOKED, ACTIVE, RETURNED, CANCELLED }
enum Damage { NONE, MINOR, MAJOR }
class Vehicle {
final String vid;
final CarClass carClass;
String branch;
VehicleStatus status = VehicleStatus.AVAILABLE;
Vehicle(String vid, CarClass carClass, String branch) {
this.vid = vid;
this.carClass = carClass;
this.branch = branch;
}
}
record Line(String label, int cents) {} // one explained line of a quote or an invoice
record Quote(List<Line> lines) {
int total() { return lines.stream().mapToInt(Line::cents).sum(); }
}
// hours since Monday 00:00 of week 0; [start, end) is half-open
record RentalRequest(CarClass carClass, int start, int end, String pickupBranch, String returnBranch) {}
class Reservation {
final int rid;
final String customer;
final RentalRequest req;
final Quote quote;
ReservationState state = ReservationState.BOOKED;
String vehicleId = null;
Quote invoice = null;
Reservation(int rid, String customer, RentalRequest req, Quote quote) {
this.rid = rid;
this.customer = customer;
this.req = req;
this.quote = quote;
}
}
interface PricingRule { // each rule sees the running subtotal and adds zero or more lines
List<Line> lines(RentalRequest req, int soFar);
}// carrental.cpp: a fleet by branch, reservations over date ranges, pricing rules, pickup and return
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
const int HOURS_PER_DAY = 24;
std::string money(int cents) { // money is an integer count of cents, never a float
char buf[32];
std::snprintf(buf, sizeof buf, "%s%d.%02d", cents < 0 ? "-" : "", std::abs(cents) / 100, std::abs(cents) % 100);
return buf;
}
int pctOf(int cents, int pct) { return (cents * pct + 50) / 100; } // rounded half up: the only rounding rule
int rentalDays(int start, int end) { return (end - start + HOURS_PER_DAY - 1) / HOURS_PER_DAY; } // a started day is a day
enum class CarClass { Economy, Suv };
enum class VehicleStatus { Available, Rented, Maintenance };
enum class ReservationState { Booked, Active, Returned, Cancelled };
enum class Damage { None, Minor, Major };
std::string name(Damage d) { return d == Damage::None ? "none" : d == Damage::Minor ? "minor" : "major"; }
struct Vehicle {
std::string vid;
CarClass carClass;
std::string branch;
VehicleStatus status = VehicleStatus::Available;
};
struct Line { // one explained line of a quote or an invoice
std::string label;
int cents;
};
struct Quote {
std::vector<Line> lines;
int total() const {
int t = 0;
for (const auto& l : lines) t += l.cents;
return t;
}
};
struct RentalRequest { // hours since Monday 00:00 of week 0; [start, end) is half-open
CarClass carClass;
int start;
int end;
std::string pickupBranch;
std::string returnBranch;
};
struct Reservation {
int rid;
std::string customer;
RentalRequest req;
Quote quote;
ReservationState state = ReservationState::Booked;
std::string vehicleId; // empty until pickup
Quote invoice;
};
class PricingRule { // each rule sees the running subtotal and adds zero or more lines
public:
virtual ~PricingRule() = default;
virtual std::vector<Line> lines(const RentalRequest& req, int soFar) const = 0;
};// carrental.js: a fleet by branch, reservations over date ranges, pricing rules, pickup and return
const HOURS_PER_DAY = 24;
// money is an integer count of cents, never a float
const money = (cents) => `${cents < 0 ? "-" : ""}${Math.floor(Math.abs(cents) / 100)}.${String(Math.abs(cents) % 100).padStart(2, "0")}`;
// an integer percentage, rounded half up: the only rounding rule
const pctOf = (cents, pct) => Math.floor((cents * pct + 50) / 100);
// a started day is a day
const rentalDays = (start, end) => Math.ceil((end - start) / HOURS_PER_DAY);
const CarClass = Object.freeze({ ECONOMY: "economy", SUV: "suv" });
const VehicleStatus = Object.freeze({ AVAILABLE: "AVAILABLE", RENTED: "RENTED", MAINTENANCE: "MAINTENANCE" });
const ReservationState = Object.freeze({ BOOKED: "BOOKED", ACTIVE: "ACTIVE", RETURNED: "RETURNED", CANCELLED: "CANCELLED" });
const Damage = Object.freeze({ NONE: "none", MINOR: "minor", MAJOR: "major" });
class Vehicle {
constructor(vid, carClass, branch) {
this.vid = vid;
this.carClass = carClass;
this.branch = branch;
this.status = VehicleStatus.AVAILABLE;
}
}
// one explained line of a quote or an invoice
const line = (label, cents) => Object.freeze({ label, cents });
const quoteOf = (lines) => Object.freeze({ lines, total: lines.reduce((t, l) => t + l.cents, 0) });
// hours since Monday 00:00 of week 0; [start, end) is half-open
const rentalRequest = (carClass, start, end, pickupBranch, returnBranch) =>
Object.freeze({ carClass, start, end, pickupBranch, returnBranch });
class Reservation {
constructor(rid, customer, req, quote) {
this.rid = rid;
this.customer = customer;
this.req = req;
this.quote = quote;
this.state = ReservationState.BOOKED;
this.vehicleId = null;
this.invoice = null;
}
}
// PricingRule: lines(req, soFar) -> an array of lines (each rule sees the running subtotal)Pricing: a pipeline of rules#
The price is not one formula. It is a list of rules run in order, each given the request and the running subtotal and returning labelled lines. The engine adds them up. Adding "young driver surcharge" or "holiday week" is a new class in the list, not an edit to the others.
DailyRate: days (rounded up) × the class's daily rate.WeekendSurcharge: a percentage of the daily rate for each rental day that falls on a Saturday or Sunday. Days are numbered from Monday = 0, so days 5 and 6 of each week.LongRentalDiscount: a percentage off everything so far, for rentals of at least N days. It runs after the base rules because it needs the subtotal, which is why the engine passes it in.OneWayFee: a flat fee when the pickup and return branches differ.
Order matters, and it is part of the design: a discount before the weekend surcharge would discount a smaller number. Keep the order in one place (the list handed to the engine) and test it.
# part 2: pricing as a pipeline of rules, each adding labelled lines
class DailyRate(PricingRule):
def __init__(self, rates: dict[CarClass, int]) -> None:
self.rates = rates
def lines(self, req: RentalRequest, so_far: int) -> list[Line]:
days = rental_days(req.start, req.end)
rate = self.rates[req.car_class]
return [Line(f"{days} day(s) x {money(rate)}", days * rate)]
class WeekendSurcharge(PricingRule): # days are numbered from Monday = 0: days 5 and 6 of each week are the weekend
def __init__(self, rates: dict[CarClass, int], pct: int) -> None:
self.rates, self.pct = rates, pct
def lines(self, req: RentalRequest, so_far: int) -> list[Line]:
first = req.start // HOURS_PER_DAY
n = sum(1 for d in range(first, first + rental_days(req.start, req.end)) if d % 7 >= 5)
if n == 0:
return []
per_day = pct_of(self.rates[req.car_class], self.pct)
return [Line(f"weekend surcharge {n} x {money(per_day)}", n * per_day)]
class LongRentalDiscount(PricingRule): # needs the subtotal, so it runs after the base rules
def __init__(self, min_days: int, pct: int) -> None:
self.min_days, self.pct = min_days, pct
def lines(self, req: RentalRequest, so_far: int) -> list[Line]:
if rental_days(req.start, req.end) < self.min_days:
return []
return [Line(f"long rental {self.pct}% off", -pct_of(so_far, self.pct))]
class OneWayFee(PricingRule):
def __init__(self, cents: int) -> None:
self.cents = cents
def lines(self, req: RentalRequest, so_far: int) -> list[Line]:
return [Line("one-way fee", self.cents)] if req.pickup_branch != req.return_branch else []
class PricingEngine:
def __init__(self, rules: list[PricingRule]) -> None:
self.rules = list(rules)
def quote(self, req: RentalRequest) -> Quote:
lines: list[Line] = []
total = 0
for rule in self.rules:
for line in rule.lines(req, total):
lines.append(line)
total += line.cents
return Quote(tuple(lines))// part 2: pricing as a pipeline of rules, each adding labelled lines
class DailyRate implements PricingRule {
private final Map<CarClass, Integer> rates;
DailyRate(Map<CarClass, Integer> rates) { this.rates = rates; }
public List<Line> lines(RentalRequest req, int soFar) {
int days = Hours.rentalDays(req.start(), req.end());
int rate = rates.get(req.carClass());
return List.of(new Line(days + " day(s) x " + Money.format(rate), days * rate));
}
}
class WeekendSurcharge implements PricingRule { // days are numbered from Monday = 0: days 5 and 6 of each week are the weekend
private final Map<CarClass, Integer> rates;
private final int pct;
WeekendSurcharge(Map<CarClass, Integer> rates, int pct) {
this.rates = rates;
this.pct = pct;
}
public List<Line> lines(RentalRequest req, int soFar) {
int first = req.start() / Hours.PER_DAY, n = 0;
for (int d = first; d < first + Hours.rentalDays(req.start(), req.end()); d++) if (d % 7 >= 5) n++;
if (n == 0) return List.of();
int perDay = Money.pctOf(rates.get(req.carClass()), pct);
return List.of(new Line("weekend surcharge " + n + " x " + Money.format(perDay), n * perDay));
}
}
class LongRentalDiscount implements PricingRule { // needs the subtotal, so it runs after the base rules
private final int minDays, pct;
LongRentalDiscount(int minDays, int pct) {
this.minDays = minDays;
this.pct = pct;
}
public List<Line> lines(RentalRequest req, int soFar) {
if (Hours.rentalDays(req.start(), req.end()) < minDays) return List.of();
return List.of(new Line("long rental " + pct + "% off", -Money.pctOf(soFar, pct)));
}
}
class OneWayFee implements PricingRule {
private final int cents;
OneWayFee(int cents) { this.cents = cents; }
public List<Line> lines(RentalRequest req, int soFar) {
return req.pickupBranch().equals(req.returnBranch()) ? List.of() : List.of(new Line("one-way fee", cents));
}
}
class PricingEngine {
private final List<PricingRule> rules;
PricingEngine(List<PricingRule> rules) { this.rules = List.copyOf(rules); }
Quote quote(RentalRequest req) {
List<Line> lines = new ArrayList<>();
int total = 0;
for (PricingRule rule : rules)
for (Line l : rule.lines(req, total)) {
lines.add(l);
total += l.cents();
}
return new Quote(lines);
}
}// part 2: pricing as a pipeline of rules, each adding labelled lines
class DailyRate : public PricingRule {
public:
explicit DailyRate(std::map<CarClass, int> r) : rates(std::move(r)) {}
std::vector<Line> lines(const RentalRequest& req, int) const override {
int days = rentalDays(req.start, req.end);
int rate = rates.at(req.carClass);
return {{std::to_string(days) + " day(s) x " + money(rate), days * rate}};
}
private:
std::map<CarClass, int> rates;
};
class WeekendSurcharge : public PricingRule { // days are numbered from Monday = 0: days 5 and 6 of each week are the weekend
public:
WeekendSurcharge(std::map<CarClass, int> r, int p) : rates(std::move(r)), pct(p) {}
std::vector<Line> lines(const RentalRequest& req, int) const override {
int first = req.start / HOURS_PER_DAY, n = 0;
for (int d = first; d < first + rentalDays(req.start, req.end); d++)
if (d % 7 >= 5) n++;
if (n == 0) return {};
int perDay = pctOf(rates.at(req.carClass), pct);
return {{"weekend surcharge " + std::to_string(n) + " x " + money(perDay), n * perDay}};
}
private:
std::map<CarClass, int> rates;
int pct;
};
class LongRentalDiscount : public PricingRule { // needs the subtotal, so it runs after the base rules
public:
LongRentalDiscount(int d, int p) : minDays(d), pct(p) {}
std::vector<Line> lines(const RentalRequest& req, int soFar) const override {
if (rentalDays(req.start, req.end) < minDays) return {};
return {{"long rental " + std::to_string(pct) + "% off", -pctOf(soFar, pct)}};
}
private:
int minDays, pct;
};
class OneWayFee : public PricingRule {
public:
explicit OneWayFee(int c) : cents(c) {}
std::vector<Line> lines(const RentalRequest& req, int) const override {
if (req.pickupBranch == req.returnBranch) return {};
return {{"one-way fee", cents}};
}
private:
int cents;
};
class PricingEngine {
public:
explicit PricingEngine(std::vector<std::unique_ptr<PricingRule>> r) : rules(std::move(r)) {}
Quote quote(const RentalRequest& req) const {
Quote q;
int total = 0;
for (const auto& rule : rules)
for (const Line& l : rule->lines(req, total)) {
q.lines.push_back(l);
total += l.cents;
}
return q;
}
private:
std::vector<std::unique_ptr<PricingRule>> rules;
};// part 2: pricing as a pipeline of rules, each adding labelled lines
class DailyRate {
constructor(rates) {
this.rates = rates;
}
lines(req) {
const days = rentalDays(req.start, req.end);
const rate = this.rates[req.carClass];
return [line(`${days} day(s) x ${money(rate)}`, days * rate)];
}
}
class WeekendSurcharge {
// days are numbered from Monday = 0: days 5 and 6 of each week are the weekend
constructor(rates, pct) {
this.rates = rates;
this.pct = pct;
}
lines(req) {
const first = Math.floor(req.start / HOURS_PER_DAY);
let n = 0;
for (let d = first; d < first + rentalDays(req.start, req.end); d++) if (d % 7 >= 5) n++;
if (n === 0) return [];
const perDay = pctOf(this.rates[req.carClass], this.pct);
return [line(`weekend surcharge ${n} x ${money(perDay)}`, n * perDay)];
}
}
class LongRentalDiscount {
// needs the subtotal, so it runs after the base rules
constructor(minDays, pct) {
this.minDays = minDays;
this.pct = pct;
}
lines(req, soFar) {
if (rentalDays(req.start, req.end) < this.minDays) return [];
return [line(`long rental ${this.pct}% off`, -pctOf(soFar, this.pct))];
}
}
class OneWayFee {
constructor(cents) {
this.cents = cents;
}
lines(req) {
return req.pickupBranch !== req.returnBranch ? [line("one-way fee", this.cents)] : [];
}
}
class PricingEngine {
constructor(rules) {
this.rules = [...rules];
}
quote(req) {
const lines = [];
let total = 0;
for (const rule of this.rules)
for (const l of rule.lines(req, total)) {
lines.push(l);
total += l.cents;
}
return quoteOf(lines);
}
}Every amount is an integer number of cents, and the only rounding in the whole system is pct_of, which rounds half up. A discount of 10% on 666.00 is exactly 66.60, and on an odd amount it rounds once, in one named place, so a customer, a test and an auditor all compute the same number.
Availability: the overlap that counts#
A reservation is a half-open range [start, end): a car returned at hour 48 can leave again at hour 48. For a class at a branch, the number of cars the company can commit is the fleet size (cars of that class based there and not in the workshop). A new range is available when, at every instant inside it, the cars already promised plus one is at most the fleet.
That is a question about the peak of the existing reservations clipped to the requested range, and it is a sweep: turn each reservation into a +1 at its start and a −1 at its end, sort the events, and walk them keeping a running count. Sorting by (time, delta) puts ends before starts at the same instant, which is exactly what half-open means. Counting overlapping reservations instead over-counts whenever two of them do not overlap each other, which is the checkpoint above.
Because cars of a class are interchangeable and reservations hold a class, the peak test is exact: there is a way to give every reservation its own car if and only if the peak never exceeds the fleet. (Intervals on a line can always be coloured with as many colours as their peak overlap.)
# part 3: availability is a peak, not a count
def peak_overlap(spans: list[tuple[int, int]], start: int, end: int) -> int:
"""The most reservations held at the same moment inside [start, end)."""
events: list[tuple[int, int]] = []
for s, e in spans:
lo, hi = max(s, start), min(e, end)
if lo < hi:
events.append((lo, 1))
events.append((hi, -1))
events.sort() # at the same instant the -1 sorts first: a car returned at hour 48 can leave at hour 48
current = best = 0
for _, delta in events:
current += delta
best = max(best, current)
return best
class Inventory:
def __init__(self, vehicles: list[Vehicle]) -> None:
self.vehicles = {v.vid: v for v in vehicles}
def branches(self) -> set[str]:
return {v.branch for v in self.vehicles.values()}
def fleet_size(self, branch: str, car_class: CarClass) -> int: # cars in the workshop are not promised to anyone
return sum(1 for v in self.vehicles.values()
if v.branch == branch and v.car_class is car_class and v.status is not VehicleStatus.MAINTENANCE)
def ready_car(self, branch: str, car_class: CarClass) -> Vehicle | None: # the lowest id, so it is deterministic
ready = [v for v in self.vehicles.values()
if v.branch == branch and v.car_class is car_class and v.status is VehicleStatus.AVAILABLE]
return min(ready, key=lambda v: v.vid) if ready else None// part 3: availability is a peak, not a count
class Availability {
// the most reservations held at the same moment inside [start, end)
static int peakOverlap(List<int[]> spans, int start, int end) {
List<int[]> events = new ArrayList<>();
for (int[] s : spans) {
int lo = Math.max(s[0], start), hi = Math.min(s[1], end);
if (lo < hi) {
events.add(new int[] {lo, 1});
events.add(new int[] {hi, -1});
}
}
// at the same instant the -1 sorts first: a car returned at hour 48 can leave at hour 48
events.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
int current = 0, best = 0;
for (int[] ev : events) {
current += ev[1];
best = Math.max(best, current);
}
return best;
}
}
class Inventory {
final Map<String, Vehicle> vehicles = new TreeMap<>(); // by id, so "the lowest id" is the first match
Inventory(List<Vehicle> list) {
for (Vehicle v : list) vehicles.put(v.vid, v);
}
boolean hasBranch(String branch) { return vehicles.values().stream().anyMatch(v -> v.branch.equals(branch)); }
int fleetSize(String branch, CarClass carClass) { // cars in the workshop are not promised to anyone
int n = 0;
for (Vehicle v : vehicles.values())
if (v.branch.equals(branch) && v.carClass == carClass && v.status != VehicleStatus.MAINTENANCE) n++;
return n;
}
Vehicle readyCar(String branch, CarClass carClass) { // the lowest id, so it is deterministic
for (Vehicle v : vehicles.values())
if (v.branch.equals(branch) && v.carClass == carClass && v.status == VehicleStatus.AVAILABLE) return v;
return null;
}
}// part 3: availability is a peak, not a count
int peakOverlap(const std::vector<std::pair<int, int>>& spans, int start, int end) {
// the most reservations held at the same moment inside [start, end)
std::vector<std::pair<int, int>> events;
for (const auto& [s, e] : spans) {
int lo = std::max(s, start), hi = std::min(e, end);
if (lo < hi) {
events.push_back({lo, 1});
events.push_back({hi, -1});
}
}
std::sort(events.begin(), events.end()); // at the same instant the -1 sorts first: a car returned at hour 48 can leave at hour 48
int current = 0, best = 0;
for (const auto& ev : events) {
current += ev.second;
best = std::max(best, current);
}
return best;
}
class Inventory {
public:
std::map<std::string, Vehicle> vehicles; // by id, so "the lowest id" is the first match
explicit Inventory(const std::vector<Vehicle>& list) {
for (const auto& v : list) vehicles[v.vid] = v;
}
bool hasBranch(const std::string& branch) const {
for (const auto& [id, v] : vehicles)
if (v.branch == branch) return true;
return false;
}
int fleetSize(const std::string& branch, CarClass carClass) const { // cars in the workshop are not promised to anyone
int n = 0;
for (const auto& [id, v] : vehicles)
if (v.branch == branch && v.carClass == carClass && v.status != VehicleStatus::Maintenance) n++;
return n;
}
Vehicle* readyCar(const std::string& branch, CarClass carClass) { // the lowest id, so it is deterministic
for (auto& [id, v] : vehicles)
if (v.branch == branch && v.carClass == carClass && v.status == VehicleStatus::Available) return &v;
return nullptr;
}
};// part 3: availability is a peak, not a count
// the most reservations held at the same moment inside [start, end)
function peakOverlap(spans, start, end) {
const events = [];
for (const [s, e] of spans) {
const lo = Math.max(s, start), hi = Math.min(e, end);
if (lo < hi) events.push([lo, 1], [hi, -1]);
}
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // at the same instant the -1 sorts first: a car returned at hour 48 can leave at hour 48
let current = 0, best = 0;
for (const [, delta] of events) {
current += delta;
best = Math.max(best, current);
}
return best;
}
class Inventory {
constructor(vehicles) {
this.vehicles = new Map([...vehicles].sort((a, b) => (a.vid < b.vid ? -1 : 1)).map((v) => [v.vid, v])); // by id
}
hasBranch(branch) {
return [...this.vehicles.values()].some((v) => v.branch === branch);
}
fleetSize(branch, carClass) {
// cars in the workshop are not promised to anyone
return [...this.vehicles.values()].filter((v) => v.branch === branch && v.carClass === carClass && v.status !== VehicleStatus.MAINTENANCE).length;
}
readyCar(branch, carClass) {
// the lowest id, so it is deterministic
return [...this.vehicles.values()].find((v) => v.branch === branch && v.carClass === carClass && v.status === VehicleStatus.AVAILABLE) ?? null;
}
}The code#
The service ties it together. reserve quotes the price and checks the peak under one lock; pickup assigns a car; return_car turns the late hours and the damage into more lines on an invoice and decides whether the car re-enters the fleet.
# part 4: the service: reserve, pick up, return
DAMAGE_FEES = {Damage.NONE: 0, Damage.MINOR: 15000, Damage.MAJOR: 60000}
class LatePolicy: # one grace hour, then a quarter of the day rate per hour; a whole late day costs a whole day
def __init__(self, rates: dict[CarClass, int], grace: int = 1, hourly_pct: int = 25) -> None:
self.rates, self.grace, self.hourly_pct = rates, grace, hourly_pct
def fee(self, car_class: CarClass, late_hours: int) -> int:
if late_hours <= self.grace:
return 0
rate = self.rates[car_class]
days, rest = divmod(late_hours, HOURS_PER_DAY)
return days * rate + min(rest * pct_of(rate, self.hourly_pct), rate)
class RentalService:
def __init__(self, inventory: Inventory, engine: PricingEngine, late: LatePolicy) -> None:
self.inventory, self.engine, self.late = inventory, engine, late
self.reservations: dict[int, Reservation] = {}
self._next = 1
self._lock = threading.RLock()
def _check(self, req: RentalRequest) -> None:
if req.start >= req.end:
raise ValueError("the end must be after the start")
branches = self.inventory.branches()
if req.pickup_branch not in branches or req.return_branch not in branches:
raise ValueError("unknown branch")
def quote(self, req: RentalRequest) -> Quote:
self._check(req)
return self.engine.quote(req)
def available(self, req: RentalRequest) -> bool:
held = [(r.req.start, r.req.end) for r in self.reservations.values()
if r.req.pickup_branch == req.pickup_branch and r.req.car_class is req.car_class
and r.state in (ReservationState.BOOKED, ReservationState.ACTIVE)]
return peak_overlap(held, req.start, req.end) < self.inventory.fleet_size(req.pickup_branch, req.car_class)
def reserve(self, customer: str, req: RentalRequest) -> Reservation:
with self._lock: # the check and the insert are one step: two customers must not both get the last car
quote = self.quote(req)
if not self.available(req):
raise RuntimeError(f"no {req.car_class.value} available at {req.pickup_branch} for those hours")
res = Reservation(self._next, customer, req, quote)
self._next += 1
self.reservations[res.rid] = res
return res
def cancel(self, rid: int) -> None:
with self._lock:
res = self.reservations[rid]
if res.state is not ReservationState.BOOKED:
raise RuntimeError("only a booked reservation can be cancelled")
res.state = ReservationState.CANCELLED
def pickup(self, rid: int, now: int) -> str:
with self._lock:
res = self.reservations[rid]
if res.state is not ReservationState.BOOKED:
raise RuntimeError("not a booked reservation")
if not res.req.start <= now < res.req.end:
raise RuntimeError("outside the booked hours")
car = self.inventory.ready_car(res.req.pickup_branch, res.req.car_class)
if car is None:
raise RuntimeError("no car ready at the branch")
car.status, res.state, res.vehicle_id = VehicleStatus.RENTED, ReservationState.ACTIVE, car.vid
return car.vid
def return_car(self, rid: int, now: int, branch: str, damage: Damage = Damage.NONE) -> Quote:
with self._lock:
res = self.reservations[rid]
if res.state is not ReservationState.ACTIVE:
raise RuntimeError("not an active rental")
if branch != res.req.return_branch:
raise ValueError(f"wrong branch: booked to return at {res.req.return_branch}")
lines = list(res.quote.lines)
late_hours = max(0, now - res.req.end)
late = self.late.fee(res.req.car_class, late_hours)
if late:
lines.append(Line(f"late {late_hours} h", late))
if damage is not Damage.NONE:
lines.append(Line(f"damage {damage.name.lower()}", DAMAGE_FEES[damage]))
car = self.inventory.vehicles[res.vehicle_id]
car.branch = branch
car.status = VehicleStatus.MAINTENANCE if damage is Damage.MAJOR else VehicleStatus.AVAILABLE
res.state, res.invoice = ReservationState.RETURNED, Quote(tuple(lines))
return res.invoice// part 4: the service: reserve, pick up, return
class LatePolicy { // one grace hour, then a quarter of the day rate per hour; a whole late day costs a whole day
private final Map<CarClass, Integer> rates;
private final int grace, hourlyPct;
LatePolicy(Map<CarClass, Integer> rates, int grace, int hourlyPct) {
this.rates = rates;
this.grace = grace;
this.hourlyPct = hourlyPct;
}
int fee(CarClass carClass, int lateHours) {
if (lateHours <= grace) return 0;
int rate = rates.get(carClass);
int days = lateHours / Hours.PER_DAY, rest = lateHours % Hours.PER_DAY;
return days * rate + Math.min(rest * Money.pctOf(rate, hourlyPct), rate);
}
}
class RentalService {
static final Map<Damage, Integer> DAMAGE_FEES = Map.of(Damage.NONE, 0, Damage.MINOR, 15000, Damage.MAJOR, 60000);
private final Inventory inventory;
private final PricingEngine engine;
private final LatePolicy late;
final Map<Integer, Reservation> reservations = new TreeMap<>();
private int next = 1;
private final ReentrantLock lock = new ReentrantLock();
RentalService(Inventory inventory, PricingEngine engine, LatePolicy late) {
this.inventory = inventory;
this.engine = engine;
this.late = late;
}
private void check(RentalRequest req) {
if (req.start() >= req.end()) throw new IllegalArgumentException("the end must be after the start");
if (!inventory.hasBranch(req.pickupBranch()) || !inventory.hasBranch(req.returnBranch()))
throw new IllegalArgumentException("unknown branch");
}
Quote quote(RentalRequest req) {
check(req);
return engine.quote(req);
}
boolean available(RentalRequest req) {
List<int[]> held = new ArrayList<>();
for (Reservation r : reservations.values())
if (r.req.pickupBranch().equals(req.pickupBranch()) && r.req.carClass() == req.carClass()
&& (r.state == ReservationState.BOOKED || r.state == ReservationState.ACTIVE))
held.add(new int[] {r.req.start(), r.req.end()});
return Availability.peakOverlap(held, req.start(), req.end()) < inventory.fleetSize(req.pickupBranch(), req.carClass());
}
Reservation reserve(String customer, RentalRequest req) {
lock.lock(); // the check and the insert are one step: two customers must not both get the last car
try {
Quote quote = quote(req);
if (!available(req))
throw new IllegalStateException("no " + req.carClass().name().toLowerCase() + " available at " + req.pickupBranch() + " for those hours");
Reservation res = new Reservation(next++, customer, req, quote);
reservations.put(res.rid, res);
return res;
} finally {
lock.unlock();
}
}
void cancel(int rid) {
lock.lock();
try {
Reservation res = reservations.get(rid);
if (res.state != ReservationState.BOOKED) throw new IllegalStateException("only a booked reservation can be cancelled");
res.state = ReservationState.CANCELLED;
} finally {
lock.unlock();
}
}
String pickup(int rid, int now) {
lock.lock();
try {
Reservation res = reservations.get(rid);
if (res.state != ReservationState.BOOKED) throw new IllegalStateException("not a booked reservation");
if (now < res.req.start() || now >= res.req.end()) throw new IllegalStateException("outside the booked hours");
Vehicle car = inventory.readyCar(res.req.pickupBranch(), res.req.carClass());
if (car == null) throw new IllegalStateException("no car ready at the branch");
car.status = VehicleStatus.RENTED;
res.state = ReservationState.ACTIVE;
res.vehicleId = car.vid;
return car.vid;
} finally {
lock.unlock();
}
}
Quote returnCar(int rid, int now, String branch, Damage damage) {
lock.lock();
try {
Reservation res = reservations.get(rid);
if (res.state != ReservationState.ACTIVE) throw new IllegalStateException("not an active rental");
if (!branch.equals(res.req.returnBranch()))
throw new IllegalArgumentException("wrong branch: booked to return at " + res.req.returnBranch());
List<Line> lines = new ArrayList<>(res.quote.lines());
int lateHours = Math.max(0, now - res.req.end());
int lateFee = late.fee(res.req.carClass(), lateHours);
if (lateFee > 0) lines.add(new Line("late " + lateHours + " h", lateFee));
if (damage != Damage.NONE) lines.add(new Line("damage " + damage.name().toLowerCase(), DAMAGE_FEES.get(damage)));
Vehicle car = inventory.vehicles.get(res.vehicleId);
car.branch = branch;
car.status = damage == Damage.MAJOR ? VehicleStatus.MAINTENANCE : VehicleStatus.AVAILABLE;
res.state = ReservationState.RETURNED;
res.invoice = new Quote(lines);
return res.invoice;
} finally {
lock.unlock();
}
}
}// part 4: the service: reserve, pick up, return
const std::map<Damage, int> DAMAGE_FEES = {{Damage::None, 0}, {Damage::Minor, 15000}, {Damage::Major, 60000}};
class LatePolicy { // one grace hour, then a quarter of the day rate per hour; a whole late day costs a whole day
public:
LatePolicy(std::map<CarClass, int> r, int g = 1, int p = 25) : rates(std::move(r)), grace(g), hourlyPct(p) {}
int fee(CarClass carClass, int lateHours) const {
if (lateHours <= grace) return 0;
int rate = rates.at(carClass);
int days = lateHours / HOURS_PER_DAY, rest = lateHours % HOURS_PER_DAY;
return days * rate + std::min(rest * pctOf(rate, hourlyPct), rate);
}
private:
std::map<CarClass, int> rates;
int grace, hourlyPct;
};
class RentalService {
public:
std::map<int, Reservation> reservations;
RentalService(Inventory& inv, PricingEngine& e, LatePolicy l) : inventory(inv), engine(e), late(std::move(l)) {}
Quote quote(const RentalRequest& req) const {
check(req);
return engine.quote(req);
}
bool available(const RentalRequest& req) const {
std::vector<std::pair<int, int>> held;
for (const auto& [rid, r] : reservations)
if (r.req.pickupBranch == req.pickupBranch && r.req.carClass == req.carClass &&
(r.state == ReservationState::Booked || r.state == ReservationState::Active))
held.push_back({r.req.start, r.req.end});
return peakOverlap(held, req.start, req.end) < inventory.fleetSize(req.pickupBranch, req.carClass);
}
int reserve(const std::string& customer, const RentalRequest& req) {
std::lock_guard<std::recursive_mutex> g(lock); // the check and the insert are one step: two customers must not both get the last car
Quote q = quote(req);
if (!available(req))
throw std::logic_error(std::string("no ") + (req.carClass == CarClass::Economy ? "economy" : "suv") + " available at " +
req.pickupBranch + " for those hours");
int rid = next++;
reservations[rid] = Reservation{rid, customer, req, q};
return rid;
}
void cancel(int rid) {
std::lock_guard<std::recursive_mutex> g(lock);
Reservation& res = reservations.at(rid);
if (res.state != ReservationState::Booked) throw std::logic_error("only a booked reservation can be cancelled");
res.state = ReservationState::Cancelled;
}
std::string pickup(int rid, int now) {
std::lock_guard<std::recursive_mutex> g(lock);
Reservation& res = reservations.at(rid);
if (res.state != ReservationState::Booked) throw std::logic_error("not a booked reservation");
if (now < res.req.start || now >= res.req.end) throw std::logic_error("outside the booked hours");
Vehicle* car = inventory.readyCar(res.req.pickupBranch, res.req.carClass);
if (!car) throw std::logic_error("no car ready at the branch");
car->status = VehicleStatus::Rented;
res.state = ReservationState::Active;
res.vehicleId = car->vid;
return car->vid;
}
Quote returnCar(int rid, int now, const std::string& branch, Damage damage = Damage::None) {
std::lock_guard<std::recursive_mutex> g(lock);
Reservation& res = reservations.at(rid);
if (res.state != ReservationState::Active) throw std::logic_error("not an active rental");
if (branch != res.req.returnBranch) throw std::invalid_argument("wrong branch: booked to return at " + res.req.returnBranch);
Quote invoice = res.quote;
int lateHours = std::max(0, now - res.req.end);
int lateFee = late.fee(res.req.carClass, lateHours);
if (lateFee > 0) invoice.lines.push_back({"late " + std::to_string(lateHours) + " h", lateFee});
if (damage != Damage::None) invoice.lines.push_back({"damage " + name(damage), DAMAGE_FEES.at(damage)});
Vehicle& car = inventory.vehicles.at(res.vehicleId);
car.branch = branch;
car.status = damage == Damage::Major ? VehicleStatus::Maintenance : VehicleStatus::Available;
res.state = ReservationState::Returned;
res.invoice = invoice;
return invoice;
}
private:
Inventory& inventory;
PricingEngine& engine;
LatePolicy late;
int next = 1;
mutable std::recursive_mutex lock;
void check(const RentalRequest& req) const {
if (req.start >= req.end) throw std::invalid_argument("the end must be after the start");
if (!inventory.hasBranch(req.pickupBranch) || !inventory.hasBranch(req.returnBranch)) throw std::invalid_argument("unknown branch");
}
};// part 4: the service: reserve, pick up, return
const DAMAGE_FEES = { [Damage.NONE]: 0, [Damage.MINOR]: 15000, [Damage.MAJOR]: 60000 };
class LatePolicy {
// one grace hour, then a quarter of the day rate per hour; a whole late day costs a whole day
constructor(rates, grace = 1, hourlyPct = 25) {
this.rates = rates;
this.grace = grace;
this.hourlyPct = hourlyPct;
}
fee(carClass, lateHours) {
if (lateHours <= this.grace) return 0;
const rate = this.rates[carClass];
const days = Math.floor(lateHours / HOURS_PER_DAY), rest = lateHours % HOURS_PER_DAY;
return days * rate + Math.min(rest * pctOf(rate, this.hourlyPct), rate);
}
}
class RentalService {
// single-threaded in JavaScript: reserve never yields between the check and the insert, so it needs no lock
constructor(inventory, engine, late) {
this.inventory = inventory;
this.engine = engine;
this.late = late;
this.reservations = new Map();
this.next = 1;
}
check(req) {
if (req.start >= req.end) throw new RangeError("the end must be after the start");
if (!this.inventory.hasBranch(req.pickupBranch) || !this.inventory.hasBranch(req.returnBranch)) throw new RangeError("unknown branch");
}
quote(req) {
this.check(req);
return this.engine.quote(req);
}
available(req) {
const held = [...this.reservations.values()]
.filter((r) => r.req.pickupBranch === req.pickupBranch && r.req.carClass === req.carClass && [ReservationState.BOOKED, ReservationState.ACTIVE].includes(r.state))
.map((r) => [r.req.start, r.req.end]);
return peakOverlap(held, req.start, req.end) < this.inventory.fleetSize(req.pickupBranch, req.carClass);
}
reserve(customer, req) {
const quote = this.quote(req);
if (!this.available(req)) throw new Error(`no ${req.carClass} available at ${req.pickupBranch} for those hours`);
const res = new Reservation(this.next++, customer, req, quote);
this.reservations.set(res.rid, res);
return res;
}
cancel(rid) {
const res = this.reservations.get(rid);
if (res.state !== ReservationState.BOOKED) throw new Error("only a booked reservation can be cancelled");
res.state = ReservationState.CANCELLED;
}
pickup(rid, now) {
const res = this.reservations.get(rid);
if (res.state !== ReservationState.BOOKED) throw new Error("not a booked reservation");
if (now < res.req.start || now >= res.req.end) throw new Error("outside the booked hours");
const car = this.inventory.readyCar(res.req.pickupBranch, res.req.carClass);
if (!car) throw new Error("no car ready at the branch");
car.status = VehicleStatus.RENTED;
res.state = ReservationState.ACTIVE;
res.vehicleId = car.vid;
return car.vid;
}
returnCar(rid, now, branch, damage = Damage.NONE) {
const res = this.reservations.get(rid);
if (res.state !== ReservationState.ACTIVE) throw new Error("not an active rental");
if (branch !== res.req.returnBranch) throw new RangeError(`wrong branch: booked to return at ${res.req.returnBranch}`);
const lines = [...res.quote.lines];
const lateHours = Math.max(0, now - res.req.end);
const lateFee = this.late.fee(res.req.carClass, lateHours);
if (lateFee > 0) lines.push(line(`late ${lateHours} h`, lateFee));
if (damage !== Damage.NONE) lines.push(line(`damage ${damage}`, DAMAGE_FEES[damage]));
const car = this.inventory.vehicles.get(res.vehicleId);
car.branch = branch;
car.status = damage === Damage.MAJOR ? VehicleStatus.MAINTENANCE : VehicleStatus.AVAILABLE;
res.state = ReservationState.RETURNED;
res.invoice = quoteOf(lines);
return res.invoice;
}
}A scripted week: two price quotes, a calendar of bookings with the peak test on show, a late and damaged return, a major-damage return that shrinks the fleet, and the rejections.
# part 5: a week at two branches
def show(title: str, q: Quote) -> None:
print(title)
for line in q.lines:
print(f" {line.label}: {money(line.cents)}")
print(f" total: {money(q.total)}")
if __name__ == "__main__":
rates = {CarClass.ECONOMY: 4000, CarClass.SUV: 9000}
fleet = [Vehicle("E1", CarClass.ECONOMY, "DOWNTOWN"), Vehicle("E2", CarClass.ECONOMY, "DOWNTOWN"),
Vehicle("S1", CarClass.SUV, "DOWNTOWN"), Vehicle("E3", CarClass.ECONOMY, "AIRPORT")]
engine = PricingEngine([DailyRate(rates), WeekendSurcharge(rates, 20), LongRentalDiscount(7, 10), OneWayFee(5000)])
svc = RentalService(Inventory(fleet), engine, LatePolicy(rates))
D, A, E, S = "DOWNTOWN", "AIRPORT", CarClass.ECONOMY, CarClass.SUV
show("Thursday 10:00 to Monday 10:00, economy", svc.quote(RentalRequest(E, 82, 178, D, D)))
show("a week of SUV, one way", svc.quote(RentalRequest(S, 9, 177, D, A)))
ann = svc.reserve("Ann", RentalRequest(E, 0, 48, D, D))
ben = svc.reserve("Ben", RentalRequest(E, 48, 96, D, D))
spans = [(0, 48), (48, 96)]
print("hours 24-72: overlapping bookings", len(spans), "| peak", peak_overlap(spans, 24, 72), "| fleet", svc.inventory.fleet_size(D, E))
cy = svc.reserve("Cy", RentalRequest(E, 24, 72, D, D))
print("Cy booked hours 24-72, reservation", cy.rid)
try:
svc.reserve("Dee", RentalRequest(E, 30, 40, D, D))
except RuntimeError as e:
print("rejected:", e)
print("Ann picks up", svc.pickup(ann.rid, 0))
show("Ann returns 2 h late with a scratch", svc.return_car(ann.rid, 50, D, Damage.MINOR))
print("Ben picks up", svc.pickup(ben.rid, 52))
show("Ben returns early with a wrecked bumper", svc.return_car(ben.rid, 90, D, Damage.MAJOR))
print("economy fleet at DOWNTOWN now:", svc.inventory.fleet_size(D, E))
svc.reserve("Eve", RentalRequest(E, 100, 110, D, D))
try:
svc.reserve("Fay", RentalRequest(E, 100, 110, D, D))
except RuntimeError as e:
print("rejected:", e)
try:
svc.pickup(cy.rid, 10)
except RuntimeError as e:
print("rejected:", e)public class CarRentalDemo {
// a week at two branches
static void show(String title, Quote q) {
System.out.println(title);
for (Line l : q.lines()) System.out.println(" " + l.label() + ": " + Money.format(l.cents()));
System.out.println(" total: " + Money.format(q.total()));
}
public static void main(String[] args) {
Map<CarClass, Integer> rates = Map.of(CarClass.ECONOMY, 4000, CarClass.SUV, 9000);
Inventory inv = new Inventory(List.of(new Vehicle("E1", CarClass.ECONOMY, "DOWNTOWN"), new Vehicle("E2", CarClass.ECONOMY, "DOWNTOWN"),
new Vehicle("S1", CarClass.SUV, "DOWNTOWN"), new Vehicle("E3", CarClass.ECONOMY, "AIRPORT")));
PricingEngine engine = new PricingEngine(List.of(new DailyRate(rates), new WeekendSurcharge(rates, 20), new LongRentalDiscount(7, 10), new OneWayFee(5000)));
RentalService svc = new RentalService(inv, engine, new LatePolicy(rates, 1, 25));
String d = "DOWNTOWN", a = "AIRPORT";
CarClass e = CarClass.ECONOMY, s = CarClass.SUV;
show("Thursday 10:00 to Monday 10:00, economy", svc.quote(new RentalRequest(e, 82, 178, d, d)));
show("a week of SUV, one way", svc.quote(new RentalRequest(s, 9, 177, d, a)));
Reservation ann = svc.reserve("Ann", new RentalRequest(e, 0, 48, d, d));
Reservation ben = svc.reserve("Ben", new RentalRequest(e, 48, 96, d, d));
List<int[]> spans = List.of(new int[] {0, 48}, new int[] {48, 96});
System.out.println("hours 24-72: overlapping bookings " + spans.size() + " | peak " + Availability.peakOverlap(spans, 24, 72) + " | fleet " + inv.fleetSize(d, e));
Reservation cy = svc.reserve("Cy", new RentalRequest(e, 24, 72, d, d));
System.out.println("Cy booked hours 24-72, reservation " + cy.rid);
try {
svc.reserve("Dee", new RentalRequest(e, 30, 40, d, d));
} catch (IllegalStateException ex) {
System.out.println("rejected: " + ex.getMessage());
}
System.out.println("Ann picks up " + svc.pickup(ann.rid, 0));
show("Ann returns 2 h late with a scratch", svc.returnCar(ann.rid, 50, d, Damage.MINOR));
System.out.println("Ben picks up " + svc.pickup(ben.rid, 52));
show("Ben returns early with a wrecked bumper", svc.returnCar(ben.rid, 90, d, Damage.MAJOR));
System.out.println("economy fleet at DOWNTOWN now: " + inv.fleetSize(d, e));
svc.reserve("Eve", new RentalRequest(e, 100, 110, d, d));
try {
svc.reserve("Fay", new RentalRequest(e, 100, 110, d, d));
} catch (IllegalStateException ex) {
System.out.println("rejected: " + ex.getMessage());
}
try {
svc.pickup(cy.rid, 10);
} catch (IllegalStateException ex) {
System.out.println("rejected: " + ex.getMessage());
}
}
}// a week at two branches
void show(const std::string& title, const Quote& q) {
std::cout << title << "\n";
for (const auto& l : q.lines) std::cout << " " << l.label << ": " << money(l.cents) << "\n";
std::cout << " total: " << money(q.total()) << "\n";
}
int main() {
std::map<CarClass, int> rates = {{CarClass::Economy, 4000}, {CarClass::Suv, 9000}};
Inventory inv({{"E1", CarClass::Economy, "DOWNTOWN"}, {"E2", CarClass::Economy, "DOWNTOWN"},
{"S1", CarClass::Suv, "DOWNTOWN"}, {"E3", CarClass::Economy, "AIRPORT"}});
std::vector<std::unique_ptr<PricingRule>> rules;
rules.push_back(std::make_unique<DailyRate>(rates));
rules.push_back(std::make_unique<WeekendSurcharge>(rates, 20));
rules.push_back(std::make_unique<LongRentalDiscount>(7, 10));
rules.push_back(std::make_unique<OneWayFee>(5000));
PricingEngine engine(std::move(rules));
RentalService svc(inv, engine, LatePolicy(rates));
const std::string D = "DOWNTOWN", A = "AIRPORT";
const CarClass E = CarClass::Economy, S = CarClass::Suv;
show("Thursday 10:00 to Monday 10:00, economy", svc.quote({E, 82, 178, D, D}));
show("a week of SUV, one way", svc.quote({S, 9, 177, D, A}));
int ann = svc.reserve("Ann", {E, 0, 48, D, D});
int ben = svc.reserve("Ben", {E, 48, 96, D, D});
std::vector<std::pair<int, int>> spans = {{0, 48}, {48, 96}};
std::cout << "hours 24-72: overlapping bookings " << spans.size() << " | peak " << peakOverlap(spans, 24, 72) << " | fleet "
<< inv.fleetSize(D, E) << "\n";
int cy = svc.reserve("Cy", {E, 24, 72, D, D});
std::cout << "Cy booked hours 24-72, reservation " << cy << "\n";
try {
svc.reserve("Dee", {E, 30, 40, D, D});
} catch (const std::logic_error& ex) {
std::cout << "rejected: " << ex.what() << "\n";
}
std::cout << "Ann picks up " << svc.pickup(ann, 0) << "\n";
show("Ann returns 2 h late with a scratch", svc.returnCar(ann, 50, D, Damage::Minor));
std::cout << "Ben picks up " << svc.pickup(ben, 52) << "\n";
show("Ben returns early with a wrecked bumper", svc.returnCar(ben, 90, D, Damage::Major));
std::cout << "economy fleet at DOWNTOWN now: " << inv.fleetSize(D, E) << "\n";
svc.reserve("Eve", {E, 100, 110, D, D});
try {
svc.reserve("Fay", {E, 100, 110, D, D});
} catch (const std::logic_error& ex) {
std::cout << "rejected: " << ex.what() << "\n";
}
try {
svc.pickup(cy, 10);
} catch (const std::logic_error& ex) {
std::cout << "rejected: " << ex.what() << "\n";
}
}// a week at two branches
const show = (title, q) => {
console.log(title);
for (const l of q.lines) console.log(` ${l.label}: ${money(l.cents)}`);
console.log(` total: ${money(q.total)}`);
};
const rates = { [CarClass.ECONOMY]: 4000, [CarClass.SUV]: 9000 };
const fleet = [new Vehicle("E1", CarClass.ECONOMY, "DOWNTOWN"), new Vehicle("E2", CarClass.ECONOMY, "DOWNTOWN"),
new Vehicle("S1", CarClass.SUV, "DOWNTOWN"), new Vehicle("E3", CarClass.ECONOMY, "AIRPORT")];
const engine = new PricingEngine([new DailyRate(rates), new WeekendSurcharge(rates, 20), new LongRentalDiscount(7, 10), new OneWayFee(5000)]);
const svc = new RentalService(new Inventory(fleet), engine, new LatePolicy(rates));
const D = "DOWNTOWN", A = "AIRPORT", E = CarClass.ECONOMY, S = CarClass.SUV;
show("Thursday 10:00 to Monday 10:00, economy", svc.quote(rentalRequest(E, 82, 178, D, D)));
show("a week of SUV, one way", svc.quote(rentalRequest(S, 9, 177, D, A)));
const ann = svc.reserve("Ann", rentalRequest(E, 0, 48, D, D));
const ben = svc.reserve("Ben", rentalRequest(E, 48, 96, D, D));
const spans = [[0, 48], [48, 96]];
console.log("hours 24-72: overlapping bookings", spans.length, "| peak", peakOverlap(spans, 24, 72), "| fleet", svc.inventory.fleetSize(D, E));
const cy = svc.reserve("Cy", rentalRequest(E, 24, 72, D, D));
console.log("Cy booked hours 24-72, reservation", cy.rid);
try {
svc.reserve("Dee", rentalRequest(E, 30, 40, D, D));
} catch (e) {
console.log("rejected:", e.message);
}
console.log("Ann picks up", svc.pickup(ann.rid, 0));
show("Ann returns 2 h late with a scratch", svc.returnCar(ann.rid, 50, D, Damage.MINOR));
console.log("Ben picks up", svc.pickup(ben.rid, 52));
show("Ben returns early with a wrecked bumper", svc.returnCar(ben.rid, 90, D, Damage.MAJOR));
console.log("economy fleet at DOWNTOWN now:", svc.inventory.fleetSize(D, E));
svc.reserve("Eve", rentalRequest(E, 100, 110, D, D));
try {
svc.reserve("Fay", rentalRequest(E, 100, 110, D, D));
} catch (e) {
console.log("rejected:", e.message);
}
try {
svc.pickup(cy.rid, 10);
} catch (e) {
console.log("rejected:", e.message);
}Thursday 10:00 to Monday 10:00, economy
4 day(s) x 40.00: 160.00
weekend surcharge 2 x 8.00: 16.00
total: 176.00
a week of SUV, one way
7 day(s) x 90.00: 630.00
weekend surcharge 2 x 18.00: 36.00
long rental 10% off: -66.60
one-way fee: 50.00
total: 649.40
hours 24-72: overlapping bookings 2 | peak 1 | fleet 2
Cy booked hours 24-72, reservation 3
rejected: no economy available at DOWNTOWN for those hours
Ann picks up E1
Ann returns 2 h late with a scratch
2 day(s) x 40.00: 80.00
late 2 h: 20.00
damage minor: 150.00
total: 250.00
Ben picks up E1
Ben returns early with a wrecked bumper
2 day(s) x 40.00: 80.00
damage major: 600.00
total: 680.00
economy fleet at DOWNTOWN now: 1
rejected: no economy available at DOWNTOWN for those hours
rejected: outside the booked hoursThe first quote shows the pipeline: base, weekend, no discount, no one-way fee. The second shows all four rules, and that the 10% is taken from the subtotal of 666.00, which is why it is 66.60. The booking calendar then makes the point of the round: Cy fits into hours 24–72 although two bookings overlap that range, and Dee does not fit into 30–40 because Ann's and Cy's bookings really are held at the same time. Ann's invoice adds a late line and a damage line to the base, Ben's wrecked bumper takes E1 into the workshop, and the economy fleet at DOWNTOWN drops to one, so Eve gets the only car for hours 100–110 and Fay is refused.
Step 6: Concurrency and edge cases#
Two customers can press "reserve" for the last economy car at the same moment. The availability check and the insertion of the reservation are one check-then-act, so RentalService holds a single lock around reserve, cancel, pickup and return_car. The sweep is cheap (it runs over the reservations for one branch and class), so a service-wide lock is fine until profiling says otherwise; a lock per (branch, class) is the next step. Pricing is pure and needs no lock.
- Back-to-back bookings: one ends at hour 48, the next starts at 48. Half-open ranges and the sort order of
(time, delta)make that fit in one car - Start not before end, or an unknown branch: reject before touching any state
- Picking up too early, or after the booking ended: reject; a no-show policy is a follow-up
- No car ready at pickup (the previous renter is late): the booking was valid, but there is no car on the lot. Say so; the real fix is an upgrade or a car from another branch
- Returning at a branch other than the booked one: reject here, with the fix being a one-way fee re-quote
- A car returned with major damage: it goes to the workshop, and the fleet size shrinks for every later booking at that branch
- Cancelling a car that is already out: only BOOKED can be cancelled
- Rounding: all percentages go through
pct_of; a total computed twice is the same to the cent
Step 7: Follow-ups and the interview checklist#
- "A customer is a no-show." A booking that is not picked up within a window (say two hours after its start) moves to
NO_SHOW, charges a fee, and releases the car from the peak count. It is a state, a sweep on a clock that is passed in, and one more line on an invoice. - "The car at the branch is late, so there is nothing to hand over."
pickupalready fails honestly. Add anUpgradePolicy(a strategy): try the next class up at the same price, then a car from the nearest branch. The reservation stays valid; only the assigned car changes. - "Add an extra: child seat, GPS, a second driver." An
Extrawith its own rate and its own stock, priced by a newPricingRuleand counted by a second peak test over its own reservations. Same two ideas, applied again. - "Dynamic pricing: a holiday week costs more." A
SeasonalMultiplierrule that looks the dates up in a calendar. It sits in the pipeline before the discount; nothing else changes. - "Returns at a different branch." Re-quote with the actual return branch at return, add the difference as a line, and move the car. The fleet counts per branch then drift; rebalancing trucks are the operational answer.
- "Insurance and deposits." A hold on the customer's card at pickup and a release at return: the same shape as the digital wallet round's holds, and a reason to keep
Quoteas lines rather than a single number.
Interview checklist#
Tick these off before you say done.
- I asked whether customers book a class or a car, and chose a class at booking with the car assigned at pickup.
- I used half-open ranges and whole-hour integers, and passed time in as a parameter, never read a clock.
- I tested availability by the peak overlap inside the requested range, and can say why counting overlapping bookings is wrong.
- I kept the sweep's tie-break right: at the same instant, ends sort before starts.
- I made the price a pipeline of
PricingRules with labelled lines, and said the order of the rules is part of the design. - I used integer cents and a single rounding helper, and no floats near money.
- I made the availability check and the insert one atomic step under a lock, and named the lock per
(branch, class)as the next step. - I gated pickup and return on the reservation state and the car's status, and rejected a pickup outside the booked hours.
- I computed the late fee with a grace period, and the damage fee from a table, as extra lines on an invoice.
- I took a heavily damaged car out of the fleet, and said that shrinks availability for later bookings.
Common mistakes#
- Counting overlapping bookings against the fleet, which turns away customers a car was free for
- Booking a specific car at reservation time, so a returned car cannot be swapped and a late return breaks a later booking
- One
calculate_pricewith nested ifs: the next rule is an edit to all of them, and the customer cannot see why - Floats for money:
0.1 + 0.2in an invoice datetime.now()inside the service: the demo and the tests cannot be repeatable- Closed ranges
[start, end]: back-to-back bookings collide at the boundary - Forgetting that a damaged car leaves the fleet: tomorrow's availability is overstated
Car rental
Q1/3 Two cars. Bookings hold hours 0–48 and 48–96. A new request is for hours 24–72. What does the right availability test say?
Answer & explanation
Answer: B
The bookings never overlap each other, so the peak inside 24–72 is 1, below the fleet of 2. Counting overlapping bookings (2) is the common mistake: it refuses a customer a car was free for.
Q2/3 An economy car costs 40.00 a day. The late fee is 25% of the day rate per hour after one grace hour. How much is a return 2 hours late?
Answer & explanation
Answer: C
The grace period decides whether a fee applies at all: 2 hours is more than 1, so the fee applies to all 2 hours, at 25% of 40.00 = 10.00 each. Capped at one day's rate for the first day, so 20.00 is below the cap.
Q3/3 A car is returned with major damage. What should happen to bookings at that branch next week?
Answer & explanation
Answer: C
The car goes to the workshop, so it no longer counts in the fleet. Existing bookings are still held against a smaller fleet, which is how an overbooking shows up, and why the follow-ups mention upgrades.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


