Start learning free

LESSON 254 OF 573 · FUNCTIONS & CLASSES

Water Bottle in Go: A Struct That Protects Its Limits

Practice O(1) Read the full explanation

A water 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 in it. Write a class WaterBottle that keeps track of how much water is in one bottle as you fill it and drink from it. Amounts are whole milliliters.

Implement the class WaterBottle:

  • WaterBottle(capacity) creates an empty bottle that holds at most capacity.
  • fill(amount) pours in amount, but only as much as fits: the water never goes above the capacity. It returns how much actually went in.
  • drink(amount) drinks amount, but only as much as is in the bottle: the water never goes below 0. It returns how much was actually drunk.
  • level() returns how much water is in the bottle now.

The input is a bottle's capacity and a list of operations, applied in order to one new WaterBottle. The output lists what the fill, drink and level calls return, in order. The starter code spells these names the way your language does (in Go and Rust, WaterBottle is a struct with methods). Give the fields names that differ from the methods' names: in several languages a field named level would clash with the method level().

 

Example 1:

Input:
    WaterBottle(500)
    fill(200)
    drink(50)
    level()
Output: 200 50 150
Explanation: The empty bottle has room for 500, so all 200 go in. Drinking 50 of them leaves 150.

Example 2:

Input:
    WaterBottle(300)
    fill(250)
    fill(100)
    level()
    fill(10)
Output: 250 50 300 0
Explanation: After the first fill, only 50 more fit: fill(100) pours in 50 and returns 50, and the bottle
is full. A full bottle takes nothing more, so fill(10) returns 0.

Example 3:

Input:
    WaterBottle(100)
    drink(10)
    fill(60)
    drink(100)
    level()
    drink(5)
Output: 0 60 60 0 0
Explanation: The bottle starts empty, so the first drink gets nothing. drink(100) can only drink
the 60 that are there, which empties the bottle again.

 

Constraints:

  • 1 <= capacity <= 104
  • 0 <= amount <= 104
  • At most 250 operations on one WaterBottle.

Note:

Each method should run in O(1) time: the bottle only needs to remember its capacity and how much water it holds.

STEP 1 OF 10
    Ctrl + Enter to check

    GO · FUNCTIONS & CLASSES

    Water Bottle in Go, explained

    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 is capacity - 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

    1. Declare capacity, an int. It never changes, but fill needs it.
    2. Declare water, the current amount. The field can't be named level, because that is a method's name.
    3. Write the constructor. It sets capacity to the parameter and water to 0, since a new bottle is empty.
    4. In fill, compute added with := as the smaller of amount and this.capacity - this.water. The method changes the struct, so it has a pointer receiver.
    5. Add added to this.water. Adding added rather than amount keeps the water within the capacity.
    6. Return added.
    7. In drink, compute drunk as the smaller of amount and this.water.
    8. Subtract drunk from this.water. It can't go below 0, because drunk is at most this.water.
    9. Return drunk.
    10. Write level, which returns this.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, min is built into the language and takes two or more values of the same ordered type. No import is needed.
    • Pointer receivers. fill and drink change this.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 variable b without 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 += amount with 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 fill or drink. It compiles, returns plausible numbers, but the level never changes.
    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free