// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "pch.h" #include "Command.h" #include "UnitConverter.h" using namespace concurrency; using namespace std; using namespace UnitConversionManager; static constexpr uint32_t EXPECTEDSERIALIZEDTOKENCOUNT = 7; static constexpr uint32_t EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT = 3; static constexpr uint32_t EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT = 3; static constexpr uint32_t EXPECTEDSERIALIZEDUNITTOKENCOUNT = 6; static constexpr uint32_t EXPECTEDSTATEDATATOKENCOUNT = 5; static constexpr uint32_t EXPECTEDMAPCOMPONENTTOKENCOUNT = 2; static constexpr int32_t MAXIMUMDIGITSALLOWED = 15; static constexpr int32_t OPTIMALDIGITSALLOWED = 7; static constexpr wchar_t LEFTESCAPECHAR = L'{'; static constexpr wchar_t RIGHTESCAPECHAR = L'}'; static const double OPTIMALDECIMALALLOWED = pow(10, -1 * (OPTIMALDIGITSALLOWED - 1)); static const double MINIMUMDECIMALALLOWED = pow(10, -1 * (MAXIMUMDIGITSALLOWED - 1)); unordered_map quoteConversions; unordered_map unquoteConversions; /// /// Constructor, sets up all the variables and requires a configLoader /// /// An instance of the IConverterDataLoader interface which we use to read in category/unit names and conversion data UnitConverter::UnitConverter(_In_ const shared_ptr& dataLoader) : UnitConverter::UnitConverter(dataLoader, nullptr) { } /// /// Constructor, sets up all the variables and requires two configLoaders /// /// An instance of the IConverterDataLoader interface which we use to read in category/unit names and conversion data /// An instance of the IConverterDataLoader interface, specialized for loading currency data from an internet service UnitConverter::UnitConverter(_In_ const shared_ptr& dataLoader, _In_ const shared_ptr& currencyDataLoader) { m_dataLoader = dataLoader; m_currencyDataLoader = currencyDataLoader; // declaring the delimiter character conversion map quoteConversions[L'|'] = L"{p}"; quoteConversions[L'['] = L"{lc}"; quoteConversions[L']'] = L"{rc}"; quoteConversions[L':'] = L"{co}"; quoteConversions[L','] = L"{cm}"; quoteConversions[L';'] = L"{sc}"; quoteConversions[LEFTESCAPECHAR] = L"{lb}"; quoteConversions[RIGHTESCAPECHAR] = L"{rb}"; unquoteConversions[L"{p}"] = L'|'; unquoteConversions[L"{lc}"] = L'['; unquoteConversions[L"{rc}"] = L']'; unquoteConversions[L"{co}"] = L':'; unquoteConversions[L"{cm}"] = L','; unquoteConversions[L"{sc}"] = L';'; unquoteConversions[L"{lb}"] = LEFTESCAPECHAR; unquoteConversions[L"{rb}"] = RIGHTESCAPECHAR; Reset(); } void UnitConverter::Initialize() { m_dataLoader->LoadData(); } bool UnitConverter::CheckLoad() { if (m_categories.empty()) { Reset(); } return !m_categories.empty(); } /// /// Returns a list of the categories in use by this converter /// vector UnitConverter::GetCategories() { CheckLoad(); return m_categories; } /// /// Sets the current category in use by this converter, /// and returns a list of unit types that exist under the given category. /// /// Category struct which we are setting CategorySelectionInitializer UnitConverter::SetCurrentCategory(const Category& input) { if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(input)) { m_currencyDataLoader->LoadData(); } vector newUnitList{}; if (CheckLoad()) { if (m_currentCategory.id != input.id) { vector& unitVector = m_categoryToUnits[m_currentCategory]; for (unsigned int i = 0; i < unitVector.size(); i++) { unitVector[i].isConversionSource = (unitVector[i].id == m_fromType.id); unitVector[i].isConversionTarget = (unitVector[i].id == m_toType.id); } m_currentCategory = input; if (!m_currentCategory.supportsNegative && m_currentDisplay.front() == L'-') { m_currentDisplay.erase(0, 1); } } newUnitList = m_categoryToUnits[input]; } InitializeSelectedUnits(); return make_tuple(newUnitList, m_fromType, m_toType); } /// /// Gets the category currently being used /// Category UnitConverter::GetCurrentCategory() { return m_currentCategory; } /// /// Sets the current unit types to be used, indicates a likely change in the /// display values, so we re-calculate and callback the updated values /// /// Unit struct which the user is modifying /// Unit struct we are converting to void UnitConverter::SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) { if (!CheckLoad()) { return; } m_fromType = fromType; m_toType = toType; Calculate(); UpdateCurrencySymbols(); UpdateViewModel(); } /// /// Switches the active field, indicating that we are now entering data into /// what was originally the return field, and storing results into what was /// originally the current field. We swap appropriate values, /// but do not callback, as values have not changed. /// /// /// wstring representing the value user had in the field they've just activated. /// We use this to handle cases where the front-end may choose to trim more digits /// than we have been storing internally, in which case appending will not function /// as expected without the use of this parameter. /// void UnitConverter::SwitchActive(const wstring& newValue) { if (!CheckLoad()) { return; } swap(m_fromType, m_toType); swap(m_currentHasDecimal, m_returnHasDecimal); m_returnDisplay = m_currentDisplay; m_currentDisplay = newValue; m_currentHasDecimal = (m_currentDisplay.find(L'.') != m_currentDisplay.npos); m_switchedActive = true; if (m_currencyDataLoader != nullptr && m_vmCurrencyCallback != nullptr) { shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); const pair currencyRatios = currencyDataLoader->GetCurrencyRatioEquality(m_fromType, m_toType); m_vmCurrencyCallback->CurrencyRatiosCallback(currencyRatios.first, currencyRatios.second); } } wstring UnitConverter::CategoryToString(const Category& c, const wchar_t * delimiter) { wstringstream out(wstringstream::out); out << Quote(std::to_wstring(c.id)) << delimiter << Quote(std::to_wstring(c.supportsNegative)) << delimiter << Quote(c.name) << delimiter; return out.str(); } vector UnitConverter::StringToVector(const wstring& w, const wchar_t * delimiter, bool addRemainder) { size_t delimiterIndex = w.find(delimiter); size_t startIndex = 0; vector serializedTokens = vector(); while (delimiterIndex != w.npos) { serializedTokens.push_back(w.substr(startIndex, delimiterIndex - startIndex)); startIndex = delimiterIndex + (int)wcslen(delimiter); delimiterIndex = w.find(delimiter, startIndex); } if (addRemainder) { delimiterIndex = w.size(); serializedTokens.push_back(w.substr(startIndex, delimiterIndex - startIndex)); } return serializedTokens; } Category UnitConverter::StringToCategory(const wstring& w) { vector tokenList = StringToVector(w, L";"); assert(tokenList.size() == EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT); Category serializedCategory; serializedCategory.id = _wtoi(Unquote(tokenList[0]).c_str()); serializedCategory.supportsNegative = (tokenList[1].compare(L"1") == 0); serializedCategory.name = Unquote(tokenList[2]); return serializedCategory; } wstring UnitConverter::UnitToString(const Unit& u, const wchar_t * delimiter) { wstringstream out(wstringstream::out); out << Quote(std::to_wstring(u.id)) << delimiter << Quote(u.name) << delimiter << Quote(u.abbreviation) << delimiter << std::to_wstring(u.isConversionSource) << delimiter << std::to_wstring(u.isConversionTarget) << delimiter << std::to_wstring(u.isWhimsical) << delimiter; return out.str(); } Unit UnitConverter::StringToUnit(const wstring& w) { vector tokenList = StringToVector(w, L";"); assert(tokenList.size() == EXPECTEDSERIALIZEDUNITTOKENCOUNT); Unit serializedUnit; serializedUnit.id = _wtoi(Unquote(tokenList[0]).c_str()); serializedUnit.name = Unquote(tokenList[1]); serializedUnit.accessibleName = serializedUnit.name; serializedUnit.abbreviation = Unquote(tokenList[2]); serializedUnit.isConversionSource = (tokenList[3].compare(L"1") == 0); serializedUnit.isConversionTarget = (tokenList[4].compare(L"1") == 0); serializedUnit.isWhimsical = (tokenList[5].compare(L"1") == 0); return serializedUnit; } ConversionData UnitConverter::StringToConversionData(const wstring& w) { vector tokenList = StringToVector(w, L";"); assert(tokenList.size() == EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT); ConversionData serializedConversionData; serializedConversionData.ratio = stod(Unquote(tokenList[0]).c_str()); serializedConversionData.offset = stod(Unquote(tokenList[1]).c_str()); serializedConversionData.offsetFirst = (tokenList[2].compare(L"1") == 0); return serializedConversionData; } wstring UnitConverter::ConversionDataToString(ConversionData d, const wchar_t * delimiter) { wstringstream out(wstringstream::out); out.precision(32); out << fixed << d.ratio; wstring ratio = out.str(); out.str(L""); out << fixed << d.offset; wstring offset = out.str(); out.str(L""); TrimString(ratio); TrimString(offset); out << Quote(ratio) << delimiter << Quote(offset) << delimiter << std::to_wstring(d.offsetFirst) << delimiter; return out.str(); } /// /// Serializes the data in the converter and returns it as a string /// wstring UnitConverter::Serialize() { if (!CheckLoad()) { return wstring(); } wstringstream out(wstringstream::out); const wchar_t * delimiter = L";"; out << UnitToString(m_fromType, delimiter) << "|"; out << UnitToString(m_toType, delimiter) << "|"; out << CategoryToString(m_currentCategory, delimiter) << "|"; out << std::to_wstring(m_currentHasDecimal) << delimiter << std::to_wstring(m_returnHasDecimal) << delimiter << std::to_wstring(m_switchedActive) << delimiter; out << m_currentDisplay << delimiter << m_returnDisplay << delimiter << "|"; wstringstream categoryString(wstringstream::out); wstringstream categoryToUnitString(wstringstream::out); wstringstream unitToUnitToDoubleString(wstringstream::out); for (const Category& c : m_categories) { categoryString << CategoryToString(c, delimiter) << ","; } for (const auto& cur : m_categoryToUnits) { categoryToUnitString << CategoryToString(cur.first, delimiter) << "["; for (const Unit& u : cur.second) { categoryToUnitString << UnitToString(u, delimiter) << ","; } categoryToUnitString << "[" << "]"; } for (const auto& cur : m_ratioMap) { unitToUnitToDoubleString << UnitToString(cur.first, delimiter) << "["; for (const auto& curConversion : cur.second) { unitToUnitToDoubleString << UnitToString(curConversion.first, delimiter) << ":"; unitToUnitToDoubleString << ConversionDataToString(curConversion.second, delimiter) << ":,"; } unitToUnitToDoubleString << "[" << "]"; } out << categoryString.str() << "|"; out << categoryToUnitString.str() << "|"; out << unitToUnitToDoubleString.str() << "|"; wstring test = out.str(); return test; } /// /// De-Serializes the data in the converter from a string /// /// wstring holding the serialized data. If it does not have expected number of parameters, we will ignore it void UnitConverter::DeSerialize(const wstring& serializedData) { Reset(); if (serializedData.empty()) { return; } vector outerTokens = StringToVector(serializedData, L"|"); assert(outerTokens.size() == EXPECTEDSERIALIZEDTOKENCOUNT); m_fromType = StringToUnit(outerTokens[0]); m_toType = StringToUnit(outerTokens[1]); m_currentCategory = StringToCategory(outerTokens[2]); vector stateDataTokens = StringToVector(outerTokens[3], L";"); assert(stateDataTokens.size() == EXPECTEDSTATEDATATOKENCOUNT); m_currentHasDecimal = (stateDataTokens[0].compare(L"1") == 0); m_returnHasDecimal = (stateDataTokens[1].compare(L"1") == 0); m_switchedActive = (stateDataTokens[2].compare(L"1") == 0); m_currentDisplay = stateDataTokens[3]; m_returnDisplay = stateDataTokens[4]; vector categoryListTokens = StringToVector(outerTokens[4], L","); for (wstring token : categoryListTokens) { m_categories.push_back(StringToCategory(token)); } vector unitVectorTokens = StringToVector(outerTokens[5], L"]"); for (wstring unitVector : unitVectorTokens) { vector mapcomponents = StringToVector(unitVector, L"["); assert(mapcomponents.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); Category key = StringToCategory(mapcomponents[0]); vector units = StringToVector(mapcomponents[1], L","); for (wstring unit : units) { m_categoryToUnits[key].push_back(StringToUnit(unit)); } } vector ratioMapTokens = StringToVector(outerTokens[6], L"]"); for (wstring token : ratioMapTokens) { vector ratioMapComponentTokens = StringToVector(token, L"["); assert(ratioMapComponentTokens.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); Unit key = StringToUnit(ratioMapComponentTokens[0]); vector ratioMapList = StringToVector(ratioMapComponentTokens[1], L","); for (wstring subtoken : ratioMapList) { vector ratioMapSubComponentTokens = StringToVector(subtoken, L":"); assert(ratioMapSubComponentTokens.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); Unit subkey = StringToUnit(ratioMapSubComponentTokens[0]); ConversionData conversion = StringToConversionData(ratioMapSubComponentTokens[1]); m_ratioMap[key][subkey] = conversion; } } UpdateViewModel(); } /// /// De-Serializes the data in the converter from a string /// /// wstring holding the serialized data. If it does not have expected number of parameters, we will ignore it void UnitConverter::RestoreUserPreferences(const wstring& userPreferences) { if (userPreferences.empty()) { return; } vector outerTokens = StringToVector(userPreferences, L"|"); if (outerTokens.size() == 3) { m_fromType = StringToUnit(outerTokens[0]); m_toType = StringToUnit(outerTokens[1]); m_currentCategory = StringToCategory(outerTokens[2]); } } /// /// Serializes the Category and Associated Units in the converter and returns it as a string /// wstring UnitConverter::SaveUserPreferences() { wstringstream out(wstringstream::out); const wchar_t * delimiter = L";"; out << UnitToString(m_fromType, delimiter) << "|"; out << UnitToString(m_toType, delimiter) << "|"; out << CategoryToString(m_currentCategory, delimiter) << "|"; wstring test = out.str(); return test; } /// /// Sanitizes the input string, escape quoting any symbols we rely on for our delimiters, and returns the sanitized string. /// /// wstring to be sanitized wstring UnitConverter::Quote(const wstring& s) { wstringstream quotedString(wstringstream::out); // Iterate over the delimiter characters we need to quote wstring::const_iterator cursor = s.begin(); while(cursor != s.end()) { if (quoteConversions.find(*cursor) != quoteConversions.end()) { quotedString << quoteConversions[*cursor]; } else { quotedString << *cursor; } ++cursor; } return quotedString.str(); } /// /// Unsanitizes the sanitized input string, returning it to its original contents before we had quoted it. /// /// wstring to be unsanitized wstring UnitConverter::Unquote(const wstring& s) { wstringstream quotedSubString(wstringstream::out); wstringstream unquotedString(wstringstream::out); wstring::const_iterator cursor = s.begin(); while(cursor != s.end()) { if(*cursor == LEFTESCAPECHAR) { quotedSubString.str(L""); while (cursor != s.end() && *cursor != RIGHTESCAPECHAR) { quotedSubString << *cursor; ++cursor; } if (cursor == s.end()) { // Badly formatted break; } else { quotedSubString << *cursor; unquotedString << unquoteConversions[quotedSubString.str()]; } } else { unquotedString << *cursor; } ++cursor; } return unquotedString.str(); } /// /// Handles inputs to the converter from the view-model, corresponding to a given button or keyboard press /// /// Command enum representing the command that was entered void UnitConverter::SendCommand(Command command) { if (!CheckLoad()) { return; } // TODO: Localization of characters bool clearFront = false; if (m_currentDisplay == L"0") { clearFront = true; } bool clearBack = false; if ((m_currentHasDecimal && m_currentDisplay.size() - 1 >= MAXIMUMDIGITSALLOWED) || (!m_currentHasDecimal && m_currentDisplay.size() >= MAXIMUMDIGITSALLOWED)) { clearBack = true; } if (command != Command::Negate && m_switchedActive) { ClearValues(); m_switchedActive = false; clearFront = true; clearBack = false; } switch (command) { case Command::Zero: m_currentDisplay += L"0"; break; case Command::One: m_currentDisplay += L"1"; break; case Command::Two: m_currentDisplay += L"2"; break; case Command::Three: m_currentDisplay += L"3"; break; case Command::Four: m_currentDisplay += L"4"; break; case Command::Five: m_currentDisplay += L"5"; break; case Command::Six: m_currentDisplay += L"6"; break; case Command::Seven: m_currentDisplay += L"7"; break; case Command::Eight: m_currentDisplay += L"8"; break; case Command::Nine: m_currentDisplay += L"9"; break; case Command::Decimal: clearFront = false; clearBack = false; if (!m_currentHasDecimal) { m_currentDisplay += L"."; m_currentHasDecimal = true; } break; case Command::Backspace: clearFront = false; clearBack = false; if ((m_currentDisplay.front() != '-' && m_currentDisplay.size() > 1) || m_currentDisplay.size() > 2) { if (m_currentDisplay.back() == '.') { m_currentHasDecimal = false; } m_currentDisplay.pop_back(); } else { m_currentDisplay = L"0"; m_currentHasDecimal = false; } break; case Command::Negate: clearFront = false; clearBack = false; if (m_currentCategory.supportsNegative) { if (m_currentDisplay.front() == '-') { m_currentDisplay.erase(0, 1); } else { m_currentDisplay.insert(0, 1, '-'); } } break; case Command::Clear: clearFront = false; clearBack = false; ClearValues(); break; case Command::Reset: clearFront = false; clearBack = false; ClearValues(); Reset(); break; default: break; } if (clearFront) { m_currentDisplay.erase(0, 1); } if (clearBack) { m_currentDisplay.erase(m_currentDisplay.size() - 1, 1); m_vmCallback->MaxDigitsReached(); } Calculate(); UpdateViewModel(); } /// /// Sets the callback interface to send display update calls to /// /// instance of IDisplayCallback interface that receives our update calls void UnitConverter::SetViewModelCallback(_In_ const shared_ptr& newCallback) { m_vmCallback = newCallback; if (CheckLoad()) { UpdateViewModel(); } } void UnitConverter::SetViewModelCurrencyCallback(_In_ const shared_ptr& newCallback) { m_vmCurrencyCallback = newCallback; shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); if (currencyDataLoader != nullptr) { currencyDataLoader->SetViewModelCallback(newCallback); } } task> UnitConverter::RefreshCurrencyRatios() { shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); return create_task([this, currencyDataLoader]() { if (currencyDataLoader != nullptr) { return currencyDataLoader->TryLoadDataFromWebOverrideAsync(); } else { return task_from_result(false); } }).then([this, currencyDataLoader](bool didLoad) { wstring timestamp = L""; if (currencyDataLoader != nullptr) { timestamp = currencyDataLoader->GetCurrencyTimestamp(); } return make_pair(didLoad, timestamp); }, task_continuation_context::use_default()); } shared_ptr UnitConverter::GetCurrencyConverterDataLoader() { return dynamic_pointer_cast(m_currencyDataLoader); } /// /// Converts a double value into another unit type, currently by multiplying by the given double ratio /// /// double input value to convert /// double conversion ratio to use double UnitConverter::Convert(double value, ConversionData conversionData) { if (conversionData.offsetFirst) { return (value + conversionData.offset) * conversionData.ratio; } else { return (value * conversionData.ratio) + conversionData.offset; } } /// /// Calculates the suggested values for the current display value and returns them as a vector /// vector> UnitConverter::CalculateSuggested() { if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(m_currentCategory)) { return vector>(); } vector> returnVector; vector intermediateVector; vector intermediateWhimsicalVector; unordered_map ratios = m_ratioMap[m_fromType]; // Calculate converted values for every other unit type in this category, along with their magnitude for (const auto& cur : ratios) { if (cur.first != m_fromType && cur.first != m_toType) { double convertedValue = Convert(stod(m_currentDisplay), cur.second); SuggestedValueIntermediate newEntry; newEntry.magnitude = log10(convertedValue); newEntry.value = convertedValue; newEntry.type = cur.first; if(newEntry.type.isWhimsical == false) intermediateVector.push_back(newEntry); else intermediateWhimsicalVector.push_back(newEntry); } } // Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value sort(intermediateVector.begin(), intermediateVector.end(), [] (SuggestedValueIntermediate first, SuggestedValueIntermediate second) { if (abs(first.magnitude) == abs(second.magnitude)) { return first.magnitude > second.magnitude; } else { return abs(first.magnitude) < abs(second.magnitude); } }); // Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings for (const auto& entry : intermediateVector) { wstring roundedString; if (abs(entry.value) < 100) { roundedString = RoundSignificant(entry.value, 2); } else if (abs(entry.value) < 1000) { roundedString = RoundSignificant(entry.value, 1); } else { roundedString = RoundSignificant(entry.value, 0); } if (stod(roundedString) != 0.0 || m_currentCategory.supportsNegative) { TrimString(roundedString); returnVector.push_back(make_tuple(roundedString, entry.type)); } } // The Whimsicals are determined differently // Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value sort(intermediateWhimsicalVector.begin(), intermediateWhimsicalVector.end(), [] (SuggestedValueIntermediate first, SuggestedValueIntermediate second) { if (abs(first.magnitude) == abs(second.magnitude)) { return first.magnitude > second.magnitude; } else { return abs(first.magnitude) < abs(second.magnitude); } }); // Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings vector> whimsicalReturnVector; for (const auto& entry : intermediateWhimsicalVector) { wstring roundedString; if (abs(entry.value) < 100) { roundedString = RoundSignificant(entry.value, 2); } else if (abs(entry.value) < 1000) { roundedString = RoundSignificant(entry.value, 1); } else { roundedString = RoundSignificant(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); whimsicalReturnVector.push_back(make_tuple(roundedString, entry.type)); } } // Pickup the 'best' whimsical value - currently the first one if (whimsicalReturnVector.size() != 0) { returnVector.push_back(whimsicalReturnVector.at(0)); } // return returnVector; } /// /// Resets the converter to its initial state /// void UnitConverter::Reset() { m_categories = m_dataLoader->LoadOrderedCategories(); ClearValues(); m_switchedActive = false; if (m_categories.empty()) { return; } m_currentCategory = m_categories[0]; m_categoryToUnits.clear(); m_ratioMap.clear(); bool readyCategoryFound = false; for (const Category& category : m_categories) { shared_ptr activeDataLoader = GetDataLoaderForCategory(category); if (activeDataLoader == nullptr) { // The data loader is different depending on the category, e.g. currency data loader // is different from the static data loader. // If there is no data loader for this category, continue. continue; } vector units = activeDataLoader->LoadOrderedUnits(category); m_categoryToUnits[category] = units; // Just because the units are empty, doesn't mean the user can't select this category, // we just want to make sure we don't let an unready category be the default. if (!units.empty()) { for (Unit u : units) { m_ratioMap[u] = activeDataLoader->LoadOrderedRatios(u); } if (!readyCategoryFound) { m_currentCategory = category; readyCategoryFound = true; } } } InitializeSelectedUnits(); Calculate(); } /// /// Sets the active data loader based on the input category. /// shared_ptr UnitConverter::GetDataLoaderForCategory(const Category& category) { if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(category)) { return m_currencyDataLoader; } else { return m_dataLoader; } } /// /// Sets the initial values for m_fromType and m_toType. /// This is an internal helper method as opposed to SetCurrentUnits /// which is for external use by clients. /// If we fail to set units, we will fallback to the EMPTY_UNIT. /// void UnitConverter::InitializeSelectedUnits() { if (m_categoryToUnits.empty()) { return; } auto itr = m_categoryToUnits.find(m_currentCategory); if (itr == m_categoryToUnits.end()) { return; } vector curUnits = itr->second; if (!curUnits.empty()) { bool conversionSourceSet = false; bool conversionTargetSet = false; for (const Unit& cur : curUnits) { if (!conversionSourceSet && cur.isConversionSource) { m_fromType = cur; conversionSourceSet = true; } if (!conversionTargetSet && cur.isConversionTarget) { m_toType = cur; conversionTargetSet = true; } if (conversionSourceSet && conversionTargetSet) { return; } } } m_fromType = EMPTY_UNIT; m_toType = EMPTY_UNIT; } /// /// Resets the value fields to 0 /// void UnitConverter::ClearValues() { m_currentHasDecimal = false; m_returnHasDecimal = false; m_currentDisplay = L"0"; } /// /// Checks if either unit is EMPTY_UNIT. /// bool UnitConverter::AnyUnitIsEmpty() { return m_fromType == EMPTY_UNIT || m_toType == EMPTY_UNIT; } /// /// Calculates a new return value based on the current display value /// void UnitConverter::Calculate() { unordered_map conversionTable = m_ratioMap[m_fromType]; double returnValue = stod(m_currentDisplay); if (AnyUnitIsEmpty() || (conversionTable[m_toType].ratio == 1.0 && conversionTable[m_toType].offset == 0.0)) { m_returnDisplay = m_currentDisplay; TrimString(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--; } if (numPreDecimal > MAXIMUMDIGITSALLOWED || (returnValue != 0 && abs(returnValue) < MINIMUMDECIMALALLOWED)) { wstringstream out(wstringstream::out); out << scientific << returnValue; m_returnDisplay = out.str(); } else { returnValue = stod(m_returnDisplay); wstring returnString; if (m_currentDisplay.size() <= OPTIMALDIGITSALLOWED && abs(returnValue) >= OPTIMALDECIMALALLOWED) { returnString = RoundSignificant(returnValue, OPTIMALDIGITSALLOWED - min(numPreDecimal, OPTIMALDIGITSALLOWED)); } else { returnString = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED - min(numPreDecimal, MAXIMUMDIGITSALLOWED)); } m_returnDisplay = returnString; TrimString(m_returnDisplay); } } m_returnHasDecimal = (m_returnDisplay.find(L'.') != m_returnDisplay.npos); } /// /// 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) { shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); const pair currencySymbols = currencyDataLoader->GetCurrencySymbols(m_fromType, m_toType); const pair currencyRatios = currencyDataLoader->GetCurrencyRatioEquality(m_fromType, m_toType); m_vmCurrencyCallback->CurrencySymbolsCallback(currencySymbols.first, currencySymbols.second); m_vmCurrencyCallback->CurrencyRatiosCallback(currencyRatios.first, currencyRatios.second); } } void UnitConverter::UpdateViewModel() { m_vmCallback->DisplayCallback(m_currentDisplay, m_returnDisplay); m_vmCallback->SuggestedValueCallback(CalculateSuggested()); }