Taro Logo

Calculator with Method Chaining

#756 Most AskedEasy
Topics:
Arrays

Design a Calculator class. The class should provide the mathematical operations of addition, subtraction, multiplication, division, and exponentiation. It should also allow consecutive operations to be performed using method chaining. The Calculator class constructor should accept a number which serves as the initial value of result.

Your Calculator class should have the following methods:

  • add - This method adds the given number value to the result and returns the updated Calculator.
  • subtract - This method subtracts the given number value from the result and returns the updated Calculator.
  • multiply - This method multiplies the result  by the given number value and returns the updated Calculator.
  • divide - This method divides the result by the given number value and returns the updated Calculator. If the passed value is 0, an error "Division by zero is not allowed" should be thrown.
  • power - This method raises the result to the power of the given number value and returns the updated Calculator.
  • getResult - This method returns the result.

Solutions within 10-5 of the actual result are considered correct.

Example 1:

Input: 
actions = ["Calculator", "add", "subtract", "getResult"], 
values = [10, 5, 7]
Output: 8
Explanation: 
new Calculator(10).add(5).subtract(7).getResult() // 10 + 5 - 7 = 8

Example 2:

Input: 
actions = ["Calculator", "multiply", "power", "getResult"], 
values = [2, 5, 2]
Output: 100
Explanation: 
new Calculator(2).multiply(5).power(2).getResult() // (2 * 5) ^ 2 = 100

Example 3:

Input: 
actions = ["Calculator", "divide", "getResult"], 
values = [20, 0]
Output: "Division by zero is not allowed"
Explanation: 
new Calculator(20).divide(0).getResult() // 20 / 0 

The error should be thrown because we cannot divide by zero.

Constraints:

  • actions is a valid JSON array of strings
  • values is a valid JSON array of numbers
  • 2 <= actions.length <= 2 * 104
  • 1 <= values.length <= 2 * 104 - 1
  • actions[i] is one of "Calculator", "add", "subtract", "multiply", "divide", "power", and "getResult"
  • First action is always "Calculator"
  • Last action is always "getResult"

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. Should the calculator handle both integer and floating-point numbers for its inputs and internal state, or should I assume all numbers are floats due to the precision requirement?
  2. The `power` method has several mathematical edge cases. What is the expected behavior for operations like `0 ** 0`, or for raising a negative base to a non-integer exponent like `(-4) ** 0.5`?
  3. The problem specifies throwing an error for division by zero. Should any other operations that result in undefined or non-real numbers, like the square root of a negative number, also throw an error?
  4. Are there any constraints on the magnitude of the input numbers or the intermediate results? For instance, do I need to consider numerical overflow or underflow where the result might become `Infinity`?
  5. After `getResult()` is called, does the internal result persist for subsequent operations on the same instance, or is an instance typically used for just one chain of calculations?

Brute Force Solution

Approach

The approach is to build a calculator that keeps a single, running total. Each time a new math operation is requested, it's immediately performed on this running total, which then becomes the new starting point for the next operation in the sequence.

Here's how the algorithm would work step-by-step:

  1. First, imagine the calculator starts by holding an initial number.
  2. This calculator has just one memory slot where it keeps the current result of all the math done so far.
  3. When you ask it to perform an operation, like adding a new number, it takes the number from its memory, does the math, and puts the new result right back into that same memory slot.
  4. This process of updating the single stored number happens for every single operation, one after the another.
  5. The key is that after each operation, the calculator is ready for the next one, allowing the commands to be linked together in a chain.
  6. This continues until all operations in the chain have been completed.
  7. Finally, a special command is used to get the final number currently stored in the calculator's memory.

Code Implementation

class Calculator:
    def __init__(self, initial_value):
        # This initializes the calculator's state, setting the starting point for all calculations.

        self.current_result = initial_value

    def add(self, number_to_add):
        self.current_result += number_to_add
        # Returning 'self' is the core mechanism that allows for the chaining of method calls.

        return self

    def subtract(self, number_to_subtract):
        self.current_result -= number_to_subtract
        return self

    def multiply(self, number_to_multiply):
        self.current_result *= number_to_multiply
        return self

    def divide(self, number_to_divide_by):
        # We must check for division by zero to prevent a runtime error, a critical safety measure.

        if number_to_divide_by == 0:
            raise ValueError("Cannot divide by zero.")
        self.current_result /= number_to_divide_by
        return self

    def get_result(self):
        # This final method provides access to the accumulated result after all chained operations.

        return self.current_result

Big(O) Analysis

