Problem overview
WaterBottle tracks how much water is in one bottle. It is created empty with a capacity. fill(amount) pours in only as much as fits and returns how much actually went in. drink(amount) drinks only what is there and returns how much was actually drunk. level() returns the water now in the bottle. Amounts are whole milliliters.
With new WaterBottle(300), fill(250) returns 250. A second fill(100) returns only 50, because only 50 more fit, and the bottle is then full. A full bottle takes nothing more, so fill(10) returns 0.
Approach
The bottle needs two numbers: the capacity, which never changes, and the water, which starts at 0. The rule for each operation is "take the smaller of what was asked and what is possible": for fill the possible amount is the room left, capacity - water; for drink it is the water in the bottle. Taking the smaller value keeps the level between 0 and the capacity without any extra checks, and it is also exactly the number to return. Every method runs in constant time.
Step by step
- Fields.
capacityhas no starting value, because the constructor sets it.waterstarts at 0 so that every new bottle is empty. It is namedwaterbecause a field calledlevelwould clash with the methodlevel(). - Constructor. Store the parameter in
this.capacity. The water needs nothing here. - fill. Compute
addedasMath.min(amount, this.capacity - this.water), add it to the water, and return it. Addingaddedrather thanamountis what keeps the water within the capacity. - drink. The same shape:
drunkis the smaller ofamountandthis.water, it is subtracted, and returned. - level. Return
this.water.
The solution in TypeScript
class WaterBottle {
capacity: number;
water: number = 0;
constructor(capacity: number) {
this.capacity = capacity;
}
fill(amount: number): number {
let added: number = Math.min(amount, this.capacity - this.water);
this.water += added;
return added;
}
drink(amount: number): number {
let drunk: number = Math.min(amount, this.water);
this.water -= drunk;
return drunk;
}
level(): number {
return this.water;
}
}
Math.mintakes two numbers and returns the smaller.- Same parameter and field name. In the constructor,
this.capacityis the field andcapacityis the parameter. - Returning the real amount. The caller learns what actually happened, not what was requested.
Using the class
const w = new WaterBottle(300);
console.log(w.fill(250));
console.log(w.fill(100));
console.log(w.level());
console.log(w.fill(10));
console.log(w.drink(1000));
console.log(w.level());
console.log(w.drink(5));
250
50
300
0
300
0
0
Asking to drink 1000 from a bottle holding 300 returns 300 and leaves it empty. Drinking from an empty bottle returns 0.
Tracing a bottle
Take new WaterBottle(100) and follow the water:
| Call | Returns | Water after |
|---|---|---|
drink(10) | 0 | 0 |
fill(60) | 60 | 60 |
drink(100) | 60 | 0 |
drink(5) | 0 | 0 |
The first drink gets nothing because the bottle starts empty. The second drink asks for 100 but only 60 are there, so it returns 60 and the bottle is empty again.
The returned value is useful on its own. A caller that pours 100 ml into a nearly full bottle can compare the result with what it asked for, and learn that part of the water did not fit. That is why both methods return the amount that really moved instead of nothing.
Testing
- On a bottle of capacity 100,
fill(100)returns 100, thenfill(0)returns 0,drink(0)returns 0 andlevel()returns 100. - On a bottle of capacity 500,
fill(200)returns 200,drink(50)returns 50 andlevel()returns 150.
Another way: explicit if statements
Math.min is short, but the same rule can be written with an if that handles the "does not fit" case on its own:
class WaterBottle {
capacity: number;
water: number = 0;
constructor(capacity: number) { this.capacity = capacity; }
fill(amount: number): number {
const room = this.capacity - this.water;
if (amount > room) { this.water = this.capacity; return room; }
this.water += amount;
return amount;
}
drink(amount: number): number {
if (amount > this.water) { const d = this.water; this.water = 0; return d; }
this.water -= amount;
return amount;
}
level(): number { return this.water; }
}
const w = new WaterBottle(300);
console.log(w.fill(250), w.fill(100), w.level(), w.fill(10), w.drink(1000), w.drink(5), w.level());
This prints 250 50 300 0 300 0 0, the same answers as the lesson's version. It has two branches in each method, so there are two places where a mistake can hide. The Math.min form has one path through each method, which is why the lesson prefers it.
Edge cases and common mistakes
- Adding
amountinstead ofadded. The level could exceed the capacity. - Returning
amount.fill(100)on a bottle with 50 ml of room must return 50, not 100. - Zero amounts.
fill(0)anddrink(0)work without special code: the smaller value is 0. - A full or empty bottle. The room left is 0 when full and the water is 0 when empty, so the same formulas return 0.
Key points
- A class can protect its own limits: every change to
watergoes throughfillanddrink, and each takes the smaller of the request and what is possible. - Returning the amount that really moved lets the caller see how much of the request was honored.
- The room left is
capacity - water, and the water that can be drunk iswateritself. - A field and a method can't share a name, so the level is stored in
waterand reported bylevel(). - Every method runs in constant time, because the bottle remembers only two numbers.
The same pattern, clamping a change to what is possible, shows up whenever a value must stay within bounds, such as a health bar, a volume control or a stock count.
