Skip to content

Java Calculator Implementation

Question

Implement a calculator in Java that supports: 1. Basic arithmetic operations (+, -, *, /) 2. Complex expressions with parentheses 3. Mathematical constants (π, e) 4. Memory operations (add, subtract, recall, clear, store) 5. Unit conversions (length, weight, temperature) 6. Expression history with timestamps 7. Error handling for invalid expressions and operations

Answer

Repository

You can find the full source code and project repository on GitHub: https://github.com/navidmo/calculator

Overview

This calculator is a modular and extensible command-line application written in Java. It supports arithmetic expression evaluation with operator precedence, memory operations, unit conversions (length, weight, temperature), and calculation history tracking. The main entry point is the CalculatorApplication class, which provides an interactive loop for reading user input, parsing commands, delegating operations to subsystems, and displaying results or errors. It supports commands such as arithmetic expressions (2 + 3 * 4), memory operations (memory add 5), unit conversions (convert 1 km to m), and history retrieval (history). The loop continues until the user enters exit, at which point the application shuts down gracefully.

The computational engine is encapsulated in the Calculator class, which acts as a facade over several modular components:
- ExpressionParser: parses infix arithmetic expressions into an abstract syntax tree and handles constants like π and e.
- UnitConverter: performs unit conversions within and across metric and imperial systems for length, weight, and temperature.
- Memory: implements calculator-style memory with support for add, subtract, recall, store, and clear.
- History: records evaluated expressions along with their results and timestamps.

Expressions are modeled via a class hierarchy rooted in the abstract Expression class. The system supports both constant values (NumberExpression) and binary operations (BinaryExpression). Arithmetic operators are encapsulated in the Operator enum, which defines each operation's symbol and implementation using BiFunction.

Exception handling is centralized using the custom CalculatorException class, which ensures consistent error reporting across expression evaluation, unit conversions, and memory operations.

Class Diagram

uml diagram

The class diagram above outlines the architecture and relationships of the calculator codes. It is organized into four major areas: core logic, expression hierarchy, supporting utilities, and testing infrastructure.

Core Classes
These classes form the backbone of the calculator's execution and user interaction logic: - CalculatorApplication: Provides the command-line interface and interactive event loop. It parses user commands and delegates actions to the appropriate subsystems. - Calculator: Acts as a façade that orchestrates expression evaluation, memory operations, unit conversion, and history tracking. - ExpressionParser: Parses infix mathematical expressions (including constants like π and e) into an expression tree based on operator precedence rules. - UnitConverter: Performs unit conversions for length, weight, and temperature. Supports mixed systems and handles special logic for temperature.

Expression Hierarchy
This set of classes represents the object-oriented model for evaluating arithmetic expressions: - Expression: Abstract base class for all mathematical expressions. Declares the contract for evaluation. - NumberExpression: Leaf node representing constant numeric values. - BinaryExpression: Internal node representing binary operations (addition, subtraction, multiplication, division). - Operator: Enum that defines arithmetic operators and encapsulates both their symbolic form and evaluation logic via functional interfaces.

Support Classes
These utility classes provide memory management, error handling, and persistent history tracking: - Memory: Implements calculator-style memory with support for store, add, subtract, recall, and clear. - History: Maintains a fixed-size list of previous calculation results (up to 100 entries by default). - HistoryEntry: Data structure representing a single evaluated expression, its result, and timestamp. - CalculatorException: Custom unchecked exception used throughout the system for consistent error reporting across evaluation, parsing, and conversion modules.

Test Classes
JUnit 5-based test suites that validate the correctness of each subsystem: - CalculatorTest: Unit tests for the Calculator class, including arithmetic, memory, history, and unit conversion scenarios. - ExpressionParserTest: Tests for the expression parsing logic, including constants, operator precedence, parentheses, and edge cases. - UnitConverterTest: Tests for unit conversion accuracy, category mismatches, identity conversions, and floating-point correctness.

Implementation Details and Main Source Files

1. CalculatorApplication.java

package calc;

import java.util.List;
import java.util.Scanner;

/**
 * A command-line interface for the calculator.
 * 
 * <p>This application provides an interactive command-line interface for:
 * <ul>
 *   <li>Evaluating mathematical expressions</li>
 *   <li>Performing unit conversions</li>
 *   <li>Managing calculator memory</li>
 *   <li>Viewing calculation history</li>
 * </ul>
 * 
 * <p>Supported commands:
 * <ul>
 *   <li>Arithmetic expressions (e.g., "2 + 3 * 4")</li>
 *   <li>Unit conversions (e.g., "convert 1 km to m")</li>
 *   <li>Memory operations (e.g., "memory add 5", "memory recall")</li>
 *   <li>History viewing ("history")</li>
 *   <li>Exit command ("exit")</li>
 * </ul>
 */
public class CalculatorApplication {
    /** The calculator instance that performs all calculations */
    private final Calculator calculator;

    /** Scanner for reading user input */
    private final Scanner scanner;

    /**
     * Creates a new calculator application with initialized components.
     */
    public CalculatorApplication() {
        this.calculator = new Calculator();
        this.scanner = new Scanner(System.in);
    }

