Project: build a calculator and unit converter
Create a small console calculator and unit converter with validated input, focused functions, error handling, and a repeatable build.
Prerequisites and project outcomes
Know functions, conditions, loops, strings, numeric types, and basic input validation. You will build a console program that adds, subtracts, multiplies, divides, and converts Celsius to Fahrenheit and kilometres to miles.
Requirements
- Read a command and the required numeric operands.
- Reject malformed numbers and trailing characters.
- Reject division by zero.
- Keep calculation logic separate from menu interaction.
- Explain floating-point limitations and test boundary cases.
Prerequisites: functions, arithmetic, input validation, and basic exception handling. Review Functions, Explicit conversions, Input validation, and Exceptions first. Unit testing and CMake are optional extensions covered later in the project module.
Complete single-file implementation (C++17)
#include <iostream>
#include <string>
#include <stdexcept>
#include <cmath>
bool parse_number(const std::string& text, double& value) {
try {
std::size_t used = 0;
value = std::stod(text, &used);
return used == text.size() && std::isfinite(value);
} catch (const std::exception&) { return false; }
}
double calculate(char op, double a, double b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if (b == 0.0) throw std::domain_error("division by zero");
return a / b;
default: throw std::invalid_argument("unknown operator");
}
}
double celsius_to_fahrenheit(double c) { return c * 9.0 / 5.0 + 32.0; }
double kilometres_to_miles(double km) { return km * 0.621371192237334; }
int main() {
for (;;) {
std::cout << "\n1 Calculate 2 C-to-F 3 km-to-mi 0 Exit\nChoice: ";
std::string line;
if (!std::getline(std::cin, line)) break;
if (line == "0") break;
if (line == "1") {
std::string op_text, left, right;
std::cout << "Operator (+ - * /): "; if (!std::getline(std::cin, op_text)) break;
std::cout << "First number: "; if (!std::getline(std::cin, left)) break;
std::cout << "Second number: "; if (!std::getline(std::cin, right)) break;
double a{}, b{};
if (op_text.size() != 1 || !parse_number(left, a) || !parse_number(right, b)) {
std::cout << "Invalid input.\n"; continue;
}
try { std::cout << "Result: " << calculate(op_text[0], a, b) << '\n'; }
catch (const std::exception& e) { std::cout << e.what() << '\n'; }
} else if (line == "2" || line == "3") {
std::string raw; std::cout << "Value: "; if (!std::getline(std::cin, raw)) break;
double value{};
if (!parse_number(raw, value)) { std::cout << "Invalid number.\n"; continue; }
if (line == "2") std::cout << celsius_to_fahrenheit(value) << " F\n";
else std::cout << kilometres_to_miles(value) << " miles\n";
} else std::cout << "Unknown choice.\n";
}
}
How the design works
parse_numberchecks that the entire string was consumed and rejects non-finite values.calculateowns arithmetic rules and reports invalid operations with exceptions.- Conversion functions are small pure functions that can be tested independently.
- The menu handles input/output only; it does not duplicate the calculation formulas.
Build and run
g++ -std=c++17 -Wall -Wextra -Wpedantic calculator.cpp -o calculator ./calculator
On Windows, run calculator.exe. A CMake build can be added using the same pattern shown in the library-catalogue capstone.
Test plan
- Calculate 7 + 5, 7 / 2, and 7 / 0.
- Try malformed values such as
abc,12cats, and an empty line. - Convert 0°C to 32°F and 100°C to 212°F.
- Convert 1 km to approximately 0.621371 miles.
- Try very large magnitudes and check whether the result remains finite.
- Send EOF at each input prompt and confirm the program exits cleanly.
Limitations and safety
This is a learning calculator, not a financial or scientific calculator. Binary floating-point represents many decimal fractions approximately. Floating-point overflow can produce infinity during arithmetic even when inputs were finite, so a more robust version should check results with std::isfinite. Unit conversion constants are approximations; state the precision expected by the application.
Practice extensions
- Add pounds-to-kilograms and metres-to-feet conversions.
- Write tests for
calculatewithout using terminal input. - Reject values outside a domain-specific range where appropriate.
- Move declarations and implementations into separate files and add CTest.
Practice extension solutions
Show implementation guidance
- Add pure conversion functions:
double pounds_to_kg(double pounds) { return pounds * 0.45359237; }anddouble metres_to_feet(double metres) { return metres * 3.280839895; }. Label the output units and document the precision of the constants. - Test
calculatedirectly: ordinary addition/subtraction/multiplication/division, negative operands, fractional results, division by zero (expectstd::domain_error), and an unknown operator (expectstd::invalid_argument). Use a tolerance for floating-point comparisons instead of exact equality for non-integer results. - Validate the domain before calculation. For example, a conversion that accepts only non-negative physical quantities should reject negative input with a clear message; use
std::isfiniteto reject NaN and infinity. The valid range depends on the application and must be documented rather than guessed. - Move declarations into a header, definitions into a source file, and keep
mainfor menu interaction. Add the files to a CMake target and register a separate test executable with CTest, following the build and test lessons.
Summary
This project practices input parsing, separation of concerns, pure functions, exceptions, and boundary testing. The next step is to study modern C++23 library additions and their compiler-support requirements.