commit 5634d1bb39eed5361afb0ae0e400ca079aafacb6 Author: Joao Carreira Date: Fri Mar 8 23:10:47 2024 +0000 feat: add challenge code 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