From 5634d1bb39eed5361afb0ae0e400ca079aafacb6 Mon Sep 17 00:00:00 2001 From: Joao Carreira Date: Fri, 8 Mar 2024 23:10:47 +0000 Subject: [PATCH] feat: add challenge code --- .clang-format | 156 +++++++++++++++++++++++++++++++++ .clang-tidy | 44 ++++++++++ .clangd | 2 + .gitignore | 2 + .vscode/c_cpp_properties.json | 10 +++ .vscode/launch.json | 17 ++++ .vscode/settings.json | 12 +++ .vscode/tasks.json | 16 ++++ BinaryOperatorFactory.hpp | 32 +++++++ BinaryOperatorFactory.tcc | 20 +++++ BinaryOperatorFactoryTests.cpp | 30 +++++++ CMakeLists.txt | 53 +++++++++++ Calculator.cpp | 85 ++++++++++++++++++ Calculator.hpp | 44 ++++++++++ CalculatorCli.cpp | 131 +++++++++++++++++++++++++++ CalculatorCli.hpp | 25 ++++++ CalculatorCliTests.cpp | 80 +++++++++++++++++ CalculatorTests.cpp | 80 +++++++++++++++++ Operand.hpp | 34 +++++++ main.cpp | 10 +++ utils.cpp | 33 +++++++ utils.hpp | 9 ++ 22 files changed, 925 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 BinaryOperatorFactory.hpp create mode 100644 BinaryOperatorFactory.tcc create mode 100644 BinaryOperatorFactoryTests.cpp create mode 100644 CMakeLists.txt create mode 100644 Calculator.cpp create mode 100644 Calculator.hpp create mode 100644 CalculatorCli.cpp create mode 100644 CalculatorCli.hpp create mode 100644 CalculatorCliTests.cpp create mode 100644 CalculatorTests.cpp create mode 100644 Operand.hpp create mode 100644 main.cpp create mode 100644 utils.cpp create mode 100644 utils.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..2ce3d17 --- /dev/null +++ b/.clang-format @@ -0,0 +1,156 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: AcrossEmptyLines +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: true +BreakBeforeInheritanceComma: false +BreakInheritanceList: AfterColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: AfterColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DeriveLineEnding: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentRequires: false +IndentWrappedFunctionNames: false +InsertNewlineAtEOF: true +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +LineEnding: LF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReferenceAlignment: Left +ReflowComments: true +ShortNamespaceLines: 0 +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeCaseColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Latest +TabWidth: 4 +UseTab: Never +... \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..5b17e31 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,44 @@ +--- +Checks: '*,-modernize-use-nodiscard' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: file +CheckOptions: + - key: bugprone-use-after-move + value: '1' + - key: bugprone-assert-side-effect.AssertMacros + value: assert + - key: bugprone-suspicious-enum-usage.StrictMode + value: '0' + - key: bugprone-unused-return-value.CheckedFunctions + value: '::std::async;::std::launder;::std::remove;::std::remove_if;::std::unique;::std::unique_ptr::release;::std::basic_string::empty;::std::vector::empty' + - key: cert-dcl59-cpp.HeaderFileExtensions + value: ',h,hh,hpp,hxx' + - key: cert-err09-cpp.CheckThrowTemporaries + value: '1' + - key: cert-err61-cpp.CheckThrowTemporaries + value: '1' + - key: cert-msc32-c.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-msc51-cpp.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-oop11-cpp.IncludeStyle + value: llvm + - key: cppcoreguidelines-no-malloc.Allocations + value: '::malloc;::calloc' + - key: cppcoreguidelines-no-malloc.Deallocations + value: '::free' + - key: cppcoreguidelines-no-malloc.Reallocations + value: '::realloc' + - key: cppcoreguidelines-pro-type-member-init.IgnoreArrays + value: '0' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: '0' + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: '0' + - key: hicpp-special-member-functions.AllowMissingMoveFunctions + value: '0' + - key: hicpp-special-member-functions.AllowSoleDefaultDtor + value: '0' +... \ No newline at end of file diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..30a5ba3 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-std=c++20] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76cc4f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cache/** +build/** diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..e93f4a7 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,10 @@ +{ + "configurations": [ + { + "compileCommands": "${workspaceFolder}/build/debug/compile_commands.json", + "cppStandard": "c++23" + + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8cd7179 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/build/calculator-challege", + "args": [], + "cwd": "${workspaceFolder}", + "preLaunchTask": "CMake: build" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6c64abe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "testMate.cpp.test.advancedExecutables": [ + { + "pattern": "${workspaceFolder}/build/*-tests", + "runTask": { + "before": [ + "CMake: build" + ] + } + } + ], +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4edc48b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cmake", + "label": "CMake: build", + "command": "build", + "targets": [ + "all" + ], + "group": "build", + "problemMatcher": [], + "detail": "CMake template build task" + } + ] +} \ No newline at end of file diff --git a/BinaryOperatorFactory.hpp b/BinaryOperatorFactory.hpp new file mode 100644 index 0000000..b1bb0a6 --- /dev/null +++ b/BinaryOperatorFactory.hpp @@ -0,0 +1,32 @@ +#ifndef CALCULATOR_CHALLENGE_BINARYOPERATORFACTORY_HPP +#define CALCULATOR_CHALLENGE_BINARYOPERATORFACTORY_HPP + +#include +#include +#include +#include + +#include "Operand.hpp" + +template +class BinaryOperatorFactory +{ + using Operator = BinaryOperatorType; + +public: + BinaryOperatorFactory() noexcept; + BinaryOperatorFactory(const BinaryOperatorFactory&) noexcept = delete; + BinaryOperatorFactory(BinaryOperatorFactory&&) noexcept = delete; + auto operator=(const BinaryOperatorFactory&) noexcept -> BinaryOperatorFactory& = delete; + auto operator=(BinaryOperatorFactory&&) noexcept -> BinaryOperatorFactory& = delete; + ~BinaryOperatorFactory() noexcept = default; + + auto make(std::string_view text) noexcept -> std::optional; + +private: + std::unordered_map _binary_operations; +}; + +#include "BinaryOperatorFactory.tcc" + +#endif diff --git a/BinaryOperatorFactory.tcc b/BinaryOperatorFactory.tcc new file mode 100644 index 0000000..a514cdc --- /dev/null +++ b/BinaryOperatorFactory.tcc @@ -0,0 +1,20 @@ + +#include "BinaryOperatorFactory.hpp" +#include "utils.hpp" + +template +BinaryOperatorFactory::BinaryOperatorFactory() noexcept +{ + _binary_operations["+"] = [](auto lhs, auto rhs) { return lhs + rhs; }; + _binary_operations["-"] = [](auto lhs, auto rhs) { return lhs - rhs; }; + _binary_operations["*"] = [](auto lhs, auto rhs) { return lhs * rhs; }; + _binary_operations["/"] = [](auto lhs, auto rhs) { return lhs / rhs; }; +} + +template +auto BinaryOperatorFactory::make(std::string_view text) noexcept -> std::optional +{ + auto found_op = _binary_operations.find(std::string{sanitize_text(text)}); + if (found_op == _binary_operations.end()) return std::nullopt; + return found_op->second; +} diff --git a/BinaryOperatorFactoryTests.cpp b/BinaryOperatorFactoryTests.cpp new file mode 100644 index 0000000..a2564e3 --- /dev/null +++ b/BinaryOperatorFactoryTests.cpp @@ -0,0 +1,30 @@ + +#include +#include + +#include "BinaryOperatorFactory.hpp" + +TEMPLATE_TEST_CASE("BinaryOperatorFactory", "[calculator-challenge]", std::int64_t, double) +{ + constexpr TestType kLeftOperand{20}; + constexpr TestType kRightOperand{2}; + + BinaryOperatorFactory factory; + + auto check_factory = [&](std::string_view str, TestType result) { + auto operation = factory.make(str); + if (!operation.has_value()) { + return false; + } + return (*operation)(kLeftOperand, kRightOperand) == result; + }; + + CHECK(check_factory("+", kLeftOperand + kRightOperand)); + CHECK(check_factory(" + ", kLeftOperand + kRightOperand)); + CHECK(check_factory(" +", kLeftOperand + kRightOperand)); + CHECK(check_factory("+ ", kLeftOperand + kRightOperand)); + CHECK(check_factory("+ a", kLeftOperand + kRightOperand)); + CHECK(check_factory("-", kLeftOperand - kRightOperand)); + CHECK(check_factory("*", kLeftOperand * kRightOperand)); + CHECK(check_factory("/", kLeftOperand / kRightOperand)); +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..513593a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.27) +project(CalculatorChallenge CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_EXTENSIONS ON) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + +INCLUDE(FetchContent) + +FetchContent_Declare( + fmtformat + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 10.2.1 +) +FetchContent_MakeAvailable(fmtformat) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.3 + ) +FetchContent_MakeAvailable(Catch2) +list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +include(CTest) +include(Catch) + +set(CMAKE_C_COMPILER /opt/homebrew/Cellar/llvm@16/16.0.6/bin/clang) +set(CMAKE_CXX_COMPILER /opt/homebrew/Cellar/llvm@16/16.0.6/bin/clang++) + +SET(CALCULATOR_CHALLENGE_SOURCES + Calculator.cpp + CalculatorCli.cpp + utils.cpp +) + +add_executable(calculator-challege + main.cpp + ${CALCULATOR_CHALLENGE_SOURCES} +) +target_link_libraries(calculator-challege PRIVATE fmt::fmt) + + +add_executable(calculator-challege-tests + CalculatorTests.cpp + BinaryOperatorFactoryTests.cpp + CalculatorCliTests.cpp + ${CALCULATOR_CHALLENGE_SOURCES} +) +target_link_libraries(calculator-challege-tests PRIVATE Catch2::Catch2WithMain fmt::fmt) +catch_discover_tests(calculator-challege-tests) diff --git a/Calculator.cpp b/Calculator.cpp new file mode 100644 index 0000000..8367ed9 --- /dev/null +++ b/Calculator.cpp @@ -0,0 +1,85 @@ +#include "Calculator.hpp" + +#include +#include + +#include "Operand.hpp" + +namespace { +using Workspace = std::unordered_map; + +class Visitor; + +auto compute(Visitor& visitor, const Operation& binary_operation) noexcept -> std::optional; + +class Visitor +{ +public: + Visitor(std::span operations, Workspace& variables) noexcept : + _operations{operations}, _variables{variables} + {} + + auto operator()(const IntegerOperand& operand) noexcept -> std::optional { return operand.get(); } + + auto operator()(const VariableOperand& operand) noexcept -> std::optional + { + auto name = operand.get(); + auto found_variable = _variables.get().find(name); + if (found_variable != _variables.get().end()) return found_variable->second; + + auto found_operation = std::ranges::find(_operations, name, &Operation::variable); + if (found_operation == _operations.end()) return std::nullopt; + + return compute(*this, *found_operation); + } + +private: + std::span _operations; + std::reference_wrapper _variables; +}; + +auto compute(Visitor& visitor, const Operation& operation) noexcept -> std::optional +{ + auto lhs = std::visit(visitor, operation.lhs); + if (!lhs.has_value()) return std::nullopt; + + auto rhs = std::visit(visitor, operation.rhs); + if (!rhs.has_value()) return std::nullopt; + + return operation.binary_op(lhs.value(), rhs.value()); +} +} // namespace + +void Calculator::addOperation(Operation operation) noexcept +{ + _operations.push_back(std::move(operation)); +} + +void Calculator::addOperation(std::string result, Operand lhs, Operand rhs, Operator binary_operator) noexcept +{ + addOperation(Operation{std::move(result), std::move(lhs), std::move(rhs), std::move(binary_operator)}); +} + +void Calculator::undoOperation(std::size_t count) noexcept +{ + count = std::min(_operations.size(), count); + _operations.erase(_operations.end() - static_cast(count), _operations.end()); +} + +auto Calculator::computeLast() noexcept -> std::pair> +{ + Workspace workspace; + Visitor visitor{_operations, workspace}; + + for (auto& operation : std::views::reverse(_operations)) { + auto result = compute(visitor, operation); + if (result.has_value()) return std::make_pair(operation.variable, result.value()); + } + return std::make_pair("na", std::nullopt); +} + +auto Calculator::computeLastValue() noexcept -> double +{ + auto [_, result] = computeLast(); + return result.value_or(0.0); +} diff --git a/Calculator.hpp b/Calculator.hpp new file mode 100644 index 0000000..ae5eb59 --- /dev/null +++ b/Calculator.hpp @@ -0,0 +1,44 @@ +#ifndef CALCULATOR_CHALLENGE_WORKSPACE_HPP +#define CALCULATOR_CHALLENGE_WORKSPACE_HPP + +#include +#include +#include +#include +#include + +#include "Operand.hpp" + +struct Operation +{ + std::string variable; + + Operand lhs; + Operand rhs; + + Operator binary_op; +}; + +class Calculator +{ +public: + Calculator() noexcept = default; + Calculator(const Calculator&) noexcept = default; + Calculator(Calculator&&) noexcept = default; + auto operator=(const Calculator&) noexcept -> Calculator& = default; + auto operator=(Calculator&&) noexcept -> Calculator& = default; + ~Calculator() noexcept = default; + + void addOperation(Operation operation) noexcept; + void addOperation(std::string result, Operand lhs, Operand rhs, Operator binary_operator) noexcept; + + void undoOperation(std::size_t count) noexcept; + + auto computeLast() noexcept -> std::pair>; + auto computeLastValue() noexcept -> double; + +private: + std::vector _operations; +}; + +#endif diff --git a/CalculatorCli.cpp b/CalculatorCli.cpp new file mode 100644 index 0000000..2a7a44c --- /dev/null +++ b/CalculatorCli.cpp @@ -0,0 +1,131 @@ +#include "CalculatorCli.hpp" + +#include + +#include +#include +#include +#include + +#include "Operand.hpp" +#include "utils.hpp" + +namespace { +constexpr std::string_view kExit{"exit"}; +constexpr std::string_view kUndo{"undo"}; +constexpr std::string_view kResult{"result"}; + +constexpr auto kSeparator{' '}; +constexpr auto kEqualOperator{'='}; + +struct OperationMembers +{ + std::string_view result; + std::string_view lhs; + std::string_view rhs; + std::string_view operation; +}; + +template +auto text_to_number(std::string_view text) noexcept +{ + if (text.empty()) return T{}; + + if constexpr (std::is_integral_v) { + return std::stoll(text.data()); + } + + if constexpr (std::is_floating_point_v) { + return T{0}; // TODO: @JC check how to do doubles + } + + return T{}; +} + +auto parse_operation_members(std::string_view text) noexcept -> std::optional +{ + OperationMembers result; + + auto result_sep = text.find_first_of(kEqualOperator); + if (result_sep == std::string::npos) return {}; + + result.result = sanitize_text(text.substr(0, result_sep)); + + auto operation_text = trim_whitespaces(text.substr(result_sep + 1)); + auto left_sep = operation_text.find_first_of(kSeparator); + if (left_sep == std::string::npos) return {}; + + result.lhs = operation_text.substr(0, left_sep); + + auto right_side = operation_text.substr(left_sep + 1); + auto right_sep = right_side.find_first_of(kSeparator); + result.operation = right_side.substr(0, right_sep); + result.rhs = sanitize_text(right_side.substr(right_sep + 1)); + return result; +} + +auto parse_operand(std::string_view text) noexcept -> Operand +{ + assert(!text.empty()); + if (!std::isdigit(text.front())) { // NOLINT + return VariableOperand{std::string{text}}; + } + auto operand{static_cast(text_to_number(text))}; + return IntegerOperand{operand}; +} +} // namespace + +auto CalculatorCli::parse(std::string_view text) noexcept -> std::string +{ + auto sanitized_text = trim_whitespaces(text); + + if (sanitized_text.empty()) return {}; + + if (sanitized_text == kUndo) { + _calculator.undoOperation(1); + return {}; + } + + if (sanitized_text.starts_with(kUndo)) { + auto left_sep = sanitized_text.find_first_of(kSeparator); + _calculator.undoOperation(text_to_number(text.substr(left_sep))); + return {}; + } + + if (sanitized_text.starts_with(kResult)) { + auto [variable, value] = _calculator.computeLast(); + if (!value.has_value()) return "N/A"; + return fmt::format("{} = {}", variable, value.value()); + } + + auto components = parse_operation_members(sanitized_text); + if (!components.has_value()) return {}; + + auto operation = _operation_factory.make(components->operation); + if (!operation.has_value()) return {}; + + _calculator.addOperation( + std::string{components->result}, + parse_operand(components->lhs), + parse_operand(components->rhs), + operation.value()); + + return fmt::format("{} = {} {} {}", components->result, components->lhs, components->operation, components->rhs); +} + +void CalculatorCli::runLoop() noexcept +{ + std::string line; + while (true) { + std::cout << "> "; + std::getline(std::cin, line); + if (std::cin.fail()) { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + } + + if (line == kExit) return; + auto result = parse(line); + std::cout << result << std::endl; + } +} diff --git a/CalculatorCli.hpp b/CalculatorCli.hpp new file mode 100644 index 0000000..33cc6a2 --- /dev/null +++ b/CalculatorCli.hpp @@ -0,0 +1,25 @@ +#ifndef CALCULATOR_CHALLENGE_CALCULATORCLI_HPP +#define CALCULATOR_CHALLENGE_CALCULATORCLI_HPP + +#include "BinaryOperatorFactory.hpp" +#include "Calculator.hpp" + +class CalculatorCli +{ +public: + CalculatorCli() noexcept = default; + CalculatorCli(const CalculatorCli&) noexcept = delete; + CalculatorCli(CalculatorCli&&) noexcept = delete; + auto operator=(const CalculatorCli&) noexcept -> CalculatorCli& = delete; + auto operator=(CalculatorCli&&) noexcept -> CalculatorCli& = delete; + ~CalculatorCli() noexcept = default; + + auto parse(std::string_view text) noexcept -> std::string; + void runLoop() noexcept; + +private: + Calculator _calculator; + BinaryOperatorFactory _operation_factory; +}; + +#endif diff --git a/CalculatorCliTests.cpp b/CalculatorCliTests.cpp new file mode 100644 index 0000000..1a80d65 --- /dev/null +++ b/CalculatorCliTests.cpp @@ -0,0 +1,80 @@ +#include + +#include "CalculatorCli.hpp" + +TEST_CASE("CalculatorCli - can parse operations", "[calculator-challenge]") +{ + CalculatorCli calculator; + + calculator.parse("a = 4 + 1"); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse(" a = 4 + 1"); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse("a = 4 + 1 "); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse("a = 4 + 1"); + CHECK(calculator.parse("result") == "a = 5"); +} + +TEST_CASE("CalculatorCli - edge cases", "[calculator-challenge]") +{ + CalculatorCli calculator; + + calculator.parse("a=31+2"); + CHECK_FALSE(calculator.parse("result") == "a = 33"); // TODO: @JC do not require spaces +} + +TEST_CASE("CalculatorCli - can undo operations", "[calculator-challenge]") +{ + CalculatorCli calculator; + + calculator.parse("a = 4 + 1"); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse(" a = 6 + 1"); + CHECK(calculator.parse("result") == "a = 7"); + + SECTION("simple command") + { + calculator.parse("undo"); + CHECK(calculator.parse("result") == "a = 5"); + } + SECTION("two undos") + { + calculator.parse("undo"); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse("undo"); + CHECK(calculator.parse("result") == "N/A"); + } + SECTION("with space") + { + calculator.parse("undo "); + CHECK(calculator.parse("result") == "a = 5"); + } + SECTION("with zero") + { + calculator.parse("undo 0"); + CHECK(calculator.parse("result") == "a = 7"); + } +} + +TEST_CASE("CalculatorCli - can undo multiple operations", "[calculator-challenge]") +{ + CalculatorCli calculator; + + calculator.parse("a = 4 + 1"); + CHECK(calculator.parse("result") == "a = 5"); + + calculator.parse(" a = 6 + 1"); + CHECK(calculator.parse("result") == "a = 7"); + + calculator.parse("b = 6 + 1"); + CHECK(calculator.parse("result") == "b = 7"); + + calculator.parse("undo 2"); + CHECK(calculator.parse("result") == "a = 5"); +} diff --git a/CalculatorTests.cpp b/CalculatorTests.cpp new file mode 100644 index 0000000..89116fb --- /dev/null +++ b/CalculatorTests.cpp @@ -0,0 +1,80 @@ +#include + +#include "Calculator.hpp" +#include "Operand.hpp" + +constexpr auto addiction = [](auto lhs, auto rhs) { return lhs + rhs; }; +constexpr auto subtraction = [](auto lhs, auto rhs) { return lhs - rhs; }; + +TEST_CASE("Calculator - can add operations", "[calculator-challenge]") +{ + Calculator calculator; + + calculator.addOperation("a", IntegerOperand{5}, IntegerOperand{10}, addiction); + CHECK(calculator.computeLastValue() == 15.0); + + calculator.addOperation("b", IntegerOperand{1}, IntegerOperand{5}, addiction); + CHECK(calculator.computeLastValue() == 6.0); + + calculator.addOperation("c", IntegerOperand{1}, IntegerOperand{5}, subtraction); + CHECK(calculator.computeLastValue() == -4.0); +} + +TEST_CASE("Calculator - can undo operations", "[calculator-challenge]") +{ + Calculator calculator; + + calculator.addOperation("a", IntegerOperand{5}, IntegerOperand{10}, addiction); + CHECK(calculator.computeLastValue() == 15.0); + calculator.addOperation("b", IntegerOperand{1}, IntegerOperand{5}, subtraction); + CHECK(calculator.computeLastValue() == -4.0); + + SECTION("one by one") + { + calculator.undoOperation(1); + CHECK(calculator.computeLastValue() == 15.0); + + calculator.undoOperation(1); + CHECK(calculator.computeLastValue() == 0.0); + } + + SECTION("all") + { + calculator.undoOperation(2); + CHECK(calculator.computeLastValue() == 0.0); + } + + SECTION("more than was added") + { + calculator.undoOperation(50); + CHECK(calculator.computeLastValue() == 0.0); + } +} + +TEST_CASE("Calculator - can add operations with variables", "[calculator-challenge]") +{ + Calculator calculator; + + calculator.addOperation("a", IntegerOperand{1}, IntegerOperand{2}, addiction); + CHECK(calculator.computeLastValue() == 3); + + calculator.addOperation("b", VariableOperand{"a"}, IntegerOperand{10}, addiction); + CHECK(calculator.computeLastValue() == 13); + + calculator.addOperation("b", VariableOperand{"a"}, IntegerOperand{2}, addiction); + CHECK(calculator.computeLastValue() == 5); +} + +TEST_CASE("Calculator - if a variable is missing the last available is returned", "[calculator-challenge]") +{ + Calculator calculator; + + calculator.addOperation("a", VariableOperand{"c"}, IntegerOperand{10}, addiction); + CHECK(calculator.computeLastValue() == 0.0); + + calculator.addOperation("a", IntegerOperand{1}, IntegerOperand{2}, addiction); + CHECK(calculator.computeLastValue() == 3); + + calculator.addOperation("a", VariableOperand{"c"}, IntegerOperand{10}, addiction); + CHECK(calculator.computeLastValue() == 3.0); +} diff --git a/Operand.hpp b/Operand.hpp new file mode 100644 index 0000000..90164de --- /dev/null +++ b/Operand.hpp @@ -0,0 +1,34 @@ +#ifndef CALCULATOR_CHALLENGE_OPERAND_HPP +#define CALCULATOR_CHALLENGE_OPERAND_HPP + +#include +#include +#include + +template +using BinaryOperatorType = std::function; +using Operator = BinaryOperatorType; + +class IntegerOperand +{ +public: + explicit IntegerOperand(double value) noexcept : _value{value} {} + auto get() const noexcept -> double { return _value; }; + +private: + double _value{0}; +}; + +class VariableOperand +{ +public: + explicit VariableOperand(std::string name) noexcept : _name{std::move(name)} {} + auto get() const noexcept -> std::string { return _name; }; + +private: + std::string _name; +}; + +using Operand = std::variant; + +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..77d35cd --- /dev/null +++ b/main.cpp @@ -0,0 +1,10 @@ +#include + +#include "CalculatorCli.hpp" + +auto main() -> int +{ + CalculatorCli cli; + cli.runLoop(); + return 0; +} diff --git a/utils.cpp b/utils.cpp new file mode 100644 index 0000000..50a05aa --- /dev/null +++ b/utils.cpp @@ -0,0 +1,33 @@ +#include "utils.hpp" + +#include + +namespace { +constexpr char kSpace{' '}; +} // namespace + +auto trim_whitespaces(std::string_view text) noexcept -> std::string_view +{ + auto leading_spaces = text.find_first_not_of(kSpace); + if (leading_spaces == std::string::npos) return {}; + + auto sub_view = text.substr(leading_spaces); + + auto trailling_spaces = sub_view.find_last_not_of(kSpace); + if (trailling_spaces == std::string::npos) return sub_view; + + return sub_view.substr(0, trailling_spaces + 1); +} + +auto sanitize_text(std::string_view text) noexcept -> std::string_view +{ + auto leading_spaces = text.find_first_not_of(kSpace); + if (leading_spaces == std::string::npos) return {}; + + auto sub_view = text.substr(leading_spaces); + + auto trailling_spaces = sub_view.find_first_of(kSpace); + if (trailling_spaces == std::string::npos) return sub_view; + + return sub_view.substr(0, trailling_spaces); +} diff --git a/utils.hpp b/utils.hpp new file mode 100644 index 0000000..e8fdeb2 --- /dev/null +++ b/utils.hpp @@ -0,0 +1,9 @@ +#ifndef CALCULATOR_CHALLENGE_UTILS_HPP +#define CALCULATOR_CHALLENGE_UTILS_HPP + +#include + +auto trim_whitespaces(std::string_view text) noexcept -> std::string_view; +auto sanitize_text(std::string_view text) noexcept -> std::string_view; + +#endif