Problem overview
A bottle holds at most a fixed amount of water. WaterBottle tracks one bottle as you fill it and drink from it, in whole milliliters.
new WaterBottle(capacity)creates an empty bottle.fill(amount)pours in as much as fits and returns how much actually went in.drink(amount)drinks as much as the bottle holds and returns how much was actually drunk.level()returns the water in the bottle now.
With capacity 300, fill(250) returns 250. Then fill(100) returns only 50, because only 50 more fit, and the bottle is full. A full bottle takes nothing, so fill(10) returns 0.
Approach
The object needs to remember two numbers: the capacity, which never changes, and the water, which starts at 0. Both operations have the same shape. Work out how much can really be moved: the smaller of the requested amount and the limit. Fill's limit is the room left, capacity - water. Drink's limit is the water in the bottle. Then change the water by that amount and return it. Because the moved amount is never more than the limit, the water stays between 0 and the capacity. Each method takes O(1) time.
Step by step
- Store the capacity in
this.capacity. - Start empty: set
this.waterto 0. The field is not calledlevel, because that would hide the methodlevel(). - Fill. Declare
addedasMath.min(amount, this.capacity - this.water). - Update the water with
this.water += added. Addingaddedrather thanamountis what keeps the bottle from overflowing. - Return
added, so the caller learns whether everything fit. - Drink works the same way:
drunkis the smaller ofamountandthis.water. Subtract it with-=, then return it. - Level returns
this.water.
The solution in JavaScript
class WaterBottle {
constructor(capacity) {
this.capacity = capacity;
this.water = 0;
}
fill(amount) {
let added = Math.min(amount, this.capacity - this.water);
this.water += added;
return added;
}
drink(amount) {
let drunk = Math.min(amount, this.water);
this.water -= drunk;
return drunk;
}
level() {
return this.water;
}
}
Using the class
let b = new WaterBottle(500);
console.log(b.fill(200), b.drink(50), b.level());
b = new WaterBottle(300);
console.log(b.fill(250), b.fill(100), b.level(), b.fill(10));
b = new WaterBottle(100);
console.log(b.drink(10), b.fill(60), b.drink(100), b.level(), b.drink(5));
This prints:
200 50 150
250 50 300 0
0 60 60 0 0
In the last line the first drink gets nothing from the empty bottle. After fill(60), drink(100) can only take the 60 that are there, which empties the bottle again.
Common mistakes
- Adding
amountinstead ofadded. The water would go past the capacity. - Returning
amount. The methods return what really happened, sofill(100)on a bottle with 50 of room returns 50. - Mixing up the limits. Fill is limited by the room left (
capacity - water), not by the capacity; drink is limited by the water, not by the capacity. - A field named
level. It would hide thelevel()method, andbottle.level()would fail with aTypeError.
A trace
This is a bottle with capacity 100, one call after another:
| Call | Returns | Level after |
|---|---|---|
| fill(70) | 70 | 70 |
| fill(70) | 30 | 100 |
| drink(30) | 30 | 70 |
| drink(200) | 70 | 0 |
The second fill(70) asks for more than fits: the room left is 100 - 70 = 30, so Math.min(70, 30) is 30 and the bottle ends exactly full. The last drink(200) asks for more than the bottle holds, so it takes the 70 that are there.
Edge cases
- Zero amounts.
fill(0)anddrink(0)move nothing and return 0. The formula needs no special case: the smaller of 0 and anything non-negative is 0. - Exactly the room left. On an empty bottle with capacity 100,
fill(100)returns 100 and the level is 100. A furtherfill(1)returns 0. - Draining the bottle. From that full bottle,
drink(100)returns 100 andlevel()returns 0. - Several bottles. Each
new WaterBottle(...)has its owncapacityandwater, so filling one never changes another.
Why the return values matter
fill and drink both change the bottle and report what happened. A plain "void" method would hide the difference between a fill that fitted completely and one that was cut short. Returning the moved amount lets the caller compare it with the request: if fill(100) returns 50, half of the water did not fit.
An equivalent version
The same rules can be written with an if instead of Math.min:
fill(amount) {
let room = this.capacity - this.water;
if (amount > room) {
amount = room;
}
this.water += amount;
return amount;
}
It does the same thing, but Math.min says it in one line: take the smaller of the two numbers.
Run on a bottle with capacity 300, this version gives the same answers as the solution: fill(250), fill(100) and fill(10) return 250, 50 and 0, and the water ends at 300.
The rule the class protects
At all times, the water is between 0 and the capacity. Nothing in the class checks this after the fact. Instead, each method makes sure the rule holds before it changes anything: fill never adds more than the room left, so the water can reach the capacity but not pass it; drink never removes more than the water, so it can reach 0 but not go below. Designing a method so that the rule cannot be broken is simpler than repairing the value afterwards, and the same idea comes back in every class that has limits to respect.
