Problem overview
A bottle holds at most a fixed amount of water. You can't pour in more than fits, and you can't drink more than is inside. Write a class WaterBottle that tracks the water in one bottle, in whole milliliters.
WaterBottle(capacity)creates an empty bottle.fill(amount)pours in as much ofamountas fits and returns how much actually went in.drink(amount)drinks as much ofamountas is there and returns how much was actually drunk.level()returns the water in the bottle now.
In a bottle of 300 that already holds 250, fill(100) pours only 50 and returns 50, and the bottle is full. A full bottle takes nothing more: fill(10) returns 0.
Approach
The bottle needs to remember two numbers: its capacity and its current water. Both operations follow the same pattern: the amount that really moves is the smaller of what was asked and what is possible.
- For
fill, what is possible is the room left,capacity - water. - For
drink, what is possible is the water in the bottle.
Taking the smaller of the two means the water never goes above the capacity or below 0, with no if needed. Each method is O(1).
Step by step
- The water property. The header already makes
capacitya property. Addvar water: Int = 0: a new bottle is empty, andvarbecause it changes. Its name differs from the methodlevel. - fill. Compute
addedasminOf(amount, capacity - water). If everything fits, that isamount; otherwise it is exactly the room left, and the bottle ends full. In a full bottle the room is 0, soaddedis 0. - Update and report. Add
added(notamount) towater, then returnadded. Returning it lets the caller see whether everything fit. - drink. The same shape:
drunkisminOf(amount, water), subtract it fromwaterand return it. From an empty bottle,drunkis 0. - level. Return
water.
The solution in Kotlin
class WaterBottle(val capacity: Int) {
var water: Int = 0
fun fill(amount: Int): Int {
val added = minOf(amount, capacity - water)
water += added
return added
}
fun drink(amount: Int): Int {
val drunk = minOf(amount, water)
water -= drunk
return drunk
}
fun level(): Int {
return water
}
}
minOf is a function from Kotlin's standard library that returns the smaller of two values. val added is a local value: it is set once and never reassigned, while water is a var because it changes with every call.
Using the class
fun main() {
val w = WaterBottle(300)
println(w.fill(250))
println(w.fill(100))
println(w.level())
println(w.fill(10))
println(w.drink(100))
println(w.level())
println(w.drink(500))
println(w.drink(5))
}
It prints:
250
50
300
0
100
200
200
0
The sixth line shows the level after drinking 100 from a full bottle of 300. drink(500) takes only the 200 that were left, and the next drink(5) gets nothing.
Tracing a few calls
The table follows a bottle with capacity 100 through the calls drink(10), fill(60), drink(100) and drink(5). The bottle starts empty.
| Call | Water before | Amount that moves | Water after | Returns |
|---|---|---|---|---|
drink(10) | 0 | min(10, 0) = 0 | 0 | 0 |
fill(60) | 0 | min(60, 100 - 0) = 60 | 60 | 60 |
drink(100) | 60 | min(100, 60) = 60 | 0 | 60 |
drink(5) | 0 | min(5, 0) = 0 | 0 | 0 |
Whenever the request is too large, the bottle ends exactly full or exactly empty. Whenever it is small enough, the water changes by exactly the requested amount.
Why the property is not a parameter
The capacity is written in the header with val, so it is a read-only property: a bottle never changes its size. The water is declared in the body with var, because it changes with every call. Each bottle you create has its own water, so filling one bottle leaves another untouched:
fun main() {
val small = WaterBottle(100)
val big = WaterBottle(1000)
println(small.fill(500))
println(big.fill(500))
println("${small.level()} ${big.level()}")
}
It prints:
100
500
100 500
Common mistakes
- Adding
amountinstead ofadded. The water would go past the capacity. - Returning
amountinstead of what moved. The tests check the return values:fill(100)in the example must return 50, not 100. - Using
waterwhere the room is meant. The limit forfilliscapacity - water, notcapacity. - Forgetting that the bottle starts empty. A first
drinkmust return 0.
Edge cases to try
- An amount of 0.
fill(0)anddrink(0)move nothing and return 0. The formulas handle it without a special case, since the smaller of 0 and anything non-negative is 0. - An exact fit. On an empty bottle of 500,
fill(500)returns 500 and leaves the bottle full. Thenfill(1)returns 0, because the room is 0. - Drinking everything.
drink(level())empties the bottle and returns what was in it. Any furtherdrinkreturns 0. - A request far beyond the limit. The constraints allow amounts up to 10,000 with capacities as small as 1. The bottle still ends exactly full or empty, and the return value says how much really moved.
A short program shows the exact fit, the empty bottle and the tiny capacity:
fun main() {
val b = WaterBottle(500)
println(b.fill(500))
println(b.fill(1))
println(b.drink(b.level()))
println(b.drink(1))
val t = WaterBottle(1)
println(t.fill(10000))
println(t.level())
}
It prints 500, 0, 500, 0, then 1 and 1 for the tiny bottle: ten thousand milliliters offered, one accepted.
The two return values are the part beginners most often forget. The water level alone doesn't tell the caller whether the whole request was honoured; the number returned by fill and drink does.