    /**
     * Runs the calculator application in an interactive loop.
     * 
     * <p>This method:
     * <ol>
     *   <li>Displays a welcome message</li>
     *   <li>Enters an input loop that:
     *     <ul>
     *       <li>Prompts for input</li>
     *       <li>Processes commands</li>
     *       <li>Displays results or errors</li>
     *     </ul>
     *   </li>
     *   <li>Continues until the user types "exit"</li>
     * </ol>
     */
    public void run() {
        System.out.println("Calculator started. Type 'exit' to quit.");

        while (true) {
            System.out.print("> ");
            String input = scanner.nextLine().trim();

            if (input.equalsIgnoreCase("exit")) {
                break;
            }

            try {
                if (input.startsWith("convert")) {
                    handleConversion(input);
                } else if (input.startsWith("memory")) {
                    handleMemory(input);
                } else if (input.equals("history")) {
                    showHistory();
                } else {
                    double result = calculator.evaluate(input);
                    System.out.printf("Result: %.2f%n", result);
                }
            } catch (CalculatorException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }

        System.out.println("Calculator terminated.");
        scanner.close();
    }

    /**
     * Handles unit conversion commands.
     * 
     * <p>Expected format: "convert {value} {from} to {to}"
     * Example: "convert 1 km to m"
     * 
     * @param input The conversion command string
     * @throws CalculatorException if the command format is invalid
     */
    private void handleConversion(String input) {
        String[] parts = input.split("\\s+");
        if (parts.length != 5) {
            throw new CalculatorException(
                "Invalid conversion format. Use: convert {value} {from} to {to}");
        }

        double value = Double.parseDouble(parts[1]);
        String fromUnit = parts[2];
        String toUnit = parts[4];

        double result = calculator.convert(value, fromUnit, toUnit);
        System.out.printf("%.2f %s = %.2f %s%n", 
                        value, fromUnit, result, toUnit);
    }

    /**
     * Handles memory operation commands.
     * 
     * <p>Supported commands:
     * <ul>
     *   <li>memory add {value}</li>
     *   <li>memory subtract {value}</li>
     *   <li>memory recall</li>
     *   <li>memory clear</li>
     * </ul>
     * 
     * @param input The memory command string
     * @throws CalculatorException if the command format is invalid
     */
    private void handleMemory(String input) {
        String[] parts = input.split("\\s+");
        if (parts.length < 2) {
            throw new CalculatorException(
                "Invalid memory command. Use: memory [add|subtract|recall|clear]");
        }

        switch (parts[1].toLowerCase()) {
            case "add" -> {
                if (parts.length != 3) {
                    throw new CalculatorException(
                        "Invalid format. Use: memory add {value}");
                }
                calculator.memoryAdd(Double.parseDouble(parts[2]));
            }
            case "subtract" -> {
                if (parts.length != 3) {
                    throw new CalculatorException(
                        "Invalid format. Use: memory subtract {value}");
                }
                calculator.memorySubtract(Double.parseDouble(parts[2]));
            }
            case "recall" -> 
                System.out.printf("Memory value: %.2f%n", 
                                calculator.memoryRecall());
            case "clear" -> calculator.memoryClear();
            default -> throw new CalculatorException(
                "Unknown memory command: " + parts[1]);
        }
    }

    /**
     * Displays the calculation history.
     * 
     * <p>Shows a list of all previously evaluated expressions with their
     * results and timestamps.
     */
    private void showHistory() {
        List<HistoryEntry> entries = calculator.getHistory();
        if (entries.isEmpty()) {
            System.out.println("No history available.");
            return;
        }

        for (HistoryEntry entry : entries) {
            System.out.printf("%s: %s = %.2f%n", 
                            entry.getTimestamp(),
                            entry.getExpression(),
                            entry.getResult());
        }
    }

    /**
     * The main entry point for the calculator application.
     * 
     * @param args Command-line arguments (not used)
     */
    public static void main(String[] args) {
        CalculatorApplication app = new CalculatorApplication();
        app.run();
    }
}

2. Calculator.java

package calc;

import java.util.List;

/**
 * A calculator that supports arithmetic operations, memory operations,
 * unit conversions, and expression history.
 * 
 * <p>This calculator provides the following features:
 * <ul>
 *   <li>Arithmetic expression evaluation with operator precedence</li>
 *   <li>Memory operations (add, subtract, recall, clear, store)</li>
 *   <li>Unit conversions between different measurement systems</li>
 *   <li>Expression history with timestamps</li>
 * </ul>
 * 
 * <p>Example usage:
 * <pre>
 * Calculator calc = new Calculator();
 * double result = calc.evaluate("2 + 3 * 4"); // returns 14.0
 * calc.memoryStore(42.0);
 * double memoryValue = calc.memoryRecall(); // returns 42.0
 * </pre>
 */
public class Calculator {
    /** Stores the history of evaluated expressions */
    private final History history;

    /** Manages calculator memory operations */
    private final Memory memory;

    /** Parses and validates mathematical expressions */
    private final ExpressionParser parser;

    /** Handles unit conversions */
    private final UnitConverter unitConverter;

    /**
     * Creates a new Calculator instance with initialized components.
     */
    public Calculator() {
        this.history = new History();
        this.memory = new Memory();
        this.parser = new ExpressionParser();
        this.unitConverter = new UnitConverter();
    }

    /**
     * Evaluates a mathematical expression and adds it to the history.
     * 
     * <p>This method:
     * <ol>
     *   <li>Parses and validates the expression</li>
     *   <li>Evaluates the expression</li>
     *   <li>Records the result in the history</li>
     * </ol>
     * 
     * @param expression The mathematical expression to evaluate
     * @return The result of evaluating the expression
     * @throws CalculatorException if the expression is invalid or evaluation fails
     */
    public double evaluate(String expression) {
        try {
            // Parse and validate expression
            Expression parsedExpr = parser.parse(expression);

            // Evaluate expression
            double result = parsedExpr.evaluate();

            // Add to history
            history.addEntry(expression, result);

            return result;
        } catch (CalculatorException e) {
            history.addError(expression, e.getMessage());
            throw e;
        }
    }

    /**
     * Adds a value to the calculator's memory.
     * 
     * @param value The value to add to memory
     */
    public void memoryAdd(double value) {
        memory.add(value);
    }

    /**
     * Subtracts a value from the calculator's memory.
     * 
     * @param value The value to subtract from memory
     */
    public void memorySubtract(double value) {
        memory.subtract(value);
    }

    /**
     * Returns the current value stored in memory.
     * 
     * @return The value stored in memory
     */
    public double memoryRecall() {
        return memory.recall();
    }

    /**
     * Clears the calculator's memory.
     */
    public void memoryClear() {
        memory.clear();
    }

    /**
     * Stores a value in the calculator's memory.
     * 
     * @param value The value to store in memory
     */
    public void memoryStore(double value) {
        memory.store(value);
    }

    /**
     * Returns the list of history entries.
     * 
     * @return A list of history entries containing expressions and their results
     */
    public List<HistoryEntry> getHistory() {
        return history.getEntries();
    }

    /**
     * Converts a value from one unit to another.
     * 
     * @param value The value to convert
     * @param fromUnit The source unit (e.g., "km", "m", "kg")
     * @param toUnit The target unit (e.g., "m", "km", "g")
     * @return The converted value
     * @throws CalculatorException if the conversion is not supported
     */
    public double convert(double value, String fromUnit, String toUnit) {
        return unitConverter.convert(value, fromUnit, toUnit);
    }

    /**
     * Clears the calculator's expression history.
     */
    public void clearHistory() {
        history.clear();
    }
}

3. Expression.java

package calc;

/**
 * Abstract base class for mathematical expressions.
 * 
 * <p>This class serves as the foundation for all mathematical expressions
 * in the calculator. It defines a single abstract method {@link #evaluate()}
 * that must be implemented by all concrete expression classes.
 * 
 * <p>Example usage:
 * <pre>
 * Expression expr = new NumberExpression(42.0);
 * double result = expr.evaluate(); // returns 42.0
 * 
 * Expression sum = new BinaryExpression(
 *     new NumberExpression(2.0),
 *     new NumberExpression(3.0),
 *     Operator.ADD
 * );
 * double sumResult = sum.evaluate(); // returns 5.0
 * </pre>
 */
public abstract class Expression {
    /**
     * Creates a new expression.
     * 
     * <p>This constructor is provided for subclasses to use.
     */
    protected Expression() {
    }