Time Complexity
O(n)The time complexity is O(n), where 'n' represents the number of operations in the method chain. The total runtime is determined by processing each operation in the sequence from start to finish. Each individual mathematical operation is a constant-time action, taking O(1) time regardless of the current total. Since we perform 'n' of these constant-time operations, the total work is approximately n * 1 operations, which simplifies directly to a linear time complexity of O(n).
Space Complexity
O(1)The algorithm's space usage is determined by the single variable required to store the running total. As described, the calculator uses 'just one memory slot' to hold the current result of all math done so far. Each operation updates this single value in place, without creating any auxiliary data structures like lists or stacks to store intermediate results or a history of operations. Therefore, the amount of extra memory used remains constant, regardless of the number of operations, N, in the chain.

Optimal Solution

Approach

The core idea is to create a calculator that maintains a running total internally. Each mathematical operation modifies this running total and then, crucially, returns the calculator object itself, allowing the next operation to be immediately performed on the new value.

Here's how the algorithm would work step-by-step:

  1. First, think of a calculator that has a single memory slot to keep track of the current result. This starts with an initial number.
  2. When you perform an operation like 'add', the calculator takes the number in its memory, adds the new number to it, and stores this new sum back into its memory.
  3. Here is the key trick: after updating its memory, the calculator hands itself back. This is what allows you to immediately 'chain' another command, like 'subtract', onto the end.
  4. The 'subtract' command then works on the new value in the calculator's memory, updates it again, and once more hands the calculator back.
  5. This process of updating the internal number and returning the calculator object is repeated for every operation in the chain, whether it's multiplication, division, or anything else.
  6. After all the chained operations are done, a final command is used to get the result. This command simply reads the final number from the calculator's memory and gives it to you.
  7. A robust design should also consider special situations, such as what to do if someone tries to divide by zero, to prevent errors.

Code Implementation

class Calculator:
    def __init__(self, initial_value):
        # This holds the running total that gets updated by each operation in the chain.
        
        self.current_total = initial_value

    def add(self, number_to_add):
        self.current_total += number_to_add
        # Returning the object itself ('self') is the key to enabling method chaining.
        
        return self

    def subtract(self, number_to_subtract):
        self.current_total -= number_to_subtract
        return self

    def multiply(self, number_to_multiply):
        self.current_total *= number_to_multiply
        return self

    def divide(self, number_to_divide_by):
        # It is critical to handle invalid mathematical operations like division by zero.
        
        if number_to_divide_by == 0:
            raise ValueError("Cannot divide by zero.")
        self.current_total /= number_to_divide_by
        return self

    def get_result(self):
        # This final method is called at the end of the chain to retrieve the final value.
        
        return self.current_total

Big(O) Analysis

Time Complexity
O(n)Let n be the number of operations in the method chain. Each operation, such as add or subtract, performs a single arithmetic calculation on the internal running total, which is a constant-time or O(1) task. The total time complexity is therefore directly driven by the number of these chained calls. Executing a sequence of n operations results in a total runtime that is linearly proportional to n, which simplifies to O(n).
Space Complexity
O(1)The space complexity is constant because the calculator object only needs to maintain a single internal variable to store the running total. Each chained operation, as described, modifies this single value in-place without creating any new data structures or increasing the object's memory footprint. The amount of auxiliary memory used does not grow with the number of operations in the chain, N. Therefore, the space required is constant, regardless of the length of the method chain.

Edge Cases

Dividing the current result by zero.
How to Handle:
The divide method must explicitly check if the input value is zero and throw the specified error.
A sequence of operations where mathematical precedence might be assumed, such as addition followed by multiplication.
How to Handle:
The method chaining design strictly enforces a left-to-right evaluation order, ignoring standard mathematical operator precedence.
Operations that can introduce floating-point inaccuracies, such as dividing by 3.
How to Handle:
The class uses standard floating-point arithmetic, and the problem's tolerance of 10^-5 accommodates for minor precision errors.
Multiplying the current result by zero at any point in the chain.
How to Handle:
The result correctly becomes zero, which affects all subsequent chained operations.
Operations resulting in numbers that exceed the standard floating-point representation limits.
How to Handle:
The result will become Infinity or -Infinity, which is the standard behavior for numeric overflow.
The current result is zero and the power method is called with a negative exponent.
How to Handle:
This is an implicit division by zero, and the result correctly becomes Infinity based on standard library behavior.
The current result is a negative number and the power method is called with a fractional exponent.
How to Handle:
This operation is undefined for real numbers, and the result correctly becomes NaN (Not a Number).
Chaining further operations after the result has become a special numeric value like Infinity or NaN.
How to Handle:
Subsequent arithmetic operations will correctly propagate these special values according to standard IEEE 754 floating-point rules.
0/1037 completed