Start learning free

LESSON 568 OF 576 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Rust: Traits with Default Methods

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: the amount plus a processing fee. The total is worked out the same way for every payment; only the fee depends on the kind of payment. That is what an abstract class is for: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

Rust has no classes and no abstract classes. As in the abstract-classes lesson, a trait plays the abstract class:

  • The trait Payment declares two methods with no body, which every payment must write: fn get_amount(&self) -> i32 and fn fee(&self) -> i32.
  • It also has a default method, fn total(&self) -> i32, with a body: self.get_amount() + self.fee(). Every type that implements Payment gets it for free.
  • A trait can't hold fields, so Card, PayPal and Cash each keep their own amount. Card::new(amount), PayPal::new(amount) and Cash::new(amount) build them, and each impl Payment for ... writes get_amount and its own fee:
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

Dividing two i32s drops the fraction.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has every struct, every impl block and every method header: declare the trait's two methods and the structs' fields, and write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the trait Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    Card::new(1999)
    PayPal::new(500)
    Cash::new(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    Card::new(49)
    PayPal::new(33)
    Card::new(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    Card::new(0)
    PayPal::new(0)
    Cash::new(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 15
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Rust, explained

    Problem overview

    A shop takes payments by card, by PayPal and in cash. Each payment has an amount in cents and a total, which is the amount plus a fee. The total is worked out the same way for every payment. Only the fee depends on the kind of payment:

    • Card: amount × 2 / 100.
    • PayPal: amount × 3 / 100 + 30.
    • Cash: 0.

    Every division drops the fraction. A card payment of 1999 cents has a fee of 39, not 39.98, so its total is 2038. A PayPal payment of 500 cents has a fee of 45 and a total of 545, and cash of 700 has a total of 700.

    Approach: a trait plays the abstract class

    Rust has no classes, so no abstract classes either. A trait does the same job. Payment declares two methods with no body, get_amount and fee, which every payment must write. It also has one default method, total, whose body calls those two. Every type that implements Payment gets total for free, and it runs the fee of the type it is called on.

    A trait can't hold fields. That is why each struct keeps its own amount and hands it to the trait through get_amount.

    Step by step

    1. Declare get_amount in the trait: it takes &self, returns an i32 and ends with a semicolon instead of a body.
    2. Declare fee the same way. The trait can't know a payment's fee, so each type has to supply it.
    3. Write the default total: self.get_amount() + self.fee(), without a semicolon.
    4. Give Card an amount field and a new that builds the struct from its parameter.
    5. Implement get_amount by returning self.amount.
    6. Implement fee as self.amount * 2 / 100.
    7. Repeat for PayPal with self.amount * 3 / 100 + 30.
    8. Repeat for Cash, whose fee is just 0. It still has to write fee, because the trait requires it.

    The solution in Rust

    trait Payment {
        fn get_amount(&self) -> i32;
        fn fee(&self) -> i32;
    
        fn total(&self) -> i32 {
            self.get_amount() + self.fee()
        }
    }
    
    struct Card {
        amount: i32,
    }
    
    impl Card {
        fn new(amount: i32) -> Card {
            Card { amount }
        }
    }
    
    impl Payment for Card {
        fn get_amount(&self) -> i32 {
            self.amount
        }
    
        fn fee(&self) -> i32 {
            self.amount * 2 / 100
        }
    }
    
    struct PayPal {
        amount: i32,
    }
    
    impl PayPal {
        fn new(amount: i32) -> PayPal {
            PayPal { amount }
        }
    }
    
    impl Payment for PayPal {
        fn get_amount(&self) -> i32 {
            self.amount
        }
    
        fn fee(&self) -> i32 {
            self.amount * 3 / 100 + 30
        }
    }
    
    struct Cash {
        amount: i32,
    }
    
    impl Cash {
        fn new(amount: i32) -> Cash {
            Cash { amount }
        }
    }
    
    impl Payment for Cash {
        fn get_amount(&self) -> i32 {
            self.amount
        }
    
        fn fee(&self) -> i32 {
            0
        }
    }
    

    Used through the trait, the three kinds are handled alike. This program builds the first example's payments and prints each total, then the sum of the fees:

    fn main() {
        let payments: Vec<Box<dyn Payment>> = vec![Box::new(Card::new(1999)), Box::new(PayPal::new(500)), Box::new(Cash::new(700))];
        let totals: Vec<String> = payments.iter().map(|p| p.total().to_string()).collect();
        println!("{}", totals.join(" "));
        println!("{}", payments.iter().map(|p| p.fee()).sum::<i32>());
    }
    
    2038 545 700
    84
    

    Rust details worth knowing

    • Required against default. A trait method without a body is required, and a type that doesn't write it fails to compile. A method with a body is a default that types may keep or replace.
    • Multiply first. Integer division drops the fraction, so the order matters. With an amount of 150, amount * 2 / 100 is 3, but amount / 100 * 2 is 2, because 150 / 100 has already been cut down to 1.
    • Size of the numbers. The largest amount is one million (1,000,000), so amount * 3 is at most 3,000,000, far inside the range of an i32.
    • Struct shorthand. Card { amount } is short for Card { amount: amount }.

    The word "fee" in the lesson is a rule per kind of payment, and the lesson treats the percentage as integer arithmetic on cents, so there are no decimals to round by hand. Working in whole cents is a common way to avoid fractions in money code: every value is an integer, and the only rounding is the division, which always rounds down.

    Edge cases and common mistakes

    • Small amounts round down to zero. A card payment of 49 cents has a fee of 0, because 49 × 2 / 100 is 0.98 rounded down. A PayPal payment of 33 cents also has 0 from the percentage, plus the 30.
    • An amount of 0. PayPal still adds its 30 cents, so PayPal::new(0) has a total of 30. Card and Cash give 0.
    • Leaving out fee for Cash. The compiler rejects the impl block, because fee has no default.
    • Overriding total by accident. It is shared on purpose. Keep the plus in one place and let the types supply only the parts that differ.

    What the default method gives you

    Any new payment type gets total without writing it. A gift card with a flat fee of 100 cents needs only the two required methods:

    struct Gift {
        amount: i32,
    }
    
    impl Payment for Gift {
        fn get_amount(&self) -> i32 {
            self.amount
        }
    
        fn fee(&self) -> i32 {
            100
        }
    }
    

    A Gift { amount: 2500 } reports a total() of 2600: the shared method added the amount and the gift's own fee. This is the same split an abstract class makes. The shared rule (the total is amount plus fee) lives once in the trait, and the part that varies (the fee) is left to each type.

    Because the code that uses payments calls everything through the trait, it can mix the kinds freely and never needs to know which struct is behind a box. The trait also keeps the two required methods small: get_amount exists only because a trait can't read a field of the struct that implements it.

    Testing

    For Card::new(49), PayPal::new(33), Card::new(50) and PayPal::new(0), the totals and the sum of the fees are:

    49 63 51 30
    61
    

    Every fee and every total takes constant time.

    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free