    /**
     * Evaluates the mathematical expression.
     * 
     * @return The result of evaluating the expression
     * @throws CalculatorException if evaluation fails (e.g., division by zero)
     */
    public abstract double evaluate();
}

4. BinaryExpression.java

package calc;

/**
 * Represents a binary operation in a mathematical expression.
 * 
 * <p>This class combines two sub-expressions with a binary operator
 * (+, -, *, /). When evaluated, it first evaluates both sub-expressions
 * and then applies the operator to their results.
 * 
 * <p>Example usage:
 * <pre>
 * Expression left = new NumberExpression(2.0);
 * Expression right = new NumberExpression(3.0);
 * Expression sum = new BinaryExpression(left, right, Operator.ADD);
 * double result = sum.evaluate(); // returns 5.0
 * </pre>
 */
public class BinaryExpression extends Expression {
    /** The left operand of the binary operation */
    private final Expression left;

    /** The right operand of the binary operation */
    private final Expression right;

    /** The operator to apply to the operands */
    private final Operator operator;

    /**
     * Creates a new binary expression with the specified operands and operator.
     * 
     * @param left The left operand expression
     * @param right The right operand expression
     * @param operator The binary operator to apply
     */
    public BinaryExpression(Expression left, Expression right, Operator operator) {
        this.left = left;
        this.right = right;
        this.operator = operator;
    }

    /**
     * Evaluates the binary expression by first evaluating both operands
     * and then applying the operator to their results.
     * 
     * @return The result of applying the operator to the evaluated operands
     * @throws CalculatorException if evaluation fails (e.g., division by zero)
     */
    @Override
    public double evaluate() {
        double leftValue = left.evaluate();
        double rightValue = right.evaluate();

        return operator.apply(leftValue, rightValue);
    }
}

5. NumberExpression.java

package calc;

/**
 * Represents a constant numerical value in a mathematical expression.
 * 
 * <p>This class is used to represent literal numbers in expressions.
 * It provides a simple wrapper around a double value and implements
 * the {@link Expression} interface.
 * 
 * <p>Example usage:
 * <pre>
 * Expression num = new NumberExpression(42.0);
 * double result = num.evaluate(); // returns 42.0
 * </pre>
 */
public class NumberExpression extends Expression {
    /** The constant numerical value represented by this expression */
    private final double value;

    /**
     * Creates a new number expression with the specified value.
     * 
     * @param value The numerical value to represent
     */
    public NumberExpression(double value) {
        this.value = value;
    }

    /**
     * Returns the constant value represented by this expression.
     * 
     * @return The numerical value
     */
    @Override
    public double evaluate() {
        return value;
    }
}

6. Operator.java

package calc;

import java.util.function.BiFunction;

/**
 * Represents the supported arithmetic operations in the calculator.
 * 
 * <p>This enum defines the basic arithmetic operations (addition, subtraction,
 * multiplication, division) along with their symbols and operation implementations.
 * Each operator provides methods to:
 * <ul>
 *   <li>Apply the operation to two operands</li>
 *   <li>Get the symbol representing the operation</li>
 * </ul>
 * 
 * <p>Example usage:
 * <pre>
 * double result = Operator.ADD.apply(2.0, 3.0); // returns 5.0
 * String symbol = Operator.MULTIPLY.getSymbol(); // returns "*"
 * </pre>
 */
public enum Operator {
    /** Addition operation (+) */
    ADD("+", (a, b) -> a + b),

    /** Subtraction operation (-) */
    SUBTRACT("-", (a, b) -> a - b),

    /** Multiplication operation (*) */
    MULTIPLY("*", (a, b) -> a * b),

    /** Division operation (/) */
    DIVIDE("/", (a, b) -> {
        if (b == 0) {
            throw new CalculatorException("Division by zero");
        }
        return a / b;
    });

    /** The symbol used to represent this operator in expressions */
    private final String symbol;

    /** The function that implements this operator's operation */
    private final BiFunction<Double, Double, Double> operation;

    /**
     * Creates a new operator with the specified symbol and operation.
     * 
     * @param symbol The symbol representing the operation
     * @param operation The function implementing the operation
     */
    Operator(String symbol, BiFunction<Double, Double, Double> operation) {
        this.symbol = symbol;
        this.operation = operation;
    }

    /**
     * Applies this operator to two operands.
     * 
     * @param a The left operand
     * @param b The right operand
     * @return The result of applying this operator to the operands
     * @throws CalculatorException if the operation fails (e.g., division by zero)
     */
    public double apply(double a, double b) {
        return operation.apply(a, b);
    }

    /**
     * Returns the symbol used to represent this operator in expressions.
     * 
     * @return The operator's symbol
     */
    public String getSymbol() {
        return symbol;
    }
}

7. ExpressionParser.java

package calc;

import java.util.Map;
import java.util.Stack;
import java.util.EmptyStackException;

/**
 * Parses and evaluates mathematical expressions.
 * 
 * <p>This class provides functionality to parse mathematical expressions
 * written in infix notation (e.g., "2 + 3 * 4") and evaluate them according
 * to standard operator precedence rules.
 * 
 * <p>Supported operations:
 * <ul>
 *   <li>Addition (+)</li>
 *   <li>Subtraction (-)</li>
 *   <li>Multiplication (*)</li>
 *   <li>Division (/)</li>
 * </ul>
 * 
 * <p>Example usage:
 * <pre>
 * ExpressionParser parser = new ExpressionParser();
 * Expression expr = parser.parse("2 + 3 * 4"); // returns Expression that evaluates to 14.0
 * Expression expr2 = parser.parse("(2 + 3) * 4"); // returns Expression that evaluates to 20.0
 * </pre>
 */
public class ExpressionParser {
    /** Map of mathematical constants supported by the parser */
    private static final Map<String, Double> CONSTANTS = Map.of(
        "π", Math.PI,
        "pi", Math.PI,
        "e", Math.E
    );

    /**
     * Creates a new expression parser.
     */
    public ExpressionParser() {
    }

    /**
     * Parses and evaluates a mathematical expression.
     * 
     * @param expression The expression to parse and evaluate
     * @return The Expression object representing the parsed expression
     * @throws CalculatorException if the expression is invalid
     */
    public Expression parse(String expression) {
        try {
            // Remove whitespace
            expression = expression.replaceAll("\\s+", "");

            // Handle constants
            for (Map.Entry<String, Double> entry : CONSTANTS.entrySet()) {
                expression = expression.replace(entry.getKey(), String.valueOf(entry.getValue()));
            }

            return parseInfix(expression);
        } catch (EmptyStackException e) {
            throw new CalculatorException("Invalid expression: incomplete expression");
        }
    }

    /**
     * Parses an infix expression using the shunting yard algorithm.
     * 
     * @param expression The infix expression to parse
     * @return The Expression object representing the parsed expression
     * @throws CalculatorException if the expression is invalid
     */
    private Expression parseInfix(String expression) {
        Stack<Expression> values = new Stack<>();
        Stack<Character> operators = new Stack<>();

        boolean expectNumber = true;  // true if we expect a number or unary minus

        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);

