diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c73654 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Build artifacts +obj/ +calculator + +# macOS +.DS_Store + +# Editor +.vscode/ +.idea/ + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4395926 --- /dev/null +++ b/Makefile @@ -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 + + diff --git a/src/Calculator.cpp b/src/Calculator.cpp new file mode 100644 index 0000000..3968740 --- /dev/null +++ b/src/Calculator.cpp @@ -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 postfixTokens = parser_.parseToPostfix(expression); + return evaluator_.evaluate(postfixTokens); +} + +} // namespace sci_calc + + diff --git a/src/Calculator.h b/src/Calculator.h new file mode 100644 index 0000000..a5c652e --- /dev/null +++ b/src/Calculator.h @@ -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 + +namespace sci_calc { + +class Calculator { +public: + Calculator(); + + double calculate(const std::string &expression); + +private: + ExpressionParser parser_; + ExpressionEvaluator evaluator_; +}; + +} // namespace sci_calc + + diff --git a/src/Exceptions.h b/src/Exceptions.h new file mode 100644 index 0000000..4775d7c --- /dev/null +++ b/src/Exceptions.h @@ -0,0 +1,31 @@ +// Exceptions.h +// Scientific Calculator custom exception hierarchy + +#pragma once + +#include +#include + +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 + + diff --git a/src/ExpressionEvaluator.cpp b/src/ExpressionEvaluator.cpp new file mode 100644 index 0000000..7c378e1 --- /dev/null +++ b/src/ExpressionEvaluator.cpp @@ -0,0 +1,158 @@ +// ExpressionEvaluator.cpp + +#include "ExpressionEvaluator.h" + +#include + +namespace sci_calc { + +double ExpressionEvaluator::evaluate(const std::vector &postfixTokens) const { + std::stack 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 + + diff --git a/src/ExpressionEvaluator.h b/src/ExpressionEvaluator.h new file mode 100644 index 0000000..ea8ea6b --- /dev/null +++ b/src/ExpressionEvaluator.h @@ -0,0 +1,27 @@ +// ExpressionEvaluator.h +// Evaluates postfix expression tokens and performs mathematical operations + +#pragma once + +#include "ExpressionParser.h" +#include "Exceptions.h" + +#include +#include +#include + +namespace sci_calc { + +class ExpressionEvaluator { +public: + double evaluate(const std::vector &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 + + diff --git a/src/ExpressionParser.cpp b/src/ExpressionParser.cpp new file mode 100644 index 0000000..134331b --- /dev/null +++ b/src/ExpressionParser.cpp @@ -0,0 +1,270 @@ +// ExpressionParser.cpp + +#include "ExpressionParser.h" + +#include +#include +#include + +namespace sci_calc { + +namespace { + +std::string toLower(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { + return static_cast(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 ExpressionParser::parseToPostfix(const std::string &expression) const { + std::vector output; + std::stack stack; + + std::size_t i = 0; + while (i < expression.size()) { + char ch = expression[i]; + + if (std::isspace(static_cast(ch))) { + ++i; + continue; + } + + if (std::isdigit(static_cast(ch)) || + (ch == '.' && i + 1 < expression.size() && + std::isdigit(static_cast(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 ¤tInfo = 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(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(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(expression[expIndex]))) { + throw InvalidInputException("Malformed exponent in number literal"); + } + index = expIndex + 1; + while (index < expression.size() && + std::isdigit(static_cast(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(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 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 &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(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 &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 + + diff --git a/src/ExpressionParser.h b/src/ExpressionParser.h new file mode 100644 index 0000000..bf253ca --- /dev/null +++ b/src/ExpressionParser.h @@ -0,0 +1,58 @@ +// ExpressionParser.h +// Responsible for tokenizing and converting infix expressions to postfix notation + +#pragma once + +#include "Exceptions.h" + +#include +#include +#include +#include + +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 parseToPostfix(const std::string &expression) const; + +private: + std::unordered_map operators_; + std::unordered_map 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 &output) const; + + bool isUnaryOperator(const std::string &expression, std::size_t index, + const std::vector &output) const; +}; + +} // namespace sci_calc + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..67a9dcf --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,93 @@ +// main.cpp +// Command-line interface for the Scientific Calculator + +#include "Calculator.h" + +#include +#include +#include + +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; +} + +