From cdd2b1c8ad834d9794c49d4c4110fac236e09071 Mon Sep 17 00:00:00 2001 From: Nicholas Baron <35079404+Nicholas-Baron@users.noreply.github.com> Date: Mon, 22 Apr 2019 16:59:13 -0700 Subject: [PATCH 1/9] Updates to copypastemanager (#390) Related to #55 and #64 Description of the changes: Added constexpr to formerly static const or #define variables Applied C++ Core Guideline NR.2 Added auto and const in appropriate places How changes were validated: Used the provided unit tests --- src/CalcViewModel/Common/CopyPasteManager.cpp | 114 ++++++++---------- src/CalcViewModel/Common/CopyPasteManager.h | 21 ++-- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/src/CalcViewModel/Common/CopyPasteManager.cpp b/src/CalcViewModel/Common/CopyPasteManager.cpp index d10c4c57..6d6807ba 100644 --- a/src/CalcViewModel/Common/CopyPasteManager.cpp +++ b/src/CalcViewModel/Common/CopyPasteManager.cpp @@ -15,14 +15,15 @@ using namespace Windows::Foundation; using namespace Windows::System; using namespace Windows::ApplicationModel::DataTransfer; -unsigned long long maxOperandNumber; - String^ CopyPasteManager::supportedFormats[] = { StandardDataFormats::Text }; -constexpr wstring_view c_validCharacterSet{ L"0123456789()+-*/.abcdefABCDEF" }; +static constexpr wstring_view c_validCharacterSet{ L"0123456789()+-*/.abcdefABCDEF" }; + +// The below values can not be "constexpr"-ed, +// as both wstring_view and wchar[] can not be concatenated // [\s\x85] means white-space characters static const wstring c_wspc = L"[\\s\\x85]*"; static const wstring c_wspcLParens = c_wspc + L"[(]*" + c_wspc; @@ -103,19 +104,16 @@ task CopyPasteManager::GetStringToPaste(ViewMode mode, CategoryGroupTyp int CopyPasteManager::ClipboardTextFormat() { - int result = -1; - - auto dataPackageView = Clipboard::GetContent(); + const auto dataPackageView = Clipboard::GetContent(); for (int i = 0; i < RTL_NUMBER_OF(supportedFormats); i++) { if (dataPackageView->Contains(supportedFormats[i])) { - result = i; - break; + return i; } } - return result; + return -1; } String^ CopyPasteManager::ValidatePasteExpression(String^ pastedText, ViewMode mode, int programmerNumberBase, int bitLengthType) @@ -268,13 +266,8 @@ bool CopyPasteManager::ExpressionRegExMatch(vector operands, ViewMode m return false; } - bool expMatched = true; vector patterns{}; - - pair operandLimits = GetMaxOperandLengthAndValue(mode, modeType, programmerNumberBase, bitLengthType); - size_t maxOperandLength = operandLimits.first; - uint64_t maxOperandValue = operandLimits.second; - + if (mode == ViewMode::Standard) { patterns.assign(standardModePatterns.begin(), standardModePatterns.end()); @@ -292,11 +285,14 @@ bool CopyPasteManager::ExpressionRegExMatch(vector operands, ViewMode m patterns.assign(unitConverterPatterns.begin(), unitConverterPatterns.end()); } - for (const wstring& operand : operands) + const auto [maxOperandLength, maxOperandValue] = GetMaxOperandLengthAndValue(mode, modeType, programmerNumberBase, bitLengthType); + bool expMatched = true; + + for (const auto& operand : operands) { // Each operand only needs to match one of the available patterns. bool operandMatched = false; - for (const wregex& pattern : patterns) + for (const auto& pattern : patterns) { operandMatched = operandMatched || regex_match(operand, pattern); } @@ -305,7 +301,7 @@ bool CopyPasteManager::ExpressionRegExMatch(vector operands, ViewMode m { // Remove characters that are valid in the expression but we do not want to include in length calculations // or which will break conversion from string-to-ULL. - wstring operandValue = SanitizeOperand(operand); + const wstring operandValue = SanitizeOperand(operand); // If an operand exceeds the maximum length allowed, break and return. if (OperandLength(operandValue, mode, modeType, programmerNumberBase) > maxOperandLength) @@ -341,16 +337,16 @@ bool CopyPasteManager::ExpressionRegExMatch(vector operands, ViewMode m pair CopyPasteManager::GetMaxOperandLengthAndValue(ViewMode mode, CategoryGroupType modeType, int programmerNumberBase, int bitLengthType) { - size_t maxLength = 0; - uint64_t maxValue = 0; - + constexpr size_t defaultMaxOperandLength = 0; + constexpr uint64_t defaultMaxValue = 0; + if (mode == ViewMode::Standard) { - maxLength = MaxStandardOperandLength; + return make_pair(MaxStandardOperandLength, defaultMaxValue); } else if (mode == ViewMode::Scientific) { - maxLength = MaxScientificOperandLength; + return make_pair(MaxScientificOperandLength, defaultMaxValue); } else if (mode == ViewMode::Programmer) { @@ -390,15 +386,17 @@ pair CopyPasteManager::GetMaxOperandLengthAndValue(ViewMode mo unsigned int signBit = (programmerNumberBase == DecBase) ? 1 : 0; - maxLength = (size_t)ceil((bitLength - signBit) / bitsPerDigit); - maxValue = UINT64_MAX >> (MaxProgrammerBitLength - (bitLength - signBit)); + const auto maxLength = static_cast(ceil((bitLength - signBit) / bitsPerDigit)); + const uint64_t maxValue = UINT64_MAX >> (MaxProgrammerBitLength - (bitLength - signBit)); + + return make_pair(maxLength, maxValue); } else if (modeType == CategoryGroupType::Converter) { - maxLength = MaxConverterInputLength; + return make_pair(MaxConverterInputLength, defaultMaxValue); } - return make_pair(maxLength, maxValue); + return make_pair(defaultMaxOperandLength, defaultMaxValue); } wstring CopyPasteManager::SanitizeOperand(const wstring& operand) @@ -417,8 +415,7 @@ bool CopyPasteManager::TryOperandToULL(const wstring& operand, int numberBase, u return false; } - // Default to base10 - int intBase = 10; + int intBase; switch (numberBase) { case HexBase: @@ -430,6 +427,7 @@ bool CopyPasteManager::TryOperandToULL(const wstring& operand, int numberBase, u case BinBase: intBase = 2; break; + default: case DecBase: intBase = 10; break; @@ -441,11 +439,11 @@ bool CopyPasteManager::TryOperandToULL(const wstring& operand, int numberBase, u result = stoull(operand, &size, intBase); return true; } - catch (invalid_argument) + catch (const invalid_argument&) { // Do nothing } - catch (out_of_range) + catch (const out_of_range&) { // Do nothing } @@ -453,35 +451,28 @@ bool CopyPasteManager::TryOperandToULL(const wstring& operand, int numberBase, u return false; } -size_t CopyPasteManager::OperandLength(wstring operand, ViewMode mode, CategoryGroupType modeType, int programmerNumberBase) -{ - size_t len = 0; - if (mode == ViewMode::Standard || mode == ViewMode::Scientific) - { - len = StandardScientificOperandLength(operand); - } - else if (mode == ViewMode::Programmer) - { - len = ProgrammerOperandLength(operand, programmerNumberBase); - } - else if (modeType == CategoryGroupType::Converter) - { - len = operand.length(); +size_t CopyPasteManager::OperandLength(const wstring& operand, ViewMode mode, CategoryGroupType modeType, int programmerNumberBase) +{ + if (modeType == CategoryGroupType::Converter) { + return operand.length(); } - return len; + switch(mode) { + case ViewMode::Standard: + case ViewMode::Scientific: + return StandardScientificOperandLength(operand); + + case ViewMode::Programmer: + return ProgrammerOperandLength(operand, programmerNumberBase); + + default: + return 0; + } } -size_t CopyPasteManager::StandardScientificOperandLength(wstring operand) -{ - bool hasDecimal = false; - for (size_t i = 0; i < operand.length(); i++) - { - if (operand[i] == L'.') - { - hasDecimal = true; - } - } +size_t CopyPasteManager::StandardScientificOperandLength(const wstring& operand) +{ + const bool hasDecimal = operand.find('.') != wstring::npos; if (hasDecimal) { @@ -503,8 +494,7 @@ size_t CopyPasteManager::StandardScientificOperandLength(wstring operand) size_t CopyPasteManager::ProgrammerOperandLength(const wstring& operand, int numberBase) { - size_t len = operand.length(); - + vector prefixes{}; vector suffixes{}; switch (numberBase) @@ -525,7 +515,7 @@ size_t CopyPasteManager::ProgrammerOperandLength(const wstring& operand, int num break; default: // No defined prefixes/suffixes - break; + return 0; } // UInt suffixes are common across all modes @@ -535,9 +525,11 @@ size_t CopyPasteManager::ProgrammerOperandLength(const wstring& operand, int num wstring operandUpper = operand; transform(operandUpper.begin(), operandUpper.end(), operandUpper.begin(), towupper); + size_t len = operand.length(); + // Detect if there is a suffix and subtract its length // Check suffixes first to allow e.g. "0b" to result in length 1 (value 0), rather than length 0 (no value). - for (const wstring& suffix : suffixes) + for (const auto& suffix : suffixes) { if (len < suffix.length()) { @@ -552,7 +544,7 @@ size_t CopyPasteManager::ProgrammerOperandLength(const wstring& operand, int num } // Detect if there is a prefix and subtract its length - for (const wstring& prefix : prefixes) + for (const auto& prefix : prefixes) { if (len < prefix.length()) { diff --git a/src/CalcViewModel/Common/CopyPasteManager.h b/src/CalcViewModel/Common/CopyPasteManager.h index 9a886154..9d6fd669 100644 --- a/src/CalcViewModel/Common/CopyPasteManager.h +++ b/src/CalcViewModel/Common/CopyPasteManager.h @@ -13,15 +13,14 @@ namespace CalculatorUnitTests namespace CalculatorApp { - -#define QwordType 1 -#define DwordType 2 -#define WordType 3 -#define ByteType 4 -#define HexBase 5 -#define DecBase 6 -#define OctBase 7 -#define BinBase 8 + inline constexpr auto QwordType = 1; + inline constexpr auto DwordType = 2; + inline constexpr auto WordType = 3; + inline constexpr auto ByteType = 4; + inline constexpr auto HexBase = 5; + inline constexpr auto DecBase = 6; + inline constexpr auto OctBase = 7; + inline constexpr auto BinBase = 8; class CopyPasteManager { @@ -55,8 +54,8 @@ namespace CalculatorApp static std::pair GetMaxOperandLengthAndValue(CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1, int bitLengthType = -1); static std::wstring SanitizeOperand(const std::wstring& operand); static bool TryOperandToULL(const std::wstring& operand, int numberBase, unsigned long long int& result); - static size_t OperandLength(std::wstring operand, CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1); - static size_t StandardScientificOperandLength(std::wstring operand); + static size_t OperandLength(const std::wstring& operand, CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1); + static size_t StandardScientificOperandLength(const std::wstring& operand); static size_t ProgrammerOperandLength(const std::wstring& operand, int numberBase); static std::wstring RemoveUnwantedCharsFromWstring(const std::wstring& input); From 2a29947a28cb0efae769ccbfdc3205eee6159a8b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 22 Apr 2019 17:30:59 -0700 Subject: [PATCH 2/9] Collapse CurrencySecondaryStatus when no text is present in the text block (#467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #313 In Scan/Item mode, Narrator focus navigates to hidden element “No next item” after “Update rates” link in "Currency Converter" window #313 Description of the changes: Adds an x:Name to the CurrencySecondaryStatus text block Adds a NormalCurrencyStatus visual state to the CurrencySecondaryStatusStates Adds functionality to the CurrencySecondaryStatusStates to show or hide the CurrencySecondaryStatus text block. How changes were validated: Verified that the textblock is not visible in the accessibility tree via inspect.exe from the windows sdk. Verified that Narrator also does not stop on the block when in scan mode. Verified that the textblock is visible in the accessibility tree and read out in Narrator when the ChargesMayApplyCurrencyStatus or FailedCurrencyStatus viewstates are set. --- src/Calculator/Views/UnitConverter.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Calculator/Views/UnitConverter.xaml b/src/Calculator/Views/UnitConverter.xaml index 1d52c3eb..a1a0a8d0 100644 --- a/src/Calculator/Views/UnitConverter.xaml +++ b/src/Calculator/Views/UnitConverter.xaml @@ -615,8 +615,7 @@ x:Uid="RefreshButtonText" Foreground="{ThemeResource SystemControlHyperlinkBaseHighBrush}" Click="CurrencyRefreshButton_Click"/> - - + From 452f18fd8e5264650edc811bafae0937c98cd0b3 Mon Sep 17 00:00:00 2001 From: Bharat Raghunathan Date: Tue, 23 Apr 2019 23:00:31 +0530 Subject: [PATCH 3/9] Update README to include infinite precision (#470) Fixes #119 Added infinite precision as a feature in README.md and in ApplicationArchitecture.md How changes were validated: Manual preview of README and ApplicationArchitecture.md --- README.md | 3 +++ docs/ApplicationArchitecture.md | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d967b175..00243142 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Calculator ships regularly with new features and bug fixes. You can get the late - Calculation history and memory capabilities. - Conversion between many units of measurement. - Currency conversion based on data retrieved from [Bing](https://www.bing.com). +- [Infinite precision](https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic) for basic + arithmetic operations (addition, subtraction, multiplication, division) so that calculations + never lose precision. ## Getting started Prerequisites: diff --git a/docs/ApplicationArchitecture.md b/docs/ApplicationArchitecture.md index 0d7c2ff3..dff02965 100644 --- a/docs/ApplicationArchitecture.md +++ b/docs/ApplicationArchitecture.md @@ -153,7 +153,9 @@ The CalcEngine contains the logic for interpreting and performing operations acc ### RatPack -The RatPack (short for Rational Pack) is the core of the Calculator model and contains the logic for performing its mathematical operations. The interface to this layer is defined in [ratpak.h][ratpak.h]. +The RatPack (short for Rational Pack) is the core of the Calculator model and contains the logic for +performing its mathematical operations (using [infinite precision][Infinite Precision] arithmetic +instead of regular floating point arithmetic). The interface to this layer is defined in [ratpak.h][ratpak.h]. [References]:#################################################################################################### From 861d4ef40845ebc5d56aa741712eb0cbb5cdd8f7 Mon Sep 17 00:00:00 2001 From: Daniel Belcher Date: Tue, 23 Apr 2019 16:41:48 -0700 Subject: [PATCH 4/9] Update the .gitignore to ignore GraphingImplOverrides.props. (#472) Description of the changes: Update the .gitignore to ignore GraphingImplOverrides.props. This update already exists in the feature branch but we can merge it to master now to make development of the feature easier and prevent accidentally commiting this file. How changes were validated: Manual. Switch to feature branch. Add GraphingImplOverrides.props Switch to dabelc/IgnoreGraphingImplOverrides.props. Confirm git shows no files changed but props file is present. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2100dfc1..1088272d 100644 --- a/.gitignore +++ b/.gitignore @@ -289,6 +289,7 @@ __pycache__/ # Calculator specific Generated Files/ +src/GraphControl/GraphingImplOverrides.props !/build/config/TRexDefs/** !src/Calculator/TemporaryKey.pfx !src/CalculatorUnitTests/CalculatorUnitTests_TemporaryKey.pfx \ No newline at end of file From b03a026f6c07c02f90eb2951178441859e5bbf9a Mon Sep 17 00:00:00 2001 From: Daniel Belcher Date: Wed, 24 Apr 2019 12:05:19 -0700 Subject: [PATCH 5/9] Allow vcxproj and sln files to merge as text. (#474) Description of the changes: This will cause Git to insert conflict markers in the file and make it easier to identify and fix merge conflicts. Previously, Git would complain about conflicts with these files, but no markers were inserted. How changes were validated: Manual. Checkout feature/GraphingCalculator and merge in upstream/master. Git identifies merge conflicts present for Calculator.vcxproj but there are no conflict markers in the file. Abort the merge. Merge in dabelc/MergeProjectFilesAsText. Confirm Git is able to merge Calculator.vcxproj. --- .gitattributes | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.gitattributes b/.gitattributes index 51617f5f..e0d31f91 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,29 +12,6 @@ ############################################################################### *.cs diff=csharp -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -*.sln merge=binary -*.csproj merge=binary -*.vbproj merge=binary -*.vcxproj merge=binary -*.vcproj merge=binary -*.dbproj merge=binary -*.fsproj merge=binary -*.lsproj merge=binary -*.wixproj merge=binary -*.modelproj merge=binary -*.sqlproj merge=binary -*.wwaproj merge=binary - ############################################################################### # behavior for image files # From 1dee9dc98429350f3e04764bbf6dc99f29eda1a1 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 24 Apr 2019 13:37:49 -0700 Subject: [PATCH 6/9] Fix #471 (#473) [JaEra] Calc: subtracting 1-year from date during Reiwa 1 yields unexpected results. --- src/CalcViewModel/Common/DateCalculator.cpp | 34 ++++++++++++ .../DateCalculatorUnitTests.cpp | 52 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/CalcViewModel/Common/DateCalculator.cpp b/src/CalcViewModel/Common/DateCalculator.cpp index 63efcdd0..4ccba38c 100644 --- a/src/CalcViewModel/Common/DateCalculator.cpp +++ b/src/CalcViewModel/Common/DateCalculator.cpp @@ -20,13 +20,27 @@ DateCalculationEngine::DateCalculationEngine(_In_ String^ calendarIdentifier) // Returns: True if function succeeds to calculate the date else returns False bool DateCalculationEngine::AddDuration(_In_ DateTime startDate, _In_ const DateDifference& duration, _Out_ DateTime *endDate) { + auto currentCalendarSystem = m_calendar->GetCalendarSystem(); + try { m_calendar->SetDateTime(startDate); if (duration.year != 0) { + // The Japanese Era system can have multiple year partitions within the same year. + // For example, April 30, 2019 is denoted April 30, Heisei 31; May 1, 2019 is denoted as May 1, Reiwa 1. + // The Calendar treats Heisei 31 and Reiwa 1 as separate years, which results in some unexpected behaviors where subtracting a year from Reiwa 1 results in a date in Heisei 31. + // To provide the expected result across era boundaries, we first convert the Japanese era system to a Gregorian system, do date math, and then convert back to the Japanese era system. + // This works because the Japanese era system maintains the same year/month boundaries and durations as the Gregorian system and is only different in display value. + if (currentCalendarSystem == CalendarIdentifiers::Japanese) + { + m_calendar->ChangeCalendarSystem(CalendarIdentifiers::Gregorian); + } + m_calendar->AddYears(duration.year); + + m_calendar->ChangeCalendarSystem(currentCalendarSystem); } if (duration.month != 0) { @@ -41,6 +55,9 @@ bool DateCalculationEngine::AddDuration(_In_ DateTime startDate, _In_ const Date } catch (Platform::InvalidArgumentException^ ex) { + // ensure that we revert to the correct calendar system + m_calendar->ChangeCalendarSystem(currentCalendarSystem); + // Do nothing return false; } @@ -52,6 +69,8 @@ bool DateCalculationEngine::AddDuration(_In_ DateTime startDate, _In_ const Date // Returns: True if function succeeds to calculate the date else returns False bool DateCalculationEngine::SubtractDuration(_In_ DateTime startDate, _In_ const DateDifference& duration, _Out_ DateTime *endDate) { + auto currentCalendarSystem = m_calendar->GetCalendarSystem(); + // For Subtract the Algorithm is different than Add. Here the smaller units are subtracted first // and then the larger units. try @@ -68,13 +87,28 @@ bool DateCalculationEngine::SubtractDuration(_In_ DateTime startDate, _In_ const } if (duration.year != 0) { + // The Japanese Era system can have multiple year partitions within the same year. + // For example, April 30, 2019 is denoted April 30, Heisei 31; May 1, 2019 is denoted as May 1, Reiwa 1. + // The Calendar treats Heisei 31 and Reiwa 1 as separate years, which results in some unexpected behaviors where subtracting a year from Reiwa 1 results in a date in Heisei 31. + // To provide the expected result across era boundaries, we first convert the Japanese era system to a Gregorian system, do date math, and then convert back to the Japanese era system. + // This works because the Japanese era system maintains the same year/month boundaries and durations as the Gregorian system and is only different in display value. + if (currentCalendarSystem == CalendarIdentifiers::Japanese) + { + m_calendar->ChangeCalendarSystem(CalendarIdentifiers::Gregorian); + } + m_calendar->AddYears(-duration.year); + + m_calendar->ChangeCalendarSystem(currentCalendarSystem); } *endDate = m_calendar->GetDateTime(); } catch (Platform::InvalidArgumentException^ ex) { + // ensure that we revert to the correct calendar system + m_calendar->ChangeCalendarSystem(currentCalendarSystem); + // Do nothing return false; } diff --git a/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp b/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp index 9bb98fab..0318f152 100644 --- a/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp +++ b/src/CalculatorUnitTests/DateCalculatorUnitTests.cpp @@ -664,5 +664,57 @@ namespace DateCalculationUnitTests VERIFY_IS_TRUE(actualValue.find(expectedValue) != wstring::npos, message.c_str()); } } + + TEST_METHOD(JaEraTransitionAddition) + { + auto viewModel = make_unique(CalendarIdentifiers::Japanese); + auto cal = ref new Calendar(); + + // Showa period ended in Jan 1989. + cal->Year = 1989; + cal->Month = 1; + cal->Day = 1; + auto startTime = cal->GetDateTime(); + + cal->Year = 1990; + cal->Month = 1; + cal->Day = 1; + + // Expect that adding a year across boundaries adds the equivalent in the Gregorian calendar. + auto expectedResult = cal->GetDateTime(); + DateDifference duration; + duration.year = 1; + + DateTime actualResult; + viewModel->AddDuration(startTime, duration, &actualResult); + + VERIFY_ARE_EQUAL(expectedResult.UniversalTime, actualResult.UniversalTime); + } + + TEST_METHOD(JaEraTransitionSubtraction) + { + auto viewModel = make_unique(CalendarIdentifiers::Japanese); + auto cal = ref new Calendar(); + + // Showa period ended in Jan 1989. + cal->Year = 1990; + cal->Month = 1; + cal->Day = 1; + auto startTime = cal->GetDateTime(); + + cal->Year = 1989; + cal->Month = 1; + cal->Day = 1; + + // Expect that adding a year across boundaries adds the equivalent in the Gregorian calendar. + auto expectedResult = cal->GetDateTime(); + DateDifference duration; + duration.year = 1; + + DateTime actualResult; + viewModel->SubtractDuration(startTime, duration, &actualResult); + + VERIFY_ARE_EQUAL(expectedResult.UniversalTime, actualResult.UniversalTime); + } }; } From cf7ec5f7293d7e3dcfa988b40e891f686ad06bcd Mon Sep 17 00:00:00 2001 From: Matt Cooley Date: Thu, 25 Apr 2019 16:54:07 -0700 Subject: [PATCH 7/9] Miscellaneous updates to internal release builds (#477) --- build/config/SignConfig.xml | 2 +- build/pipelines/templates/prepare-release-internalonly.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/config/SignConfig.xml b/build/config/SignConfig.xml index 7209725e..eb298e87 100644 --- a/build/config/SignConfig.xml +++ b/build/config/SignConfig.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/build/pipelines/templates/prepare-release-internalonly.yaml b/build/pipelines/templates/prepare-release-internalonly.yaml index a6a4973b..178fd268 100644 --- a/build/pipelines/templates/prepare-release-internalonly.yaml +++ b/build/pipelines/templates/prepare-release-internalonly.yaml @@ -1,5 +1,5 @@ # This template contains a job which builds artifacts needed to release the app to the store and to -# Windows using Microsoft-internal systems. It relies Microsoft-internal resources and will not +# Windows using Microsoft-internal systems. It relies on Microsoft-internal resources and will not # work outside of Microsoft. # Specifically, this job: # - Signs the bundle using a secure system. If you want to build your own, use SignTool following From 8e1a14793ae731b537885a65419cc17fc6b229e3 Mon Sep 17 00:00:00 2001 From: Hassan Uraizee Date: Thu, 25 Apr 2019 16:54:36 -0700 Subject: [PATCH 8/9] Added WinAppDriver UI Tests to the Calculator Project. (#411) --- build/pipelines/azure-pipelines.ci.yaml | 4 + .../templates/build-single-architecture.yaml | 2 +- build/pipelines/templates/run-ui-tests.yaml | 52 ++++++++ src/Calculator.sln | 50 +++++--- src/CalculatorUITests/CalculatorSession.cs | 44 +++++++ .../CalculatorUITests.csproj | 22 ++++ .../CalculatorUITests.runsettings | 9 ++ src/CalculatorUITests/ScenarioStandard.cs | 112 ++++++++++++++++++ 8 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 build/pipelines/templates/run-ui-tests.yaml create mode 100644 src/CalculatorUITests/CalculatorSession.cs create mode 100644 src/CalculatorUITests/CalculatorUITests.csproj create mode 100644 src/CalculatorUITests/CalculatorUITests.runsettings create mode 100644 src/CalculatorUITests/ScenarioStandard.cs diff --git a/build/pipelines/azure-pipelines.ci.yaml b/build/pipelines/azure-pipelines.ci.yaml index 31ce98fb..23eb46ca 100644 --- a/build/pipelines/azure-pipelines.ci.yaml +++ b/build/pipelines/azure-pipelines.ci.yaml @@ -33,6 +33,10 @@ jobs: platform: ARM64 condition: not(eq(variables['Build.Reason'], 'PullRequest')) +- template: ./templates/run-ui-tests.yaml + parameters: + platform: x64 + - template: ./templates/run-unit-tests.yaml parameters: platform: x64 diff --git a/build/pipelines/templates/build-single-architecture.yaml b/build/pipelines/templates/build-single-architecture.yaml index 92e06f7b..50859db3 100644 --- a/build/pipelines/templates/build-single-architecture.yaml +++ b/build/pipelines/templates/build-single-architecture.yaml @@ -30,7 +30,7 @@ steps: inputs: solution: src/Calculator.sln vsVersion: 15.0 - msbuildArgs: /bl:$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\Calculator.binlog /p:OutDir=$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\ /p:GenerateProjectSpecificOutputFolder=true /p:AppVersion=$(Build.BuildNumber) ${{ parameters.extraMsBuildArgs }} + msbuildArgs: /bl:$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\Calculator.binlog /p:OutDir=$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\ /p:GenerateProjectSpecificOutputFolder=true /p:AppVersion=$(Build.BuildNumber) /t:Publish /p:PublishDir=$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\publish\ ${{ parameters.extraMsBuildArgs }} platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true diff --git a/build/pipelines/templates/run-ui-tests.yaml b/build/pipelines/templates/run-ui-tests.yaml new file mode 100644 index 00000000..41d441ad --- /dev/null +++ b/build/pipelines/templates/run-ui-tests.yaml @@ -0,0 +1,52 @@ +# This template contains jobs to run UI tests using WinAppDriver. + +parameters: + platform: '' + +jobs: +- job: UITests${{ parameters.platform }} + displayName: UITests ${{ parameters.platform }} + dependsOn: Build${{ parameters.platform }} + condition: succeeded() + pool: + vmImage: windows-2019 + variables: + skipComponentGovernanceDetection: true + steps: + - checkout: none + + - powershell: Set-DisplayResolution -Width 1920 -Height 1080 -Force + displayName: Set resolution to 1920x1080 + continueOnError: true + + - task: DownloadBuildArtifacts@0 + displayName: Download AppxBundle and CalculatorUITests + inputs: + artifactName: drop + itemPattern: | + drop/Release/${{ parameters.platform }}/Calculator/AppPackages/** + drop/Release/${{ parameters.platform }}/publish/** + + - task: PowerShell@2 + displayName: Install certificate + inputs: + filePath: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/Calculator/AppPackages/Calculator_$(Build.BuildNumber)_Test/Add-AppDevPackage.ps1 + arguments: -CertificatePath $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/Calculator/AppPackages/Calculator_$(Build.BuildNumber)_Test/Calculator_$(Build.BuildNumber)_${{ parameters.platform }}.cer -Force + + - task: PowerShell@2 + displayName: Install app + inputs: + filePath: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/Calculator/AppPackages/Calculator_$(Build.BuildNumber)_Test/Add-AppDevPackage.ps1 + arguments: -Force + + - powershell: Start-Process -FilePath "C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe" -Verb RunAs + displayName: Start WinAppDriver + + - task: VSTest@2 + displayName: Run CalculatorUITests + inputs: + testAssemblyVer2: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/publish/CalculatorUITests.dll + vsTestVersion: 16.0 + runSettingsFile: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/publish/CalculatorUITests.runsettings + platform: ${{ parameters.platform }} + configuration: Release \ No newline at end of file diff --git a/src/Calculator.sln b/src/Calculator.sln index 2d36a93f..232fcd1e 100644 --- a/src/Calculator.sln +++ b/src/Calculator.sln @@ -16,6 +16,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalcViewModel", "CalcViewMo EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalculatorUnitTests", "CalculatorUnitTests\CalculatorUnitTests.vcxproj", "{D3BAED2C-4B07-4E1D-8807-9D6499450349}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalculatorUITests", "CalculatorUITests\CalculatorUITests.csproj", "{B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM @@ -28,22 +30,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.ActiveCfg = Debug|ARM - {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.Build.0 = Debug|ARM - {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.Build.0 = Debug|ARM64 - {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.ActiveCfg = Debug|x64 - {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.Build.0 = Debug|x64 - {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.ActiveCfg = Debug|Win32 - {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.Build.0 = Debug|Win32 - {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.ActiveCfg = Release|ARM - {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.Build.0 = Release|ARM - {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.ActiveCfg = Release|ARM64 - {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.Build.0 = Release|ARM64 - {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.ActiveCfg = Release|x64 - {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.Build.0 = Release|x64 - {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.ActiveCfg = Release|Win32 - {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.Build.0 = Release|Win32 {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.ActiveCfg = Debug|ARM {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Build.0 = Debug|ARM {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Deploy.0 = Debug|ARM @@ -68,6 +54,22 @@ Global {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.ActiveCfg = Release|Win32 {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Build.0 = Release|Win32 {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Deploy.0 = Release|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.ActiveCfg = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.Build.0 = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.Build.0 = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.ActiveCfg = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.Build.0 = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.ActiveCfg = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.Build.0 = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.ActiveCfg = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.Build.0 = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.ActiveCfg = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.Build.0 = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.ActiveCfg = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.Build.0 = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.ActiveCfg = Release|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.Build.0 = Release|Win32 {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.ActiveCfg = Debug|ARM {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.Build.0 = Debug|ARM {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -100,6 +102,22 @@ Global {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.ActiveCfg = Release|Win32 {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.Build.0 = Release|Win32 {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.Deploy.0 = Release|Win32 + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|ARM.ActiveCfg = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|ARM.Build.0 = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|ARM64.Build.0 = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|x64.Build.0 = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Debug|x86.Build.0 = Debug|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|ARM.ActiveCfg = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|ARM.Build.0 = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|ARM64.ActiveCfg = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|ARM64.Build.0 = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|x64.ActiveCfg = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|x64.Build.0 = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|x86.ActiveCfg = Release|Any CPU + {B2C5ADFF-D6B5-48C1-BB8C-571BFD583D7F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CalculatorUITests/CalculatorSession.cs b/src/CalculatorUITests/CalculatorSession.cs new file mode 100644 index 00000000..9e4caa9b --- /dev/null +++ b/src/CalculatorUITests/CalculatorSession.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Windows; +using System; + +namespace CalculatorUITests +{ + public class CalculatorSession + { + // Note: append /wd/hub to the URL if you're directing the test at Appium + private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723"; + private const string CalculatorAppId = "Microsoft.WindowsCalculator.Dev_8wekyb3d8bbwe!App"; + protected static WindowsDriver session; + + public static void Setup(TestContext context) + { + // Launch Calculator application if it is not yet launched + if (session == null) + { + // Create a new session to bring up an instance of the Calculator application + // Note: Multiple calculator windows (instances) share the same process Id + var options = new AppiumOptions(); + options.AddAdditionalCapability("app", CalculatorAppId); + options.AddAdditionalCapability("deviceName", "WindowsPC"); + session = new WindowsDriver(new Uri(WindowsApplicationDriverUrl), options); + session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(180); + Assert.IsNotNull(session); + } + } + + public static void TearDown() + { + // Close the application and delete the session + if (session != null) + { + session.Quit(); + session = null; + } + } + } +} diff --git a/src/CalculatorUITests/CalculatorUITests.csproj b/src/CalculatorUITests/CalculatorUITests.csproj new file mode 100644 index 00000000..42ed8672 --- /dev/null +++ b/src/CalculatorUITests/CalculatorUITests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/CalculatorUITests/CalculatorUITests.runsettings b/src/CalculatorUITests/CalculatorUITests.runsettings new file mode 100644 index 00000000..ac9968d5 --- /dev/null +++ b/src/CalculatorUITests/CalculatorUITests.runsettings @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/CalculatorUITests/ScenarioStandard.cs b/src/CalculatorUITests/ScenarioStandard.cs new file mode 100644 index 00000000..8bc824a3 --- /dev/null +++ b/src/CalculatorUITests/ScenarioStandard.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Appium.Windows; +using System.Threading; +using System; + +namespace CalculatorUITests +{ + [TestClass] + public class StandardModeTests : CalculatorSession + { + private static WindowsElement header; + private static WindowsElement calculatorResult; + + [TestMethod] + public void Addition() + { + // Find the buttons by their names and click them in sequence to perform 1 + 7 = 8 + session.FindElementByName("One").Click(); + session.FindElementByName("Plus").Click(); + session.FindElementByName("Seven").Click(); + session.FindElementByName("Equals").Click(); + Assert.AreEqual("8", GetCalculatorResultText()); + } + + [TestMethod] + public void Division() + { + // Find the buttons by their accessibility ids and click them in sequence to perform 88 / 11 = 8 + session.FindElementByAccessibilityId("num8Button").Click(); + session.FindElementByAccessibilityId("num8Button").Click(); + session.FindElementByAccessibilityId("divideButton").Click(); + session.FindElementByAccessibilityId("num1Button").Click(); + session.FindElementByAccessibilityId("num1Button").Click(); + session.FindElementByAccessibilityId("equalButton").Click(); + Assert.AreEqual("8", GetCalculatorResultText()); + } + + [TestMethod] + public void Multiplication() + { + session.FindElementByAccessibilityId("num9Button").Click(); + session.FindElementByAccessibilityId("multiplyButton").Click(); + session.FindElementByAccessibilityId("num9Button").Click(); + session.FindElementByAccessibilityId("equalButton").Click(); + Assert.AreEqual("81", GetCalculatorResultText()); + } + + [TestMethod] + public void Subtraction() + { + // Find the buttons by their accessibility ids using XPath and click them in sequence to perform 9 - 1 = 8 + session.FindElementByAccessibilityId("num9Button").Click(); + session.FindElementByAccessibilityId("minusButton").Click(); + session.FindElementByAccessibilityId("num1Button").Click(); + session.FindElementByAccessibilityId("equalButton").Click(); + Assert.AreEqual("8", GetCalculatorResultText()); + } + + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + // Create session to launch a Calculator window + Setup(context); + // Identify calculator mode by locating the header + try + { + header = session.FindElementByAccessibilityId("Header"); + } + catch + { + header = session.FindElementByAccessibilityId("ContentPresenter"); + } + + // Ensure that calculator is in standard mode + if (!header.Text.Equals("Standard", StringComparison.OrdinalIgnoreCase)) + { + session.FindElementByAccessibilityId("TogglePaneButton").Click(); + Thread.Sleep(TimeSpan.FromSeconds(1)); + var splitViewPane = session.FindElementByClassName("SplitViewPane"); + splitViewPane.FindElementByName("Standard Calculator").Click(); + Thread.Sleep(TimeSpan.FromSeconds(1)); + Assert.IsTrue(header.Text.Equals("Standard", StringComparison.OrdinalIgnoreCase)); + } + + // Locate the calculatorResult element + calculatorResult = session.FindElementByAccessibilityId("CalculatorResults"); + Assert.IsNotNull(calculatorResult); + } + + [ClassCleanup] + public static void ClassCleanup() + { + TearDown(); + } + + [TestInitialize] + public void Clear() + { + session.FindElementByName("Clear").Click(); + Assert.AreEqual("0", GetCalculatorResultText()); + } + + private string GetCalculatorResultText() + { + return calculatorResult.Text.Replace("Display is", string.Empty).Trim(); + } + } +} From 9728ccf119d930ab3e9da4673a8ed7786cfe1fbc Mon Sep 17 00:00:00 2001 From: Rudy Huyn Date: Fri, 26 Apr 2019 16:07:14 -0700 Subject: [PATCH 9/9] Fix bug when the negate sign is before a space (#345) * Ignore None characters while parsing the clipboard * reformat --- .../StandardCalculatorViewModel.cpp | 125 +++++++++--------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/src/CalcViewModel/StandardCalculatorViewModel.cpp b/src/CalcViewModel/StandardCalculatorViewModel.cpp index 8b55299e..d3c71cb3 100644 --- a/src/CalcViewModel/StandardCalculatorViewModel.cpp +++ b/src/CalcViewModel/StandardCalculatorViewModel.cpp @@ -790,6 +790,12 @@ void StandardCalculatorViewModel::OnPaste(String^ pastedString, ViewMode mode) NumbersAndOperatorsEnum mappedNumOp = MapCharacterToButtonId(*it, canSendNegate); + if (mappedNumOp == NumbersAndOperatorsEnum::None) + { + ++it; + continue; + } + if (isFirstLegalChar || isPreviousOperator) { isFirstLegalChar = false; @@ -811,74 +817,71 @@ void StandardCalculatorViewModel::OnPaste(String^ pastedString, ViewMode mode) } } - if (mappedNumOp != NumbersAndOperatorsEnum::None) + switch (mappedNumOp) { - switch (mappedNumOp) + // Opening parenthesis starts a new expression and pushes negation state onto the stack + case NumbersAndOperatorsEnum::OpenParenthesis: + negateStack.push_back(sendNegate); + sendNegate = false; + break; + + // Closing parenthesis pops the negation state off the stack and sends it down to the calc engine + case NumbersAndOperatorsEnum::CloseParenthesis: + if (!negateStack.empty()) { - // Opening parenthesis starts a new expression and pushes negation state onto the stack - case NumbersAndOperatorsEnum::OpenParenthesis: - negateStack.push_back(sendNegate); - sendNegate = false; - break; - - // Closing parenthesis pops the negation state off the stack and sends it down to the calc engine - case NumbersAndOperatorsEnum::CloseParenthesis: - if (!negateStack.empty()) - { - sendNegate = negateStack.back(); - negateStack.pop_back(); - canSendNegate = true; - } - else - { - // Don't send a closing parenthesis if a matching opening parenthesis hasn't been sent already - sendCommand = false; - } - break; - - case NumbersAndOperatorsEnum::Zero: - case NumbersAndOperatorsEnum::One: - case NumbersAndOperatorsEnum::Two: - case NumbersAndOperatorsEnum::Three: - case NumbersAndOperatorsEnum::Four: - case NumbersAndOperatorsEnum::Five: - case NumbersAndOperatorsEnum::Six: - case NumbersAndOperatorsEnum::Seven: - case NumbersAndOperatorsEnum::Eight: - case NumbersAndOperatorsEnum::Nine: - processedDigit = true; - break; - - case NumbersAndOperatorsEnum::Add: - case NumbersAndOperatorsEnum::Subtract: - case NumbersAndOperatorsEnum::Multiply: - case NumbersAndOperatorsEnum::Divide: - isPreviousOperator = true; - break; + sendNegate = negateStack.back(); + negateStack.pop_back(); + canSendNegate = true; } - - if (sendCommand) + else { - sentEquals = (mappedNumOp == NumbersAndOperatorsEnum::Equals); - Command cmdenum = ConvertToOperatorsEnum(mappedNumOp); - m_standardCalculatorManager.SendCommand(cmdenum); + // Don't send a closing parenthesis if a matching opening parenthesis hasn't been sent already + sendCommand = false; + } + break; - // The CalcEngine state machine won't allow the negate command to be sent before any - // other digits, so instead a flag is set and the command is sent after the first appropriate - // command. - if (sendNegate) + case NumbersAndOperatorsEnum::Zero: + case NumbersAndOperatorsEnum::One: + case NumbersAndOperatorsEnum::Two: + case NumbersAndOperatorsEnum::Three: + case NumbersAndOperatorsEnum::Four: + case NumbersAndOperatorsEnum::Five: + case NumbersAndOperatorsEnum::Six: + case NumbersAndOperatorsEnum::Seven: + case NumbersAndOperatorsEnum::Eight: + case NumbersAndOperatorsEnum::Nine: + processedDigit = true; + break; + + case NumbersAndOperatorsEnum::Add: + case NumbersAndOperatorsEnum::Subtract: + case NumbersAndOperatorsEnum::Multiply: + case NumbersAndOperatorsEnum::Divide: + isPreviousOperator = true; + break; + } + + if (sendCommand) + { + sentEquals = (mappedNumOp == NumbersAndOperatorsEnum::Equals); + Command cmdenum = ConvertToOperatorsEnum(mappedNumOp); + m_standardCalculatorManager.SendCommand(cmdenum); + + // The CalcEngine state machine won't allow the negate command to be sent before any + // other digits, so instead a flag is set and the command is sent after the first appropriate + // command. + if (sendNegate) + { + if (canSendNegate) { - if (canSendNegate) - { - Command cmdNegate = ConvertToOperatorsEnum(NumbersAndOperatorsEnum::Negate); - m_standardCalculatorManager.SendCommand(cmdNegate); - } + Command cmdNegate = ConvertToOperatorsEnum(NumbersAndOperatorsEnum::Negate); + m_standardCalculatorManager.SendCommand(cmdNegate); + } - // Can't send negate on a leading zero, so wait until the appropriate time to send it. - if (NumbersAndOperatorsEnum::Zero != mappedNumOp && NumbersAndOperatorsEnum::Decimal != mappedNumOp) - { - sendNegate = false; - } + // Can't send negate on a leading zero, so wait until the appropriate time to send it. + if (NumbersAndOperatorsEnum::Zero != mappedNumOp && NumbersAndOperatorsEnum::Decimal != mappedNumOp) + { + sendNegate = false; } } }