            if (Character.isDigit(c) || c == '.') {
                StringBuilder num = new StringBuilder();
                while (i < expression.length() && 
                       (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                i--;
                values.push(new NumberExpression(Double.parseDouble(num.toString())));
                expectNumber = false;
                continue;
            }

            if (c == '-' && expectNumber) {
                // Handle unary minus
                StringBuilder num = new StringBuilder();
                num.append('-');
                i++;
                if (i >= expression.length() || (!Character.isDigit(expression.charAt(i)) && expression.charAt(i) != '.')) {
                    throw new CalculatorException("Invalid expression: expected number after unary minus");
                }
                while (i < expression.length() && 
                       (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                i--;
                values.push(new NumberExpression(Double.parseDouble(num.toString())));
                expectNumber = false;
                continue;
            }

            if (c == '+' || c == '-' || c == '*' || c == '/') {
                if (expectNumber && c != '-') {
                    throw new CalculatorException("Invalid expression: operator at wrong position");
                }
                while (!operators.empty() && hasPrecedence(c, operators.peek())) {
                    if (values.size() < 2) {
                        throw new CalculatorException("Invalid expression: not enough operands");
                    }
                    values.push(applyOperator(operators.pop(), values.pop(), values.pop()));
                }
                operators.push(c);
                expectNumber = true;
                continue;
            }

            if (c == '(') {
                operators.push(c);
                expectNumber = true;
                continue;
            }

            if (c == ')') {
                while (!operators.empty() && operators.peek() != '(') {
                    if (values.size() < 2) {
                        throw new CalculatorException("Invalid expression: not enough operands");
                    }
                    values.push(applyOperator(operators.pop(), values.pop(), values.pop()));
                }
                if (operators.empty() || operators.pop() != '(') {
                    throw new CalculatorException("Invalid expression: unmatched parentheses");
                }
                expectNumber = false;
                continue;
            }

            throw new CalculatorException("Invalid character in expression: " + c);
        }

        while (!operators.empty()) {
            if (operators.peek() == '(' || operators.peek() == ')') {
                throw new CalculatorException("Invalid expression: unmatched parentheses");
            }
            if (values.size() < 2) {
                throw new CalculatorException("Invalid expression: not enough operands");
            }
            values.push(applyOperator(operators.pop(), values.pop(), values.pop()));
        }

        if (values.isEmpty()) {
            throw new CalculatorException("Invalid expression: empty expression");
        }

        if (values.size() > 1) {
            throw new CalculatorException("Invalid expression: too many operands");
        }

        return values.pop();
    }

    /**
     * Applies an operator to two operands.
     * 
     * @param operator The operator to apply
     * @param b The right operand
     * @param a The left operand
     * @return The Expression representing the operation
     * @throws CalculatorException if the operator is invalid
     */
    private Expression applyOperator(char operator, Expression b, Expression a) {
        Operator op;
        switch (operator) {
            case '+': op = Operator.ADD; break;
            case '-': op = Operator.SUBTRACT; break;
            case '*': op = Operator.MULTIPLY; break;
            case '/': op = Operator.DIVIDE; break;
            default:
                throw new CalculatorException("Invalid operator: " + operator);
        }
        return new BinaryExpression(a, b, op);
    }

    /**
     * Checks if the first operator has higher precedence than the second.
     * 
     * @param op1 The first operator
     * @param op2 The second operator
     * @return true if op1 has higher precedence than op2
     */
    private boolean hasPrecedence(char op1, char op2) {
        if (op2 == '(' || op2 == ')') {
            return false;
        }
        if ((op1 == '*' || op1 == '/') && (op2 == '+' || op2 == '-')) {
            return false;
        }
        return true;
    }
}

8. UnitConverter.java

package calc;

import java.util.Map;
import java.util.HashMap;

/**
 * Handles unit conversions between different measurement systems.
 * 
 * <p>This class provides functionality to convert values between different units
 * of measurement, including:
 * <ul>
 *   <li>Length units (m, km, ft, in, etc.)</li>
 *   <li>Weight units (kg, g, lb, oz, etc.)</li>
 *   <li>Temperature units (C, F)</li>
 * </ul>
 * 
 * <p>Example usage:
 * <pre>
 * UnitConverter converter = new UnitConverter();
 * double meters = converter.convert(5.0, "ft", "m"); // converts 5 feet to meters
 * double celsius = converter.convert(98.6, "F", "C"); // converts 98.6°F to °C
 * </pre>
 */
public class UnitConverter {
    /** 
     * Maps unit categories to their conversion rates.
     * Each category (e.g., "length", "weight") maps to a set of unit conversion rates.
     */
    private final Map<String, Map<String, Double>> conversionRates;

    /**
     * Creates a new unit converter with predefined conversion rates.
     */
    public UnitConverter() {
        this.conversionRates = new HashMap<>();
        initializeConversionRates();
    }

    /**
     * Converts a value from one unit to another.
     * 
     * @param value The value to convert
     * @param from The source unit
     * @param to The target unit
     * @return The converted value
     * @throws CalculatorException if the conversion is not supported
     */
    public double convert(double value, String from, String to) {
        String category = findCategory(from, to);
        if (category == null) {
            throw new CalculatorException("Unsupported unit conversion: " + from + " to " + to);
        }

        if (category.equals("temperature")) {
            return convertTemperature(value, from.toLowerCase(), to.toLowerCase());
        }

        Map<String, Double> rates = conversionRates.get(category);
        Double fromRate = rates.get(from.toLowerCase());
        Double toRate = rates.get(to.toLowerCase());

        if (fromRate == null || toRate == null) {
            throw new CalculatorException("Unsupported unit conversion: " + from + " to " + to);
        }

        return value * fromRate / toRate;
    }

    /**
     * Determines the category of units being converted.
     * 
     * @param from The source unit
     * @param to The target unit
     * @return The category of units, or null if not found
     */
    private String findCategory(String from, String to) {
        for (Map.Entry<String, Map<String, Double>> entry : conversionRates.entrySet()) {
            Map<String, Double> rates = entry.getValue();
            if (rates.containsKey(from.toLowerCase()) && rates.containsKey(to.toLowerCase())) {
                return entry.getKey();
            }
        }
        return null;
    }

    /**
     * Converts a temperature value between Celsius and Fahrenheit.
     * 
     * @param value The temperature value to convert
     * @param from The source unit ("c" or "f")
     * @param to The target unit ("c" or "f")
     * @return The converted temperature
     */
    private double convertTemperature(double value, String from, String to) {
        if (from.equals(to)) {
            return value;
        }
        if (from.equals("c")) {
            return (value * 9/5) + 32;
        } else {
            return (value - 32) * 5/9;
        }
    }

    /**
     * Initializes the conversion rates for different unit categories.
     */
    private void initializeConversionRates() {
        // Length conversions (base unit: meters)
        Map<String, Double> lengthRates = new HashMap<>();
        lengthRates.put("m", 1.0);
        lengthRates.put("meters", 1.0);
        lengthRates.put("km", 1000.0);
        lengthRates.put("kilometers", 1000.0);
        lengthRates.put("ft", 0.3048);
        lengthRates.put("feet", 0.3048);
        lengthRates.put("in", 0.0254);
        lengthRates.put("inches", 0.0254);
        lengthRates.put("cm", 0.01);
        lengthRates.put("centimeters", 0.01);
        lengthRates.put("mm", 0.001);
        lengthRates.put("millimeters", 0.001);
        conversionRates.put("length", lengthRates);

        // Weight conversions (base unit: kilograms)
        Map<String, Double> weightRates = new HashMap<>();
        weightRates.put("kg", 1.0);
        weightRates.put("kilograms", 1.0);
        weightRates.put("lb", 0.453592);
        weightRates.put("pounds", 0.453592);
        weightRates.put("oz", 0.0283495);
        weightRates.put("ounces", 0.0283495);
        weightRates.put("g", 0.001);
        weightRates.put("grams", 0.001);
        weightRates.put("mg", 0.000001);
        weightRates.put("milligrams", 0.000001);
        conversionRates.put("weight", weightRates);

        // Temperature is handled separately in convertTemperature()
        Map<String, Double> tempRates = new HashMap<>();
        tempRates.put("c", 1.0);
        tempRates.put("celsius", 1.0);
        tempRates.put("f", 1.0);
        tempRates.put("fahrenheit", 1.0);
        conversionRates.put("temperature", tempRates);
    }
}

9. Memory.java

package calc;

/**
 * Manages the calculator's memory storage.
 * 
 * <p>This class provides functionality to store and manipulate a single value
 * in memory, supporting basic calculator memory operations like store, recall,
 * add, subtract, and clear.
 * 
 * <p>Example usage:
 * <pre>
 * Memory memory = new Memory();
 * memory.store(42.0);
 * double value = memory.recall(); // returns 42.0
 * memory.add(5.0); // memory now contains 47.0
 * memory.subtract(2.0); // memory now contains 45.0
 * memory.clear(); // removes the value
 * </pre>
 */
public class Memory {
    /** The current value stored in memory */
    private double value;

    /** Whether a value is currently stored */
    private boolean hasValue;

    /**
     * Creates a new memory with no stored value.
     */
    public Memory() {
        this.value = 0.0;
        this.hasValue = false;
    }

    /**
     * Stores a value in memory.
     * 
     * @param value The value to store
     */
    public void store(double value) {
        this.value = value;
        this.hasValue = true;
    }

    /**
     * Retrieves the value from memory.
     * 
     * @return The stored value (0.0 if memory is empty)
     */
    public double recall() {
        return value;
    }

    /**
     * Adds a value to the current memory value.
     * If memory is empty, stores the value.
     * 
     * @param value The value to add
     */
    public void add(double value) {
        if (!hasValue) {
            store(value);
        } else {
            this.value += value;
        }
    }

    /**
     * Subtracts a value from the current memory value.
     * If memory is empty, stores the negation of the value.
     * 
     * @param value The value to subtract
     */
    public void subtract(double value) {
        if (!hasValue) {
            store(-value);
        } else {
            this.value -= value;
        }
    }

    /**
     * Clears the memory, resetting it to 0.0.
     */
    public void clear() {
        this.value = 0.0;
        this.hasValue = false;
    }
}

10. History.java

package calc;

import java.util.ArrayList;
import java.util.List;

/**
 * Manages the calculation history of the calculator.
 * 
 * <p>This class maintains a list of calculation entries, each containing:
 * <ul>
 *   <li>The expression that was evaluated</li>
 *   <li>The result of the evaluation</li>
 *   <li>The timestamp of when the calculation was performed</li>
 * </ul>
 * 
 * <p>The history is limited to a maximum number of entries, with older entries
 * being removed when the limit is reached.
 * 
 * <p>Example usage:
 * <pre>
 * History history = new History(); // Keep last 100 calculations by default
 * // Or specify a custom size:
 * History history2 = new History(10); // Keep last 10 calculations
 * history.addEntry("2 + 2", 4.0);
 * List&lt;{@link HistoryEntry}&gt; entries = history.getEntries();
 * </pre>
 */
public class History {
    /** Default maximum number of entries to keep in history */
    private static final int DEFAULT_MAX_SIZE = 100;

    /** List of history entries, ordered from oldest to newest */
    private final List<HistoryEntry> entries;

    /** Maximum number of entries to keep in history */
    private final int maxSize;

    /**
     * Creates a new history with the default maximum number of entries (100).
     */
    public History() {
        this(DEFAULT_MAX_SIZE);
    }

    /**
     * Creates a new history with the specified maximum number of entries.
     * 
     * @param maxSize The maximum number of entries to keep in the history
     */
    public History(int maxSize) {
        this.entries = new ArrayList<>();
        this.maxSize = maxSize;
    }

    /**
     * Adds a new entry to the history.
     * 
     * <p>If the history is at its maximum size, the oldest entry is removed
     * before adding the new one.
     * 
     * @param expression The expression that was evaluated
     * @param result The result of the evaluation
     */
    public void addEntry(String expression, double result) {
        if (entries.size() >= maxSize) {
            entries.remove(0);
        }
        entries.add(new HistoryEntry(expression, result));
    }

    /**
     * Records an error that occurred during expression evaluation.
     * 
     * <p>Currently, error entries are not stored in the history.
     * This method is a placeholder for future error tracking functionality.
     * 
     * @param expression The expression that caused the error
     * @param error The error message
     */
    public void addError(String expression, String error) {
        // Skip error entries for now
    }

    /**
     * Returns all entries in the history.
     * 
     * @return A list of history entries, ordered from newest to oldest
     */
    public List<HistoryEntry> getEntries() {
        return new ArrayList<>(entries);
    }

    /**
     * Returns the most recent history entry.
     * 
     * @return The last entry in the history, or null if the history is empty
     */
    public HistoryEntry getLastEntry() {
        return entries.isEmpty() ? null : entries.get(entries.size() - 1);
    }

    /**
     * Clears all entries from the history.
     */
    public void clear() {
        entries.clear();
    }
}

11. HistoryEntry.java

package calc;

import java.time.LocalDateTime;

/**
 * Represents a single entry in the calculator's history.
 * 
 * <p>This class stores information about a calculation that was performed,
 * including:
 * <ul>
 *   <li>The expression that was evaluated</li>
 *   <li>The result of the evaluation</li>
 *   <li>The timestamp when the calculation was performed</li>
 * </ul>
 * 
 * <p>Example usage:
 * <pre>
 * HistoryEntry entry = new HistoryEntry("2 + 2", 4.0);
 * String expr = entry.getExpression(); // returns "2 + 2"
 * double result = entry.getResult(); // returns 4.0
 * LocalDateTime time = entry.getTimestamp(); // returns calculation time
 * </pre>
 */
public class HistoryEntry {
    /** The mathematical expression that was evaluated */
    private final String expression;

    /** The result of evaluating the expression */
    private final double result;

    /** The date and time when the evaluation occurred */
    private final LocalDateTime timestamp;

    /**
     * Creates a new history entry with the specified expression and result.
     * The timestamp is automatically set to the current date and time.
     * 
     * @param expression The expression that was evaluated
     * @param result The result of the evaluation
     */
    public HistoryEntry(String expression, double result) {
        this.expression = expression;
        this.result = result;
        this.timestamp = LocalDateTime.now();
    }

    /**
     * Returns the expression that was evaluated.
     * 
     * @return The mathematical expression
     */
    public String getExpression() {
        return expression;
    }

    /**
     * Returns the result of evaluating the expression.
     * 
     * @return The numerical result
     */
    public double getResult() {
        return result;
    }

    /**
     * Returns the timestamp when the calculation was performed.
     * 
     * @return The calculation timestamp
     */
    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    /**
     * Returns a string representation of the history entry.
     * 
     * <p>The format is: "timestamp: expression = result"
     * Example: "2025-03-30T00:37:18: 2 + 2 = 4.00"
     * 
     * @return A formatted string containing all entry information
     */
    @Override
    public String toString() {
        return String.format("%s: %s = %.2f", timestamp, expression, result);
    }
}

12. CalculatorException.java

package calc;

/**
 * Custom exception class for calculator-related errors.
 * 
 * <p>This exception is thrown when an error occurs during calculator operations,
 * such as division by zero, invalid expressions, or unsupported operations.
 * 
 * <p>Example usage:
 * <pre>
 * try {
 *     calculator.evaluate("1 / 0");
 * } catch (CalculatorException e) {
 *     System.err.println("Calculator error: " + e.getMessage());
 * }
 * </pre>
 */
public class CalculatorException extends RuntimeException {
    /**
     * Creates a new calculator exception with the specified message.
     * 
     * @param message The error message describing the exception
     */
    public CalculatorException(String message) {
        super(message);
    }

    /**
     * Creates a new calculator exception with the specified message and cause.
     * 
     * @param message The error message describing the exception
     * @param cause The underlying cause of the exception
     */
    public CalculatorException(String message, Throwable cause) {
        super(message, cause);
    }
}

Test Files

1. CalculatorTest.java

package calc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void testBasicArithmetic() {
        assertEquals(4.0, calculator.evaluate("2 + 2"));
        assertEquals(6.0, calculator.evaluate("3 * 2"));
        assertEquals(1.0, calculator.evaluate("5 - 4"));
        assertEquals(2.0, calculator.evaluate("6 / 3"));
    }

    @Test
    void testComplexExpressions() {
        assertEquals(18.0, calculator.evaluate("3 * (4 + 2)"));
        assertEquals(14.0, calculator.evaluate("2 + 3 * 4"));
        assertEquals(20.0, calculator.evaluate("(2 + 3) * 4"));
    }

    @Test
    void testConstants() {
        assertEquals(Math.PI, calculator.evaluate("π"));
        assertEquals(Math.E, calculator.evaluate("e"));
    }

    @Test
    void testMemoryOperations() {
        calculator.memoryStore(5.0);
        assertEquals(5.0, calculator.memoryRecall());

        calculator.memoryAdd(3.0);
        assertEquals(8.0, calculator.memoryRecall());

        calculator.memorySubtract(2.0);
        assertEquals(6.0, calculator.memoryRecall());

        calculator.memoryClear();
        assertEquals(0.0, calculator.memoryRecall());
    }

    @Test
    void testUnitConversions() {
        assertEquals(1000.0, calculator.convert(1.0, "km", "m"));
        assertEquals(0.001, calculator.convert(1.0, "g", "kg"));
        assertEquals(32.0, calculator.convert(0.0, "C", "F"));
        assertEquals(0.0, calculator.convert(32.0, "F", "C"));
    }

    @Test
    void testHistory() {
        calculator.evaluate("2 + 2");
        calculator.evaluate("3 * 4");

        var history = calculator.getHistory();
        assertEquals(2, history.size());
        assertEquals("2 + 2", history.get(0).getExpression());
        assertEquals("3 * 4", history.get(1).getExpression());
    }

    @Test
    void testInvalidExpressions() {
        assertThrows(CalculatorException.class, () -> calculator.evaluate("2 +"));
        assertThrows(CalculatorException.class, () -> calculator.evaluate("(2 + 3"));
        assertThrows(CalculatorException.class, () -> calculator.evaluate("2 / 0"));
    }

    @Test
    void testInvalidUnitConversion() {
        assertThrows(CalculatorException.class, 
            () -> calculator.convert(1.0, "invalid", "m"));
        assertThrows(CalculatorException.class, 
            () -> calculator.convert(1.0, "m", "invalid"));
    }
}

2. ExpressionParserTest.java

package calc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ExpressionParserTest {
    private ExpressionParser parser;

    @BeforeEach
    void setUp() {
        parser = new ExpressionParser();
    }

    @Test
    void testSimpleExpressions() {
        assertEquals(4.0, parser.parse("2 + 2").evaluate());
        assertEquals(6.0, parser.parse("3 * 2").evaluate());
        assertEquals(1.0, parser.parse("5 - 4").evaluate());
        assertEquals(2.0, parser.parse("6 / 3").evaluate());
    }

    @Test
    void testComplexExpressions() {
        assertEquals(18.0, parser.parse("3 * (4 + 2)").evaluate());
        assertEquals(14.0, parser.parse("2 + 3 * 4").evaluate());
        assertEquals(20.0, parser.parse("(2 + 3) * 4").evaluate());
    }

    @Test
    void testConstants() {
        assertEquals(Math.PI, parser.parse("π").evaluate());
        assertEquals(Math.E, parser.parse("e").evaluate());
    }

    @Test
    void testWhitespaceHandling() {
        assertEquals(4.0, parser.parse("2  +  2").evaluate());
        assertEquals(6.0, parser.parse("3  *  2").evaluate());
    }

    @Test
    void testInvalidExpressions() {
        assertThrows(CalculatorException.class, () -> parser.parse("2 +"));
        assertThrows(CalculatorException.class, () -> parser.parse("(2 + 3"));
        assertThrows(CalculatorException.class, () -> parser.parse("2 + * 3"));
        assertThrows(CalculatorException.class, () -> parser.parse("2 + 3)"));
    }

    @Test
    void testDecimalNumbers() {
        assertEquals(3.5, parser.parse("1.5 + 2").evaluate());
        assertEquals(0.5, parser.parse("1 / 2").evaluate());
    }

    @Test
    void testOperatorPrecedence() {
        assertEquals(14.0, parser.parse("2 + 3 * 4").evaluate());
        assertEquals(20.0, parser.parse("(2 + 3) * 4").evaluate());
    }

    @Test
    void testLeftToRightEvaluation() {
        assertEquals(4.5, parser.parse("3-1+2.5").evaluate());
    }

    @Test
    void testComprehensiveArithmetic() {
        // Basic arithmetic
        assertEquals(5.0, parser.parse("2 + 3").evaluate());
        assertEquals(10.0, parser.parse("15 - 5").evaluate());
        assertEquals(24.0, parser.parse("8 * 3").evaluate());
        assertEquals(4.0, parser.parse("12 / 3").evaluate());

        // Multiple operations with same precedence
        assertEquals(9.0, parser.parse("3 + 4 + 2").evaluate());
        assertEquals(1.0, parser.parse("10 - 6 - 3").evaluate());
        assertEquals(24.0, parser.parse("2 * 3 * 4").evaluate());
        assertEquals(2.0, parser.parse("12 / 2 / 3").evaluate());

        // Mixed operations
        assertEquals(11.0, parser.parse("3 + 4 * 2").evaluate());
        assertEquals(14.0, parser.parse("2 * 5 + 4").evaluate());
        assertEquals(7.0, parser.parse("15 - 4 * 2").evaluate());
        assertEquals(8.0, parser.parse("4 * 3 - 4").evaluate());

        // Decimal numbers
        assertEquals(3.5, parser.parse("1.5 + 2").evaluate());
        assertEquals(2.5, parser.parse("5.5 - 3").evaluate());
        assertEquals(4.2, parser.parse("2.1 * 2").evaluate());
        assertEquals(2.5, parser.parse("5 / 2").evaluate());

        // Complex expressions with parentheses
        assertEquals(25.0, parser.parse("(2 + 3) * 5").evaluate());
        assertEquals(13.0, parser.parse("2 * (4 + 2.5)").evaluate());
        assertEquals(6.5, parser.parse("(15 - 4) / 2 + 1").evaluate());
        assertEquals(21.0, parser.parse("3 * (5 + 2)").evaluate());

        // Multiple parentheses
        assertEquals(30.0, parser.parse("(2 + 3) * (4 + 2)").evaluate());
        assertEquals(13.0, parser.parse("((2 + 3) * 2) + 3").evaluate());
        assertEquals(25.0, parser.parse("(5 * (3 + 2))").evaluate());
        assertEquals(21.0, parser.parse("3 * (2 + (4 + 1))").evaluate());

        // Long expressions
        assertEquals(15.0, parser.parse("2 + 3 * 4 + 1").evaluate());
        assertEquals(26.0, parser.parse("2 * 3 + 4 * 5").evaluate());
        assertEquals(11.0, parser.parse("10 - 2 + 4 - 1").evaluate());
        assertEquals(8.0, parser.parse("2 * 3 + 4 / 2").evaluate());

        // Mixed decimals and whole numbers
        assertEquals(7.8, parser.parse("2.3 + 5.5").evaluate());
        assertEquals(3.7, parser.parse("5.2 - 1.5").evaluate());
        assertEquals(7.5, parser.parse("2.5 * 3").evaluate());
        assertEquals(2.5, parser.parse("7.5 / 3").evaluate());

        // Complex decimal calculations
        assertEquals(15.125, parser.parse("2.5 * 3.25 + 7").evaluate(), 0.000000000000001);
        assertEquals(18.6875, parser.parse("(2.25 + 3.5) * 3.25").evaluate(), 0.000000000000001);
        assertEquals(12.925, parser.parse("2.5 * (3.75 + 1.42)").evaluate(), 0.000000000000001);
        assertEquals(9.898477157360405, parser.parse("(7.25 + 2.5) / 0.985").evaluate(), 0.000000000000001);

        // Multiple operations with mixed numbers
        assertEquals(23.0, parser.parse("2.5 * 3 + 4.5 * 3 + 2").evaluate(), 0.000000000000001);
        assertEquals(19.625, parser.parse("3.25 * (2 + 4) + 0.125").evaluate(), 0.000000000000001);
        assertEquals(30.5, parser.parse("(3.25 * 4) + (2.5 * 7)").evaluate(), 0.000000000000001);
        assertEquals(14.5125, parser.parse("2.25 * (3 + 4.25) - 1.8").evaluate(), 0.000000000000001);

        // Complex expressions with multiple operations
        assertEquals(40.0, parser.parse("2.5 * (3 + 4) * 2 + 5").evaluate(), 0.000000000000001);
        assertEquals(40.3125, parser.parse("(2.25 * 3 + 4) * 3.75").evaluate(), 0.000000000000001);
        assertEquals(22.875, parser.parse("3.25 * (2 + 4.5) + 1.75").evaluate(), 0.000000000000001);
        assertEquals(16.725, parser.parse("(3.5 + 2.25) * 2.7 + 1.2").evaluate(), 0.000000000000001);

        // Mixed complex expressions
        assertEquals(86.25, parser.parse("(2.5 * 3 + 4) * (5 + 2.5)").evaluate(), 0.000000000000001);
        assertEquals(44.125, parser.parse("3.25 * (4 + 2.5) * 2 + 1.875").evaluate(), 0.000000000000001);
        assertEquals(38.75, parser.parse("(5 * 2.5 + 3) * (2 + 0.5)").evaluate(), 0.000000000000001);
        assertEquals(116.25, parser.parse("(3.25 * 4 + 2.5) * (5 + 2.5)").evaluate(), 0.000000000000001);
    }

    @Test
    void testNegativeNumbersAndPrecedence() {
        assertEquals(7.0, parser.parse("3-4*-1").evaluate());  // 3 - (4 * -1) = 3 - (-4) = 7
        assertEquals(-1.0, parser.parse("-1").evaluate());
        assertEquals(-6.0, parser.parse("-2*3").evaluate());
        assertEquals(-6.0, parser.parse("2*-3").evaluate());
        assertEquals(6.0, parser.parse("-2*-3").evaluate());
        assertEquals(-5.0, parser.parse("-2-3").evaluate());
        assertEquals(1.0, parser.parse("-2+3").evaluate());
    }

    @Test
    void testExtendedNegativeNumbersAndPrecedence() {
        // Basic negative number operations
        assertEquals(-12.0, parser.parse("-3 * 4").evaluate());
        assertEquals(-12.0, parser.parse("3 * -4").evaluate());
        assertEquals(12.0, parser.parse("-3 * -4").evaluate());
        assertEquals(1.0, parser.parse("-3 + 4").evaluate());
        assertEquals(-1.0, parser.parse("3 + -4").evaluate());
        assertEquals(-7.0, parser.parse("-3 + -4").evaluate());
        assertEquals(-7.0, parser.parse("-3 - 4").evaluate());
        assertEquals(7.0, parser.parse("3 - -4").evaluate());
        assertEquals(1.0, parser.parse("-3 - -4").evaluate());

        // Division with negative numbers
        assertEquals(-0.75, parser.parse("-3 / 4").evaluate());
        assertEquals(-0.75, parser.parse("3 / -4").evaluate());
        assertEquals(0.75, parser.parse("-3 / -4").evaluate());

        // Complex expressions with negative numbers
        assertEquals(26.0, parser.parse("2 * 3 + 4 * 5").evaluate());
        assertEquals(19.0, parser.parse("2 + 3 * 4 + 5").evaluate());
        assertEquals(45.0, parser.parse("(2 + 3) * (4 + 5)").evaluate());
        assertEquals(70.0, parser.parse("2 * (3 + 4) * 5").evaluate());
        assertEquals(-70.0, parser.parse("-2 * (3 + 4) * 5").evaluate());
        assertEquals(10.0, parser.parse("2 * (-3 + 4) * 5").evaluate());
        assertEquals(-10.0, parser.parse("2 * (3 + -4) * 5").evaluate());
        assertEquals(-70.0, parser.parse("2 * (3 + 4) * -5").evaluate());
        assertEquals(-70.0, parser.parse("-2 * (-3 + -4) * -5").evaluate());

        // Decimal numbers with negative operations
        assertEquals(20.0, parser.parse("2.5 * 3.5 + 4.5 * 2.5").evaluate());
        assertEquals(2.5, parser.parse("-2.5 * 3.5 + 4.5 * 2.5").evaluate());
        assertEquals(2.5, parser.parse("2.5 * -3.5 + 4.5 * 2.5").evaluate());
        assertEquals(-2.5, parser.parse("2.5 * 3.5 - 4.5 * 2.5").evaluate());
        assertEquals(20.0, parser.parse("-2.5 * -3.5 - 4.5 * -2.5").evaluate());

        // Complex decimal expressions with negative numbers
        assertEquals(12.0, parser.parse("(2.5 + 3.5) * (4.5 - 2.5)").evaluate());
        assertEquals(2.0, parser.parse("(-2.5 + 3.5) * (4.5 - 2.5)").evaluate());
        assertEquals(-7.0, parser.parse("(2.5 - 3.5) * (4.5 + 2.5)").evaluate());
        assertEquals(12.0, parser.parse("(-2.5 - 3.5) * (-4.5 + 2.5)").evaluate());
    }
}

3. UnitConverterTest.java

package calc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UnitConverterTest {
    private UnitConverter converter;
    private static final double DELTA = 1e-10; // For floating-point comparisons

    @BeforeEach
    void setUp() {
        converter = new UnitConverter();
    }

    @Test
    void testLengthConversions() {
        assertEquals(1000.0, converter.convert(1.0, "km", "m"), DELTA);
        assertEquals(0.001, converter.convert(1.0, "m", "km"), DELTA);
        assertEquals(100.0, converter.convert(1.0, "m", "cm"), DELTA);
        assertEquals(0.01, converter.convert(1.0, "cm", "m"), DELTA);
    }

    @Test
    void testWeightConversions() {
        assertEquals(1000.0, converter.convert(1.0, "kg", "g"), DELTA);
        assertEquals(0.001, converter.convert(1.0, "g", "kg"), DELTA);
        assertEquals(1000.0, converter.convert(1.0, "g", "mg"), DELTA);
        assertEquals(0.001, converter.convert(1.0, "mg", "g"), DELTA);
    }

    @Test
    void testTemperatureConversions() {
        assertEquals(32.0, converter.convert(0.0, "C", "F"), DELTA);
        assertEquals(0.0, converter.convert(32.0, "F", "C"), DELTA);
        assertEquals(212.0, converter.convert(100.0, "C", "F"), DELTA);
        assertEquals(100.0, converter.convert(212.0, "F", "C"), DELTA);
    }

    @Test
    void testSameUnitConversion() {
        assertEquals(1.0, converter.convert(1.0, "m", "m"), DELTA);
        assertEquals(1.0, converter.convert(1.0, "kg", "kg"), DELTA);
        assertEquals(1.0, converter.convert(1.0, "C", "C"), DELTA);
    }

    @Test
    void testInvalidConversions() {
        assertThrows(CalculatorException.class, 
            () -> converter.convert(1.0, "invalid", "m"));
        assertThrows(CalculatorException.class, 
            () -> converter.convert(1.0, "m", "invalid"));
        assertThrows(CalculatorException.class, 
            () -> converter.convert(1.0, "m", "kg")); // Different categories
    }

    @Test
    void testZeroValues() {
        assertEquals(0.0, converter.convert(0.0, "km", "m"), DELTA);
        assertEquals(0.0, converter.convert(0.0, "kg", "g"), DELTA);
        assertEquals(32.0, converter.convert(0.0, "C", "F"), DELTA);
    }
}

Notes:

In future similar projects, it is essential to prioritize correctness, usability, and maintainability through sound software engineering practices. As demonstrated in this calculator, a key area to invest in is robust expression parsing. Ensure that your parser supports standard infix notation with correct operator precedence and associativity. Handle parentheses thoroughly, account for constants like π and e, and support unary operations such as negative numbers. Validating expression structure—including operand count, operator position, and illegal characters—is crucial for preventing subtle bugs and user confusion.

You should also plan for flexible and resilient memory management. Implement memory features that go beyond a basic recall, including addition, subtraction, storage, and clearing. Ensure the memory state transitions are well-defined even in edge cases (e.g., recalling before storing). Encapsulate memory logic in a dedicated, extensible class, making it easier to introduce features such as persistent memory, multi-slot support, or thread-safe access in concurrent environments.

Unit conversion is another feature area where future systems should maintain semantic correctness and ease of use. Support a wide range of unit types within clearly defined categories (length, weight, temperature), and ensure conversions are only allowed between compatible units. Case-insensitive matching and support for both full and abbreviated unit names can greatly improve user experience. When dealing with nonlinear conversions, such as temperature, incorporate special-case logic and avoid assuming linearity across all domains.

To aid user interaction and post-analysis, incorporate detailed history tracking mechanisms. Logging past inputs, results, and timestamps not only improves transparency but also facilitates debugging and future enhancements like command recall or session exports. Limit the history size to ensure performance and memory usage remain stable over time. Consider including error events in future logs to provide more complete diagnostic context.

Effective error handling must be a first-class concern in any project. Define custom exception types to clearly distinguish between user input errors and system-level failures. Communicate errors clearly through the user interface, while ensuring exceptions are caught and propagated responsibly. Future systems should also consider integrating error logging into history or telemetry systems for deeper observability.

Follow object-oriented design principles to structure your application for flexibility and clarity. Use inheritance and polymorphism where appropriate, especially when modeling hierarchical data such as mathematical expressions. Strive for clean separation of concerns, ensuring that each class has a single, focused responsibility. Immutable data types, such as history entries, can significantly reduce the risk of unintended side effects and make concurrent programs safer and more predictable.

Maintain high standards in code organization and documentation. Group related functionality into packages, use consistent and meaningful naming conventions, and document all public classes and methods with comprehensive JavaDoc. This not only helps during development but also ensures that future contributors (including your future self) can understand, extend, and maintain the codebase efficiently.

Lastly, design with testability in mind. Structure your application in a modular fashion with minimal coupling and clear interfaces, which allows you to test components in isolation. Use a unit testing framework such as JUnit to cover edge cases, validate correctness, and detect regressions early. Identify common pitfalls—such as invalid syntax, unit mismatches, or incorrect memory state—and write tests that explicitly guard against them. A well-tested codebase forms the foundation of robust and dependable software.