I am self-learning C++ and this was my first smallish project. The goal was to create a working calculator that runs in the terminal, saves history of previous calculations, and has the ability to enter commands to display history, re-display commands, and quit. The display history/command commands need to work at any point while entering values for a calculation without disrupting the calculation, and the quit command needs to end the whole program right after it is pressed. I wanted proper exception handling for incorrect input and (lower vs uppercase) command characters. Additionally, I had the goals of wanting the terminal UI to be easy-to-use and readable, and to use good naming conventions in my code.
Some questions I have:
- Is there a better/cleaner way to [Q]uit without having multiple if-statements to check the s_run flag?
- Is there a better/cleaner way to check if cin input was a command or double in the setNum method?
- I made the class a singleton since I only need one instance of the calculator class. Was this a good choice, and did I implement this right? First time creating a singleton.
Additional notes:
- I may add more features to the calculator, which is why I have the displayCommand command even though there are only 3 commands currently.
- I made this project with the idea of potentially making a small text-based adventure game in the terminal and wanted a way to use a calculator in-game without leaving the terminal.
Calculator.hpp:
#pragma once
#include <iostream> //std::cout, std::cin
#include <iomanip> //std::setprecision
#include <vector> //std::vector, std::vector::push_back
#include <cctype> //std::toupper
#include <string> //std::string, std::string::strtod(), for setNum input commands
struct Calculation
{
double n1;
char op;
double n2;
double ans;
};
class Calculator
{
public:
Calculator(const Calculator&) = delete; //Delete the copy constructor so that multiple instances of the singleton are not created
static Calculator& get(); //get function used to get single Singleton instance of Calculator class
static void calculate() {return get().i_calculate();} //Main member function, calls internal calculate function on the singleton instance
private:
Calculation m_calc; //Struct of current calculation, holding n1, op, and n2 values for calculating + ans for answer value
std::vector<Calculation> m_history; //vector of calculation history
inline static bool s_run; //static flag to keep calculator while-loop running until [Q]uit
static Calculator s_Instance; //Singleton instance used for Calculator
private:
Calculator() : m_calc({0,'+',0,0}) {}; //Default constructor, should not be called since Calculator is a Singleton, initialize m_calc Calculation struct
void i_calculate(); //Internal: Main member function that runs until Calculator object is destroyed
void setAll(); //sets n1, op, and n2 by calling setNum and setOp functions
void setNum(double&); //sets Calculation n1 or n2 values, only excepts numbers
void setOp(char&); //sets Calculation operator value, only excepts ('+' || '-' || '*' || '/')
bool is_valid_command(const char&); //returns true if char is a command character: [Q]uit, [H]istory, or [C]ommands
bool is_valid_op(const char&); //returns true if char is an calculator operator ('+' || '-' || '*' || '/')
void command(const char&); //Implements command corresponding to char parameter: [Q]uit, [H]istory, or [C]ommands
void displayOperators() const; //Displays operators to be used in calculator
void displayAnswer() const; //Displays "n1 op n2 =" and the answer of the calculation
void displayHistory() const; //Displays previous calculations
void displayLine() const; //prints a long "------" line to terminal, used for visual visibility
void displayInfo() const; //prints info about how calculator works
void displayCommands() const; //prints list of commands
void startUp(); //reset m_run flag to true, display calculator info
void shutDown(); //clear m_history and terminal
};
Calculator.cpp:
//Basic calculator in C++ that can perform addition, subtraction, multiplication, and division
#include "Calculator.hpp" //Calculator class, Calculation struct
Calculator& Calculator::get()
{
static Calculator s_Instance;
return s_Instance;
}
void Calculator::i_calculate()
{
startUp();
while(s_run == true) //run calculations until [Q]uit is called
{
setAll(); //prompt user for n1, op, and n2 values
switch(m_calc.op)
{
case '+':
m_calc.ans = m_calc.n1 + m_calc.n2;
break;
case '-':
m_calc.ans = m_calc.n1 - m_calc.n2;
break;
case '*':
m_calc.ans = m_calc.n1 * m_calc.n2;
break;
case '/':
if(m_calc.n2 != 0)
m_calc.ans = m_calc.n1 / m_calc.n2;
else m_calc.ans = 0;
break;
}
if(s_run == true)
{
displayAnswer(); //display calc + ans on terminal
m_history.push_back(m_calc); //add calculation to history
}
}
shutDown();
}
void Calculator::setAll()
{
std::cout << "Enter first number: ";
setNum(m_calc.n1);
if(s_run == true)
{
displayOperators();
std::cout << "Enter operation: ";
setOp(m_calc.op);
}
if(s_run == true)
{
std::cout << "Enter second number: ";
setNum(m_calc.n2);
}
}
void Calculator::setNum(double& n)
{
std::string input; //string for testing if input is a double or char
char* c_ptr = nullptr; //char pointer, will point to first char that cannot be converted to a double
do
{
std::cin >> input; //get user input as a string
double d_test = strtod(input.c_str(), &c_ptr); //test if string is a double by using strtod. If double, c_ptr == nullptr
if(*c_ptr == 0) //if a double
{
n = d_test; //set n to double
break; //break do-while loop
}
else if(input.length() == 1) //if string is only a char
{
if(is_valid_command(*c_ptr)) //if the char is a command
{
command(*c_ptr); //implement command
if(s_run == false) //if command was [Q]uit
break; //break do-while loop
}
}
else //Invalid input: not a double or a command char
{
std::cout << "Error: not a number" << std::endl;
std::cin.clear(); //clears the error flag on cin
std::cin.ignore(256, '\n'); //skip to the next new line
}
std::cout << "Enter number: "; //ask for new number
} while (true);
}
void Calculator::setOp(char& ch)
{
do
{
std::cin >> ch;
if(is_valid_command(ch)) //check if char is a command
command(ch); //if a command, implement command
else if(!(is_valid_op(ch))) //check if ch is an operator char (+,-,*,/)
{
std::cout << "Error: not an operator" << std::endl;
std::cin.clear(); //clears the error flag on cin
std::cin.ignore(256, '\n'); //skip to the next new line
}
else break; //end do-while loop once a valid char has been entered
std::cout << "Enter operation: "; //ask for new operator
} while (true);
}
bool Calculator::is_valid_command(const char& ch)
{
return (std::toupper(ch) == 'Q')
|| (std::toupper(ch) == 'H')
|| (std::toupper(ch) == 'C');
}
bool Calculator::is_valid_op(const char& ch)
{
return ch == '+'
|| ch == '-'
|| ch == '*'
|| ch == '/';
}
void Calculator::command(const char& ch)
{
if(std::toupper(ch) == 'Q') //if Q was entered, shut down calculator
{
s_run = false; //set run flag to false for calculate do-while-loop
}
else if(std::toupper(ch) == 'H') //if H was entered, display calculation history
displayHistory();
else if(std::toupper(ch) == 'C') //if C was entered, display commands list
displayCommands();
}
void Calculator::displayOperators() const
{
std::cout << "Addition: + | Subtraction: - | Multiplication: * | Division: /" << std::endl;
}
void Calculator::displayAnswer() const
{
system("clear"); //clear the terminal before displaying answer
displayLine();
std::cout << m_calc.n1 << " " << m_calc.op << " " << m_calc.n2 << " =" << std::endl;
std::cout << m_calc.ans << std::endl;
displayLine();
}
void Calculator::displayHistory() const
{
displayLine();
if(m_history.empty())
std::cout << "No calculations have been done yet." << std::endl;
else
{
for (const Calculation& struct_i : m_history) //ranged-based for-loop, iterate over each Calculation struct
{
std::cout << struct_i.n1 << " " << struct_i.op << " " << struct_i.n2 << " =" << std::endl;
std::cout << struct_i.ans << std::endl;
displayLine();
}
}
}
void Calculator::displayLine() const
{
std::cout << "--------------------------------------------" << std::endl;
}
void Calculator::displayInfo() const
{
std::cout << "Terminal controllable calculator that does (+,-,*,/) operations and holds a history of calculations" << std::endl;
}
void Calculator::displayCommands() const
{
std::cout << "Commands: [Q]uit, [H]istory, [C]ommands" << std::endl;
}
void Calculator::startUp()
{
std::cout << "Calculator starting up..." << std::endl;
displayInfo();
displayCommands();
s_run = true; //reset s_run if reusing calculator object
std::cout.precision(16); //set precision to 16 decimal places max
}
void Calculator::shutDown()
{
m_history.clear(); //clear Calculation vector history
system("clear"); //clear terminal
std::cout << "Calculator shutting down..." << std::endl;
}
```