From 93fdfe02c6352526c04748b6d6c7d587498822b4 Mon Sep 17 00:00:00 2001 From: "Dr.Rx" Date: Wed, 22 May 2019 11:33:30 -0400 Subject: [PATCH] Enable converters --- src/Calculator.Droid/Calculator.Droid.csproj | 1 + .../CalcManager/CalculatorList.cs | 60 +- .../CalculatorObservableCollection.cs | 19 + .../CalcManager/UnitConversionManager.cs | 1456 ++++++++++++- .../Calculator.Shared.projitems | 12 + .../Common/AlwaysSelectedCollectionView.cs | 5 + .../Common/Automation/NarratorNotifier.cs | 62 + .../Common/ConversionResultTaskHelper.cs | 44 + .../Common/LocalizationService.cs | 4 +- .../Common/LocalizationSettings.cs | 2 +- src/Calculator.Shared/Common/NavCategory.cs | 14 +- .../Common/NetworkManager.cs | 68 + src/Calculator.Shared/Common/TraceLogger.cs | 9 +- src/Calculator.Shared/Common/Utils.cs | 47 +- .../Common/ValidSelectedItemConverter.cs | 37 + .../Controls/CalculationResult.cs | 9 +- .../HorizontalNoOverflowStackPanel.cs | 73 + .../Controls/SupplementaryItemsControl.cs | 17 +- .../AlwaysSelectedCollectionViewConverter.cs | 63 +- .../Converters/VisibilityNegationConverter.cs | 7 +- .../DataLoaders/CurrencyDataLoader.cs | 792 +++++++ .../DataLoaders/CurrencyHttpClient.cs | 53 + .../DataLoaders/ICurrencyHttpClient.cs | 18 + .../DataLoaders/UnitConverterDataConstants.cs | 170 ++ .../DataLoaders/UnitConverterDataLoader.cs | 1322 ++++++++++++ .../ViewModels/ApplicationViewModel.cs | 54 +- .../ViewModels/HistoryViewModel.cs | 8 +- .../ViewModels/StandardCalculatorViewModel.cs | 9 +- .../ViewModels/UnitConverterViewModel.cs | 1851 ++++++++++++++++- src/Calculator.Shared/Views/MainPage.xaml | 4 +- src/Calculator.Shared/Views/MainPage.xaml.cs | 45 +- .../Views/StateTriggers/AspectRatioTrigger.cs | 8 +- .../Views/SupplementaryResults.xaml | 125 +- .../Views/SupplementaryResults.xaml.cs | 107 +- .../Views/UnitConverter.xaml | 1220 +++++------ .../Views/UnitConverter.xaml.cs | 371 +++- .../_adapters/StringExtensions.cs | 45 + src/Calculator.iOS/Calculator.iOS.csproj | 1 + 38 files changed, 7255 insertions(+), 957 deletions(-) create mode 100644 src/Calculator.Shared/CalcManager/CalculatorObservableCollection.cs create mode 100644 src/Calculator.Shared/Common/Automation/NarratorNotifier.cs create mode 100644 src/Calculator.Shared/Common/ConversionResultTaskHelper.cs create mode 100644 src/Calculator.Shared/Common/NetworkManager.cs create mode 100644 src/Calculator.Shared/Common/ValidSelectedItemConverter.cs create mode 100644 src/Calculator.Shared/Controls/HorizontalNoOverflowStackPanel.cs create mode 100644 src/Calculator.Shared/DataLoaders/CurrencyDataLoader.cs create mode 100644 src/Calculator.Shared/DataLoaders/CurrencyHttpClient.cs create mode 100644 src/Calculator.Shared/DataLoaders/ICurrencyHttpClient.cs create mode 100644 src/Calculator.Shared/DataLoaders/UnitConverterDataConstants.cs create mode 100644 src/Calculator.Shared/DataLoaders/UnitConverterDataLoader.cs create mode 100644 src/Calculator.Shared/_adapters/StringExtensions.cs diff --git a/src/Calculator.Droid/Calculator.Droid.csproj b/src/Calculator.Droid/Calculator.Droid.csproj index b9661773..799ce13e 100644 --- a/src/Calculator.Droid/Calculator.Droid.csproj +++ b/src/Calculator.Droid/Calculator.Droid.csproj @@ -20,6 +20,7 @@ Properties\AndroidManifest.xml True ..\Calculator.Shared\Strings + 7.3 true diff --git a/src/Calculator.Shared/CalcManager/CalculatorList.cs b/src/Calculator.Shared/CalcManager/CalculatorList.cs index 00b58e90..5b77304c 100644 --- a/src/Calculator.Shared/CalcManager/CalculatorList.cs +++ b/src/Calculator.Shared/CalcManager/CalculatorList.cs @@ -5,7 +5,7 @@ using System.Text; namespace CalculatorApp { - public class CalculatorList : IEnumerable, IEnumerable + public class CalculatorList : IEnumerable, IEnumerable { List m_vector; @@ -19,7 +19,22 @@ namespace CalculatorApp m_vector = new List(source.m_vector); } - public bool GetAt(int index, out TType item) + public TType this[int index] + { + get => m_vector[index]; + } + + public TType this[uint index] + { + get => m_vector[(int)index]; + } + + public TType At(int index) + { + return m_vector[index]; + } + + public bool GetAt(int index, out TType item) { item = m_vector[index]; return true; @@ -70,12 +85,27 @@ namespace CalculatorApp return true; } - public bool Append(TType item) + public void Add(TType item) { m_vector.Add(item); - return true; } + public bool Append(TType item) + { + m_vector.Add(item); + return true; + } + + public void EmplaceBack(TType item) + { + m_vector.Add(item); + } + + public void PushBack(TType item) + { + m_vector.Add(item); + } + public bool RemoveAtEnd() { m_vector.RemoveAt(m_vector.Count - 1); @@ -88,6 +118,28 @@ namespace CalculatorApp return true; } + public bool IsEmpty() + { + return m_vector.Count == 0; + } + + public uint Size() + { + return (uint)m_vector.Count; + } + + public void Sort(Func comparison) + { + m_vector.Sort((t1, t2) => comparison(t1, t2) ? -1 : 1); + } + + public int IndexOf(TType item) + { + return m_vector.IndexOf(item); + } + + public int Count => m_vector.Count; + public bool GetString(out string expression) { // UNO TODO diff --git a/src/Calculator.Shared/CalcManager/CalculatorObservableCollection.cs b/src/Calculator.Shared/CalcManager/CalculatorObservableCollection.cs new file mode 100644 index 00000000..0e02de9a --- /dev/null +++ b/src/Calculator.Shared/CalcManager/CalculatorObservableCollection.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace CalculatorApp +{ + public class CalculatorObservableCollection : ObservableCollection + { + public void Append(TType item) + { + Add(item); + } + + public TType GetAt(int index) + { + return this[index]; + } + } +} \ No newline at end of file diff --git a/src/Calculator.Shared/CalcManager/UnitConversionManager.cs b/src/Calculator.Shared/CalcManager/UnitConversionManager.cs index d193f26e..6aeaec41 100644 --- a/src/Calculator.Shared/CalcManager/UnitConversionManager.cs +++ b/src/Calculator.Shared/CalcManager/UnitConversionManager.cs @@ -1,10 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Calculator; +using CalculatorApp; +using CategorySelectionInitializer = System.Tuple, UnitConversionManager.Unit, UnitConversionManager.Unit>; +using UnitToUnitToConversionDataMap = System.Collections.Generic.Dictionary>; +using CategoryToUnitVectorMap = System.Collections.Generic.Dictionary>; namespace UnitConversionManager { - public struct Unit + public class Unit { - public Unit(int id, string name, string abbreviation, bool isConversionSource, bool isConversionTarget, bool isWhimsical) + public static readonly Unit EMPTY_UNIT = new Unit(-1, "", "", true, true, false); + + public Unit() + { + } + + public Unit(int id, string name, string abbreviation, bool isConversionSource, bool isConversionTarget, bool isWhimsical) { this.id = id; this.name = name; @@ -37,22 +57,1420 @@ namespace UnitConversionManager accessibleName = nameValue1 + " " + nameValue2; } - public readonly int id; - public readonly string name; - public readonly string accessibleName; - public readonly string abbreviation; - public readonly bool isConversionSource; - public readonly bool isConversionTarget; - public readonly bool isWhimsical; + public int id; + public string name; + public string accessibleName; + public string abbreviation; + public bool isConversionSource; + public bool isConversionTarget; + public bool isWhimsical; - // bool operator !=(Unit that) const - // { - // return that.id != id; - // } + /// + public override bool Equals(object obj) + => obj is Unit other && id == other.id; - //bool operator ==(Unit that) const - // { - // return that.id == id; - // } - } -} \ No newline at end of file + public override int GetHashCode() + => id; + + public static bool operator !=(Unit left, Unit right) + { + return left.id != right.id; + } + + public static bool operator ==(Unit left, Unit right) + { + return left.id == right.id; + } + + // class UnitHash + public static explicit operator int(Unit x) + { + return x.id; + } + } + + public struct Category + { + public Category(int id, string name, bool supportsNegative) + { + this.id = id; + this.name = name; + this.supportsNegative = supportsNegative; + } + + public int id; + public string name; + public bool supportsNegative; + + /// + public override bool Equals(object obj) + => obj is Category other + && id == other.id; + + /// + public override int GetHashCode() + => id; + + public static bool operator!=(Category left, Category right) + { + return left.id != right.id; + } + + public static bool operator ==(Category left, Category right) + { + return left.id == right.id; + } + + // class CategoryHash + public static explicit operator int(Category x) + { + return x.id; + } + } + + public struct SuggestedValueIntermediate + { + public double magnitude; + public double value; + public Unit type; + }; + + public class ConversionData + { + public ConversionData() + { + } + + public ConversionData(double ratio, double offset, bool offsetFirst) + { + this.ratio = ratio; + this.offset = offset; + this.offsetFirst = offsetFirst; + } + + public double ratio; + public double offset; + public bool offsetFirst; + }; + + public struct CurrencyStaticData + { + public CurrencyStaticData( + string countryCode, + string countryName, + string currencyCode, + string currencyName, + string currencySymbol) + { + this.countryCode = countryCode; + this.countryName = countryName; + this.currencyCode = currencyCode; + this.currencyName = currencyName; + this.currencySymbol = currencySymbol; + } + + public string countryCode; + public string countryName; + public string currencyCode; + public string currencyName; + public string currencySymbol; + }; + + public struct CurrencyRatio + { + public CurrencyRatio( + double ratio, + string sourceCurrencyCode, + string targetCurrencyCode) + { + this.ratio = ratio; + this.sourceCurrencyCode = sourceCurrencyCode; + this.targetCurrencyCode = targetCurrencyCode; + } + + public double ratio; + public string sourceCurrencyCode; + public string targetCurrencyCode; + }; + + // typedef std.tuple, UnitConversionManager.Unit, UnitConversionManager.Unit> CategorySelectionInitializer; + // typedef std.unordered_map< + // UnitConversionManager.Unit, + // std.unordered_map, + // UnitConversionManager.UnitHash> + // UnitToUnitToConversionDataMap; + // typedef std.unordered_map, UnitConversionManager.CategoryHash> + // CategoryToUnitVectorMap; + // + + public interface IViewModelCurrencyCallback + { + void CurrencyDataLoadFinished(bool didLoad); + void CurrencySymbolsCallback(string fromSymbol, string toSymbol); + void CurrencyRatiosCallback(string ratioEquality, string accRatioEquality); + void CurrencyTimestampCallback(string timestamp, bool isWeekOldData); + void NetworkBehaviorChanged( int newBehavior); + }; + + public interface IConverterDataLoader + { + void LoadData(); // prepare data if necessary before calling other functions + CalculatorList LoadOrderedCategories(); + CalculatorList LoadOrderedUnits(Category c); + Dictionary LoadOrderedRatios(Unit u); + bool SupportsCategory(Category target); + }; + + public interface ICurrencyConverterDataLoader + { + void SetViewModelCallback(IViewModelCurrencyCallback callback); + KeyValuePair GetCurrencySymbols( Unit unit1, Unit unit2); + KeyValuePair GetCurrencyRatioEquality( Unit unit1, Unit unit2); + string GetCurrencyTimestamp(); + + Task TryLoadDataFromCacheAsync(); + Task TryLoadDataFromWebAsync(); + Task TryLoadDataFromWebOverrideAsync(); + }; + + public interface IUnitConverterVMCallback + { + void DisplayCallback(string from, string to); + void SuggestedValueCallback(CalculatorList> suggestedValues); + void MaxDigitsReached(); + }; + + public interface IUnitConverter + { + void Initialize(); // Use to initialize first time, use deserialize instead to rehydrate + CalculatorList GetCategories(); + CategorySelectionInitializer SetCurrentCategory(Category input); + Category GetCurrentCategory(); + void SetCurrentUnitTypes(Unit fromType, Unit toType); + void SwitchActive(string newValue); + string Serialize(); + void DeSerialize(string serializedData); + string SaveUserPreferences(); + void RestoreUserPreferences( string userPreferences); + void SendCommand(Command command); + void SetViewModelCallback(IUnitConverterVMCallback newCallback); + void SetViewModelCurrencyCallback(IViewModelCurrencyCallback newCallback); + Task> RefreshCurrencyRatios(); + void Calculate(); + void ResetCategoriesAndRatios(); + }; + + // .h + public partial class UnitConverter : IUnitConverter//, public std.enable_shared_from_this + { + //public UnitConverter(IConverterDataLoader dataLoader); + //public UnitConverter(IConverterDataLoader dataLoader, IConverterDataLoader currencyDataLoader); + + // IUnitConverter + //public partial void Initialize() ; + //public partial CalculatorList GetCategories() ; + //public partial CategorySelectionInitializer SetCurrentCategory(Category input) ; + //public partial Category GetCurrentCategory() ; + //public partial void SetCurrentUnitTypes(Unit fromType, Unit toType) ; + //public partial void SwitchActive(string newValue) ; + //public partial string Serialize() ; + //public partial void DeSerialize(string serializedData) ; + //public partial string SaveUserPreferences() ; + //public partial void RestoreUserPreferences(string userPreference) ; + //public partial void SendCommand(Command command) ; + //public partial void SetViewModelCallback( IUnitConverterVMCallback newCallback) ; + //public partial void SetViewModelCurrencyCallback( IViewModelCurrencyCallback newCallback) ; + //public partial Task> RefreshCurrencyRatios() ; + //public partial void Calculate() ; + //public partial void ResetCategoriesAndRatios() ; + //// IUnitConverter + + //static CalculatorList StringToVector(string w, char delimiter, bool addRemainder = false); + //static string Quote(string s); + //static string Unquote(string s); + + //private: + // bool CheckLoad(); + // double Convert(double value, ConversionData conversionData); + // CalculatorList> CalculateSuggested(); + // void ClearValues(); + // void TrimString(string& input); + // void InitializeSelectedUnits(); + // string RoundSignificant(double num, int numSignificant); + // Category StringToCategory(string w); + // string CategoryToString(Category c, char delimiter); + // string UnitToString(Unit u, char delimiter); + // Unit StringToUnit(string w); + // ConversionData StringToConversionData(string w); + // string ConversionDataToString(ConversionData d, char delimiter); + // void UpdateCurrencySymbols(); + // void UpdateViewModel(); + // bool AnyUnitIsEmpty(); + // std.IConverterDataLoader GetDataLoaderForCategory(Category category); + // std.ICurrencyConverterDataLoader GetCurrencyConverterDataLoader(); + + private IConverterDataLoader m_dataLoader; + private IConverterDataLoader m_currencyDataLoader; + private IUnitConverterVMCallback m_vmCallback; + private IViewModelCurrencyCallback m_vmCurrencyCallback; + private CalculatorList m_categories = new CalculatorList(); + private CategoryToUnitVectorMap m_categoryToUnits = new CategoryToUnitVectorMap(EqualityComparer.Default); + private UnitToUnitToConversionDataMap m_ratioMap = new UnitToUnitToConversionDataMap(EqualityComparer.Default); + private Category m_currentCategory; + private Unit m_fromType = Unit.EMPTY_UNIT; + private Unit m_toType = Unit.EMPTY_UNIT; + private string m_currentDisplay; + private string m_returnDisplay; + private bool m_currentHasDecimal; + private bool m_returnHasDecimal; + private bool m_switchedActive; + }; + + // .cpp + partial class UnitConverter + { + private static readonly Unit EMPTY_UNIT = Unit.EMPTY_UNIT; + + const uint EXPECTEDSERIALIZEDTOKENCOUNT = 7; + const uint EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT = 3; + const uint EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT = 3; + const uint EXPECTEDSERIALIZEDUNITTOKENCOUNT = 6; + const uint EXPECTEDSTATEDATATOKENCOUNT = 5; + const uint EXPECTEDMAPCOMPONENTTOKENCOUNT = 2; + + static int MAXIMUMDIGITSALLOWED = 15; + static int OPTIMALDIGITSALLOWED = 7; + + static char LEFTESCAPECHAR = '{'; + static char RIGHTESCAPECHAR = '}'; + + static readonly double OPTIMALDECIMALALLOWED = Math.Pow(10, -1 * (OPTIMALDIGITSALLOWED - 1)); + static readonly double MINIMUMDECIMALALLOWED = Math.Pow(10, -1 * (MAXIMUMDIGITSALLOWED - 1)); + + Dictionary quoteConversions = new Dictionary(); + Dictionary unquoteConversions = new Dictionary(); + + /// + /// 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 + public UnitConverter( IConverterDataLoader dataLoader) + : this(dataLoader, null) + { + } + + /// + /// 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 + public UnitConverter( IConverterDataLoader dataLoader, IConverterDataLoader currencyDataLoader) + { + m_dataLoader = dataLoader; + m_currencyDataLoader = currencyDataLoader; + // declaring the delimiter character conversion map + quoteConversions['|'] = "{p}"; + quoteConversions['['] = "{lc}"; + quoteConversions[']'] = "{rc}"; + quoteConversions[':'] = "{co}"; + quoteConversions[','] = "{cm}"; + quoteConversions[';'] = "{sc}"; + quoteConversions[LEFTESCAPECHAR] = "{lb}"; + quoteConversions[RIGHTESCAPECHAR] = "{rb}"; + unquoteConversions["{p}"] = '|'; + unquoteConversions["{lc}"] = '['; + unquoteConversions["{rc}"] = ']'; + unquoteConversions["{co}"] = ':'; + unquoteConversions["{cm}"] = ','; + unquoteConversions["{sc}"] = ';'; + unquoteConversions["{lb}"] = LEFTESCAPECHAR; + unquoteConversions["{rb}"] = RIGHTESCAPECHAR; + ClearValues(); + ResetCategoriesAndRatios(); + } + + public void Initialize() + { + m_dataLoader.LoadData(); + } + + bool CheckLoad() + { + if (m_categories.IsEmpty()) + { + ResetCategoriesAndRatios(); + } + return !m_categories.IsEmpty(); + } + + /// + /// Returns a list of the categories in use by this converter + /// + public CalculatorList 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 + public CategorySelectionInitializer SetCurrentCategory(Category input) + { + if (m_currencyDataLoader != null && m_currencyDataLoader.SupportsCategory(input)) + { + m_currencyDataLoader.LoadData(); + } + + CalculatorList newUnitList = new CalculatorList(); + if (CheckLoad()) + { + if (m_currentCategory.id != input.id) + { + CalculatorList unitVector = m_categoryToUnits[m_currentCategory]; + for (uint 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.First() == '-') + { + m_currentDisplay = m_currentDisplay.Remove(0, 1); + } + } + + newUnitList = m_categoryToUnits[input]; + } + + InitializeSelectedUnits(); + return Tuple.Create(newUnitList, m_fromType, m_toType); + } + + /// + /// Gets the category currently being used + /// + public Category 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 + public void SetCurrentUnitTypes(Unit fromType, Unit toType) + { + if (!CheckLoad()) + { + return; + } + + m_fromType = fromType; + m_toType = toType; + Calculate(); + + UpdateCurrencySymbols(); + } + + /// + /// 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. + /// + /// + /// string 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. + /// + public void SwitchActive(string newValue) + { + if (!CheckLoad()) + { + return; + } + + Utils.Swap(ref m_fromType, ref m_toType); + Utils.Swap(ref m_currentHasDecimal, ref m_returnHasDecimal); + m_returnDisplay = m_currentDisplay; + m_currentDisplay = newValue; + m_currentHasDecimal = (m_currentDisplay.IndexOf('.') != m_currentDisplay.npos()); + m_switchedActive = true; + + if (m_currencyDataLoader != null && m_vmCurrencyCallback != null) + { + ICurrencyConverterDataLoader currencyDataLoader = GetCurrencyConverterDataLoader(); + KeyValuePair currencyRatios = currencyDataLoader.GetCurrencyRatioEquality(m_fromType, m_toType); + + m_vmCurrencyCallback.CurrencyRatiosCallback(currencyRatios.Key, currencyRatios.Value); + } + } + + string CategoryToString(Category c, char delimiter) + { + using (var @out = new StringWriter()) + { + + @out.Write(Quote(c.id.ToString())); + @out.Write(delimiter); + @out.Write(Quote(c.supportsNegative.ToString())); + @out.Write(delimiter); + @out.Write(Quote(c.name)); + @out.Write(delimiter); + + return @out.ToString(); + } + } + + public static CalculatorList StringToVector(string w, char delimiter, bool addRemainder = false) + { + int delimiterIndex = w.IndexOf(delimiter); + int startIndex = 0; + CalculatorList serializedTokens = new CalculatorList(); + while (delimiterIndex != w.npos()) + { + serializedTokens.PushBack(w.substr(startIndex, delimiterIndex - startIndex)); + startIndex = delimiterIndex + 1 /*(int)wcslen(delimiter)*/; + delimiterIndex = w.IndexOf(delimiter, startIndex); + } + if (addRemainder) + { + delimiterIndex = w.Length; + serializedTokens.PushBack(w.substr(startIndex, delimiterIndex - startIndex)); + } + return serializedTokens; + } + + public static CalculatorList StringToVector(string w, string delimiter, bool addRemainder = false) + { + int delimiterIndex = w.IndexOf(delimiter); + int startIndex = 0; + CalculatorList serializedTokens = new CalculatorList(); + while (delimiterIndex != w.npos()) + { + serializedTokens.PushBack(w.substr(startIndex, delimiterIndex - startIndex)); + startIndex = delimiterIndex + 1 /*(int)wcslen(delimiter)*/; + delimiterIndex = w.IndexOf(delimiter, startIndex); + } + if (addRemainder) + { + delimiterIndex = w.Length; + serializedTokens.PushBack(w.substr(startIndex, delimiterIndex - startIndex)); + } + return serializedTokens; + } + + Category StringToCategory(string w) + { + CalculatorList tokenList = StringToVector(w, ';'); + Debug.Assert(tokenList.Size() == EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT); + Category serializedCategory = new Category(); + serializedCategory.id = System.Convert.ToInt32(Unquote(tokenList[0])); + serializedCategory.supportsNegative = (tokenList[1].CompareTo("1") == 0); + serializedCategory.name = Unquote(tokenList[2]); + return serializedCategory; + } + + string UnitToString(Unit u, char delimiter) + { + using (var @out = new StringWriter()) + { + @out.Write(Quote(u.id.ToString())); + @out.Write(delimiter); + @out.Write(Quote(u.name)); + @out.Write(delimiter); + @out.Write(Quote(u.abbreviation)); + @out.Write(delimiter); + @out.Write(u.isConversionSource.ToString()); + @out.Write(delimiter); + @out.Write(u.isConversionTarget.ToString()); + @out.Write(delimiter); + @out.Write(u.isWhimsical.ToString()); + @out.Write(delimiter); + + return @out.ToString(); + } + } + + Unit StringToUnit(string w) + { + CalculatorList tokenList = StringToVector(w, ';'); + Debug.Assert(tokenList.Size() == EXPECTEDSERIALIZEDUNITTOKENCOUNT); + Unit serializedUnit = new Unit(); + serializedUnit.id = System.Convert.ToInt32(Unquote(tokenList[0])); + serializedUnit.name = Unquote(tokenList[1]); + serializedUnit.accessibleName = serializedUnit.name; + serializedUnit.abbreviation = Unquote(tokenList[2]); + serializedUnit.isConversionSource = (tokenList[3].CompareTo("1") == 0); + serializedUnit.isConversionTarget = (tokenList[4].CompareTo("1") == 0); + serializedUnit.isWhimsical = (tokenList[5].CompareTo("1") == 0); + return serializedUnit; + } + + ConversionData StringToConversionData(string w) + { + CalculatorList tokenList = StringToVector(w, ';'); + Debug.Assert(tokenList.Size() == EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT); + ConversionData serializedConversionData = new ConversionData(); + serializedConversionData.ratio = System.Convert.ToDouble(Unquote(tokenList[0])); + serializedConversionData.offset = System.Convert.ToDouble(Unquote(tokenList[1])); + serializedConversionData.offsetFirst = (tokenList[2].CompareTo("1") == 0); + return serializedConversionData; + } + + string ConversionDataToString(ConversionData d, char delimiter) + { + using (var @out = new StringWriter()) + { + string ratio = d.ratio.ToString("F32", CultureInfo.InvariantCulture); + string offset = d.offset.ToString("F32", CultureInfo.InvariantCulture); + TrimString(ref ratio); + TrimString(ref offset); + @out.Write(Quote(ratio)); + @out.Write(delimiter); + @out.Write(Quote(offset)); + @out.Write(delimiter); + @out.Write(d.offsetFirst); + @out.Write(delimiter); + + return @out.ToString(); + } + } + + /// + /// Serializes the data in the converter and returns it as a string + /// + public string Serialize() + { + if (!CheckLoad()) + { + return string.Empty; + } + + using (var @out = new StringWriter()) + { + char delimiter = ';'; + + @out.Write(UnitToString(m_fromType, delimiter)); + @out.Write('|'); + @out.Write(UnitToString(m_toType, delimiter)); + @out.Write('|'); + @out.Write(CategoryToString(m_currentCategory, delimiter)); + @out.Write('|'); + @out.Write(m_currentHasDecimal); + @out.Write(delimiter); + @out.Write(m_returnHasDecimal); + @out.Write(delimiter); + @out.Write(m_switchedActive); + @out.Write(delimiter); + @out.Write(m_currentDisplay); + @out.Write(delimiter); + @out.Write(m_returnDisplay); + @out.Write(delimiter); + @out.Write('|'); + + using (var categoryString = new StringWriter()) + using (var categoryToUnitString = new StringWriter()) + using (var unitToUnitToDoubleString = new StringWriter()) + { + foreach (Category c in m_categories) + { + categoryString.Write(CategoryToString(c, delimiter)); + categoryString.Write(','); + } + + foreach (var cur in m_categoryToUnits) + { + categoryToUnitString.Write(CategoryToString(cur.Key, delimiter)); + categoryToUnitString.Write('['); + foreach (Unit u in cur.Value) + { + categoryToUnitString.Write(UnitToString(u, delimiter)); + categoryToUnitString.Write(','); + } + categoryToUnitString.Write("[]"); + } + + foreach (var cur in m_ratioMap) + { + unitToUnitToDoubleString.Write(UnitToString(cur.Key, delimiter)); + categoryToUnitString.Write('['); + foreach (var curConversion in cur.Value) + { + unitToUnitToDoubleString.Write(UnitToString(curConversion.Key, delimiter)); + unitToUnitToDoubleString.Write(':'); + unitToUnitToDoubleString.Write(ConversionDataToString(curConversion.Value, delimiter)); + unitToUnitToDoubleString.Write(":,"); + } + unitToUnitToDoubleString.Write("[]"); + } + + @out.Write(categoryString.ToString()); + @out.Write('|'); + @out.Write(categoryToUnitString.ToString()); + @out.Write('|'); + @out.Write(unitToUnitToDoubleString.ToString()); + @out.Write('|'); + + return @out.ToString(); + } + } + } + + /// + /// De-Serializes the data in the converter from a string + /// + /// string holding the serialized data. If it does not have expected number of parameters, we will ignore it + public void DeSerialize(string serializedData) + { + ClearValues(); + ResetCategoriesAndRatios(); + + if (string.IsNullOrEmpty(serializedData)) + { + return; + } + + CalculatorList outerTokens = StringToVector(serializedData, '|'); + Debug.Assert(outerTokens.Size() == EXPECTEDSERIALIZEDTOKENCOUNT); + m_fromType = StringToUnit(outerTokens[0]); + m_toType = StringToUnit(outerTokens[1]); + m_currentCategory = StringToCategory(outerTokens[2]); + CalculatorList stateDataTokens = StringToVector(outerTokens[3], ';'); + Debug.Assert(stateDataTokens.Size() == EXPECTEDSTATEDATATOKENCOUNT); + m_currentHasDecimal = (stateDataTokens[0].CompareTo('1') == 0); + m_returnHasDecimal = (stateDataTokens[1].CompareTo('1') == 0); + m_switchedActive = (stateDataTokens[2].CompareTo('1') == 0); + m_currentDisplay = stateDataTokens[3]; + m_returnDisplay = stateDataTokens[4]; + CalculatorList categoryListTokens = StringToVector(outerTokens[4], ','); + foreach (string token in categoryListTokens) + { + m_categories.PushBack(StringToCategory(token)); + } + CalculatorList unitVectorTokens = StringToVector(outerTokens[5], ']'); + foreach (string unitVector in unitVectorTokens) + { + CalculatorList mapcomponents = StringToVector(unitVector, '['); + Debug.Assert(mapcomponents.Size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); + Category key = StringToCategory(mapcomponents[0]); + CalculatorList units = StringToVector(mapcomponents[1], ','); + foreach (string unit in units) + { + m_categoryToUnits[key].PushBack(StringToUnit(unit)); + } + } + CalculatorList ratioMapTokens = StringToVector(outerTokens[6], ']'); + foreach (string token in ratioMapTokens) + { + CalculatorList ratioMapComponentTokens = StringToVector(token, '['); + Debug.Assert(ratioMapComponentTokens.Size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); + Unit key = StringToUnit(ratioMapComponentTokens[0]); + CalculatorList ratioMapList = StringToVector(ratioMapComponentTokens[1], ','); + foreach (string subtoken in ratioMapList) + { + CalculatorList ratioMapSubComponentTokens = StringToVector(subtoken, ':'); + Debug.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 + /// + /// string holding the serialized data. If it does not have expected number of parameters, we will ignore it + public void RestoreUserPreferences(string userPreferences) + { + if (string.IsNullOrEmpty(userPreferences)) + { + return; + } + + CalculatorList outerTokens = StringToVector(userPreferences, '|'); + if (outerTokens.Size() != 3) + { + return; + } + + var fromType = StringToUnit(outerTokens[0]); + var toType = StringToUnit(outerTokens[1]); + m_currentCategory = StringToCategory(outerTokens[2]); + + // Only restore from the saved units if they are valid in the current available units. + if (m_categoryToUnits.TryGetValue(m_currentCategory, out var curUnits)) + { + if (curUnits.IndexOf(fromType) != curUnits.Count) + { + m_fromType = fromType; + } + if (curUnits.IndexOf(toType) != curUnits.Count) + { + m_toType = toType; + } + } + } + + /// + /// Serializes the Category and Associated Units in the converter and returns it as a string + /// + public string SaveUserPreferences() + { + char delimiter = ';'; + + using (var writer = new StringWriter()) + { + writer.Write(UnitToString(m_fromType, delimiter)); + writer.Write('|'); + writer.Write(UnitToString(m_toType, delimiter)); + writer.Write('|'); + writer.Write(CategoryToString(m_currentCategory, delimiter)); + writer.Write('|'); + + return writer.ToString(); + } + } + + /// + /// Sanitizes the input string, escape quoting any symbols we rely on for our delimiters, and returns the sanitized string. + /// + /// string to be sanitized + public string Quote(string s) + { + using (var quotedString = new StringWriter()) + using (var cursor = s.GetEnumerator()) + { + while (cursor.MoveNext()) + { + if (quoteConversions.TryGetValue(cursor.Current, out var conversion)) + { + quotedString.Write(conversion); + } + else + { + quotedString.Write(cursor.Current); + } + } + + return quotedString.ToString(); + } + } + + /// + /// Unsanitizes the sanitized input string, returning it to its original contents before we had quoted it. + /// + /// string to be unsanitized + public string Unquote(string s) + { + using (var unquotedString = new StringWriter()) + using (var cursor = s.GetEnumerator()) + { + while (cursor.MoveNext()) + { + if (cursor.Current == LEFTESCAPECHAR) + { + using (var quotedSubString = new StringWriter()) + { + while (cursor.MoveNext() && cursor.Current != RIGHTESCAPECHAR) + { + quotedSubString.Write(cursor.Current); + } + + if (unquoteConversions.TryGetValue(quotedSubString.ToString(), out var conversion)) + { + unquotedString.Write(conversion); + } + else + { + // Badly formatted + break; + } + } + } + else + { + unquotedString.Write(cursor.Current); + } + } + + return unquotedString.ToString(); + } + } + + /// + /// 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 + public void SendCommand(Command command) + { + if (!CheckLoad()) + { + return; + } + + // TODO: Localization of characters + bool clearFront = false; + if (m_currentDisplay == "0") + { + clearFront = true; + } + bool clearBack = false; + if ((m_currentHasDecimal && m_currentDisplay.Length - 1 >= MAXIMUMDIGITSALLOWED) + || (!m_currentHasDecimal && m_currentDisplay.Length >= MAXIMUMDIGITSALLOWED)) + { + clearBack = true; + } + if (command != Command.Negate && m_switchedActive) + { + ClearValues(); + m_switchedActive = false; + clearFront = true; + clearBack = false; + } + switch (command) + { + case Command.Zero: + m_currentDisplay += "0"; + break; + + case Command.One: + m_currentDisplay += "1"; + break; + + case Command.Two: + m_currentDisplay += "2"; + break; + + case Command.Three: + m_currentDisplay += "3"; + break; + + case Command.Four: + m_currentDisplay += "4"; + break; + + case Command.Five: + m_currentDisplay += "5"; + break; + + case Command.Six: + m_currentDisplay += "6"; + break; + + case Command.Seven: + m_currentDisplay += "7"; + break; + + case Command.Eight: + m_currentDisplay += "8"; + break; + + case Command.Nine: + m_currentDisplay += "9"; + break; + + case Command.Decimal: + clearFront = false; + clearBack = false; + if (!m_currentHasDecimal) + { + m_currentDisplay += "."; + m_currentHasDecimal = true; + } + break; + + case Command.Backspace: + clearFront = false; + clearBack = false; + if ((m_currentDisplay.First() != '-' && m_currentDisplay.Length > 1) || m_currentDisplay.Length > 2) + { + if (m_currentDisplay.Last() == '.') + { + m_currentHasDecimal = false; + } + m_currentDisplay = m_currentDisplay.Remove(m_currentDisplay.Length - 1); + } + else + { + m_currentDisplay = "0"; + m_currentHasDecimal = false; + } + break; + + case Command.Negate: + clearFront = false; + clearBack = false; + if (m_currentCategory.supportsNegative) + { + if (m_currentDisplay.First() == '-') + { + m_currentDisplay = m_currentDisplay.Remove(0, 1); + } + else + { + m_currentDisplay = m_currentDisplay.Insert(0, "-"); + } + } + break; + + case Command.Clear: + clearFront = false; + clearBack = false; + ClearValues(); + break; + + case Command.Reset: + clearFront = false; + clearBack = false; + ClearValues(); + ResetCategoriesAndRatios(); + break; + + default: + break; + } + + if (clearFront) + { + m_currentDisplay = m_currentDisplay.Remove(0, 1); + } + if (clearBack) + { + m_currentDisplay = m_currentDisplay.Remove(m_currentDisplay.Length - 1, 1); + m_vmCallback.MaxDigitsReached(); + } + + Calculate(); + } + + /// + /// Sets the callback interface to send display update calls to + /// + /// instance of IDisplayCallback interface that receives our update calls + public void SetViewModelCallback( IUnitConverterVMCallback newCallback) + { + m_vmCallback = newCallback; + if (CheckLoad()) + { + UpdateViewModel(); + } + } + + public void SetViewModelCurrencyCallback( IViewModelCurrencyCallback newCallback) + { + m_vmCurrencyCallback = newCallback; + + ICurrencyConverterDataLoader currencyDataLoader = GetCurrencyConverterDataLoader(); + if (currencyDataLoader != null) + { + currencyDataLoader.SetViewModelCallback(newCallback); + } + } + + public async Task> RefreshCurrencyRatios() + { + ICurrencyConverterDataLoader currencyDataLoader = GetCurrencyConverterDataLoader(); + bool didLoad; + if (currencyDataLoader != null) + { + didLoad = await currencyDataLoader.TryLoadDataFromWebOverrideAsync(); + } + else + { + didLoad = false; + } + + string timestamp = ""; + if (currencyDataLoader != null) + { + timestamp = currencyDataLoader.GetCurrencyTimestamp(); + } + + return new KeyValuePair(didLoad, timestamp); + } + + ICurrencyConverterDataLoader GetCurrencyConverterDataLoader() + { + return (ICurrencyConverterDataLoader)(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 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 + /// + CalculatorList> CalculateSuggested() + { + if (m_currencyDataLoader != null && m_currencyDataLoader.SupportsCategory(m_currentCategory)) + { + return new CalculatorList>(); + } + + CalculatorList> returnVector = new CalculatorList>(); + CalculatorList intermediateVector = new CalculatorList(); + CalculatorList intermediateWhimsicalVector = new CalculatorList(); + + Dictionary ratios = m_ratioMap[m_fromType]; + // Calculate converted values for every other unit type in this category, along with their magnitude + foreach (var cur in ratios) + { + if (cur.Key != m_fromType && cur.Key != m_toType) + { + double convertedValue = Convert(System.Convert.ToDouble(m_currentDisplay), cur.Value); + SuggestedValueIntermediate newEntry; + newEntry.magnitude = Math.Log10(convertedValue); + newEntry.value = convertedValue; + newEntry.type = cur.Key; + if (newEntry.type.isWhimsical == false) + intermediateVector.PushBack(newEntry); + else + intermediateWhimsicalVector.PushBack(newEntry); + } + } + + // Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value + intermediateVector.Sort((SuggestedValueIntermediate first, SuggestedValueIntermediate second) => { + if (Math.Abs(first.magnitude) == Math.Abs(second.magnitude)) + { + return first.magnitude > second.magnitude; + } + else + { + return Math.Abs(first.magnitude) < Math.Abs(second.magnitude); + } + }); + + // Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings + foreach (var entry in intermediateVector) + { + string roundedString; + if (Math.Abs(entry.value) < 100) + { + roundedString = RoundSignificant(entry.value, 2); + } + else if (Math.Abs(entry.value) < 1000) + { + roundedString = RoundSignificant(entry.value, 1); + } + else + { + roundedString = RoundSignificant(entry.value, 0); + } + if (System.Convert.ToDouble(roundedString) != 0.0 || m_currentCategory.supportsNegative) + { + TrimString(ref roundedString); + returnVector.PushBack(Tuple.Create(roundedString, entry.type)); + } + } + + // The Whimsicals are determined differently + // Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value + intermediateWhimsicalVector.Sort((SuggestedValueIntermediate first, SuggestedValueIntermediate second) => { + if (Math.Abs(first.magnitude) == Math.Abs(second.magnitude)) + { + return first.magnitude > second.magnitude; + } + else + { + return Math.Abs(first.magnitude) < Math.Abs(second.magnitude); + } + }); + + // Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings + CalculatorList> whimsicalReturnVector = new CalculatorList>(); + + foreach (var entry in intermediateWhimsicalVector) + { + string roundedString; + if (Math.Abs(entry.value) < 100) + { + roundedString = RoundSignificant(entry.value, 2); + } + else if (Math.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 (System.Convert.ToDouble(roundedString) != 0.0) + { + TrimString(ref roundedString); + whimsicalReturnVector.PushBack(Tuple.Create(roundedString, entry.type)); + } + } + // Pickup the 'best' whimsical value - currently the first one + if (whimsicalReturnVector.Size() != 0) + { + returnVector.PushBack(whimsicalReturnVector.At(0)); + } + + return returnVector; + } + + /// + /// Resets categories and ratios + /// + public void ResetCategoriesAndRatios() + { + m_categories = m_dataLoader.LoadOrderedCategories(); + + m_switchedActive = false; + + if (m_categories.IsEmpty()) + { + return; + } + + m_currentCategory = m_categories[0]; + + m_categoryToUnits.Clear(); + m_ratioMap.Clear(); + bool readyCategoryFound = false; + foreach (Category category in m_categories) + { + IConverterDataLoader activeDataLoader = GetDataLoaderForCategory(category); + if (activeDataLoader == null) + { + // 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; + } + + CalculatorList 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.IsEmpty()) + { + foreach (Unit u in units) + { + m_ratioMap[u] = activeDataLoader.LoadOrderedRatios(u); + } + + if (!readyCategoryFound) + { + m_currentCategory = category; + readyCategoryFound = true; + } + } + } + + InitializeSelectedUnits(); + } + + /// + /// Sets the active data loader based on the input category. + /// + IConverterDataLoader GetDataLoaderForCategory(Category category) + { + if (m_currencyDataLoader != null && 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 InitializeSelectedUnits() + { + if (m_categoryToUnits.Count == 0) + { + return; + } + + if (!m_categoryToUnits.TryGetValue(m_currentCategory, out var curUnits)) + { + return; + } + + if (!curUnits.IsEmpty()) + { + bool conversionSourceSet = false; + bool conversionTargetSet = false; + foreach (Unit cur in 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 ClearValues() + { + m_currentHasDecimal = false; + m_returnHasDecimal = false; + m_currentDisplay = "0"; + } + + /// + /// Checks if either unit is EMPTY_UNIT. + /// + bool AnyUnitIsEmpty() + { + return m_fromType == EMPTY_UNIT || m_toType == EMPTY_UNIT; + } + + /// + /// Calculates a new return value based on the current display value + /// + public void Calculate() + { + if (AnyUnitIsEmpty()) + { + m_returnDisplay = m_currentDisplay; + m_returnHasDecimal = m_currentHasDecimal; + TrimString(ref m_returnDisplay); + UpdateViewModel(); + return; + } + + Dictionary conversionTable = m_ratioMap[m_fromType]; + double returnValue = System.Convert.ToDouble(m_currentDisplay); + if (conversionTable[m_toType].ratio == 1.0 && conversionTable[m_toType].offset == 0.0) + { + m_returnDisplay = m_currentDisplay; + m_returnHasDecimal = m_currentHasDecimal; + TrimString(ref m_returnDisplay); + } + else + { + returnValue = Convert(returnValue, conversionTable[m_toType]); + m_returnDisplay = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED); + TrimString(ref m_returnDisplay); + int numPreDecimal = (int)m_returnDisplay.size(); + if (m_returnDisplay.IndexOf('.') != m_returnDisplay.npos()) + { + numPreDecimal = (int)m_returnDisplay.find('.'); + } + if (returnValue < 0) + { + numPreDecimal--; + } + + if (numPreDecimal > MAXIMUMDIGITSALLOWED || (returnValue != 0 && Math.Abs(returnValue) < MINIMUMDECIMALALLOWED)) + { + m_returnDisplay = returnValue.ToString("e"); + } + else + { + returnValue = System.Convert.ToDouble(m_returnDisplay); + string returnString; + if (m_currentDisplay.size() <= OPTIMALDIGITSALLOWED && Math.Abs(returnValue) >= OPTIMALDECIMALALLOWED) + { + returnString = RoundSignificant(returnValue, OPTIMALDIGITSALLOWED - Math.Min(numPreDecimal, OPTIMALDIGITSALLOWED)); + } + else + { + returnString = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED - Math.Min(numPreDecimal, MAXIMUMDIGITSALLOWED)); + } + m_returnDisplay = returnString; + TrimString(ref m_returnDisplay); + } + m_returnHasDecimal = (m_returnDisplay.IndexOf('.') != m_returnDisplay.npos()); + } + UpdateViewModel(); + } + + /// + /// Trims out any trailing zeros or decimals in the given input string + /// + /// string to trim + void TrimString(ref string returnString) + { + if (returnString.IndexOf('.') == m_returnDisplay.Length) + { + return; + } + + returnString = returnString.TrimEnd('0'); + returnString = returnString.TrimEnd('.'); + } + + /// + /// Rounds the given double to the given number of significant digits + /// + /// input double + /// int number of significant digits to round to + string RoundSignificant(double num, int numSignificant) + { + return num.ToString($"F{numSignificant}"); + } + + void UpdateCurrencySymbols() + { + if (m_currencyDataLoader != null && m_vmCurrencyCallback != null) + { + ICurrencyConverterDataLoader currencyDataLoader = GetCurrencyConverterDataLoader(); + KeyValuePair currencySymbols = currencyDataLoader.GetCurrencySymbols(m_fromType, m_toType); + KeyValuePair currencyRatios = currencyDataLoader.GetCurrencyRatioEquality(m_fromType, m_toType); + + m_vmCurrencyCallback.CurrencySymbolsCallback(currencySymbols.Key, currencySymbols.Value); + m_vmCurrencyCallback.CurrencyRatiosCallback(currencyRatios.Key, currencyRatios.Value); + } + } + + void UpdateViewModel() + { + m_vmCallback.DisplayCallback(m_currentDisplay, m_returnDisplay); + m_vmCallback.SuggestedValueCallback(CalculateSuggested()); + } + } +} diff --git a/src/Calculator.Shared/Calculator.Shared.projitems b/src/Calculator.Shared/Calculator.Shared.projitems index 6004fdf4..ecef1078 100644 --- a/src/Calculator.Shared/Calculator.Shared.projitems +++ b/src/Calculator.Shared/Calculator.Shared.projitems @@ -22,6 +22,7 @@ + @@ -34,10 +35,12 @@ + + @@ -48,13 +51,16 @@ + + + @@ -72,6 +78,11 @@ + + + + + @@ -138,6 +149,7 @@ UnitConverter.xaml + diff --git a/src/Calculator.Shared/Common/AlwaysSelectedCollectionView.cs b/src/Calculator.Shared/Common/AlwaysSelectedCollectionView.cs index a76eb105..4ce82a31 100644 --- a/src/Calculator.Shared/Common/AlwaysSelectedCollectionView.cs +++ b/src/Calculator.Shared/Common/AlwaysSelectedCollectionView.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Text; using Windows.Foundation; @@ -8,5 +9,9 @@ namespace WindowsCalculator.Shared.Common { class AlwaysSelectedCollectionView { + public AlwaysSelectedCollectionView(IEnumerable result) + { + + } } } diff --git a/src/Calculator.Shared/Common/Automation/NarratorNotifier.cs b/src/Calculator.Shared/Common/Automation/NarratorNotifier.cs new file mode 100644 index 00000000..47ce0086 --- /dev/null +++ b/src/Calculator.Shared/Common/Automation/NarratorNotifier.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Windows.UI.Xaml; + +namespace CalculatorApp.Common.Automation +{ + public sealed partial class NarratorNotifier : DependencyObject + { + public static DependencyProperty AnnouncementProperty { get; } = DependencyProperty.RegisterAttached( + "Announcement", + typeof(NarratorAnnouncement), + typeof(NarratorNotifier), + new PropertyMetadata(default(NarratorAnnouncement), OnAnnouncementChanged)); + + public static NarratorAnnouncement GetAnnouncement(Windows.UI.Xaml.DependencyObject element) + { + return element.GetValue(AnnouncementProperty) as NarratorAnnouncement; + } + + public static void SetAnnouncement(Windows.UI.Xaml.DependencyObject element, NarratorAnnouncement value) + { + element.SetValue(AnnouncementProperty, value); + } + + public NarratorAnnouncement Announcement + { + get { return GetAnnouncement(this); } + set { SetAnnouncement(this, value); } + } + + + //TODO UNO INarratorAnnouncementHost m_announcementHost; + + public NarratorNotifier() + { + // TODO UNO + // m_announcementHost = NarratorAnnouncementHostFactory.MakeHost(); + } + + public void Announce(NarratorAnnouncement announcement) + { + // TODO UNO: + //if (NarratorAnnouncement.IsValid(announcement) && m_announcementHost != null) + //{ + // m_announcementHost.Announce(announcement); + //} + } + + static void OnAnnouncementChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + var instance = dependencyObject as NarratorNotifier; + if (instance != null) + { + instance.Announce((NarratorAnnouncement)(e.NewValue)); + } + } + } +} diff --git a/src/Calculator.Shared/Common/ConversionResultTaskHelper.cs b/src/Calculator.Shared/Common/ConversionResultTaskHelper.cs new file mode 100644 index 00000000..4e792ae1 --- /dev/null +++ b/src/Calculator.Shared/Common/ConversionResultTaskHelper.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CalculatorApp.Common +{ + class ConversionResultTaskHelper + { + uint m_delay; + CancellationTokenSource m_cts = new CancellationTokenSource(); + Action m_storedFunction; + + public ConversionResultTaskHelper(uint delay, Action functionToRun) + { + m_delay = delay; + m_storedFunction = functionToRun; + + var delayTask = CompleteAfter(delay); + } + + ~ConversionResultTaskHelper() + { + m_cts.Cancel(); + } + + // Creates a task that completes after the specified delay. + // + // Taken from: How to: Create a Task that Completes After a Delay + // https://msdn.microsoft.com/en-us/library/hh873170.aspx + async Task CompleteAfter(uint timeout) + { + await Task.Delay(TimeSpan.FromMilliseconds(timeout)); + if (!m_cts.Token.IsCancellationRequested) + { + m_storedFunction(); + } + } + } +} diff --git a/src/Calculator.Shared/Common/LocalizationService.cs b/src/Calculator.Shared/Common/LocalizationService.cs index e715fd9d..1577467f 100644 --- a/src/Calculator.Shared/Common/LocalizationService.cs +++ b/src/Calculator.Shared/Common/LocalizationService.cs @@ -28,7 +28,7 @@ namespace CalculatorApp.Common public sealed class LocalizationService { - static string DefaultCurrencyCode = "USD"; + internal static string DefaultCurrencyCode = "USD"; @@ -167,7 +167,7 @@ namespace CalculatorApp.Common } } - FlowDirection GetFlowDirection() + public FlowDirection GetFlowDirection() { return m_flowDirection; } diff --git a/src/Calculator.Shared/Common/LocalizationSettings.cs b/src/Calculator.Shared/Common/LocalizationSettings.cs index 902d00e8..d734332d 100644 --- a/src/Calculator.Shared/Common/LocalizationSettings.cs +++ b/src/Calculator.Shared/Common/LocalizationSettings.cs @@ -335,7 +335,7 @@ namespace CalculatorApp return m_currencyTrailingDigits; } - int GetCurrencySymbolPrecedence() + public int GetCurrencySymbolPrecedence() { return m_currencySymbolPrecedence; } diff --git a/src/Calculator.Shared/Common/NavCategory.cs b/src/Calculator.Shared/Common/NavCategory.cs index 8570ae16..3dc8cb01 100644 --- a/src/Calculator.Shared/Common/NavCategory.cs +++ b/src/Calculator.Shared/Common/NavCategory.cs @@ -140,7 +140,7 @@ namespace CalculatorApp public string AccessKey => m_accessKey; - bool SupportsNegative + public bool SupportsNegative { get { @@ -531,13 +531,13 @@ namespace CalculatorApp public CategoryGroupType GroupType { get => m_GroupType; private set { m_GroupType = value; RaisePropertyChanged("GroupType"); } } - private ObservableCollection m_Categories; - public ObservableCollection Categories { get => m_Categories; private set { m_Categories = value; RaisePropertyChanged("Categories"); } } + private CalculatorObservableCollection m_Categories; + public CalculatorObservableCollection Categories { get => m_Categories; private set { m_Categories = value; RaisePropertyChanged("Categories"); } } internal NavCategoryGroup(NavCategoryGroupInitializer groupInitializer) { - m_Categories = new ObservableCollection(); + m_Categories = new CalculatorObservableCollection(); m_GroupType = groupInitializer.type; var resProvider = AppResourceProvider.GetInstance(); @@ -573,9 +573,9 @@ namespace CalculatorApp } } - public static ObservableCollection CreateMenuOptions() + public static CalculatorObservableCollection CreateMenuOptions() { - var menuOptions = new ObservableCollection(); + var menuOptions = new CalculatorObservableCollection(); menuOptions.Add(CreateCalculatorCategory()); menuOptions.Add(CreateConverterCategory()); return menuOptions; @@ -586,7 +586,7 @@ namespace CalculatorApp return new NavCategoryGroup(NavCategory.s_categoryGroupManifest[0].Value); } - static NavCategoryGroup CreateConverterCategory() + public static NavCategoryGroup CreateConverterCategory() { return new NavCategoryGroup(NavCategory.s_categoryGroupManifest[1].Value); } diff --git a/src/Calculator.Shared/Common/NetworkManager.cs b/src/Calculator.Shared/Common/NetworkManager.cs new file mode 100644 index 00000000..8361c09e --- /dev/null +++ b/src/Calculator.Shared/Common/NetworkManager.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Windows.Networking.Connectivity; + +namespace CalculatorApp +{ + public enum NetworkAccessBehavior + { + Normal = 0, + OptIn = 1, + Offline = 2 + }; + + public delegate void NetworkBehaviorChangedHandler(NetworkAccessBehavior behavior); + + public sealed class NetworkManager + { + public event NetworkBehaviorChangedHandler NetworkBehaviorChanged; + + public NetworkManager() + { + NetworkInformation.NetworkStatusChanged += new NetworkStatusChangedEventHandler(OnNetworkStatusChange); + } + + ~NetworkManager() + { + NetworkInformation.NetworkStatusChanged -= OnNetworkStatusChange; + } + + public static NetworkAccessBehavior GetNetworkAccessBehavior() + { + NetworkAccessBehavior behavior = NetworkAccessBehavior.Offline; + ConnectionProfile connectionProfile = NetworkInformation.GetInternetConnectionProfile(); + if (connectionProfile != null) + { + NetworkConnectivityLevel connectivityLevel = connectionProfile.GetNetworkConnectivityLevel(); + if (connectivityLevel == NetworkConnectivityLevel.InternetAccess || connectivityLevel == NetworkConnectivityLevel.ConstrainedInternetAccess) + { + ConnectionCost connectionCost = connectionProfile.GetConnectionCost(); + behavior = ConvertCostInfoToBehavior(connectionCost); + } + } + + return behavior; + } + + void OnNetworkStatusChange(object sender) + { + NetworkBehaviorChanged?.Invoke(GetNetworkAccessBehavior()); + } + + // See app behavior guidelines at https://msdn.microsoft.com/en-us/library/windows/apps/xaml/jj835821(v=win.10).aspx + static NetworkAccessBehavior ConvertCostInfoToBehavior(ConnectionCost connectionCost) + { + if (connectionCost.Roaming || connectionCost.OverDataLimit || connectionCost.NetworkCostType == NetworkCostType.Variable + || connectionCost.NetworkCostType == NetworkCostType.Fixed) + { + return NetworkAccessBehavior.OptIn; + } + + return NetworkAccessBehavior.Normal; + } + } +} diff --git a/src/Calculator.Shared/Common/TraceLogger.cs b/src/Calculator.Shared/Common/TraceLogger.cs index 8b769748..2605b19d 100644 --- a/src/Calculator.Shared/Common/TraceLogger.cs +++ b/src/Calculator.Shared/Common/TraceLogger.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; using Windows.Globalization; using CalculatorApp.Common; @@ -63,7 +64,7 @@ namespace CalculatorApp public void LogWindowActivated() {} public void LogWindowLaunched() {} public void LogUserRequestedRefreshFailed() {} - //public void LogConversionResult(std::wstring_view fromValue, std::wstring_view fromUnit, std::wstring_view toValue, std::wstring_view toUnit) {} + public void LogConversionResult(string fromValue, string fromUnit, string toValue, string toUnit) {} public void LogAboutFlyoutOpened() {} public void LogNavBarOpened() {} public void LogViewClosingTelemetry(int i) {} @@ -74,8 +75,8 @@ namespace CalculatorApp public void LogDateAddSubtractModeUsed(int windowId, bool isAddMode) {} public void LogDateClippedTimeDifferenceFound(Calendar today, DateTime clippedTime) {} - //public void LogStandardException(std::wstring_view functionName, _In_ const std::exception& e) {} - //public void LogWinRTException(std::wstring_view functionName, _In_ winrt::hresult_error const& e) {} - //public void LogPlatformException(std::wstring_view functionName, _In_ Platform::Exception ^ e) {} + public void LogStandardException(Exception e, [CallerMemberName] string functionName = null) { } + public void LogWinRTException(Exception e, [CallerMemberName] string functionName = null) { } + public void LogPlatformException(Exception e, [CallerMemberName] string functionName = null) {} } } diff --git a/src/Calculator.Shared/Common/Utils.cs b/src/Calculator.Shared/Common/Utils.cs index 158784e8..56670e63 100644 --- a/src/Calculator.Shared/Common/Utils.cs +++ b/src/Calculator.Shared/Common/Utils.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; +using Windows.Storage; using Windows.UI.Core; using Windows.UI.ViewManagement; @@ -31,5 +34,47 @@ namespace CalculatorApp { return !string.IsNullOrEmpty(input) && input.Last() == target; } - } + + public static void Swap(ref T field1, ref T field2) + { + var tmp = field1; + field1 = field2; + field2 = tmp; + } + + public static bool IsDateTimeOlderThan(DateTime dateTime, long duration) + { + return dateTime + TimeSpan.FromTicks(duration) < DateTime.Now; + } + + public static async Task ReadFileFromFolder(StorageFolder folder, string filename) + { + if (folder == null) + { + return null; + } + + var filePath = Path.Combine(folder.Path, filename); + using (var reader = new StreamReader(filePath)) + { + return await reader.ReadToEndAsync(); + } + } + + public static async Task WriteFileToFolder(StorageFolder folder, string filename, string contents, CreationCollisionOption colisionOption) + { + if (folder== null) + { + return; + } + + var filePath = Path.Combine(folder.Path, filename); + using (var writer = new StreamWriter(filePath, append: false)) + { + await writer.WriteAsync(contents); + } + } + + public static DateTime GetUniversalSystemTime() => DateTime.UtcNow; + } } diff --git a/src/Calculator.Shared/Common/ValidSelectedItemConverter.cs b/src/Calculator.Shared/Common/ValidSelectedItemConverter.cs new file mode 100644 index 00000000..1d77caba --- /dev/null +++ b/src/Calculator.Shared/Common/ValidSelectedItemConverter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Windows.UI.Xaml.Data; + +namespace CalculatorApp.Common +{ + public sealed class ValidSelectedItemConverter : IValueConverter + { + public ValidSelectedItemConverter() + { + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + // Pass through as we don't want to change the value from the source + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value != null) + { + if (value is int i && i > 0) + { + return value; + } + } + + // Stop the binding if the object is null + return Windows.UI.Xaml.DependencyProperty.UnsetValue; + } + } +} diff --git a/src/Calculator.Shared/Controls/CalculationResult.cs b/src/Calculator.Shared/Controls/CalculationResult.cs index eee528ed..e2357a09 100644 --- a/src/Calculator.Shared/Controls/CalculationResult.cs +++ b/src/Calculator.Shared/Controls/CalculationResult.cs @@ -10,6 +10,7 @@ using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; +using CalculatorApp.ViewModel; namespace CalculatorApp { @@ -17,7 +18,7 @@ namespace CalculatorApp { public delegate void SelectedEventHandler(object sender); - public sealed partial class CalculationResult : Windows.UI.Xaml.Controls.Control + public sealed partial class CalculationResult : Windows.UI.Xaml.Controls.Control, IActivatable { public Visibility ExpressionVisibility { @@ -130,7 +131,7 @@ namespace CalculatorApp public static readonly DependencyProperty IsOperatorCommandProperty = DependencyProperty.Register("IsOperatorCommand", typeof(bool), typeof(CalculationResult), new PropertyMetadata(false)); - event SelectedEventHandler Selected; + public event SelectedEventHandler Selected; private Windows.UI.Xaml.Controls.ScrollViewer m_textContainer; @@ -163,7 +164,7 @@ namespace CalculatorApp m_haveCalculatedMax = false; } - string GetRawDisplayValue() + public string GetRawDisplayValue() { string rawValue = null; @@ -298,7 +299,7 @@ namespace CalculatorApp } } - void UpdateTextState() + public void UpdateTextState() { if ((m_textContainer == null) || (m_textBlock == null)) { diff --git a/src/Calculator.Shared/Controls/HorizontalNoOverflowStackPanel.cs b/src/Calculator.Shared/Controls/HorizontalNoOverflowStackPanel.cs new file mode 100644 index 00000000..8d6f4114 --- /dev/null +++ b/src/Calculator.Shared/Controls/HorizontalNoOverflowStackPanel.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace CalculatorApp.Controls +{ + public partial class HorizontalNoOverflowStackPanel : Panel + { + // TODO UNO: DEPENDENCY_PROPERTY_OWNER(HorizontalNoOverflowStackPanel); + + protected override Size MeasureOverride(Size availableSize) + { + double maxHeight = 0; + double width = 0; + foreach (UIElement child in Children) + { + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + width += child.DesiredSize.Width; + } + return new Size(Math.Min(width, availableSize.Width), Math.Min(availableSize.Height, maxHeight)); + } + + protected virtual bool ShouldPrioritizeLastItem() + { + return false; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (Children.Count == 0) + { + return finalSize; + } + + double posX = 0; + var lastChild = (UIElement)Children.Last(); + double lastChildWidth = 0; + if (Children.Count > 2 && ShouldPrioritizeLastItem()) + { + lastChildWidth = lastChild.DesiredSize.Width; + } + foreach (UIElement item in Children) + { + var widthAvailable = finalSize.Width - posX; + if (item != lastChild) + { + widthAvailable -= lastChildWidth; + } + double itemWidth = item.DesiredSize.Width; + if (widthAvailable > 0 && itemWidth <= widthAvailable) + { + // stack the items horizontally (left to right) + item.Arrange(new Rect(posX, 0, itemWidth, finalSize.Height)); + posX += item.RenderSize.Width; + } + else + { + // Not display the item + item.Arrange(new Rect(0, 0, 0, 0)); + } + } + return finalSize; + } + }; +} diff --git a/src/Calculator.Shared/Controls/SupplementaryItemsControl.cs b/src/Calculator.Shared/Controls/SupplementaryItemsControl.cs index a4b9adbb..4d48f748 100644 --- a/src/Calculator.Shared/Controls/SupplementaryItemsControl.cs +++ b/src/Calculator.Shared/Controls/SupplementaryItemsControl.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System; using System.Collections.Generic; using System.Text; @@ -5,6 +8,7 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Automation; using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Controls; +using CalculatorApp.ViewModel; namespace CalculatorApp.Controls { @@ -20,13 +24,12 @@ namespace CalculatorApp.Controls { base.PrepareContainerForItemOverride(element, item); - // UNO TODO - //var supplementaryResult = (SupplementaryResult)(item); - //if (supplementaryResult) - //{ - // AutomationProperties.SetName(element, supplementaryResult.GetLocalizedAutomationName()); - //} - } + var supplementaryResult = (SupplementaryResult)(item); + if (supplementaryResult != null) + { + AutomationProperties.SetName(element, supplementaryResult.GetLocalizedAutomationName()); + } + } } public sealed partial class SupplementaryContentPresenter : ContentPresenter diff --git a/src/Calculator.Shared/Converters/AlwaysSelectedCollectionViewConverter.cs b/src/Calculator.Shared/Converters/AlwaysSelectedCollectionViewConverter.cs index 5be2b44a..9cd34a25 100644 --- a/src/Calculator.Shared/Converters/AlwaysSelectedCollectionViewConverter.cs +++ b/src/Calculator.Shared/Converters/AlwaysSelectedCollectionViewConverter.cs @@ -1,39 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System; +using System.Collections; using System.Collections.Generic; using System.Text; +using WindowsCalculator.Shared.Common; namespace CalculatorApp { namespace Common { - //class AlwaysSelectedCollectionViewConverter : Windows.UI.Xaml.Data.IValueConverter - //{ - // public AlwaysSelectedCollectionViewConverter() - // { - // } + class AlwaysSelectedCollectionViewConverter : Windows.UI.Xaml.Data.IValueConverter + { + public AlwaysSelectedCollectionViewConverter() + { + } - // object Convert( - // object value, - // Type targetType, - // object parameter, - // string language) - // { - // var result = (IEnumerable)(value); - // if (result) - // { - // return new AlwaysSelectedCollectionView(result); - // } - // return Windows.UI.Xaml.DependencyProperty.UnsetValue; // Can't convert - // } + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + var result = value as IEnumerable; + if (result != null) + { + return new AlwaysSelectedCollectionView(result); + } + return Windows.UI.Xaml.DependencyProperty.UnsetValue; // Can't convert + } - // object ConvertBack( - // object value, - // Type targetType, - // object parameter, - // string language) - // { - // return Windows.UI.Xaml.DependencyProperty.UnsetValue; - // } - //} - } -} \ No newline at end of file + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return Windows.UI.Xaml.DependencyProperty.UnsetValue; + } + } + } +} diff --git a/src/Calculator.Shared/Converters/VisibilityNegationConverter.cs b/src/Calculator.Shared/Converters/VisibilityNegationConverter.cs index 59e829ec..43b21a1a 100644 --- a/src/Calculator.Shared/Converters/VisibilityNegationConverter.cs +++ b/src/Calculator.Shared/Converters/VisibilityNegationConverter.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text; using Windows.UI.Xaml.Interop; @@ -6,7 +9,7 @@ using Windows.Foundation.Metadata; using Windows.UI.Xaml.Data; using Windows.UI.Xaml; -namespace CalculatorApp.Converters +namespace CalculatorApp.Common { [WebHostHidden] public sealed class VisibilityNegationConverter : IValueConverter diff --git a/src/Calculator.Shared/DataLoaders/CurrencyDataLoader.cs b/src/Calculator.Shared/DataLoaders/CurrencyDataLoader.cs new file mode 100644 index 00000000..25795257 --- /dev/null +++ b/src/Calculator.Shared/DataLoaders/CurrencyDataLoader.cs @@ -0,0 +1,792 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Windows.Data.Json; +using Windows.Foundation.Collections; +using Windows.Globalization.DateTimeFormatting; +using Windows.Storage; +using Windows.System.UserProfile; +using Windows.UI.Core; +using CalculatorApp.Common; +using CalculatorApp.DataLoaders; +using UCM = UnitConversionManager; +using CurrencyRatioMap = System.Collections.Generic.Dictionary; +using SelectedUnits = System.Collections.Generic.KeyValuePair; +using CategorySelectionInitializer = System.Tuple, UnitConversionManager.Unit, UnitConversionManager.Unit>; +using UnitToUnitToConversionDataMap = System.Collections.Generic.Dictionary>; +using CategoryToUnitVectorMap = System.Collections.Generic.Dictionary>; + +namespace CalculatorApp.ViewModel +{ + public enum CurrencyLoadStatus + { + NotLoaded = 0, + FailedToLoad = 1, + LoadedFromCache = 2, + LoadedFromWeb = 3 + } + + public static class UnitConverterResourceKeys + { + public const string CurrencyUnitFromKey = CurrencyDataLoader.CURRENCY_UNIT_FROM_KEY; + public const string CurrencyUnitToKey = CurrencyDataLoader.CURRENCY_UNIT_TO_KEY; + } + + public static class CurrencyDataLoaderConstants + { + public const string CacheTimestampKey = CurrencyDataLoader.CACHE_TIMESTAMP_KEY; + public const string CacheLangcodeKey = CurrencyDataLoader.CACHE_LANGCODE_KEY; + public const string CacheDelimiter = CurrencyDataLoader.CACHE_DELIMITER; + public const string StaticDataFilename = CurrencyDataLoader.STATIC_DATA_FILENAME; + public const string AllRatiosDataFilename = CurrencyDataLoader.ALL_RATIOS_DATA_FILENAME; + public const long DayDuration = CurrencyDataLoader.DAY_DURATION; + } + + struct CurrencyUnitMetadata + { + public CurrencyUnitMetadata(string s) + { + symbol = s; + } + + public string symbol; + }; + + public class CurrencyDataLoader : UCM.IConverterDataLoader, UCM.ICurrencyConverterDataLoader + { + //private: + string m_responseLanguage; + CalculatorApp.DataLoaders.ICurrencyHttpClient m_client; + + bool m_isRtlLanguage; + + //Mutex m_currencyUnitsMutex; + object m_currencyUnitsMutex = new object(); + CalculatorList m_currencyUnits = new CalculatorList(); + UnitToUnitToConversionDataMap m_currencyRatioMap = new UnitToUnitToConversionDataMap(EqualityComparer.Default); + Dictionary m_currencyMetadata = new Dictionary(EqualityComparer.Default); + + UCM.IViewModelCurrencyCallback m_vmCallback; + + Windows.Globalization.NumberFormatting.DecimalFormatter m_ratioFormatter; + string m_ratioFormat; + DateTime m_cacheTimestamp; + string m_timestampFormat; + + CurrencyLoadStatus m_loadStatus; + + CalculatorApp.NetworkManager m_networkManager; + + CalculatorApp.NetworkAccessBehavior m_networkAccessBehavior; + + //Windows.Foundation.EventRegistrationToken m_networkBehaviorToken; + bool m_meteredOverrideSet; + + internal const string CURRENCY_UNIT_FROM_KEY = "CURRENCY_UNIT_FROM_KEY"; + internal const string CURRENCY_UNIT_TO_KEY = "CURRENCY_UNIT_TO_KEY"; + + // Calculate number of 100-nanosecond intervals-per-day + // (1 interval/100 nanosecond)(100 nanosecond/1e-7 s)(60 s/1 min)(60 min/1 hr)(24 hr/1 day) = (interval/day) + internal const long DAY_DURATION = 1L * 60 * 60 * 24 * 10000000; + internal const long WEEK_DURATION = DAY_DURATION * 7; + + static int FORMATTER_DIGIT_COUNT = 4; + + internal const string CACHE_TIMESTAMP_KEY = "CURRENCY_CONVERTER_TIMESTAMP"; + internal const string CACHE_LANGCODE_KEY = "CURRENCY_CONVERTER_LANGCODE"; + internal const string CACHE_DELIMITER = "%"; + + internal const string STATIC_DATA_FILENAME = "CURRENCY_CONVERTER_STATIC_DATA.txt"; + + private static string[] STATIC_DATA_PROPERTIES = + { + "CountryCode", + "CountryName", + "CurrencyCode", + "CurrencyName", + "CurrencySymbol" + }; + + internal const string ALL_RATIOS_DATA_FILENAME = "CURRENCY_CONVERTER_ALL_RATIOS_DATA.txt"; + internal const string RATIO_KEY = "Rt"; + internal const string CURRENCY_CODE_KEY = "An"; + internal static string[] ALL_RATIOS_DATA_PROPERTIES = {RATIO_KEY, CURRENCY_CODE_KEY}; + + static string DEFAULT_FROM_TO_CURRENCY_FILE_URI = "ms-appx:///DataLoaders/DefaultFromToCurrency.json"; + static string FROM_KEY = "from"; + static string TO_KEY = "to"; + + // Fallback default values. + static string DEFAULT_FROM_CURRENCY = DefaultCurrencyCode; + static string DEFAULT_TO_CURRENCY = "EUR"; + + private static string DefaultCurrencyCode = LocalizationService.DefaultCurrencyCode; + + public CurrencyDataLoader(ICurrencyHttpClient client) + { + this.m_client = client; + ; + this.m_loadStatus = CurrencyLoadStatus.NotLoaded; + this.m_responseLanguage = "en-US"; + this.m_ratioFormat = ""; + this.m_timestampFormat = ""; + this.m_networkManager = new NetworkManager(); + this.m_meteredOverrideSet = false; + + + if (GlobalizationPreferences.Languages.Count > 0) + { + m_responseLanguage = GlobalizationPreferences.Languages[0]; + } + + if (m_client != null) + { + m_client.SetSourceCurrencyCode(DefaultCurrencyCode); + m_client.SetResponseLanguage(m_responseLanguage); + } + + if (CoreWindow.GetForCurrentThread() != null) + { + // Must have a CoreWindow to access the resource context. + m_isRtlLanguage = LocalizationService.GetInstance().IsRtlLayout(); + } + + m_ratioFormatter = LocalizationService.GetRegionalSettingsAwareDecimalFormatter(); + m_ratioFormatter.IsGrouped = true; + m_ratioFormatter.IsDecimalPointAlwaysDisplayed = true; + m_ratioFormatter.FractionDigits = FORMATTER_DIGIT_COUNT; + + m_ratioFormat = AppResourceProvider.GetInstance().GetResourceString("CurrencyFromToRatioFormat"); + m_timestampFormat = AppResourceProvider.GetInstance().GetResourceString("CurrencyTimestampFormat"); + } + + ~CurrencyDataLoader() + { + UnregisterForNetworkBehaviorChanges(); + } + + void UnregisterForNetworkBehaviorChanges() + { + m_networkManager.NetworkBehaviorChanged -= OnNetworkBehaviorChanged; + } + + void RegisterForNetworkBehaviorChanges() + { + UnregisterForNetworkBehaviorChanges(); + + m_networkManager.NetworkBehaviorChanged += new NetworkBehaviorChangedHandler(OnNetworkBehaviorChanged); + + OnNetworkBehaviorChanged(NetworkManager.GetNetworkAccessBehavior()); + } + + void OnNetworkBehaviorChanged(NetworkAccessBehavior newBehavior) + { + m_networkAccessBehavior = newBehavior; + if (m_vmCallback != null) + { + m_vmCallback.NetworkBehaviorChanged((int)(m_networkAccessBehavior)); + } + } + + bool LoadFinished() + { + return m_loadStatus != CurrencyLoadStatus.NotLoaded; + } + + bool LoadedFromCache() + { + return m_loadStatus == CurrencyLoadStatus.LoadedFromCache; + } + + bool LoadedFromWeb() + { + return m_loadStatus == CurrencyLoadStatus.LoadedFromWeb; + } + + void ResetLoadStatus() + { + m_loadStatus = CurrencyLoadStatus.NotLoaded; + } + + public async void LoadData() + { + RegisterForNetworkBehaviorChanges(); + + if (!LoadFinished()) + { + var loadFunctions = new Func>[] + { + TryLoadDataFromCacheAsync, + TryLoadDataFromWebAsync + }; + + bool didLoad = false; + foreach (var f in loadFunctions) + { + didLoad = await f(); + if (didLoad) + { + break; + } + } + + UpdateDisplayedTimestamp(); + NotifyDataLoadFinished(didLoad); + } + } + + public CalculatorList LoadOrderedCategories() + { + // This function should not be called + // The model will use the categories from UnitConverterDataLoader + return new CalculatorList(); + } + + public CalculatorList LoadOrderedUnits(UCM.Category category) + { + lock (m_currencyUnitsMutex) + { + return m_currencyUnits; + } + } + + public Dictionary LoadOrderedRatios(UCM.Unit unit) + { + lock (m_currencyUnitsMutex) ; + { + return m_currencyRatioMap[unit]; + } + } + + public bool SupportsCategory(UCM.Category target) + { + int currencyId = NavCategory.Serialize(ViewMode.Currency); + return target.id == currencyId; + } + + public void SetViewModelCallback(UCM.IViewModelCurrencyCallback callback) + { + m_vmCallback = callback; + OnNetworkBehaviorChanged(m_networkAccessBehavior); + } + + public KeyValuePair GetCurrencySymbols(UCM.Unit unit1, UCM.Unit unit2) + { + lock (m_currencyUnitsMutex) + { + + string symbol1 = ""; + string symbol2 = ""; + + if (m_currencyMetadata.TryGetValue(unit1, out var itr1) && m_currencyMetadata.TryGetValue(unit2, out var itr2)) + { + symbol1 = itr1.symbol; + symbol2 = itr2.symbol; + } + + return new KeyValuePair(symbol1, symbol2); + } + } + + public KeyValuePair GetCurrencyRatioEquality(UCM.Unit unit1, UCM.Unit unit2) + { + try + { + if (m_currencyRatioMap.TryGetValue(unit1, out var ratioMap)) + { + if (ratioMap.TryGetValue(unit2, out var iter2)) + { + var ratio = iter2.ratio; + + // Round the ratio to FORMATTER_DIGIT_COUNT digits using int math. + // Ex: to round 1.23456 to three digits, use + // ((int) 1.23456 * (10^3)) / (10^3) + double scale = Math.Pow(10, FORMATTER_DIGIT_COUNT); + double rounded = (int)(ratio * (int)(scale)) / scale; + + string digitSymbol = LocalizationSettings.GetInstance().GetDigitSymbolFromEnUsDigit('1').ToString(); + string roundedFormat = m_ratioFormatter.Format(rounded); + + string ratioString = LocalizationStringUtil.GetLocalizedString( + m_ratioFormat, + digitSymbol, + unit1.abbreviation, + roundedFormat, + unit2.abbreviation); + + string accessibleRatioString = LocalizationStringUtil.GetLocalizedString( + m_ratioFormat, + digitSymbol, + unit1.accessibleName, + roundedFormat, + unit2.accessibleName); + + return new KeyValuePair(ratioString, accessibleRatioString); + } + } + } + catch + { + } + + return new KeyValuePair("", ""); + } + +#pragma optimize("", off) // Turn off optimizations to work around DevDiv 393321 + public async Task TryLoadDataFromCacheAsync() + { + try + { + ResetLoadStatus(); + + var localSettings = ApplicationData.Current.LocalSettings; + if (localSettings == null || !localSettings.Values.ContainsKey(CurrencyDataLoaderConstants.CacheTimestampKey)) + { + return false; + } + + bool loadComplete = false; + m_cacheTimestamp = DateTime.Parse((string)localSettings.Values[CurrencyDataLoaderConstants.CacheTimestampKey]); + if (Utils.IsDateTimeOlderThan(m_cacheTimestamp, DAY_DURATION) && m_networkAccessBehavior == NetworkAccessBehavior.Normal) + { + loadComplete = await TryLoadDataFromWebAsync(); + } + + if (!loadComplete) + { + loadComplete = await TryFinishLoadFromCacheAsync(); + } + + return loadComplete; + } + catch (Exception e) + { + TraceLogger.GetInstance().LogStandardException(e); + return false; + } + } + + public async Task TryFinishLoadFromCacheAsync() + { + var localSettings = ApplicationData.Current.LocalSettings; + if (localSettings == null) + { + return false; + } + + if (!localSettings.Values.ContainsKey(CurrencyDataLoaderConstants.CacheLangcodeKey) || !((string)localSettings.Values[CurrencyDataLoaderConstants.CacheLangcodeKey]).Equals(m_responseLanguage)) + { + return false; + } + + StorageFolder localCacheFolder = ApplicationData.Current.LocalCacheFolder; + if (localCacheFolder == null) + { + return false; + } + + string staticDataResponse = await Utils.ReadFileFromFolder(localCacheFolder, CurrencyDataLoaderConstants.StaticDataFilename); + string allRatiosResponse = await Utils.ReadFileFromFolder(localCacheFolder, CurrencyDataLoaderConstants.AllRatiosDataFilename); + + CalculatorList staticData = new CalculatorList(); + CurrencyRatioMap ratioMap = new CurrencyRatioMap(); + + bool didParse = TryParseWebResponses(staticDataResponse, allRatiosResponse, staticData, ratioMap); + if (!didParse) + { + return false; + } + + m_loadStatus = CurrencyLoadStatus.LoadedFromCache; + await FinalizeUnits(staticData, ratioMap); + + return true; + } + + public async Task TryLoadDataFromWebAsync() + { + try + { + ResetLoadStatus(); + + if (m_client == null) + { + return false; + } + + if (m_networkAccessBehavior == NetworkAccessBehavior.Offline || (m_networkAccessBehavior == NetworkAccessBehavior.OptIn && !m_meteredOverrideSet)) + { + return false; + } + + String staticDataResponse = await m_client.GetCurrencyMetadata(); + String allRatiosResponse = await m_client.GetCurrencyRatios(); + if (staticDataResponse == null || allRatiosResponse == null) + { + return false; + } + + CalculatorList staticData = new CalculatorList(); + CurrencyRatioMap ratioMap = new CurrencyRatioMap(); + + bool didParse = TryParseWebResponses(staticDataResponse, allRatiosResponse, staticData, ratioMap); + if (!didParse) + { + return false; + } + + // Set the timestamp before saving it below. + m_cacheTimestamp = Utils.GetUniversalSystemTime(); + + try + { + CalculatorList> cachedFiles = new CalculatorList> + { + new KeyValuePair(CurrencyDataLoaderConstants.StaticDataFilename, staticDataResponse), + new KeyValuePair(CurrencyDataLoaderConstants.AllRatiosDataFilename, allRatiosResponse) + }; + + StorageFolder localCacheFolder = ApplicationData.Current.LocalCacheFolder; + foreach (var fileInfo in cachedFiles) + { + await Utils.WriteFileToFolder(localCacheFolder, fileInfo.Key, fileInfo.Value, CreationCollisionOption.ReplaceExisting); + } + + SaveLangCodeAndTimestamp(); + } + catch + { + // If we fail to save to cache it's okay, we should still continue. + } + + m_loadStatus = CurrencyLoadStatus.LoadedFromWeb; + await FinalizeUnits(staticData, ratioMap); + + return true; + } + catch (Exception e) + { + TraceLogger.GetInstance().LogStandardException(e); + return false; + } + } + + public async Task TryLoadDataFromWebOverrideAsync() + { + m_meteredOverrideSet = true; + bool didLoad = await TryLoadDataFromWebAsync(); + if (!didLoad) + { + m_loadStatus = CurrencyLoadStatus.FailedToLoad; + TraceLogger.GetInstance().LogUserRequestedRefreshFailed(); + } + + return didLoad; + } + + bool TryParseWebResponses( + String staticDataJson, + String allRatiosJson, + CalculatorList staticData, + CurrencyRatioMap allRatiosData) + { + return TryParseStaticData(staticDataJson, staticData) && TryParseAllRatiosData(allRatiosJson, allRatiosData); + } + + bool TryParseStaticData(String rawJson, CalculatorList staticData) + { + JsonArray data = null; + if (!JsonArray.TryParse(rawJson, out data)) + { + return false; + } + + string countryCode = ""; + string countryName = ""; + string currencyCode = ""; + string currencyName = ""; + string currencySymbol = ""; + + string[] values = {countryCode, countryName, currencyCode, currencyName, currencySymbol}; + + Debug.Assert(values.Length == STATIC_DATA_PROPERTIES.Length); + staticData.Clear(); + for (int i = 0; i < data.Count; i++) + { + JsonObject obj = data[i].GetObject(); + + countryCode = obj.GetNamedString(STATIC_DATA_PROPERTIES[0]); + countryName = obj.GetNamedString(STATIC_DATA_PROPERTIES[1]); + currencyCode = obj.GetNamedString(STATIC_DATA_PROPERTIES[2]); + currencyName = obj.GetNamedString(STATIC_DATA_PROPERTIES[3]); + currencySymbol = obj.GetNamedString(STATIC_DATA_PROPERTIES[4]); + + staticData.Add(new UCM.CurrencyStaticData(countryCode, countryName, currencyCode, currencyName, currencySymbol)); + } + + // TODO - MSFT 8533667: this sort will be replaced by a WinRT call to sort localized strings + staticData.Sort((UCM.CurrencyStaticData unit1, UCM.CurrencyStaticData unit2) => { return unit1.countryName.CompareTo(unit2.countryName) < 0; }); + + return true; + } + + public bool TryParseAllRatiosData(String rawJson, CurrencyRatioMap allRatios) + { + JsonArray data = null; + if (!JsonArray.TryParse(rawJson, out data)) + { + return false; + } + + string sourceCurrencyCode = DefaultCurrencyCode; + + allRatios.Clear(); + for (int i = 0; i < data.Count; i++) + { + JsonObject obj = data[i].GetObject(); + + // Rt is ratio, An is target currency ISO code. + double relativeRatio = obj.GetNamedNumber(RATIO_KEY); + string targetCurrencyCode = obj.GetNamedString(CURRENCY_CODE_KEY); + + allRatios.Add(targetCurrencyCode, new UCM.CurrencyRatio(relativeRatio, sourceCurrencyCode, targetCurrencyCode)); + } + + return true; + } + + // FinalizeUnits + // + // There are a few ways we can get the data needed for Currency Converter, including from cache or from web. + // This function accepts the data from any source, and acts as a 'last-steps' for the converter to be ready. + // This includes identifying which units will be selected and building the map of currency ratios. + public async Task FinalizeUnits(CalculatorList staticData, CurrencyRatioMap ratioMap) + { + Dictionary> idToUnit = new Dictionary>(); + + SelectedUnits defaultCurrencies = await GetDefaultFromToCurrency(); + string fromCurrency = defaultCurrencies.Key; + string toCurrency = defaultCurrencies.Value; + + lock (m_currencyUnitsMutex) + { + + int i = 1; + m_currencyUnits.Clear(); + m_currencyMetadata.Clear(); + bool isConversionSourceSet = false; + bool isConversionTargetSet = false; + foreach (UCM.CurrencyStaticData currencyUnit in staticData) + { + if (ratioMap.TryGetValue(currencyUnit.currencyCode, out var itr) && itr.ratio > 0) + { + int id = (int)(UnitConverterUnits.UnitEnd + i); + + bool isConversionSource = (fromCurrency == currencyUnit.currencyCode); + isConversionSourceSet = isConversionSourceSet || isConversionSource; + + bool isConversionTarget = (toCurrency == currencyUnit.currencyCode); + isConversionTargetSet = isConversionTargetSet || isConversionTarget; + + UCM.Unit unit = new UCM.Unit( + id, // id + currencyUnit.currencyName, // currencyName + currencyUnit.countryName, // countryName + currencyUnit.currencyCode, // abbreviation + m_isRtlLanguage, // isRtlLanguage + isConversionSource, // isConversionSource + isConversionTarget // isConversionTarget + ); + + m_currencyUnits.PushBack(unit); + m_currencyMetadata.Add(unit, new CurrencyUnitMetadata(currencyUnit.currencySymbol)); + idToUnit.Add(unit.id, new KeyValuePair(unit, itr.ratio)); + i++; + } + } + + if (!isConversionSourceSet || !isConversionTargetSet) + { + GuaranteeSelectedUnits(); + defaultCurrencies = new SelectedUnits(DEFAULT_FROM_CURRENCY, DEFAULT_TO_CURRENCY); + } + + m_currencyRatioMap.Clear(); + foreach (var unit in m_currencyUnits) + { + Dictionary conversions = new Dictionary(EqualityComparer.Default); + double unitFactor = idToUnit[unit.id].Value; + foreach (var itr in idToUnit) + { + UCM.Unit targetUnit = (itr.Value).Key; + double conversionRatio = (itr.Value).Value; + UCM.ConversionData parsedData = new UCM.ConversionData(1.0, 0.0, false); + Debug.Assert(unitFactor > 0); // divide by zero assert + parsedData.ratio = conversionRatio / unitFactor; + conversions.Add(targetUnit, parsedData); + } + + m_currencyRatioMap.Add(unit, conversions); + } + } // unlocked m_currencyUnitsMutex + + SaveSelectedUnitsToLocalSettings(defaultCurrencies); + } + + + void GuaranteeSelectedUnits() + { + bool isConversionSourceSet = false; + bool isConversionTargetSet = false; + foreach (UCM.Unit unit in m_currencyUnits) + { + unit.isConversionSource = false; + unit.isConversionTarget = false; + + if (!isConversionSourceSet && unit.abbreviation == DEFAULT_FROM_CURRENCY) + { + unit.isConversionSource = true; + isConversionSourceSet = true; + } + + if (!isConversionTargetSet && unit.abbreviation == DEFAULT_TO_CURRENCY) + { + unit.isConversionTarget = true; + isConversionTargetSet = true; + } + } + } + + void NotifyDataLoadFinished(bool didLoad) + { + if (!didLoad) + { + m_loadStatus = CurrencyLoadStatus.FailedToLoad; + } + + if (m_vmCallback != null) + { + m_vmCallback.CurrencyDataLoadFinished(didLoad); + } + } + + void SaveLangCodeAndTimestamp() + { + ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + if (localSettings == null) + { + return; + } + + localSettings.Values[CurrencyDataLoaderConstants.CacheTimestampKey] = m_cacheTimestamp.ToString("R"); + localSettings.Values[CurrencyDataLoaderConstants.CacheLangcodeKey] = m_responseLanguage; + } + + void UpdateDisplayedTimestamp() + { + if (m_vmCallback != null) + { + string timestamp = GetCurrencyTimestamp(); + bool isWeekOld = Utils.IsDateTimeOlderThan(m_cacheTimestamp, WEEK_DURATION); + + m_vmCallback.CurrencyTimestampCallback(timestamp, isWeekOld); + } + } + + public string GetCurrencyTimestamp() + { + string timestamp = ""; + + DateTime epoch = default(DateTime); + if (m_cacheTimestamp.ToUniversalTime() != epoch.ToUniversalTime()) + { + DateTimeFormatter dateFormatter = new DateTimeFormatter("{month.abbreviated} {day.integer}, {year.full}"); + string date = dateFormatter.Format(m_cacheTimestamp); + + DateTimeFormatter timeFormatter = new DateTimeFormatter("shorttime"); + string time = timeFormatter.Format(m_cacheTimestamp); + + timestamp = LocalizationStringUtil.GetLocalizedString(m_timestampFormat, date, time); + } + + return timestamp; + } + + async Task GetDefaultFromToCurrency() + { + string fromCurrency = DEFAULT_FROM_CURRENCY; + string toCurrency = DEFAULT_TO_CURRENCY; + + // First, check if we previously stored the last used currencies. + bool foundInLocalSettings = TryGetLastUsedCurrenciesFromLocalSettings(out fromCurrency, out toCurrency); + if (!foundInLocalSettings) + { + try + { + // Second, see if the current locale has preset defaults in DefaultFromToCurrency.json. + Uri fileUri = new Uri(DEFAULT_FROM_TO_CURRENCY_FILE_URI); + StorageFile defaultFromToCurrencyFile = await StorageFile.GetFileFromApplicationUriAsync(fileUri); // TODO UNO + if (defaultFromToCurrencyFile != null) + { + String fileContents = await FileIO.ReadTextAsync(defaultFromToCurrencyFile); + JsonObject fromToObject = JsonObject.Parse(fileContents); + JsonObject regionalDefaults = fromToObject.GetNamedObject(m_responseLanguage); + + // Get both values before assignment in-case either fails. + String selectedFrom = regionalDefaults.GetNamedString(FROM_KEY); + String selectedTo = regionalDefaults.GetNamedString(TO_KEY); + + fromCurrency = selectedFrom; + toCurrency = selectedTo; + } + } + catch + { + } + } + + return new SelectedUnits(fromCurrency, toCurrency); + } + + bool TryGetLastUsedCurrenciesFromLocalSettings(out string fromCurrency, out string toCurrency) + { + fromCurrency = toCurrency = null; + + String fromKey = UnitConverterResourceKeys.CurrencyUnitFromKey; + String toKey = UnitConverterResourceKeys.CurrencyUnitToKey; + ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + if (localSettings != null && localSettings.Values != null) + { + IPropertySet values = localSettings.Values; + if (values.ContainsKey(fromKey) && values.ContainsKey(toKey)) + { + fromCurrency = (String)(values[fromKey]); + toCurrency = (String)(values[toKey]); + + return true; + } + } + + return false; + } + + void SaveSelectedUnitsToLocalSettings(SelectedUnits selectedUnits) + { + String fromKey = UnitConverterResourceKeys.CurrencyUnitFromKey; + String toKey = UnitConverterResourceKeys.CurrencyUnitToKey; + ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + if (localSettings != null && localSettings.Values != null) + { + IPropertySet values = localSettings.Values; + values[fromKey] = selectedUnits.Key; + values[toKey] = selectedUnits.Value; + } + } + } +} diff --git a/src/Calculator.Shared/DataLoaders/CurrencyHttpClient.cs b/src/Calculator.Shared/DataLoaders/CurrencyHttpClient.cs new file mode 100644 index 00000000..8472e6bf --- /dev/null +++ b/src/Calculator.Shared/DataLoaders/CurrencyHttpClient.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Windows.Foundation; +using Windows.Web.Http; + +namespace CalculatorApp.DataLoaders +{ + public class CurrencyHttpClient : ICurrencyHttpClient + { + Windows.Web.Http.HttpClient m_client; + string m_responseLanguage; + string m_sourceCurrencyCode; + + static string sc_MetadataUriLocalizeFor = "https://go.microsoft.com/fwlink/?linkid=2041093&localizeFor="; + static string sc_RatiosUriRelativeTo = "https://go.microsoft.com/fwlink/?linkid=2041339&localCurrency="; + + public CurrencyHttpClient() + { + m_client = new HttpClient(); + m_responseLanguage = "en-US"; + } + + public void SetSourceCurrencyCode(String sourceCurrencyCode) + { + m_sourceCurrencyCode = sourceCurrencyCode; + } + + public void SetResponseLanguage(String responseLanguage) + { + m_responseLanguage = responseLanguage; + } + + public IAsyncOperationWithProgress GetCurrencyMetadata() + { + string uri = sc_MetadataUriLocalizeFor + m_responseLanguage; + var metadataUri = new Uri(uri); + + return m_client.GetStringAsync(metadataUri); + } + + public IAsyncOperationWithProgress GetCurrencyRatios() + { + string uri = sc_RatiosUriRelativeTo + m_sourceCurrencyCode; + var ratiosUri = new Uri(uri); + + return m_client.GetStringAsync(ratiosUri); + } + } +} diff --git a/src/Calculator.Shared/DataLoaders/ICurrencyHttpClient.cs b/src/Calculator.Shared/DataLoaders/ICurrencyHttpClient.cs new file mode 100644 index 00000000..cf4a18f9 --- /dev/null +++ b/src/Calculator.Shared/DataLoaders/ICurrencyHttpClient.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace CalculatorApp.DataLoaders +{ + public interface ICurrencyHttpClient + { + void SetSourceCurrencyCode(string sourceCurrencyCode); + void SetResponseLanguage(string responseLanguage); + + Windows.Foundation.IAsyncOperationWithProgress GetCurrencyMetadata(); + Windows.Foundation.IAsyncOperationWithProgress GetCurrencyRatios(); + } +} diff --git a/src/Calculator.Shared/DataLoaders/UnitConverterDataConstants.cs b/src/Calculator.Shared/DataLoaders/UnitConverterDataConstants.cs new file mode 100644 index 00000000..c1452ca4 --- /dev/null +++ b/src/Calculator.Shared/DataLoaders/UnitConverterDataConstants.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace CalculatorApp.ViewModel +{ + public enum UnitConverterUnits + { + UnitStart = 0, + Area_Acre = UnitStart + 1, + Area_Hectare = UnitStart + 2, + Area_SquareCentimeter = UnitStart + 3, + Area_SquareFoot = UnitStart + 4, + Area_SquareInch = UnitStart + 5, + Area_SquareKilometer = UnitStart + 6, + Area_SquareMeter = UnitStart + 7, + Area_SquareMile = UnitStart + 8, + Area_SquareMillimeter = UnitStart + 9, + Area_SquareYard = UnitStart + 10, + Data_Bit = UnitStart + 11, + Data_Byte = UnitStart + 12, + Data_Gigabit = UnitStart + 13, + Data_Gigabyte = UnitStart + 14, + Data_Kilobit = UnitStart + 15, + Data_Kilobyte = UnitStart + 16, + Data_Megabit = UnitStart + 17, + Data_Megabyte = UnitStart + 18, + Data_Petabit = UnitStart + 19, + Data_Petabyte = UnitStart + 20, + Data_Terabit = UnitStart + 21, + Data_Terabyte = UnitStart + 22, + Energy_BritishThermalUnit = UnitStart + 23, + Energy_Calorie = UnitStart + 24, + Energy_ElectronVolt = UnitStart + 25, + Energy_FootPound = UnitStart + 26, + Energy_Joule = UnitStart + 27, + Energy_Kilocalorie = UnitStart + 28, + Energy_Kilojoule = UnitStart + 29, + Length_Centimeter = UnitStart + 30, + Length_Foot = UnitStart + 31, + Length_Inch = UnitStart + 32, + Length_Kilometer = UnitStart + 33, + Length_Meter = UnitStart + 34, + Length_Micron = UnitStart + 35, + Length_Mile = UnitStart + 36, + Length_Millimeter = UnitStart + 37, + Length_Nanometer = UnitStart + 38, + Length_NauticalMile = UnitStart + 39, + Length_Yard = UnitStart + 40, + Power_BritishThermalUnitPerMinute = UnitStart + 41, + Power_FootPoundPerMinute = UnitStart + 42, + Power_Horsepower = UnitStart + 43, + Power_Kilowatt = UnitStart + 44, + Power_Watt = UnitStart + 45, + Temperature_DegreesCelsius = UnitStart + 46, + Temperature_DegreesFahrenheit = UnitStart + 47, + Temperature_Kelvin = UnitStart + 48, + Time_Day = UnitStart + 49, + Time_Hour = UnitStart + 50, + Time_Microsecond = UnitStart + 51, + Time_Millisecond = UnitStart + 52, + Time_Minute = UnitStart + 53, + Time_Second = UnitStart + 54, + Time_Week = UnitStart + 55, + Time_Year = UnitStart + 56, + Speed_CentimetersPerSecond = UnitStart + 57, + Speed_FeetPerSecond = UnitStart + 58, + Speed_KilometersPerHour = UnitStart + 59, + Speed_Knot = UnitStart + 60, + Speed_Mach = UnitStart + 61, + Speed_MetersPerSecond = UnitStart + 62, + Speed_MilesPerHour = UnitStart + 63, + Volume_CubicCentimeter = UnitStart + 64, + Volume_CubicFoot = UnitStart + 65, + Volume_CubicInch = UnitStart + 66, + Volume_CubicMeter = UnitStart + 67, + Volume_CubicYard = UnitStart + 68, + Volume_CupUS = UnitStart + 69, + Volume_FluidOunceUK = UnitStart + 70, + Volume_FluidOunceUS = UnitStart + 71, + Volume_GallonUK = UnitStart + 72, + Volume_GallonUS = UnitStart + 73, + Volume_Liter = UnitStart + 74, + Volume_Milliliter = UnitStart + 75, + Volume_PintUK = UnitStart + 76, + Volume_PintUS = UnitStart + 77, + Volume_TablespoonUS = UnitStart + 78, + Volume_TeaspoonUS = UnitStart + 79, + Volume_QuartUK = UnitStart + 80, + Volume_QuartUS = UnitStart + 81, + Weight_Carat = UnitStart + 82, + Weight_Centigram = UnitStart + 83, + Weight_Decigram = UnitStart + 84, + Weight_Decagram = UnitStart + 85, + Weight_Gram = UnitStart + 86, + Weight_Hectogram = UnitStart + 87, + Weight_Kilogram = UnitStart + 88, + Weight_LongTon = UnitStart + 89, + Weight_Milligram = UnitStart + 90, + Weight_Ounce = UnitStart + 91, + Weight_Pound = UnitStart + 92, + Weight_ShortTon = UnitStart + 93, + Weight_Stone = UnitStart + 94, + Weight_Tonne = UnitStart + 95, + Area_SoccerField = UnitStart + 99, + Data_FloppyDisk = UnitStart + 100, + Data_CD = UnitStart + 101, + Data_DVD = UnitStart + 102, + Energy_Battery = UnitStart + 103, + Length_Paperclip = UnitStart + 105, + Length_JumboJet = UnitStart + 107, + Power_LightBulb = UnitStart + 108, + Power_Horse = UnitStart + 109, + Volume_Bathtub = UnitStart + 111, + Weight_Snowflake = UnitStart + 113, + Weight_Elephant = UnitStart + 114, + Volume_TeaspoonUK = UnitStart + 115, + Volume_TablespoonUK = UnitStart + 116, + Area_Hand = UnitStart + 118, + Speed_Turtle = UnitStart + 121, + Speed_Jet = UnitStart + 122, + Volume_CoffeeCup = UnitStart + 124, + Weight_Whale = UnitStart + 123, + Volume_SwimmingPool = UnitStart + 125, + Speed_Horse = UnitStart + 126, + Area_Paper = UnitStart + 127, + Area_Castle = UnitStart + 128, + Energy_Banana = UnitStart + 129, + Energy_SliceOfCake = UnitStart + 130, + Length_Hand = UnitStart + 131, + Power_TrainEngine = UnitStart + 132, + Weight_SoccerBall = UnitStart + 133, + Angle_Degree = UnitStart + 134, + Angle_Radian = UnitStart + 135, + Angle_Gradian = UnitStart + 136, + Pressure_Atmosphere = UnitStart + 137, + Pressure_Bar = UnitStart + 138, + Pressure_KiloPascal = UnitStart + 139, + Pressure_MillimeterOfMercury = UnitStart + 140, + Pressure_Pascal = UnitStart + 141, + Pressure_PSI = UnitStart + 142, + Data_Exabits = UnitStart + 143, + Data_Exabytes = UnitStart + 144, + Data_Exbibits = UnitStart + 145, + Data_Exbibytes = UnitStart + 146, + Data_Gibibits = UnitStart + 147, + Data_Gibibytes = UnitStart + 148, + Data_Kibibits = UnitStart + 149, + Data_Kibibytes = UnitStart + 150, + Data_Mebibits = UnitStart + 151, + Data_Mebibytes = UnitStart + 152, + Data_Pebibits = UnitStart + 153, + Data_Pebibytes = UnitStart + 154, + Data_Tebibits = UnitStart + 155, + Data_Tebibytes = UnitStart + 156, + Data_Yobibits = UnitStart + 157, + Data_Yobibytes = UnitStart + 158, + Data_Yottabit = UnitStart + 159, + Data_Yottabyte = UnitStart + 160, + Data_Zebibits = UnitStart + 161, + Data_Zebibytes = UnitStart + 162, + Data_Zetabits = UnitStart + 163, + Data_Zetabytes = UnitStart + 164, + Area_Pyeong = UnitStart + 165, + UnitEnd = Area_Pyeong + }; +} diff --git a/src/Calculator.Shared/DataLoaders/UnitConverterDataLoader.cs b/src/Calculator.Shared/DataLoaders/UnitConverterDataLoader.cs new file mode 100644 index 00000000..3f50eb12 --- /dev/null +++ b/src/Calculator.Shared/DataLoaders/UnitConverterDataLoader.cs @@ -0,0 +1,1322 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Windows.Globalization; +using CalculatorApp; +using CalculatorApp.Common; +using CalculatorApp.ViewModel; +using CategorySelectionInitializer = System.Tuple, UnitConversionManager.Unit, UnitConversionManager.Unit>; +using UnitToUnitToConversionDataMap = System.Collections.Generic.Dictionary>; +using CategoryToUnitVectorMap = System.Collections.Generic.Dictionary>; +using UCM = UnitConversionManager; + +namespace CalculatorApp.ViewModel +{ + public class OrderedUnit : UnitConversionManager.Unit + { + public OrderedUnit() + { + } + + public OrderedUnit( + UnitConverterUnits id, + string name, + string abbreviation, + int order, + bool isConversionSource = false, + bool isConversionTarget = false, + bool isWhimsical = false) + : this((int)id, name, abbreviation, order, isConversionSource, isConversionTarget, isWhimsical) + { + } + + public OrderedUnit( + int id, + string name, + string abbreviation, + int order, + bool isConversionSource = false, + bool isConversionTarget = false, + bool isWhimsical = false) + : base(id, name, abbreviation, isConversionSource, isConversionTarget, isWhimsical) + { + this.order = order; + } + + public int order; + }; + + public struct UnitData + { + public UnitData(ViewMode categoryId, UnitConverterUnits unit, double factor) + { + this.categoryId = categoryId; + this.unitId = (int)unit; + this.factor = factor; + } + + public CalculatorApp.Common.ViewMode categoryId; + public int unitId; + public double factor; + }; + + class ExplicitUnitConversionData : UnitConversionManager.ConversionData + { + //public ExplicitUnitConversionData() + //{ + //} + public ExplicitUnitConversionData( + CalculatorApp.Common.ViewMode categoryId, + UnitConverterUnits parentUnitId, + UnitConverterUnits unitId, + double ratio, + double offset, + bool offsetFirst = false) + : this(categoryId, (int)parentUnitId, (int)unitId, ratio, offset, offsetFirst) + { + + } + + + public ExplicitUnitConversionData( + CalculatorApp.Common.ViewMode categoryId, + int parentUnitId, + int unitId, + double ratio, + double offset, + bool offsetFirst = false) + : base(ratio, offset, offsetFirst) + { + this.categoryId = categoryId; + this.parentUnitId = parentUnitId; + this.unitId = unitId; + } + + public CalculatorApp.Common.ViewMode categoryId; + public int parentUnitId; + public int unitId; + }; + + class UnitConverterDataLoader : UnitConversionManager.IConverterDataLoader + { + CalculatorList m_categoryList; + CategoryToUnitVectorMap m_categoryToUnits; + UnitToUnitToConversionDataMap m_ratioMap; + string m_currentRegionCode; + + + static bool CONVERT_WITH_OFFSET_FIRST = true; + + public UnitConverterDataLoader(GeographicRegion region) + { + m_currentRegionCode = region.CodeTwoLetter; + + m_categoryList = new CalculatorList(); + m_categoryToUnits = new CategoryToUnitVectorMap(EqualityComparer.Default); + m_ratioMap = new UnitToUnitToConversionDataMap(EqualityComparer.Default); + } + + public CalculatorList LoadOrderedCategories() + { + return new CalculatorList(m_categoryList); + } + + public CalculatorList LoadOrderedUnits(UCM.Category category) + { + return m_categoryToUnits[category]; + } + + public Dictionary LoadOrderedRatios(UCM.Unit unit) + { + return m_ratioMap[unit]; + } + + public bool SupportsCategory(UCM.Category target) + { + CalculatorList supportedCategories = null; + if (!m_categoryList.IsEmpty()) + { + supportedCategories = m_categoryList; + } + else + { + GetCategories(supportedCategories); + } + + int currencyId = NavCategory.Serialize(ViewMode.Currency); + var itr = !supportedCategories.Any(category => currencyId != category.id && target.id == category.id); + return itr; + } + + public void LoadData() + { + Dictionary idToUnit = new Dictionary(); + + Dictionary> orderedUnitMap = new Dictionary>(); + Dictionary> categoryToUnitConversionDataMap = new Dictionary>(); + Dictionary> explicitConversionData = new Dictionary>(); + + // Load categories, units and conversion data into data structures. This will be then used to populate hashmaps used by CalcEngine and UI layer + GetCategories(m_categoryList); + GetUnits(orderedUnitMap); + GetConversionData(categoryToUnitConversionDataMap); + GetExplicitConversionData(explicitConversionData); // This is needed for temperature conversions + + m_categoryToUnits.Clear(); + m_ratioMap.Clear(); + foreach (UCM.Category objectCategory in m_categoryList) + { + ViewMode categoryViewMode = NavCategory.Deserialize(objectCategory.id); + Debug.Assert(NavCategory.IsConverterViewMode(categoryViewMode)); + if (categoryViewMode == ViewMode.Currency) + { + // Currency is an ordered category but we do not want to process it here + // because this function is not thread-safe and currency data is asynchronously + // loaded. + m_categoryToUnits.Add(objectCategory, new CalculatorList()); + continue; + } + + CalculatorList orderedUnits = orderedUnitMap[categoryViewMode]; + CalculatorList unitList = new CalculatorList(); + + // Sort the units by order + orderedUnits.Sort((OrderedUnit first, OrderedUnit second) => { return first.order < second.order; }); + + foreach (OrderedUnit u in orderedUnits) + { + unitList.PushBack((UCM.Unit)(u)); + idToUnit.Add(u.id, u); + } + + // Save units per category + m_categoryToUnits.Add(objectCategory, unitList); + + // For each unit, populate the conversion data + foreach (UCM.Unit unit in unitList) + { + Dictionary conversions = new Dictionary(EqualityComparer.Default); + + //if (explicitConversionData.find(unit.id) == explicitConversionData.end()) + if (!explicitConversionData.TryGetValue(unit.id, out var unitConversions)) + { + // Get the associated units for a category id + Dictionary unitConversions2 = categoryToUnitConversionDataMap[categoryViewMode]; + double unitFactor = unitConversions2[unit.id]; + + foreach (var kvp in unitConversions2) + { + var id = kvp.Key; + var conversionFactor = kvp.Value; + if (!idToUnit.ContainsKey(id)) + { + // Optional units will not be in idToUnit but can be in unitConversions. + // For optional units that did not make it to the current set of units, just continue. + continue; + } + + UCM.ConversionData parsedData = new UCM.ConversionData(1.0, 0.0, false); + Debug.Assert(conversionFactor > 0); // divide by zero assert + parsedData.ratio = unitFactor / conversionFactor; + conversions.Add(idToUnit[id], parsedData); + } + } + else + { + //Dictionary unitConversions = explicitConversionData.at(unit.id); + foreach (var itr in unitConversions) + { + conversions.Add(idToUnit[itr.Key], itr.Value); + } + } + + m_ratioMap.Add(unit, conversions); + } + } + } + + void GetCategories(CalculatorList categoriesList) + { + categoriesList.Clear(); + var converterCategory = NavCategoryGroup.CreateConverterCategory(); + foreach (var category in converterCategory.Categories) + { + /* Id, CategoryName, SupportsNegative */ + categoriesList.EmplaceBack(new UCM.Category(NavCategory.Serialize(category.Mode), category.Name, category.SupportsNegative)); + } + } + + void GetUnits(Dictionary> unitMap) + { + // US + Federated States of Micronesia, Marshall Islands, Palau + bool useUSCustomaryAndFahrenheit = + m_currentRegionCode == "US" || m_currentRegionCode == "FM" || m_currentRegionCode == "MH" || m_currentRegionCode == "PW"; + + // useUSCustomaryAndFahrenheit + Liberia + // Source: https://en.wikipedia.org/wiki/Metrication + bool useUSCustomary = useUSCustomaryAndFahrenheit || m_currentRegionCode == "LR"; + + // Use 'Système International' (International System of Units - Metrics) + bool useSI = !useUSCustomary; + + // useUSCustomaryAndFahrenheit + the Bahamas, the Cayman Islands and Liberia + // Source: http://en.wikipedia.org/wiki/Fahrenheit + bool useFahrenheit = useUSCustomaryAndFahrenheit || m_currentRegionCode == "BS" || m_currentRegionCode == "KY" || m_currentRegionCode == "LR"; + + bool useWattInsteadOfKilowatt = m_currentRegionCode == "GB"; + + // Use Pyeong, a Korean floorspace unit. + // https://en.wikipedia.org/wiki/Korean_units_of_measurement#Area + bool usePyeong = m_currentRegionCode == "KP" || m_currentRegionCode == "KR"; + + CalculatorList areaUnits = new CalculatorList(); + areaUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Area_Acre, GetLocalizedStringName("UnitName_Acre"), GetLocalizedStringName("UnitAbbreviation_Acre"), 9)); + areaUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Area_Hectare, GetLocalizedStringName("UnitName_Hectare"), GetLocalizedStringName("UnitAbbreviation_Hectare"), 4)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SquareCentimeter, + GetLocalizedStringName("UnitName_SquareCentimeter"), + GetLocalizedStringName("UnitAbbreviation_SquareCentimeter"), + 2)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SquareFoot, + GetLocalizedStringName("UnitName_SquareFoot"), + GetLocalizedStringName("UnitAbbreviation_SquareFoot"), + 7, + useSI, + useUSCustomary, + false)); + areaUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Area_SquareInch, + GetLocalizedStringName("UnitName_SquareInch"), + GetLocalizedStringName("UnitAbbreviation_SquareInch"), + 6)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SquareKilometer, + GetLocalizedStringName("UnitName_SquareKilometer"), + GetLocalizedStringName("UnitAbbreviation_SquareKilometer"), + 5)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SquareMeter, + GetLocalizedStringName("UnitName_SquareMeter"), + GetLocalizedStringName("UnitAbbreviation_SquareMeter"), + 3, + useUSCustomary, + useSI, + false)); + areaUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Area_SquareMile, + GetLocalizedStringName("UnitName_SquareMile"), + GetLocalizedStringName("UnitAbbreviation_SquareMile"), + 10)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SquareMillimeter, + GetLocalizedStringName("UnitName_SquareMillimeter"), + GetLocalizedStringName("UnitAbbreviation_SquareMillimeter"), + 1)); + areaUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Area_SquareYard, + GetLocalizedStringName("UnitName_SquareYard"), + GetLocalizedStringName("UnitAbbreviation_SquareYard"), + 8)); + areaUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Area_Hand, + GetLocalizedStringName("UnitName_Hand"), + GetLocalizedStringName("UnitAbbreviation_Hand"), + 11, + false, + false, + true)); + areaUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Area_Paper, + GetLocalizedStringName("UnitName_Paper"), + GetLocalizedStringName("UnitAbbreviation_Paper"), + 12, + false, + false, + true)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_SoccerField, + GetLocalizedStringName("UnitName_SoccerField"), + GetLocalizedStringName("UnitAbbreviation_SoccerField"), + 13, + false, + false, + true)); + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_Castle, + GetLocalizedStringName("UnitName_Castle"), + GetLocalizedStringName("UnitAbbreviation_Castle"), + 14, + false, + false, + true)); + if (usePyeong) + { + areaUnits.PushBack(new OrderedUnit(UnitConverterUnits.Area_Pyeong, + GetLocalizedStringName("UnitName_Pyeong"), + GetLocalizedStringName("UnitAbbreviation_Pyeong"), + 15, + false, + false, + false)); + } + + unitMap.Add(ViewMode.Area, areaUnits); + + CalculatorList dataUnits = new CalculatorList(); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Bit, GetLocalizedStringName("UnitName_Bit"), GetLocalizedStringName("UnitAbbreviation_Bit"), 1)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Byte, GetLocalizedStringName("UnitName_Byte"), GetLocalizedStringName("UnitAbbreviation_Byte"), 2)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Exabits, GetLocalizedStringName("UnitName_Exabits"), GetLocalizedStringName("UnitAbbreviation_Exabits"), 23)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Exabytes, + GetLocalizedStringName("UnitName_Exabytes"), + GetLocalizedStringName("UnitAbbreviation_Exabytes"), + 25)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Exbibits, + GetLocalizedStringName("UnitName_Exbibits"), + GetLocalizedStringName("UnitAbbreviation_Exbibits"), + 24)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Exbibytes, + GetLocalizedStringName("UnitName_Exbibytes"), + GetLocalizedStringName("UnitAbbreviation_Exbibytes"), + 26)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Gibibits, + GetLocalizedStringName("UnitName_Gibibits"), + GetLocalizedStringName("UnitAbbreviation_Gibibits"), + 12)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Gibibytes, + GetLocalizedStringName("UnitName_Gibibytes"), + GetLocalizedStringName("UnitAbbreviation_Gibibytes"), + 14)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Gigabit, GetLocalizedStringName("UnitName_Gigabit"), GetLocalizedStringName("UnitAbbreviation_Gigabit"), 11)); + dataUnits.PushBack(new OrderedUnit(UnitConverterUnits.Data_Gigabyte, + GetLocalizedStringName("UnitName_Gigabyte"), + GetLocalizedStringName("UnitAbbreviation_Gigabyte"), + 13, + true, + false, + false)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Kibibits, + GetLocalizedStringName("UnitName_Kibibits"), + GetLocalizedStringName("UnitAbbreviation_Kibibits"), + 4)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Kibibytes, + GetLocalizedStringName("UnitName_Kibibytes"), + GetLocalizedStringName("UnitAbbreviation_Kibibytes"), + 6)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Kilobit, GetLocalizedStringName("UnitName_Kilobit"), GetLocalizedStringName("UnitAbbreviation_Kilobit"), 3)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Kilobyte, + GetLocalizedStringName("UnitName_Kilobyte"), + GetLocalizedStringName("UnitAbbreviation_Kilobyte"), + 5)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Mebibits, + GetLocalizedStringName("UnitName_Mebibits"), + GetLocalizedStringName("UnitAbbreviation_Mebibits"), + 8)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Mebibytes, + GetLocalizedStringName("UnitName_Mebibytes"), + GetLocalizedStringName("UnitAbbreviation_Mebibytes"), + 10)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Megabit, GetLocalizedStringName("UnitName_Megabit"), GetLocalizedStringName("UnitAbbreviation_Megabit"), 7)); + dataUnits.PushBack(new OrderedUnit(UnitConverterUnits.Data_Megabyte, + GetLocalizedStringName("UnitName_Megabyte"), + GetLocalizedStringName("UnitAbbreviation_Megabyte"), + 9, + false, + true, + false)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Pebibits, + GetLocalizedStringName("UnitName_Pebibits"), + GetLocalizedStringName("UnitAbbreviation_Pebibits"), + 20)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Pebibytes, + GetLocalizedStringName("UnitName_Pebibytes"), + GetLocalizedStringName("UnitAbbreviation_Pebibytes"), + 22)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Petabit, GetLocalizedStringName("UnitName_Petabit"), GetLocalizedStringName("UnitAbbreviation_Petabit"), 19)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Petabyte, + GetLocalizedStringName("UnitName_Petabyte"), + GetLocalizedStringName("UnitAbbreviation_Petabyte"), + 21)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Tebibits, + GetLocalizedStringName("UnitName_Tebibits"), + GetLocalizedStringName("UnitAbbreviation_Tebibits"), + 16)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Tebibytes, + GetLocalizedStringName("UnitName_Tebibytes"), + GetLocalizedStringName("UnitAbbreviation_Tebibytes"), + 18)); + dataUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Data_Terabit, GetLocalizedStringName("UnitName_Terabit"), GetLocalizedStringName("UnitAbbreviation_Terabit"), 15)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Terabyte, + GetLocalizedStringName("UnitName_Terabyte"), + GetLocalizedStringName("UnitAbbreviation_Terabyte"), + 17)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Yobibits, + GetLocalizedStringName("UnitName_Yobibits"), + GetLocalizedStringName("UnitAbbreviation_Yobibits"), + 32)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Yobibytes, + GetLocalizedStringName("UnitName_Yobibytes"), + GetLocalizedStringName("UnitAbbreviation_Yobibytes"), + 34)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Yottabit, + GetLocalizedStringName("UnitName_Yottabit"), + GetLocalizedStringName("UnitAbbreviation_Yottabit"), + 31)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Yottabyte, + GetLocalizedStringName("UnitName_Yottabyte"), + GetLocalizedStringName("UnitAbbreviation_Yottabyte"), + 33)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Zebibits, + GetLocalizedStringName("UnitName_Zebibits"), + GetLocalizedStringName("UnitAbbreviation_Zebibits"), + 28)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Zebibytes, + GetLocalizedStringName("UnitName_Zebibytes"), + GetLocalizedStringName("UnitAbbreviation_Zebibytes"), + 30)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Zetabits, + GetLocalizedStringName("UnitName_Zetabits"), + GetLocalizedStringName("UnitAbbreviation_Zetabits"), + 27)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_Zetabytes, + GetLocalizedStringName("UnitName_Zetabytes"), + GetLocalizedStringName("UnitAbbreviation_Zetabytes"), + 29)); + dataUnits.PushBack(new OrderedUnit(UnitConverterUnits.Data_FloppyDisk, + GetLocalizedStringName("UnitName_FloppyDisk"), + GetLocalizedStringName("UnitAbbreviation_FloppyDisk"), + 13, + false, + false, + true)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_CD, + GetLocalizedStringName("UnitName_CD"), + GetLocalizedStringName("UnitAbbreviation_CD"), + 14, + false, + false, + true)); + dataUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Data_DVD, + GetLocalizedStringName("UnitName_DVD"), + GetLocalizedStringName("UnitAbbreviation_DVD"), + 15, + false, + false, + true)); + unitMap.Add(ViewMode.Data, dataUnits); + + CalculatorList energyUnits = new CalculatorList(); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_BritishThermalUnit, + GetLocalizedStringName("UnitName_BritishThermalUnit"), + GetLocalizedStringName("UnitAbbreviation_BritishThermalUnit"), + 7)); + energyUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Energy_Calorie, GetLocalizedStringName("UnitName_Calorie"), GetLocalizedStringName("UnitAbbreviation_Calorie"), 4)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_ElectronVolt, + GetLocalizedStringName("UnitName_Electron-Volt"), + GetLocalizedStringName("UnitAbbreviation_Electron-Volt"), + 1)); + energyUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Energy_FootPound, + GetLocalizedStringName("UnitName_Foot-Pound"), + GetLocalizedStringName("UnitAbbreviation_Foot-Pound"), + 6)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_Joule, + GetLocalizedStringName("UnitName_Joule"), + GetLocalizedStringName("UnitAbbreviation_Joule"), + 2, + true, + false, + false)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_Kilocalorie, + GetLocalizedStringName("UnitName_Kilocalorie"), + GetLocalizedStringName("UnitAbbreviation_Kilocalorie"), + 5, + false, + true, + false)); + energyUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Energy_Kilojoule, + GetLocalizedStringName("UnitName_Kilojoule"), + GetLocalizedStringName("UnitAbbreviation_Kilojoule"), + 3)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_Battery, + GetLocalizedStringName("UnitName_Battery"), + GetLocalizedStringName("UnitAbbreviation_Battery"), + 8, + false, + false, + true)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_Banana, + GetLocalizedStringName("UnitName_Banana"), + GetLocalizedStringName("UnitAbbreviation_Banana"), + 9, + false, + false, + true)); + energyUnits.PushBack(new OrderedUnit(UnitConverterUnits.Energy_SliceOfCake, + GetLocalizedStringName("UnitName_SliceOfCake"), + GetLocalizedStringName("UnitAbbreviation_SliceOfCake"), + 10, + false, + false, + true)); + unitMap.Add(ViewMode.Energy, energyUnits); + + CalculatorList lengthUnits = new CalculatorList(); + lengthUnits.PushBack(new OrderedUnit(UnitConverterUnits.Length_Centimeter, + GetLocalizedStringName("UnitName_Centimeter"), + GetLocalizedStringName("UnitAbbreviation_Centimeter"), + 4, + useUSCustomary, + useSI, + false)); + lengthUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Length_Foot, GetLocalizedStringName("UnitName_Foot"), GetLocalizedStringName("UnitAbbreviation_Foot"), 8)); + lengthUnits.PushBack(new OrderedUnit(UnitConverterUnits.Length_Inch, + GetLocalizedStringName("UnitName_Inch"), + GetLocalizedStringName("UnitAbbreviation_Inch"), + 7, + useSI, + useUSCustomary, + false)); + lengthUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Length_Kilometer, + GetLocalizedStringName("UnitName_Kilometer"), + GetLocalizedStringName("UnitAbbreviation_Kilometer"), + 6)); + lengthUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Length_Meter, GetLocalizedStringName("UnitName_Meter"), GetLocalizedStringName("UnitAbbreviation_Meter"), 5)); + lengthUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Length_Micron, GetLocalizedStringName("UnitName_Micron"), GetLocalizedStringName("UnitAbbreviation_Micron"), 2)); + lengthUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Length_Mile, GetLocalizedStringName("UnitName_Mile"), GetLocalizedStringName("UnitAbbreviation_Mile"), 10)); + lengthUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Length_Millimeter, + GetLocalizedStringName("UnitName_Millimeter"), + GetLocalizedStringName("UnitAbbreviation_Millimeter"), + 3)); + lengthUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Length_Nanometer, + GetLocalizedStringName("UnitName_Nanometer"), + GetLocalizedStringName("UnitAbbreviation_Nanometer"), + 1)); + lengthUnits.PushBack(new OrderedUnit(UnitConverterUnits.Length_NauticalMile, + GetLocalizedStringName("UnitName_NauticalMile"), + GetLocalizedStringName("UnitAbbreviation_NauticalMile"), + 11)); + lengthUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Length_Yard, GetLocalizedStringName("UnitName_Yard"), GetLocalizedStringName("UnitAbbreviation_Yard"), 9)); + lengthUnits.PushBack(new OrderedUnit(UnitConverterUnits.Length_Paperclip, + GetLocalizedStringName("UnitName_Paperclip"), + GetLocalizedStringName("UnitAbbreviation_Paperclip"), + 12, + false, + false, + true)); + lengthUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Length_Hand, + GetLocalizedStringName("UnitName_Hand"), + GetLocalizedStringName("UnitAbbreviation_Hand"), + 13, + false, + false, + true)); + lengthUnits.PushBack(new OrderedUnit(UnitConverterUnits.Length_JumboJet, + GetLocalizedStringName("UnitName_JumboJet"), + GetLocalizedStringName("UnitAbbreviation_JumboJet"), + 14, + false, + false, + true)); + unitMap.Add(ViewMode.Length, lengthUnits); + + CalculatorList powerUnits = new CalculatorList(); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_BritishThermalUnitPerMinute, + GetLocalizedStringName("UnitName_BTUPerMinute"), + GetLocalizedStringName("UnitAbbreviation_BTUPerMinute"), + 5)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_FootPoundPerMinute, + GetLocalizedStringName("UnitName_Foot-PoundPerMinute"), + GetLocalizedStringName("UnitAbbreviation_Foot-PoundPerMinute"), + 4)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_Horsepower, + GetLocalizedStringName("UnitName_Horsepower"), + GetLocalizedStringName("UnitAbbreviation_Horsepower"), + 3, + false, + true, + false)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_Kilowatt, + GetLocalizedStringName("UnitName_Kilowatt"), + GetLocalizedStringName("UnitAbbreviation_Kilowatt"), + 2, + !useWattInsteadOfKilowatt)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_Watt, + GetLocalizedStringName("UnitName_Watt"), + GetLocalizedStringName("UnitAbbreviation_Watt"), + 1, + useWattInsteadOfKilowatt)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_LightBulb, + GetLocalizedStringName("UnitName_LightBulb"), + GetLocalizedStringName("UnitAbbreviation_LightBulb"), + 6, + false, + false, + true)); + powerUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Power_Horse, + GetLocalizedStringName("UnitName_Horse"), + GetLocalizedStringName("UnitAbbreviation_Horse"), + 7, + false, + false, + true)); + powerUnits.PushBack(new OrderedUnit(UnitConverterUnits.Power_TrainEngine, + GetLocalizedStringName("UnitName_TrainEngine"), + GetLocalizedStringName("UnitAbbreviation_TrainEngine"), + 8, + false, + false, + true)); + unitMap.Add(ViewMode.Power, powerUnits); + + CalculatorList tempUnits = new CalculatorList(); + tempUnits.PushBack(new OrderedUnit(UnitConverterUnits.Temperature_DegreesCelsius, + GetLocalizedStringName("UnitName_DegreesCelsius"), + GetLocalizedStringName("UnitAbbreviation_DegreesCelsius"), + 1, + useFahrenheit, + !useFahrenheit, + false)); + tempUnits.PushBack(new OrderedUnit(UnitConverterUnits.Temperature_DegreesFahrenheit, + GetLocalizedStringName("UnitName_DegreesFahrenheit"), + GetLocalizedStringName("UnitAbbreviation_DegreesFahrenheit"), + 2, + !useFahrenheit, + useFahrenheit, + false)); + tempUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Temperature_Kelvin, + GetLocalizedStringName("UnitName_Kelvin"), + GetLocalizedStringName("UnitAbbreviation_Kelvin"), + 3)); + unitMap.Add(ViewMode.Temperature, tempUnits); + + CalculatorList timeUnits = new CalculatorList(); + timeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Time_Day, GetLocalizedStringName("UnitName_Day"), GetLocalizedStringName("UnitAbbreviation_Day"), 6)); + timeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Time_Hour, + GetLocalizedStringName("UnitName_Hour"), + GetLocalizedStringName("UnitAbbreviation_Hour"), + 5, + true, + false, + false)); + timeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Time_Microsecond, + GetLocalizedStringName("UnitName_Microsecond"), + GetLocalizedStringName("UnitAbbreviation_Microsecond"), + 1)); + timeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Time_Millisecond, + GetLocalizedStringName("UnitName_Millisecond"), + GetLocalizedStringName("UnitAbbreviation_Millisecond"), + 2)); + timeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Time_Minute, + GetLocalizedStringName("UnitName_Minute"), + GetLocalizedStringName("UnitAbbreviation_Minute"), + 4, + false, + true, + false)); + timeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Time_Second, GetLocalizedStringName("UnitName_Second"), GetLocalizedStringName("UnitAbbreviation_Second"), 3)); + timeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Time_Week, GetLocalizedStringName("UnitName_Week"), GetLocalizedStringName("UnitAbbreviation_Week"), 7)); + timeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Time_Year, GetLocalizedStringName("UnitName_Year"), GetLocalizedStringName("UnitAbbreviation_Year"), 8)); + unitMap.Add(ViewMode.Time, timeUnits); + + CalculatorList speedUnits = new CalculatorList(); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_CentimetersPerSecond, + GetLocalizedStringName("UnitName_CentimetersPerSecond"), + GetLocalizedStringName("UnitAbbreviation_CentimetersPerSecond"), + 1)); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_FeetPerSecond, + GetLocalizedStringName("UnitName_FeetPerSecond"), + GetLocalizedStringName("UnitAbbreviation_FeetPerSecond"), + 4)); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_KilometersPerHour, + GetLocalizedStringName("UnitName_KilometersPerHour"), + GetLocalizedStringName("UnitAbbreviation_KilometersPerHour"), + 3, + useUSCustomary, + useSI, + false)); + speedUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Speed_Knot, GetLocalizedStringName("UnitName_Knot"), GetLocalizedStringName("UnitAbbreviation_Knot"), 6)); + speedUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Speed_Mach, GetLocalizedStringName("UnitName_Mach"), GetLocalizedStringName("UnitAbbreviation_Mach"), 7)); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_MetersPerSecond, + GetLocalizedStringName("UnitName_MetersPerSecond"), + GetLocalizedStringName("UnitAbbreviation_MetersPerSecond"), + 2)); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_MilesPerHour, + GetLocalizedStringName("UnitName_MilesPerHour"), + GetLocalizedStringName("UnitAbbreviation_MilesPerHour"), + 5, + useSI, + useUSCustomary, + false)); + speedUnits.PushBack(new OrderedUnit(UnitConverterUnits.Speed_Turtle, + GetLocalizedStringName("UnitName_Turtle"), + GetLocalizedStringName("UnitAbbreviation_Turtle"), + 8, + false, + false, + true)); + speedUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Speed_Horse, + GetLocalizedStringName("UnitName_Horse"), + GetLocalizedStringName("UnitAbbreviation_Horse"), + 9, + false, + false, + true)); + speedUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Speed_Jet, + GetLocalizedStringName("UnitName_Jet"), + GetLocalizedStringName("UnitAbbreviation_Jet"), + 10, + false, + false, + true)); + unitMap.Add(ViewMode.Speed, speedUnits); + + CalculatorList volumeUnits = new CalculatorList(); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_CubicCentimeter, + GetLocalizedStringName("UnitName_CubicCentimeter"), + GetLocalizedStringName("UnitAbbreviation_CubicCentimeter"), + 2)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_CubicFoot, + GetLocalizedStringName("UnitName_CubicFoot"), + GetLocalizedStringName("UnitAbbreviation_CubicFoot"), + 13)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_CubicInch, + GetLocalizedStringName("UnitName_CubicInch"), + GetLocalizedStringName("UnitAbbreviation_CubicInch"), + 12)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_CubicMeter, + GetLocalizedStringName("UnitName_CubicMeter"), + GetLocalizedStringName("UnitAbbreviation_CubicMeter"), + 4)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_CubicYard, + GetLocalizedStringName("UnitName_CubicYard"), + GetLocalizedStringName("UnitAbbreviation_CubicYard"), + 14)); + volumeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Volume_CupUS, GetLocalizedStringName("UnitName_CupUS"), GetLocalizedStringName("UnitAbbreviation_CupUS"), 8)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_FluidOunceUK, + GetLocalizedStringName("UnitName_FluidOunceUK"), + GetLocalizedStringName("UnitAbbreviation_FluidOunceUK"), + 17)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_FluidOunceUS, + GetLocalizedStringName("UnitName_FluidOunceUS"), + GetLocalizedStringName("UnitAbbreviation_FluidOunceUS"), + 7)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_GallonUK, + GetLocalizedStringName("UnitName_GallonUK"), + GetLocalizedStringName("UnitAbbreviation_GallonUK"), + 20)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_GallonUS, + GetLocalizedStringName("UnitName_GallonUS"), + GetLocalizedStringName("UnitAbbreviation_GallonUS"), + 11)); + volumeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Volume_Liter, GetLocalizedStringName("UnitName_Liter"), GetLocalizedStringName("UnitAbbreviation_Liter"), 3)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_Milliliter, + GetLocalizedStringName("UnitName_Milliliter"), + GetLocalizedStringName("UnitAbbreviation_Milliliter"), + 1, + useUSCustomary, + useSI)); + volumeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Volume_PintUK, GetLocalizedStringName("UnitName_PintUK"), GetLocalizedStringName("UnitAbbreviation_PintUK"), 18)); + volumeUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Volume_PintUS, GetLocalizedStringName("UnitName_PintUS"), GetLocalizedStringName("UnitAbbreviation_PintUS"), 9)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_TablespoonUS, + GetLocalizedStringName("UnitName_TablespoonUS"), + GetLocalizedStringName("UnitAbbreviation_TablespoonUS"), + 6)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_TeaspoonUS, + GetLocalizedStringName("UnitName_TeaspoonUS"), + GetLocalizedStringName("UnitAbbreviation_TeaspoonUS"), + 5, + useSI, + useUSCustomary && m_currentRegionCode != "GB")); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_QuartUK, + GetLocalizedStringName("UnitName_QuartUK"), + GetLocalizedStringName("UnitAbbreviation_QuartUK"), + 19)); + volumeUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Volume_QuartUS, + GetLocalizedStringName("UnitName_QuartUS"), + GetLocalizedStringName("UnitAbbreviation_QuartUS"), + 10)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_TeaspoonUK, + GetLocalizedStringName("UnitName_TeaspoonUK"), + GetLocalizedStringName("UnitAbbreviation_TeaspoonUK"), + 15, + false, + useUSCustomary && m_currentRegionCode == "GB")); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_TablespoonUK, + GetLocalizedStringName("UnitName_TablespoonUK"), + GetLocalizedStringName("UnitAbbreviation_TablespoonUK"), + 16)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_CoffeeCup, + GetLocalizedStringName("UnitName_CoffeeCup"), + GetLocalizedStringName("UnitAbbreviation_CoffeeCup"), + 22, + false, + false, + true)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_Bathtub, + GetLocalizedStringName("UnitName_Bathtub"), + GetLocalizedStringName("UnitAbbreviation_Bathtub"), + 23, + false, + false, + true)); + volumeUnits.PushBack(new OrderedUnit(UnitConverterUnits.Volume_SwimmingPool, + GetLocalizedStringName("UnitName_SwimmingPool"), + GetLocalizedStringName("UnitAbbreviation_SwimmingPool"), + 24, + false, + false, + true)); + unitMap.Add(ViewMode.Volume, volumeUnits); + + CalculatorList weightUnits = new CalculatorList(); + weightUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Weight_Carat, GetLocalizedStringName("UnitName_Carat"), GetLocalizedStringName("UnitAbbreviation_Carat"), 1)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_Centigram, + GetLocalizedStringName("UnitName_Centigram"), + GetLocalizedStringName("UnitAbbreviation_Centigram"), + 3)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_Decigram, + GetLocalizedStringName("UnitName_Decigram"), + GetLocalizedStringName("UnitAbbreviation_Decigram"), + 4)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_Decagram, + GetLocalizedStringName("UnitName_Decagram"), + GetLocalizedStringName("UnitAbbreviation_Decagram"), + 6)); + weightUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Weight_Gram, GetLocalizedStringName("UnitName_Gram"), GetLocalizedStringName("UnitAbbreviation_Gram"), 5)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_Hectogram, + GetLocalizedStringName("UnitName_Hectogram"), + GetLocalizedStringName("UnitAbbreviation_Hectogram"), + 7)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_Kilogram, + GetLocalizedStringName("UnitName_Kilogram"), + GetLocalizedStringName("UnitAbbreviation_Kilogram"), + 8, + useUSCustomary, + useSI)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_LongTon, + GetLocalizedStringName("UnitName_LongTon"), + GetLocalizedStringName("UnitAbbreviation_LongTon"), + 14)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_Milligram, + GetLocalizedStringName("UnitName_Milligram"), + GetLocalizedStringName("UnitAbbreviation_Milligram"), + 2)); + weightUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Weight_Ounce, GetLocalizedStringName("UnitName_Ounce"), GetLocalizedStringName("UnitAbbreviation_Ounce"), 10)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_Pound, + GetLocalizedStringName("UnitName_Pound"), + GetLocalizedStringName("UnitAbbreviation_Pound"), + 11, + useSI, + useUSCustomary)); + weightUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Weight_ShortTon, + GetLocalizedStringName("UnitName_ShortTon"), + GetLocalizedStringName("UnitAbbreviation_ShortTon"), + 13)); + weightUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Weight_Stone, GetLocalizedStringName("UnitName_Stone"), GetLocalizedStringName("UnitAbbreviation_Stone"), 12)); + weightUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Weight_Tonne, GetLocalizedStringName("UnitName_Tonne"), GetLocalizedStringName("UnitAbbreviation_Tonne"), 9)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_Snowflake, + GetLocalizedStringName("UnitName_Snowflake"), + GetLocalizedStringName("UnitAbbreviation_Snowflake"), + 15, + false, + false, + true)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_SoccerBall, + GetLocalizedStringName("UnitName_SoccerBall"), + GetLocalizedStringName("UnitAbbreviation_SoccerBall"), + 16, + false, + false, + true)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_Elephant, + GetLocalizedStringName("UnitName_Elephant"), + GetLocalizedStringName("UnitAbbreviation_Elephant"), + 17, + false, + false, + true)); + weightUnits.PushBack(new OrderedUnit(UnitConverterUnits.Weight_Whale, + GetLocalizedStringName("UnitName_Whale"), + GetLocalizedStringName("UnitAbbreviation_Whale"), + 18, + false, + false, + true)); + unitMap.Add(ViewMode.Weight, weightUnits); + + CalculatorList pressureUnits = new CalculatorList(); + pressureUnits.PushBack(new OrderedUnit(UnitConverterUnits.Pressure_Atmosphere, + GetLocalizedStringName("UnitName_Atmosphere"), + GetLocalizedStringName("UnitAbbreviation_Atmosphere"), + 1, + true, + false, + false)); + pressureUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Pressure_Bar, + GetLocalizedStringName("UnitName_Bar"), + GetLocalizedStringName("UnitAbbreviation_Bar"), + 2, + false, + true, + false)); + pressureUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Pressure_KiloPascal, + GetLocalizedStringName("UnitName_KiloPascal"), + GetLocalizedStringName("UnitAbbreviation_KiloPascal"), + 3)); + pressureUnits.PushBack(new OrderedUnit(UnitConverterUnits.Pressure_MillimeterOfMercury, + GetLocalizedStringName("UnitName_MillimeterOfMercury "), + GetLocalizedStringName("UnitAbbreviation_MillimeterOfMercury "), + 4)); + pressureUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Pressure_Pascal, GetLocalizedStringName("UnitName_Pascal"), GetLocalizedStringName("UnitAbbreviation_Pascal"), 5)); + pressureUnits.PushBack(new OrderedUnit( + UnitConverterUnits.Pressure_PSI, + GetLocalizedStringName("UnitName_PSI"), + GetLocalizedStringName("UnitAbbreviation_PSI"), + 6, + false, + false, + false)); + unitMap.Add(ViewMode.Pressure, pressureUnits); + + CalculatorList angleUnits = new CalculatorList(); + angleUnits.PushBack(new OrderedUnit(UnitConverterUnits.Angle_Degree, + GetLocalizedStringName("UnitName_Degree"), + GetLocalizedStringName("UnitAbbreviation_Degree"), + 1, + true, + false, + false)); + angleUnits.PushBack(new OrderedUnit(UnitConverterUnits.Angle_Radian, + GetLocalizedStringName("UnitName_Radian"), + GetLocalizedStringName("UnitAbbreviation_Radian"), + 2, + false, + true, + false)); + angleUnits.PushBack( + new OrderedUnit(UnitConverterUnits.Angle_Gradian, GetLocalizedStringName("UnitName_Gradian"), GetLocalizedStringName("UnitAbbreviation_Gradian"), 3)); + unitMap.Add(ViewMode.Angle, angleUnits); + } + + void GetConversionData(Dictionary> categoryToUnitConversionMap) + { + /*categoryId, UnitId, factor*/ + CalculatorList unitDataList = new CalculatorList + { + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Acre, 4046.8564224), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareMeter, 1), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareFoot, 0.09290304), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareYard, 0.83612736), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareMillimeter, 0.000001), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareCentimeter, 0.0001), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareInch, 0.00064516), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareMile, 2589988.110336), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SquareKilometer, 1000000), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Hectare, 10000), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Hand, 0.012516104), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Paper, 0.06032246), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_SoccerField, 10869.66), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Castle, 100000), + new UnitData(ViewMode.Area, UnitConverterUnits.Area_Pyeong, 400.0 / 121.0), + + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Bit, 0.000000125), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Byte, 0.000001), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Kilobyte, 0.001), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Megabyte, 1), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Gigabyte, 1000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Terabyte, 1000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Petabyte, 1000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Exabytes, 1000000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Zetabytes, 1000000000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Yottabyte, 1000000000000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Kilobit, 0.000125), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Megabit, 0.125), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Gigabit, 125), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Terabit, 125000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Petabit, 125000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Exabits, 125000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Zetabits, 125000000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Yottabit, 125000000000000000), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Gibibits, 134.217728), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Gibibytes, 1073.741824), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Kibibits, 0.000128), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Kibibytes, 0.001024), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Mebibits, 0.131072), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Mebibytes, 1.048576), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Pebibits, 140737488.355328), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Pebibytes, 1125899906.842624), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Tebibits, 137438.953472), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Tebibytes, 1099511.627776), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Exbibits, 144115188075.855872), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Exbibytes, 1152921504606.846976), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Zebibits, 147573952589676.412928), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Zebibytes, 1180591620717411.303424), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Yobibits, 151115727451828646.838272), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_Yobibytes, 1208925819614629174.706176), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_FloppyDisk, 1.509949), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_CD, 734.003200), + new UnitData(ViewMode.Data, UnitConverterUnits.Data_DVD, 5046.586573), + + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Calorie, 4.184), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Kilocalorie, 4184), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_BritishThermalUnit, 1055.056), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Kilojoule, 1000), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_ElectronVolt, 0.0000000000000000001602176565), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Joule, 1), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_FootPound, 1.3558179483314), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Battery, 9000), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_Banana, 439614), + new UnitData(ViewMode.Energy, UnitConverterUnits.Energy_SliceOfCake, 1046700), + + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Inch, 0.0254), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Foot, 0.3048), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Yard, 0.9144), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Mile, 1609.344), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Micron, 0.000001), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Millimeter, 0.001), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Nanometer, 0.000000001), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Centimeter, 0.01), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Meter, 1), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Kilometer, 1000), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_NauticalMile, 1852), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Paperclip, 0.035052), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_Hand, 0.18669), + new UnitData(ViewMode.Length, UnitConverterUnits.Length_JumboJet, 76), + + new UnitData(ViewMode.Power, UnitConverterUnits.Power_BritishThermalUnitPerMinute, 17.58426666666667), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_FootPoundPerMinute, 0.0225969658055233), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_Watt, 1), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_Kilowatt, 1000), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_Horsepower, 745.69987158227022), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_LightBulb, 60), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_Horse, 745.7), + new UnitData(ViewMode.Power, UnitConverterUnits.Power_TrainEngine, 2982799.486329081), + + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Day, 86400), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Second, 1), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Week, 604800), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Year, 31557600), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Millisecond, 0.001), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Microsecond, 0.000001), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Minute, 60), + new UnitData(ViewMode.Time, UnitConverterUnits.Time_Hour, 3600), + + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CupUS, 236.588237), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_PintUS, 473.176473), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_PintUK, 568.26125), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_QuartUS, 946.352946), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_QuartUK, 1136.5225), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_GallonUS, 3785.411784), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_GallonUK, 4546.09), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_Liter, 1000), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_TeaspoonUS, 4.92892159375), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_TablespoonUS, 14.78676478125), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CubicCentimeter, 1), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CubicYard, 764554.857984), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CubicMeter, 1000000), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_Milliliter, 1), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CubicInch, 16.387064), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CubicFoot, 28316.846592), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_FluidOunceUS, 29.5735295625), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_FluidOunceUK, 28.4130625), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_TeaspoonUK, 5.91938802083333333333), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_TablespoonUK, 17.7581640625), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_CoffeeCup, 236.5882), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_Bathtub, 378541.2), + new UnitData(ViewMode.Volume, UnitConverterUnits.Volume_SwimmingPool, 3750000000), + + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Kilogram, 1), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Hectogram, 0.1), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Decagram, 0.01), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Gram, 0.001), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Pound, 0.45359237), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Ounce, 0.028349523125), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Milligram, 0.000001), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Centigram, 0.00001), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Decigram, 0.0001), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_LongTon, 1016.0469088), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Tonne, 1000), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Stone, 6.35029318), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Carat, 0.0002), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_ShortTon, 907.18474), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Snowflake, 0.000002), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_SoccerBall, 0.4325), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Elephant, 4000), + new UnitData(ViewMode.Weight, UnitConverterUnits.Weight_Whale, 90000), + + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_CentimetersPerSecond, 1), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_FeetPerSecond, 30.48), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_KilometersPerHour, 27.777777777777777777778), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_Knot, 51.44), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_Mach, 34030), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_MetersPerSecond, 100), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_MilesPerHour, 44.7), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_Turtle, 8.94), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_Horse, 2011.5), + new UnitData(ViewMode.Speed, UnitConverterUnits.Speed_Jet, 24585), + + new UnitData(ViewMode.Angle, UnitConverterUnits.Angle_Degree, 1), + new UnitData(ViewMode.Angle, UnitConverterUnits.Angle_Radian, 57.29577951308233), + new UnitData(ViewMode.Angle, UnitConverterUnits.Angle_Gradian, 0.9), + + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_Atmosphere, 1), + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_Bar, 0.9869232667160128), + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_KiloPascal, 0.0098692326671601), + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_MillimeterOfMercury, 0.0013155687145324), + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_Pascal, 9.869232667160128e-6), + new UnitData(ViewMode.Pressure, UnitConverterUnits.Pressure_PSI, 0.068045961016531) + }; + + // Populate the hash map and return; + foreach (UnitData unitdata in unitDataList) + { + if (!categoryToUnitConversionMap.TryGetValue(unitdata.categoryId, out var conversionData)) + { + conversionData = new Dictionary {{unitdata.unitId, unitdata.factor}}; + categoryToUnitConversionMap.Add(unitdata.categoryId, conversionData); + } + else + { + conversionData.Add(unitdata.unitId, unitdata.factor); + } + } + } + + string GetLocalizedStringName(String stringId) + { + return AppResourceProvider.GetInstance().GetResourceString(stringId); + } + + void GetExplicitConversionData(Dictionary> unitToUnitConversionList) + { + /* categoryId, ParentUnitId, UnitId, ratio, offset, offsetfirst*/ + ExplicitUnitConversionData[] conversionDataList = new[] + { + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_DegreesCelsius, UnitConverterUnits.Temperature_DegreesCelsius, 1, 0), + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_DegreesCelsius, UnitConverterUnits.Temperature_DegreesFahrenheit, 1.8, 32), + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_DegreesCelsius, UnitConverterUnits.Temperature_Kelvin, 1, 273.15), + new ExplicitUnitConversionData(ViewMode.Temperature, + UnitConverterUnits.Temperature_DegreesFahrenheit, + UnitConverterUnits.Temperature_DegreesCelsius, + 0.55555555555555555555555555555556, + -32, + CONVERT_WITH_OFFSET_FIRST), + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_DegreesFahrenheit, UnitConverterUnits.Temperature_DegreesFahrenheit, 1, 0), + new ExplicitUnitConversionData(ViewMode.Temperature, + UnitConverterUnits.Temperature_DegreesFahrenheit, + UnitConverterUnits.Temperature_Kelvin, + 0.55555555555555555555555555555556, + 459.67, + CONVERT_WITH_OFFSET_FIRST), + new ExplicitUnitConversionData(ViewMode.Temperature, + UnitConverterUnits.Temperature_Kelvin, + UnitConverterUnits.Temperature_DegreesCelsius, + 1, + -273.15, + CONVERT_WITH_OFFSET_FIRST), + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_Kelvin, UnitConverterUnits.Temperature_DegreesFahrenheit, 1.8, -459.67), + new ExplicitUnitConversionData(ViewMode.Temperature, UnitConverterUnits.Temperature_Kelvin, UnitConverterUnits.Temperature_Kelvin, 1, 0) + }; + + // Populate the hash map and return; + foreach (ExplicitUnitConversionData data in conversionDataList) + { + if (!unitToUnitConversionList.TryGetValue(data.parentUnitId, out var conversionData)) + { + conversionData = new Dictionary {{data.unitId, (UCM.ConversionData)(data)}}; + unitToUnitConversionList.Add(data.parentUnitId, conversionData); + } + else + { + conversionData.Add(data.unitId, (UCM.ConversionData)(data)); + } + } + } + } +} diff --git a/src/Calculator.Shared/ViewModels/ApplicationViewModel.cs b/src/Calculator.Shared/ViewModels/ApplicationViewModel.cs index b2b609d4..ed6d78a8 100644 --- a/src/Calculator.Shared/ViewModels/ApplicationViewModel.cs +++ b/src/Calculator.Shared/ViewModels/ApplicationViewModel.cs @@ -21,6 +21,7 @@ using System.Windows.Input; using System; using System.Diagnostics; using System.Collections.ObjectModel; +using CalculatorApp.DataLoaders; namespace CalculatorApp.ViewModel { @@ -38,10 +39,8 @@ namespace CalculatorApp.ViewModel private DateCalculatorViewModel m_DateCalcViewModel; public DateCalculatorViewModel DateCalcViewModel { get => m_DateCalcViewModel; set { m_DateCalcViewModel = value; RaisePropertyChanged("DateCalcViewModel"); } } - - // UNO TODO - //private UnitConverterViewModel m_ConverterViewModel; - //public UnitConverterViewModel ConverterViewModel { get => m_ConverterViewModel; set { m_ConverterViewModel = value; RaisePropertyChanged("ConverterViewModel"); } } + private UnitConverterViewModel m_ConverterViewModel; + public UnitConverterViewModel ConverterViewModel { get => m_ConverterViewModel; set { m_ConverterViewModel = value; RaisePropertyChanged("ConverterViewModel"); } } private CalculatorApp.Common.ViewMode m_PreviousMode; @@ -62,7 +61,7 @@ namespace CalculatorApp.ViewModel ViewMode m_mode = ViewMode.None; - ObservableCollection m_categories; + CalculatorObservableCollection m_categories; public ApplicationViewModel() { @@ -87,7 +86,7 @@ namespace CalculatorApp.ViewModel } } - public ObservableCollection Categories + public CalculatorObservableCollection Categories { get => m_categories; set @@ -166,17 +165,16 @@ namespace CalculatorApp.ViewModel } else if (NavCategory.IsConverterViewMode(m_mode)) { - // UNO TODO - //// TraceLogger.GetInstance().LogConverterModeViewed(m_mode, ApplicationView.GetApplicationViewIdForWindow(CoreWindow.GetForCurrentThread())); - //if (!m_ConverterViewModel) - //{ - // var dataLoader = new UnitConverterDataLoader(new GeographicRegion()); - // var currencyDataLoader = new CurrencyDataLoader(new CurrencyHttpClient()); - // m_ConverterViewModel = new UnitConverterViewModel(new UnitConversionManager.UnitConverter(dataLoader, currencyDataLoader)); - //} + // TraceLogger.GetInstance().LogConverterModeViewed(m_mode, ApplicationView.GetApplicationViewIdForWindow(CoreWindow.GetForCurrentThread())); + if (m_ConverterViewModel == null) + { + var dataLoader = new UnitConverterDataLoader(new GeographicRegion()); + var currencyDataLoader = new CurrencyDataLoader(new CurrencyHttpClient()); + m_ConverterViewModel = new UnitConverterViewModel(new UnitConversionManager.UnitConverter(dataLoader, currencyDataLoader)); + } - //m_ConverterViewModel.Mode = m_mode; - } + m_ConverterViewModel.Mode = m_mode; + } var resProvider = AppResourceProvider.GetInstance(); CategoryName = resProvider.GetResourceString(NavCategory.GetNameResourceKey(m_mode)); @@ -193,13 +191,11 @@ namespace CalculatorApp.ViewModel void OnCopyCommand(object parameter) { - // UNO TODO - //if (NavCategory.IsConverterViewMode(m_mode)) - //{ - // ConverterViewModel.OnCopyCommand(parameter); - //} - //else - if (NavCategory.IsDateCalculatorViewMode(m_mode)) + if (NavCategory.IsConverterViewMode(m_mode)) + { + ConverterViewModel.OnCopyCommand(parameter); + } + else if (NavCategory.IsDateCalculatorViewMode(m_mode)) { DateCalcViewModel.OnCopyCommand(parameter); } @@ -211,13 +207,11 @@ namespace CalculatorApp.ViewModel void OnPasteCommand(object parameter) { - // UNO TODO - //if (NavCategory.IsConverterViewMode(m_mode)) - //{ - // ConverterViewModel.OnPasteCommand(parameter); - //} - //else - if (NavCategory.IsCalculatorViewMode(m_mode)) + if (NavCategory.IsConverterViewMode(m_mode)) + { + ConverterViewModel.OnPasteCommand(parameter); + } + else if (NavCategory.IsCalculatorViewMode(m_mode)) { CalculatorViewModel.OnPasteCommand(parameter); } diff --git a/src/Calculator.Shared/ViewModels/HistoryViewModel.cs b/src/Calculator.Shared/ViewModels/HistoryViewModel.cs index ee9f169a..08694ceb 100644 --- a/src/Calculator.Shared/ViewModels/HistoryViewModel.cs +++ b/src/Calculator.Shared/ViewModels/HistoryViewModel.cs @@ -30,8 +30,8 @@ namespace CalculatorApp public int ItemSize { get => m_ItemSize; set { m_ItemSize = value; RaisePropertyChanged("ItemSize"); } } - private ObservableCollection m_Items; - public ObservableCollection Items { get => m_Items; set { m_Items = value; RaisePropertyChanged("Items"); } } + private CalculatorObservableCollection m_Items; + public CalculatorObservableCollection Items { get => m_Items; set { m_Items = value; RaisePropertyChanged("Items"); } } private bool m_AreHistoryShortcutsEnabled; @@ -70,7 +70,7 @@ namespace CalculatorApp AreHistoryShortcutsEnabled = true; - Items = new ObservableCollection(); + Items = new CalculatorObservableCollection(); ItemSize = 0; } @@ -97,7 +97,7 @@ namespace CalculatorApp } var historyListModel = m_calculatorManager.GetHistoryItems(m_currentMode); - var historyListVM = new ObservableCollection(); + var historyListVM = new CalculatorObservableCollection(); var localizer = LocalizationSettings.GetInstance(); if (historyListModel.Count > 0) { diff --git a/src/Calculator.Shared/ViewModels/StandardCalculatorViewModel.cs b/src/Calculator.Shared/ViewModels/StandardCalculatorViewModel.cs index 4b9d9c28..786850a0 100644 --- a/src/Calculator.Shared/ViewModels/StandardCalculatorViewModel.cs +++ b/src/Calculator.Shared/ViewModels/StandardCalculatorViewModel.cs @@ -23,6 +23,7 @@ using System.Text; using System.Linq; using System.Collections.ObjectModel; using System.Threading.Tasks; +using Calculator; namespace CalculatorApp.ViewModel { @@ -86,8 +87,8 @@ namespace CalculatorApp.ViewModel public string DisplayStringExpression { get => m_DisplayStringExpression; set { m_DisplayStringExpression = value; RaisePropertyChanged("DisplayStringExpression"); } } - private ObservableCollection m_ExpressionTokens; - public ObservableCollection ExpressionTokens { get => m_ExpressionTokens; private set { m_ExpressionTokens = value; RaisePropertyChanged("ExpressionTokens"); } } + private CalculatorObservableCollection m_ExpressionTokens; + public CalculatorObservableCollection ExpressionTokens { get => m_ExpressionTokens; private set { m_ExpressionTokens = value; RaisePropertyChanged("ExpressionTokens"); } } private string m_DecimalDisplayValue; @@ -518,7 +519,7 @@ namespace CalculatorApp.ViewModel m_BinaryDisplayValue = "0"; m_OctalDisplayValue = "0"; m_standardCalculatorManager = new CalculatorManager(ref m_calculatorDisplay, ref m_resourceProvider); - m_ExpressionTokens = new ObservableCollection(); + m_ExpressionTokens = new CalculatorObservableCollection(); m_MemorizedNumbers = new List(); m_IsMemoryEmpty = true; m_IsFToEChecked = false; @@ -618,7 +619,7 @@ namespace CalculatorApp.ViewModel if (Utils.IsLastCharacterTarget(displayValue, m_decimalSeparator)) { // remove the decimal separator, to avoid a long pause between words - localizedValue = LocalizeDisplayValue(displayValue.Substring(0, displayValue.Length - 1), isError); + localizedValue = LocalizeDisplayValue(displayValue.substr(0, displayValue.Length - 1), isError); // Use a format which has a word in the decimal separator's place // "The Display is 10 point" diff --git a/src/Calculator.Shared/ViewModels/UnitConverterViewModel.cs b/src/Calculator.Shared/ViewModels/UnitConverterViewModel.cs index 23fd72fc..2c7eb3da 100644 --- a/src/Calculator.Shared/ViewModels/UnitConverterViewModel.cs +++ b/src/Calculator.Shared/ViewModels/UnitConverterViewModel.cs @@ -1,10 +1,1843 @@ -//using System; -//using System.Collections.Generic; -//using System.Text; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -//namespace WindowsCalculator.Shared.ViewModels -//{ -// class UnitConverterViewModel -// { -// } -//} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Windows.Input; +using Windows.Foundation.Metadata; +using Windows.Globalization.NumberFormatting; +using Windows.Storage; +using Windows.System.Threading; +using Windows.UI.Core; +using Windows.UI.Xaml; +using CalculationManager; +using Calculator; +using CalculatorApp; +using CalculatorApp.Common; +using CalculatorApp.Common.Automation; +using UnitConversionManager; +using UCM = UnitConversionManager; +using CategorySelectionInitializer = System.Tuple, UnitConversionManager.Unit, UnitConversionManager.Unit>; +using UnitToUnitToConversionDataMap = System.Collections.Generic.Dictionary>; +using CategoryToUnitVectorMap = System.Collections.Generic.Dictionary>; + +namespace CalculatorApp.ViewModel +{ + + + [Windows.UI.Xaml.Data.Bindable] + public sealed class Category : INotifyPropertyChanged + { + internal Category(UnitConversionManager.Category category) + { + m_original = category; + + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged( + [System.Runtime.CompilerServices.CallerMemberName] + string p = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p)); + + public string Name + { + get { return m_original.name; } + } + + public Windows.UI.Xaml.Visibility NegateVisibility + { + get { return m_original.supportsNegative ? Windows.UI.Xaml.Visibility.Visible : Windows.UI.Xaml.Visibility.Collapsed; } + } + + internal UnitConversionManager.Category GetModelCategory() + { + return m_original; + } + + private UnitConversionManager.Category m_original; + }; + + [Windows.UI.Xaml.Data.Bindable] + public sealed class Unit : INotifyPropertyChanged + { + internal Unit(UnitConversionManager.Unit unit) + { + m_original = unit; + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged( + [System.Runtime.CompilerServices.CallerMemberName] + string p = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p)); + + public string Name + { + get { return m_original.name; } + } + + public string AccessibleName + { + get { return m_original.accessibleName; } + } + + public string Abbreviation + { + get { return m_original.abbreviation; } + } + + // This method is used to return the desired automation name for default unit in UnitConverter combo box. + public override string ToString() + { + return AccessibleName; + } + + internal UnitConversionManager.Unit GetModelUnit() + { + return m_original; + } + + private UnitConversionManager.Unit m_original; + } + + [Windows.UI.Xaml.Data.Bindable] + public sealed class SupplementaryResult : INotifyPropertyChanged + { + internal SupplementaryResult(string value, Unit unit) + { + m_Value = value; + m_Unit = unit; + + } + + public bool IsWhimsical() + { + return m_Unit.GetModelUnit().isWhimsical; + } + + public string GetLocalizedAutomationName() + { + // TODO UNO + return "TODO UNO"; + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged( + [System.Runtime.CompilerServices.CallerMemberName] + string p = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p)); + + private string m_Value; + + public string Value + { + get => m_Value; + private set + { + m_Value = value; + RaisePropertyChanged("Value"); + } + } + + + private CalculatorApp.ViewModel.Unit m_Unit; + + public CalculatorApp.ViewModel.Unit Unit + { + get => m_Unit; + private set + { + m_Unit = value; + RaisePropertyChanged("Unit"); + } + } + } + + public interface IActivatable + { + bool IsActive { get; set; } + }; + + public sealed class Activatable : IActivatable + where TActivatable : IActivatable + { + TActivatable m_activatable; + + public Activatable(TActivatable activatable) + { + m_activatable = activatable; + } + + public bool IsActive + { + get { return m_activatable.IsActive; } + set { m_activatable.IsActive = value; } + } + }; + + + [Windows.UI.Xaml.Data.Bindable] + public sealed partial class UnitConverterViewModel : INotifyPropertyChanged + { + + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged( + [System.Runtime.CompilerServices.CallerMemberName] + string p = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p)); + OnPropertyChanged(p); + } + + private CalculatorObservableCollection m_Categories; + + public CalculatorObservableCollection Categories + { + get => m_Categories; + private set + { + m_Categories = value; + RaisePropertyChanged("Categories"); + } + } + + + private Category m_CurrentCategory; + + public Category CurrentCategory + { + get => m_CurrentCategory; + set + { + m_CurrentCategory = value; + RaisePropertyChanged("CurrentCategory"); + } + } + + + private CalculatorApp.Common.ViewMode m_Mode; + + public CalculatorApp.Common.ViewMode Mode + { + get => m_Mode; + set + { + m_Mode = value; + RaisePropertyChanged("Mode"); + } + } + + + private CalculatorObservableCollection m_Units; + + public CalculatorObservableCollection Units + { + get => m_Units; + private set + { + m_Units = value; + RaisePropertyChanged("Units"); + } + } + + + private string m_CurrencySymbol1; + + public string CurrencySymbol1 + { + get => m_CurrencySymbol1; + set + { + m_CurrencySymbol1 = value; + RaisePropertyChanged("CurrencySymbol1"); + } + } + + + private Unit m_Unit1; + + public Unit Unit1 + { + get => m_Unit1; + set + { + m_Unit1 = value; + RaisePropertyChanged("Unit1"); + } + } + + + private string m_Value1; + + public string Value1 + { + get => m_Value1; + set + { + m_Value1 = value; + RaisePropertyChanged("Value1"); + } + } + + + private string m_CurrencySymbol2; + + public string CurrencySymbol2 + { + get => m_CurrencySymbol2; + set + { + m_CurrencySymbol2 = value; + RaisePropertyChanged("CurrencySymbol2"); + } + } + + + private Unit m_Unit2; + + public Unit Unit2 + { + get => m_Unit2; + set + { + m_Unit2 = value; + RaisePropertyChanged("Unit2"); + } + } + + + private string m_Value2; + + public string Value2 + { + get => m_Value2; + set + { + m_Value2 = value; + RaisePropertyChanged("Value2"); + } + } + + + private CalculatorObservableCollection m_SupplementaryResults; + + public CalculatorObservableCollection SupplementaryResults + { + get => m_SupplementaryResults; + set + { + m_SupplementaryResults = value; + RaisePropertyChanged("SupplementaryResults"); + } + } + + internal static string SupplementaryResultsPropertyName => "SupplementaryResults"; + + private bool m_Value1Active; + + public bool Value1Active + { + get => m_Value1Active; + set + { + m_Value1Active = value; + RaisePropertyChanged("Value1Active"); + } + } + + + private bool m_Value2Active; + + public bool Value2Active + { + get => m_Value2Active; + set + { + m_Value2Active = value; + RaisePropertyChanged("Value2Active"); + } + } + + + private string m_Value1AutomationName; + + public string Value1AutomationName + { + get => m_Value1AutomationName; + set + { + m_Value1AutomationName = value; + RaisePropertyChanged("Value1AutomationName"); + } + } + + + private string m_Value2AutomationName; + + public string Value2AutomationName + { + get => m_Value2AutomationName; + set + { + m_Value2AutomationName = value; + RaisePropertyChanged("Value2AutomationName"); + } + } + + + private string m_Unit1AutomationName; + + public string Unit1AutomationName + { + get => m_Unit1AutomationName; + set + { + m_Unit1AutomationName = value; + RaisePropertyChanged("Unit1AutomationName"); + } + } + + + private string m_Unit2AutomationName; + + public string Unit2AutomationName + { + get => m_Unit2AutomationName; + set + { + m_Unit2AutomationName = value; + RaisePropertyChanged("Unit2AutomationName"); + } + } + + + private CalculatorApp.Common.Automation.NarratorAnnouncement m_Announcement; + + public CalculatorApp.Common.Automation.NarratorAnnouncement Announcement + { + get => m_Announcement; + set + { + m_Announcement = value; + RaisePropertyChanged("Announcement"); + } + } + + + private bool m_IsDecimalEnabled; + + public bool IsDecimalEnabled + { + get => m_IsDecimalEnabled; + set + { + m_IsDecimalEnabled = value; + RaisePropertyChanged("IsDecimalEnabled"); + } + } + + + private bool m_IsDropDownOpen; + + public bool IsDropDownOpen + { + get => m_IsDropDownOpen; + set + { + m_IsDropDownOpen = value; + RaisePropertyChanged("IsDropDownOpen"); + } + } + + + private bool m_IsDropDownEnabled; + + public bool IsDropDownEnabled + { + get => m_IsDropDownEnabled; + set + { + m_IsDropDownEnabled = value; + RaisePropertyChanged("IsDropDownEnabled"); + } + } + + + internal const string IsCurrencyLoadingVisiblePropertyName = "IsCurrencyLoadingVisible"; + private bool m_IsCurrencyLoadingVisible; + + public bool IsCurrencyLoadingVisible + { + get => m_IsCurrencyLoadingVisible; + set + { + m_IsCurrencyLoadingVisible = value; + RaisePropertyChanged("IsCurrencyLoadingVisible"); + } + } + + + private bool m_IsCurrencyCurrentCategory; + + public bool IsCurrencyCurrentCategory + { + get => m_IsCurrencyCurrentCategory; + set + { + m_IsCurrencyCurrentCategory = value; + RaisePropertyChanged("IsCurrencyCurrentCategory"); + } + } + + + private string m_CurrencyRatioEquality; + + public string CurrencyRatioEquality + { + get => m_CurrencyRatioEquality; + set + { + m_CurrencyRatioEquality = value; + RaisePropertyChanged("CurrencyRatioEquality"); + } + } + + + private string m_CurrencyRatioEqualityAutomationName = "TODO UNO"; + + public string CurrencyRatioEqualityAutomationName + { + get => m_CurrencyRatioEqualityAutomationName; + set + { + m_CurrencyRatioEqualityAutomationName = value; + RaisePropertyChanged("CurrencyRatioEqualityAutomationName"); + } + } + + + private string m_CurrencyTimestamp; + + public string CurrencyTimestamp + { + get => m_CurrencyTimestamp; + set + { + m_CurrencyTimestamp = value; + RaisePropertyChanged("CurrencyTimestamp"); + } + } + + + internal const string NetworkBehaviorPropertyName = "NetworkBehavior"; + private CalculatorApp.NetworkAccessBehavior m_NetworkBehavior; + + public CalculatorApp.NetworkAccessBehavior NetworkBehavior + { + get => m_NetworkBehavior; + set + { + m_NetworkBehavior = value; + RaisePropertyChanged("NetworkBehavior"); + } + } + + + internal const string CurrencyDataLoadFailedPropertyName = "CurrencyDataLoadFailed"; + private bool m_CurrencyDataLoadFailed; + + public bool CurrencyDataLoadFailed + { + get => m_CurrencyDataLoadFailed; + set + { + m_CurrencyDataLoadFailed = value; + RaisePropertyChanged("CurrencyDataLoadFailed"); + } + } + + + internal const string CurrencyDataIsWeekOldPropertyName = "CurrencyDataIsWeekOld"; + private bool m_CurrencyDataIsWeekOld; + + public bool CurrencyDataIsWeekOld + { + get => m_CurrencyDataIsWeekOld; + set + { + m_CurrencyDataIsWeekOld = value; + RaisePropertyChanged("CurrencyDataIsWeekOld"); + } + } + + + + public Windows.UI.Xaml.Visibility SupplementaryVisibility + { + get { return SupplementaryResults.Count > 0 ? Windows.UI.Xaml.Visibility.Visible : Windows.UI.Xaml.Visibility.Collapsed; } + } + + public Windows.UI.Xaml.Visibility CurrencySymbolVisibility + { + get + { + return (CurrencySymbol1.IsEmpty() || CurrencySymbol2.IsEmpty()) + ? Windows.UI.Xaml.Visibility.Collapsed + : Windows.UI.Xaml.Visibility.Visible; + } + } + + public ICommand CategoryChanged { get; } + public ICommand UnitChanged { get; } + public ICommand SwitchActive { get; } + public ICommand ButtonPressed { get; } + public ICommand CopyCommand { get; } + public ICommand PasteCommand { get; } + + + string GetValueFromUnlocalized() + { + return m_valueFromUnlocalized; + } + + string GetValueToUnlocalized() + { + return m_valueToUnlocalized; + } + + UnitConversionManager.IUnitConverter m_model; + char m_decimalSeparator; + + enum ConversionParameter + { + Source, + Target + } + + ConversionParameter m_value1cp; + + private string ValueFrom + { + get { return m_value1cp == ConversionParameter.Source ? Value1 : Value2; } + set + { + if (m_value1cp == ConversionParameter.Source) Value1 = value; + else Value2 = value; + } + } + + private Unit UnitFrom + { + get { return m_value1cp == ConversionParameter.Source ? Unit1 : Unit2; } + set + { + if (m_value1cp == ConversionParameter.Source) Unit1 = value; + else Unit2 = value; + } + } + + private string ValueTo + { + get { return m_value1cp == ConversionParameter.Target ? Value1 : Value2; } + set + { + if (m_value1cp == ConversionParameter.Target) Value1 = value; + else Value2 = value; + } + } + + Unit UnitTo + { + get { return m_value1cp == ConversionParameter.Target ? Unit1 : Unit2; } + set + { + if (m_value1cp == ConversionParameter.Target) Unit1 = value; + else Unit2 = value; + } + } + + void SwitchConversionParameters() + { + m_value1cp = m_value1cp == ConversionParameter.Source ? ConversionParameter.Target : ConversionParameter.Source; + } + + bool m_isInputBlocked; + Windows.System.Threading.ThreadPoolTimer m_supplementaryResultsTimer; + bool m_resettingTimer; + + CalculatorList> m_cachedSuggestedValues; + + //Mutex m_cacheMutex; + object m_cacheMutex = new object(); + Windows.Globalization.NumberFormatting.DecimalFormatter m_decimalFormatter; + Windows.Globalization.NumberFormatting.CurrencyFormatter m_currencyFormatter; + int m_currencyMaxFractionDigits; + string m_valueFromUnlocalized; + string m_valueToUnlocalized; + + bool m_relocalizeStringOnSwitch; + + // For Saving the User Preferences only if the Unit converter ViewModel is initialised for the first time + bool m_IsFirstTime; + + string m_localizedValueFromFormat; + string m_localizedValueFromDecimalFormat; + string m_localizedValueToFormat; + string m_localizedConversionResultFormat; + string m_localizedInputUnitName; + string m_localizedOutputUnitName; + + bool m_isValue1Updating; + bool m_isValue2Updating; + string m_lastAnnouncedFrom; + string m_lastAnnouncedTo; + string m_lastAnnouncedConversionResult; + + bool m_isCurrencyDataLoaded; + + CalculatorApp.Common.ConversionResultTaskHelper m_conversionResultTaskHelper; + } + + internal class UnitConverterVMCallback : UnitConversionManager.IUnitConverterVMCallback + { + public UnitConverterVMCallback(UnitConverterViewModel viewModel) + { + m_viewModel = viewModel; + } + + public void DisplayCallback(string from, string to) + { + m_viewModel.UpdateDisplay(from, to); + } + + public void SuggestedValueCallback(CalculatorList> suggestedValues) + { + m_viewModel.UpdateSupplementaryResults(suggestedValues); + } + + public void MaxDigitsReached() + { + m_viewModel.OnMaxDigitsReached(); + } + + UnitConverterViewModel m_viewModel; + } + + internal class ViewModelCurrencyCallback : UnitConversionManager.IViewModelCurrencyCallback + { + public ViewModelCurrencyCallback(UnitConverterViewModel viewModel) + { + m_viewModel = viewModel; + } + + public void CurrencyDataLoadFinished(bool didLoad) + { + m_viewModel.OnCurrencyDataLoadFinished(didLoad); + } + + public void CurrencySymbolsCallback(string symbol1, string symbol2) + { + string sym1 = symbol1; + string sym2 = symbol2; + + bool value1Active = m_viewModel.Value1Active; + m_viewModel.CurrencySymbol1 = value1Active ? sym1 : sym2; + m_viewModel.CurrencySymbol2 = value1Active ? sym2 : sym1; + } + + public void CurrencyRatiosCallback(string ratioEquality, string accRatioEquality) + { + m_viewModel.CurrencyRatioEquality = ratioEquality; + m_viewModel.CurrencyRatioEqualityAutomationName = accRatioEquality; + } + + public void CurrencyTimestampCallback(string timestamp, bool isWeekOld) + { + m_viewModel.OnCurrencyTimestampUpdated(timestamp, isWeekOld); + } + + public void NetworkBehaviorChanged(int newBehavior) + { + m_viewModel.OnNetworkBehaviorChanged((CalculatorApp.NetworkAccessBehavior)(newBehavior)); + } + + UnitConverterViewModel m_viewModel; + }; + + public sealed partial class UnitConverterViewModel + { + const int EXPECTEDVIEWMODELDATATOKENS = 8; + + // interval is in 100 nanosecond units + const uint TIMER_INTERVAL_IN_MS = 10000; + + //#if UNIT_TESTS + //#define TIMER_CALLBACK_CONTEXT // CallbackContext.Any ==> UNO cf. _supplementaryResultsTimerContext + //#else + //#define TIMER_CALLBACK_CONTEXT // CallbackContext.Same ==> UNO cf. _supplementaryResultsTimerContext + //#endif + + private static readonly TimeSpan SUPPLEMENTARY_VALUES_INTERVAL = TimeSpan.FromTicks(10 * TIMER_INTERVAL_IN_MS); + + static Unit EMPTY_UNIT = new Unit(UCM.Unit.EMPTY_UNIT); + + private static readonly Func> UNIT_LIST = csi => csi.Item1; + private static readonly Func SELECTED_SOURCE_UNIT = csi => csi.Item2; + private static readonly Func SELECTED_TARGET_UNIT = csi => csi.Item3; + + + // x millisecond delay before we consider conversion to be final + uint CONVERSION_FINALIZED_DELAY_IN_MS = 1000; + static readonly Regex regexTrimSpacesStart = new Regex("^\\s+"); + static readonly Regex regexTrimSpacesEnd = new Regex("\\s+$"); + + const string CurrentCategoryPropertyName = "CurrentCategory"; + const string Unit1AutomationNamePropertyName = "Unit1AutomationName"; + const string Unit2AutomationNamePropertyName = "Unit2AutomationName"; + const string Unit1PropertyName = "Unit1"; + const string Unit2PropertyName = "Unit2"; + const string Value1PropertyName = "Value1"; + const string Value2PropertyName = "Value2"; + const string Value1ActivePropertyName = "Value1Active"; + const string Value2ActivePropertyName = "Value2Active"; + const string Value1AutomationNamePropertyName = "Value1AutomationName"; + const string Value2AutomationNamePropertyName = "Value2AutomationName"; + const string CurrencySymbol1PropertyName = "CurrencySymbol1"; + const string CurrencySymbol2PropertyName = "CurrencySymbol2"; + const string CurrencySymbolVisibilityPropertyName = "CurrencySymbolVisibility"; + const string SupplementaryVisibilityPropertyName = "SupplementaryVisibility"; + + public static class UnitConverterResourceKeys + { + public const string ValueFromFormat = "Format_ValueFrom"; + public const string ValueFromDecimalFormat = "Format_ValueFrom_Decimal"; + public const string ValueToFormat = "Format_ValueTo"; + public const string ConversionResultFormat = "Format_ConversionResult"; + public const string InputUnit_Name = "InputUnit_Name"; + public const string OutputUnit_Name = "OutputUnit_Name"; + public const string MaxDigitsReachedFormat = "Format_MaxDigitsReached"; + public const string UpdatingCurrencyRates = "UpdatingCurrencyRates"; + public const string CurrencyRatesUpdated = "CurrencyRatesUpdated"; + public const string CurrencyRatesUpdateFailed = "CurrencyRatesUpdateFailed"; + } + + public UnitConverterViewModel(IUnitConverter model) + { + CategoryChanged = new DelegateCommand(OnCategoryChanged); + UnitChanged = new DelegateCommand(OnUnitChanged); + SwitchActive = new DelegateCommand(OnSwitchActive); + ButtonPressed = new DelegateCommand(OnButtonPressed); + CopyCommand = new DelegateCommand(OnCopyCommand); + PasteCommand = new DelegateCommand(OnPasteCommand); + + m_model = model; + m_resettingTimer = false; + m_value1cp = ConversionParameter.Source; + m_Value1Active = true; + m_Value2Active = false; + m_Value1 = "0"; + m_Value2 = "0"; + m_valueToUnlocalized = "0"; + m_valueFromUnlocalized = "0"; + m_relocalizeStringOnSwitch = false; + m_Categories = new CalculatorObservableCollection(); + m_Units = new CalculatorObservableCollection(); + m_SupplementaryResults = new CalculatorObservableCollection(); + m_IsDropDownOpen = false; + m_IsDropDownEnabled = true; + m_IsCurrencyLoadingVisible = false; + m_isCurrencyDataLoaded = false; + m_lastAnnouncedFrom = ""; + m_lastAnnouncedTo = ""; + m_lastAnnouncedConversionResult = ""; + m_isValue1Updating = false; + m_isValue2Updating = false; + m_Announcement = null; + m_Mode = ViewMode.None; + m_CurrencySymbol1 = ""; + m_CurrencySymbol2 = ""; + m_IsCurrencyCurrentCategory = false; + m_CurrencyRatioEquality = ""; + m_CurrencyRatioEqualityAutomationName = ""; + m_isInputBlocked = false; + m_CurrencyDataLoadFailed = false; + + m_model.SetViewModelCallback(new UnitConverterVMCallback(this)); + m_model.SetViewModelCurrencyCallback(new ViewModelCurrencyCallback(this)); + m_decimalFormatter = LocalizationService.GetRegionalSettingsAwareDecimalFormatter(); + m_decimalFormatter.FractionDigits = 0; + m_decimalFormatter.IsGrouped = true; + m_decimalSeparator = LocalizationSettings.GetInstance().GetDecimalSeparator(); + + m_currencyFormatter = LocalizationService.GetRegionalSettingsAwareCurrencyFormatter(); + m_currencyFormatter.IsGrouped = true; + m_currencyFormatter.Mode = CurrencyFormatterMode.UseCurrencyCode; + m_currencyFormatter.ApplyRoundingForCurrency(RoundingAlgorithm.RoundHalfDown); + m_currencyMaxFractionDigits = m_currencyFormatter.FractionDigits; + + var resourceLoader = AppResourceProvider.GetInstance(); + m_localizedValueFromFormat = resourceLoader.GetResourceString(UnitConverterResourceKeys.ValueFromFormat); + m_localizedValueToFormat = resourceLoader.GetResourceString(UnitConverterResourceKeys.ValueToFormat); + m_localizedConversionResultFormat = resourceLoader.GetResourceString(UnitConverterResourceKeys.ConversionResultFormat); + m_localizedValueFromDecimalFormat = resourceLoader.GetResourceString(UnitConverterResourceKeys.ValueFromDecimalFormat); + m_localizedInputUnitName = resourceLoader.GetResourceString(UnitConverterResourceKeys.InputUnit_Name); + m_localizedOutputUnitName = resourceLoader.GetResourceString(UnitConverterResourceKeys.OutputUnit_Name); + + Unit1AutomationName = m_localizedInputUnitName; + Unit2AutomationName = m_localizedOutputUnitName; + IsDecimalEnabled = true; + + m_IsFirstTime = true; + m_model.Initialize(); + PopulateData(); + } + + void ResetView() + { + m_model.SendCommand(UCM.Command.Reset); + m_IsFirstTime = true; + OnCategoryChanged(null); + } + + void PopulateData() + { + InitializeView(); + } + + void OnCategoryChanged(object parameter) + { + m_model.SendCommand(UCM.Command.Clear); + ResetCategory(); + } + + void ResetCategory() + { + UCM.Category currentCategory = CurrentCategory.GetModelCategory(); + IsCurrencyCurrentCategory = currentCategory.id == NavCategory.Serialize(ViewMode.Currency); + + m_isInputBlocked = false; + SetSelectedUnits(); + + IsCurrencyLoadingVisible = m_IsCurrencyCurrentCategory && !m_isCurrencyDataLoaded; + IsDropDownEnabled = m_Units[0] != EMPTY_UNIT; + + UnitChanged.Execute(null); + } + + void SetSelectedUnits() + { + CategorySelectionInitializer categoryInitializer = m_model.SetCurrentCategory(CurrentCategory.GetModelCategory()); + BuildUnitList(UNIT_LIST(categoryInitializer)); + + UnitFrom = FindUnitInList(SELECTED_SOURCE_UNIT(categoryInitializer)); + UnitTo = FindUnitInList(SELECTED_TARGET_UNIT(categoryInitializer)); + } + + void BuildUnitList(CalculatorList modelUnitList) + { + m_Units.Clear(); + foreach (UCM.Unit modelUnit in modelUnitList) + { + if (!modelUnit.isWhimsical) + { + m_Units.Append(new Unit(modelUnit)); + } + } + + if (m_Units.Count == 0) + { + m_Units.Append(EMPTY_UNIT); + } + } + + Unit FindUnitInList(UCM.Unit target) + { + foreach (Unit vmUnit in m_Units) + { + UCM.Unit modelUnit = vmUnit.GetModelUnit(); + if (modelUnit.id == target.id) + { + return vmUnit; + } + } + + return EMPTY_UNIT; + } + + void OnUnitChanged(object parameter) + { + if ((m_Unit1 == null) || (m_Unit2 == null)) + { + // Return if both Unit1 & Unit2 are not set + return; + } + + m_model.SetCurrentUnitTypes(UnitFrom.GetModelUnit(), UnitTo.GetModelUnit()); + if (m_supplementaryResultsTimer != null) + { + // End timer to show results immediately + m_supplementaryResultsTimer.Cancel(); + } + + if (!m_IsFirstTime) + { + SaveUserPreferences(); + } + else + { + RestoreUserPreferences(); + m_IsFirstTime = false; + } + } + + void OnSwitchActive(object unused) + { + // this can be false if this switch occurs without the user having explicitly updated any strings + // (for example, during deserialization). We only want to try this cleanup if there's actually + // something to clean up. + if (m_relocalizeStringOnSwitch) + { + // clean up any ill-formed strings that were in progress before the switch + ValueFrom = ConvertToLocalizedString(m_valueFromUnlocalized, false); + } + + SwitchConversionParameters(); + // Now deactivate the other + if (m_value1cp == ConversionParameter.Source) + { + Value2Active = false; + } + else + { + Value1Active = false; + } + + Utils.Swap(ref m_valueFromUnlocalized, ref m_valueToUnlocalized); + Utils.Swap(ref m_localizedValueFromFormat, ref m_localizedValueToFormat); + + Utils.Swap(ref m_Unit1AutomationName, ref m_Unit2AutomationName); + RaisePropertyChanged(Unit1AutomationNamePropertyName); + RaisePropertyChanged(Unit2AutomationNamePropertyName); + + m_isInputBlocked = false; + m_model.SwitchActive(m_valueFromUnlocalized); + } + + string ConvertToLocalizedString(string stringToLocalize, bool allowPartialStrings) + { + string result = null; + + if (stringToLocalize.empty()) + { + return result; + } + + m_decimalFormatter.IsDecimalPointAlwaysDisplayed = false; + m_decimalFormatter.FractionDigits = 0; + m_currencyFormatter.IsDecimalPointAlwaysDisplayed = false; + m_currencyFormatter.FractionDigits = 0; + + var posOfE = stringToLocalize.find('e'); + if (posOfE != "".npos()) + { + int posOfSign = posOfE + 1; + char signOfE = stringToLocalize.at(posOfSign); + string significandStr = stringToLocalize.substr(0, posOfE); + string exponentStr = stringToLocalize.substr(posOfSign + 1, stringToLocalize.length() - posOfSign); + + result += ConvertToLocalizedString(significandStr, allowPartialStrings) + "e" + signOfE + ConvertToLocalizedString(exponentStr, allowPartialStrings); + } + else + { + // stringToLocalize is in en-US and has the default decimal separator, so this is safe to do. + int posOfDecimal = stringToLocalize.find('.'); + + bool hasDecimal = "".npos() != posOfDecimal; + + if (hasDecimal) + { + if (allowPartialStrings) + { + // allow "in progress" strings, like "3." that occur during the composition of + // a final number. Without this, when typing the three characters in "3.2" + // you don't see the decimal point when typing it, you only see it once you've finally + // typed a post-decimal digit. + + m_decimalFormatter.IsDecimalPointAlwaysDisplayed = true; + m_currencyFormatter.IsDecimalPointAlwaysDisplayed = true; + } + + // force post-decimal digits so that trailing zeroes entered by the user aren't suddenly cut off. + m_decimalFormatter.FractionDigits = (int)(stringToLocalize.length() - (posOfDecimal + 1)); + m_currencyFormatter.FractionDigits = m_currencyMaxFractionDigits; + } + + if (IsCurrencyCurrentCategory) + { + string currencyResult = m_currencyFormatter.Format(System.Convert.ToDouble(stringToLocalize)); + string currencyCode = m_currencyFormatter.Currency; + + // CurrencyFormatter always includes LangCode or Symbol. Make it include LangCode + // because this includes a non-breaking space. Remove the LangCode. + var pos = currencyResult.find(currencyCode); + if (pos != "".npos()) + { + currencyResult = currencyResult.Remove(pos, currencyCode.length()); + currencyResult = regexTrimSpacesStart.Replace(currencyResult, ""); + currencyResult = regexTrimSpacesEnd.Replace(currencyResult, ""); + } + + result = currencyResult; + } + else + { + // Convert the input string to double using stod + // Then use the decimalFormatter to reformat the double to Platform String + result = m_decimalFormatter.Format(System.Convert.ToDouble(stringToLocalize)); + } + + if (hasDecimal) + { + // Since the output from GetLocaleInfoEx() and DecimalFormatter are differing for decimal string + // we are adding the below work-around of editing the string returned by DecimalFormatter + // and replacing the decimal separator with the one returned by GetLocaleInfoEx() + String formattedSampleString = m_decimalFormatter.Format(System.Convert.ToDouble("1.1")); + string formattedSampleWString = formattedSampleString; + + string resultWithDecimal = result; + resultWithDecimal = resultWithDecimal.Replace(formattedSampleWString[1], m_decimalSeparator); + + // Copy back the edited string to the result + result = resultWithDecimal; + } + } + + string resultHolder = result; + if ((stringToLocalize.front() == '-' && System.Convert.ToDouble(stringToLocalize) == 0) || resultHolder.back() == '-') + { + if (resultHolder.back() == '-') + { + result = resultHolder.Remove(resultHolder.size() - 1, 1); + } + + result = "-" + result; + } + + result = Utils.LRE + result + Utils.PDF; + return result; + } + + void DisplayPasteError() + { + String errorMsg = AppResourceProvider.GetInstance().GetCEngineString(EngineStrings.SIDS_DOMAIN); /*SIDS_DOMAIN is for "invalid input"*/ + Value1 = errorMsg; + Value2 = errorMsg; + m_relocalizeStringOnSwitch = false; + } + + internal void UpdateDisplay(string from, string to) + { + String fromStr = this.ConvertToLocalizedString(from, true); + UpdateInputBlocked(from); + String toStr = this.ConvertToLocalizedString(to, true); + + bool updatedValueFrom = ValueFrom != fromStr; + bool updatedValueTo = ValueTo != toStr; + if (updatedValueFrom) + { + m_valueFromUnlocalized = from; + // once we've updated the unlocalized from string, we'll potentially need to clean it back up when switching between fields + // to eliminate dangling decimal points. + m_relocalizeStringOnSwitch = true; + } + + if (updatedValueTo) + { + // This is supposed to use trimming logic, but that's highly dependent + // on the auto-scaling textbox control which we dont have yet. For now, + // not doing anything. It will have to be integrated once that control is + // created. + m_valueToUnlocalized = to; + } + + m_isValue1Updating = m_Value1Active ? updatedValueFrom : updatedValueTo; + m_isValue2Updating = m_Value2Active ? updatedValueFrom : updatedValueTo; + + // Setting these properties before setting the member variables above causes + // a chain of properties that can result in the wrong result being announced + // to Narrator. We need to know which values are updating before setting the + // below properties, so that we know when to announce the result. + if (updatedValueFrom) + { + ValueFrom = fromStr; + } + + if (updatedValueTo) + { + ValueTo = toStr; + } + } + + internal void UpdateSupplementaryResults(CalculatorList> suggestedValues) + { + lock (m_cacheMutex) + { + m_cachedSuggestedValues = suggestedValues; + } + + // If we're already "ticking", reset the timer + if (m_supplementaryResultsTimer != null) + { + m_resettingTimer = true; + m_supplementaryResultsTimer.Cancel(); + m_resettingTimer = false; + } + + // Schedule the timer + m_supplementaryResultsTimer = ThreadPoolTimer.CreateTimer( + new TimerElapsedHandler(SupplementaryResultsTimerTick), + SUPPLEMENTARY_VALUES_INTERVAL, + new TimerDestroyedHandler(SupplementaryResultsTimerCancel)); // UNO: TIMER_CALLBACK_CONTEXT: cf. _supplementaryResultsTimerContext + } + + public void OnValueActivated(IActivatable control) + { + control.IsActive = true; + } + + void OnButtonPressed(object parameter) + { + NumbersAndOperatorsEnum numOpEnum = CalculatorButtonPressedEventArgs.GetOperationFromCommandParameter(parameter); + UCM.Command command = CommandFromButtonId(numOpEnum); + + // Don't clear the display if combo box is open and escape is pressed + if (command == UCM.Command.Clear && IsDropDownOpen) + { + return; + } + + CalculatorList OPERANDS = new CalculatorList + { + UCM.Command.Zero, UCM.Command.One, UCM.Command.Two, UCM.Command.Three, UCM.Command.Four, + UCM.Command.Five, UCM.Command.Six, UCM.Command.Seven, UCM.Command.Eight, UCM.Command.Nine + }; + + if (OPERANDS.IndexOf(command) != -1) + { + if (m_isInputBlocked) + { + return; + } + + if (m_IsCurrencyCurrentCategory) + { + StartConversionResultTimer(); + } + } + + m_model.SendCommand(command); + } + + public void OnCopyCommand(object parameter) + { + // EventWriteClipboardCopy_Start(); + CopyPasteManager.CopyToClipboard(m_valueFromUnlocalized); + // EventWriteClipboardCopy_Stop(); + } + + public async void OnPasteCommand(object parameter) + { + // if there's nothing to copy early out + if (!CopyPasteManager.HasStringToPaste()) + { + return; + } + + // Ensure that the paste happens on the UI thread + // EventWriteClipboardPaste_Start(); + // Any converter ViewMode is fine here. + var pastedString = await CopyPasteManager.GetStringToPaste(m_Mode, NavCategory.GetGroupType(m_Mode), default(int), default(int)); + OnPaste(pastedString, m_Mode); + } + + void InitializeView() + { + CalculatorList categories = m_model.GetCategories(); + for (uint i = 0; i < categories.Size(); i++) + { + Category category = new Category(categories[i]); + m_Categories.Append(category); + } + + RestoreUserPreferences(); + CurrentCategory = new Category(m_model.GetCurrentCategory()); + } + + static bool isCategoryChanging = false; + + void OnPropertyChanged(string prop) + { + if (prop == CurrentCategoryPropertyName) + { + isCategoryChanging = true; + CategoryChanged.Execute(null); + isCategoryChanging = false; + } + else if (prop == Unit1PropertyName || prop == Unit2PropertyName) + { + // Category changes will handle updating units after they've both been updated. + // This event should only be used to update units from explicit user interaction. + if (!isCategoryChanging) + { + UnitChanged.Execute(null); + } + + // Get the localized automation name for each CalculationResults field + if (prop == Unit1PropertyName) + { + UpdateValue1AutomationName(); + } + else + { + UpdateValue2AutomationName(); + } + } + else if (prop == Value1PropertyName) + { + UpdateValue1AutomationName(); + } + else if (prop == Value2PropertyName) + { + UpdateValue2AutomationName(); + } + else if (prop == Value1ActivePropertyName || prop == Value2ActivePropertyName) + { + // if one of the values is activated, and as a result both are true, it means + // that we're trying to switch. + if (Value1Active && Value2Active) + { + SwitchActive.Execute(null); + } + + UpdateValue1AutomationName(); + UpdateValue2AutomationName(); + } + else if (prop == SupplementaryResultsPropertyName) + { + RaisePropertyChanged(SupplementaryVisibilityPropertyName); + } + else if (prop == Value1AutomationNamePropertyName) + { + m_isValue1Updating = false; + if (!m_isValue2Updating) + { + AnnounceConversionResult(); + } + } + else if (prop == Value2AutomationNamePropertyName) + { + m_isValue2Updating = false; + if (!m_isValue1Updating) + { + AnnounceConversionResult(); + } + } + else if (prop == CurrencySymbol1PropertyName || prop == CurrencySymbol2PropertyName) + { + RaisePropertyChanged(CurrencySymbolVisibilityPropertyName); + } + } + + String Serialize() + { + using (var @out = new StringWriter()) + { + const string delimiter = "[;;;]"; + @out.Write(m_resettingTimer); + @out.Write(delimiter); + @out.Write((int)(m_value1cp)); + @out.Write(delimiter); + @out.Write(m_Value1Active); + @out.Write(delimiter); + @out.Write(m_Value2Active); + @out.Write(delimiter); + @out.Write(m_Value1); + @out.Write(delimiter); + @out.Write(m_Value2); + @out.Write(delimiter); + @out.Write(m_valueFromUnlocalized); + @out.Write(delimiter); + @out.Write(m_valueToUnlocalized); + @out.Write(delimiter); + @out.Write("[###]"); + string unitConverterSerializedData = m_model.Serialize(); + + if (!unitConverterSerializedData.empty()) + { + @out.Write(m_model.Serialize()); + @out.Write("[###]"); + String serializedData = @out.ToString(); + return serializedData; + } + + return null; + } + } + + void Deserialize(string state) + { + string serializedData = state; + CalculatorList tokens = UCM.UnitConverter.StringToVector(serializedData, "[###]"); + Debug.Assert(tokens.Size() >= 2); + CalculatorList viewModelData = UCM.UnitConverter.StringToVector(tokens[0], "[;;;]"); + Debug.Assert(viewModelData.Size() == EXPECTEDVIEWMODELDATATOKENS); + m_resettingTimer = (viewModelData[0].CompareTo("1") == 0); + m_value1cp = (ConversionParameter)System.Convert.ToInt32(viewModelData[1]); + m_Value1Active = (viewModelData[2].CompareTo("1") == 0); + m_Value2Active = (viewModelData[3].CompareTo("1") == 0); + m_Value1 = viewModelData[4]; + m_Value2 = viewModelData[5]; + m_valueFromUnlocalized = viewModelData[6]; + m_valueToUnlocalized = viewModelData[7]; + string modelData = string.Empty; + for (uint i = 1; i < tokens.Size(); i++) + { + modelData += tokens[i] + "[###]"; + } + + m_model.DeSerialize(modelData); + InitializeView(); + RaisePropertyChanged(null); // Update since all props have been updated. + } + + // Saving User Preferences of Category and Associated-Units across Sessions. + void SaveUserPreferences() + { + if (UnitsAreValid()) + { + ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + if (!m_IsCurrencyCurrentCategory) + { + var userPreferences = m_model.SaveUserPreferences(); + localSettings.Values["UnitConverterPreferences"] = userPreferences; + } + else + { + // Currency preferences shouldn't be saved in the same way as standard converter modes because + // the delay loading creates a big mess of issues that are better to avoid. + localSettings.Values[CalculatorApp.ViewModel.UnitConverterResourceKeys.CurrencyUnitFromKey] = UnitFrom.Abbreviation; + localSettings.Values[CalculatorApp.ViewModel.UnitConverterResourceKeys.CurrencyUnitToKey] = UnitTo.Abbreviation; + } + } + } + + // Restoring User Preferences of Category and Associated-Units. + void RestoreUserPreferences() + { + if (!IsCurrencyCurrentCategory) + { + ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + if (localSettings.Values.ContainsKey("UnitConverterPreferences")) + { + var userPreferences = (string)localSettings.Values["UnitConverterPreferences"]; + m_model.RestoreUserPreferences(userPreferences); + } + } + } + + internal void OnCurrencyDataLoadFinished(bool didLoad) + { + m_isCurrencyDataLoaded = true; + CurrencyDataLoadFailed = !didLoad; + m_model.ResetCategoriesAndRatios(); + m_model.Calculate(); + ResetCategory(); + + string key = didLoad ? UnitConverterResourceKeys.CurrencyRatesUpdated : UnitConverterResourceKeys.CurrencyRatesUpdateFailed; + String announcement = AppResourceProvider.GetInstance().GetResourceString(key); + Announcement = CalculatorAnnouncement.GetUpdateCurrencyRatesAnnouncement(announcement); + } + + internal void OnCurrencyTimestampUpdated(string timestamp, bool isWeekOld) + { + CurrencyDataIsWeekOld = isWeekOld; + CurrencyTimestamp = timestamp; + } + + public async void RefreshCurrencyRatios() + { + m_isCurrencyDataLoaded = false; + IsCurrencyLoadingVisible = true; + + String announcement = AppResourceProvider.GetInstance().GetResourceString(UnitConverterResourceKeys.UpdatingCurrencyRates); + Announcement = CalculatorAnnouncement.GetUpdateCurrencyRatesAnnouncement(announcement); + + var refreshTask = m_model.RefreshCurrencyRatios(); + var refreshResult = await refreshTask; + bool didLoad = refreshResult.Key; + string timestamp = refreshResult.Value; + + OnCurrencyTimestampUpdated(timestamp, false /*isWeekOldData*/); + OnCurrencyDataLoadFinished(didLoad); + } + + internal void OnNetworkBehaviorChanged(NetworkAccessBehavior newBehavior) + { + CurrencyDataLoadFailed = false; + NetworkBehavior = newBehavior; + } + + UnitConversionManager.Command CommandFromButtonId(NumbersAndOperatorsEnum button) + { + UCM.Command command; + + switch (button) + { + case NumbersAndOperatorsEnum.Zero: + command = UCM.Command.Zero; + break; + case NumbersAndOperatorsEnum.One: + command = UCM.Command.One; + break; + case NumbersAndOperatorsEnum.Two: + command = UCM.Command.Two; + break; + case NumbersAndOperatorsEnum.Three: + command = UCM.Command.Three; + break; + case NumbersAndOperatorsEnum.Four: + command = UCM.Command.Four; + break; + case NumbersAndOperatorsEnum.Five: + command = UCM.Command.Five; + break; + case NumbersAndOperatorsEnum.Six: + command = UCM.Command.Six; + break; + case NumbersAndOperatorsEnum.Seven: + command = UCM.Command.Seven; + break; + case NumbersAndOperatorsEnum.Eight: + command = UCM.Command.Eight; + break; + case NumbersAndOperatorsEnum.Nine: + command = UCM.Command.Nine; + break; + case NumbersAndOperatorsEnum.Decimal: + command = UCM.Command.Decimal; + break; + case NumbersAndOperatorsEnum.Negate: + command = UCM.Command.Negate; + break; + case NumbersAndOperatorsEnum.Backspace: + command = UCM.Command.Backspace; + break; + case NumbersAndOperatorsEnum.Clear: + command = UCM.Command.Clear; + break; + default: + command = UCM.Command.None; + break; + } + + return command; + } + + void SupplementaryResultsTimerTick(ThreadPoolTimer timer) + { + timer.Cancel(); + } + + // UNO: We cannot specify the resturning context of ThreadPoolTime in C#. + // Instead we capture the dispatcher on which this VM has been created, and send back the UI update on it + private readonly CoreDispatcher _supplementaryResultsTimerContext = Window.Current.Dispatcher; + + void SupplementaryResultsTimerCancel(ThreadPoolTimer timer) + { + if (!m_resettingTimer) + { + _supplementaryResultsTimerContext.RunAsync(CoreDispatcherPriority.Normal, RefreshSupplementaryResults); + } + } + + void RefreshSupplementaryResults() + { + lock (m_cacheMutex) + { + m_SupplementaryResults.Clear(); + + CalculatorList whimsicals = new CalculatorList(); + + foreach (Tuple suggestedValue in m_cachedSuggestedValues) + { + SupplementaryResult result = new SupplementaryResult(this.ConvertToLocalizedString(suggestedValue.Item1, false), new Unit(suggestedValue.Item2)); + if (result.IsWhimsical()) + { + whimsicals.PushBack(result); + } + else + { + m_SupplementaryResults.Append(result); + } + } + + if (whimsicals.Size() > 0) + { + m_SupplementaryResults.Append(whimsicals[0]); + } + + } + + RaisePropertyChanged(SupplementaryResultsPropertyName); + // EventWriteConverterSupplementaryResultsUpdated(); + } + + // When UpdateDisplay is called, the ViewModel will remember the From/To unlocalized display values + // This function will announce the conversion result after the ValueTo/ValueFrom automation names update, + // only if the new unlocalized display values are different from the last announced values, and if the + // values are not both zero. + void AnnounceConversionResult() + { + if ((m_valueFromUnlocalized != m_lastAnnouncedFrom || m_valueToUnlocalized != m_lastAnnouncedTo) && Unit1 != null && Unit2 != null) + { + m_lastAnnouncedFrom = m_valueFromUnlocalized; + m_lastAnnouncedTo = m_valueToUnlocalized; + + Unit unitFrom = Value1Active ? Unit1 : Unit2; + Unit unitTo = (unitFrom == Unit1) ? Unit2 : Unit1; + m_lastAnnouncedConversionResult = GetLocalizedConversionResultStringFormat(ValueFrom, unitFrom.Name, ValueTo, unitTo.Name); + + Announcement = CalculatorAnnouncement.GetDisplayUpdatedAnnouncement(m_lastAnnouncedConversionResult); + } + } + + void UpdateInputBlocked(string currencyInput) + { + // currencyInput is in en-US and has the default decimal separator, so this is safe to do. + var posOfDecimal = currencyInput.find('.'); + m_isInputBlocked = false; + if (posOfDecimal != "".npos() && IsCurrencyCurrentCategory) + { + m_isInputBlocked = (posOfDecimal + (int)(m_currencyMaxFractionDigits) + 1 == currencyInput.length()); + } + } + + NumbersAndOperatorsEnum MapCharacterToButtonId(char ch, bool canSendNegate) + { + Debug.Assert(NumbersAndOperatorsEnum.Zero < NumbersAndOperatorsEnum.One, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.One < NumbersAndOperatorsEnum.Two, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Two < NumbersAndOperatorsEnum.Three, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Three < NumbersAndOperatorsEnum.Four, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Four < NumbersAndOperatorsEnum.Five, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Five < NumbersAndOperatorsEnum.Six, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Six < NumbersAndOperatorsEnum.Seven, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Seven < NumbersAndOperatorsEnum.Eight, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Eight < NumbersAndOperatorsEnum.Nine, "NumbersAndOperatorsEnum order is invalid"); + Debug.Assert(NumbersAndOperatorsEnum.Zero < NumbersAndOperatorsEnum.Nine, "NumbersAndOperatorsEnum order is invalid"); + + NumbersAndOperatorsEnum mappedValue = NumbersAndOperatorsEnum.None; + canSendNegate = false; + + switch (ch) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + mappedValue = (int)NumbersAndOperatorsEnum.Zero + (NumbersAndOperatorsEnum)(ch - '0'); + canSendNegate = true; + break; + + case '-': + mappedValue = NumbersAndOperatorsEnum.Negate; + break; + + default: + // Respect the user setting for decimal separator + if (ch == m_decimalSeparator) + { + mappedValue = NumbersAndOperatorsEnum.Decimal; + canSendNegate = true; + break; + } + + break; + } + + if (mappedValue == NumbersAndOperatorsEnum.None) + { + if (LocalizationSettings.GetInstance().IsLocalizedDigit(ch)) + { + mappedValue = (int)NumbersAndOperatorsEnum.Zero + + (NumbersAndOperatorsEnum)(ch - LocalizationSettings.GetInstance().GetDigitSymbolFromEnUsDigit('0')); + canSendNegate = true; + } + } + + return mappedValue; + } + + public void OnPaste(String stringToPaste, ViewMode mode) + { + // If pastedString is invalid("NoOp") then display pasteError else process the string + if (stringToPaste == CopyPasteManager.PasteErrorString) + { + this.DisplayPasteError(); + return; + } + + TraceLogger.GetInstance().LogValidInputPasted(mode); + bool isFirstLegalChar = true; + bool sendNegate = false; + string accumulation = ""; + + foreach (char it in stringToPaste) + { + bool canSendNegate = false; + + NumbersAndOperatorsEnum op = MapCharacterToButtonId(it, canSendNegate); + + if (NumbersAndOperatorsEnum.None != op) + { + if (isFirstLegalChar) + { + // Send Clear before sending something that will actually apply + // to the field. + m_model.SendCommand(UCM.Command.Clear); + isFirstLegalChar = false; + + // If the very first legal character is a - sign, send negate + // after sending the next legal character. Send nothing now, or + // it will be ignored. + if (NumbersAndOperatorsEnum.Negate == op) + { + sendNegate = true; + } + } + + // Negate is only allowed if it's the first legal character, which is handled above. + if (NumbersAndOperatorsEnum.Negate != op) + { + UCM.Command cmd = CommandFromButtonId(op); + m_model.SendCommand(cmd); + + if (sendNegate) + { + if (canSendNegate) + { + m_model.SendCommand(UCM.Command.Negate); + } + + sendNegate = false; + } + } + + accumulation += it; + UpdateInputBlocked(accumulation); + if (m_isInputBlocked) + { + break; + } + } + else + { + sendNegate = false; + } + } + } + + String GetLocalizedAutomationName(String displayvalue, String unitname, String format) + { + String valueToLocalize = displayvalue; + if (displayvalue == ValueFrom && Utils.IsLastCharacterTarget(m_valueFromUnlocalized, m_decimalSeparator)) + { + // Need to compute a second localized value for the automation + // name that does not include the decimal separator. + displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/); + format = m_localizedValueFromDecimalFormat; + } + + string localizedResult = LocalizationStringUtil.GetLocalizedString(format, displayvalue, unitname); + return localizedResult; + } + + String GetLocalizedConversionResultStringFormat( + String fromValue, + String fromUnit, + String toValue, + String toUnit) + { + String localizedString = LocalizationStringUtil.GetLocalizedString(m_localizedConversionResultFormat, fromValue, fromUnit, toValue, toUnit); + return localizedString; + } + + void UpdateValue1AutomationName() + { + if (Unit1 != null) + { + Value1AutomationName = GetLocalizedAutomationName(Value1, Unit1.AccessibleName, m_localizedValueFromFormat); + } + } + + void UpdateValue2AutomationName() + { + if (Unit2 != null) + { + Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2.AccessibleName, m_localizedValueToFormat); + } + } + + internal void OnMaxDigitsReached() + { + String format = AppResourceProvider.GetInstance().GetResourceString(UnitConverterResourceKeys.MaxDigitsReachedFormat); + string announcement = LocalizationStringUtil.GetLocalizedString(format, m_lastAnnouncedConversionResult); + Announcement = CalculatorAnnouncement.GetMaxDigitsReachedAnnouncement(announcement); + } + + bool UnitsAreValid() + { + return UnitFrom != null && !UnitFrom.Abbreviation.IsEmpty() && UnitTo != null && !UnitTo.Abbreviation.IsEmpty(); + } + + void StartConversionResultTimer() + { + m_conversionResultTaskHelper = new ConversionResultTaskHelper(CONVERSION_FINALIZED_DELAY_IN_MS, + () => + { + if (UnitsAreValid()) + { + String valueFrom = m_Value1Active ? m_Value1 : m_Value2; + String valueTo = m_Value1Active ? m_Value2 : m_Value1; + TraceLogger.GetInstance().LogConversionResult(valueFrom, UnitFrom.ToString(), valueTo, UnitTo.ToString()); + } + }); + } + + String GetLocalizedAutomationName() + { + // TODO UNO + //var format = AppResourceProvider.GetInstance().GetResourceString("SupplementaryUnit_AutomationName"); + //return new String(LocalizationStringUtil.GetLocalizedString(format, this.Value, this.Unit.Name)); + + return string.Empty; + } + } +} diff --git a/src/Calculator.Shared/Views/MainPage.xaml b/src/Calculator.Shared/Views/MainPage.xaml index 6c04c6f4..0aa64f79 100644 --- a/src/Calculator.Shared/Views/MainPage.xaml +++ b/src/Calculator.Shared/Views/MainPage.xaml @@ -25,14 +25,14 @@ --> - - + - - - - - - - - - - - - - + + - + - - + - + - - - - + + - + - - + - + - + From="0" + Storyboard.TargetName="CurrencyTimestampTextBlock" + Storyboard.TargetProperty="Opacity" + To="1"/> + - + HorizontalAlignment="Stretch" + > + - + + Height="56*" + MinHeight="56"/> + Height="32*" + MinHeight="32"/> + Height="56*" + MinHeight="56"/> + Height="32*" + MinHeight="32"/> - + Height="Auto" + MinHeight="48"/> + - - - - - + + + + - --> - + + NumeratorAspect="Width" + Source="{x:Bind}" + Threshold="1"/> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - + - - - - - - - - - - - + + + + + + + + + + - - - + - - - - - - - - - - - + + + + + + + + + + - - - + - - - - - - - - - - - + + + + + + + + + + - - + - - - + - - - - + + + - + - - - - + + - - - + + - - - + - - + + + + + + + + + + + + - + Grid.Row="1" + Grid.RowSpan="5" + Grid.Column="0" + Grid.ColumnSpan="4" + Visibility="{x:Bind Model.IsCurrencyLoadingVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"> - - - + + + - + Grid.Row="1" + Grid.Column="1" + MaxWidth="140" + MaxHeight="140" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + IsActive="False"/> - + Grid.Row="1" + Grid.Column="1" + HorizontalAlignment="{x:Bind FlowDirectionHorizontalAlignment}" + Style="{ThemeResource ValueContainerStyle}" + Visibility="{x:Bind Model.IsCurrencyLoadingVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityNegationConverter}}"> - - + + + - - + Grid.Column="0" + Padding="12,0,0,0" + Style="{ThemeResource CurrencySymbolMediumStyle}" + Text="{x:Bind Model.CurrencySymbol1, Mode=OneWay}" + Visibility="{x:Bind Model.CurrencySymbolVisibility, Mode=OneWay}"/> + - - - - + Grid.Column="1" + Style="{ThemeResource ValueMediumStyle}" + ContextCanceled="OnContextCanceled" + ContextRequested="OnContextRequested" + DisplayValue="{x:Bind Model.Value1, Mode=OneWay}" + ExpressionVisibility="Collapsed" + FlowDirection="{x:Bind LayoutDirection}" + IsActive="{Binding Value1Active, Mode=TwoWay}" + KeyDown="OnValueKeyDown" + Selected="OnValueSelected" + TabIndex="1"/> + + Grid.Row="2" + Grid.Column="1" + HorizontalAlignment="{x:Bind FlowDirectionHorizontalAlignment}" + Style="{ThemeResource ComboStyle}" + DropDownClosed="UpdateDropDownState" + DropDownOpened="UpdateDropDownState" + FlowDirection="{x:Bind LayoutDirection}" + IsEnabled="{Binding IsDropDownEnabled}" + IsEnabledChanged="Units1_IsEnabledChanged" + ItemTemplate="{StaticResource UnitTemplate}" + ItemsSource="{Binding Units}" + SelectedItem="{Binding Unit1, Mode=TwoWay}" + TabIndex="2" + Visibility="{x:Bind Model.IsCurrencyLoadingVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityNegationConverter}}"/> + - + Grid.Row="3" + Grid.Column="1" + HorizontalAlignment="{x:Bind FlowDirectionHorizontalAlignment}" + Style="{ThemeResource ValueContainerStyle}" + Visibility="{x:Bind Model.IsCurrencyLoadingVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityNegationConverter}}"> - - + + + - - + Grid.Column="0" + Padding="12,0,0,0" + Style="{ThemeResource CurrencySymbolMediumStyle}" + Text="{x:Bind Model.CurrencySymbol2, Mode=OneWay}" + Visibility="{x:Bind Model.CurrencySymbolVisibility, Mode=OneWay}"/> + - - - - + Grid.Column="1" + Style="{ThemeResource ValueMediumStyle}" + ContextCanceled="OnContextCanceled" + ContextRequested="OnContextRequested" + DisplayValue="{x:Bind Model.Value2, Mode=OneWay}" + ExpressionVisibility="Collapsed" + FlowDirection="{x:Bind LayoutDirection}" + IsActive="{Binding Value2Active, Mode=TwoWay}" + KeyDown="OnValueKeyDown" + Selected="OnValueSelected" + TabIndex="3"/> + + Grid.Row="4" + Grid.Column="1" + HorizontalAlignment="{x:Bind FlowDirectionHorizontalAlignment}" + Style="{ThemeResource ComboStyle}" + DropDownClosed="UpdateDropDownState" + DropDownOpened="UpdateDropDownState" + FlowDirection="{x:Bind LayoutDirection}" + IsEnabled="{Binding IsDropDownEnabled}" + ItemTemplate="{StaticResource UnitTemplate}" + ItemsSource="{Binding Units}" + SelectedItem="{Binding Unit2, Mode=TwoWay}" + TabIndex="4" + Visibility="{x:Bind Model.IsCurrencyLoadingVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityNegationConverter}}"/> + - - + - + HorizontalAlignment="Left" + VerticalAlignment="Center" + Results="{x:Bind Model.SupplementaryResults, Mode=OneWay}" + Visibility="{x:Bind Model.SupplementaryVisibility, Mode=OneWay}"/> - - --> + Style="{ThemeResource CaptionTextBlockStyle}" + Foreground="{ThemeResource SystemControlPageTextBaseHighBrush}" + AutomationProperties.Name="{x:Bind Model.CurrencyRatioEqualityAutomationName, Mode=OneWay}" + Text="{x:Bind Model.CurrencyRatioEquality, Mode=OneWay}"/> - --> + Style="{ThemeResource CaptionTextBlockStyle}" + Text="{x:Bind Model.CurrencyTimestamp, Mode=OneWay}"/> - --> - + IsTabStop="False" + TabIndex="5" + Visibility="Collapsed"> - - - - - - - + x:Uid="RefreshButtonText" + Foreground="{ThemeResource SystemControlHyperlinkBaseHighBrush}" + Click="CurrencyRefreshButton_Click"/> + + - --> - + IsTabStop="False" + TabIndex="5" + Visibility="Collapsed"> - - - - - - - - + Foreground="{ThemeResource SystemControlPageTextBaseHighBrush}" + AutomationProperties.AccessibilityView="Raw"> + + + + + - + - + Grid.Row="6" + Grid.Column="1" + Margin="0,0,0,6" + FlowDirection="LeftToRight" + RenderTransformOrigin="0.5,0.5"> - + - - - - - - - - + + + + + + + - - - - - - + + + + + - - + Grid.Row="1" + Grid.Column="2" + Grid.ColumnSpan="2" + AutomationProperties.HeadingLevel="Level1"> - - + + - - - - + + - - - + + - - --> + + diff --git a/src/Calculator.Shared/Views/UnitConverter.xaml.cs b/src/Calculator.Shared/Views/UnitConverter.xaml.cs index cf4f5826..b890d63c 100644 --- a/src/Calculator.Shared/Views/UnitConverter.xaml.cs +++ b/src/Calculator.Shared/Views/UnitConverter.xaml.cs @@ -1,17 +1,30 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; using Windows.Foundation.Collections; +using Windows.System; +using Windows.UI.ViewManagement; using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; +using Calculator; +using CalculatorApp.Common; +using CalculatorApp.Controls; +using CalculatorApp.ViewModel; +using UnitConversionManager; // The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 @@ -19,9 +32,365 @@ namespace CalculatorApp { public sealed partial class UnitConverter : UserControl { + const long DURATION_500_MS = 10000 * 500; + + Windows.UI.Xaml.FlowDirection m_layoutDirection; + //Windows.Foundation.EventRegistrationToken m_propertyChangedToken; + Windows.UI.Xaml.Controls.MenuFlyout m_resultsFlyout; + + string m_chargesMayApplyText; + string m_failedToRefreshText; + + bool m_meteredConnectionOverride; + + Windows.UI.Xaml.DispatcherTimer m_delayTimer; + + bool m_isAnimationEnabled; + + // TODO UNO: DEPENDENCY_PROPERTY_OWNER(UnitConverter); + + private HorizontalAlignment m_FlowDirectionHorizontalAlignment; + public HorizontalAlignment FlowDirectionHorizontalAlignment + { + get => m_FlowDirectionHorizontalAlignment; + set => m_FlowDirectionHorizontalAlignment = value; + } + + public UnitConverterViewModel Model + { + get { return (UnitConverterViewModel)this.DataContext; } + } + + public FlowDirection LayoutDirection + + { + get { return m_layoutDirection; + } + } + public UnitConverter() { + m_meteredConnectionOverride = false; + m_isAnimationEnabled = false; + + m_layoutDirection = LocalizationService.GetInstance().GetFlowDirection(); + m_FlowDirectionHorizontalAlignment = m_layoutDirection == FlowDirection.RightToLeft ? HorizontalAlignment.Right : HorizontalAlignment.Left; + this.InitializeComponent(); + + // adding ESC key shortcut binding to clear button + ClearEntryButtonPos0.SetValue(Common.KeyboardShortcutManager.VirtualKeyProperty, Common.MyVirtualKey.Escape); + + // Is currency symbol preference set to right side + bool preferRight = LocalizationSettings.GetInstance().GetCurrencySymbolPrecedence() == 0; + VisualStateManager.GoToState(this, preferRight ? "CurrencySymbolRightState" : "CurrencySymbolLeftState", false); + + var userSettings = new UISettings(); + m_isAnimationEnabled = userSettings.AnimationsEnabled; + + var resLoader = AppResourceProvider.GetInstance(); + m_chargesMayApplyText = resLoader.GetResourceString("DataChargesMayApply"); + m_failedToRefreshText = resLoader.GetResourceString("FailedToRefresh"); + + InitializeOfflineStatusTextBlock(); + + // TODO UNO: m_resultsFlyout = (MenuFlyout) (Resources["CalculationResultContextMenu"]); + // CopyMenuItem.Text = resLoader.GetResourceString("copyMenuItem"); + // PasteMenuItem.Text = resLoader.GetResourceString("pasteMenuItem"); } + + void OnPropertyChanged( object sender, PropertyChangedEventArgs e) + { + String propertyName = e.PropertyName; + if (propertyName == UnitConverterViewModel.NetworkBehaviorPropertyName || propertyName == UnitConverterViewModel.CurrencyDataLoadFailedPropertyName) + { + OnNetworkBehaviorChanged(); + } + else if (propertyName == UnitConverterViewModel.CurrencyDataIsWeekOldPropertyName) + { + SetCurrencyTimestampFontWeight(); + } + else if (propertyName == UnitConverterViewModel.IsCurrencyLoadingVisiblePropertyName) + { + OnIsDisplayVisibleChanged(); + } + } + + void OnNetworkBehaviorChanged() + { + switch (Model.NetworkBehavior) + { + case NetworkAccessBehavior.Normal: + OnNormalNetworkAccess(); + break; + case NetworkAccessBehavior.OptIn: + OnOptInNetworkAccess(); + break; + case NetworkAccessBehavior.Offline: + OnOfflineNetworkAccess(); + break; + } + } + + void OnNormalNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Visible; + OfflineBlock.Visibility = Visibility.Collapsed; + + if (Model.CurrencyDataLoadFailed) + { + SetFailedToRefreshStatus(); + } + else + { + SetNormalCurrencyStatus(); + } + } + + void OnOptInNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Visible; + OfflineBlock.Visibility = Visibility.Collapsed; + + if (m_meteredConnectionOverride && Model.CurrencyDataLoadFailed) + { + SetFailedToRefreshStatus(); + } + else + { + SetChargesMayApplyStatus(); + } + } + + void OnOfflineNetworkAccess() + { + CurrencyRefreshBlockControl.Visibility = Visibility.Collapsed; + OfflineBlock.Visibility = Visibility.Visible; + } + + void SetNormalCurrencyStatus() + { + CurrencySecondaryStatus.Text = ""; + } + + void SetChargesMayApplyStatus() + { + VisualStateManager.GoToState(this, "ChargesMayApplyCurrencyStatus", false); + CurrencySecondaryStatus.Text = m_chargesMayApplyText; + } + + void SetFailedToRefreshStatus() + { + VisualStateManager.GoToState(this, "FailedCurrencyStatus", false); + CurrencySecondaryStatus.Text = m_failedToRefreshText; + } + + void InitializeOfflineStatusTextBlock() + { + var resProvider = AppResourceProvider.GetInstance(); + string offlineStatusHyperlinkText = resProvider.GetResourceString("OfflineStatusHyperlinkText"); + + // The resource string has the 'NetworkSettings' hyperlink wrapped with '%HL%'. + // Break the string and assign pieces appropriately. + string delimiter = "%HL%" ; + int delimiterLength = delimiter.Length; + + // Find the delimiters. + int firstSplitPosition = offlineStatusHyperlinkText.find(delimiter, 0); + Debug.Assert(firstSplitPosition != "".npos()); + int secondSplitPosition = offlineStatusHyperlinkText.find(delimiter, firstSplitPosition + 1); + Debug.Assert(secondSplitPosition != "".npos()); + int hyperlinkTextLength = secondSplitPosition - (firstSplitPosition + delimiterLength); + + // Assign pieces. + var offlineStatusTextBeforeHyperlink = offlineStatusHyperlinkText.substr(0, firstSplitPosition); + var offlineStatusTextLink = offlineStatusHyperlinkText.substr(firstSplitPosition + delimiterLength, hyperlinkTextLength); + var offlineStatusTextAfterHyperlink = offlineStatusHyperlinkText.substr(secondSplitPosition + delimiterLength); + + OfflineRunBeforeLink.Text = offlineStatusTextBeforeHyperlink; + OfflineRunLink.Text = offlineStatusTextLink; + OfflineRunAfterLink.Text = offlineStatusTextAfterHyperlink; + + AutomationProperties.SetName(OfflineBlock, offlineStatusTextBeforeHyperlink + " " + offlineStatusTextLink + " " + offlineStatusTextAfterHyperlink); + } + + void SetCurrencyTimestampFontWeight() + { + if (Model.CurrencyDataIsWeekOld) + { + VisualStateManager.GoToState(this, "WeekOldTimestamp", false); + } + else + { + VisualStateManager.GoToState(this, "DefaultTimestamp", false); + } + } + + void OnValueKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Space) + { + OnValueSelected(sender); + } + } + + void OnContextRequested(UIElement sender, ContextRequestedEventArgs e) + { + OnValueSelected(sender); + var requestedElement = sender as FrameworkElement; + + // TODO UNO + //PasteMenuItem.IsEnabled = CopyPasteManager.HasStringToPaste(); + + Point point; + if (e.TryGetPosition(requestedElement, out point)) + { + m_resultsFlyout.ShowAt(requestedElement, point); + } + else + { + // Not invoked via pointer, so let XAML choose a default location. + m_resultsFlyout.ShowAt(requestedElement); + } + + e.Handled = true; + } + + void OnContextCanceled(UIElement sender, RoutedEventArgs e) + { + m_resultsFlyout.Hide(); + } + + void OnCopyMenuItemClicked( object sender, RoutedEventArgs e) + { + var calcResult = (m_resultsFlyout.Target) as CalculationResult; + CopyPasteManager.CopyToClipboard(calcResult.GetRawDisplayValue()); + } + + async void OnPasteMenuItemClicked( object sender, RoutedEventArgs e) + { + var pastedString = await CopyPasteManager.GetStringToPaste(Model.Mode, CategoryGroupType.Converter, default(int), default(int)); + Model.OnPaste(pastedString, Model.Mode); + } + + public void AnimateConverter() + { + if (App.IsAnimationEnabled()) + { + AnimationStory.Begin(); + } + } + + void OnValueSelected( object sender) + { + var value = (CalculationResult) sender; + // update the font size since the font is changed to bold + value.UpdateTextState(); + ((UnitConverterViewModel) this.DataContext).OnValueActivated(value); + } + + void UpdateDropDownState( object sender, object e) + { + ((UnitConverterViewModel)this.DataContext).IsDropDownOpen = (Units1.IsDropDownOpen) || (Units2.IsDropDownOpen); + // TODO UNO: KeyboardShortcutManager.UpdateDropDownState((Units1.IsDropDownOpen) || (Units2.IsDropDownOpen)); + } + + void OnLoaded(object sender, RoutedEventArgs args) + { + } + + public void SetDefaultFocus() + { + CalculatorList focusPrecedence = new CalculatorList{ Value1, CurrencyRefreshBlockControl, OfflineBlock, ClearEntryButtonPos0 }; + + foreach (Control control in focusPrecedence) + { + if (control.Focus(FocusState.Programmatic)) + { + break; + } + } + } + + void CurrencyRefreshButton_Click( object sender, RoutedEventArgs e) + { + if (Model.NetworkBehavior == NetworkAccessBehavior.OptIn) + { + m_meteredConnectionOverride = true; + } + + Model.RefreshCurrencyRatios(); + } + + void OnDataContextChanged( DependencyObject sender, DataContextChangedEventArgs args) + { + Model.PropertyChanged -= OnPropertyChanged; + + Model.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged); + + OnNetworkBehaviorChanged(); + } + + void Units1_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if ((Units1.Visibility == Visibility.Visible) && Units1.IsEnabled) + { + SetDefaultFocus(); + } + } + + void OnIsDisplayVisibleChanged() + { + if (Model.IsCurrencyLoadingVisible) + { + StartProgressRingWithDelay(); + } + else + { + HideProgressRing(); + + if (m_isAnimationEnabled && Model.IsCurrencyCurrentCategory && !Model.CurrencyTimestamp.IsEmpty()) + { + TimestampFadeInAnimation.Begin(); + } + } + } + + void StartProgressRingWithDelay() + { + HideProgressRing(); + + TimeSpan delay = TimeSpan.FromMilliseconds(DURATION_500_MS); + + m_delayTimer = new DispatcherTimer(); + m_delayTimer.Interval = delay; + m_delayTimer.Tick += new EventHandler (OnDelayTimerTick); + + m_delayTimer.Start(); + } + + void OnDelayTimerTick(object sender, object e) + { + CurrencyLoadingProgressRing.IsActive = true; + m_delayTimer.Stop(); + } + + void HideProgressRing() + { + if (m_delayTimer != null) + { + m_delayTimer.Stop(); + } + + CurrencyLoadingProgressRing.IsActive = false; + } + + // The function will make sure the UI will have enough space to display supplementary results and currency information + void SupplementaryResultsPanelInGrid_SizeChanged(object sender, Windows.UI.Xaml.SizeChangedEventArgs e) + { + // We add 0.01 to be sure to not create an infinite loop with SizeChanged events cascading due to float approximation + RowDltrUnits.MinHeight = Math.Max(48.0, e.NewSize.Height + 0.01); + } + } } diff --git a/src/Calculator.Shared/_adapters/StringExtensions.cs b/src/Calculator.Shared/_adapters/StringExtensions.cs new file mode 100644 index 00000000..446a6276 --- /dev/null +++ b/src/Calculator.Shared/_adapters/StringExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; + +namespace Calculator +{ + public static class StringExtensions + { + private const int _npos = -1; + + public static char back(this string str) => str.Last(); + public static char front(this string str) => str.First(); + public static int length(this string str) => str.Length; + public static char at(this string str, int index) => str[index]; + public static bool empty(this string str) => string.IsNullOrEmpty(str); + public static bool IsEmpty(this string str) => string.IsNullOrEmpty(str); + public static int npos(this string str) => _npos; + public static int size(this string str) => str.Length; + public static int find(this string str, char c) => str.IndexOf(c); + public static int find(this string str, char c, int startIndex) => str.IndexOf(c, startIndex); + public static int find(this string str, string c) => str.IndexOf(c); + public static int find(this string str, string c, int startIndex) => str.IndexOf(c, startIndex); + + public static string substr(this string str, int pos = 0, int len = _npos) + { + if (pos == str.Length) + { + return string.Empty; + } + + if (pos > str.Length) + { + throw new ArgumentOutOfRangeException(nameof(pos)); + } + + if (len == _npos) + { + return str.Substring(pos); + } + else + { + return str.Substring(pos, Math.Min(len, str.Length - pos)); + } + } + } +} diff --git a/src/Calculator.iOS/Calculator.iOS.csproj b/src/Calculator.iOS/Calculator.iOS.csproj index 57c94e53..53e1b64b 100644 --- a/src/Calculator.iOS/Calculator.iOS.csproj +++ b/src/Calculator.iOS/Calculator.iOS.csproj @@ -12,6 +12,7 @@ ..\Calculator.Shared\Strings + 7.3 true