Problem overview
A water bottle holds at most a fixed amount. You can't pour in more than fits, and you can't drink more than is in it. The WaterBottle in this lesson tracks one bottle in whole milliliters.
NewWaterBottle(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 is there and returns how much was actually drunk.level()returns how much water is in the bottle now.
With a capacity of 300, fill(250) returns 250. Then fill(100) returns only 50, because only 50 fit, and the bottle is full. One more fill(10) returns 0.
Approach
The bottle needs to remember two numbers: its capacity, which never changes, and the water in it now. Both operations follow the same pattern: work out how much can really move, then move exactly that.
- For
fill, the room left iscapacity - water. The amount that goes in is the smaller of the request and the room. - For
drink, the most you can drink is what is in the bottle. The amount drunk is the smaller of the request and the water.
Because the moved amount is never larger than the room or the water, the level always stays between 0 and the capacity, with no extra checks. Returning the moved amount tells the caller whether the whole request was met. Each method is constant time.
Step by step
- Declare
capacity, anint. It never changes, butfillneeds it. - Declare
water, the current amount. The field can't be namedlevel, because that is a method's name. - Write the constructor. It sets
capacityto the parameter andwaterto 0, since a new bottle is empty. - In
fill, computeaddedwith:=as the smaller ofamountandthis.capacity - this.water. The method changes the struct, so it has a pointer receiver. - Add
addedtothis.water. Addingaddedrather thanamountkeeps the water within the capacity. - Return
added. - In
drink, computedrunkas the smaller ofamountandthis.water. - Subtract
drunkfromthis.water. It can't go below 0, becausedrunkis at mostthis.water. - Return
drunk. - Write
level, which returnsthis.water. It only reads, so a value receiver is enough.
The solution in Go
type WaterBottle struct {
capacity int
water int
}
func NewWaterBottle(capacity int) WaterBottle {
return WaterBottle{capacity: capacity, water: 0}
}
func (this *WaterBottle) fill(amount int) int {
added := min(amount, this.capacity - this.water)
this.water += added
return added
}
func (this *WaterBottle) drink(amount int) int {
drunk := min(amount, this.water)
this.water -= drunk
return drunk
}
func (this WaterBottle) level() int {
return this.water
}
Go details:
- The built-in
min. Since Go 1.21,minis built into the language and takes two or more values of the same ordered type. No import is needed. - Pointer receivers.
fillanddrinkchangethis.water, so they need(this *WaterBottle). With a value receiver, they would change a copy and the bottle would stay as it was.b.fill(250)works on a variablebwithout writing&b. - Short declaration.
added := ...declares and sets a local variable in one step. An unused local variable is a compile error, so the code has to use it, and it does.
Why the level can't go wrong
There is no if in either method, yet the level can't leave the range from 0 to the capacity. After fill, the new level is water + min(amount, capacity - water), which is at most capacity. After drink, it is water - min(amount, water), which is at least 0. The two limits come from the choice of the moved amount, not from patching the level afterwards. Writing the methods this way means the rule is kept in one place and every caller gets it for free.
A common first attempt adds the amount and then clamps the level back, for example setting it to the capacity if it went over. That gives the right level but loses the information about how much really went in, and the methods are supposed to return it.
Using it
b := NewWaterBottle(300)
fmt.Println(b.fill(250))
fmt.Println(b.fill(100))
fmt.Println(b.level())
fmt.Println(b.fill(10))
fmt.Println(b.drink(1000))
fmt.Println(b.drink(5), b.level())
This prints:
250
50
300
0
300
0 0
The request drink(1000) returns 300, everything that was there. After that the bottle is empty, so drink(5) returns 0 and the level stays 0.
For a bottle with capacity 100, the sequence drink(10), fill(60), drink(100), level(), drink(5) returns 0, 60, 60, 0 and 0.
Asking for too much is not an error in this design. Pouring 100 into a nearly full bottle or drinking 1000 from a small one just moves what is possible and reports it. The caller can compare the returned number with the request to see whether it was met in full.
Edge cases and common mistakes
- Adding the full amount.
this.water += amountwith no limit lets the level pass the capacity. - Returning the request instead of the result.
fill(100)on a nearly full bottle must return what went in, not 100. - Drinking from an empty bottle. It returns 0 and leaves the level at 0.
- Amount 0. Both methods return 0 and change nothing.
- A value receiver on
fillordrink. It compiles, returns plausible numbers, but the level never changes.
