Initial commit: Scientific Calculator with build + CLI

This commit is contained in:
Mo@Cipher 2025-11-10 09:19:56 +01:00
parent 3cb779d873
commit 5dabd51653
10 changed files with 738 additions and 0 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# Build artifacts
obj/
calculator
# macOS
.DS_Store
# Editor
.vscode/
.idea/

41
Makefile Normal file
View file

@ -0,0 +1,41 @@
# Makefile for Scientific Calculator
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
TARGET = calculator
SRCDIR = src
OBJDIR = obj
# Source files
SOURCES = $(wildcard $(SRCDIR)/*.cpp)
OBJECTS = $(SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o)
# Default target
all: $(TARGET)
# Create object directory if it doesn't exist
$(OBJDIR):
mkdir -p $(OBJDIR)
# Link object files to create executable
$(TARGET): $(OBJECTS) | $(OBJDIR)
$(CXX) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
@echo "Build complete: $(TARGET)"
# Compile source files to object files
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp | $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean build artifacts
clean:
rm -rf $(OBJDIR) $(TARGET)
@echo "Clean complete"
# Run the calculator in interactive mode
run: $(TARGET)
./$(TARGET)
# Phony targets
.PHONY: all clean run

21
src/Calculator.cpp Normal file
View file

@ -0,0 +1,21 @@
// Calculator.cpp
#include "Calculator.h"
namespace sci_calc {
Calculator::Calculator() : parser_(), evaluator_() {
}
double Calculator::calculate(const std::string &expression) {
if (expression.empty()) {
throw InvalidInputException("Empty expression");
}
std::vector<Token> postfixTokens = parser_.parseToPostfix(expression);
return evaluator_.evaluate(postfixTokens);
}
} // namespace sci_calc

27
src/Calculator.h Normal file
View file

@ -0,0 +1,27 @@
// Calculator.h
// Main calculator class that coordinates parsing and evaluation
#pragma once
#include "ExpressionParser.h"
#include "ExpressionEvaluator.h"
#include "Exceptions.h"
#include <string>
namespace sci_calc {
class Calculator {
public:
Calculator();
double calculate(const std::string &expression);
private:
ExpressionParser parser_;
ExpressionEvaluator evaluator_;
};
} // namespace sci_calc

31
src/Exceptions.h Normal file
View file

@ -0,0 +1,31 @@
// Exceptions.h
// Scientific Calculator custom exception hierarchy
#pragma once
#include <stdexcept>
#include <string>
namespace sci_calc {
class CalculatorException : public std::runtime_error {
public:
explicit CalculatorException(const std::string &message)
: std::runtime_error(message) {}
};
class InvalidInputException : public CalculatorException {
public:
explicit InvalidInputException(const std::string &message)
: CalculatorException(message) {}
};
class MathDomainException : public CalculatorException {
public:
explicit MathDomainException(const std::string &message)
: CalculatorException(message) {}
};
} // namespace sci_calc

158
src/ExpressionEvaluator.cpp Normal file
View file

@ -0,0 +1,158 @@
// ExpressionEvaluator.cpp
#include "ExpressionEvaluator.h"
#include <stdexcept>
namespace sci_calc {
double ExpressionEvaluator::evaluate(const std::vector<Token> &postfixTokens) const {
std::stack<double> stack;
for (const auto &token : postfixTokens) {
switch (token.type) {
case TokenType::Number:
case TokenType::Constant:
stack.push(token.value);
break;
case TokenType::Operator: {
if (stack.size() < 1) {
throw InvalidInputException("Insufficient operands for operator: " + token.text);
}
if (token.text == "neg" || token.text == "pos") {
double a = stack.top();
stack.pop();
stack.push(applyUnaryOperator(token.text, a));
} else {
if (stack.size() < 2) {
throw InvalidInputException("Insufficient operands for binary operator: " + token.text);
}
double b = stack.top();
stack.pop();
double a = stack.top();
stack.pop();
stack.push(applyOperator(token.text, a, b));
}
break;
}
case TokenType::Function: {
if (stack.empty()) {
throw InvalidInputException("Insufficient operands for function: " + token.text);
}
double arg = stack.top();
stack.pop();
stack.push(applyFunction(token.text, arg));
break;
}
default:
throw InvalidInputException("Unexpected token type in postfix expression");
}
}
if (stack.size() != 1) {
throw InvalidInputException("Invalid expression: stack contains " +
std::to_string(stack.size()) + " values");
}
return stack.top();
}
double ExpressionEvaluator::applyOperator(const std::string &op, double a, double b) const {
if (op == "+") {
return a + b;
} else if (op == "-") {
return a - b;
} else if (op == "*") {
return a * b;
} else if (op == "/") {
if (std::abs(b) < 1e-10) {
throw MathDomainException("Division by zero");
}
return a / b;
} else if (op == "%") {
if (std::abs(b) < 1e-10) {
throw MathDomainException("Modulo by zero");
}
return std::fmod(a, b);
} else if (op == "^") {
if (a < 0 && std::fmod(b, 1.0) != 0.0) {
throw MathDomainException("Cannot raise negative number to non-integer power");
}
double result = std::pow(a, b);
if (std::isnan(result) || std::isinf(result)) {
throw MathDomainException("Power operation resulted in invalid value");
}
return result;
} else {
throw InvalidInputException("Unknown operator: " + op);
}
}
double ExpressionEvaluator::applyUnaryOperator(const std::string &op, double a) const {
if (op == "neg") {
return -a;
} else if (op == "pos") {
return a;
} else {
throw InvalidInputException("Unknown unary operator: " + op);
}
}
double ExpressionEvaluator::applyFunction(const std::string &func, double arg) const {
if (func == "sin") {
return std::sin(arg);
} else if (func == "cos") {
return std::cos(arg);
} else if (func == "tan") {
double result = std::tan(arg);
if (std::isinf(result) || std::isnan(result)) {
throw MathDomainException("Tangent is undefined for the given angle");
}
return result;
} else if (func == "asin") {
if (arg < -1.0 || arg > 1.0) {
throw MathDomainException("Arcsin argument must be in range [-1, 1]");
}
return std::asin(arg);
} else if (func == "acos") {
if (arg < -1.0 || arg > 1.0) {
throw MathDomainException("Arccos argument must be in range [-1, 1]");
}
return std::acos(arg);
} else if (func == "atan") {
return std::atan(arg);
} else if (func == "log") {
if (arg <= 0.0) {
throw MathDomainException("Logarithm argument must be positive");
}
return std::log10(arg);
} else if (func == "ln") {
if (arg <= 0.0) {
throw MathDomainException("Natural logarithm argument must be positive");
}
return std::log(arg);
} else if (func == "sqrt") {
if (arg < 0.0) {
throw MathDomainException("Square root of negative number is not a real number");
}
return std::sqrt(arg);
} else if (func == "exp") {
double result = std::exp(arg);
if (std::isinf(result)) {
throw MathDomainException("Exponential overflow");
}
return result;
} else if (func == "abs") {
return std::abs(arg);
} else {
throw InvalidInputException("Unknown function: " + func);
}
}
} // namespace sci_calc

27
src/ExpressionEvaluator.h Normal file
View file

@ -0,0 +1,27 @@
// ExpressionEvaluator.h
// Evaluates postfix expression tokens and performs mathematical operations
#pragma once
#include "ExpressionParser.h"
#include "Exceptions.h"
#include <stack>
#include <vector>
#include <cmath>
namespace sci_calc {
class ExpressionEvaluator {
public:
double evaluate(const std::vector<Token> &postfixTokens) const;
private:
double applyOperator(const std::string &op, double a, double b) const;
double applyUnaryOperator(const std::string &op, double a) const;
double applyFunction(const std::string &func, double arg) const;
};
} // namespace sci_calc

270
src/ExpressionParser.cpp Normal file
View file

@ -0,0 +1,270 @@
// ExpressionParser.cpp
#include "ExpressionParser.h"
#include <algorithm>
#include <cctype>
#include <stack>
namespace sci_calc {
namespace {
std::string toLower(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return text;
}
} // namespace
ExpressionParser::ExpressionParser() {
operators_.emplace("+", OperatorInfo{2, false, 2});
operators_.emplace("-", OperatorInfo{2, false, 2});
operators_.emplace("*", OperatorInfo{3, false, 2});
operators_.emplace("/", OperatorInfo{3, false, 2});
operators_.emplace("%", OperatorInfo{3, false, 2});
operators_.emplace("^", OperatorInfo{4, true, 2});
operators_.emplace("neg", OperatorInfo{5, true, 1});
constants_.emplace("pi", 3.14159265358979323846);
constants_.emplace("e", 2.71828182845904523536);
}
std::vector<Token> ExpressionParser::parseToPostfix(const std::string &expression) const {
std::vector<Token> output;
std::stack<Token> stack;
std::size_t i = 0;
while (i < expression.size()) {
char ch = expression[i];
if (std::isspace(static_cast<unsigned char>(ch))) {
++i;
continue;
}
if (std::isdigit(static_cast<unsigned char>(ch)) ||
(ch == '.' && i + 1 < expression.size() &&
std::isdigit(static_cast<unsigned char>(expression[i + 1])))) {
output.push_back(parseNumber(expression, i));
continue;
}
if (isIdentifierChar(ch)) {
Token identifier = parseIdentifier(expression, i);
if (identifier.type == TokenType::Constant) {
output.push_back(identifier);
} else {
stack.push(identifier);
}
continue;
}
if (ch == ',') {
// Function argument separator is unsupported (all functions are unary)
throw InvalidInputException("Unexpected ',' in expression: multiple arguments not supported");
}
if (ch == '(') {
stack.push(Token{TokenType::OpenParen, "(", 0.0});
++i;
continue;
}
if (ch == ')') {
++i;
bool foundOpenParen = false;
while (!stack.empty()) {
Token top = stack.top();
stack.pop();
if (top.type == TokenType::OpenParen) {
foundOpenParen = true;
break;
}
output.push_back(top);
}
if (!foundOpenParen) {
throw InvalidInputException("Unmatched ')' in expression");
}
if (!stack.empty() && stack.top().type == TokenType::Function) {
output.push_back(stack.top());
stack.pop();
}
continue;
}
// Operators
Token opToken = parseOperator(expression, i, output);
while (!stack.empty()) {
const Token &top = stack.top();
if (top.type != TokenType::Operator) {
break;
}
const auto &currentInfo = operators_.at(opToken.text);
const auto &topInfo = operators_.at(top.text);
bool shouldPop = (!currentInfo.rightAssociative && currentInfo.precedence <= topInfo.precedence) ||
(currentInfo.rightAssociative && currentInfo.precedence < topInfo.precedence);
if (!shouldPop) {
break;
}
output.push_back(top);
stack.pop();
}
stack.push(opToken);
}
while (!stack.empty()) {
Token top = stack.top();
stack.pop();
if (top.type == TokenType::OpenParen || top.type == TokenType::CloseParen) {
throw InvalidInputException("Unmatched '(' in expression");
}
output.push_back(top);
}
return output;
}
bool ExpressionParser::isIdentifierChar(char ch) {
return std::isalpha(static_cast<unsigned char>(ch)) || ch == '_';
}
Token ExpressionParser::parseNumber(const std::string &expression, std::size_t &index) const {
std::size_t start = index;
bool hasDecimalPoint = false;
while (index < expression.size()) {
char ch = expression[index];
if (std::isdigit(static_cast<unsigned char>(ch))) {
++index;
continue;
}
if (ch == '.' && !hasDecimalPoint) {
hasDecimalPoint = true;
++index;
continue;
}
if ((ch == 'e' || ch == 'E') && index + 1 < expression.size()) {
std::size_t expIndex = index + 1;
if (expression[expIndex] == '+' || expression[expIndex] == '-') {
++expIndex;
}
if (expIndex >= expression.size() ||
!std::isdigit(static_cast<unsigned char>(expression[expIndex]))) {
throw InvalidInputException("Malformed exponent in number literal");
}
index = expIndex + 1;
while (index < expression.size() &&
std::isdigit(static_cast<unsigned char>(expression[index]))) {
++index;
}
break;
}
break;
}
const std::string literal = expression.substr(start, index - start);
try {
double value = std::stod(literal);
return Token{TokenType::Number, literal, value};
} catch (const std::exception &) {
throw InvalidInputException("Unable to parse number literal: " + literal);
}
}
Token ExpressionParser::parseIdentifier(const std::string &expression, std::size_t &index) const {
std::size_t start = index;
while (index < expression.size() &&
(isIdentifierChar(expression[index]) ||
std::isdigit(static_cast<unsigned char>(expression[index])))) {
++index;
}
std::string identifier = expression.substr(start, index - start);
std::string lower = toLower(identifier);
auto constantIt = constants_.find(lower);
if (constantIt != constants_.end()) {
return Token{TokenType::Constant, lower, constantIt->second};
}
static const std::unordered_map<std::string, int> functions = {
{"sin", 1}, {"cos", 1}, {"tan", 1}, {"asin", 1}, {"acos", 1}, {"atan", 1},
{"log", 1}, {"ln", 1}, {"sqrt", 1}, {"exp", 1}, {"abs", 1}};
if (functions.find(lower) != functions.end()) {
return Token{TokenType::Function, lower, 0.0};
}
throw InvalidInputException("Unknown identifier: " + identifier);
}
bool ExpressionParser::isUnaryOperator(const std::string &expression, std::size_t index,
const std::vector<Token> &output) const {
if (index == 0) {
return true;
}
std::size_t prevIndex = index;
while (prevIndex > 0) {
char prevChar = expression[prevIndex - 1];
if (std::isspace(static_cast<unsigned char>(prevChar))) {
--prevIndex;
continue;
}
break;
}
if (prevIndex == 0) {
return true;
}
char prevChar = expression[prevIndex - 1];
if (prevChar == '(' || prevChar == ',' || operators_.count(std::string(1, prevChar)) > 0) {
return true;
}
if (!output.empty()) {
Token lastToken = output.back();
if (lastToken.type == TokenType::Operator) {
return true;
}
}
return false;
}
Token ExpressionParser::parseOperator(const std::string &expression, std::size_t &index,
const std::vector<Token> &output) const {
char ch = expression[index];
std::string op(1, ch);
if (ch == '+' || ch == '-') {
if (isUnaryOperator(expression, index, output)) {
++index;
return Token{TokenType::Operator, ch == '-' ? "neg" : "+", 0.0};
}
}
if (operators_.find(op) == operators_.end()) {
throw InvalidInputException(std::string("Unsupported operator: ") + ch);
}
++index;
return Token{TokenType::Operator, op, 0.0};
}
} // namespace sci_calc

58
src/ExpressionParser.h Normal file
View file

@ -0,0 +1,58 @@
// ExpressionParser.h
// Responsible for tokenizing and converting infix expressions to postfix notation
#pragma once
#include "Exceptions.h"
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
namespace sci_calc {
enum class TokenType {
Number,
Operator,
Function,
OpenParen,
CloseParen,
Constant
};
struct Token {
TokenType type;
std::string text;
double value = 0.0;
};
struct OperatorInfo {
int precedence;
bool rightAssociative;
int arity;
};
class ExpressionParser {
public:
ExpressionParser();
std::vector<Token> parseToPostfix(const std::string &expression) const;
private:
std::unordered_map<std::string, OperatorInfo> operators_;
std::unordered_map<std::string, double> constants_;
static bool isIdentifierChar(char ch);
Token parseNumber(const std::string &expression, std::size_t &index) const;
Token parseIdentifier(const std::string &expression, std::size_t &index) const;
Token parseOperator(const std::string &expression, std::size_t &index,
const std::vector<Token> &output) const;
bool isUnaryOperator(const std::string &expression, std::size_t index,
const std::vector<Token> &output) const;
};
} // namespace sci_calc

93
src/main.cpp Normal file
View file

@ -0,0 +1,93 @@
// main.cpp
// Command-line interface for the Scientific Calculator
#include "Calculator.h"
#include <iostream>
#include <string>
#include <iomanip>
void printUsage(const char *programName) {
std::cout << "Scientific Calculator\n";
std::cout << "Usage: " << programName << " [expression]\n\n";
std::cout << "If no expression is provided, the calculator runs in interactive mode.\n\n";
std::cout << "Supported operations:\n";
std::cout << " Basic: +, -, *, /, %, ^ (power)\n";
std::cout << " Functions: sin, cos, tan, asin, acos, atan\n";
std::cout << " log (base 10), ln (natural), sqrt, exp, abs\n";
std::cout << " Constants: pi, e\n";
std::cout << "\nExamples:\n";
std::cout << " " << programName << " \"2 + 3 * 4\"\n";
std::cout << " " << programName << " \"sin(pi/2)\"\n";
std::cout << " " << programName << " \"sqrt(16) + log(100)\"\n";
std::cout << " " << programName << " \"2^3 + 5\"\n";
}
int main(int argc, char *argv[]) {
sci_calc::Calculator calculator;
// If expression provided as command-line argument
if (argc > 1) {
std::string expression = argv[1];
if (expression == "-h" || expression == "--help") {
printUsage(argv[0]);
return 0;
}
try {
double result = calculator.calculate(expression);
std::cout << std::fixed << std::setprecision(10) << result << std::endl;
return 0;
} catch (const sci_calc::CalculatorException &e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
} catch (const std::exception &e) {
std::cerr << "Unexpected error: " << e.what() << std::endl;
return 1;
}
}
// Interactive mode
std::cout << "Scientific Calculator - Interactive Mode\n";
std::cout << "Enter expressions to evaluate. Type 'quit' or 'exit' to exit.\n";
std::cout << "Type 'help' for usage information.\n\n";
std::string line;
while (true) {
std::cout << "> ";
if (!std::getline(std::cin, line)) {
break;
}
// Trim whitespace
line.erase(0, line.find_first_not_of(" \t\n\r"));
line.erase(line.find_last_not_of(" \t\n\r") + 1);
if (line.empty()) {
continue;
}
if (line == "quit" || line == "exit" || line == "q") {
std::cout << "Goodbye!\n";
break;
}
if (line == "help" || line == "h") {
printUsage(argv[0]);
continue;
}
try {
double result = calculator.calculate(line);
std::cout << std::fixed << std::setprecision(10) << result << std::endl;
} catch (const sci_calc::CalculatorException &e) {
std::cerr << "Error: " << e.what() << std::endl;
} catch (const std::exception &e) {
std::cerr << "Unexpected error: " << e.what() << std::endl;
}
}
return 0;
}