diff --git a/src/CalcManager/CalcManager.vcxproj b/src/CalcManager/CalcManager.vcxproj index c14d5ed..7022cd3 100644 --- a/src/CalcManager/CalcManager.vcxproj +++ b/src/CalcManager/CalcManager.vcxproj @@ -309,6 +309,7 @@ + @@ -349,6 +350,7 @@ Create Create + diff --git a/src/CalcManager/CalcManager.vcxproj.filters b/src/CalcManager/CalcManager.vcxproj.filters index 2ca1166..5309891 100644 --- a/src/CalcManager/CalcManager.vcxproj.filters +++ b/src/CalcManager/CalcManager.vcxproj.filters @@ -89,6 +89,7 @@ CEngine + @@ -160,5 +161,6 @@ Header Files + - + \ No newline at end of file diff --git a/src/CalcManager/NumberFormattingUtils.cpp b/src/CalcManager/NumberFormattingUtils.cpp new file mode 100644 index 0000000..8fde3d9 --- /dev/null +++ b/src/CalcManager/NumberFormattingUtils.cpp @@ -0,0 +1,84 @@ +#include "pch.h" +#include "NumberFormattingUtils.h" + +using namespace std; + +namespace CalcManager::NumberFormattingUtils +{ + /// + /// Trims out any trailing zeros or decimals in the given input string + /// + /// number to trim + void TrimTrailingZeros(_Inout_ wstring& number) + { + if (number.find(L'.') == wstring::npos) + { + return; + } + + wstring::iterator iter; + for (iter = number.end() - 1;; iter--) + { + if (*iter != L'0') + { + number.erase(iter + 1, number.end()); + break; + } + } + if (*(number.end() - 1) == L'.') + { + number.erase(number.end() - 1, number.end()); + } + } + + /// + /// Get number of digits (whole number part + decimal part) + /// the number + unsigned int GetNumberDigits(wstring value) + { + TrimTrailingZeros(value); + unsigned int numberSignificantDigits = static_cast(value.size()); + if (value.find(L'.') != wstring::npos) + { + --numberSignificantDigits; + } + if (value.find(L'-') != wstring::npos) + { + --numberSignificantDigits; + } + return numberSignificantDigits; + } + + /// + /// Get number of digits (whole number part only) + /// the number + unsigned int GetNumberDigitsWholeNumberPart(double value) + { + return value == 0 ? 1 : (1 + (int)log10(abs(value))); + } + + /// + /// Rounds the given double to the given number of significant digits + /// + /// input double + /// int number of significant digits to round to + wstring RoundSignificantDigits(double num, int numSignificant) + { + wstringstream out(wstringstream::out); + out << fixed; + out.precision(numSignificant); + out << num; + return out.str(); + } + + /// + /// Convert a Number to Scientific Notation + /// + /// number to convert + wstring ToScientificNumber(double number) + { + wstringstream out(wstringstream::out); + out << scientific << number; + return out.str(); + } +} diff --git a/src/CalcManager/NumberFormattingUtils.h b/src/CalcManager/NumberFormattingUtils.h new file mode 100644 index 0000000..ab337ee --- /dev/null +++ b/src/CalcManager/NumberFormattingUtils.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace CalcManager::NumberFormattingUtils +{ + void TrimTrailingZeros(_Inout_ std::wstring& input); + unsigned int GetNumberDigits(std::wstring value); + unsigned int GetNumberDigitsWholeNumberPart(double value); + std::wstring RoundSignificantDigits(double value, int numberSignificantDigits); + std::wstring ToScientificNumber(double number); +} diff --git a/src/CalcManager/UnitConverter.cpp b/src/CalcManager/UnitConverter.cpp index bbf761b..983ef6d 100644 --- a/src/CalcManager/UnitConverter.cpp +++ b/src/CalcManager/UnitConverter.cpp @@ -7,9 +7,11 @@ #include // for std::sort #include "Command.h" #include "UnitConverter.h" +#include "NumberFormattingUtils.h" using namespace std; using namespace UnitConversionManager; +using namespace CalcManager::NumberFormattingUtils; static constexpr uint32_t EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT = 3; static constexpr uint32_t EXPECTEDSERIALIZEDUNITTOKENCOUNT = 6; @@ -178,7 +180,7 @@ void UnitConverter::SwitchActive(const wstring& newValue) swap(m_currentHasDecimal, m_returnHasDecimal); m_returnDisplay = m_currentDisplay; m_currentDisplay = newValue; - m_currentHasDecimal = (m_currentDisplay.find(L'.') != m_currentDisplay.npos); + m_currentHasDecimal = (m_currentDisplay.find(L'.') != wstring::npos); m_switchedActive = true; if (m_currencyDataLoader != nullptr && m_vmCurrencyCallback != nullptr) @@ -202,7 +204,7 @@ vector UnitConverter::StringToVector(const wstring& w, const wchar_t* d size_t delimiterIndex = w.find(delimiter); size_t startIndex = 0; vector serializedTokens = vector(); - while (delimiterIndex != w.npos) + while (delimiterIndex != wstring::npos) { serializedTokens.push_back(w.substr(startIndex, delimiterIndex - startIndex)); startIndex = delimiterIndex + (int)wcslen(delimiter); @@ -634,19 +636,19 @@ vector> UnitConverter::CalculateSuggested() wstring roundedString; if (abs(entry.value) < 100) { - roundedString = RoundSignificant(entry.value, 2); + roundedString = RoundSignificantDigits(entry.value, 2); } else if (abs(entry.value) < 1000) { - roundedString = RoundSignificant(entry.value, 1); + roundedString = RoundSignificantDigits(entry.value, 1); } else { - roundedString = RoundSignificant(entry.value, 0); + roundedString = RoundSignificantDigits(entry.value, 0); } if (stod(roundedString) != 0.0 || m_currentCategory.supportsNegative) { - TrimString(roundedString); + TrimTrailingZeros(roundedString); returnVector.push_back(make_tuple(roundedString, entry.type)); } } @@ -672,21 +674,21 @@ vector> UnitConverter::CalculateSuggested() wstring roundedString; if (abs(entry.value) < 100) { - roundedString = RoundSignificant(entry.value, 2); + roundedString = RoundSignificantDigits(entry.value, 2); } else if (abs(entry.value) < 1000) { - roundedString = RoundSignificant(entry.value, 1); + roundedString = RoundSignificantDigits(entry.value, 1); } else { - roundedString = RoundSignificant(entry.value, 0); + roundedString = RoundSignificantDigits(entry.value, 0); } // How to work out which is the best whimsical value to add to the vector? if (stod(roundedString) != 0.0) { - TrimString(roundedString); + TrimTrailingZeros(roundedString); whimsicalReturnVector.push_back(make_tuple(roundedString, entry.type)); } } @@ -843,100 +845,61 @@ void UnitConverter::Calculate() { m_returnDisplay = m_currentDisplay; m_returnHasDecimal = m_currentHasDecimal; - TrimString(m_returnDisplay); + TrimTrailingZeros(m_returnDisplay); UpdateViewModel(); return; } unordered_map conversionTable = m_ratioMap[m_fromType]; - double returnValue = stod(m_currentDisplay); - if (conversionTable[m_toType].ratio == 1.0 && conversionTable[m_toType].offset == 0.0) + if (AnyUnitIsEmpty() || (conversionTable[m_toType].ratio == 1.0 && conversionTable[m_toType].offset == 0.0)) { m_returnDisplay = m_currentDisplay; m_returnHasDecimal = m_currentHasDecimal; - TrimString(m_returnDisplay); + TrimTrailingZeros(m_returnDisplay); } else { - returnValue = Convert(returnValue, conversionTable[m_toType]); - m_returnDisplay = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED); - TrimString(m_returnDisplay); - int numPreDecimal = (int)m_returnDisplay.size(); - if (m_returnDisplay.find(L'.') != m_returnDisplay.npos) - { - numPreDecimal = (int)m_returnDisplay.find(L'.'); - } - if (returnValue < 0) - { - numPreDecimal--; - } + double currentValue = stod(m_currentDisplay); + double returnValue = Convert(currentValue, conversionTable[m_toType]); - if (numPreDecimal > MAXIMUMDIGITSALLOWED || (returnValue != 0 && abs(returnValue) < MINIMUMDECIMALALLOWED)) + auto isCurrencyConverter = m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(this->m_currentCategory); + if (isCurrencyConverter) { - wstringstream out(wstringstream::out); - out << scientific << returnValue; - m_returnDisplay = out.str(); + // We don't need to trim the value when it's a currency. + m_returnDisplay = RoundSignificantDigits(returnValue, MAXIMUMDIGITSALLOWED); + TrimTrailingZeros(m_returnDisplay); } else { - returnValue = stod(m_returnDisplay); - wstring returnString; - if (m_currentDisplay.size() <= OPTIMALDIGITSALLOWED && abs(returnValue) >= OPTIMALDECIMALALLOWED) + int numPreDecimal = GetNumberDigitsWholeNumberPart(returnValue); + if (numPreDecimal > MAXIMUMDIGITSALLOWED || (returnValue != 0 && abs(returnValue) < MINIMUMDECIMALALLOWED)) { - returnString = RoundSignificant(returnValue, OPTIMALDIGITSALLOWED - min(numPreDecimal, OPTIMALDIGITSALLOWED)); + m_returnDisplay = ToScientificNumber(returnValue); } else { - returnString = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED - min(numPreDecimal, MAXIMUMDIGITSALLOWED)); + int currentNumberSignificantDigits = GetNumberDigits(m_currentDisplay); + int precision = 0; + if (abs(returnValue) < OPTIMALDECIMALALLOWED) + { + precision = MAXIMUMDIGITSALLOWED; + } + else + { + // Fewer digits are needed following the decimal if the number is large, + // we calculate the number of decimals necessary based on the number of digits in the integer part. + precision = max(0, max(OPTIMALDIGITSALLOWED, min(MAXIMUMDIGITSALLOWED, currentNumberSignificantDigits)) - numPreDecimal); + } + + m_returnDisplay = RoundSignificantDigits(returnValue, precision); + TrimTrailingZeros(m_returnDisplay); } - m_returnDisplay = returnString; - TrimString(m_returnDisplay); + m_returnHasDecimal = (m_returnDisplay.find(L'.') != wstring::npos); } - m_returnHasDecimal = (m_returnDisplay.find(L'.') != m_returnDisplay.npos); } UpdateViewModel(); } -/// -/// Trims out any trailing zeros or decimals in the given input string -/// -/// wstring to trim -void UnitConverter::TrimString(wstring& returnString) -{ - if (returnString.find(L'.') == m_returnDisplay.npos) - { - return; - } - - wstring::iterator iter; - for (iter = returnString.end() - 1;; iter--) - { - if (*iter != L'0') - { - returnString.erase(iter + 1, returnString.end()); - break; - } - } - if (*(returnString.end() - 1) == L'.') - { - returnString.erase(returnString.end() - 1, returnString.end()); - } -} - -/// -/// Rounds the given double to the given number of significant digits -/// -/// input double -/// int number of significant digits to round to -wstring UnitConverter::RoundSignificant(double num, int numSignificant) -{ - wstringstream out(wstringstream::out); - out << fixed; - out.precision(numSignificant); - out << num; - return out.str(); -} - void UnitConverter::UpdateCurrencySymbols() { if (m_currencyDataLoader != nullptr && m_vmCurrencyCallback != nullptr) diff --git a/src/CalcManager/UnitConverter.h b/src/CalcManager/UnitConverter.h index 0d1b8ac..27b8629 100644 --- a/src/CalcManager/UnitConverter.h +++ b/src/CalcManager/UnitConverter.h @@ -279,9 +279,7 @@ namespace UnitConversionManager double Convert(double value, ConversionData conversionData); std::vector> CalculateSuggested(); void ClearValues(); - void TrimString(std::wstring& input); void InitializeSelectedUnits(); - std::wstring RoundSignificant(double num, int numSignificant); Category StringToCategory(const std::wstring& w); std::wstring CategoryToString(const Category& c, const wchar_t* delimiter); std::wstring UnitToString(const Unit& u, const wchar_t* delimiter); diff --git a/src/CalcManager/pch.h b/src/CalcManager/pch.h index 2bcedb7..9207ebf 100644 --- a/src/CalcManager/pch.h +++ b/src/CalcManager/pch.h @@ -20,3 +20,5 @@ #include #include #include +#include +#include diff --git a/src/CalculatorUnitTests/CalculatorManagerTest.cpp b/src/CalculatorUnitTests/CalculatorManagerTest.cpp index 34acafe..9696a89 100644 --- a/src/CalculatorUnitTests/CalculatorManagerTest.cpp +++ b/src/CalculatorUnitTests/CalculatorManagerTest.cpp @@ -7,9 +7,11 @@ #include "CalcManager/CalculatorHistory.h" #include "CalcViewModel/Common/EngineResourceProvider.h" +#include "CalcManager/NumberFormattingUtils.h" using namespace CalculatorApp; using namespace CalculationManager; +using namespace CalcManager::NumberFormattingUtils; using namespace Platform; using namespace std; using namespace Microsoft::VisualStudio::CppUnitTestFramework; @@ -185,6 +187,11 @@ namespace CalculatorManagerTest TEST_METHOD(CalculatorManagerTestMaxDigitsReached_LeadingDecimal); TEST_METHOD(CalculatorManagerTestMaxDigitsReached_TrailingDecimal); + TEST_METHOD(CalculatorManagerNumberFormattingUtils_TrimTrailingZeros); + TEST_METHOD(CalculatorManagerNumberFormattingUtils_GetNumberDigits); + TEST_METHOD(CalculatorManagerNumberFormattingUtils_GetNumberDigitsWholeNumberPart); + TEST_METHOD(CalculatorManagerNumberFormattingUtils_RoundSignificantDigits); + TEST_METHOD(CalculatorManagerNumberFormattingUtils_ToScientificNumber); // TODO re-enable when cause of failure is determined. Bug 20226670 // TEST_METHOD(CalculatorManagerTestBinaryOperatorReceived); // TEST_METHOD(CalculatorManagerTestBinaryOperatorReceived_Multiple); @@ -807,6 +814,102 @@ namespace CalculatorManagerTest TestMaxDigitsReachedScenario(L"123,456,789,101,112.13"); } + void CalculatorManagerTest::CalculatorManagerNumberFormattingUtils_TrimTrailingZeros() + { + wstring number = L"2.1032100000000"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"2.10321"); + number = L"-122.123200"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"-122.1232"); + number = L"0.0001200"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"0.00012"); + number = L"12.000"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"12"); + number = L"-12.00000"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"-12"); + number = L"0.000"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"0"); + number = L"322423"; + TrimTrailingZeros(number); + VERIFY_ARE_EQUAL(number, L"322423"); + } + + void CalculatorManagerTest::CalculatorManagerNumberFormattingUtils_GetNumberDigits() + { + wstring number = L"2.10321"; + unsigned int digitsCount = GetNumberDigits(number); + VERIFY_ARE_EQUAL(digitsCount, 6); + number = L"-122.1232"; + digitsCount = GetNumberDigits(number); + VERIFY_ARE_EQUAL(digitsCount, 7); + number = L"-3432"; + digitsCount = GetNumberDigits(number); + VERIFY_ARE_EQUAL(digitsCount, 4); + number = L"0"; + digitsCount = GetNumberDigits(number); + VERIFY_ARE_EQUAL(digitsCount, 1); + number = L"0.0001223"; + digitsCount = GetNumberDigits(number); + VERIFY_ARE_EQUAL(digitsCount, 8); + } + + void CalculatorManagerTest::CalculatorManagerNumberFormattingUtils_GetNumberDigitsWholeNumberPart() + { + unsigned int digitsCount = GetNumberDigitsWholeNumberPart(2.10321); + VERIFY_ARE_EQUAL(digitsCount, 1); + digitsCount = GetNumberDigitsWholeNumberPart(-122.1232); + VERIFY_ARE_EQUAL(digitsCount, 3); + digitsCount = GetNumberDigitsWholeNumberPart(-3432); + VERIFY_ARE_EQUAL(digitsCount, 4); + digitsCount = GetNumberDigitsWholeNumberPart(0); + VERIFY_ARE_EQUAL(digitsCount, 1); + digitsCount = GetNumberDigitsWholeNumberPart(324328412837382); + VERIFY_ARE_EQUAL(digitsCount, 15); + digitsCount = GetNumberDigitsWholeNumberPart(324328412837382.232213214324234); + VERIFY_ARE_EQUAL(digitsCount, 15); + } + + void CalculatorManagerTest::CalculatorManagerNumberFormattingUtils_RoundSignificantDigits() + { + wstring result = RoundSignificantDigits(12.342343242, 3); + VERIFY_ARE_EQUAL(result, L"12.342"); + result = RoundSignificantDigits(12.3429999, 3); + VERIFY_ARE_EQUAL(result, L"12.343"); + result = RoundSignificantDigits(12.342500001, 3); + VERIFY_ARE_EQUAL(result, L"12.343"); + result = RoundSignificantDigits(-2312.1244243346454345, 5); + VERIFY_ARE_EQUAL(result, L"-2312.12442"); + result = RoundSignificantDigits(0.3423432423, 5); + VERIFY_ARE_EQUAL(result, L"0.34234"); + result = RoundSignificantDigits(0.3423, 7); + VERIFY_ARE_EQUAL(result, L"0.3423000"); + } + + void CalculatorManagerTest::CalculatorManagerNumberFormattingUtils_ToScientificNumber() + { + wstring result = ToScientificNumber(3423); + VERIFY_ARE_EQUAL(result, L"3.423000e+03"); + result = ToScientificNumber(-21); + VERIFY_ARE_EQUAL(result, L"-2.100000e+01"); + result = ToScientificNumber(0.0232); + VERIFY_ARE_EQUAL(result, L"2.320000e-02"); + result = ToScientificNumber(-0.00921); + VERIFY_ARE_EQUAL(result, L"-9.210000e-03"); + result = ToScientificNumber(2343243345677); + VERIFY_ARE_EQUAL(result, L"2.343243e+12"); + result = ToScientificNumber(-3432474247332942); + VERIFY_ARE_EQUAL(result, L"-3.432474e+15"); + result = ToScientificNumber(0.000000003432432); + VERIFY_ARE_EQUAL(result, L"3.432432e-09"); + result = ToScientificNumber(-0.000000003432432); + VERIFY_ARE_EQUAL(result, L"-3.432432e-09"); + } + // TODO re-enable when cause of failure is determined. Bug 20226670 // void CalculatorManagerTest::CalculatorManagerTestBinaryOperatorReceived() // { diff --git a/src/CalculatorUnitTests/UnitConverterTest.cpp b/src/CalculatorUnitTests/UnitConverterTest.cpp index 3f3559d..fc2655f 100644 --- a/src/CalculatorUnitTests/UnitConverterTest.cpp +++ b/src/CalculatorUnitTests/UnitConverterTest.cpp @@ -441,7 +441,7 @@ namespace UnitConverterUnitTests s_unitConverter->SendCommand(Command::Six); s_unitConverter->SendCommand(Command::Seven); s_unitConverter->SendCommand(Command::Eight); - VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"12345678"), wstring(L"27217528.63236"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"12345678"), wstring(L"27217529"))); } // Test large values