diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e844ca00 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +## IDE-independent coding style via EditorConfig: http://editorconfig.org/ + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..51617f5f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +*.sln merge=binary +*.csproj merge=binary +*.vbproj merge=binary +*.vcxproj merge=binary +*.vcproj merge=binary +*.dbproj merge=binary +*.fsproj merge=binary +*.lsproj merge=binary +*.wixproj merge=binary +*.modelproj merge=binary +*.sqlproj merge=binary +*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +*.jpg binary +*.png binary +*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index 3e759b75..459b8a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -## Ignore Visual Studio temporary files, build results, and +## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore @@ -24,14 +24,11 @@ bld/ [Oo]bj/ [Ll]og/ -# Visual Studio 2015/2017 cache/options directory +# Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ -# Visual Studio 2017 auto generated files -Generated\ Files/ - # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -45,29 +42,20 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# Benchmark Results -BenchmarkDotNet.Artifacts/ - # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio *_i.c *_p.c *_i.h *.ilk *.meta *.obj -*.iobj *.pch *.pdb -*.ipdb *.pgc *.pgd *.rsp @@ -105,9 +93,6 @@ ipch/ *.vspx *.sap -# Visual Studio Trace Files -*.e2e - # TFS 2012 Local Workspace $tf/ @@ -128,10 +113,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - # Visual Studio code coverage results *.coverage *.coveragexml @@ -167,7 +148,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj @@ -180,11 +161,11 @@ PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* +**/packages/* # except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ +!**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config +#!**/packages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets @@ -202,7 +183,6 @@ AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt -*.appx # Visual Studio cache files # files ending in .cache can be ignored @@ -221,10 +201,6 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ @@ -239,8 +215,6 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak # SQL Server files *.mdf @@ -251,7 +225,6 @@ ServiceFabricBackup/ *.rdl.data *.bim.layout *.bim_*.settings -*.rptproj.rsuser # Microsoft Fakes FakesAssemblies/ @@ -263,6 +236,9 @@ FakesAssemblies/ .ntvs_analysis.dat node_modules/ +# Typescript v1 declaration files +typings/ + # Visual Studio 6 build log *.plg @@ -302,9 +278,6 @@ __pycache__/ # tools/** # !tools/packages.config -# Tabs Studio -*.tss - # Telerik's JustMock configuration file *.jmconfig @@ -314,17 +287,15 @@ __pycache__/ *.odx.cs *.xsd.cs -# OpenCover UI analysis results -OpenCover/ +# Calculator specific +Generated Files/ +# Don't ignore anything under SpkgDefs or TrexDefs +!/SpkgDefs/** +!/TrexDefs/** +!/build/config/TRexDefs/** -# Azure Stream Analytics local run output -ASALocalRun/ +# Localized strings +# LCT files are loc system artifacts that we don't want in our repo. +*.lct -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +WorkspaceInfo.xml diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..181cf44b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or +comments. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..851ea9eb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to Calculator +The Calculator team encourages community feedback and contributions. Thank you for your interest in +making Calculator better! There are several ways you can get involved. + +## Reporting issues and suggesting new features +If Calculator is not working properly, please file a report in the [Feedback Hub](https://insider.windows.com/en-us/fb/?contextid=130&newFeedback=True). +Feedback Hub reports automatically include diagnostic data, such as the version of Calculator +you're using. + +We are happy to hear your ideas for the future of Calculator. Check the +[Feedback Hub](https://insider.windows.com/en-us/fb/?contextid=130) and see if others have +submitted similar feedback. You can upvote existing feedback or submit a new suggestion. + +We always look at upvoted items in Feedback Hub when we decide what to work on next. We read the +comments in both Feedback Hub and GitHub, and we look forward to hearing your input. Remember that +all community interactions must abide by the [Code of Conduct](CODE_OF_CONDUCT.md). + +## Finding issues you can help with +Looking for something to work on? +[Issues marked *up for grabs*](https://microsoft.com/#TODO_MIGRATE_THIS_LINK_AFTER_GITHUB_MIGRATION) +are a good place to start. + +Another way you can help is by reproducing issues others have reported. +[Issues marked *more info needed*](https://microsoft.com/#TODO_MIGRATE_THIS_LINK_AFTER_GITHUB_MIGRATION) +don't have all the information needed to take action. You can try to reproduce the issue on your +machine and add more details so we can take action on the report. + +## Contributions we accept +We welcome your contributions to the Calculator project, especially to fix bugs and to make +improvements which address the top issues reported by Calculator users. + +We have a high bar for new features and changes to the user experience. We prefer to evaluate +*prototypes* to ensure that the design meets users' needs before we start discussing implementation +details and reviewing code. We follow a [user-centered process for developing features](docs/NewFeatureProcess.md). +New features need sponsorship from the Calculator team, but we welcome community contributions at +many stages of the process. + +## Making changes to the code + +### Preparing your development environment +To learn how to build the code and run tests, follow the instructions in the [README](README.md). + +### Style guidelines +The code in this project uses several different coding styles, depending on the age and history of +the code. Please attempt to match the style of surrounding code as much as possible. In new +components, prefer the patterns described in the [C++ core guidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) +and the [modern C++/WinRT language projections](https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/). + +### Testing +Your change should include tests to verify new functionality wherever possible. Code should be +structured so that it can be unit tested independently of the UI. [Manual test cases](docs/ManualTests.md) +should be used where automated testing is not feasible. + +### Git workflow +Calculator uses the [GitHub flow](https://guides.github.com/introduction/flow/) where most +development happens directly on the `master` branch. The `master` branch should always be in a +healthy state which is ready for release. + +If your change is complex, please clean up the branch history before submitting a pull request. +You can use [git rebase](https://docs.microsoft.com/en-us/azure/devops/repos/git/rebase#squash-local-commits) +to group your changes into a small number of commits which we can review one at a time. + +When completing a pull request, we will generally squash your changes into a single commit. Please +let us know if your pull request needs to be merged as separate commits. + +## Submitting a pull request and participating in code review +Writing a good description for your pull request is crucial to help reviewers and future +maintainers understand your change. More detail is better. +- [Link the issue you're addressing in the pull request](https://github.com/blog/957-introducing-issue-mentions). +- Describe *why* the change is being made and *why* you've chosen a particular solution. +- Describe any manual testing you performed to validate your change. + +Please submit one pull request per issue. Large pull requests which have unrelated changes can be +difficult to review. + +After submitting a pull request, members of the calculator team will review your code. We will +assign the request to an appropriate reviewer within two days. Any member of the community may +participate in the review, but at least one member of the Calculator team will ultimately approve +the request. + +Often, multiple iterations will be needed to responding to feedback from reviewers. Try looking at +[past pull requests](https://microsoft.com/#TODO_MIGRATE_THIS_LINK_AFTER_GITHUB_MIGRATION) to see +what the experience might be like. + +## Contributor License Agreement +Before we can review and accept a pull request from you, you'll need to sign a Contributor License +Agreement (CLA). The CLA ensures that the community is free to use your contributions. More +information about the agreement is available on the [Microsoft web site](https://cla.opensource.microsoft.com/). +Signing the CLA is an automated process, and you only need to do it once for Microsoft projects on +GitHub. + +You don't need to sign a CLA until you're ready to create a pull request. When your pull request is +created, it is classified by a bot. If the change is trivial (i.e. you just fixed a typo) then the +bot will label the PR `cla-not-required`. Otherwise, it's classified as `cla-required`. In that +case, the system will also tell you how you can sign the CLA. Once you have signed a CLA, the +current and all future pull requests will be labeled as `cla-signed`. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 21071075..8cb179cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ - MIT License +Copyright (c) Microsoft Corporation. All rights reserved. - Copyright (c) Microsoft Corporation. All rights reserved. +MIT License - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..60d809cb --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,39 @@ +THIRD PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. Microsoft makes certain +open source code available at http://3rdpartysource.microsoft.com, or you may +send a check or money order for US $5.00, including the product name, the open +source component name, and version number, to: + +Source Code Compliance Team +Microsoft Corporation +One Microsoft Way +Redmond, WA 98052 +USA + +Notwithstanding any other terms, you may reverse engineer this software to the +extent required to debug changes to any libraries licensed under the GNU Lesser +General Public License. + +--- + +Hebrew OpenType Layout logic + +The MIT License (MIT) +Copyright (c) 2003, 2007 Ralph Hancock & John Hudson +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 72f1506a..08848d00 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,71 @@ +# Calculator +The Windows Calculator app is a modern Windows app written in C++ that ships pre-installed with Windows. +The app provides standard, scientific, and programmer calculator functionality, as well as a set of converters between various units of measurement and currencies. -# Contributing +Calculator ships regularly with new features and bug fixes. You can get the latest version of Calculator in the [Windows Store.](https://www.microsoft.com/store/apps/9WZDNCRFHVN5) -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. + +[![Build Status](https://microsoft.visualstudio.com/Apps/_apis/build/status/Utility%20Apps/Calculator-Daily)](https://microsoft.visualstudio.com/Apps/_build?definitionId=3539) -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. + ![Calculator Screenshot](\docs\Images\CalculatorScreenshot.png) -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +## Features +- Standard Calculator functionality which offers basic operations and evaluates commands immediately as they are entered. +- Scientific Calculator functionality which offers expanded operations and evaluates commands using order of operations. +- Programmer Calculator functionality which offers common mathematical operations for developers including conversion between common bases. +- Calculation history and memory capabilities. +- Conversion between many units of measurement. +- Currency conversion based on data retrieved from [Bing](https://www.bing.com). + +## Getting started +Prerequisites: +- Your computer must be running Windows 10, version 1803 or newer +- Install the latest version of [Visual Studio](https://developer.microsoft.com/en-us/windows/downloads) (the free community edition is sufficient) + - Install the "Universal Windows Platform Development" workload + - Install the optional "C++ Universal Windows Platform tools" component + - Install the latest Windows 10 SDK + + ![Visual Studio Installation Screenshot](\docs\Images\VSInstallationScreenshot.png) +- Install the [XamlStyler](https://marketplace.visualstudio.com/items?itemName=TeamXavalon.XAMLStyler) Visual Studio extension + + +- Get the code: + ``` + git clone https://microsoft.visualstudio.com/DefaultCollection/Apps/_git/calculator.app + ``` + +- Open [src\Calculator.sln](\src\Calculator.sln) in Visual Studio to build and run the Calculator app. +- For a general description of the Calculator project architecture see [ApplicationArchitecture.md](docs\ApplicationArchitecture.md). + +## Running Tests +To run tests in Visual Studio, install [TDP](https://osgwiki.com/wiki/TDP) and use the TAEF explorer pane. + +Calculator has two primary sets of tests: +- [CalculatorUnitTests.vcxproj](\src\CalculatorUnitTests\CalculatorUnitTests.vcxproj) - Unit Tests +- [UIAutomationTests.csproj](\src\UIAutomationTests\UIAutomationTests.csproj) - UI Automation using [MitaLite](http://osgwiki.com/mitalite) + +## Exploring the repo +- [Build](\build) - Scripts which run during the build +- [Docs](\Docs) - Documentation for developers +- [Loc](\Loc) - Localization settings and files +- [PDP](\PDP) - Information about the app for the Store's Product Description Page +- [SpkgDefs](\SpkgDefs) - Definitions for building [test packages](https://osgwiki.com/wiki/Test_Package) +- [Src](\src) - Main folder for source code +- [Tools](\tools) - Scripts and tools to aid in development +- [TRexDefs](\TRexDefs) - Definitions for running tests using [TReX](https://osgwiki.com/wiki/TReX) + +## Contributing +Want to contribute? The team encourages community feedback and contributions. Please follow our [contributing guidelines](\CONTRIBUTING.md). + +If Calculator is not working properly, please file a report in the [Feedback Hub](https://insider.windows.com/en-us/fb/?contextid=130). +If you want to submit an issue to this repository, please read the +[issue reporting guidelines](\IssueTracking.md). + + + + +## Contact +Questions? Reach out to the PAX Essential Experiences App email alias: \ No newline at end of file diff --git a/Tools/PGO/Microsoft.WindowsCalculator.PGO.nuspec b/Tools/PGO/Microsoft.WindowsCalculator.PGO.nuspec new file mode 100644 index 00000000..4f1b114d --- /dev/null +++ b/Tools/PGO/Microsoft.WindowsCalculator.PGO.nuspec @@ -0,0 +1,15 @@ + + + + Microsoft.WindowsCalculator.PGO + $version$ + Microsoft + sourceapps + https://microsoft.visualstudio.com/Apps/_git/calculator.app + false + A package containing the required files for Profile Guided Optimization for Calculator. + Microsoft Corporation. All rights reserved. + pgo + en-US + + \ No newline at end of file diff --git a/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.props b/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.props new file mode 100644 index 00000000..56e2faf4 --- /dev/null +++ b/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.props @@ -0,0 +1,8 @@ + + + + + Optimize + + + \ No newline at end of file diff --git a/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.targets b/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.targets new file mode 100644 index 00000000..b46488dc --- /dev/null +++ b/Tools/PGO/build/native/Microsoft.WindowsCalculator.PGO.targets @@ -0,0 +1,40 @@ + + + + + $(VCToolsInstallDir)bin\Host$(PlatformShortName)\$(PlatformShortName) + $(VCToolsInstallDir)bin\$(PlatformShortName) + $(MSBuildThisFileDirectory)..\..\tools\$(PlatformShortName)\Calculator.pgd + + + + + + PGInstrument + $(VC_ReferencesPath_VC_ARM)\pgort.lib;%(AdditionalDependencies) + $(VC_ReferencesPath_VC_ARM64)\pgort.lib;%(AdditionalDependencies) + $(VC_ReferencesPath_VC_x86)\pgort.lib;%(AdditionalDependencies) + $(VC_ReferencesPath_VC_x64)\pgort.lib;%(AdditionalDependencies) + + + + + + + %(Filename)%(Extension) + true + false + false + PreserveNewest + + + + + + + PGUpdate + $(ProfileGuidedDatabaseFileName) + + + + \ No newline at end of file diff --git a/build/config/LocConfigPackageEs.xml b/build/config/LocConfigPackageEs.xml new file mode 100644 index 00000000..d566c7cf --- /dev/null +++ b/build/config/LocConfigPackageEs.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/build/config/SignConfig.xml b/build/config/SignConfig.xml new file mode 100644 index 00000000..7209725e --- /dev/null +++ b/build/config/SignConfig.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/amd64/performance.xml b/build/config/TRexDefs/amd64/performance.xml new file mode 100644 index 00000000..e6b6b40e --- /dev/null +++ b/build/config/TRexDefs/amd64/performance.xml @@ -0,0 +1,30 @@ + + + + + RS_APPS_VALIDATE AMD64 Desktop WinPerf Test Run + 134858 + paxeedev + true + true + Performance_AMD64.testlist + + + + VSTS\IsReliabilityRun + True + + + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/amd64/rs4_release.xml b/build/config/TRexDefs/amd64/rs4_release.xml new file mode 100644 index 00000000..78a52df9 --- /dev/null +++ b/build/config/TRexDefs/amd64/rs4_release.xml @@ -0,0 +1,24 @@ + + + + + RS4_RELEASE AMD64 DesktopVM Test Run + 139642 + paxeedev + true + true + Desktop_AMD64.testlist + rs4_release + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/amd64/rs5_release.xml b/build/config/TRexDefs/amd64/rs5_release.xml new file mode 100644 index 00000000..7fbccdc2 --- /dev/null +++ b/build/config/TRexDefs/amd64/rs5_release.xml @@ -0,0 +1,25 @@ + + + + + + RS5_RELEASE AMD64 DesktopVM Test Run + 139642 + paxeedev + true + true + Desktop_AMD64.testlist + rs5_release + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/amd64/rs_apps_validate.xml b/build/config/TRexDefs/amd64/rs_apps_validate.xml new file mode 100644 index 00000000..a90ca288 --- /dev/null +++ b/build/config/TRexDefs/amd64/rs_apps_validate.xml @@ -0,0 +1,44 @@ + + + + + RS_APPS_VALIDATE AMD64 DesktopVM Test Run + 139642 + paxeedev + true + true + Desktop_AMD64.testlist + rs_apps_validate + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + + RS_APPS_VALIDATE AMD64 WCOS Test Run + 153648 + paxeedev + true + true + WCOS_AMD64.testlist + rs_apps_validate + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/arm/performance.xml b/build/config/TRexDefs/arm/performance.xml new file mode 100644 index 00000000..5fde2b8a --- /dev/null +++ b/build/config/TRexDefs/arm/performance.xml @@ -0,0 +1,31 @@ + + + + + + RS_PRERELEASE ARM WindowsCore WinPerf Test Run + 135258 + paxeedev + true + true + Performance_ARM.testlist + + + + VSTS\IsReliabilityRun + True + + + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/arm/rs_apps_validate.xml b/build/config/TRexDefs/arm/rs_apps_validate.xml new file mode 100644 index 00000000..277c9761 --- /dev/null +++ b/build/config/TRexDefs/arm/rs_apps_validate.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/x86/rs4_release.xml b/build/config/TRexDefs/x86/rs4_release.xml new file mode 100644 index 00000000..7f3f2cbf --- /dev/null +++ b/build/config/TRexDefs/x86/rs4_release.xml @@ -0,0 +1,25 @@ + + + + + + RS_APPS_VALIDATE x86 DesktopVM Test Run + 139643 + paxeedev + true + true + Desktop_x86.testlist + rs4_release + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/x86/rs5_release.xml b/build/config/TRexDefs/x86/rs5_release.xml new file mode 100644 index 00000000..205a9c09 --- /dev/null +++ b/build/config/TRexDefs/x86/rs5_release.xml @@ -0,0 +1,25 @@ + + + + + + RS_APPS_VALIDATE x86 DesktopVM Test Run + 139643 + paxeedev + true + true + Desktop_x86.testlist + rs5_release + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TRexDefs/x86/rs_apps_validate.xml b/build/config/TRexDefs/x86/rs_apps_validate.xml new file mode 100644 index 00000000..130dc8f7 --- /dev/null +++ b/build/config/TRexDefs/x86/rs_apps_validate.xml @@ -0,0 +1,46 @@ + + + + + + RS_APPS_VALIDATE x86 DesktopVM Test Run + 139643 + paxeedev + true + true + Desktop_x86.testlist + rs_apps_validate + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + + + RS_PRERELEASE x86 WindowsCoreVM Test Run + 153715 + paxeedev + true + true + WCOS_x86.testlist + rs_apps_validate + + + AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + _AlternatePackageRoot + \\pkges\release\TAEF\validation.taef.provenance\1812.20007-develop\UniversalTestPackages;\\edge-svcs\Release\MITALite\Apps_eng.mitalite_ci\Latest.tst + + + + + \ No newline at end of file diff --git a/build/config/TestLists/ARM/Desktop_ARM.testlist b/build/config/TestLists/ARM/Desktop_ARM.testlist new file mode 100644 index 00000000..cf0e5b66 --- /dev/null +++ b/build/config/TestLists/ARM/Desktop_ARM.testlist @@ -0,0 +1,12 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs": [ + { + "FilePath": "Calculator.UITests\\Prebuilt\\Test\\arm\\fre\\Calculator.UITests.testmd", + "ExecutionProfile": "All" + }, + { + "FilePath": "CalculatorUnitTests\\Prebuilt\\Test\\arm\\fre\\CalculatorUnitTests.testmd", + } + ] +} \ No newline at end of file diff --git a/build/config/TestLists/ARM/Performance_ARM.testlist b/build/config/TestLists/ARM/Performance_ARM.testlist new file mode 100644 index 00000000..2ec2d933 --- /dev/null +++ b/build/config/TestLists/ARM/Performance_ARM.testlist @@ -0,0 +1,9 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs": [ + { + "FilePath": "Calculator.UITests\\Prebuilt\\Test\\arm\\fre\\Calculator.UITests.testmd", + "ExecutionProfile": "Performance" + } + ] +} \ No newline at end of file diff --git a/build/config/TestLists/ARM/WCOS_ARM.testlist b/build/config/TestLists/ARM/WCOS_ARM.testlist new file mode 100644 index 00000000..cf0e5b66 --- /dev/null +++ b/build/config/TestLists/ARM/WCOS_ARM.testlist @@ -0,0 +1,12 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs": [ + { + "FilePath": "Calculator.UITests\\Prebuilt\\Test\\arm\\fre\\Calculator.UITests.testmd", + "ExecutionProfile": "All" + }, + { + "FilePath": "CalculatorUnitTests\\Prebuilt\\Test\\arm\\fre\\CalculatorUnitTests.testmd", + } + ] +} \ No newline at end of file diff --git a/build/config/TestLists/ARM64/Desktop_ARM64.testlist b/build/config/TestLists/ARM64/Desktop_ARM64.testlist new file mode 100644 index 00000000..a2e620ca --- /dev/null +++ b/build/config/TestLists/ARM64/Desktop_ARM64.testlist @@ -0,0 +1,12 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs": [ + { + "FilePath": "Calculator.UITests\\Prebuilt\\Test\\arm64\\fre\\Calculator.UITests.testmd", + "ExecutionProfile": "All" + }, + { + "FilePath": "CalculatorUnitTests\\Prebuilt\\Test\\arm64\\fre\\CalculatorUnitTests.testmd", + } + ] +} \ No newline at end of file diff --git a/build/config/TestLists/ARM64/WCOS_ARM64.testlist b/build/config/TestLists/ARM64/WCOS_ARM64.testlist new file mode 100644 index 00000000..a2e620ca --- /dev/null +++ b/build/config/TestLists/ARM64/WCOS_ARM64.testlist @@ -0,0 +1,12 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs": [ + { + "FilePath": "Calculator.UITests\\Prebuilt\\Test\\arm64\\fre\\Calculator.UITests.testmd", + "ExecutionProfile": "All" + }, + { + "FilePath": "CalculatorUnitTests\\Prebuilt\\Test\\arm64\\fre\\CalculatorUnitTests.testmd", + } + ] +} \ No newline at end of file diff --git a/build/config/rs_apps_utils.json b/build/config/rs_apps_utils.json new file mode 100644 index 00000000..9c16d817 --- /dev/null +++ b/build/config/rs_apps_utils.json @@ -0,0 +1,28 @@ +{ + "Branch": [ + { + "collection":"microsoft", + "project":"OS", + "repo":"os", + "name":"official/rs_apps_utils", + "CheckinFiles": [ + { + "source":"vpack/app/calculator.app.man", + "path": "/redist/mspartners/ipa/Calculator", + "type": "File" + }, + { + "source":"vpack/app/calculator.app.man", + "path": "/onecoreuap/redist/mspartners/ipa/Calculator", + "type": "File" + } + ] + } + ], + "Email": [ + { + "sendTo":"paxeedev", + "sendOnErrorOnly": "True" + } + ] +} \ No newline at end of file diff --git a/build/config/rs_apps_validate.json b/build/config/rs_apps_validate.json new file mode 100644 index 00000000..3fbe98b5 --- /dev/null +++ b/build/config/rs_apps_validate.json @@ -0,0 +1,28 @@ +{ + "Branch": [ + { + "collection":"microsoft", + "project":"OS", + "repo":"os", + "name":"official/rs_apps_validate", + "CheckinFiles": [ + { + "source":"vpack/app/calculator.app.man", + "path": "/redist/mspartners/ipa/Calculator", + "type": "File" + }, + { + "source":"vpack/app/calculator.app.man", + "path": "/onecoreuap/redist/mspartners/ipa/Calculator", + "type": "File" + } + ] + } + ], + "Email": [ + { + "sendTo":"paxeedev", + "sendOnErrorOnly": "True" + } + ] +} \ No newline at end of file diff --git a/build/pipelines/azure-pipelines.ci.yaml b/build/pipelines/azure-pipelines.ci.yaml new file mode 100644 index 00000000..f8c70188 --- /dev/null +++ b/build/pipelines/azure-pipelines.ci.yaml @@ -0,0 +1,44 @@ +# +# Continuous Integration (CI) +# This pipeline builds and validate the app in all supported configurations. If the build was +# queued to validate a pull request, we build and test only x64. +# + +trigger: +- master +- servicing/* +pr: +- master +- servicing/* + +name: 0.$(Date:yyMM).$(Date:dd)$(Rev:rr).0 + +jobs: +- template: ./templates/build-app-public.yaml + parameters: + platform: x64 + +- template: ./templates/build-app-public.yaml + parameters: + platform: x86 + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/build-app-public.yaml + parameters: + platform: ARM + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/build-app-public.yaml + parameters: + platform: ARM64 + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/run-unit-tests.yaml + parameters: + platform: x64 + +- template: ./templates/run-unit-tests.yaml + parameters: + platform: x86 + +- template: ./templates/package-appxbundle.yaml diff --git a/build/pipelines/azure-pipelines.loc.yaml b/build/pipelines/azure-pipelines.loc.yaml new file mode 100644 index 00000000..d2bef95e --- /dev/null +++ b/build/pipelines/azure-pipelines.loc.yaml @@ -0,0 +1,44 @@ +# +# Localization +# This pipeline uploads English strings files to the localization service, downloads any translated +# files which are available, and checks them in to git. This pipeline relies on Microsoft-internal +# resources to run. +# + +# Expects a variable called LocServiceKey to contain the OAuth client secret for Touchdown. + +trigger: none +pr: none + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +jobs: +- job: Localize + pool: + name: Package ES Custom Demands Lab A + demands: + - ClientAlias -equals PKGESUTILAPPS + workspace: + clean: outputs + steps: + - checkout: self + clean: true + + - task: PkgESSetupBuild@10 + displayName: Initialize Package ES + inputs: + productName: Calculator + branchVersion: true + + - task: PkgESTouchdownLocService@10 + displayName: Package ES Touchdown Loc Service + inputs: + IsCallToServiceStepSelected: true + IsCheckedInFileSelected: true + CheckinFilesAtOriginFilePath: true + GitLocPath: Loc/Resources + LocConfigFile: build/config/LocConfigPackageEs.xml + AuthenticationMode: OAuth + ClientApplicationID: d3dd8113-65b3-4526-bdca-a00a7d1c37ba + ApplicationKeyID: $(LocServiceKey) + SendToLoc: true diff --git a/build/pipelines/azure-pipelines.release.yaml b/build/pipelines/azure-pipelines.release.yaml new file mode 100644 index 00000000..22d88994 --- /dev/null +++ b/build/pipelines/azure-pipelines.release.yaml @@ -0,0 +1,48 @@ +# +# Release +# This pipeline builds a version of the app in a production configuration to be released to the +# Store and the Windows image. This pipeline relies on Microsoft-internal resources to run. +# + +trigger: none +pr: none + +variables: + versionMajor: 10 + versionMinor: 1901 + versionBuild: $[counter('10.1901.*', 500)] + versionPatch: 0 + +name: '$(versionMajor).$(versionMinor).$(versionBuild).$(versionPatch)' + +jobs: +- template: ./templates/build-app-internal.yaml + parameters: + platform: x64 + +- template: ./templates/build-app-internal.yaml + parameters: + platform: x86 + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/build-app-internal.yaml + parameters: + platform: ARM + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/build-app-internal.yaml + parameters: + platform: ARM64 + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + +- template: ./templates/run-unit-tests.yaml + parameters: + platform: x64 + +- template: ./templates/run-unit-tests.yaml + parameters: + platform: x86 + +- template: ./templates/package-appxbundle.yaml + +- template: ./templates/prepare-release-internalonly.yaml diff --git a/build/pipelines/templates/build-app-internal.yaml b/build/pipelines/templates/build-app-internal.yaml new file mode 100644 index 00000000..a84c0bcf --- /dev/null +++ b/build/pipelines/templates/build-app-internal.yaml @@ -0,0 +1,62 @@ +# This template contains a job to build the app for a single architecture and run static analysis +# tools on the binaries. +# The app is built in a production configuration to be released to the Store and the Windows image. +# This job relies on Microsoft-internal resources to run. + +parameters: + platform: '' + condition: '' + +jobs: +- job: Build${{ parameters.platform }} + displayName: Build ${{ parameters.platform }} + condition: ${{ parameters.condition }} + pool: + name: Package ES Custom Demands Lab A + demands: + - msbuild + - visualstudio + - ClientAlias -equals PKGESUTILAPPS + variables: + BuildConfiguration: Release + BuildPlatform: ${{ parameters.platform }} + workspace: + clean: outputs + steps: + - checkout: self + clean: true + + - task: UniversalPackages@0 + displayName: Download internals package + inputs: + command: download + downloadDirectory: $(Build.SourcesDirectory) + vstsFeed: WindowsApps + vstsFeedPackage: calculator-internals + vstsPackageVersion: 0.0.7 + + - template: ./build-single-architecture.yaml + parameters: + extraMsBuildArgs: '/p:IsStoreBuild=true' + + - task: securedevelopmentteam.vss-secure-development-tools.build-task-binskim.BinSkim@3 + displayName: Run BinSkim + inputs: + inputType: Basic + analyzeTarget: $(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\Calculator\* + analyzeVerbose: true + analyzeHashes: true + continueOnError: true + + - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@1 + displayName: Run PoliCheck + inputs: + targetType: F + + - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2 + displayName: Publish security analysis logs + + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: Detect open source components + inputs: + sourceScanPath: $(Agent.BuildDirectory) \ No newline at end of file diff --git a/build/pipelines/templates/build-app-public.yaml b/build/pipelines/templates/build-app-public.yaml new file mode 100644 index 00000000..87f862f1 --- /dev/null +++ b/build/pipelines/templates/build-app-public.yaml @@ -0,0 +1,27 @@ +# This template contains a job to build the app for a single architecture. +# Only the contents of the public repository are built; internal resources are not used. + +parameters: + platform: '' + condition: '' + +jobs: +- job: Build${{ parameters.platform }} + displayName: Build ${{ parameters.platform }} + condition: ${{ parameters.condition }} + pool: + name: Package ES Custom Demands Lab A + demands: + - msbuild + - visualstudio + - ClientAlias -equals PKGESUTILAPPS + variables: + BuildConfiguration: Release + BuildPlatform: ${{ parameters.platform }} + workspace: + clean: outputs + steps: + - checkout: self + clean: true + + - template: ./build-single-architecture.yaml \ No newline at end of file diff --git a/build/pipelines/templates/build-single-architecture.yaml b/build/pipelines/templates/build-single-architecture.yaml new file mode 100644 index 00000000..feb9896f --- /dev/null +++ b/build/pipelines/templates/build-single-architecture.yaml @@ -0,0 +1,53 @@ +# This template contains steps to build the app for a single architecture. +# The job containing these steps must set the variables 'BuildConfiguration' and 'BuildPlatform'. + +parameters: + extraMsBuildArgs: '' + +steps: + - task: NuGetToolInstaller@0 + displayName: Use NuGet 4.7.1 + inputs: + versionSpec: 4.7.1 + checkLatest: true + + # In most accounts, you can just use 'NuGetCommand' instead of this GUID. + # In the microsoft.visualstudio.com account, NuGetCommand is ambiguous so the GUID is needed. + - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 + displayName: NuGet restore src/Calculator.sln + inputs: + command: custom + arguments: restore src/Calculator.sln -Verbosity Detailed -NonInteractive + + - task: PowerShell@2 + displayName: Set version number in AppxManifest + inputs: + filePath: $(Build.SourcesDirectory)\build\scripts\UpdateAppxManifestVersion.ps1 + arguments: '-AppxManifest $(Build.SourcesDirectory)\src\Calculator\Package.appxmanifest -Version $(Build.BuildNumber)' + + - task: VSBuild@1 + displayName: 'Build solution src/Calculator.sln' + inputs: + solution: src/Calculator.sln + vsVersion: 15.0 + msbuildArgs: /bl:$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\Calculator.binlog /p:OutDir=$(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform)\ /p:GenerateProjectSpecificOutputFolder=true /p:AppVersion=$(Build.BuildNumber) ${{ parameters.extraMsBuildArgs }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: true + maximumCpuCount: true + + - task: PublishBuildArtifacts@1 + displayName: Publish drop artifact + inputs: + artifactName: drop + pathToPublish: $(Build.BinariesDirectory) + parallel: true + + - task: PublishSymbols@2 + displayName: Publish symbols + inputs: + symbolsFolder: $(Build.BinariesDirectory)\$(BuildConfiguration)\$(BuildPlatform) + searchPattern: '**/*.pdb' + symbolServerType: teamServices + treatNotIndexedAsWarning: true + symbolsArtifactName: $(System.teamProject)/$(Build.BuildNumber)_$(BuildPlatform)$(BuildConfiguration) \ No newline at end of file diff --git a/build/pipelines/templates/package-appxbundle.yaml b/build/pipelines/templates/package-appxbundle.yaml new file mode 100644 index 00000000..1854c898 --- /dev/null +++ b/build/pipelines/templates/package-appxbundle.yaml @@ -0,0 +1,61 @@ +# This template contains a job which takes .appx packages which were built separately for each +# architecture (arm, x86, etc.) and combines them into a single .appxbundle. + +jobs: +- job: Package + dependsOn: + - Buildx64 + - Buildx86 + - BuildARM + - BuildARM64 + condition: | + and + ( + in(dependencies.Buildx64.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), + in(dependencies.Buildx86.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), + in(dependencies.BuildARM.result, 'Succeeded', 'SucceededWithIssues', 'Skipped'), + in(dependencies.BuildARM64.result, 'Succeeded', 'SucceededWithIssues', 'Skipped') + ) + pool: + name: Package ES Custom Demands Lab A + demands: + - msbuild + - visualstudio + - ClientAlias -equals PKGESUTILAPPS + workspace: + clean: outputs + steps: + - checkout: self + clean: true + + - task: DownloadBuildArtifacts@0 + displayName: Download all .appx artifacts + inputs: + artifactName: drop + itemPattern: '**/*.appx' + + - task: PowerShell@2 + displayName: Generate AppxBundle mapping + inputs: + filePath: $(Build.SourcesDirectory)\build\scripts\CreateAppxBundleMapping.ps1 + arguments: '-InputPath $(Build.ArtifactStagingDirectory)\drop\Release -ProjectName Calculator -OutputFile $(Build.BinariesDirectory)\AppxBundleMapping.txt' + + - script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\MakeAppx.exe" bundle /v /bv %BUNDLEVERSION% /f %MAPPINGFILEPATH% /p %OUTPUTPATH%' + displayName: Make AppxBundle + env: + BUNDLEVERSION: $(Build.BuildNumber) + MAPPINGFILEPATH: $(Build.BinariesDirectory)\AppxBundleMapping.txt + OUTPUTPATH: $(Build.BinariesDirectory)\Microsoft.WindowsCalculator_8wekyb3d8bbwe.appxbundle + + - task: CopyFiles@2 + displayName: Copy AppxBundle to staging directory + inputs: + sourceFolder: $(Build.BinariesDirectory) + contents: Microsoft.WindowsCalculator_8wekyb3d8bbwe.appxbundle + targetFolder: $(Build.ArtifactStagingDirectory)\appxBundle + + - task: PublishBuildArtifacts@1 + displayName: Publish AppxBundle artifact + inputs: + artifactName: appxBundle + pathToPublish: $(Build.ArtifactStagingDirectory)\appxBundle \ No newline at end of file diff --git a/build/pipelines/templates/prepare-release-internalonly.yaml b/build/pipelines/templates/prepare-release-internalonly.yaml new file mode 100644 index 00000000..fb8caf0d --- /dev/null +++ b/build/pipelines/templates/prepare-release-internalonly.yaml @@ -0,0 +1,111 @@ +# This template contains a job which builds artifacts needed to release the app to the store and to +# Windows using Microsoft-internal systems. It relies Microsoft-internal resources and will not +# work outside of Microsoft. +# Specifically, this job: +# - Signs the bundle using a secure system. If you want to build your own, use SignTool following +# the example in the continuous integration pipeline. +# - Builds VPacks for including the app in the Windows OS build. Azure DevOps Universal Packages +# offers similar capabilities. +# - Creates StoreBroker packages containing Microsoft Store assets. Although the Store assets for +# this app are not open source, the StoreBroker tool is available at +# https://github.com/Microsoft/StoreBroker. + +jobs: +- job: WindowsInternalRelease + dependsOn: Package + pool: + name: Package ES Custom Demands Lab A + demands: + - ClientAlias -equals PKGESUTILAPPS + workspace: + clean: outputs + steps: + - checkout: self + clean: true + + # This must be the first task in the job definition, since it modifies the build environment + # in ways other tasks would not expect (for example, it clears the artifacts directory). + - task: PkgESSetupBuild@10 + displayName: Initialize Package ES + inputs: + productName: Calculator + disableWorkspace: true + env: + XES_DISABLEPROV: true + + - task: DownloadBuildArtifacts@0 + displayName: Download appxBundle artifact + inputs: + artifactName: appxBundle + + - task: PkgESCodeSign@10 + displayName: Send bundle to Package ES code signing service + inputs: + signConfigXml: build\config\SignConfig.xml + inPathRoot: $(Build.ArtifactStagingDirectory)\appxBundle + outPathRoot: $(Build.ArtifactStagingDirectory)\appxBundleSigned + + - task: PublishBuildArtifacts@1 + displayName: Publish AppxBundleSigned artifact + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)\appxBundleSigned + artifactName: AppxBundleSigned + + - task: CopyFiles@2 + displayName: Copy signed AppxBundle to vpack staging folder + inputs: + sourceFolder: $(Build.ArtifactStagingDirectory)\appxBundleSigned + targetFolder: $(Build.ArtifactStagingDirectory)\vpack\appxBundle + + - task: PkgESVPack@10 + displayName: Create and push vpack for app + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + sourceDirectory: $(Build.ArtifactStagingDirectory)\vpack\appxBundle + description: VPack for the Calculator Application + pushPkgName: calculator.app + version: $(versionMajor).$(versionMinor).$(versionBuild) + owner: paxeeapps + + - task: PublishBuildArtifacts@1 + displayName: Publish vpack\app artifact with vpack manifest + inputs: + pathtoPublish: $(XES_VPACKMANIFESTDIRECTORY)\$(XES_VPACKMANIFESTNAME) + artifactName: vpack\app + + # TODO (macool): create and push internal test packages and test config + + - task: UniversalPackages@0 + displayName: Download internals package + inputs: + command: download + downloadDirectory: $(Build.SourcesDirectory) + vstsFeed: WindowsApps + vstsFeedPackage: calculator-internals + vstsPackageVersion: 0.0.7 + + - task: PkgESStoreBrokerPackage@10 + displayName: Create StoreBroker Packages + env: + XES_SERIALPOSTBUILDREADY: True + inputs: + addToFlight: false + configPath: tools/Build/StoreBroker/SBCalculatorConfig.json + PDPRootPath: $(Build.SourcesDirectory)\PDP + imagesRootPath: $(Build.SourcesDirectory)\PDPMediaRoot + appxPath: $(Build.ArtifactStagingDirectory)\appxBundleSigned\Microsoft.WindowsCalculator_8wekyb3d8bbwe.appxbundle + useArtifactServiceForMedia: true + outPath: $(Build.ArtifactStagingDirectory)\StoreBrokerPayload + paToken: $(System.AccessToken) + + - task: PublishBuildArtifacts@1 + displayName: Publish StoreBrokerPayload artifact + inputs: + artifactName: storeBrokerPayload + pathToPublish: $(Build.ArtifactStagingDirectory)/StoreBrokerPayload + + - task: PkgESLateTasks@10 + displayName: Run PackageES LateTasks + env: + XES_DISABLEPROV: true \ No newline at end of file diff --git a/build/pipelines/templates/run-unit-tests.yaml b/build/pipelines/templates/run-unit-tests.yaml new file mode 100644 index 00000000..89c31673 --- /dev/null +++ b/build/pipelines/templates/run-unit-tests.yaml @@ -0,0 +1,54 @@ +# This template contains jobs to run unit tests on the interactive test agents. + +parameters: + platform: '' + +jobs: +- job: UnitTests${{ parameters.platform }} + displayName: UnitTests ${{ parameters.platform }} + dependsOn: Build${{ parameters.platform }} + pool: + name: Essential Experiences Interactive + workspace: + clean: outputs + steps: + - checkout: none + + - powershell: Write-Host "##vso[task.setvariable variable=agentInstanceId;isOutput=true]$($env:AgentName -replace '\D+' -as [int])" + name: LogAgentStep + displayName: Log this agent's instance for later cleanup + env: + AgentName: $(Agent.Name) + + - task: DownloadBuildArtifacts@0 + displayName: Download CalculatorUnitTests + inputs: + artifactName: drop + itemPattern: drop/Release/${{ parameters.platform }}/CalculatorUnitTests_VS/AppPackages/CalculatorUnitTests_Test/** + + - task: PowerShell@2 + displayName: Install Certificate + inputs: + filePath: $(Build.ArtifactStagingDirectory)\drop\Release\${{ parameters.platform }}\CalculatorUnitTests_VS\AppPackages\CalculatorUnitTests_Test\Add-AppDevPackage.ps1 + arguments: -CertificatePath $(Build.ArtifactStagingDirectory)\drop\Release\${{ parameters.platform }}\CalculatorUnitTests_VS\AppPackages\CalculatorUnitTests_Test\CalculatorUnitTests.cer -Force + + - task: VSTest@2 + displayName: Run CalculatorUnitTests + inputs: + testAssemblyVer2: $(Build.ArtifactStagingDirectory)\drop\Release\${{ parameters.platform }}\CalculatorUnitTests_VS\AppPackages\CalculatorUnitTests_Test\CalculatorUnitTests.appx + otherConsoleOptions: /Platform:${{ parameters.platform }} + +- job: CleanUpUnitTests${{ parameters.platform }} + dependsOn: UnitTests${{ parameters.platform }} + condition: and(always(), ne(dependencies.UnitTests${{ parameters.platform }}.Outputs['LogAgentStep.agentInstanceId'], '')) + pool: server + variables: + agentInstanceId: $[ dependencies.UnitTests${{ parameters.platform }}.outputs['LogAgentStep.agentInstanceId'] ] + steps: + - task: InvokeRESTAPI@1 + displayName: Reimage test machine + inputs: + connectionType: connectedServiceNameARM + azureServiceConnection: macool-sandbox-interactiveDesktopRS5 + urlSuffix: subscriptions/012a8008-c00f-45b3-9828-41ebba30141d/resourceGroups/interactiveDesktopRS5/providers/Microsoft.Compute/virtualMachineScaleSets/essential/reimage?api-version=2018-10-01 + body: '{ "instanceIds": ["$(agentInstanceId)"] }' \ No newline at end of file diff --git a/build/scripts/CreateAppxBundleMapping.ps1 b/build/scripts/CreateAppxBundleMapping.ps1 new file mode 100644 index 00000000..6a6abc84 --- /dev/null +++ b/build/scripts/CreateAppxBundleMapping.ps1 @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Generates a mapping file to be used with the MakeAppx tool. It generates the file based on + a folder structure grouped by architecture then by project, like this example: + + drop\ + ARM\ + Project\ + AppPackages\ + Project_ARM.appx + Project_scale-100.appx + x64\ + Project\ + AppPackages\ + Project_x64.appx + Project_scale-100.appx + +.PARAMETER InputPath + The path where appx packages to bundle are located. + +.PARAMETER ProjectName + The folder name within each architecture to search recursively for appx packages. The appx files + must also have the ProjectName in their file names. + +.PARAMETER OutputFile + The path to write the generated mapping file. + +.EXAMPLE + Create-AppxBundleMapping -InputPath "C:\drop" -ProjectName "CalculatorApp" -OutputFile "C:\Temp\AppxBundleMapping.txt" +#> +param( + [Parameter(Mandatory)] + [string] + $InputPath, + + [Parameter(Mandatory)] + [string] + $ProjectName, + + [Parameter(Mandatory)] + [string] + $OutputFile +) + +# List all appx packages by architecture +$architectures = @(Get-ChildItem -Path $InputPath -Directory | Foreach-Object Name | Foreach-Object ToLower) +if ($architectures.Count -lt 1) +{ + throw "No architecture-specific folders found in $InputPath" +} + +$defaultArchitecture = $architectures[0] +$packages = @{} +foreach ($architecture in $architectures) +{ + $projectPath = [IO.Path]::Combine($InputPath, $architecture, $ProjectName) + $packages[$architecture] = Get-ChildItem -Path $projectPath -Recurse -Filter *$ProjectName*.appx + + if ($packages[$architecture].Count -lt 1) + { + throw "No .appx files found for architecture $architecture in $projectPath" + } +} + +# List appx packages which are common to all architectures +$commonPackages = $packages[$defaultArchitecture] +foreach ($architecture in $architectures) +{ + $commonPackages = $packages[$architecture] | Where {$commonPackages.Name -Contains $_.Name} +} + +# List appx packages which are architecture-specific and verify that there is exactly one per +# architecture. +$architectureSpecificPackages = @() +if ($architectures.Count -gt 1) +{ + foreach ($architecture in $architectures) + { + $uniquePackages = $packages[$architecture] | Where {$commonPackages.Name -NotContains $_.Name} + if ($uniquePackages.Count -ne 1) + { + throw "Found multiple architecture-specific packages for architecture $($architecture): $($uniquePackages.Name)" + } + $architectureSpecificPackages += $uniquePackages[0] + } +} + +# Write the mapping file +Set-Content $OutputFile "[Files]" +foreach ($package in ($architectureSpecificPackages + $commonPackages)) +{ + $mapping = "`"$($package.FullName)`" `"$($package.Name)`"" + Write-Host $mapping + Add-Content $OutputFile $mapping +} \ No newline at end of file diff --git a/build/scripts/UpdateAppxManifestVersion.ps1 b/build/scripts/UpdateAppxManifestVersion.ps1 new file mode 100644 index 00000000..1c6bc5dd --- /dev/null +++ b/build/scripts/UpdateAppxManifestVersion.ps1 @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Updates the version number in an AppxManifest file. + +.PARAMETER AppxManifest + The path to the AppxManifest file. + +.PARAMETER Version + The version number to write into the file. + +.EXAMPLE + Update-AppxManifestVersion -AppxManifest "C:\App\Package.appxmanifest" -Version "3.2.1.0" +#> +param( + [ValidateScript({Test-Path $_ -PathType 'Leaf'})] + [Parameter(Mandatory)] + [string] + $AppxManifest, + + [ValidateScript({[version]$_})] + [Parameter(Mandatory)] + [string] + $Version +) + +$xmlDoc = [XML](Get-Content $AppxManifest) +$xmlDoc.Package.Identity.setAttribute("Version", $Version); +$xmlDoc.Save($AppxManifest) \ No newline at end of file diff --git a/docs/ApplicationArchitecture.md b/docs/ApplicationArchitecture.md new file mode 100644 index 00000000..d9a053de --- /dev/null +++ b/docs/ApplicationArchitecture.md @@ -0,0 +1,188 @@ +# Application Architecture + +Windows Calculator is a [C++/CX][C++/CX] application, built for the Universal Windows Platform ([UWP][What is UWP?]). +Calculator uses the [XAML][XAML Overview] UI framework, and the project follows the Model-View-ViewModel ([MVVM][MVVM]) +design pattern. This document discusses each of the layers and how they are related to the three Visual Studio projects +that build into the final Calculator application. + +-------------------- +## Table of Contents + +* [View](#view) + * [VisualStates](#visualstates) + * [Data-Binding](#data-binding) +* [ViewModel](#viewmodel) + * [PropertyChanged Events](#propertychanged-events) +* [Model](#model) + +-------------------- + +## View + +The View layer is contained in the [Calculator project][Calculator folder]. This project contains mostly XAML files +and various custom controls that support the UI. [App.xaml][App.xaml] contains many of the [static][StaticResource] and +[theme][ThemeResource] resources that the other XAML files will reference. Its code-behind file, [App.xaml.cpp][App.xaml.cpp], +contains the main entry point to the application. On startup, it navigates to the main page. + +```C++ +rootFrame->Navigate(MainPage::typeid, argument) +``` + +In Calculator, there is only one concrete [Page][Page] class: [MainPage.xaml][MainPage.xaml]. `MainPage` is the root +container for all the other application UI elements. As you can see, there's not much content. `MainPage` uses a +`NavigationView` control to display the toggleable navigation menu, and empty containers for delay-loaded UI elements. +Of the many modes that Calculator shows in its menu, there are actually only three XAML files that `MainPage` needs to +manage in order to support all modes. They are: + +* [Calculator.xaml][Calculator.xaml]: This [UserControl] is itself a container for the [Standard][CalculatorStandardOperators.xaml], + [Scientific][CalculatorScientificOperators.xaml], and [Programmer][CalculatorProgrammerOperators.xaml] modes. +* [DateCalculator.xaml][DateCalculator.xaml]: Everything needed for the DateCalculator mode. +* [UnitConverter.xaml][UnitConverter.xaml]: One `UserControl` to support every Converter mode. + +### VisualStates + +[VisualStates][VisualState] are used to change the size, position, and appearance ([Style][Style]) of UI elements +in order to create an adaptive, responsive UI. A transition to a new `VisualState` is often triggered by specific +window sizes. Here are a few important examples of `VisualStates` in Calculator. Note that it is not a +complete list. When making UI changes, make sure you are considering the various `VisualStates` and layouts that +Calculator defines. + +#### History/Memory Dock Panel expansion + +In the Standard, Scientific, and Programmer modes, the History/Memory panel is exposed as a flyout in small window sizes. +Once the window is resized to have enough space, the panel becomes docked along the edge of the window. + + + +#### Scientific mode, inverse function button presence + +In the Scientific mode, for small window sizes there is not enough room to show all the function buttons. The mode +hides some of the buttons and provides a Shift (↑) button to toggle the visibility of the collapsed rows. When the +window size is large enough, the buttons are re-arranged to display all function buttons at the same time. + + + +#### Unit Converter aspect ratio adjustment + +In the Unit Converter mode, the converter inputs and the numberpad will re-arrange depending on if the window is in +a Portrait or Landscape aspect ratio. + + + +### Data-Binding + +Calculator uses [data binding][Data Binding] to dynamically update the properties of UI elements. If this concept +is new for you, it's also worth reading about [data binding in depth][Data binding in depth]. + +The [x:Bind][x:Bind] markup extension is a newer replacement for the older [Binding][Binding] style. You may see both +styles in the Calculator codebase. Prefer `x:Bind` in new contributions because it has better performance. If you need +to add or modify an existing `Binding`, updating to `x:Bind` is a great first step. Make sure to read and understand +the difference between the two styles, as there are some subtle behavioral changes. Refer to the +[binding feature comparison][BindingComparison] to learn more. + +------------ +## ViewModel + +The ViewModel layer is contained in the [CalcViewModel][CalcViewModel folder] project. ViewModels provide a source of +data for the UI to bind against and act as the intermediary separating pure business logic from UI components that +should not care about the model's implementation. Just as the View layer consists of a hierarchy of XAML files, the +ViewModel consists of a hierarchy of ViewModel files. The relationship between XAML and ViewModel files is often 1:1. +Here are the noteable ViewModel files to start exploring with: + +* [ApplicationViewModel.h][ApplicationViewModel.h]: The ViewModel for [MainPage.xaml][MainPage.xaml]. This ViewModel + is the root of the other mode-specific ViewModels. The application changes between modes by updating the `Mode` property + of the `ApplicationViewModel`. The ViewModel will make sure the appropriate ViewModel for the new mode is initialized. +* [StandardCalculatorViewModel.h][StandardCalculatorViewModel.h]: The ViewModel for [Calculator.xaml][Calculator.xaml]. + This ViewModel exposes functionality for the main three Calculator modes: Standard, Scientific, and Programmer. +* [DateCalculatorViewModel.h][DateCalculatorViewModel.h]: The ViewModel for [DateCalculator.xaml][DateCalculator.xaml]. +* [UnitConverterViewModel.h][UnitConverterViewModel.h]: The ViewModel for [UnitConverter.xaml][UnitConverter.xaml]. + This ViewModel implements the logic to support every converter mode, including Currency Converter. + +### PropertyChanged Events + +In order for [data binding](#data-binding) to work, ViewModels need a way to inform the XAML framework about +updates to their member properties. Most ViewModels in the project do so by implementing the +[INotifyPropertyChanged][INotifyPropertyChanged] interface. The interface requires that the class provides a +[PropertyChanged event][PropertyChanged]. Clients of the ViewModel (such as the UI), can register for the +`PropertyChanged` event from the ViewModel, then re-evaluate bindings or handle the event in code-behind when the +ViewModel decides to raise the event. ViewModels in the Calculator codebase generally uses a macro, defined in the +[Utils.h][Utils.h] utility file, to implement the `INotifyPropertyChanged` interface. Here is a standard +implementation, taken from [ApplicationViewModel.h][ApplicationViewModel.h]. + +```C++ +[Windows::UI::Xaml::Data::Bindable] +public ref class ApplicationViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged +{ +public: + ApplicationViewModel(); + + OBSERVABLE_OBJECT(); +``` + +The `OBSERVABLE_OBJECT()` macro defines the required `PropertyChanged` event. It also defines a private +`RaisePropertyChanged` helper function for the class. The function takes a property name and raises a +`PropertyChanged` event for that property. + +Properties that are intended to be the source for a data binding are also typically implemented with a macro. Here is +one such property from `ApplicationViewModel`: + +```C++ +OBSERVABLE_PROPERTY_RW(Platform::String^, CategoryName); +``` + +The `OBSERVABLE_PROPERTY_RW` macro defines a Read/Write property that will raise a `PropertyChanged` event if its value +changes. Read/Write means the property exposes both a public getter and setter. For efficiency and to avoid raising +unnecessary `PropertyChanged` events, the setter for these types of properties will check if the new value is +different from the previous value before raising the event. + +From this example, either `ApplicationViewModel` or clients of the class can simply assign to the `CategoryName` +property and a `PropertyChanged` event will be raised, allowing the UI to respond to the new `CategoryName` value. + +-------- +## Model + +The Model for the Calculator modes is contained in the [CalcManager][CalcManager folder] project. + + +[References]:#################################################################################################### + +[C++/CX]: https://docs.microsoft.com/en-us/cpp/cppcx/visual-c-language-reference-c-cx +[What is UWP?]: https://docs.microsoft.com/en-us/windows/uwp/get-started/universal-application-platform-guide +[XAML Overview]: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-overview +[MVVM]: https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-and-mvvm + +[Calculator folder]: ..\src\Calculator +[App.xaml]: ..\src\Calculator\App.xaml +[App.xaml.cpp]: ..\src\Calculator\App.xaml.cpp +[StaticResource]: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/staticresource-markup-extension +[ThemeResource]: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/themeresource-markup-extension +[Page]: https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.Page +[UserControl]: https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.UserControl +[MainPage.xaml]: ..\src\Calculator\Views\MainPage.xaml +[Calculator.xaml]: ..\src\Calculator\Views\Calculator.xaml +[CalculatorStandardOperators.xaml]: ..\src\Calculator\Views\CalculatorStandardOperators.xaml +[CalculatorScientificOperators.xaml]: ..\src\Calculator\Views\CalculatorScientificOperators.xaml +[CalculatorProgrammerOperators.xaml]: ..\src\Calculator\Views\CalculatorProgrammerOperators.xaml +[DateCalculator.xaml]: ..\src\Calculator\Views\DateCalculator.xaml +[UnitConverter.xaml]: ..\src\Calculator\Views\UnitConverter.xaml + +[VisualState]: https://docs.microsoft.com/en-us/windows/uwp/design/layout/layouts-with-xaml#adaptive-layouts-with-visual-states-and-state-triggers +[Style]: https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-styles + +[Data Binding]: https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-quickstart +[Data binding in depth]: https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-in-depth +[x:Bind]: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension +[Binding]: https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/binding-markup-extension +[BindingComparison]: https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-in-depth#xbind-and-binding-feature-comparison + +[CalcViewModel folder]: ..\src\CalcViewModel +[ApplicationViewModel.h]: ..\src\CalcViewModel\ApplicationViewModel.h +[StandardCalculatorViewModel.h]: ..\src\CalcViewModel\StandardCalculatorViewModel.h +[DateCalculatorViewModel.h]: ..\src\CalcViewModel\DateCalculatorViewModel.h +[UnitConverterViewModel.h]: ..\src\CalcViewModel\UnitConverterViewModel.h + +[INotifyPropertyChanged]: https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.data.inotifypropertychanged +[PropertyChanged]: https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.data.inotifypropertychanged.propertychanged +[Utils.h]: ..\src\CalcViewModel\Common\Utils.h + +[CalcManager folder]: ..\src\CalcManager diff --git a/docs/Images/CalculatorScreenshot.png b/docs/Images/CalculatorScreenshot.png new file mode 100644 index 00000000..302e66b0 Binary files /dev/null and b/docs/Images/CalculatorScreenshot.png differ diff --git a/docs/Images/VSInstallationScreenshot.png b/docs/Images/VSInstallationScreenshot.png new file mode 100644 index 00000000..baa85c63 Binary files /dev/null and b/docs/Images/VSInstallationScreenshot.png differ diff --git a/docs/Images/VisualStates/Converter1.gif b/docs/Images/VisualStates/Converter1.gif new file mode 100644 index 00000000..20923f8e Binary files /dev/null and b/docs/Images/VisualStates/Converter1.gif differ diff --git a/docs/Images/VisualStates/Scientific1.gif b/docs/Images/VisualStates/Scientific1.gif new file mode 100644 index 00000000..a797e7cd Binary files /dev/null and b/docs/Images/VisualStates/Scientific1.gif differ diff --git a/docs/Images/VisualStates/Standard1.gif b/docs/Images/VisualStates/Standard1.gif new file mode 100644 index 00000000..2396e8df Binary files /dev/null and b/docs/Images/VisualStates/Standard1.gif differ diff --git a/docs/ManualTests.md b/docs/ManualTests.md new file mode 100644 index 00000000..606fefdb --- /dev/null +++ b/docs/ManualTests.md @@ -0,0 +1,353 @@ +# Calculator Manual Tests +These manual tests are run before every release of the Calculator app. + +## Smoke Tests + +### Calculators + +### Math in Standard Calculator + +**Test 1** +Steps: +1. From the Standard Calculator page, input “3”, “+”, “3”, “Enter” on the keyboard +Expected: “6” shows up in the display +2. Input “4”, “-“, “2”, “=” using the in-app buttons +*Expected: “2” shows up in the display* + + **Test 2** + Steps: +1. From the Standard Calculator page, input “3”, “+”, “3”, “Enter” on the keyboard +2. Navigate to the History pane, and verify that “3 + 3 = 6” shows up in the pane +3. Input “MS” using the in-app buttons +4. Navigate to the Memory pane +*Expected: “6” shows up in the pane* + +### Math in Scientific Calculator + +**Test 1** +Steps: +1. From the Scientific Calculator page, input “3”, “^”, “3”, “Enter” on the keyboard +*Expected: “27” shows up in the display* + +**Test 2** +Steps: +1. Input “5”, “n!“, “=” using the in-app buttons +*Expected: “120” shows up in the display* + +### Math in Programmer Calculator + +**Test 1** +Steps: +1. From the Programmer Calculator page, input “1”, “&”, “0”, “Enter” on the keyboard +*Expected: “0” shows up in the display* + +**Test 2** +Steps: +1. Input “15” using the in-app buttons and select “HEX” +*Expected: “F” shows up in the display and the letters A-F show up as in-app buttons* + +### Converters + +**Converter Usage** +Steps: +1. From the Length Converter page, select “kilometers” as the unit type in the input field and input “5” using the keyboard +2. Select “miles” as the unit type in the output field +*Expected: The output starts with is “3.106856”* + + +## Basic Verification Tests + +**Launch App Test** +Steps: +1. Press the Windows key. +2. Navigate to "all apps". +3. Look for "Calculator". +4. Click to launch the "Calculator" app. +*Expected: The calculator app launches gracefully.* + + +**All Calculators Test: Verify All Numbers & Input Methods** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Standard" Calculator. +3. Mouse Input +*Expected: All numbers work via mouse click.* +4. Keyboard Input: +*Expected: All numbers work via number pad.* +5. Navigate to "Scientific" Calculator and Repeat Steps 3-5 +*Expected: Steps 3-5 pass in Scientific mode* +6. Navigate to "Programmer" Calculator and Repeat Steps 3-5 +*Expected: Steps 3-5 pass in Programmer mode* + + +**All Calculators Test: Verify Basic Arithmetic Functions** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Standard" Calculator. +3. Using the Number Pad and Mouse perform the following arithmetic functions and verify the result. + a. (+) Addition + b. (-) Subtraction + c. (x) Multiplication + d. (÷) Division + e. (1/x) Reciprocal + f. (√) Square Root + g. (x²) Squared + h. (x³) Cubed + i. (%) Percent + j. (±) Positive / Negative + k. (=) Equals + l. Delete Button (flag with x in it) + m. [CE] Clear + n. [C] Global Clear + o. (.) Decimal +4. Navigate to "Scientific" Calculator and Repeat Steps 3-19. +5. Navigate to "Programmer" Calculator and Repeat Steps 3-18 (No Decimal in Programming Calc). + + +**Scientific Calculator Test: Verify advanced arithmetic functions** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Scientific" Calculator. +3. Using the Number Pad and Mouse perform the following arithmetic functions and verify the result. + a. (xʸ) Xth Power of Y + b. (y√x) Y Root of X + c. (10ˣ) 10 Power of X + d. (ex) E Power of X + e. (π) Pi + f. (n!) Factorial + g. (Ln) Natural Logarithm + h. (Log) Logarithm + i. (Exp) Exponential + j. (dms) Degrees, Minutes, Seconds + k. (deg) Degrees + l. (Mod) Modulo + m. “( )" Parenthesis + + +**All Calulators Test: Verify memory functions** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Standard" Calculator. +3. Perform a calculation and press the MS button. +4. If small scale, Select the (M) with the drop down arrow +*Expected: Calculation from previous step is present.* +5. Click the (M+) Add to Memory. +*Expected: Previous calculation is added to itself.* +6. Click the (M-) Subtract from Memory. +*Expected: Previous calculation is subtracted from the base calculation.* +7. Click the (MR) Memory Recall. +*Expected: Previous calculation is made primary (This is not available in the Programmer mode).* +8. Check the MC button. +*Expected: The stored information is cleared.* +9. Navigate to "Scientific" Calculator and Repeat Steps 3-8. +*Expected: All in "Scientific" mode.* +10. Navigate to "Programmer" Calculator and Repeat Steps 3-8. +*Expected: All in "Programmer" mode.* + + +**Scientific Calculator Test: Verify trigonometric functions** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Scientific" Calculator. +3. Using the Number Pad and Mouse perform the following trigonometric functions and verify the result. +3. Sine (sin) +4. Cosine (cos) +5. Tangent (tan) +6. Inverse Sine (sin-1) +7. Inverse Cosine (cos-1) +8. Inverse Tangent (tan-1) Inverse Tangent: +9. Press (HYP) for Hyperbolic trig functions: +*Expected: Trig function buttons show hyperbolic trig functions.* +10. Hyperbolic Sine (sinh) +11. Hyperbolic Tangent (tanh) +12. Hyperbolic Cosine (cosh) +13. Inverse Hyperbolic Sine (sinh-1) +14. Inverse Hyperbolic Tangent (tanh-1) +15. Inverse Hyperbolic Cosine (cosh-1) + + +**Programmer Calculator Test: Verify logical functions** +Steps: +1. Launch the "Calculator" app +2. Navigate to "Programmer" Calculator. +3. Using the Number Pad and Mouse perform the following trigonometric functions and verify the result. +4. Rotate Left (RoL) Logical Operator: + 01011001 rol 3 = 11001010. +5. Rotate Right (RoR) Logical Operator: + 01011001 RoR 3 = 00101011. +6. (Lsh) Logical Operator: + (10 multiplied by 2 three times) + 10 Lsh 3 = gives 80. + 10.345 Lsh 3 = also gives 80. +7. (Rsh) Logical Operator: + (16 divided by 2 twice) + 16 Rsh 2 = gives 4. + 16.999 Rsh 2 = also gives 4. +7. (Or) Logical Operator + 101 OR 110 = gives 111. +9. Exclusive Or (Xor) Logical Operator: + 101 XOR 110 = gives 11. +9. (Not) Logical Operator + NOT 1001100111001001 = + 0110011000110110. +10. (And) Logical Operator + 101 AND 110 = gives 100. +11. (Mod) Logical Operator + Remainder of integer division (Modulo x) +12. "( )" Parenthesis + + +**All Calculators and Converters Test: Verify scaling functions and languages** +Steps: +1. Launch the "Calculator" app. +2. For All Modes: While scaling in both directions to capacity +*Expected: Elements like Memory and History are shifted or integrated appropriately.* +3. In Any Mode: While at the Smallest scale, Select the Menu Button +*Expected: The menu items are scrollable with nothing overlapping.* +4. While in the Menu: Check the About Page +*Expected: Everything in the about page fits into its window* +5. For Scientific Mode: At a Larger Scale +*Expected: All buttons are present and the up arrow is grayed out.* +6. For Scientific Mode: At a Smaller Scale +*Expected: All buttons are present and the up arrow is able to be toggled.* +7. For Programmer Mode: At a Any Scale +*Expected: All buttons are present and the up arrow is able to be toggled.* +8. For Converter Mode: While Scaling +*Expected: The number pad and input areas move around each other gracefully.* +9. Changing Language: Open Settings app > Time & language > Region & language > Add a language > Select a Right to Left (RTL) language such as Hebrew > Install the associated files> Set it to the system default. +10. Set the system number format preference: Open a Run dialog (WIN + R) > type ‘intl.cpl’ > Enter > In the format dropdown list > Select Hebrew > Apply. +11. Initiating the change: Package has completed installing > Sign out > Sign in. (This change to the app may also require a reinstallation of the build) +12. Repeat Steps 2-6 again in a (RTL) language. +*Expected: No elements fall out of intended boundaries.* + + +**All Calculators Test: Verify toggling functions** +Steps: +1. Launch the "Calculator" app. +2. For Standard & Scientific Modes: While in the Smallest scale, verify that the History Icon brings up the history panel gracefully and is displayed appropriately. +For Scientific Mode: At a Smaller Scale +Verify the following: +3. Grad / Deg / Rad + Perform a trig function +*Expected: The answer to the function is in the selected grad/deg/rad. Repeat for each of the modes.* +4. (Hyp) Hyperbolic +*Expected: Sin toggles to Sinh, Cos toggles to Cosh, Tan toggles to Tanh.* +5. (F-E key) Floating Point Notation & Scientific Notation. +*Expected: Display toggles between floating point and Scientific notation.* +For Programmer Mode +Verify the following: +6. "Bit Toggling Keypad": +*Expected: In app keypad changes to represent Bits (1s and 0s).* +7. "QWORD / DWORD / WORD / BYTE": +*Expected: Toggles as expected.* +8. "Hex" Hexadecimal: +*Expected: A B C D E F become active and user can use them. A maximum of 16 characters can be entered.* +9. "Dec" Decimal: +*Expected: A B C D E F are inactive. A maximum of 19 characters can be entered.* +10. "Oct" Octal: +*Expected: A B C D E F 8 9 are inactive. A maximum of 22 characters can be entered.* +11. "Bin" Binary: +*Expected: A B C D E F 2 3 4 5 6 7 8 9 are inactive. A maximum of 64 characters can be entered.* + + +**Date Calculation Test: Verify dates can be calculated.** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Date Calculation" Calculator. +3. With "Difference between dates" Selected + Change the various date input fields +*Expected: From and To reflect dates input respectively.* +5. With "Add or Subtract days" Selected + Change the various date input fields +*Expected: Verify changes made to both add and subtract reflect input respectively.* + + +**Currency Converter Test: Verify conversion & updating current currency rates.** +Steps: +1. Launch the "Calculator" app. +2. Navigate to "Currency Converter" Calculator. +3. Select 2 Currency types from the dropdowns & enter a "1" into a conversion slot. +*Expected: The currency is slotted properly and converted rate matches the ratio provided under the selected currency types.* +4. Click "Updated" +*Expected: Display matches PC's date and time.* +5. After at least a minute: Select "Update rates" & Check "Updated" again: +*Expected: The "Update Rates" button changes the date and time to match the computer's current date and time.* + + +**All Calculators Test: Hotkeys: Verify Hot Key function.** +Steps: +1. Launch the "Calculator" app. +For All Applicable Modes: +Verify the following: +2. Press **Alt +1** to Enter "Standard" mode +*Expected: Move to "Standard" screen.* +3. Press **Alt +2** to Enter "Scientific" mode +*Expected: Move to "Scientific" screen.* +4. Press **Alt +3** to Enter "Programmer" mode +*Expected: Move to "Programming" screen.* +5. Press **Alt +4** to Enter "Date Calculation" mode +*Expected: Move to "Date Calculation" screen.* +6. Press **Ctrl +M** to Store in Memory +7. Press **Ctrl +P** to Add to Active Memory +8. Press **Ctrl +Q** to Subtract form Active Memory +9. Press **Ctrl +R** to Recall from Memory +10. Press **Ctrl +L** to Clear from Memory +11. Press **Delete** to Clear Current Input 'CE' +12. Press **Esc** to Full Clear Input 'C' +13. Press **F9** to Toggle '±' +14. Press **R** to Select '1/x' +15. Press **@** to Select '√' +16. Press **Ctrl + H** to Toggle History Panel +*Expected: Function when in small scale window.* +17. Press **Up arrow** to Move up History Panel +*Expected: Function when in small scale window.* +18. Press **Down arrow** to Move Down History Panel +*Expected: Function when in small scale window.* +19. Press **Ctrl + Shift + D** to Clear History Panel +*Expected: Function when in small scale window.* +20. Press **Spacebar** to Repeat Last Input: +Verify the following in Scientific Mode +21. Press **F3** to Select 'DEG' +22. Press **F4** to Select 'RAD' +23. Press **F5** to Select 'GRAD' +24. Press **Ctrl +G** to Select '10ˣ' +25. Press **Ctrl +Y** to Select 'y√x' +26. Press **Shift +O** to Select 'sin-1' +27. Press **Shift + S** to Select 'cos-1' +28. Press **Shift +T** to Select 'tan-1' +29. Press **Ctrl +O** to Select 'Cosh' +30. Press **Ctrl +S** to Select 'Sinh' +31. Press **Ctrl +T** to Select 'Tanh' +32. Press **D** to Select 'Mod' +33. Press **L** to Select 'log' +34. Press **M** to Select 'dms' +35. Press **N** to Select 'ln' +36. Press **Ctrl +N** to Select 'ex' +37. Press **O** to Select 'Cos' +38. Press **P** to Select 'π' +39. Press **Q** to Select 'x²' +40. Press **S** to Select 'Sin' +41. Press **T** to Select 'Tan' +42. Press **V** to Toggle 'F-E' +43. Press **X** to Select 'Exp' +44. Press **Y** or **^** to Select 'xʸ' +45. Press **#** to Select 'x³' +46. Press **!** to Select 'n!' +Verify the following in Programmer Mode +47. Press **F2** to Select 'DWORD' +48. Press **F3** to Select 'WORD' +49. Press **F4** to Select 'BYTE' +50. Press **F5** to Select 'HEX' +51. Press **F6** to Select 'DEC' +52. Press **F7** to Select 'OCT' +53. Press **F8** to Select 'BIN' +54. Press **F12** to Select 'QWORD' +55. Press **A-F** to Input in HEX +56. Press **J** to Select 'RoL' +57. Press **K** to Select 'RoR' +58. Press **<** to Select 'Lsh' +59. Press **>** to Select 'Rsh' +60. Press **%** to Select 'Mod' +61. Press ** | ** to Select 'Or' +62. Press **~** to Select 'Not' +63. Press **&** to Select 'And' diff --git a/docs/NewFeatureProcess.md b/docs/NewFeatureProcess.md new file mode 100644 index 00000000..875bacf3 --- /dev/null +++ b/docs/NewFeatureProcess.md @@ -0,0 +1,149 @@ +# New feature process + +## When should I follow this process? +You need to follow this process for any change which "users will notice". This applies especially +to new features and major visual changes. + +You do not need to follow this process for bug fixes, performance improvements, or changes to the +development tools. To contribute these changes, discuss the issue with the team and then submit a +pull request. + +## Step 1: Create an issue and discuss with the community +New features are submitted in Feedback Hub. In Feedback Hub you can upvote existing feedback or +submit your own. We also encourage discussion on open issues in Feedback Hub and in GitHub. + +## Step 2: Wait for Microsoft product team sponsorship +New features must have a sponsor from the Microsoft product team. We can only work on a few ideas +at a time, so some good feature ideas might remain open but unassigned to a sponsor. + +## Step 3: Scoping and feature pitch +Once we've decided to sponsor a feature, a member of the Microsoft team will write a +*feature pitch*. The feature pitch concisely describes our point of view on the problem and will +typically include these sections: + +* **Problem Statement**: What problem are we trying to solve? Who’s the customer? Is there a + customer need or pain point we need to remedy? Is there a business goal or metric we are trying + to improve? Do we have a hypothesis we want to prove or disprove? +* **Evidence or User Insights**: Why should we do this? Potential sources of data: Feedback Hub, + GitHub, request from another team, telemetry data, anecdotes from listening to customers in + person, user research, market or competitive research +* **Proposal**: How will the solution/feature help us solve the problem? How will the + solution/feature meet the customer’s needs? How will the solution/feature improve the metrics? + Who’s the target audience? +* **Risks**: This section may not be necessary if covered by the problem statement. What is the + risk if we don’t do this work? What is the risk if we do? +* **Goals**: What you want to accomplish with this feature. Typical examples include + “User Can *perform some task*” +* **Non-Goals**: Things we are explicitly not doing or supporting or that are out of scope, + including any reasoning to why. + +The feature pitch may also include a low-fidelity concept which will be refined during the +prototyping process. + +We will edit the issue description on GitHub to include the feature pitch. + +## Step 4: Prototyping +After the goals are written, we think of a variety of ways to address these goals and build +*prototypes* to try them out. We welcome community participation in this process. + +Prototypes can take many forms. For many ideas, making changes directly to the app code (without +spending too much time making the code robust or maintainable) can be a fast and effective way to +try out new ideas. Or you might prefer to use design software, or even pencil and paper. Work from +low-fidelity to high-fidelity—try a few ideas for the overall approach before making your +preferred design pixel-perfect. + +An important part of the prototyping process is sharing our work along the way, getting feedback, +and iterating on the design. Drawings, links to code, and other work-in-progress can be added to +the wiki for this project. Progress updates will be posted in the issues section. + +During the investigation phase, we might discover that the idea isn't feasible or doesn't align +with our product roadmap. If this happens, we'll document what we learned and close the issue. + +## Step 5: Prototype review +Once there is a high-fidelity design which addresses the goals described in the original pitch, the +Microsoft product team will review the prototype and ensure all items on this checklist are +addressed: + +- [ ] Is there a high-fidelity design which gives reviewers a clear idea of how the feature will + look and function when implemented? +- [ ] Has the plan been shared with the community (documented on the wiki and updates posted in the + original issue) and have others been given an opportunity to provide suggestions? +- [ ] Are [Fluent design principles](https://docs.microsoft.com/en-us/windows/uwp/design/fluent-design-system/) + followed? If we do something which deviates from the guidelines, do we have a good reason? +- [ ] Does the design include provisions for [all users](https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/designing-inclusive-software) + and [all cultures](https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/guidelines-and-checklist-for-globalizing-your-app)? +- [ ] Is it technically feasible to build this feature? Take a look at the "before committing" + checklist below and identify any issues which are likely to be blockers. + +## Step 6: Implementation +A feature can be implemented by the original proposer, the Microsoft team sponsor, or by other +community members. Code contributions and testing help are greatly appreciated. Please let us know +in the issue comments if you're actively working on a feature so we can ensure it's assigned to +you. + +You might be able to reuse code written during the prototype process, although there will typically +be more work required to make the solution robust. Once the code is ready, you can begin +[submitting pull requests](../CONTRIBUTING.md). + +## Step 7: Technical review +As with all changes, the code for new features will be reviewed by a member of the Microsoft team +before being checked in to the master branch. + +New features often need a more thorough technical review than bug fixes. When reviewing code for +new features, the Microsoft team considers at least these items: + +- [ ] All items on the [Accessibility checklist](https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/accessibility-checklist) + should be addressed. +- [ ] All items on the [Globalization checklist](https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/guidelines-and-checklist-for-globalizing-your-app) + should be addressed. +- [ ] The change should be tested on the oldest version of Windows that the app supports. You can + find this version number in AppxManifest.xml. Any calls to APIs newer than that version should be + [conditionally enabled](https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/version-adaptive-apps). +- [ ] The change should use only supported APIs. If there are any questions about whether legacy or + undocumented APIs are in use, the [Windows App Certification Kit](https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/windows-app-certification-kit) + should be run to check. +- [ ] The change should save the user's progress if the app is + [suspended and resumed](https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-suspend-resume). + Code to handle these cases should be + [tested in the Visual Studio debugger](https://docs.microsoft.com/en-us/visualstudio/debugger/how-to-trigger-suspend-resume-and-background-events-for-windows-store-apps-in-visual-studio). +- [ ] If the change [has customizations for particular device families](https://docs.microsoft.com/en-us/uwp/extension-sdks/device-families-overview), + it should be tested on those device families. +- [ ] The change should be tested with the app window resized to the smallest possible size. +- [ ] The change should be tested with light, dark, and high contrast themes. It should honor the + user's preferred [accent color](https://docs.microsoft.com/en-us/windows/uwp/design/style/color#accent-color-palette). +- [ ] If the change adds new libraries or other dependencies: + - [ ] If the library is packaged with the app, the increased size of the binaries should be + measured. + - [ ] If the library is not maintained by Microsoft, the Microsoft team will need to set up a + plan to monitor the upstream library for changes like security fixes. + - [ ] If the library is being used under an open-source license, we must comply with the license + and credit third parties appropriately. +- [ ] If the change adds code which runs during the app's startup path, or adds new XAML elements + which are loaded at startup: + - [ ] Run the perf tests to measure any increase in startup time. Move work out of the startup + path if possible. +- [ ] If the change adds additional logging: + - [ ] All logging should use [TraceLogging](https://docs.microsoft.com/en-us/windows/desktop/tracelogging/trace-logging-about). + - [ ] Unnecessary log events should be removed, or configured so that they are collected only when + needed to debug issues or measure feature usage. +- [ ] If the change reads user data from files or app settings: + - [ ] Verify that state saved in a previous version of the app can be used with the new version. +- [ ] If the change makes network requests: + - [ ] Microsoft must plan to keep these dependencies secure and functional for the lifetime of + the app (which might be several years). + - [ ] The app should be fully functional if some network requests are slow or fail. Tools like + [Fiddler](http://docs.telerik.com/fiddler/knowledgebase/fiddlerscript/perftesting) + can be used to simulate slow or failed requests. + +## Step 8: Final product review and merge to master +After the technical review is complete, the product team will review the finished product to make +sure the final implementation is ready to release to Windows customers. + +## Step 9: Release +The release process is handled internally by the Microsoft team. When we release, we create a +`servicing` branch from master. We merge changes into servicing branches only to fix severe bugs. + +Releases are distributed through the Microsoft Store, first to Windows Insiders and then to all +supported Windows 10 devices once we are confident in the update's quality. We usually ship an +update every month. After the app has been released to the Microsoft Store, it will be part of +the next major update to Windows 10 (especially for new devices). \ No newline at end of file diff --git a/internal/Calculator.TestPackage/Calculator.TestPackage.csproj b/internal/Calculator.TestPackage/Calculator.TestPackage.csproj new file mode 100644 index 00000000..2a3a29d2 --- /dev/null +++ b/internal/Calculator.TestPackage/Calculator.TestPackage.csproj @@ -0,0 +1,131 @@ + + + + + Debug + AnyCPU + {24767C43-CD5A-4DC9-8D6B-429F255524E5} + Library + Properties + Calculator.TestPackage + Calculator.TestPackage + .NETPortable + v5.0 + .NETCore,Version=v5.0 + UAP + 10.0.17763.0 + 10.0.17134.0 + 512 + + true + True + + + true + full + false + bin\Debug\x86 + DEBUG;TRACE + prompt + 4 + x86 + + + pdbonly + true + bin\Release\x86 + TRACE + prompt + 4 + x86 + + + true + full + false + bin\Debug\x64 + DEBUG;TRACE + prompt + 4 + x64 + + + pdbonly + true + bin\Release\x64 + TRACE + prompt + 4 + x64 + + + true + full + false + bin\Debug\arm + DEBUG;TRACE + prompt + 4 + ARM + + + pdbonly + true + bin\Release\arm + TRACE + prompt + 4 + ARM + + + true + full + false + bin\Debug\ARM64\ + DEBUG;TRACE + prompt + 4 + ARM64 + + + pdbonly + true + bin\Release\ARM64\ + TRACE + prompt + 4 + ARM64 + + + + + + + + {9447424a-0e05-4911-beb8-e0354405f39a} + Calculator + + + + 15.0 + + + + + + + + + + + + + @(AppxBundleOutput->'%(RootDir)%(Directory)') + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\$(PlatformTarget)\Microsoft.VCLibs.$(PlatformTarget).Debug.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\$(PlatformTarget)\Microsoft.VCLibs.$(PlatformTarget).14.00.appx; + $(UniversalTestCustomMacros)AppxPackageWinUIDependency=$(AppxPackageTestDir)Dependencies\$(PlatformTarget)\Microsoft.UI.Xaml.2.0.appx; + $(UniversalTestCustomMacros)AppxPackagePublicKeyFile=@(AppxPackagePublicKeyFile->'%(FullPath)');AppxBundleOutput=@(AppxBundleOutput->'%(FullPath)'); + + + + diff --git a/internal/Calculator.TestPackage/Calculator.TestPackage.wm.xml b/internal/Calculator.TestPackage/Calculator.TestPackage.wm.xml new file mode 100644 index 00000000..1f3e6c26 --- /dev/null +++ b/internal/Calculator.TestPackage/Calculator.TestPackage.wm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/internal/Calculator.TestPackage/project.json b/internal/Calculator.TestPackage/project.json new file mode 100644 index 00000000..3f3f1eb9 --- /dev/null +++ b/internal/Calculator.TestPackage/project.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + "Microsoft.TestInfrastructure.UniversalTest": "1.0.20181107.1" + }, + "frameworks": { + "netcore50": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-x64": {}, + "win10-x86": {} + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Calculator.UIAutomationLibrary.csproj b/internal/Calculator.UIAutomationLibrary/Calculator.UIAutomationLibrary.csproj new file mode 100644 index 00000000..f752c1a2 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Calculator.UIAutomationLibrary.csproj @@ -0,0 +1,99 @@ + + + + Debug + AnyCPU + {A43517B5-8BE3-4312-913F-004978C34444} + Library + Properties + Calculator.UIAutomationLibrary + Calculator.UIAutomationLibrary + .NETPortable + v5.0 + .NETCore,Version=v5.0 + UAP + 10.0.17763.0 + 10.0.17134.0 + 512 + 15.0 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 15.0 + + + + + \ No newline at end of file diff --git a/internal/Calculator.UIAutomationLibrary/CalculatorAppLauncher.cs b/internal/Calculator.UIAutomationLibrary/CalculatorAppLauncher.cs new file mode 100644 index 00000000..b90c984e --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/CalculatorAppLauncher.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Etw.Managed; +using Microsoft.OneCoreUap.Test.AppModel; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Calculator.UIAutomationLibrary.Components; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary +{ + /// + /// Class that can open and close the Calculator app. + /// + public static class CalculatorAppLauncher + { + public const string CoreWindowClassName = "Windows.UI.Core.CoreWindow"; + + // This doesn't actually work right now becaue popup will disappear + // Bug 13713223: ContentDialog/Popup does not show up in the UIA tree when Windows.Current.Content has an AutomationName set. + // public static readonly UICondition TopLevelWindowUICondition = UICondition.CreateFromId(Constants.TopLevelWindowAutomationId); + public static readonly UICondition CoreWindowUICondition = UICondition.CreateFromClassName(CoreWindowClassName) + .AndWith(UICondition.CreateFromName(Constants.AppWindowName)); + + /// + /// Launch the app + /// + public static CalculatorAppLfm Launch() + { + Log.Comment("Launching Calculator and waiting for first page load..."); + + // Need to set this for the MITALite Tap~ methods to work on high DPI screens. + UAPApp.SetTestDPIAwareness(); + + // We want to be able to see any element in the tree + Context.RawContext.Activate(); + + // Set default wait timeout. + MS.Internal.Mita.Foundation.Waiters.Waiter.DefaultTimeout = TimeSpan.FromSeconds(30); + + // Enable Mita internal logging. + MS.Internal.Mita.Foundation.Utilities.Log.OutImplementation = (s, a) => { Log.Comment($"- [MitaLite] { string.Format(s, a) }"); }; + + using (EtwWaiter appLaunchWaiter = new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.AppLaunchEndETWEventName)) + { + var viewDescriptor = NavigationHelper.LaunchApplication(Constants.PackageAppUserModelId); + appLaunchWaiter.Wait(TimeSpan.FromSeconds(30)); + + Window calculatorWindow = new Window(UIObject.Root.Descendants.Find(CoreWindowUICondition)); + Debug.Assert(calculatorWindow.ClassName == CoreWindowClassName); + + // Move our window to the foreground. + WindowHelper.SetAsForeground(calculatorWindow.GetTopLevelWindow()); + + return new CalculatorAppLfm(new CalculatorAppPom(calculatorWindow), viewDescriptor); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppLfm.cs new file mode 100644 index 00000000..41c34be5 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppLfm.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.OneCoreUap.Test.AppModel; +using MS.Internal.Mita.Foundation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class CalculatorAppLfm + { + private readonly IViewDescriptor viewDescriptor; + + public CalculatorAppLfm(CalculatorAppPom objectModel, IViewDescriptor viewDescriptor) + { + this.ObjectModel = objectModel; + this.viewDescriptor = viewDescriptor; + } + + public CalculatorAppPom ObjectModel { get; } + + public MainPageLfm MainPageLfm + { + get + { + return new MainPageLfm(this.ObjectModel.MainPagePom); + } + } + + public void Close() + { + // ObjectModel is essentially the window ui object. + if (this.viewDescriptor != null) + { + NavigationHelper.CloseApplication(this.viewDescriptor.AUMID); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppPom.cs b/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppPom.cs new file mode 100644 index 00000000..dac82b21 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/App/CalculatorAppPom.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class CalculatorAppPom : UIObject + { + /// + /// Creates a new instance of the class. + /// + /// UIObject for the calculator app window. + public CalculatorAppPom(UIObject uiObject) + : base(uiObject) + { + } + public MainPagePom MainPagePom + { + get + { + return new MainPagePom(this); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/ContentDialogLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/ContentDialogLfm.cs new file mode 100644 index 00000000..08888c96 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/ContentDialogLfm.cs @@ -0,0 +1,35 @@ +// 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 System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class ContentDialogLfm + { + public ContentDialogLfm(ContentDialogPom objectModel) + { + this.ObjectModel = objectModel; + } + + public ContentDialogPom ObjectModel { get; } + + public void InvokePrimary() + { + this.ObjectModel.PrimaryButton.Invoke(); + } + public void InvokeSecondary() + { + this.ObjectModel.SecondaryButton.Invoke(); + } + + public void InvokeClose() + { + this.ObjectModel.CloseButton.Invoke(); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/ContentDialogPom.cs b/internal/Calculator.UIAutomationLibrary/Components/ContentDialogPom.cs new file mode 100644 index 00000000..b52e97db --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/ContentDialogPom.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class ContentDialogPom : UIObject + { + private static readonly UICondition textScrollViewerCondition = UICondition.CreateFromId("ContentScrollViewer"); + private static readonly UICondition titleTextBlockCondition = UICondition.CreateFromClassName("TextBlock"); + + private static readonly UICondition primaryButtonCondition = + UICondition.CreateFromClassName("Button") + .AndWith(UICondition.CreateFromId("PrimaryButton")); + private static readonly UICondition secondaryButtonCondition = + UICondition.CreateFromClassName("Button") + .AndWith(UICondition.CreateFromId("SecondaryButton")); + private static readonly UICondition closeButtonCondition = + UICondition.CreateFromClassName("Button") + .AndWith(UICondition.CreateFromId("CloseButton")); + + public ContentDialogPom(UIObject uiObject) : base(uiObject) + { + } + + public Button PrimaryButton + { + get + { + return new Button(this.Children.Find(primaryButtonCondition)); + } + } + + public Button SecondaryButton + { + get + { + return new Button(this.Children.Find(secondaryButtonCondition)); + } + } + + public Button CloseButton + { + get + { + return new Button(this.Children.Find(closeButtonCondition)); + } + } + + public string Title + { + get + { + var scrollViewer = this.Children.Find(textScrollViewerCondition); + var textBlock = scrollViewer.Children.Find(titleTextBlockCondition); + return textBlock.Name; + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutLfm.cs new file mode 100644 index 00000000..d6b3e00c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutLfm.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Calculator.UIAutomationLibrary.Components +{ + public class AboutFlyoutLfm : ICanFocusWithClicks + { + private const string FlyoutId = "FlyoutNav"; + + /// + /// Initializes a new instance of the class. + /// + /// The AboutFlyoutPom that represents the About flyout panel. + public AboutFlyoutLfm(AboutFlyoutPom objectModel) + { + this.ObjectModel = objectModel; + } + + public AboutFlyoutPom ObjectModel { get; } + + public void FocusWithClicks() + { + this.ObjectModel.Title.DoubleClick(); + } + + public void Close() + { + this.ObjectModel.SendKeys("{ESC}"); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutPom.cs new file mode 100644 index 00000000..29fa28ce --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/AboutFlyoutPom.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class AboutFlyoutPom : UIObject + { + private const string TitleId = "Header"; + + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the navigation menu. + public AboutFlyoutPom(UIObject uiObject) : base(uiObject) + { + } + + public UIObject Title + { + get + { + return new UIObject(this.Descendants.Find(TitleId)); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/CalculatorBasePom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/CalculatorBasePom.cs new file mode 100644 index 00000000..c8cdd952 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/CalculatorBasePom.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the Display section of the calculator modes. + /// + public class CalculatorBasePom : UIObject + { + private const string ExpressionContainerId = "CalculatorExpression"; + private const string NormalOutputId = "normalOutput"; + + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the standard calculator. + public CalculatorBasePom(UIObject uiObject) : base(uiObject) + { + } + + public TextBlock Expression + { + get + { + return new TextBlock(this.Descendants.Find(ExpressionContainerId)); + } + } + + public UIEventWaiter GetExpressionChangedWaiter() + { + return new PropertyChangedEventWaiter(this.Expression, UIProperty.Get("Name")); + } + + public TextBlock Display + { + get + { + return new TextBlock(this.Descendants.Find(NormalOutputId)); + } + } + + public UIEventWaiter GetDisplayChangedWaiter() + { + return new PropertyChangedEventWaiter(this.Display, UIProperty.Get("Name")); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorLfm.cs new file mode 100644 index 00000000..727b5dd9 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorLfm.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Calculator.UIAutomationLibrary.Components +{ + public class DateCalculatorLfm + { + public DateCalculatorLfm(DateCalculatorPom dateCalculatorPom) + { + this.ObjectModel = dateCalculatorPom; + } + + public DateCalculatorPom ObjectModel { get; } + + public void EnsureDateDifferenceMode() + { + this.OpenModeSelector(); + this.ObjectModel.ModeSelector.AllItems[0].Select(); + } + + public void EnsureAddSubtractMode() + { + this.OpenModeSelector(); + this.ObjectModel.ModeSelector.AllItems[1].Select(); + } + + private void OpenModeSelector() + { + using (var waiter = this.ObjectModel.ModeSelector.GetExpandedWaiter()) + { + this.ObjectModel.ModeSelector.Expand(); + waiter.TryWait(); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorPom.cs new file mode 100644 index 00000000..2c240f0c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/DateCalculatorPom.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class DateCalculatorPom : UIObject + { + private const string ModeSelectorId = "DateCalculationOption"; + + public DateCalculatorPom(UIObject uiObject) : base(uiObject) + { + } + + public ComboBox ModeSelector + { + get + { + return new ComboBox(this.Descendants.Find(ModeSelectorId)); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPageLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPageLfm.cs new file mode 100644 index 00000000..41f12eaf --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPageLfm.cs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Etw.Managed; +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class MainPageLfm : ICanFocusWithClicks + { + public MainPageLfm(MainPagePom objectModel) + { + this.ObjectModel = objectModel; + } + + public MainPagePom ObjectModel { get; } + + public NavBarLfm OpenNavBar() + { + using (EtwWaiter waiter = this.ObjectModel.GetNavBarOpenedWaiter()) + { + this.ObjectModel.NavButton.Invoke(); + waiter.Wait(); + } + + return new NavBarLfm(this.ObjectModel.NavBarPom); + } + + public void CloseNavBar() + { + this.ObjectModel.NavBarPom.CloseButton.Invoke(); + } + + public void FocusWithClicks() + { + this.ObjectModel.Header.DoubleClick(); + } + + public StandardCalculatorLfm NavigateToStandardCalculator() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectStandard(); + waiter.TryWait(); + } + + return new StandardCalculatorLfm(this.ObjectModel.StandardCalculatorPom); + } + + public ScientificCalculatorLfm NavigateToScientificCalculator() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectScientific(); + waiter.TryWait(); + } + + return new ScientificCalculatorLfm(this.ObjectModel.ScientificCalculatorPom); + } + + public ProgrammerCalculatorLfm NavigateToProgrammerCalculator() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectProgrammer(); + waiter.TryWait(); + } + + return new ProgrammerCalculatorLfm(this.ObjectModel.ProgrammerCalculatorPom); + } + + public DateCalculatorLfm NavigateToDateCalculator() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectDate(); + waiter.TryWait(); + } + + return new DateCalculatorLfm(this.ObjectModel.DateCalculatorPom); + } + + public UnitConverterLfm NavigateToCurrencyConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectCurrency(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToVolumeConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectVolume(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToLengthConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectLength(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToWeightConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectWeight(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToTemperatureConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectTemperature(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToEnergyConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectEnergy(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToAreaConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectArea(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToSpeedConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectSpeed(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToTimeConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectTime(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToPowerConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectPower(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToDataConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectData(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToPressureConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectPressure(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public UnitConverterLfm NavigateToAngleConverter() + { + var navBar = this.OpenNavBar(); + using (var waiter = this.ObjectModel.GetModeChangedWaiter()) + { + navBar.SelectAngle(); + waiter.TryWait(); + } + + return new UnitConverterLfm(this.ObjectModel.UnitConverterPom); + } + + public AboutFlyoutLfm OpenAboutFlyout() + { + var navBar = this.OpenNavBar(); + using (EtwWaiter waiter = new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.AboutFlyoutOpenedETWEventName)) + { + navBar.SelectAbout(); + waiter.Wait(); + } + + return new AboutFlyoutLfm(this.ObjectModel.AboutFlyoutPom); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPagePom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPagePom.cs new file mode 100644 index 00000000..aecca8f7 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/MainPagePom.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Etw.Managed; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Physical Object Model for the app window. + /// POM is the implementation model of the app. + /// See following references to POM: + /// * https://blogs.msdn.microsoft.com/wltester/2011/11/14/object-model-design/ + /// * https://blogs.msdn.microsoft.com/micahel/2005/06/03/how-do-i-invoke-thee-let-me-count-the-ways-the-physical-object-model/ + /// See https://en.wikipedia.org/wiki/Model-based_testing for model-based testing. + /// + public class MainPagePom : UIObject + { + private const string NavButtonId = "TogglePaneButton"; + private const string SplitViewPaneRootId = "PaneRoot"; + private const string NavBarFlyoutId = "FlyoutNav"; + private const string HeaderId = "Header"; + private const string AboutPageFlyoutId = "AboutPageFlyout"; + + public MainPagePom(UIObject uiObject) + : base(uiObject) + { + } + + public Button NavButton => new Button(this.Descendants.Find(UICondition.CreateFromId(NavButtonId))); + + public UIObject Header => new UIObject(this.Descendants.Find(HeaderId)); + + public NavBarPom NavBarPom => new NavBarPom(this.Children.Find(SplitViewPaneRootId)); + + public EtwWaiter GetNavBarOpenedWaiter() => new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.NavBarOpenedETWEventName); + + public StandardCalculatorPom StandardCalculatorPom => new StandardCalculatorPom(this); + + public ScientificCalculatorPom ScientificCalculatorPom => new ScientificCalculatorPom(this); + + public ProgrammerCalculatorPom ProgrammerCalculatorPom => new ProgrammerCalculatorPom(this); + + public DateCalculatorPom DateCalculatorPom => new DateCalculatorPom(this); + + public UnitConverterPom UnitConverterPom => new UnitConverterPom(this); + + public AboutFlyoutPom AboutFlyoutPom => new AboutFlyoutPom(this.Descendants.Find(AboutPageFlyoutId)); + + public EtwWaiter GetModeChangedWaiter() => new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.AppModeChangeEndETWEventName); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarLfm.cs new file mode 100644 index 00000000..9201eb97 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarLfm.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Windows.Automation; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Patterns; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the NavBar of the Calculator app. + /// + public class NavBarLfm : ICanFocusWithClicks + { + /// + /// Initializes a new instance of the class. + /// + /// The NavBarPom that represents the NavBar. + public NavBarLfm(NavBarPom objectModel) + { + this.ObjectModel = objectModel; + } + + public NavBarPom ObjectModel { get; } + + public void SelectStandard() + { + SelectItem(this.ObjectModel.StandardMenuItem); + } + + public void SelectScientific() + { + SelectItem(this.ObjectModel.ScientificMenuItem); + } + + public void SelectProgrammer() + { + SelectItem(this.ObjectModel.ProgrammerMenuItem); + } + + public void SelectDate() + { + SelectItem(this.ObjectModel.DateMenuItem); + } + + public void SelectCurrency() + { + SelectItem(this.ObjectModel.CurrencyMenuItem); + } + + public void SelectVolume() + { + SelectItem(this.ObjectModel.VolumeMenuItem); + } + + public void SelectLength() + { + SelectItem(this.ObjectModel.LengthMenuItem); + } + + public void SelectWeight() + { + SelectItem(this.ObjectModel.WeightMenuItem); + } + + public void SelectTemperature() + { + SelectItem(this.ObjectModel.TemperatureMenuItem); + } + + public void SelectEnergy() + { + SelectItem(this.ObjectModel.EnergyMenuItem); + } + + public void SelectArea() + { + SelectItem(this.ObjectModel.AreaMenuItem); + } + + public void SelectSpeed() + { + SelectItem(this.ObjectModel.SpeedMenuItem); + } + + public void SelectTime() + { + SelectItem(this.ObjectModel.TimeMenuItem); + } + + public void SelectPower() + { + SelectItem(this.ObjectModel.PowerMenuItem); + } + + public void SelectData() + { + SelectItem(this.ObjectModel.DataMenuItem); + } + + public void SelectPressure() + { + SelectItem(this.ObjectModel.PressureMenuItem); + } + + public void SelectAngle() + { + SelectItem(this.ObjectModel.AngleMenuItem); + } + + public void SelectAbout() + { + this.ObjectModel.AboutButton.Invoke(); + } + + public void Close() + { + this.ObjectModel.CloseButton.Invoke(); + } + + public void FocusWithClicks() + { + // To focus (for AccSpot) without changing anything, click to the right of the close button. + Button button = this.ObjectModel.CloseButton; + int xPos = button.BoundingRectangle.Width + Constants.ClickMargin; + int yPos = button.BoundingRectangle.Height / 2; + button.DoubleClick(PointerButtons.Primary, xPos, yPos); + } + + private void SelectItem(ListViewItem item) + { + if (item.IsSelected) + { + this.Close(); + } + else + { + item.Select(); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarPom.cs new file mode 100644 index 00000000..0b8b47a2 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/NavBarPom.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the navigation menu. + /// + public class NavBarPom : UIObject + { + private const string StandardId = "Standard"; + private const string ScientificId = "Scientific"; + private const string ProgrammerId = "Programmer"; + private const string DateId = "Date"; + private const string CurrencyId = "Currency"; + private const string VolumeId = "Volume"; + private const string LengthId = "Length"; + private const string WeightId = "Weight"; + private const string TemperatureId = "Temperature"; + private const string EnergyId = "Energy"; + private const string AreaId = "Area"; + private const string SpeedId = "Speed"; + private const string TimeId = "Time"; + private const string PowerId = "Power"; + private const string DataId = "Data"; + private const string PressureId = "Pressure"; + private const string AngleId = "Angle"; + private const string AboutId = "AboutButton"; + private const string CloseId = "TogglePaneButton"; + private const string FlyoutListViewId = "MenuItemsHost"; + private const string ConverterSectionId = "Converter"; + private const string ConverterTextKey = "ConverterModeText"; + + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the navigation menu. + public NavBarPom(UIObject uiObject) : base(uiObject) + { + } + + public ListViewItem StandardMenuItem => ScrollAndGetItem(StandardId); + + public ListViewItem ScientificMenuItem => ScrollAndGetItem(ScientificId); + + public ListViewItem ProgrammerMenuItem => ScrollAndGetItem(ProgrammerId); + + public ListViewItem DateMenuItem => ScrollAndGetItem(DateId); + + public ListViewItem CurrencyMenuItem => ScrollAndGetItem(CurrencyId); + + public ListViewItem VolumeMenuItem => ScrollAndGetItem(VolumeId); + + public ListViewItem LengthMenuItem => ScrollAndGetItem(LengthId); + + public ListViewItem WeightMenuItem => ScrollAndGetItem(WeightId); + + public ListViewItem TemperatureMenuItem => ScrollAndGetItem(TemperatureId); + + public ListViewItem EnergyMenuItem => ScrollAndGetItem(EnergyId); + + public ListViewItem AreaMenuItem => ScrollAndGetItem(AreaId); + + public ListViewItem SpeedMenuItem => ScrollAndGetItem(SpeedId); + + public ListViewItem TimeMenuItem => ScrollAndGetItem(TimeId); + + public ListViewItem PowerMenuItem => ScrollAndGetItem(PowerId); + + public ListViewItem DataMenuItem => ScrollAndGetItem(DataId); + + public ListViewItem PressureMenuItem => ScrollAndGetItem(PressureId); + + public ListViewItem AngleMenuItem => ScrollAndGetItem(AngleId); + + public Button AboutButton => new Button(this.Descendants.Find(AboutId)); + + public Button CloseButton => new Button(this.Parent.Children.Find(CloseId)); + + public ListView ModeListView => new ListView(this.Descendants.Find(FlyoutListViewId)); + + private ListViewItem ScrollAndGetItem(string id) + { + ListViewItem item; + var res = this.ModeListView.AllItems.TryFind(id, out item); + + item.ScrollIntoView(); + + return item; + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorLfm.cs new file mode 100644 index 00000000..916ec0bc --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorLfm.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class ProgrammerCalculatorLfm + { + public ProgrammerCalculatorLfm(ProgrammerCalculatorPom programmerCalculatorPom) + { + this.ObjectModel = programmerCalculatorPom; + } + + public ProgrammerCalculatorPom ObjectModel { get; } + + public void EnsureFullKeypad() + { + if (!this.ObjectModel.FullKeypadButton.IsSelected) + { + this.ObjectModel.FullKeypadButton.Select(); + } + } + + public void EnsureBitTogglingKeypad() + { + if (!this.ObjectModel.BitFlipKeypadButton.IsSelected) + { + this.ObjectModel.BitFlipKeypadButton.Select(); + } + } + + public void ChangeBitLength() + { + this.ObjectModel.GetCurrentBitLengthButton().Invoke(); + } + + public MemoryLfm OpenMemory() + { + MemoryLfm lfm = new MemoryLfm(this.ObjectModel.MemoryControls); + lfm.OpenBody(); + return lfm; + } + + public void FiveMemorySet() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + this.ObjectModel.NumberPad.FiveButton.Invoke(); + waiter.TryWait(); + } + + this.ObjectModel.MemoryControls.SetButton.Invoke(); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorPom.cs new file mode 100644 index 00000000..90a07867 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/ProgrammerCalculatorPom.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class ProgrammerCalculatorPom : UIObject + { + private const string FullKeypadButtonId = "fullKeypad"; + private const string BitFlipKeypadButtonId = "bitFlip"; + private const string CalculatorResultsId = "CalculatorResults"; + private const string NumberPadId = "NumberPad"; + + private readonly string[] BitLengthButtonIds = + { + "qwordButton", + "dwordButton", + "wordButton", + "byteButton" + }; + + public ProgrammerCalculatorPom(UIObject uiObject) : base(uiObject) + { + } + + public NumberPadPom NumberPad => new NumberPadPom(this.Descendants.Find(NumberPadId)); + + public MemoryPom MemoryControls => new MemoryPom(this); + + public RadioButton FullKeypadButton => new RadioButton(this.Descendants.Find(FullKeypadButtonId)); + + public RadioButton BitFlipKeypadButton => new RadioButton(this.Descendants.Find(BitFlipKeypadButtonId)); + + public TextBlock Display => new TextBlock(this.Descendants.Find(CalculatorResultsId)); + + public UIEventWaiter GetDisplayChangedWaiter() => this.Display.GetNameChangedWaiter(); + + public Button GetCurrentBitLengthButton() + { + // There are four bit length buttons, with only one visible at a time. + UIObject button = null; + foreach (var buttonId in this.BitLengthButtonIds) + { + if (this.Descendants.TryFind(buttonId, out button)) + { + break; + } + } + + return new Button(button); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorLfm.cs new file mode 100644 index 00000000..0320ee3c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorLfm.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation.Waiters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the Standard calculator view. + /// + public class ScientificCalculatorLfm + { + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the scientific Calculator. + public ScientificCalculatorLfm(ScientificCalculatorPom objectModel) + { + this.ObjectModel = objectModel; + } + + public ScientificCalculatorPom ObjectModel { get; } + + public void Press1() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking 1"); + this.ObjectModel.OneButton.Invoke(); + waiter.TryWait(); + } + } + + public void Press2() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking 2"); + this.ObjectModel.NumberPad.TwoButton.Invoke(); + waiter.TryWait(); + } + } + + public void Press3() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking 3"); + this.ObjectModel.ThreeButton.Invoke(); + waiter.TryWait(); + } + } + + public void Press4() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking 4"); + this.ObjectModel.FourButton.Invoke(); + waiter.TryWait(); + } + } + + public void PressSqrt() + { + // When invoking sqrt, both the expression changes. + using (UIEventWaiter waiter = this.ObjectModel.GetExpressionChangedWaiter()) + { + Log.Comment("Invoking sqrt"); + this.ObjectModel.SqrtButton.Invoke(); + waiter.TryWait(); + } + } + + public void PressMinus() + { + using (UIEventWaiter waiter = this.ObjectModel.GetExpressionChangedWaiter()) + { + Log.Comment("Invoking minus"); + this.ObjectModel.MinusButton.Invoke(); + waiter.TryWait(); + } + } + + public void PressPlus() + { + using (UIEventWaiter waiter = this.ObjectModel.GetExpressionChangedWaiter()) + { + Log.Comment("Invoking plus"); + this.ObjectModel.PlusButton.Invoke(); + waiter.TryWait(); + } + } + + public void PressEquals() + { + // When invoking equals, both the display and the expression change. + using (UIEventWaiter expressionWaiter = this.ObjectModel.GetExpressionChangedWaiter()) + using (UIEventWaiter displayWaiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking equals"); + this.ObjectModel.EqualButton.Invoke(); + expressionWaiter.TryWait(); + displayWaiter.TryWait(); + } + } + + public void OnePlusTwoEnter() + { + Press1(); + PressPlus(); + Press2(); + PressEquals(); + } + + public void MemorySet() => this.ObjectModel.MemoryControls.SetButton.Invoke(); + + public MemoryLfm OpenMemory() + { + MemoryLfm lfm = new MemoryLfm(this.ObjectModel.MemoryControls); + lfm.OpenBody(); + return lfm; + } + + public HistoryLfm OpenHistory() + { + HistoryLfm lfm = new HistoryLfm(this.ObjectModel.HistoryControls); + lfm.OpenBody(); + return lfm; + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorPom.cs new file mode 100644 index 00000000..bb729613 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/ScientificCalculatorPom.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the Scientific Calculator + /// + public class ScientificCalculatorPom : CalculatorBasePom + { + private const string NumberPadId = "NumberPad"; + private const string StandardOperatorsId = "StandardOperators"; + private const string DisplayControlsId = "DisplayControls"; + private const string ScientificFunctionsId = "ScientificFunctions"; + private const string OneButtonId = "num1Button"; + private const string ThreeButtonId = "num3Button"; + private const string FourButtonId = "num4Button"; + private const string SqrtButtonId = "squareRootButton"; + private const string MinusButtonId = "minusButton"; + private const string PlusButtonId = "plusButton"; + private const string EqualButtonId = "equalButton"; + private const string ClearButtonId = "clearButton"; + + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the scientific calculator. + public ScientificCalculatorPom(UIObject uiObject) : base(uiObject) + { + } + + public UIObject StandardOperatorsGroup => this.Descendants.Find(StandardOperatorsId); + + public UIObject DisplayControlsGroup => this.Descendants.Find(DisplayControlsId); + + public UIObject ScientificFunctionsGroup => this.Descendants.Find(ScientificFunctionsId); + + public Button OneButton => this.NumberPad.OneButton; + + public Button ThreeButton => this.NumberPad.ThreeButton; + + public Button FourButton => this.NumberPad.FourButton; + + public Button SqrtButton => new Button(this.ScientificFunctionsGroup.Children.Find(SqrtButtonId)); + + public Button MinusButton => new Button(this.StandardOperatorsGroup.Children.Find(MinusButtonId)); + + public Button PlusButton => new Button(this.StandardOperatorsGroup.Children.Find(PlusButtonId)); + + public Button EqualButton => new Button(this.StandardOperatorsGroup.Children.Find(EqualButtonId)); + + public Button ClearButton => new Button(this.DisplayControlsGroup.Children.Find(ClearButtonId)); + + public NumberPadPom NumberPad => new NumberPadPom(this.Descendants.Find(NumberPadId)); + + public HistoryPom HistoryControls => new HistoryPom(this); + + public MemoryPom MemoryControls => new MemoryPom(this); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorLfm.cs new file mode 100644 index 00000000..503ece53 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorLfm.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation.Waiters; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the Standard calculator view. + /// + public class StandardCalculatorLfm + { + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the Standard Calculator. + public StandardCalculatorLfm(StandardCalculatorPom objectModel) + { + this.ObjectModel = objectModel; + } + + public StandardCalculatorPom ObjectModel { get; } + + public void OnePlusTwoEnter() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking 1"); + this.ObjectModel.NumPad.OneButton.Invoke(); + waiter.TryWait(); + } + using (UIEventWaiter waiter = this.ObjectModel.GetExpressionChangedWaiter()) + { + Log.Comment("Pressing +"); + this.ObjectModel.SendKeys("{ADD}"); + // PropertyChangeWaiter is unreliable for the first name changed notification + // Bug 17624996: PropertyChanged event not fired when Name is updated for the first time for a control with custom automation peer. + waiter.TryWait(); + } + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Pressing 2"); + this.ObjectModel.SendKeys("2"); + waiter.TryWait(); + } + // When pressing enter, both the display and the expression change. + using (UIEventWaiter expressionWaiter = this.ObjectModel.GetExpressionChangedWaiter()) + using (UIEventWaiter displayWaiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Invoking equals"); + this.ObjectModel.EqualButton.Invoke(); + expressionWaiter.TryWait(); + displayWaiter.TryWait(); + } + } + + public void Clear() + { + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + Log.Comment("Pressing escape"); + this.ObjectModel.ClearButton.Invoke(); + waiter.TryWait(); + } + } + + public void ClearFiveMemorySet() + { + this.Clear(); + using (UIEventWaiter waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + this.ObjectModel.NumPad.FiveButton.Invoke(); + waiter.TryWait(); + } + + this.ObjectModel.MemoryControls.SetButton.Invoke(); + } + + public MemoryLfm OpenMemory() + { + MemoryLfm lfm = new MemoryLfm(this.ObjectModel.MemoryControls); + lfm.OpenBody(); + return lfm; + } + + public HistoryLfm OpenHistory() + { + HistoryLfm lfm = new HistoryLfm(this.ObjectModel.HistoryControls); + lfm.OpenBody(); + return lfm; + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorPom.cs new file mode 100644 index 00000000..8e8d5c41 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/StandardCalculatorPom.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + /// + /// Represents the Standard Calculator + /// + public class StandardCalculatorPom : CalculatorBasePom + { + private const string CalculatorResultsId = "CalculatorResults"; + private const string ExpressionContainerId = "CalculatorExpression"; + private const string NumberPadId = "NumberPad"; + private const string StandardOperatorsId = "StandardOperators"; + private const string DisplayControlsId = "DisplayControls"; + private const string EqualButtonId = "equalButton"; + private const string ClearButtonId = "clearButton"; + + /// + /// Initializes a new instance of the class. + /// + /// The UIObject that is the root of the standard calculator. + public StandardCalculatorPom(UIObject uiObject) : base(uiObject) + { + } + + public NumberPadPom NumPad => new NumberPadPom(this.Descendants.Find(NumberPadId)); + + public MemoryPom MemoryControls => new MemoryPom(this); + + public HistoryPom HistoryControls => new HistoryPom(this); + + public UIObject StandardOperatorsGroup => this.Descendants.Find(StandardOperatorsId); + + public UIObject DisplayControlsGroup => this.Descendants.Find(DisplayControlsId); + + public Button EqualButton => new Button(this.StandardOperatorsGroup.Children.Find(EqualButtonId)); + + public Button ClearButton => new Button(this.DisplayControlsGroup.Children.Find(ClearButtonId)); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterLfm.cs new file mode 100644 index 00000000..9a17c743 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterLfm.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Calculator.UIAutomationLibrary.Components +{ + public class UnitConverterLfm + { + public UnitConverterLfm(UnitConverterPom unitConverterPom) + { + this.ObjectModel = unitConverterPom; + } + + public UnitConverterPom ObjectModel { get; } + + public void Eight() + { + using (var waiter = this.ObjectModel.GetDisplayChangedWaiter()) + { + this.ObjectModel.NumberPad.EightButton.Invoke(); + waiter.TryWait(); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterPom.cs new file mode 100644 index 00000000..23e6a80d --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Pages/UnitConverterPom.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class UnitConverterPom : UIObject + { + private const string DisplayId = "Value1"; + private const string NumberPadId = "numberPad"; + + public UnitConverterPom(UIObject uiObject) : base(uiObject) + { + } + + public NumberPadPom NumberPad => new NumberPadPom(this.Descendants.Find(NumberPadId)); + + public TextBlock Display => new TextBlock(this.Descendants.Find(DisplayId)); + + public PropertyChangedEventWaiter GetDisplayChangedWaiter() => this.Display.GetNameChangedWaiter(); + + public ElementAddedWaiter GetDisplayElementAddedWaiter() => new ElementAddedWaiter(this, Scope.Descendants, DisplayId); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryLfm.cs new file mode 100644 index 00000000..a186a351 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryLfm.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Etw.Managed; +using MS.Internal.Mita.Foundation; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class HistoryLfm : ICanFocusWithClicks + { + public HistoryLfm(HistoryPom historyPom) + { + this.ObjectModel = historyPom; + } + + public HistoryPom ObjectModel { get; } + + public void FocusWithClicks() + { + // On the Programming calc, the default click location can land on the first memory item, dismissing the flyout. + // Instead, click just below, in the gutter to the left of the trash can. + var body = this.ObjectModel.Body; + int height = body.BoundingRectangle.Height; + body.DoubleClick(PointerButtons.Primary, Constants.ClickMargin, height + Constants.ClickMargin); + } + + public void OpenBody() + { + using (EtwWaiter waiter = new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.HistoryBodyOpenedETWEventName)) + { + // Only one exists at a given time. + if (this.ObjectModel.IsHistoryButtonVisible) + { + if (!this.ObjectModel.IsBodyOpen) + { + this.ObjectModel.HistoryButton.Invoke(); + waiter.Wait(); + } + } + else if (!this.ObjectModel.HistoryPivot.IsSelected) + { + this.ObjectModel.HistoryPivot.Click(); + waiter.Wait(); + } + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryPom.cs new file mode 100644 index 00000000..d5d8d809 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/HistoryPom.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class HistoryPom : UIObject + { + private const string HistoryButtonId = "HistoryButton"; + private const string HistoryPivotId = "HistoryLabel"; + private const string BodyId = "HistoryListView"; + + public HistoryPom(UIObject uiObject) : base(uiObject) + { + } + + public Button HistoryButton => new Button(this.Descendants.Find(HistoryButtonId)); + + public bool IsHistoryButtonVisible => this.DoesDescendantExist(HistoryButtonId); + + public TabItem HistoryPivot => new TabItem(this.Descendants.Find(HistoryPivotId)); + + public UIObject Body => this.Descendants.Find(BodyId); + + public bool IsBodyOpen => this.DoesDescendantExist(BodyId); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/ICanFocusWithClicks.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/ICanFocusWithClicks.cs new file mode 100644 index 00000000..bd27cc16 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/ICanFocusWithClicks.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Calculator.UIAutomationLibrary.Components +{ + public interface ICanFocusWithClicks + { + /// + /// Sets focus on an object by clicking on it, without causing further action. + /// Supports AccSpot scans by raising click events. + /// + void FocusWithClicks(); + } +} + diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryLfm.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryLfm.cs new file mode 100644 index 00000000..216acfbc --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryLfm.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Etw.Managed; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Waiters; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class MemoryLfm : ICanFocusWithClicks + { + public MemoryLfm(MemoryPom memoryPom) + { + this.ObjectModel = memoryPom; + } + + public MemoryPom ObjectModel { get; } + + public void FocusWithClicks() + { + // On the Programming calc, the default click location can land on the first memory item, dismissing the flyout. + // Instead, click just below, in the gutter to the left of the trash can. + var body = this.ObjectModel.Body; + int height = body.BoundingRectangle.Height; + body.DoubleClick(PointerButtons.Primary, Constants.ClickMargin, height + Constants.ClickMargin); + } + + public void OpenBody() + { + using (EtwWaiter waiter = new EtwWaiter(Constants.CalculatorETWProviderGUID, Constants.MemoryBodyOpenedETWEventName)) + { + // Only one exists at a given time + if (this.ObjectModel.IsMemoryButtonVisible) + { + if (!this.ObjectModel.IsBodyOpen) + { + this.ObjectModel.MemoryButton.Invoke(); + waiter.Wait(); + } + } + else if (!this.ObjectModel.MemoryPivot.IsSelected) + { + this.ObjectModel.MemoryPivot.Click(); + waiter.Wait(); + } + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryPom.cs new file mode 100644 index 00000000..a235258d --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/MemoryPom.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class MemoryPom : UIObject + { + private const string ClearMemoryButtonId = "ClearMemoryButton"; + private const string RecallButtonId = "MemRecall"; + private const string PlusButtonId = "MemPlus"; + private const string MinusButtonId = "MemMinus"; + private const string SetButtonId = "memButton"; + private const string MemoryButtonId = "MemoryButton"; + private const string MemoryPivotId = "MemoryLabel"; + private const string BodyId = "MemoryListView"; + + public MemoryPom(UIObject uiObject) : base(uiObject) + { + } + + public Button ClearButton => new Button(this.Descendants.Find(ClearMemoryButtonId)); + + public Button RecallButton => new Button(this.Descendants.Find(RecallButtonId)); + + public Button PlusButton => new Button(this.Descendants.Find(PlusButtonId)); + + public Button MinusButton => new Button(this.Descendants.Find(MinusButtonId)); + + public Button SetButton => new Button(this.Descendants.Find(SetButtonId)); + + public Button MemoryButton => new Button(this.Descendants.Find(MemoryButtonId)); + + public bool IsMemoryButtonVisible => this.DoesDescendantExist(MemoryButtonId); + + public TabItem MemoryPivot => new TabItem(this.Descendants.Find(MemoryPivotId)); + + public UIObject Body => this.Descendants.Find(BodyId); + + public bool IsBodyOpen => this.DoesDescendantExist(BodyId); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Components/Shared/NumberPadPom.cs b/internal/Calculator.UIAutomationLibrary/Components/Shared/NumberPadPom.cs new file mode 100644 index 00000000..6da757f5 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Components/Shared/NumberPadPom.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; + +namespace Calculator.UIAutomationLibrary.Components +{ + public class NumberPadPom : UIObject + { + private const string OneButtonId = "num1Button"; + private const string TwoButtonId = "num2Button"; + private const string ThreeButtonId = "num3Button"; + private const string FourButtonId = "num4Button"; + private const string FiveButtonId = "num5Button"; + private const string SixButtonId = "num6Button"; + private const string SevenButtonId = "num7Button"; + private const string EightButtonId = "num8Button"; + private const string NineButtonId = "num9Button"; + private const string ZeroButtonId = "num0Button"; + private const string DecimalButtonId = "decimalSeparatorButton"; + + public NumberPadPom(UIObject uiObject) : base(uiObject) + { + } + + public Button ZeroButton => new Button(this.Children.Find(ZeroButtonId)); + + public Button OneButton => new Button(this.Children.Find(OneButtonId)); + + public Button TwoButton => new Button(this.Children.Find(TwoButtonId)); + + public Button ThreeButton => new Button(this.Children.Find(ThreeButtonId)); + + public Button FourButton => new Button(this.Children.Find(FourButtonId)); + + public Button FiveButton => new Button(this.Children.Find(FiveButtonId)); + + public Button SixButton => new Button(this.Children.Find(SixButtonId)); + + public Button SevenButton => new Button(this.Children.Find(SevenButtonId)); + + public Button EightButton => new Button(this.Children.Find(EightButtonId)); + + public Button NineButton => new Button(this.Children.Find(NineButtonId)); + + public Button DecimalButton => new Button(this.Children.Find(DecimalButtonId)); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Properties/AssemblyInfo.cs b/internal/Calculator.UIAutomationLibrary/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..89cda6fe --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Calculator.UIAutomationLibrary")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("D6913DAD-1C3B-4229-915F-8301A57970FC")] + diff --git a/internal/Calculator.UIAutomationLibrary/Tests/BasicCalculationTest.cs b/internal/Calculator.UIAutomationLibrary/Tests/BasicCalculationTest.cs new file mode 100644 index 00000000..737584eb --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Tests/BasicCalculationTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Etw.Managed; +using Calculator.UIAutomationLibrary.Components; +using WEX.TestExecution; + +namespace Calculator.UIAutomationLibrary.Tests +{ + public static class BasicCalculationTest + { + /// + /// This test uses LFMs to add and then delete an alarm. + /// + /// The LFM for the alarms app window. + public static void CalculateOnePlusTwo(this CalculatorAppLfm calculatorAppLfm) + { + var mainPage = calculatorAppLfm.MainPageLfm; + + var standardCalculator = mainPage.NavigateToStandardCalculator(); + + standardCalculator.OnePlusTwoEnter(); + + Verify.AreEqual("\u202D3\u202C", standardCalculator.ObjectModel.Display.Name, "Ensure display value is 3"); + + standardCalculator.Clear(); + + Verify.AreEqual("\u202D0\u202C", standardCalculator.ObjectModel.Display.Name, "Ensure display value is 0"); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Tests/ScientificCalculationTest.cs b/internal/Calculator.UIAutomationLibrary/Tests/ScientificCalculationTest.cs new file mode 100644 index 00000000..7a2b8e7c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Tests/ScientificCalculationTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Calculator.UIAutomationLibrary.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WEX.TestExecution; + +namespace Calculator.UIAutomationLibrary.Tests +{ + public static class ScientificCalculationTest + { + /// + /// This test uses LFMs to calculate the sqrt(4) - 2. + /// + /// The LFM for the calculator app window. + public static void CalculateSqrt4Minus2(this CalculatorAppLfm calculatorAppLfm) + { + var mainPage = calculatorAppLfm.MainPageLfm; + + var scientificCalculator = mainPage.NavigateToScientificCalculator(); + + scientificCalculator.Press4(); + scientificCalculator.PressSqrt(); + scientificCalculator.PressMinus(); + scientificCalculator.Press3(); + scientificCalculator.PressPlus(); + scientificCalculator.Press1(); + scientificCalculator.PressEquals(); + + Verify.AreEqual("\u202D0\u202C", scientificCalculator.ObjectModel.Display.Name); + } + } +} + diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/Constants.cs b/internal/Calculator.UIAutomationLibrary/Utilities/Constants.cs new file mode 100644 index 00000000..4a8957d8 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/Constants.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Calculator.UIAutomationLibrary +{ + public class Constants + { + /// + /// The path to the certificate file. + /// + public const string CertificateFileName = @"Calculator.cer"; + + /// + /// The path to the appxbundle file. + /// + public const string PackageFileName = @"Calculator.appxbundle"; + + /// + /// The path to the appxbundle file. + /// + public const string VCLibsPackageFileName = @"Microsoft.VCLibs.appx"; + + /// + /// The path to the appxbundle file. + /// + public const string WinUIPackageFileName = @"Microsoft.UI.Xaml.appx"; + + /// + /// Name of the CoreWindow. + /// + public const string AppWindowName = "Calculator"; + + /// + /// Name of the process executable. + /// + public const string ProcessName = "Calculator.exe"; + + /// + /// The package name. + /// + public const string PackageName = "Microsoft.WindowsCalculator"; + + /// + /// The package family name for the app to test. + /// + public const string PackageFamilyName = PackageName + "_8wekyb3d8bbwe"; + + /// + /// The package App User Model Id. + /// + public const string PackageAppUserModelId = PackageFamilyName + "!App"; + + /// + /// AutomationId for the top level UI element. + /// + public const string TopLevelWindowAutomationId = "CalculatorWindow"; + + /// + /// Event fired when the first page is loaded. + /// + public const string AppLaunchEndETWEventName = "AppLaunchEnd"; + + /// + /// App Provider GUID for ETW Events + /// + public static readonly Guid CalculatorETWProviderGUID = new Guid("0905CA09-610E-401E-B650-2F212980B9E0"); + + /// + /// Event fired when a calculator mode change is complete. + /// + public const string AppModeChangeEndETWEventName = "ModeChangeEnd"; + + /// + /// Event fired when the History panel is opened by flyout or by changing pivot tabs. + /// + public const string HistoryBodyOpenedETWEventName = "HistoryBodyOpened"; + + /// + /// Event fired when the Memory panel is opened by flyout or by changing pivot tabs. + /// + public const string MemoryBodyOpenedETWEventName = "MemoryBodyOpened"; + + /// + /// Event fired when the About flyout control is loaded. + /// + public const string AboutFlyoutOpenedETWEventName = "AboutFlyoutOpened"; + + /// + /// Event fired when the Nav Bar control is opened. + /// + public const string NavBarOpenedETWEventName = "NavBarOpened"; + + /// + /// Margin used to click in the gutter beneath the History and Memory lists + /// + public const int ClickMargin = 10; + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/EtwHelper.cs b/internal/Calculator.UIAutomationLibrary/Utilities/EtwHelper.cs new file mode 100644 index 00000000..d1615eed --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/EtwHelper.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OneCoreUap.Test.AppModel; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary +{ + public class EtwHelper + { + private static bool etwServiceWasInstalled = false; + + /// + /// Installs and starts the Etw.Service so that Tests may utilize Etw Waiters. + /// Wex.Services.exe is part of TAEF. + /// + public static void InstallAndStartETWService() + { + etwServiceWasInstalled = ServiceHelper.IsInstalled("Etw.Service"); + if (!etwServiceWasInstalled) + { + string wexServices = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), "Wex.Services.exe"); + if (!File.Exists(wexServices)) + { + throw new FileNotFoundException(wexServices); + } + + Log.Comment("Attempting to install Etw.Service..."); + var startInfo = new ProcessStartInfo(); + startInfo.FileName = wexServices; + startInfo.Arguments = "/install:Etw.Service"; + + var process = new Process(); + process.StartInfo = startInfo; + if (process.Start()) + { + process.WaitForExit(); + Log.Comment("Completed installation of Etw.Service"); + } + else + { + throw new Exception("ETW service was not able to be installed. Process didn't start."); + } + } + + Log.Comment("Attempting to start Etw.Service..."); + ServiceHelper.Start("Etw.Service"); + Log.Comment("Etw.Service started"); + } + + /// + /// Stops the Etw.Service. + /// + public static void StopAndRemoveETWService() + { + if (ServiceHelper.IsInstalled("Etw.Service")) + { + Log.Comment("Attempting to stop Etw.Service..."); + ServiceHelper.Stop("Etw.Service"); + Log.Comment("Etw.Service stopped"); + + // if we installed the Etw.Service as part of this test we should also remove it. + // This prevents cases where TDP is upgraded but the service tregistration is referencing the old + // location that no longer exists. + if (!etwServiceWasInstalled) + { + + string wexServices = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), "Wex.Services.exe"); + if (!File.Exists(wexServices)) + { + throw new FileNotFoundException(wexServices); + } + + Log.Comment("Attempting to remove Etw.Service..."); + var startInfo = new ProcessStartInfo(); + startInfo.FileName = wexServices; + startInfo.Arguments = "/remove:Etw.Service"; + + var process = new Process(); + process.StartInfo = startInfo; + if (process.Start()) + { + process.WaitForExit(); + Log.Comment("Completed removal of Etw.Service"); + } + else + { + throw new Exception("ETW service could not be removed. Process didn't start."); + } + } + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/Impersonator.cs b/internal/Calculator.UIAutomationLibrary/Utilities/Impersonator.cs new file mode 100644 index 00000000..1b31b385 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/Impersonator.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary +{ + public static class Impersonater + { + public static void RunAs(RunAsUser user, Action action) + { + IntPtr errorInfo; + SafeAccessTokenHandle restrictedToken; + GetRunAsUserToken getRunAsUserToken = ResolveGetRunAsUserTokenMethod(); + Marshal.ThrowExceptionForHR(getRunAsUserToken(user, out restrictedToken, out errorInfo), errorInfo); + + WindowsIdentity.RunImpersonated(restrictedToken, action); + } + + public static void RunAs(RunAsUser user, Func function) + { + IntPtr errorInfo; + SafeAccessTokenHandle restrictedToken; + GetRunAsUserToken getRunAsUserToken = ResolveGetRunAsUserTokenMethod(); + Marshal.ThrowExceptionForHR(getRunAsUserToken(user, out restrictedToken, out errorInfo), errorInfo); + + WindowsIdentity.RunImpersonated(restrictedToken, function); + } + + // From: https://microsoft.visualstudio.com/EngSys/_git/validation.taef?path=%2Fsrc%2FTAEF%2FCommon%2FPublished%2FRunAsFromSystem.h&version=GBdevelop + public enum RunAsUser + { + ElevatedUser, + User, + RestrictedUser, + LowIL, + InteractiveUser, + }; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int GetRunAsUserToken(RunAsUser user, out SafeAccessTokenHandle token, out IntPtr errorInfo); + + // GetRunAsUserToken is defined in a namespace so we have to do some tricks to use P/Invoke. + // We got the actual exported method name by running dumpbin /exports TE.Common.dll + private static GetRunAsUserToken ResolveGetRunAsUserTokenMethod() + { + IntPtr teCommonDll = IntPtr.Zero; + try + { + teCommonDll = LoadLibrary(@"TE.Common.dll"); + + IntPtr x64GetRunAsUserToken = GetProcAddress(teCommonDll, "?GetRunAsUserToken@TestExecution@WEX@@YAJW4RunAsUser@12@PEAPEAXPEAPEAUIErrorInfo@@@Z"); + if (x64GetRunAsUserToken != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(x64GetRunAsUserToken); + } + IntPtr x86GetRunAsUserToken = GetProcAddress(teCommonDll, "?GetRunAsUserToken@TestExecution@WEX@@YGJW4RunAsUser@12@PAPAXPAPAUIErrorInfo@@@Z"); + if (x86GetRunAsUserToken != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(x86GetRunAsUserToken); + } + IntPtr armGetRunAsUserToken = GetProcAddress(teCommonDll, "?GetRunAsUserToken@TestExecution@WEX@@YAJW4RunAsUser@12@PAPAXPAPAUIErrorInfo@@@Z"); + if (armGetRunAsUserToken != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(armGetRunAsUserToken); + } + IntPtr arm64GetRunAsUserToken = GetProcAddress(teCommonDll, "?GetRunAsUserToken@TestExecution@WEX@@YAJW4RunAsUser@12@PEAPEAXPEAPEAUIErrorInfo@@@Z"); + if (arm64GetRunAsUserToken != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(arm64GetRunAsUserToken); + } + + throw new Exception("Unable to find the GetRunAsUserToken function in DLL 'TE.Common.dll'"); + } + finally + { + if (teCommonDll != IntPtr.Zero) + { + FreeLibrary(teCommonDll); + } + } + } + + [DllImport("kernel32.dll")] + public static extern IntPtr LoadLibrary(string dllToLoad); + + [DllImport("kernel32.dll")] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName); + + [DllImport("kernel32.dll")] + public static extern bool FreeLibrary(IntPtr hModule); + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/InstallHelper.cs b/internal/Calculator.UIAutomationLibrary/Utilities/InstallHelper.cs new file mode 100644 index 00000000..d9760be5 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/InstallHelper.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Microsoft.OneCoreUap.Test.AppModel; +using WEX.Logging.Interop; +using Windows.Foundation; +using Windows.Management.Deployment; + +namespace Calculator.UIAutomationLibrary +{ + public class InstallHelper + { + public static void InstallCertFile(string certFilePath) + { + // Ensure cert exists. + if (!File.Exists(certFilePath)) + { + throw new FileNotFoundException(certFilePath); + } + + // For some reason, attempting to use CertificateHelper.InstallFromSignedPackage() to install + // the certificate associated with the appx package fails with the error: + // "A certificate chain could not be built to a trusted root authority." + // The reason is that the cert needs to be installed in 'StoreName.TrustedPeople', + // but DeploymentHelper.AddPackage() attempts to install it in 'StoreName.Root'. + // Therefore, the cert has been installed manually beforehand. + Log.Comment($"Starting install of certificate at {certFilePath}"); + + X509Certificate2 certificate = new X509Certificate2(certFilePath); + X509Store trustedPeopleStore = new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine); + trustedPeopleStore.Open(OpenFlags.MaxAllowed); + trustedPeopleStore.Add(certificate); + + Log.Comment($"Completed install of certificate"); + } + + /// + /// Upgrades the appx/appxbundle from the path if needed. + /// + public static void InstallPackage(string appxFilePath, params string[] dependencyAppxFilePaths) + { + // Ensure the files exist. + if (!File.Exists(appxFilePath)) + { + throw new FileNotFoundException(appxFilePath); + } + foreach (var path in dependencyAppxFilePaths.Where(p => !File.Exists(p))) + { + throw new FileNotFoundException(path); + } + + Log.Comment($"Starting install of app package at {appxFilePath}"); + + PackageManager packageManager = new PackageManager(); + var deploymentOperation = packageManager.AddPackageAsync( + new Uri(appxFilePath), + dependencyAppxFilePaths.Select(d => new Uri(d)), + DeploymentOptions.ForceApplicationShutdown | DeploymentOptions.ForceTargetApplicationShutdown | DeploymentOptions.ForceUpdateFromAnyVersion); + + WaitForDeploymentOperation(deploymentOperation); + + Log.Comment("Completed install of app package"); + } + + /// + /// Waits for a deployment operation to complete + /// + private static void WaitForDeploymentOperation(IAsyncOperationWithProgress operation) + { + ManualResetEvent isCompletedEvent = new ManualResetEvent(false); + operation.Completed = (IAsyncOperationWithProgress asyncInfo, AsyncStatus asyncStatus) => + { + isCompletedEvent.Set(); + }; + + isCompletedEvent.WaitOne(); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/NativeMethods.cs b/internal/Calculator.UIAutomationLibrary/Utilities/NativeMethods.cs new file mode 100644 index 00000000..39d50bbe --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/NativeMethods.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Calculator.UIAutomationLibrary +{ + internal static class NativeMethods + { + internal const int GW_OWNER = 4; + + internal delegate bool EnumThreadWindowsCallback(IntPtr hWnd, IntPtr lParam); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern bool EnumWindows(EnumThreadWindowsCallback callback, IntPtr extraData); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern int GetWindowThreadProcessId(IntPtr handle, out int processId); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern IntPtr GetWindow(IntPtr hWnd, int uCmd); + + [DllImport("api-ms-win-ntuser-ie-window-l1-1-0.dll", SetLastError = true)] + internal static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("api-ms-win-service-management-l1-1-0.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool StartService(IntPtr hService, int dwNumServiceArgs, string[] lpServiceArgVectors); + + [DllImport("api-ms-win-service-management-l1-1-0.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool ControlService(IntPtr hService, SERVICE_CONTROL dwControl, ref SERVICE_STATUS lpServiceStatus); + + [DllImport("api-ms-win-service-management-l1-1-0.dll")] + internal static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess); + + [DllImport("api-ms-win-service-management-l1-1-0.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CloseServiceHandle(IntPtr hSCObject); + + [DllImport("api-ms-win-service-management-l1-1-0.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess); + + [DllImport("api-ms-win-service-management-l1-1-0.dll", SetLastError = true)] + internal static extern bool QueryServiceStatus(IntPtr hService, ref SERVICE_STATUS dwServiceStatus); + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct SERVICE_STATUS + { + public int dwServiceType; + public int dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + } + + internal static uint STANDARD_RIGHTS_REQUIRED = 0xF0000; + + internal static uint SC_MANAGER_CONNECT = 0x0001; + internal static uint SC_MANAGER_CREATE_SERVICE = 0x0002; + internal static uint SC_MANAGER_ENUMERATE_SERVICE = 0x0004; + internal static uint SC_MANAGER_LOCK = 0x0008; + internal static uint SC_MANAGER_QUERY_LOCK_STATUS = 0x0010; + internal static uint SC_MANAGER_MODIFY_BOOT_CONFIG = 0x0020; + internal static uint SC_MANAGER_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | + SC_MANAGER_CONNECT | + SC_MANAGER_CREATE_SERVICE | + SC_MANAGER_ENUMERATE_SERVICE | + SC_MANAGER_LOCK | + SC_MANAGER_QUERY_LOCK_STATUS | + SC_MANAGER_MODIFY_BOOT_CONFIG); + + internal static uint SERVICE_QUERY_CONFIG = 0x0001; + internal static uint SERVICE_CHANGE_CONFIG = 0x0002; + internal static uint SERVICE_QUERY_STATUS = 0x0004; + internal static uint SERVICE_ENUMERATE_DEPENDENTS = 0x0008; + internal static uint SERVICE_START = 0x0010; + internal static uint SERVICE_STOP = 0x0020; + internal static uint SERVICE_PAUSE_CONTINUE = 0x0040; + internal static uint SERVICE_INTERROGATE = 0x0080; + internal static uint SERVICE_USER_DEFINED_CONTROL = 0x0100; + internal static uint SERVICE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | + SERVICE_QUERY_CONFIG | + SERVICE_CHANGE_CONFIG | + SERVICE_QUERY_STATUS | + SERVICE_ENUMERATE_DEPENDENTS | + SERVICE_START | + SERVICE_STOP | + SERVICE_PAUSE_CONTINUE | + SERVICE_INTERROGATE | + SERVICE_USER_DEFINED_CONTROL); + + [Flags] + internal enum SERVICE_CONTROL : uint + { + STOP = 0x00000001, + PAUSE = 0x00000002, + CONTINUE = 0x00000003, + INTERROGATE = 0x00000004, + SHUTDOWN = 0x00000005, + PARAMCHANGE = 0x00000006, + NETBINDADD = 0x00000007, + NETBINDREMOVE = 0x00000008, + NETBINDENABLE = 0x00000009, + NETBINDDISABLE = 0x0000000A, + DEVICEEVENT = 0x0000000B, + HARDWAREPROFILECHANGE = 0x0000000C, + POWEREVENT = 0x0000000D, + SESSIONCHANGE = 0x0000000E + } + + internal enum SERVICE_STATE : int + { + SERVICE_STOPPED = 0x00000001, + SERVICE_START_PENDING = 0x00000002, + SERVICE_STOP_PENDING = 0x00000003, + SERVICE_RUNNING = 0x00000004, + SERVICE_CONTINUE_PENDING = 0x00000005, + SERVICE_PAUSE_PENDING = 0x00000006, + SERVICE_PAUSED = 0x00000007 + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/PerfTestConstants.cs b/internal/Calculator.UIAutomationLibrary/Utilities/PerfTestConstants.cs new file mode 100644 index 00000000..ae329b7c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/PerfTestConstants.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Calculator.UIAutomationLibrary +{ + public class PerfConstants + { + /// + /// Path where the regions, wprprofiles and wpaprofiles will be deployed to by the spkg. + /// + public const string ConfigDirectory = @"Config\"; + + /// + /// Our FunGates source, where we can view test results. + /// + public const string FunGatesSource = +#if DEBUG + "TestSite"; +#else + "Utility Apps Performance Tests"; +#endif + + /// + /// The Windows Performance Recorder profile. These strings must have the config directory prefix. + /// For use with the WPRProfileFile test attribute. + /// + public const string AppLifecycleWPRProfile = ConfigDirectory + "AppLifecycle.Profile.wprp"; + + /// + /// The regions of interest file that contains the events we are interested in measuring. + /// + public const string AppLifecycleRegions = ConfigDirectory + "AppLifecycle.regions.xml"; + + /// + /// These are uses with the DataSource test property to specify iteration info. + /// + public const string AppLifecycleInterationsSource = "Table:" + ConfigDirectory + "AppLifecycle.Iterations.xml#PerformanceConfigurations"; + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/ServiceHelper.cs b/internal/Calculator.UIAutomationLibrary/Utilities/ServiceHelper.cs new file mode 100644 index 00000000..4cae7728 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/ServiceHelper.cs @@ -0,0 +1,156 @@ +// 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.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Calculator.UIAutomationLibrary +{ + public class ServiceHelper + { + public static void Start(string serviceName, int timeoutInMilliSeconds = 30000) + { + IntPtr hService = IntPtr.Zero; + IntPtr hSCManager = IntPtr.Zero; + try + { + hSCManager = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_ALL_ACCESS); + if (IntPtr.Zero == hSCManager) + { + throw new Exception($"Start: Cannot Open OpenSCManager, {Marshal.GetLastWin32Error()}"); + } + + hService = NativeMethods.OpenService(hSCManager, serviceName, NativeMethods.SERVICE_ALL_ACCESS); + if (IntPtr.Zero == hService) + { + throw new Exception($"Start: Cannot Open Service, {Marshal.GetLastWin32Error()}"); + } + + NativeMethods.SERVICE_STATUS serviceStatus = new NativeMethods.SERVICE_STATUS(); + if (!NativeMethods.QueryServiceStatus(hService, ref serviceStatus)) + { + throw new Exception($"Start: Unable to query status of Service, {Marshal.GetLastWin32Error()}"); + } + + if (serviceStatus.dwCurrentState != (int)NativeMethods.SERVICE_STATE.SERVICE_RUNNING && + serviceStatus.dwCurrentState != (int)NativeMethods.SERVICE_STATE.SERVICE_START_PENDING) + { + if (!NativeMethods.StartService(hService, 0, null)) + { + throw new Exception($"Start: Service cannot be started, {Marshal.GetLastWin32Error()}"); + } + } + + WaitForStatus(hService, NativeMethods.SERVICE_STATE.SERVICE_RUNNING, TimeSpan.FromMilliseconds(timeoutInMilliSeconds)); + } + finally + { + if (IntPtr.Zero != hService) + { + NativeMethods.CloseServiceHandle(hService); + } + if (IntPtr.Zero != hSCManager) + { + NativeMethods.CloseServiceHandle(hSCManager); + } + } + } + + public static void Stop(string serviceName, int timeoutInMilliSeconds = 30000) + { + IntPtr hSCManager = IntPtr.Zero; + IntPtr hService = IntPtr.Zero; + try + { + hSCManager = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_ALL_ACCESS); + if (IntPtr.Zero == hSCManager) + { + throw new Exception($"Stop: Cannot Open OpenSCManager, {Marshal.GetLastWin32Error()}"); + } + + hService = NativeMethods.OpenService(hSCManager, serviceName, NativeMethods.SERVICE_ALL_ACCESS); + if (IntPtr.Zero == hService) + { + throw new Exception($"Stop: Cannot Open Service, {Marshal.GetLastWin32Error()}"); + } + + NativeMethods.SERVICE_STATUS serviceStatus = new NativeMethods.SERVICE_STATUS(); + if (!NativeMethods.QueryServiceStatus(hService, ref serviceStatus)) + { + throw new Exception($"Stop: Unable to query status of Service, {Marshal.GetLastWin32Error()}"); + } + + if (serviceStatus.dwCurrentState != (int)NativeMethods.SERVICE_STATE.SERVICE_STOPPED && + serviceStatus.dwCurrentState != (int)NativeMethods.SERVICE_STATE.SERVICE_STOP_PENDING) + { + if (!NativeMethods.ControlService(hService, NativeMethods.SERVICE_CONTROL.STOP, ref serviceStatus)) + { + throw new Exception($"Stop: Service cannot be stopped, {Marshal.GetLastWin32Error()}"); + } + } + + WaitForStatus(hService, NativeMethods.SERVICE_STATE.SERVICE_STOPPED, TimeSpan.FromMilliseconds(timeoutInMilliSeconds)); + } + finally + { + if (IntPtr.Zero != hService) + { + NativeMethods.CloseServiceHandle(hService); + } + + if (IntPtr.Zero != hSCManager) + { + NativeMethods.CloseServiceHandle(hSCManager); + } + } + } + + public static bool IsInstalled(string svcName) + { + IntPtr sc_handle = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_ALL_ACCESS); + if (sc_handle == IntPtr.Zero) + { + throw new Exception($"IsInstalled: Cannot open service manager, {Marshal.GetLastWin32Error()}"); + } + + bool bResult = false; + IntPtr sv_handle = NativeMethods.OpenService(sc_handle, svcName, NativeMethods.SERVICE_QUERY_CONFIG); + if (sv_handle.ToInt64() != 0) + { + bResult = true; + NativeMethods.CloseServiceHandle(sv_handle); + } + NativeMethods.CloseServiceHandle(sc_handle); + return bResult; + } + + private static void WaitForStatus(IntPtr hService, NativeMethods.SERVICE_STATE desiredStatus, TimeSpan timeout) + { + Stopwatch swLoop = new Stopwatch(); + swLoop.Start(); + + NativeMethods.SERVICE_STATUS serviceStatus = new NativeMethods.SERVICE_STATUS(); + + do + { + Thread.Sleep(500); + if (!NativeMethods.QueryServiceStatus(hService, ref serviceStatus)) + { + throw new Exception($"WaitForStatus: Unable to query status of service, {Marshal.GetLastWin32Error()}"); + } + } + while (serviceStatus.dwCurrentState != (int)desiredStatus && (swLoop.ElapsedMilliseconds <= timeout.TotalMilliseconds)); + + if (serviceStatus.dwCurrentState != (int)desiredStatus) + { + throw new Exception($"WaitForStatus: Service failed to reach desired state: {desiredStatus}, current state: {serviceStatus.dwCurrentState}"); + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/UIObjectExtensions.cs b/internal/Calculator.UIAutomationLibrary/Utilities/UIObjectExtensions.cs new file mode 100644 index 00000000..c77f4d64 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/UIObjectExtensions.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Windows.Automation; +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary +{ + public static class UIObjectExtensions + { + private const string NamePropertyName = "Name"; + private static UICondition CoreWindowCondition = UICondition.CreateFromClassName("Windows.UI.Core.CoreWindow"); + + public static Window GetParentCoreWindow(this UIObject uiObject) + { + if (uiObject.Matches(CoreWindowCondition)) + { + return new Window(uiObject); + } + + return new Window(uiObject.Ancestors.Find(CoreWindowCondition)); + } + + public static Window GetTopLevelWindow(this Window window) + { + var node = window; + while (node != null && node.Parent != null && node.Parent.ControlType == ControlType.Window) + { + node = new Window(node.Parent); + } + return node; + } + + public static void VerifyParentTreeStructure(this UIObject uiObject) + { + var node = uiObject; + while (node != null && node.Parent != null) + { + if (!node.Parent.Children.Contains(node)) + { + Log.Comment($"- [VerifyingTree] {node} specifies {node.Parent} as parent but is not part of its children."); + } + if (!node.Parent.Descendants.Contains(node)) + { + Log.Comment($"- [VerifyingTree] {node} specifies {node.Parent} as parent but is not part of its descendants."); + } + node = node.Parent; + } + } + + public static bool DoesDescendantExist(this UIObject uiObject, string automationId) + { + UIObject temp; + return uiObject.Descendants.TryFind(automationId, out temp); + } + + public static PropertyChangedEventWaiter GetNameChangedWaiter(this TextBlock textBlock) + { + return new PropertyChangedEventWaiter(textBlock, UIProperty.Get(NamePropertyName)); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/Utilities.cs b/internal/Calculator.UIAutomationLibrary/Utilities/Utilities.cs new file mode 100644 index 00000000..c8dd58d3 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/Utilities.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary +{ + public class Utilities + { + public static void KillExistingCalculatorProcesses() + { + Log.Comment("Killing any existing Calculator processes"); + + foreach (var process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(Constants.ProcessName))) + { + try + { + process.Kill(); + Log.Comment($"Killed {process.ProcessName}, Id: {process.Id}"); + } + catch (Exception) when (process.HasExited) + { + Log.Comment($"{process.ProcessName}, Id: {process.Id} already exited."); + } + } + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/Utilities/WindowHelper.cs b/internal/Calculator.UIAutomationLibrary/Utilities/WindowHelper.cs new file mode 100644 index 00000000..bf19700c --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/Utilities/WindowHelper.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation.Controls; +using WEX.Logging.Interop; + +namespace Calculator.UIAutomationLibrary +{ + public class WindowHelper + { + public static void SetAsForeground(Window window) + { + Log.Comment($"Set window {window.NativeWindowHandle} as the foreground window."); + + NativeMethods.SetForegroundWindow(window.NativeWindowHandle); + } + } +} diff --git a/internal/Calculator.UIAutomationLibrary/project.json b/internal/Calculator.UIAutomationLibrary/project.json new file mode 100644 index 00000000..71f67e19 --- /dev/null +++ b/internal/Calculator.UIAutomationLibrary/project.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "AppModel.TestHelper": "2018.3.22", + "EtwProcessor.Managed": "10.34.181220007", + "MITALite": "1.0.180128001", + "Taef.Managed": "10.34.181220007", + "Test.Net.Redist": "2.0.1" + }, + "frameworks": { + "netcore50": {} + } +} \ No newline at end of file diff --git a/internal/Calculator.UITests/Calculator.UITests.csproj b/internal/Calculator.UITests/Calculator.UITests.csproj new file mode 100644 index 00000000..70da6c2f --- /dev/null +++ b/internal/Calculator.UITests/Calculator.UITests.csproj @@ -0,0 +1,145 @@ + + + + + Debug + x86 + {0224A709-0C48-4C4F-BA17-843A49842C15} + Library + Properties + Calculator.UITests + Calculator.UITests + .NETPortable + v5.0 + .NETCore,Version=v5.0 + UAP + 10.0.17763.0 + 10.0.17134.0 + 512 + true + + + true + full + false + bin\Debug\x86 + DEBUG;TRACE + prompt + 4 + x86 + + + pdbonly + true + bin\Release\x86 + TRACE + prompt + 4 + x86 + + + true + full + false + bin\Debug\x64 + DEBUG;TRACE + prompt + 4 + x64 + + + pdbonly + true + bin\Release\x64 + TRACE + prompt + 4 + x64 + + + true + full + false + bin\Debug\arm + DEBUG;TRACE + prompt + 4 + ARM + + + pdbonly + true + bin\Release\arm + TRACE + prompt + 4 + ARM + + + true + full + false + bin\Debug\ARM64\ + DEBUG;TRACE + prompt + 4 + ARM64 + + + pdbonly + true + bin\Release\ARM64\ + TRACE + prompt + 4 + ARM64 + + + + + + + + + + + PreserveNewest + + + TextTemplatingFileGenerator + AppLifecycle.Regions.xml + Designer + + + AppLifecycle.Regions.tt + True + PreserveNewest + True + + + + + + + {a43517b5-8be3-4312-913f-004978c34444} + Calculator.UIAutomationLibrary + + + + + Designer + PreserveNewest + + + + 15.0 + + + + \ No newline at end of file diff --git a/internal/Calculator.UITests/Config/AppLifecycle.Iterations.xml b/internal/Calculator.UITests/Config/AppLifecycle.Iterations.xml new file mode 100644 index 00000000..91a96bc8 --- /dev/null +++ b/internal/Calculator.UITests/Config/AppLifecycle.Iterations.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + +
+
diff --git a/internal/Calculator.UITests/Config/AppLifecycle.Profile.wprp b/internal/Calculator.UITests/Config/AppLifecycle.Profile.wprp new file mode 100644 index 00000000..651b7c65 --- /dev/null +++ b/internal/Calculator.UITests/Config/AppLifecycle.Profile.wprp @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/internal/Calculator.UITests/Config/AppLifecycle.Regions.tt b/internal/Calculator.UITests/Config/AppLifecycle.Regions.tt new file mode 100644 index 00000000..acd8ead6 --- /dev/null +++ b/internal/Calculator.UITests/Config/AppLifecycle.Regions.tt @@ -0,0 +1,364 @@ +<# // Copyright (c) Microsoft Corporation. All rights reserved. #> +<#@ template language="C#" hostspecific="True" #> +<#@ output extension=".xml" #> +<# // Set options specific to your app here +var options = new { + RegionPrefix = "Calculator", + ImageFileName = "Calculator.exe", + PackageFamilyName = "Microsoft.WindowsCalculator", + FirstView = "ms-resource:///Files/Views/MainPage.xbf" +}; +#> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + + + + + + + + + + + + CPU + <#= options.ImageFileName #> + Commit + <#= options.ImageFileName #> + + + + + + + diff --git a/internal/Calculator.UITests/Config/AppLifecycle.Regions.xml b/internal/Calculator.UITests/Config/AppLifecycle.Regions.xml new file mode 100644 index 00000000..e9ee8aed --- /dev/null +++ b/internal/Calculator.UITests/Config/AppLifecycle.Regions.xml @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + + + + + + + + + + + + CPU + Calculator.exe + Commit + Calculator.exe + + + + + + + diff --git a/internal/Calculator.UITests/Initialization.cs b/internal/Calculator.UITests/Initialization.cs new file mode 100644 index 00000000..bb254fbd --- /dev/null +++ b/internal/Calculator.UITests/Initialization.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Calculator.UIAutomationLibrary; +using Microsoft.OneCoreUap.Test.AppModel; +using WEX.TestExecution; +using WEX.TestExecution.Markup; + +namespace Calculator.UITests +{ + [TestClass] + public class Initialization + { + [DllImport("AppModel.TestHelper.dll")] + private static extern Int32 WinRTHelper_Register(); + + [AssemblyInitialize] + [TestProperty("CoreClrProfile", "TestNetv2.0")] + [TestProperty("RunFixtureAs:Assembly", "System")] + public static void AssemblySetup(TestContext context) + { + Verify.AreEqual(0, WinRTHelper_Register()); + TestHelper.Initialize(); + + // Install and Start the Etw.Service service to enable the use of EtwWaiter. + EtwHelper.InstallAndStartETWService(); + + bool installApp = false; + if (context.Properties.Contains("InstallApp") && (bool.TryParse(context.Properties["InstallApp"].ToString(), out installApp)) && installApp) + { + string certToDeploy = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), Constants.CertificateFileName); + InstallHelper.InstallCertFile(certToDeploy); + + string vcLibsToDeploy = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), Constants.VCLibsPackageFileName); + string winUIToDeploy = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), Constants.WinUIPackageFileName); + string appxToDeploy = Path.Combine(TAEFHelper.GetTestDeploymentDirectory(), Constants.PackageFileName); + Impersonater.RunAs(Impersonater.RunAsUser.RestrictedUser, () => InstallHelper.InstallPackage(appxToDeploy, vcLibsToDeploy, winUIToDeploy)); + } + } + + [AssemblyCleanup] + [TestProperty("RunFixtureAs:Assembly", "System")] + public static void AssemblyCleanup() + { + // Stop and remove the Etw.Service service. + EtwHelper.StopAndRemoveETWService(); + + TestHelper.Uninitialize(); + } + } +} diff --git a/internal/Calculator.UITests/Properties/AssemblyInfo.cs b/internal/Calculator.UITests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..c6441765 --- /dev/null +++ b/internal/Calculator.UITests/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UIAutomationTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] + +[assembly: AssemblyCulture("")] + + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f5e8f9b8-f7f1-4300-a5cb-35bfab83f51e")] +// The following was autogenerated by UpdateVersion.ps1 +[assembly: AssemblyProduct(@"10.16.1128.1252")] diff --git a/internal/Calculator.UITests/Tests/AppLifecycleTests.cs b/internal/Calculator.UITests/Tests/AppLifecycleTests.cs new file mode 100644 index 00000000..e2990043 --- /dev/null +++ b/internal/Calculator.UITests/Tests/AppLifecycleTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Etw.Managed; +using Microsoft.Windows.Apps.Performance; +using WEX.Logging.Interop; +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using Calculator.UIAutomationLibrary; + +namespace Calculator.PerfTests +{ + [TestClass] + public class AppLifecycleTests + { + [TestInitialize] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodSetup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + [TestCleanup] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodCleanup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + /// + /// This method executes the AppLifecycle performance test. + /// The test launches the application, suspend and resumes it and then terminates the app. + /// + /// A TAEF data source is used to specify the Windows Performance Recorder profile and + /// regions of interest file to use and defines threee different configurations to execute this test with. + /// Cold: The initial run to get dlls loaded into memory and to execute any first-run app logic. + /// Warm: The run that will produce consistent results and that would be used for measures. + /// Memory: A run that is focused on collecting more info on allocations. + /// + [TestMethod] + [TestProperty("RunAs", "User")] + [TestProperty("Category", "Performance")] + [TestProperty(WinperfConstants.DataSource, PerfConstants.AppLifecycleInterationsSource)] + public void AppLifecycleTest() + { + AppLifecycle.Run(Constants.PackageAppUserModelId); + } + } +} diff --git a/internal/Calculator.UITests/Tests/CalculatorTests.cs b/internal/Calculator.UITests/Tests/CalculatorTests.cs new file mode 100644 index 00000000..718830e2 --- /dev/null +++ b/internal/Calculator.UITests/Tests/CalculatorTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Calculator.UIAutomationLibrary; +using Calculator.UIAutomationLibrary.Tests; +using WEX.TestExecution.Markup; + +namespace Calculator.UITests +{ + [TestClass] + public class CalculatorTests + { + [TestInitialize] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodSetup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + [TestCleanup] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodCleanup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + [TestMethod] + [TestProperty("RunAs", "User")] + public void OnePlusTwoTest() + { + var calculatorLfm = CalculatorAppLauncher.Launch(); + + calculatorLfm.CalculateOnePlusTwo(); + + calculatorLfm.Close(); + } + + [TestMethod] + [TestProperty("RunAs", "User")] + public void Sqrt4Minus2Test() + { + var calculatorLfm = CalculatorAppLauncher.Launch(); + + calculatorLfm.CalculateSqrt4Minus2(); + + calculatorLfm.Close(); + } + } +} diff --git a/internal/Calculator.UITests/Tests/LaunchTests.cs b/internal/Calculator.UITests/Tests/LaunchTests.cs new file mode 100644 index 00000000..9af376a5 --- /dev/null +++ b/internal/Calculator.UITests/Tests/LaunchTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using MS.Internal.Mita.Foundation; +using MS.Internal.Mita.Foundation.Controls; +using MS.Internal.Mita.Foundation.Waiters; +using System; +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using System.Runtime.InteropServices; +using Microsoft.OneCoreUap.Test.AppModel; +using System.IO; +using Calculator.UITests; +using Calculator.UIAutomationLibrary; + +namespace Calculator.UITests +{ + [TestClass] + public class LaunchTests + { + [TestInitialize] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodSetup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + [TestCleanup] + [TestProperty("RunAs", "ElevatedUserOrSystem")] + public void MethodCleanup() + { + Utilities.KillExistingCalculatorProcesses(); + } + + [TestMethod] + [TestProperty("RunAs", "User")] + public void NormalLaunchTest() + { + var calculatorLfm = CalculatorAppLauncher.Launch(); + calculatorLfm.Close(); + } + } +} diff --git a/internal/Calculator.UITests/project.json b/internal/Calculator.UITests/project.json new file mode 100644 index 00000000..790fc2a5 --- /dev/null +++ b/internal/Calculator.UITests/project.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "AppModel.TestHelper": "2018.3.22", + "EtwProcessor.Managed": "10.34.181220007", + "Microsoft.TestInfrastructure.UniversalTest": "1.0.20181107.1", + "Microsoft.Windows.Apps.Performance": "1.0.7", + "MITALite": "1.0.180128001", + "Taef.Managed": "10.34.181220007", + "Test.Net.Redist": "2.0.1" + }, + "frameworks": { + "netcore50": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-x64": {}, + "win10-x86": {} + } +} \ No newline at end of file diff --git a/internal/Calculator.UITests/testmd.definition b/internal/Calculator.UITests/testmd.definition new file mode 100644 index 00000000..c65b246e --- /dev/null +++ b/internal/Calculator.UITests/testmd.definition @@ -0,0 +1,73 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-4.json", + "Package": { + "ComponentName": "Calculator", + "SubComponentName": "UITests" + }, + "SupportedArchitectures": [ "All" ], + "Execution": { + "Type": "TAEF", + "Parameter": "/ScreenCaptureOnError /TestMode:EnsureLoggedOnUser", + "ExecutionTimeoutInMinutes": "30" + }, + "Dependencies": { + "Files": [ + { + "SourcePath": "$(OUT_DIR)Calculator.UIAutomationLibrary.dll", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + }, + { + "SourcePath": "$(OUT_DIR)Microsoft.Windows.Apps.Performance.dll", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + }, + { + "SourcePath": "$(OUT_DIR)Config\\AppLifecycle.Profile.wprp", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)\\Config\\" + }, + { + "SourcePath": "$(OUT_DIR)Config\\AppLifecycle.Regions.xml", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)\\Config\\" + }, + { + "SourcePath": "$(OUT_DIR)Config\\AppLifecycle.Iterations.xml", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)\\Config\\" + } + ], + "Packages": [ + "Microsoft-Windows-Test-Taef", + "Microsoft-Windows-Test-EtwProcessor", + "Microsoft-Test-Taef-EnsureLoggedOnUserTestMode", + "Microsoft-Test-Taef-EtwLoggerTestMode", + "Microsoft-Windows-Test-MitaLite", + "Microsoft-Windows-Test-TestNetV2.0", + "Microsoft-OneCoreUap-Test-AppModel-AreaLibrary" + ] + }, + "Logs": [], + "Plugins": [], + "Profiles": [ + { + "Name": "All", + "Execution": { + "AdditionalParameter": "/TestMode:EtwLogger /p:InstallApp=true /select:not(@Category='Performance')" + }, + "Dependencies": { + "AdditionalPackages": [ + "Microsoft-Calculator-App" + ] + } + }, + { + "Name": "Performance", + "Execution": { + "AdditionalParameter": "/TestMode:WinPerf /WinPerf:Upload=Full /winperf:WinPerfSource=\"Utility Apps Performance Tests\" /winperf:VersionProcess=Calculator.exe /winperf:VersionImage=Calculator.exe /p:InstallApp=true /select:@Category='Performance'" + }, + "Dependencies": { + "AdditionalPackages": [ + "Microsoft-Windows-Performance-Winperf-Winperf", + "Microsoft-Calculator-App" + ] + } + } + ] +} diff --git a/internal/CalculatorInternal.sln b/internal/CalculatorInternal.sln new file mode 100644 index 00000000..a6ee5b14 --- /dev/null +++ b/internal/CalculatorInternal.sln @@ -0,0 +1,165 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calculator.UIAutomationLibrary", "Calculator.UIAutomationLibrary\Calculator.UIAutomationLibrary.csproj", "{A43517B5-8BE3-4312-913F-004978C34444}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calculator.UITests", "Calculator.UITests\Calculator.UITests.csproj", "{0224A709-0C48-4C4F-BA17-843A49842C15}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E73C8A6E-BB94-4258-ACED-7C837A6A587B}" + ProjectSection(SolutionItems) = preProject + nuget.config = nuget.config + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Calculator", "..\src\Calculator\Calculator.vcxproj", "{9447424A-0E05-4911-BEB8-E0354405F39A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalcViewModel", "..\src\CalcViewModel\CalcViewModel.vcxproj", "{90E9761D-9262-4773-942D-CAEAE75D7140}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalcManager", "..\src\CalcManager\CalcManager.vcxproj", "{311E866D-8B93-4609-A691-265941FEE101}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calculator.TestPackage", "Calculator.TestPackage\Calculator.TestPackage.csproj", "{24767C43-CD5A-4DC9-8D6B-429F255524E5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalculatorUnitTests", "CalculatorUnitTests\CalculatorUnitTests.vcxproj", "{E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|ARM.ActiveCfg = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|ARM.Build.0 = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|x64.ActiveCfg = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|x64.Build.0 = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|x86.ActiveCfg = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Debug|x86.Build.0 = Debug|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|ARM.ActiveCfg = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|ARM.Build.0 = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|ARM64.ActiveCfg = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|x64.ActiveCfg = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|x64.Build.0 = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|x86.ActiveCfg = Release|Any CPU + {A43517B5-8BE3-4312-913F-004978C34444}.Release|x86.Build.0 = Release|Any CPU + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|ARM.ActiveCfg = Debug|ARM + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|ARM.Build.0 = Debug|ARM + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|x64.ActiveCfg = Debug|x64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|x64.Build.0 = Debug|x64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|x86.ActiveCfg = Debug|x86 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Debug|x86.Build.0 = Debug|x86 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|ARM.ActiveCfg = Release|ARM + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|ARM.Build.0 = Release|ARM + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|ARM64.ActiveCfg = Release|ARM64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|x64.ActiveCfg = Release|x64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|x64.Build.0 = Release|x64 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|x86.ActiveCfg = Release|x86 + {0224A709-0C48-4C4F-BA17-843A49842C15}.Release|x86.Build.0 = Release|x86 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.ActiveCfg = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Build.0 = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Deploy.0 = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.Build.0 = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.ActiveCfg = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.Build.0 = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.Deploy.0 = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.ActiveCfg = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.Build.0 = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.Deploy.0 = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.ActiveCfg = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.Build.0 = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.Deploy.0 = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.ActiveCfg = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.Build.0 = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.Deploy.0 = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.ActiveCfg = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.Build.0 = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.Deploy.0 = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.ActiveCfg = Release|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Build.0 = Release|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Deploy.0 = Release|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.ActiveCfg = Debug|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.Build.0 = Debug|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM64.Build.0 = Debug|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x64.ActiveCfg = Debug|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x64.Build.0 = Debug|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x86.ActiveCfg = Debug|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x86.Build.0 = Debug|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM.ActiveCfg = Release|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM.Build.0 = Release|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM64.ActiveCfg = Release|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM64.Build.0 = Release|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x64.ActiveCfg = Release|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x64.Build.0 = Release|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x86.ActiveCfg = Release|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x86.Build.0 = Release|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.ActiveCfg = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.Build.0 = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.Build.0 = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.ActiveCfg = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.Build.0 = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.ActiveCfg = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.Build.0 = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.ActiveCfg = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.Build.0 = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.ActiveCfg = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.Build.0 = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.ActiveCfg = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.Build.0 = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.ActiveCfg = Release|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.Build.0 = Release|Win32 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|ARM.ActiveCfg = Debug|ARM + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|ARM.Build.0 = Debug|ARM + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|x64.ActiveCfg = Debug|x64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|x64.Build.0 = Debug|x64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|x86.ActiveCfg = Debug|x86 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Debug|x86.Build.0 = Debug|x86 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|ARM.ActiveCfg = Release|ARM + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|ARM.Build.0 = Release|ARM + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|ARM64.ActiveCfg = Release|ARM64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|x64.ActiveCfg = Release|x64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|x64.Build.0 = Release|x64 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|x86.ActiveCfg = Release|x86 + {24767C43-CD5A-4DC9-8D6B-429F255524E5}.Release|x86.Build.0 = Release|x86 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM.ActiveCfg = Debug|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM.Build.0 = Debug|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM.Deploy.0 = Debug|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM64.Build.0 = Debug|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x64.ActiveCfg = Debug|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x64.Build.0 = Debug|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x64.Deploy.0 = Debug|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x86.ActiveCfg = Debug|Win32 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x86.Build.0 = Debug|Win32 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Debug|x86.Deploy.0 = Debug|Win32 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM.ActiveCfg = Release|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM.Build.0 = Release|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM.Deploy.0 = Release|ARM + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM64.ActiveCfg = Release|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM64.Build.0 = Release|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|ARM64.Deploy.0 = Release|ARM64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x64.ActiveCfg = Release|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x64.Build.0 = Release|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x64.Deploy.0 = Release|x64 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x86.ActiveCfg = Release|Win32 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x86.Build.0 = Release|Win32 + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {97D9888A-CF9F-4147-8CCE-71093C057452} + EndGlobalSection +EndGlobal diff --git a/internal/CalculatorUnitTests/Assets/LockScreenLogo.scale-200.png b/internal/CalculatorUnitTests/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..735f57ad Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/LockScreenLogo.scale-200.png differ diff --git a/internal/CalculatorUnitTests/Assets/SplashScreen.scale-200.png b/internal/CalculatorUnitTests/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000..023e7f1f Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/SplashScreen.scale-200.png differ diff --git a/internal/CalculatorUnitTests/Assets/Square150x150Logo.scale-200.png b/internal/CalculatorUnitTests/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..af49fec1 Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/Square150x150Logo.scale-200.png differ diff --git a/internal/CalculatorUnitTests/Assets/Square44x44Logo.scale-200.png b/internal/CalculatorUnitTests/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..ce342a2e Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/Square44x44Logo.scale-200.png differ diff --git a/internal/CalculatorUnitTests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/internal/CalculatorUnitTests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..f6c02ce9 Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/internal/CalculatorUnitTests/Assets/StoreLogo.png b/internal/CalculatorUnitTests/Assets/StoreLogo.png new file mode 100644 index 00000000..7385b56c Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/StoreLogo.png differ diff --git a/internal/CalculatorUnitTests/Assets/Wide310x150Logo.scale-200.png b/internal/CalculatorUnitTests/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..288995b3 Binary files /dev/null and b/internal/CalculatorUnitTests/Assets/Wide310x150Logo.scale-200.png differ diff --git a/internal/CalculatorUnitTests/AsyncHelper.cpp b/internal/CalculatorUnitTests/AsyncHelper.cpp new file mode 100644 index 00000000..8d54d1dc --- /dev/null +++ b/internal/CalculatorUnitTests/AsyncHelper.cpp @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "AsyncHelper.h" +#include +#include + +using namespace std; +using namespace concurrency; +using namespace Platform; +using namespace CalculatorApp; +using namespace Windows::UI::Core; +using namespace Windows::ApplicationModel::Core; + +task AsyncHelper::RunOnUIThreadAsync(function&& action) +{ + auto callback = ref new DispatchedHandler([action]() + { + action(); + }); + + return create_task(CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, callback)); +} + +void AsyncHelper::RunOnUIThread(function&& action, DWORD timeout) +{ + task waitTask = RunOnUIThreadAsync([action]() + { + action(); + }); + + WaitForTask(waitTask, timeout); +} + +void AsyncHelper::Delay(DWORD milliseconds) +{ + thread timer(bind(CalculatorApp::AsyncHelper::Sleep, milliseconds)); + timer.join(); +} + +void AsyncHelper::Sleep(DWORD milliseconds) +{ + this_thread::sleep_for(chrono::milliseconds(milliseconds)); +} diff --git a/internal/CalculatorUnitTests/AsyncHelper.h b/internal/CalculatorUnitTests/AsyncHelper.h new file mode 100644 index 00000000..de3a88ea --- /dev/null +++ b/internal/CalculatorUnitTests/AsyncHelper.h @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include + +namespace CalculatorApp +{ + class AsyncHelper + { + public: + static concurrency::task RunOnUIThreadAsync(std::function&& action); + static void RunOnUIThread(std::function&& action, DWORD timeout = INFINITE); + static void Delay(DWORD milliseconds); + + template + static void RunOnUIThread(std::function()>&& action, DWORD timeout = INFINITE) + { + concurrency::task t; + concurrency::task uiTask = RunOnUIThreadAsync([&t, action]() + { + t = action(); + }).then([&t]() + { + t.wait(); + }, concurrency::task_continuation_context::use_arbitrary()); + + WaitForTask(uiTask, timeout); + } + + template + static bool WaitForTask(concurrency::task& t, DWORD timeout = INFINITE) + { + Microsoft::WRL::Wrappers::Event event(CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS)); + if (!event.IsValid()) + { + throw std::bad_alloc(); + } + + Platform::Exception^ ex; + t.then([&event, &ex](concurrency::task prevTask) + { + try + { + prevTask.get(); + } + catch (Platform::Exception^ e) + { + ex = e; + } + + if (event.IsValid()) + { + SetEvent(event.Get()); + } + }, concurrency::task_continuation_context::use_arbitrary()); + + DWORD waitResult;// = STATUS_PENDING; + waitResult = WaitForSingleObjectEx(event.Get(), timeout, true); + event.Close(); + + if (ex != nullptr) + { + throw ex; + } + + if (waitResult == WAIT_FAILED) + { + throw ref new Platform::Exception(-1, L"Error in waiting for task completion: " + waitResult.ToString()); + } + + return waitResult == WAIT_OBJECT_0; + } + private: + static void Sleep(DWORD milliseconds); + }; +} diff --git a/internal/CalculatorUnitTests/CalcEngineTests.cpp b/internal/CalculatorUnitTests/CalcEngineTests.cpp new file mode 100644 index 00000000..75890844 --- /dev/null +++ b/internal/CalculatorUnitTests/CalcEngineTests.cpp @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +#include "CalcViewModel\Common\EngineResourceProvider.h" + +using namespace std; +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace CalculatorApp; +using namespace CalculationManager; + +static constexpr size_t MAX_HISTORY_SIZE = 20; + +namespace CalculatorUnitTests +{ + class CalcEngineTests + { + TEST_CLASS(CalcEngineTests); + TEST_CLASS_SETUP(CommonSetup) + { + m_resourceProvider = make_shared(); + m_history = make_shared(CM_STD, MAX_HISTORY_SIZE); + CCalcEngine::InitialOneTimeOnlySetup(*(m_resourceProvider.get())); + m_calcEngine = make_unique(false /* Respect Order of Operations */, false /* Set to Integer Mode */, m_resourceProvider.get(), nullptr, m_history); + return true; + } + TEST_METHOD_CLEANUP(Cleanup) + { + return true; + } + + TEST_METHOD(TestGroupDigitsPerRadix) + { + // Empty/Error cases + VERIFY_IS_TRUE(m_calcEngine->GroupDigitsPerRadix(L"", 10).empty(), L"Verify grouping empty string returns empty string."); + VERIFY_ARE_EQUAL(L"12345678", m_calcEngine->GroupDigitsPerRadix(L"12345678", 9), L"Verify grouping on invalid base returns original string"); + + // Octal + VERIFY_ARE_EQUAL(L"1 234 567", m_calcEngine->GroupDigitsPerRadix(L"1234567", 8), L"Verify grouping in octal."); + VERIFY_ARE_EQUAL(L"123", m_calcEngine->GroupDigitsPerRadix(L"123", 8), L"Verify minimum grouping in octal."); + + // Binary/Hexadecimal + VERIFY_ARE_EQUAL(L"12 3456 7890", m_calcEngine->GroupDigitsPerRadix(L"1234567890", 2), L"Verify grouping in binary."); + VERIFY_ARE_EQUAL(L"1234", m_calcEngine->GroupDigitsPerRadix(L"1234", 2), L"Verify minimum grouping in binary."); + VERIFY_ARE_EQUAL(L"12 3456 7890", m_calcEngine->GroupDigitsPerRadix(L"1234567890", 16), L"Verify grouping in hexadecimal."); + VERIFY_ARE_EQUAL(L"1234", m_calcEngine->GroupDigitsPerRadix(L"1234", 16), L"Verify minimum grouping in hexadecimal."); + + // Decimal + VERIFY_ARE_EQUAL(L"1,234,567,890", m_calcEngine->GroupDigitsPerRadix(L"1234567890", 10), L"Verify grouping in base10."); + VERIFY_ARE_EQUAL(L"1,234,567.89", m_calcEngine->GroupDigitsPerRadix(L"1234567.89", 10), L"Verify grouping in base10 with decimal."); + VERIFY_ARE_EQUAL(L"1,234,567e89", m_calcEngine->GroupDigitsPerRadix(L"1234567e89", 10), L"Verify grouping in base10 with exponent."); + VERIFY_ARE_EQUAL(L"1,234,567.89e5", m_calcEngine->GroupDigitsPerRadix(L"1234567.89e5", 10), L"Verify grouping in base10 with decimal and exponent."); + VERIFY_ARE_EQUAL(L"-123,456,789", m_calcEngine->GroupDigitsPerRadix(L"-123456789", 10), L"Verify grouping in base10 with negative."); + } + + TEST_METHOD(TestIsNumberInvalid) + { + // Binary Number Checks + vector validBinStrs{ L"0", L"1", L"0011", L"1100" }; + vector invalidBinStrs{ L"2", L"A", L"0.1" }; + for (wstring const& str : validBinStrs) + { + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(str, 0, 0, 2 /* Binary */)); + } + for (wstring const& str : invalidBinStrs) + { + VERIFY_ARE_EQUAL(IDS_ERR_UNK_CH, m_calcEngine->IsNumberInvalid(str, 0, 0, 2 /* Binary */)); + } + + // Octal Number Checks + vector validOctStrs{ L"0", L"7", L"01234567", L"76543210" }; + vector invalidOctStrs{ L"8", L"A", L"0.7" }; + for (wstring const& str : validOctStrs) + { + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(str, 0, 0, 8 /* Octal */)); + } + for (wstring const& str : invalidOctStrs) + { + VERIFY_ARE_EQUAL(IDS_ERR_UNK_CH, m_calcEngine->IsNumberInvalid(str, 0, 0, 8 /* Octal */)); + } + + // Hexadecimal Number Checks + vector validHexStrs{ L"0", L"F", L"0123456789ABCDEF", L"FEDCBA9876543210" }; + vector invalidHexStrs{ L"G", L"abcdef", L"x", L"0.1" }; + for (wstring const& str : validHexStrs) + { + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(str, 0, 0, 16 /* HEx */)); + } + for (wstring const& str : invalidHexStrs) + { + VERIFY_ARE_EQUAL(IDS_ERR_UNK_CH, m_calcEngine->IsNumberInvalid(str, 0, 0, 16 /* Hex */)); + } + + // Decimal Number Checks + + // Special case errors: long exponent, long mantissa + wstring longExp(L"1e12345"); + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(longExp, 5 /* Max exp length */, 100, 10 /* Decimal */)); + VERIFY_ARE_EQUAL(IDS_ERR_INPUT_OVERFLOW, m_calcEngine->IsNumberInvalid(longExp, 4 /* Max exp length */, 100, 10 /* Decimal */)); + // Mantissa length is sum of: + // - digits before decimal separator, minus leading zeroes + // - digits after decimal separator, including trailing zeroes + // Each of these mantissa values should calculate as a length of 5 + vector longMantStrs{ L"10000", L"10.000", L"0000012345", L"123.45", L"0.00123", L"0.12345", L"-123.45e678" }; + for (wstring const& str : longMantStrs) + { + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(str, 100, 5 /* Max mantissa length */, 10 /* Decimal */)); + } + for (wstring const& str : longMantStrs) + { + VERIFY_ARE_EQUAL(IDS_ERR_INPUT_OVERFLOW, m_calcEngine->IsNumberInvalid(str, 100, 4 /* Max mantissa length */, 10 /* Decimal */)); + } + + // Regex matching (descriptions taken from CalcUtils.cpp) + // Use 100 for exp/mantissa length as they are tested above + vector validDecStrs{ + // Start with an optional + or - + L"+1", L"-1", L"1", + // Followed by zero or more digits + L"-", L"", L"1234567890", + // Followed by an optional decimal point + L"1.0", L"-.", L"1.", + // Followed by zero or more digits + L"0.0", L"0.123456", + // Followed by an optional exponent ('e') + L"1e", L"1.e", L"-e", + // If there's an exponent, its optionally followed by + or - + // and followed by zero or more digits + L"1e+12345", L"1e-12345", L"1e123", + // All together + L"-123.456e+789" + }; + vector invalidDecStrs{ L"x123", L"123-", L"1e1.2", L"1-e2" }; + for (wstring const& str : validDecStrs) + { + VERIFY_ARE_EQUAL(0, m_calcEngine->IsNumberInvalid(str, 100, 100, 10 /* Dec */)); + } + for (wstring const& str : invalidDecStrs) + { + VERIFY_ARE_EQUAL(IDS_ERR_UNK_CH, m_calcEngine->IsNumberInvalid(str, 100, 100, 10 /* Dec */)); + } + + } + + TEST_METHOD(TestDigitGroupingStringToGroupingVector) + { + vector groupingVector{}; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L""), L"Verify empty grouping"); + + groupingVector = { 1 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"1"), L"Verify simple grouping"); + + groupingVector = { 3, 0 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"3;0"), L"Verify standard grouping"); + + groupingVector = { 3, 0, 0 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"3;0;0"), L"Verify expanded non-repeating grouping"); + + groupingVector = { 5, 3, 2, 4, 6 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"5;3;2;4;6"), L"Verify long grouping"); + + groupingVector = { 15, 15, 15, 0 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"15;15;15;0"), L"Verify large grouping"); + + groupingVector = { 4, 7, 0 }; + VERIFY_ARE_EQUAL(groupingVector, CCalcEngine::DigitGroupingStringToGroupingVector(L"4;16;7;25;0"), L"Verify we ignore oversize grouping"); + } + + TEST_METHOD(TestGroupDigits) + { + wstring result{ L"1234567" }; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L"", { 3, 0 }, L"1234567", false), L"Verify handling of empty delimiter."); + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", {}, L"1234567", false), L"Verify handling of empty grouping."); + + result = L"1,234,567"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0 }, L"1234567", false), L"Verify standard digit grouping."); + + result = L"1 234 567"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L" ", { 3, 0 }, L"1234567", false), L"Verify delimiter change."); + + result = L"1|||234|||567"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L"|||", { 3, 0 }, L"1234567", false), L"Verify long delimiter."); + + result = L"12,345e67"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0 }, L"12345e67", false), L"Verify respect of exponent."); + + result = L"12,345.67"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0 }, L"12345.67", false), L"Verify respect of decimal."); + + result = L"1,234.56e7"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0 }, L"1234.56e7", false), L"Verify respect of exponent and decimal."); + + result = L"-1,234,567"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0 }, L"-1234567", true), L"Verify negative number grouping."); + + // Test various groupings + result = L"1234567890123456"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 0, 0 }, L"1234567890123456", false), L"Verify no grouping."); + + result = L"1234567890123,456"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3 }, L"1234567890123456", false), L"Verify non-repeating grouping."); + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 3, 0, 0 }, L"1234567890123456", false), L"Verify expanded form non-repeating grouping."); + + result = L"12,34,56,78,901,23456"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 5, 3, 2, 0 }, L"1234567890123456", false), L"Verify multigroup with repeating grouping."); + + result = L"1234,5678,9012,3456"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 4, 0 }, L"1234567890123456", false), L"Verify repeating non-standard grouping."); + + result = L"123456,78,901,23456"; + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 5, 3, 2 }, L"1234567890123456", false), L"Verify multigroup non-repeating grouping."); + VERIFY_ARE_EQUAL(result, m_calcEngine->GroupDigits(L",", { 5, 3, 2, 0, 0 }, L"1234567890123456", false), L"Verify expanded form multigroup non-repeating grouping."); + } + + private: + unique_ptr m_calcEngine; + shared_ptr m_resourceProvider; + shared_ptr m_history; + }; +} diff --git a/internal/CalculatorUnitTests/CalcInputTest.cpp b/internal/CalculatorUnitTests/CalcInputTest.cpp new file mode 100644 index 00000000..e29072b5 --- /dev/null +++ b/internal/CalculatorUnitTests/CalcInputTest.cpp @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +using namespace std; +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace CalculationManager; + +namespace CalculatorUnitTests +{ + class CalcInputTest + { + TEST_CLASS(CalcInputTest); + TEST_CLASS_SETUP(CommonSetup) + { + m_calcInput = CalcEngine::CalcInput(L'.'); + return true; + } + TEST_METHOD_CLEANUP(Cleanup) + { + m_calcInput.Clear(); + m_calcInput.SetDecimalSymbol(L'.'); + return true; + } + + TEST_METHOD(Clear) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryToggleSign(false, L"999"); + m_calcInput.TryAddDecimalPt(); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + m_calcInput.TryAddDigit(3, 10, false, L"999", 64, 32); + + VERIFY_ARE_EQUAL(L"-1.2e+3", m_calcInput.ToString(10, false), L"Verify input is correct."); + + m_calcInput.Clear(); + + ::Log::Comment(m_calcInput.ToString(10, false).c_str()); + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify input is 0 after clear."); + } + + TEST_METHOD(TryToggleSignZero) + { + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(false, L"999"), L"Verify toggling 0 succeeds."); + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify toggling 0 does not create -0."); + } + TEST_METHOD(TryToggleSignExponent) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(false, L"999"), L"Verify toggling exponent sign succeeds."); + VERIFY_ARE_EQUAL(L"1.e-2", m_calcInput.ToString(10, false), L"Verify toggling exponent sign does not toggle base sign."); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(false, L"999"), L"Verify toggling exponent sign succeeds."); + VERIFY_ARE_EQUAL(L"1.e+2", m_calcInput.ToString(10, false), L"Verify toggling negative exponent sign does not toggle base sign."); + } + TEST_METHOD(TryToggleSignBase) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(false, L"999"), L"Verify toggling base sign succeeds."); + VERIFY_ARE_EQUAL(L"-1", m_calcInput.ToString(10, false), L"Verify toggling base sign creates negative base."); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(false, L"999"), L"Verify toggling base sign succeeds."); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify toggling negative base sign creates positive base."); + } + TEST_METHOD(TryToggleSignBaseIntegerMode) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(true, L"999"), L"Verify toggling base sign in integer mode succeeds."); + VERIFY_ARE_EQUAL(L"-1", m_calcInput.ToString(10, false), L"Verify toggling base sign creates negative base."); + } + TEST_METHOD(TryToggleSignRollover) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryToggleSign(true, L"127"), L"Verify toggling base sign in integer mode succeeds."); + m_calcInput.TryAddDigit(8, 10, false, L"999", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryToggleSign(true, L"127"), L"Verify toggling base sign in integer mode fails on rollover."); + VERIFY_ARE_EQUAL(L"-128", m_calcInput.ToString(10, false), L"Verify toggling base sign on rollover does not change value."); + } + + TEST_METHOD(TryAddDigitLeadingZeroes) + { + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(0, 10, false, L"999", 64, 32), L"Verify TryAddDigit succeeds."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(0, 10, false, L"999", 64, 32), L"Verify TryAddDigit succeeds."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(0, 10, false, L"999", 64, 32), L"Verify TryAddDigit succeeds."); + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify leading zeroes are ignored."); + } + TEST_METHOD(TryAddDigitMaxCount) + { + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32), L"Verify TryAddDigit for base with length < maxDigits succeeds."); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify adding digit for base with length < maxDigits succeeded."); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 1), L"Verify TryAddDigit for base with length > maxDigits fails."); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify digit for base was not added."); + m_calcInput.TryBeginExponent(); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32), L"Verify TryAddDigit for exponent with length < maxDigits succeeds."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32), L"Verify TryAddDigit for exponent with length < maxDigits succeeds."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(3, 10, false, L"999", 64, 32), L"Verify TryAddDigit for exponent with length < maxDigits succeeds."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(4, 10, false, L"999", 64, 32), L"Verify TryAddDigit for exponent with length < maxDigits succeeds."); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(5, 10, false, L"999", 64, 32), L"Verify TryAddDigit for exponent with length > maxDigits fails."); + VERIFY_ARE_EQUAL(L"1.e+1234", m_calcInput.ToString(10, false), L"Verify adding digits for exponent with length < maxDigits succeeded."); + + m_calcInput.Clear(); + m_calcInput.TryAddDecimalPt(); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 1), L"Verify decimal point and leading zero does not count toward maxDigits."); + VERIFY_ARE_EQUAL(L"0.1", m_calcInput.ToString(10, false), L"Verify input value checking dec pt and leading zero impact on maxDigits."); + } + TEST_METHOD(TryAddDigitValues) + { + // Use an arbitrary value > 16 to test that input accepts digits > hexadecimal 0xF. + // TryAddDigit does not validate whether the digit fits within the current radix. + for (unsigned int i = 0; i < 25; i++) + { + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(i, 10, false, L"999", 64, 32), String().Format(L"Verify TryAddDigit succeeds for %d", i)); + m_calcInput.Clear(); + } + } + TEST_METHOD(TryAddDigitRolloverBaseCheck) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 16, true, L"999", 64, 1), L"Verify TryAddDigit rollover fails for bases other than 8,10."); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(1, 2, true, L"999", 64, 1), L"Verify TryAddDigit rollover fails for bases other than 8,10."); + } + TEST_METHOD(TryAddDigitRolloverOctalByte) + { + m_calcInput.TryAddDigit(1, 8, true, L"777", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(2, 8, true, L"377", 8, 1), L"Verify we can add an extra digit in OctalByte if first digit <= 3."); + + m_calcInput.Clear(); + m_calcInput.TryAddDigit(4, 8, true, L"777", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 8, true, L"377", 8, 1), L"Verify we cannot add an extra digit in OctalByte if first digit > 3."); + } + TEST_METHOD(TryAddDigitRolloverOctalWord) + { + m_calcInput.TryAddDigit(1, 8, true, L"777", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(2, 8, true, L"377", 16, 1), L"Verify we can add an extra digit in OctalByte if first digit == 1."); + + m_calcInput.Clear(); + m_calcInput.TryAddDigit(2, 8, true, L"777", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 8, true, L"377", 16, 1), L"Verify we cannot add an extra digit in OctalByte if first digit > 1."); + } + TEST_METHOD(TryAddDigitRolloverOctalDword) + { + m_calcInput.TryAddDigit(1, 8, true, L"777", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(2, 8, true, L"377", 32, 1), L"Verify we can add an extra digit in OctalByte if first digit <= 3."); + + m_calcInput.Clear(); + m_calcInput.TryAddDigit(4, 8, true, L"777", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 8, true, L"377", 32, 1), L"Verify we cannot add an extra digit in OctalByte if first digit > 3."); + } + TEST_METHOD(TryAddDigitRolloverOctalQword) + { + m_calcInput.TryAddDigit(1, 8, true, L"777", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(2, 8, true, L"377", 64, 1), L"Verify we can add an extra digit in OctalByte if first digit == 1."); + + m_calcInput.Clear(); + m_calcInput.TryAddDigit(2, 8, true, L"777", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 8, true, L"377", 64, 1), L"Verify we cannot add an extra digit in OctalByte if first digit > 1."); + } + TEST_METHOD(TryAddDigitRolloverDecimal) + { + m_calcInput.TryAddDigit(1, 10, true, L"127", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(0, 10, true, L"1", 8, 1), L"Verify we cannot add a digit if input size matches maxStr size."); + m_calcInput.TryAddDigit(2, 10, true, L"127", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(2, 10, true, L"110", 8, 2), L"Verify we cannot add a digit if n char comparison > 0."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(7, 10, true, L"130", 8, 2), L"Verify we can add a digit if n char comparison < 0."); + + m_calcInput.Clear(); + m_calcInput.TryAddDigit(1, 10, true, L"127", 64, 32); + m_calcInput.TryAddDigit(2, 10, true, L"127", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(8, 10, true, L"127", 8, 2), L"Verify we cannot add a digit if digit exceeds max value."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(7, 10, true, L"127", 8, 2), L"Verify we can add a digit if digit does not exceed max value."); + + m_calcInput.Backspace(); + m_calcInput.TryToggleSign(true, L"127"); + VERIFY_IS_FALSE(m_calcInput.TryAddDigit(9, 10, true, L"127", 8, 2), L"Negative value: verify we cannot add a digit if digit exceeds max value."); + VERIFY_IS_TRUE(m_calcInput.TryAddDigit(8, 10, true, L"127", 8, 2), L"Negative value: verify we can add a digit if digit does not exceed max value."); + } + + TEST_METHOD(TryAddDecimalPtEmpty) + { + VERIFY_IS_FALSE(m_calcInput.HasDecimalPt(), L"Verify input has no decimal point."); + VERIFY_IS_TRUE(m_calcInput.TryAddDecimalPt(), L"Verify adding decimal to empty input."); + VERIFY_IS_TRUE(m_calcInput.HasDecimalPt(), L"Verify input has decimal point."); + VERIFY_ARE_EQUAL(L"0.", m_calcInput.ToString(10, false), L"Verify decimal on empty input."); + } + TEST_METHOD(TryAddDecimalPointTwice) + { + VERIFY_IS_FALSE(m_calcInput.HasDecimalPt(), L"Verify input has no decimal point."); + VERIFY_IS_TRUE(m_calcInput.TryAddDecimalPt(), L"Verify adding decimal to empty input."); + VERIFY_IS_TRUE(m_calcInput.HasDecimalPt(), L"Verify input has decimal point."); + VERIFY_IS_FALSE(m_calcInput.TryAddDecimalPt(), L"Verify adding decimal point fails if input has decimal point."); + } + TEST_METHOD(TryAddDecimalPointExponent) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + VERIFY_IS_FALSE(m_calcInput.TryAddDecimalPt(), L"Verify adding decimal point fails if input has exponent."); + } + + TEST_METHOD(TryBeginExponentNoExponent) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryBeginExponent(), L"Verify adding exponent succeeds on input without exponent."); + VERIFY_ARE_EQUAL(L"1.e+0", m_calcInput.ToString(10, false), L"Verify exponent present."); + } + TEST_METHOD(TryBeginExponentWithExponent) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_IS_TRUE(m_calcInput.TryBeginExponent(), L"Verify adding exponent succeeds on input without exponent."); + VERIFY_IS_FALSE(m_calcInput.TryBeginExponent(), L"Verify cannot add exponent if input already has exponent."); + } + + TEST_METHOD(BackspaceZero) + { + m_calcInput.Backspace(); + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify backspace on 0 is still 0."); + } + TEST_METHOD(BackspaceSingleChar) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify input before backspace."); + m_calcInput.Backspace(); + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify input after backspace."); + } + TEST_METHOD(BackspaceMultiChar) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"12", m_calcInput.ToString(10, false), L"Verify input before backspace."); + m_calcInput.Backspace(); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify input after backspace."); + } + TEST_METHOD(BackspaceDecimal) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryAddDecimalPt(); + VERIFY_ARE_EQUAL(L"1.", m_calcInput.ToString(10, false), L"Verify input before backspace."); + VERIFY_IS_TRUE(m_calcInput.HasDecimalPt(), L"Verify input has decimal point."); + m_calcInput.Backspace(); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, false), L"Verify input after backspace."); + VERIFY_IS_FALSE(m_calcInput.HasDecimalPt(), L"Verify decimal point was removed."); + } + TEST_METHOD(BackspaceMultiCharDecimal) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryAddDecimalPt(); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(3, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"1.23", m_calcInput.ToString(10, false), L"Verify input before backspace."); + m_calcInput.Backspace(); + VERIFY_ARE_EQUAL(L"1.2", m_calcInput.ToString(10, false), L"Verify input after backspace."); + } + + TEST_METHOD(SetDecimalSymbol) + { + m_calcInput.TryAddDecimalPt(); + VERIFY_ARE_EQUAL(L"0.", m_calcInput.ToString(10, false), L"Verify default decimal point."); + m_calcInput.SetDecimalSymbol(L','); + VERIFY_ARE_EQUAL(L"0,", m_calcInput.ToString(10, false), L"Verify new decimal point."); + } + + TEST_METHOD(ToStringEmpty) + { + VERIFY_ARE_EQUAL(L"0", m_calcInput.ToString(10, false), L"Verify ToString of empty value."); + } + TEST_METHOD(ToStringNegative) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryToggleSign(false, L"999"); + VERIFY_ARE_EQUAL(L"-1", m_calcInput.ToString(10, false), L"Verify ToString of negative value."); + } + TEST_METHOD(ToStringExponentBase10) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + VERIFY_ARE_EQUAL(L"1.e+0", m_calcInput.ToString(10, false), L"Verify ToString of empty base10 exponent."); + } + TEST_METHOD(ToStringExponentBase8) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + VERIFY_ARE_EQUAL(L"1.^+0", m_calcInput.ToString(8, false), L"Verify ToString of empty base8 exponent."); + } + TEST_METHOD(ToStringExponentNegative) + { + m_calcInput.TryAddDigit(1, 8, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + m_calcInput.TryToggleSign(false, L"999"); + VERIFY_ARE_EQUAL(L"1.e-0", m_calcInput.ToString(10, false), L"Verify ToString of empty negative exponent."); + } + TEST_METHOD(ToStringExponentPositive) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(3, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(4, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"1.e+234", m_calcInput.ToString(10, false), L"Verify ToString of exponent with value."); + } + TEST_METHOD(ToStringInteger) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"1", m_calcInput.ToString(10, true), L"Verify ToString of integer value hides decimal."); + } + TEST_METHOD(ToStringBaseTooLong) + { + wstring maxStr{}; + for (size_t i = 0; i < MAX_STRLEN + 1; i++) + { + maxStr += L"1"; + m_calcInput.TryAddDigit(1, 10, false, maxStr, 64, 100); + } + auto result = m_calcInput.ToString(10, false); + VERIFY_IS_TRUE(result.empty(), L"Verify ToString of base value that is too large yields empty string."); + } + TEST_METHOD(ToStringExponentTooLong) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryBeginExponent(); + wstring maxStr{L"11"}; + bool exponentCapped = false; + for (size_t i = 0; i < MAX_STRLEN + 1; i++) + { + maxStr += L"1"; + if (!m_calcInput.TryAddDigit(1, 10, false, maxStr, 64, MAX_STRLEN + 25)) + { + exponentCapped = true; + } + } + auto result = m_calcInput.ToString(10, false); + + // TryAddDigit caps the exponent length to C_EXP_MAX_DIGITS = 4, so ToString() succeeds. + // If that cap is removed, ToString() should return an empty string. + if (exponentCapped) + { + VERIFY_ARE_EQUAL(L"1.e+1111", result, L"Verify ToString succeeds; exponent length is capped at C_EXP_MAX_DIGITS."); + } + else + { + VERIFY_IS_TRUE(result.empty(), L"Verify ToString of exponent value that is too large yields empty string."); + } + } + + TEST_METHOD(ToRational) + { + m_calcInput.TryAddDigit(1, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(2, 10, false, L"999", 64, 32); + m_calcInput.TryAddDigit(3, 10, false, L"999", 64, 32); + VERIFY_ARE_EQUAL(L"123", m_calcInput.ToString(10, false), L"Verify input before conversion to rational."); + + auto rat = m_calcInput.ToRational(10, false); + VERIFY_ARE_EQUAL(1, rat.P().Mantissa().size(), L"Verify digit count of rational."); + VERIFY_ARE_EQUAL(123, rat.P().Mantissa().front(), L"Verify first digit of mantissa."); + } + + private: + CalcEngine::CalcInput m_calcInput; + }; +} diff --git a/internal/CalculatorUnitTests/CalculatorManagerTest.cpp b/internal/CalculatorUnitTests/CalculatorManagerTest.cpp new file mode 100644 index 00000000..5101e7d7 --- /dev/null +++ b/internal/CalculatorUnitTests/CalculatorManagerTest.cpp @@ -0,0 +1,1123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +#include + +#include "CalcManager\CalculatorHistory.h" +#include "CalcViewModel\Common\EngineResourceProvider.h" + +using namespace CalculatorApp; +using namespace CalculationManager; +using namespace Platform; +using namespace std; + +namespace CalculatorManagerTest +{ + class CalculatorManagerDisplayTester : public ICalcDisplay + { + public: + CalculatorManagerDisplayTester() + { + Reset(); + } + + void Reset() + { + m_isError = false; + m_maxDigitsCalledCount = 0; + m_binaryOperatorReceivedCallCount = 0; + } + + void SetPrimaryDisplay(const wstring& text, bool isError) override + { + m_primaryDisplay = text; + m_isError = isError; + } + void SetIsInError(bool isError) override + { + m_isError = isError; + } + void SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands) + { + m_expression.clear(); + unsigned int nTokens = 0; + std::pair currentPair; + tokens->GetSize(&nTokens); + for (unsigned int i = 0; i < nTokens; ++i) + { + tokens->GetAt(i, ¤tPair); + m_expression += currentPair.first; + } + } + void SetMemorizedNumbers(const vector& numbers) override + { + m_memorizedNumberStrings = numbers; + } + + void SetParenDisplayText(const std::wstring& parenthesisCount) override + { + m_parenDisplay = parenthesisCount; + } + + const wstring& GetPrimaryDisplay() const + { + return m_primaryDisplay; + } + const wstring& GetExpression() const + { + return m_expression; + } + const vector& GetMemorizedNumbers() const + { + return m_memorizedNumberStrings; + } + bool GetIsError() const + { + return m_isError; + } + + void OnHistoryItemAdded(_In_ unsigned int addedItemIndex) + { + } + + void MaxDigitsReached() override + { + m_maxDigitsCalledCount++; + } + + int GetMaxDigitsCalledCount() + { + return m_maxDigitsCalledCount; + } + + void BinaryOperatorReceived() + { + m_binaryOperatorReceivedCallCount++; + } + + void MemoryItemChanged(unsigned int indexOfMemory) + { + } + + int GetBinaryOperatorReceivedCallCount() + { + return m_binaryOperatorReceivedCallCount; + } + + private: + wstring m_primaryDisplay; + wstring m_expression; + wstring m_parenDisplay; + bool m_isError; + vector m_memorizedNumberStrings; + int m_maxDigitsCalledCount; + int m_binaryOperatorReceivedCallCount; + }; + + + class TestDriver + { + private: + static shared_ptr m_displayTester; + static shared_ptr m_calculatorManager; + + public: + static void Initialize(shared_ptr displayTester, shared_ptr calculatorManager) + { + m_displayTester = displayTester; + m_calculatorManager = calculatorManager; + } + + static void Test(wstring expectedPrimary, wstring expectedExpression, Command testCommands[], + bool cleanup = true, bool isScientific = false) + { + if (cleanup) + { + m_calculatorManager->Reset(); + } + + if (isScientific) + { + m_calculatorManager->SendCommand(Command::ModeScientific); + } + + Command* currentCommand = testCommands; + while (*currentCommand != Command::CommandNULL) + { + m_calculatorManager->SendCommand(*currentCommand++); + } + + VERIFY_ARE_EQUAL(expectedPrimary, m_displayTester->GetPrimaryDisplay()); + if (expectedExpression != L"N/A") + { + VERIFY_ARE_EQUAL(expectedExpression, m_displayTester->GetExpression()); + } + } + }; + + class CalculatorManagerTest + { + public: + // Declare this class as a TestClass, and supply metadata if necessary. + TEST_CLASS(CalculatorManagerTest); + TEST_CLASS_SETUP(CommonSetup); + + TEST_METHOD(CalculatorManagerTestStandard); + + TEST_METHOD(CalculatorManagerTestScientific); + TEST_METHOD(CalculatorManagerTestScientific2); + TEST_METHOD(CalculatorManagerTestScientificParenthesis); + TEST_METHOD(CalculatorManagerTestScientificError); + TEST_METHOD(CalculatorManagerTestScientificModeChange); + + TEST_METHOD(CalculatorManagerTestModeChange); + + TEST_METHOD(CalculatorManagerTestMemory); + + TEST_METHOD(CalculatorManagerTestSerialize); + TEST_METHOD(CalculatorManagerTestSerializePrecision); + TEST_METHOD(CalculatorManagerTestSerializeMultiple); + TEST_METHOD(CalculatorManagerTestSerializeDegreeMode); + TEST_METHOD(CalculatorManagerTestSerializeMemory); + + TEST_METHOD(CalculatorManagerTestMaxDigitsReached); + TEST_METHOD(CalculatorManagerTestMaxDigitsReached_LeadingDecimal); + TEST_METHOD(CalculatorManagerTestMaxDigitsReached_TrailingDecimal); + + TEST_METHOD(CalculatorManagerTestBinaryOperatorReceived); + TEST_METHOD(CalculatorManagerTestBinaryOperatorReceived_Multiple); + TEST_METHOD(CalculatorManagerTestBinaryOperatorReceived_LongInput); + + TEST_METHOD_CLEANUP(Cleanup); + + + private: + static std::shared_ptr m_calculatorManager; + static std::shared_ptr m_resourceProvider; + static std::shared_ptr m_calculatorDisplayTester; + void ExecuteCommands(Command commands[]); + void ExecuteCommands(const vector& commands); + + vector CommandListFromStringInput(const wstring& input) + { + vector result{}; + for (auto iter = input.begin(); iter != input.end(); iter++) + { + wchar_t ch = *iter; + Command asCommand = Command::CommandNULL; + if (ch == L'.') + { + asCommand = Command::CommandPNT; + } + else if (L'0' <= ch && ch <= L'9') + { + int diff = static_cast(ch) - static_cast(L'0'); + asCommand = static_cast(diff + static_cast(Command::Command0)); + } + + if (asCommand != Command::CommandNULL) + { + result.push_back(asCommand); + } + } + + return result; + } + + void TestMaxDigitsReachedScenario(const wstring& constInput) + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + + // Make sure we're in a clean state. + VERIFY_ARE_EQUAL(0, pCalculatorDisplay->GetMaxDigitsCalledCount()); + + vector commands = CommandListFromStringInput(constInput); + VERIFY_IS_FALSE(commands.empty()); + + // The last element in the list should always cause MaxDigitsReached + // Remember the command but remove from the actual input that is sent + Command finalInput = commands[commands.size() - 1]; + commands.pop_back(); + wstring input = constInput.substr(0, constInput.length() - 1); + + m_calculatorManager->SetStandardMode(); + ExecuteCommands(commands); + + wstring expectedDisplay = input; + wstring display = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(expectedDisplay, display); + + m_calculatorManager->SendCommand(finalInput); + + // Verify MaxDigitsReached + display = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(expectedDisplay, display); + + // MaxDigitsReached should have been called once + VERIFY_IS_LESS_THAN(0, pCalculatorDisplay->GetMaxDigitsCalledCount()); + } + + void SerialzieAndDeSerialize() + { + auto serializedCommands = m_calculatorManager->SerializeCommands(); + auto serializedMemory = m_calculatorManager->GetSerializedMemory(); + auto serializedDisplay = m_calculatorManager->GetSerializedPrimaryDisplay(); + + Cleanup(); + + m_calculatorManager->DeSerializePrimaryDisplay(serializedDisplay); + m_calculatorManager->DeSerializeMemory(serializedMemory); + m_calculatorManager->DeSerializeCommands(serializedCommands); + } + + void VerifyPersistence() + { + auto savedPrimary = m_calculatorDisplayTester->GetPrimaryDisplay(); + auto savedExpression = m_calculatorDisplayTester->GetExpression(); + auto savedMemory = m_calculatorDisplayTester->GetMemorizedNumbers(); + SerialzieAndDeSerialize(); + VERIFY_ARE_EQUAL(savedPrimary, m_calculatorDisplayTester->GetPrimaryDisplay()); + VERIFY_ARE_EQUAL(savedExpression, m_calculatorDisplayTester->GetExpression()); + auto loadedMemory = m_calculatorDisplayTester->GetMemorizedNumbers(); + VERIFY_ARE_EQUAL(savedMemory.size(), loadedMemory.size()); + for (unsigned int i = 0; i < savedMemory.size(); i++) + { + VERIFY_ARE_EQUAL(savedMemory.at(i), loadedMemory.at(i)); + } + } + }; + + std::shared_ptr CalculatorManagerTest::m_calculatorManager; + std::shared_ptr CalculatorManagerTest::m_calculatorDisplayTester; + std::shared_ptr CalculatorManagerTest::m_resourceProvider; + std::shared_ptr TestDriver::m_displayTester; + std::shared_ptr TestDriver::m_calculatorManager; + + // Creates instance of CalculationManager before running tests + bool CalculatorManagerTest::CommonSetup() + { + m_calculatorDisplayTester = std::make_shared(); + m_resourceProvider = std::make_shared(); + m_calculatorManager = std::make_shared(m_calculatorDisplayTester.get(), m_resourceProvider.get()); + TestDriver::Initialize(m_calculatorDisplayTester, m_calculatorManager); + return true; + } + + // Resets calculator state to start state after each test + bool CalculatorManagerTest::Cleanup() + { + m_calculatorManager->Reset(); + m_calculatorDisplayTester->Reset(); + return true; + } + + void CalculatorManagerTest::ExecuteCommands(Command commands[]) + { + Command* itr = commands; + while (*itr != Command::CommandNULL) + { + m_calculatorManager->SendCommand(*itr); + itr++; + } + } + + void CalculatorManagerTest::ExecuteCommands(const vector& commands) + { + for (const Command& command : commands) + { + if (command == Command::CommandNULL) + { + break; + } + + m_calculatorManager->SendCommand(command); + } + } + + void CalculatorManagerTest::CalculatorManagerTestStandard() + { + Command commands1[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandPNT, Command::Command4, Command::Command5, Command::Command6, Command::CommandNULL }; + TestDriver::Test(L"123.456", L"", commands1); + + Command commands2[] = { Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"0 + ", commands2); + + Command commands3[] = { Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(0)", commands3); + + Command commands4[] = { Command::Command2, Command::CommandADD, Command::Command3, + Command::CommandEQU, Command::Command4, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"7", L"", commands4); + + Command commands5[] = { Command::Command4, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"4", L"", commands5); + + Command commands6[] = { Command::Command2, Command::Command5, Command::Command6, + Command::CommandSQRT, Command::CommandSQRT, Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"2", L"\x221A(\x221A(\x221A(256)))", commands6); + + Command commands7[] = { Command::Command3, Command::CommandSUB, Command::Command6, + Command::CommandEQU, Command::CommandMUL, Command::Command3, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"-9", L"", commands7); + + Command commands8[] = { Command::Command9, Command::CommandMUL, Command::Command6, + Command::CommandSUB, Command::CommandCENTR, Command::Command8, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"46", L"", commands8); + + Command commands9[] = { Command::Command6, Command::CommandMUL, Command::Command6, + Command::CommandPERCENT, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"0.36", L"", commands9); + + Command commands10[] = { Command::Command5, Command::Command0, Command::CommandADD, + Command::Command2, Command::Command0, Command::CommandPERCENT, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"60", L"", commands10); + + Command commands11[] = { Command::Command4, Command::CommandADD, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"8", L"", commands11); + + Command commands12[] = { Command::Command5, Command::CommandADD, Command::CommandMUL, + Command::Command3, Command::CommandNULL }; + TestDriver::Test(L"3", L"5 \x00D7 ", commands12); + + Command commands13[] = { Command::Command1, Command::CommandEXP, Command::CommandSIGN, + Command::Command9, Command::Command9, Command::Command9, + Command::Command9, Command::CommandDIV, Command::Command1, + Command::Command0, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Overflow", L"1.e-9999 \x00F7 ", commands13); + + Command commands14[] = { Command::Command5, Command::Command0, Command::CommandADD, + Command::Command2, Command::Command0, Command::CommandPERCENT, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"60", L"", commands14); + + Command commands15[] = { Command::Command0, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Result is undefined", L"0 \x00F7 ", commands15); + + Command commands16[] = { Command::Command1, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Cannot divide by zero", L"1 \x00F7 ", commands16); + + Command commands17[] = { Command::Command1, Command::Command2, Command::CommandADD, + Command::Command5, Command::CommandCENTR, Command::Command2, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"14", L"12 + 2 + ", commands17); + + Command commands18[] = { Command::Command1, Command::Command0, Command::Command0, + Command::CommandSIGN, Command::CommandREC, Command::CommandNULL }; + TestDriver::Test(L"-0.01", L"1/(-100)", commands18); + + Command commands19[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandBACK, Command::CommandBACK, Command::CommandNULL }; + TestDriver::Test(L"1", L"", commands19); + + Command commands20[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandBACK, Command::CommandBACK, Command::CommandBACK, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands20); + + Command commands21[] = { Command::Command4, Command::CommandSQRT, Command::CommandSUB, + Command::Command2, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(4) - 2 + ", commands21); + + Command commands22[] = { Command::Command1, Command::Command0, Command::Command2, Command::Command4, + Command::CommandSQRT, Command::CommandSUB, Command::Command3, Command::Command2, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(1024) - 32 + ", commands22); + } + + void CalculatorManagerTest::CalculatorManagerTestScientific() + { + Command commands1[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandPNT, Command::Command4, Command::Command5, Command::Command6, Command::CommandNULL }; + TestDriver::Test(L"123.456", L"", commands1, true, true); + + Command commands2[] = { Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"0 + ", commands2, true, true); + + Command commands3[] = { Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(0)", commands3, true, true); + + Command commands4[] = { Command::Command1, Command::CommandADD, Command::Command0, + Command::CommandMUL, Command::Command2, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"1", L"", commands4, true, true); + + Command commands5[] = { Command::Command4, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"4", L"", commands5, true, true); + + Command commands6[] = { Command::Command2, Command::Command5, Command::Command6, + Command::CommandSQRT, Command::CommandSQRT, Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"2", L"\x221A(\x221A(\x221A(256)))", commands6, true, true); + + Command commands7[] = { Command::Command3, Command::CommandSUB, Command::Command6, + Command::CommandEQU, Command::CommandMUL, Command::Command3, \ + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"-9", L"-3 \x00D7 3 + ", commands7, true, true); + + Command commands8[] = { Command::Command9, Command::CommandMUL, Command::Command6, + Command::CommandSUB, Command::CommandCENTR, Command::Command8, + Command::CommandMUL, Command::Command2, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"38", L"9 \x00D7 6 - 8 \x00D7 2 + ", commands8, true, true); + + Command commands9[] = { Command::Command6, Command::CommandMUL, Command::Command6, + Command::CommandSIGN, Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"Invalid input", L"6 \x00D7 \x221A(-6)", commands9, true, true); + + Command commands10[] = { Command::Command5, Command::Command0, Command::CommandADD, + Command::Command2, Command::Command0, Command::CommandREC, + Command::CommandSUB, Command::CommandNULL }; + TestDriver::Test(L"50.05", L"50 + 1/(20) - ", commands10, true, true); + + Command commands11[] = { Command::Command4, Command::CommandADD, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"8", L"", commands11, true, true); + + Command commands12[] = { Command::Command5, Command::CommandADD, Command::CommandMUL, + Command::Command3, Command::CommandNULL }; + TestDriver::Test(L"3", L"5 \x00D7 ", commands12, true, true); + + Command commands13[] = { Command::Command1, Command::CommandEXP, Command::CommandSIGN, + Command::Command9, Command::Command9, Command::Command9, + Command::Command9, Command::CommandDIV, Command::Command1, + Command::Command0, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Overflow", L"1.e-9999 \x00F7 ", commands13, true, true); + + Command commands14[] = { Command::Command5, Command::Command0, Command::CommandADD, + Command::Command2, Command::Command0, Command::CommandPERCENT, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"60", L"", commands14, true, true); + + Command commands15[] = { Command::Command0, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Result is undefined", L"0 \x00F7 ", commands15, true, true); + + Command commands16[] = { Command::Command1, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Cannot divide by zero", L"1 \x00F7 ", commands16, true, true); + + Command commands17[] = { Command::Command1, Command::Command2, Command::CommandADD, + Command::Command5, Command::CommandCENTR, Command::Command2, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"14", L"12 + 2 + ", commands17, true, true); + + Command commands18[] = { Command::Command1, Command::Command0, Command::Command0, + Command::CommandSIGN, Command::CommandREC, Command::CommandNULL }; + TestDriver::Test(L"-0.01", L"1/(-100)", commands18, true, true); + + Command commands19[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandBACK, Command::CommandBACK, Command::CommandNULL }; + TestDriver::Test(L"1", L"", commands19, true, true); + + Command commands20[] = { Command::Command1, Command::Command2, Command::Command3, + Command::CommandBACK, Command::CommandBACK, Command::CommandBACK, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands20, true, true); + + Command commands21[] = { Command::Command4, Command::CommandSQRT, Command::CommandSUB, + Command::Command2, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(4) - 2 + ", commands21); + + Command commands22[] = { Command::Command0, Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(0)", commands22); + + Command commands23[] = { Command::Command1, Command::Command0, Command::Command2, Command::Command4, + Command::CommandSQRT, Command::CommandSUB, Command::Command3, Command::Command2, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"\x221A(1024) - 32 + ", commands23); + + Command commands24[] = { Command::Command2, Command::Command5, Command::Command7, + Command::CommandSQRT, Command::CommandSQRT, Command::CommandSQRT, Command::CommandNULL }; + TestDriver::Test(L"2.0009748976330773374220277351385", L"\x221A(\x221A(\x221A(257)))", commands24, true, true); + } + + // Scientific functions from the scientific calculator + void CalculatorManagerTest::CalculatorManagerTestScientific2() + { + Command commands1[] = { Command::Command1, Command::Command2, Command::CommandSQR, Command::CommandNULL }; + TestDriver::Test(L"144", L"sqr(12)", commands1, true, true); + + Command commands2[] = { Command::Command5, Command::CommandFAC, Command::CommandNULL }; + TestDriver::Test(L"120", L"fact(5)", commands2, true, true); + + Command commands3[] = { Command::Command5, Command::CommandPWR, Command::Command2, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"25", L"5 ^ 2 + ", commands3, true, true); + + Command commands4[] = { Command::Command8, Command::CommandROOT, Command::Command3, + Command::CommandMUL, Command::CommandNULL }; + TestDriver::Test(L"2", L"8 yroot 3 \x00D7 ", commands4, true, true); + + Command commands5[] = { Command::Command8, Command::CommandCUB, Command::CommandNULL }; + TestDriver::Test(L"512", L"cube(8)", commands5, true, true); +/* + Command commands6[] = { Command::Command8, Command::CommandCUB, Command::CommandCUBEROOT, Command::CommandNULL }; + TestDriver::Test(L"8", L"cuberoot(cube(8))", commands6, true, true);*/ + + Command commands7[] = { Command::Command1, Command::Command0, Command::CommandLOG, Command::CommandNULL }; + TestDriver::Test(L"1", L"log(10)", commands7, true, true); + + Command commands8[] = { Command::Command5, Command::CommandPOW10, Command::CommandNULL }; + TestDriver::Test(L"100,000", L"10^(5)", commands8, true, true); + + Command commands9[] = { Command::Command1, Command::Command0, Command::CommandLN, Command::CommandNULL }; + TestDriver::Test(L"2.3025850929940456840179914546844", L"ln(10)", commands9, true, true); + + Command commands10[] = { Command::Command1, Command::CommandSIN, Command::CommandNULL }; + TestDriver::Test(L"0.01745240643728351281941897851632", L"sin\x2080(1)", commands10, true, true); + + Command commands11[] = { Command::Command1, Command::CommandCOS, Command::CommandNULL }; + TestDriver::Test(L"0.99984769515639123915701155881391", L"cos\x2080(1)", commands11, true, true); + + Command commands12[] = { Command::Command1, Command::CommandTAN, Command::CommandNULL }; + TestDriver::Test(L"0.01745506492821758576512889521973", L"tan\x2080(1)", commands12, true, true); + + Command commands13[] = { Command::Command1, Command::CommandASIN, Command::CommandNULL }; + TestDriver::Test(L"90", L"sin\x2080\x207B\x00B9(1)", commands13, true, true); + + Command commands14[] = { Command::Command1, Command::CommandACOS, Command::CommandNULL }; + TestDriver::Test(L"0", L"cos\x2080\x207B\x00B9(1)", commands14, true, true); + + Command commands15[] = { Command::Command1, Command::CommandATAN, Command::CommandNULL }; + TestDriver::Test(L"45", L"tan\x2080\x207B\x00B9(1)", commands15, true, true); + + Command commands16[] = { Command::Command2, Command::CommandPOWE, Command::CommandNULL }; + TestDriver::Test(L"7.389056098930650227230427460575", L"e^(2)", commands16, true, true); + + Command commands17[] = { Command::Command5, Command::CommandPWR, Command::Command0, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"1", L"5 ^ 0 + ", commands17); + + Command commands18[] = { Command::Command0, Command::CommandPWR, Command::Command0, + Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"1", L"0 ^ 0 + ", commands18); + + Command commands19[] = { Command::Command2, Command::Command7, Command::CommandSIGN, Command::CommandROOT, + Command::Command3, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"-3", L"-27 yroot 3 + ", commands19, true, true); + + Command commands20[] = { Command::Command8, Command::CommandPWR, Command::CommandOPENP, + Command::Command2, Command::CommandDIV, Command::Command3, Command::CommandCLOSEP, + Command::CommandSUB, Command::Command4, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"8 ^ (2 \x00F7 3) - 4 + ", commands20, true, true); + + Command commands21[] = { Command::Command4, Command::CommandPWR, Command::CommandOPENP, + Command::Command3, Command::CommandDIV, Command::Command2, Command::CommandCLOSEP, + Command::CommandSUB, Command::Command8, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"0", L"4 ^ (3 \x00F7 2) - 8 + ", commands21, true, true); + + Command commands22[] = { Command::Command1, Command::Command0, Command::CommandPWR, Command::Command1, + Command::CommandPNT, Command::Command2, Command::Command3, Command::Command4, Command::Command5, + Command::Command6, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"17.161687912241792074207286679393", L"10 ^ 1.23456 + ", commands22, true, true); + } + + void CalculatorManagerTest::CalculatorManagerTestScientificParenthesis() + { + Command commands1[] = { Command::Command1, Command::CommandADD, Command::CommandOPENP, + Command::CommandADD, Command::Command3, Command::CommandCLOSEP, Command::CommandNULL }; + TestDriver::Test(L"3", L"1 + (0 + 3)", commands1, true, true); + + Command commands2[] = { Command::CommandOPENP, Command::CommandOPENP, Command::Command1, + Command::Command2, Command::CommandCLOSEP, Command::CommandNULL }; + TestDriver::Test(L"12", L"((12)", commands2, true, true); + + Command commands3[] = { Command::Command1, Command::Command2, Command::CommandCLOSEP, + Command::CommandCLOSEP, Command::CommandOPENP, Command::CommandNULL }; + TestDriver::Test(L"12", L"(", commands3, true, true); + + Command commands4[] = { Command::Command2, Command::CommandOPENP, Command::Command2, + Command::CommandCLOSEP, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"2", L"(2) + ", commands4, true, true); + + Command commands5[] = { Command::Command2, Command::CommandOPENP, Command::Command2, + Command::CommandCLOSEP, Command::CommandADD, Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"4", L"", commands5, true, true); + } + + void CalculatorManagerTest::CalculatorManagerTestScientificError() + { + Command commands1[] = { Command::Command1, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Cannot divide by zero", L"1 \x00F7 ", commands1, true, true); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + + Command commands2[] = { Command::Command2, Command::CommandSIGN, Command::CommandLOG, Command::CommandNULL }; + TestDriver::Test(L"Invalid input", L"log(-2)", commands2, true, true); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + + Command commands3[] = { Command::Command0, Command::CommandDIV, Command::Command0, + Command::CommandEQU, Command::CommandNULL }; + TestDriver::Test(L"Result is undefined", L"0 \x00F7 ", commands3, true, true); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + + // Do the same tests for the basic calculator + TestDriver::Test(L"Cannot divide by zero", L"1 \x00F7 ", commands1); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + TestDriver::Test(L"Invalid input", L"log(-2)", commands2); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + TestDriver::Test(L"Result is undefined", L"0 \x00F7 ", commands3); + VERIFY_IS_TRUE(m_calculatorDisplayTester->GetIsError()); + } + + // Radians and Grads Test + void CalculatorManagerTest::CalculatorManagerTestScientificModeChange() + { + Command commands1[] = { Command::CommandRAD, Command::CommandPI, Command::CommandSIN, Command::CommandNULL }; + TestDriver::Test(L"0", L"N/A", commands1, true, true); + + Command commands2[] = { Command::CommandRAD, Command::CommandPI, Command::CommandCOS, Command::CommandNULL }; + TestDriver::Test(L"-1", L"N/A", commands2, true, true); + + Command commands3[] = { Command::CommandRAD, Command::CommandPI, Command::CommandTAN, Command::CommandNULL }; + TestDriver::Test(L"0", L"N/A", commands3, true, true); + + Command commands4[] = { Command::CommandGRAD, Command::Command4, Command::Command0, Command::Command0, + Command::CommandSIN, Command::CommandNULL }; + TestDriver::Test(L"0", L"N/A", commands4, true, true); + + Command commands5[] = { Command::CommandGRAD, Command::Command4, Command::Command0, Command::Command0, + Command::CommandCOS, Command::CommandNULL }; + TestDriver::Test(L"1", L"N/A", commands5, true, true); + + Command commands6[] = { Command::CommandGRAD, Command::Command4, Command::Command0, Command::Command0, + Command::CommandTAN, Command::CommandNULL }; + TestDriver::Test(L"0", L"N/A", commands6, true, true); + } + + void CalculatorManagerTest::CalculatorManagerTestModeChange() + { + Command commands1[] = { Command::Command1, Command::Command2, Command::Command3, Command::CommandNULL }; + TestDriver::Test(L"123", L"", commands1, true, false); + + Command commands2[] = { Command::ModeScientific, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands2, true, false); + + Command commands3[] = { Command::Command1, Command::Command2, Command::Command3, Command::CommandNULL }; + TestDriver::Test(L"123", L"", commands3, true, false); + + Command commands4[] = { Command::ModeProgrammer, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands4, true, false); + + Command commands5[] = { Command::Command1, Command::Command2, Command::Command3, Command::CommandNULL }; + TestDriver::Test(L"123", L"", commands5, true, false); + + Command commands6[] = { Command::ModeScientific, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands6, true, false); + + Command commands7[] = { Command::Command6, Command::Command7, Command::CommandADD, Command::CommandNULL }; + TestDriver::Test(L"67", L"67 + ", commands7, true, false); + + Command commands8[] = { Command::ModeBasic, Command::CommandNULL }; + TestDriver::Test(L"0", L"", commands8, true, false); + } + + void CalculatorManagerTest::CalculatorManagerTestMemory() + { + Command scientificCalculatorTest52[] = { Command::Command1, Command::CommandSTORE, Command::CommandNULL }; + wstring expectedPrimaryDisplayTestScientific52(L"1"); + wstring expectedExpressionDisplayTestScientific52(L""); + + Command scientificCalculatorTest53[] = { Command::Command1, Command::CommandNULL }; + wstring expectedPrimaryDisplayTestScientific53(L"1"); + wstring expectedExpressionDisplayTestScientific53(L""); + + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + wstring resultPrimary = L""; + wstring resultExpression = L""; + + Cleanup(); + ExecuteCommands(scientificCalculatorTest52); + resultPrimary = pCalculatorDisplay->GetPrimaryDisplay(); + resultExpression = pCalculatorDisplay->GetExpression(); + VERIFY_ARE_EQUAL(expectedPrimaryDisplayTestScientific52, resultPrimary); + + Cleanup(); + ExecuteCommands(scientificCalculatorTest53); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandCLEAR); + m_calculatorManager->MemorizedNumberLoad(0); + resultPrimary = pCalculatorDisplay->GetPrimaryDisplay(); + resultExpression = pCalculatorDisplay->GetExpression(); + VERIFY_ARE_EQUAL(expectedPrimaryDisplayTestScientific52, resultPrimary); + + Cleanup(); + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandCLEAR); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandCLEAR); + m_calculatorManager->MemorizedNumberLoad(1); + resultPrimary = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(wstring(L"1"), resultPrimary); + + m_calculatorManager->MemorizedNumberLoad(0); + resultPrimary = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(wstring(L"2"), resultPrimary); + + Cleanup(); + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->SendCommand(Command::CommandSIGN); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandADD); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->SendCommand(Command::CommandEQU); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandMUL); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->MemorizeNumber(); + + vector memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + + vector expectedMemorizedNumbers; + expectedMemorizedNumbers.push_back(L"2"); + expectedMemorizedNumbers.push_back(L"1"); + expectedMemorizedNumbers.push_back(L"-1"); + + + bool isEqual = false; + if (memorizedNumbers.size() < expectedMemorizedNumbers.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), expectedMemorizedNumbers.begin()); + } + else + { + isEqual = std::equal(expectedMemorizedNumbers.begin(), expectedMemorizedNumbers.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + + m_calculatorManager->SendCommand(Command::CommandCLEAR); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->MemorizedNumberAdd(0); + m_calculatorManager->MemorizedNumberAdd(1); + m_calculatorManager->MemorizedNumberAdd(2); + + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + + expectedMemorizedNumbers.clear(); + expectedMemorizedNumbers.push_back(L"4"); + expectedMemorizedNumbers.push_back(L"3"); + expectedMemorizedNumbers.push_back(L"1"); + + if (memorizedNumbers.size() < expectedMemorizedNumbers.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), expectedMemorizedNumbers.begin()); + } + else + { + isEqual = std::equal(expectedMemorizedNumbers.begin(), expectedMemorizedNumbers.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + + m_calculatorManager->SendCommand(Command::CommandCLEAR); + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->SendCommand(Command::CommandPNT); + m_calculatorManager->SendCommand(Command::Command5); + + m_calculatorManager->MemorizedNumberSubtract(0); + m_calculatorManager->MemorizedNumberSubtract(1); + m_calculatorManager->MemorizedNumberSubtract(2); + + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + + expectedMemorizedNumbers.clear(); + expectedMemorizedNumbers.push_back(L"2.5"); + expectedMemorizedNumbers.push_back(L"1.5"); + expectedMemorizedNumbers.push_back(L"-0.5"); + + if (memorizedNumbers.size() < expectedMemorizedNumbers.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), expectedMemorizedNumbers.begin()); + } + else + { + isEqual = std::equal(expectedMemorizedNumbers.begin(), expectedMemorizedNumbers.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + + // Memorizing 101 numbers, which exceeds the limit. + Cleanup(); + for (int i = 0; i < 101; i++) + { + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->MemorizeNumber(); + } + + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + VERIFY_ARE_EQUAL((size_t)100, memorizedNumbers.size()); + + // Memorizing new number, which should show up at the top of the memory + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->MemorizeNumber(); + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + VERIFY_ARE_EQUAL(wstring(L"2"), memorizedNumbers.at(0)); + + // Test for trying to memorize invalid value + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->SendCommand(Command::CommandSIGN); + m_calculatorManager->SendCommand(Command::CommandSQRT); + m_calculatorManager->MemorizeNumber(); + } + + void CalculatorManagerTest::CalculatorManagerTestSerializeMemory() + { + Cleanup(); + + Command commands[] = { Command::Command1, Command::CommandNULL }; + ExecuteCommands(commands); + + for (int i = 0; i < 110; i++) + { + m_calculatorManager->MemorizeNumber(); + } + + VerifyPersistence(); + } + + void CalculatorManagerTest::CalculatorManagerTestSerializeDegreeMode() + { + Cleanup(); + + Command commands[] = { Command::Command1, Command::CommandRAD, Command::CommandSIN, Command::CommandADD, + Command::Command1, Command::CommandGRAD, Command::CommandCOS, Command::CommandADD, + Command::Command1, Command::CommandDEG, Command::CommandTAN, Command::CommandADD, + Command::CommandNULL }; + ExecuteCommands(commands); + + VerifyPersistence(); + } + + // 1 + 2 then serialize and deserialze 3 times + // Check if the values are persisted correctly + void CalculatorManagerTest::CalculatorManagerTestSerializeMultiple() + { + Cleanup(); + + Command commands[] = { Command::Command1, Command::CommandADD, Command::Command2, Command::CommandNULL }; + ExecuteCommands(commands); + + VerifyPersistence(); + VerifyPersistence(); + VerifyPersistence(); + } + + // Calculate 1/3 then serialize and deserialize + // Multiply by 3 and check if the result is 1 not 0.999999999999999999... + void CalculatorManagerTest::CalculatorManagerTestSerializePrecision() + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + wstring resultPrimary = L""; + wstring resultExpression = L""; + Cleanup(); + + Command commands[] = { Command::Command1, Command::CommandDIV, Command::Command3, Command::CommandEQU, Command::CommandNULL }; + ExecuteCommands(commands); + + SerialzieAndDeSerialize(); + + Command commands2[] = { Command::CommandMUL, Command::Command3, Command::CommandEQU, Command::CommandNULL }; + ExecuteCommands(commands2); + + VERIFY_ARE_EQUAL(StringReference(L"1"), ref new String(pCalculatorDisplay->GetPrimaryDisplay().c_str())); + } + + void CalculatorManagerTest::CalculatorManagerTestSerialize() + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + wstring resultPrimary = L""; + wstring resultExpression = L""; + Cleanup(); + + m_calculatorManager->SendCommand(Command::ModeScientific); + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->SendCommand(Command::CommandADD); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->SendCommand(Command::CommandMUL); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::CommandEQU); + m_calculatorManager->MemorizeNumber(); + + vector memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + wstring primaryDisplay = pCalculatorDisplay->GetPrimaryDisplay(); + wstring expressionDisplay = pCalculatorDisplay->GetExpression(); + + SerialzieAndDeSerialize(); + + vector memorizedNumbersAfterDeSerialize = pCalculatorDisplay->GetMemorizedNumbers(); + wstring primaryDisplayAfterDeSerialize = pCalculatorDisplay->GetPrimaryDisplay(); + wstring expressionDisplayAfterDeSerialize = pCalculatorDisplay->GetExpression(); + + VERIFY_ARE_EQUAL(primaryDisplay, primaryDisplayAfterDeSerialize); + VERIFY_ARE_EQUAL(expressionDisplay, expressionDisplayAfterDeSerialize); + + bool isEqual = false; + + if (memorizedNumbers.size() < memorizedNumbersAfterDeSerialize.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), memorizedNumbersAfterDeSerialize.begin()); + } + else + { + isEqual = std::equal(memorizedNumbersAfterDeSerialize.begin(), memorizedNumbersAfterDeSerialize.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + + m_calculatorManager->SendCommand(Command::CommandGRAD); + m_calculatorManager->SendCommand(Command::Command5); + m_calculatorManager->SendCommand(Command::Command0); + m_calculatorManager->SendCommand(Command::CommandSIGN); + m_calculatorManager->SendCommand(Command::CommandMUL); + + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + primaryDisplay = pCalculatorDisplay->GetPrimaryDisplay(); + expressionDisplay = pCalculatorDisplay->GetExpression(); + + SerialzieAndDeSerialize(); + + memorizedNumbersAfterDeSerialize = pCalculatorDisplay->GetMemorizedNumbers(); + primaryDisplayAfterDeSerialize = pCalculatorDisplay->GetPrimaryDisplay(); + expressionDisplayAfterDeSerialize = pCalculatorDisplay->GetExpression(); + + VERIFY_ARE_EQUAL(primaryDisplay, primaryDisplayAfterDeSerialize); + VERIFY_ARE_EQUAL(expressionDisplay, expressionDisplayAfterDeSerialize); + + isEqual = false; + if (memorizedNumbers.size() < memorizedNumbersAfterDeSerialize.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), memorizedNumbersAfterDeSerialize.begin()); + } + else + { + isEqual = std::equal(memorizedNumbersAfterDeSerialize.begin(), memorizedNumbersAfterDeSerialize.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->SendCommand(Command::Command3); + m_calculatorManager->SendCommand(Command::CommandPNT); + m_calculatorManager->SendCommand(Command::Command8); + m_calculatorManager->SendCommand(Command::CommandSIGN); + m_calculatorManager->MemorizeNumber(); + m_calculatorManager->SendCommand(Command::Command2); + m_calculatorManager->SendCommand(Command::Command3); + m_calculatorManager->MemorizedNumberAdd(0); + m_calculatorManager->SendCommand(Command::CommandCENTR); + m_calculatorManager->SendCommand(Command::Command3); + m_calculatorManager->SendCommand(Command::Command1); + m_calculatorManager->SendCommand(Command::CommandSIN); + m_calculatorManager->MemorizedNumberSubtract(2); + m_calculatorManager->MemorizedNumberLoad(2); + + memorizedNumbers = pCalculatorDisplay->GetMemorizedNumbers(); + primaryDisplay = pCalculatorDisplay->GetPrimaryDisplay(); + expressionDisplay = pCalculatorDisplay->GetExpression(); + + SerialzieAndDeSerialize(); + + memorizedNumbersAfterDeSerialize = pCalculatorDisplay->GetMemorizedNumbers(); + primaryDisplayAfterDeSerialize = pCalculatorDisplay->GetPrimaryDisplay(); + expressionDisplayAfterDeSerialize = pCalculatorDisplay->GetExpression(); + + VERIFY_ARE_EQUAL(primaryDisplay, primaryDisplayAfterDeSerialize); + VERIFY_ARE_EQUAL(expressionDisplay, expressionDisplayAfterDeSerialize); + + isEqual = false; + if (memorizedNumbers.size() < memorizedNumbersAfterDeSerialize.size()) + { + isEqual = std::equal(memorizedNumbers.begin(), memorizedNumbers.end(), memorizedNumbersAfterDeSerialize.begin()); + } + else + { + isEqual = std::equal(memorizedNumbersAfterDeSerialize.begin(), memorizedNumbersAfterDeSerialize.end(), memorizedNumbers.begin()); + } + VERIFY_IS_TRUE(isEqual); + } + + // Send 12345678910111213 and verify MaxDigitsReached + void CalculatorManagerTest::CalculatorManagerTestMaxDigitsReached() + { + TestMaxDigitsReachedScenario(L"1,234,567,891,011,1213"); + } + + void CalculatorManagerTest::CalculatorManagerTestMaxDigitsReached_LeadingDecimal() + { + TestMaxDigitsReachedScenario(L"0.12345678910111213"); + } + + void CalculatorManagerTest::CalculatorManagerTestMaxDigitsReached_TrailingDecimal() + { + TestMaxDigitsReachedScenario(L"123,456,789,101,112.13"); + } + + void CalculatorManagerTest::CalculatorManagerTestBinaryOperatorReceived() + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + + VERIFY_ARE_EQUAL(0, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + + ExecuteCommands({ + Command::Command1, + Command::CommandADD + }); + + wstring display = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(wstring(L"1"), display); + + // Verify BinaryOperatorReceived + VERIFY_ARE_EQUAL(1, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + } + + void CalculatorManagerTest::CalculatorManagerTestBinaryOperatorReceived_Multiple() + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + + VERIFY_ARE_EQUAL(0, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + + ExecuteCommands({ + Command::Command1, + Command::CommandADD, + Command::CommandSUB, + Command::CommandMUL + }); + + wstring display = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(wstring(L"1"), display); + + // Verify BinaryOperatorReceived + VERIFY_ARE_EQUAL(3, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + } + + void CalculatorManagerTest::CalculatorManagerTestBinaryOperatorReceived_LongInput() + { + CalculatorManagerDisplayTester* pCalculatorDisplay = (CalculatorManagerDisplayTester *)m_calculatorDisplayTester.get(); + + VERIFY_ARE_EQUAL(0, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + + ExecuteCommands({ + Command::Command1, + Command::CommandADD, + Command::Command2, + Command::CommandMUL, + Command::Command1, + Command::Command0, + Command::CommandSUB, + Command::Command5, + Command::CommandDIV, + Command::Command5, + Command::CommandEQU + }); + + wstring display = pCalculatorDisplay->GetPrimaryDisplay(); + VERIFY_ARE_EQUAL(wstring(L"5"), display); + + // Verify BinaryOperatorReceived + VERIFY_ARE_EQUAL(4, pCalculatorDisplay->GetBinaryOperatorReceivedCallCount()); + } + +} /* namespace CalculationManagerUnitTests */ + diff --git a/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj b/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj new file mode 100644 index 00000000..e92bdfaf --- /dev/null +++ b/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj @@ -0,0 +1,365 @@ + + + + + + + {E527A1F4-6B63-4DD0-B540-B8C06CFC3AFE} + CalculatorUnitTests + en-US + 15.0 + true + Windows Store + 10.0.17763.0 + 10.0.17134.0 + 10.0 + CalculatorUnitTests + + true + true + + true + + + + + x64 + + + true + true + false + + + true + true + false + + + true + false + + + true + false + + + true + true + false + + + true + false + + + true + true + false + + + true + false + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + Application + true + v141 + + + Application + true + v141 + + + Application + true + v141 + + + Application + true + v141 + + + Application + false + true + v141 + + + Application + false + true + v141 + + + Application + false + true + v141 + + + Application + false + true + v141 + + + + + + + + + + + + + + + + + + + + + + + + + + + + TemporaryKey.pfx + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + /bigobj /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + 4453;28204 + $(SolutionDir)..\src\;$(SolutionDir)..\src\CalcManager;$(SolutionDir)..\src\CalcViewModel;%(AdditionalIncludeDirectories) + + + %(AdditionalLibraryDirectories) + + + + + + + + + + + + + + + + + + + + + + UnitTestApp.xaml + + + + + Designer + + + + + Designer + + + + + + + + + + + + + + + + + + + + + UnitTestApp.xaml + + + Create + Create + Create + Create + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + {311e866d-8b93-4609-a691-265941fee101} + + + {90e9761d-9262-4773-942d-caeae75d7140} + + + + + + + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\x86\Microsoft.VCLibs.x86.Debug.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\x86\Microsoft.VCLibs.x86.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\x64\Microsoft.VCLibs.x64.Debug.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\x64\Microsoft.VCLibs.x64.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\ARM\Microsoft.VCLibs.arm.Debug.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\ARM64\Microsoft.VCLibs.arm64.Debug.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\ARM\Microsoft.VCLibs.arm.14.00.appx; + $(UniversalTestCustomMacros)AppxPackageVCLibsDependency=$(AppxPackageTestDir)Dependencies\ARM64\Microsoft.VCLibs.arm64.14.00.appx; + $(UniversalTestCustomMacros)AppxPackagePublicKeyFile=$(AppxPackagePublicKeyFile);AppxPackageOutput=$(AppxPackageOutput); + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj.filters b/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj.filters new file mode 100644 index 00000000..ac251d6c --- /dev/null +++ b/internal/CalculatorUnitTests/CalculatorUnitTests.vcxproj.filters @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/CopyPasteManagerTest.cpp b/internal/CalculatorUnitTests/CopyPasteManagerTest.cpp new file mode 100644 index 00000000..f4e9329b --- /dev/null +++ b/internal/CalculatorUnitTests/CopyPasteManagerTest.cpp @@ -0,0 +1,602 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +#include "CalcViewModel\StandardCalculatorViewModel.h" +#include "CalcViewModel\Common\CopyPasteManager.h" + +using namespace CalculationManager; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace std; +using namespace WEX::Logging; +using namespace Windows::ApplicationModel::DataTransfer; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Globalization; +using namespace Windows::Storage; + +namespace CalculatorUnitTests +{ + extern void ChangeMode(StandardCalculatorViewModel^ viewModel, int mode); + +#define ASSERT_POSITIVE_TESTCASES(func, dataSet) \ +{\ + int size = sizeof(dataSet)/sizeof(*dataSet);\ + while(--size)\ + {\ + VERIFY_ARE_EQUAL(func(dataSet[size]), dataSet[size]);\ + }\ +} + +#define ASSERT_NEGATIVE_TESTCASES(func, dataSet) \ +{\ + int size = sizeof(dataSet)/sizeof(*dataSet);;\ + while(--size)\ + {\ + VERIFY_ARE_EQUAL(func(dataSet[size]), StringReference(L"NoOp"));\ + }\ +} + +// returns a iterator from end +#define START_LOOP(dataSet)\ +{\ + int size = sizeof(dataSet)/sizeof(*dataSet);\ + while(--size)\ + { + +#define END_LOOP\ + }\ +} + + class CopyPasteManagerTest + { + public: + TEST_CLASS(CopyPasteManagerTest); + + TEST_METHOD(FunctionalCopyPasteTest); + TEST_METHOD(ValidateStandardPasteExpressionTest); + TEST_METHOD(ValidateScientificPasteExpressionTest); + TEST_METHOD(ValidateProgrammerDecPasteExpressionTest); + TEST_METHOD(ValidateProgrammerOctPasteExpressionTest); + TEST_METHOD(ValidateProgrammerHexPasteExpressionTest); + TEST_METHOD(ValidateProgrammerBinPasteExpressionTest); + TEST_METHOD(ValidateConverterPasteExpressionTest); + + TEST_METHOD(ValidatePasteExpressionErrorStates) + { + wstring exp_TooLong = L""; + for (int i = 0; i < m_CopyPasteManager.MaxPasteableLength / 8; i++) + { + exp_TooLong += L"-1234567"; + } + VERIFY_ARE_EQUAL(m_CopyPasteManager.ValidatePasteExpression(StringReference(exp_TooLong.c_str()), ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), StringReference(exp_TooLong.c_str()), L"Verify ValidatePasteExpression handles expressions up to max length"); + exp_TooLong += L"1"; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ValidatePasteExpression(StringReference(exp_TooLong.c_str()), ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), StringReference(L"NoOp"), L"Verify ValidatePasteExpression returns NoOp for strings over max length"); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ValidatePasteExpression(StringReference(L""), ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), StringReference(L"NoOp"), L"Verify empty string is invalid"); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ValidatePasteExpression(StringReference(L"123e456"), ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), StringReference(L"NoOp"), L"Verify pasting unsupported strings for the current mode is invalid"); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ValidatePasteExpression(StringReference(L"123"), ViewMode::None, CategoryGroupType::None, -1, -1), StringReference(L"NoOp"), L"Verify pasting without a ViewMode or Category is invalid"); + }; + + TEST_METHOD(ValidateExtractOperands) + { + vector results = {}; + + vector oneOperand = { L"123456" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123456", ViewMode::Standard), oneOperand); + oneOperand = { L"123^456" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123^456", ViewMode::Standard), oneOperand); + + vector twoOperands = { L"123", L"456" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123+456", ViewMode::Standard), twoOperands); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123-456", ViewMode::Standard), twoOperands); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123*456", ViewMode::Standard), twoOperands); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123/456", ViewMode::Standard), twoOperands); + + vector expOperand = { L"123e456" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123e456", ViewMode::Standard), expOperand); + expOperand = { L"123e4567" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"123e4567", ViewMode::Standard), expOperand); + + vector twoOperandsParens = { L"((45)", L"(-30))" }; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"((45)+(-30))", ViewMode::Scientific), twoOperandsParens); + }; + + TEST_METHOD(ValidateExtractOperandsErrors) + { + wstring exp_OperandLimit = L""; + for (int i = 0; i < m_CopyPasteManager.MaxOperandCount; i++) + { + exp_OperandLimit += L"+1"; + } + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(exp_OperandLimit, ViewMode::Standard).size(), 100, L"Verify ExtractOperands handles up to MaxOperandCount operands"); + + exp_OperandLimit += L"+1"; + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(exp_OperandLimit, ViewMode::Standard).size(), 0, L"Verify ExtractOperands returns empty vector on too many operands"); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"12e9999", ViewMode::Standard).size(), 1, L"Verify ExtractOperands handles up to 4 digit exponents"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ExtractOperands(L"12e10000", ViewMode::Standard).size(), 0, L"Verify ExtractOperands returns empty vector when the exponent is too long"); + }; + + TEST_METHOD(ValidateExpressionRegExMatch) + { + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{}, ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), L"Verify empty list of operands returns false."); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123" }, ViewMode::None, CategoryGroupType::Calculator, -1, -1), L"Verify invalid ViewMode/CategoryGroups return false."); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123" }, ViewMode::Currency, CategoryGroupType::None, -1, -1), L"Verify invalid ViewMode/CategoryGroups return false."); + + Log::Comment(L"Verify operand lengths > max return false."); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"12345678901234567" }, ViewMode::Standard, CategoryGroupType::Calculator, -1, -1)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123456789012345678901234567890123" }, ViewMode::Scientific, CategoryGroupType::Calculator, -1, -1)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"12345678901234567" }, ViewMode::None, CategoryGroupType::Converter, -1, -1)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"11111111111111111" }, ViewMode::Programmer, CategoryGroupType::Calculator, HexBase, QwordType)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"12345678901234567890" }, ViewMode::Programmer, CategoryGroupType::Calculator, DecBase, QwordType)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"11111111111111111111111" }, ViewMode::Programmer, CategoryGroupType::Calculator, OctBase, QwordType)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"10000000000000000000000000000000000000000000000000000000000000000" }, ViewMode::Programmer, CategoryGroupType::Calculator, BinBase, QwordType)); + + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"9223372036854775808" }, ViewMode::Programmer, CategoryGroupType::Calculator, DecBase, QwordType), L"Verify operand values > max return false."); + + VERIFY_IS_TRUE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"((((((((((((((((((((123))))))))))))))))))))" }, ViewMode::Scientific, CategoryGroupType::Calculator, -1, -1), L"Verify sanitized operand is detected as within max length."); + VERIFY_IS_TRUE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"9223372036854775807" }, ViewMode::Programmer, CategoryGroupType::Calculator, DecBase, QwordType), L"Verify operand values == max return true."); + + Log::Comment(L"Verify all operands must match patterns."); + VERIFY_IS_TRUE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123", L"456" }, ViewMode::Standard, CategoryGroupType::Calculator, -1, -1)); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123", L"1e23" }, ViewMode::Standard, CategoryGroupType::Calculator, -1, -1)); + + VERIFY_IS_TRUE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"1.23e+456" }, ViewMode::Scientific, CategoryGroupType::Calculator, -1, -1), L"Verify operand only needs to match one pattern."); + + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123", L"12345678901234567" }, ViewMode::Standard, CategoryGroupType::Calculator, -1, -1), L"Verify all operands must be within maxlength"); + VERIFY_IS_FALSE(m_CopyPasteManager.ExpressionRegExMatch(vector{ L"123", L"9223372036854775808" }, ViewMode::Programmer, CategoryGroupType::Calculator, DecBase, QwordType), L"Verify all operand must be within max value."); + }; + + TEST_METHOD(ValidateGetMaxOperandLengthAndValue) + { + pair standardModeMaximums = make_pair(m_CopyPasteManager.MaxStandardOperandLength, 0); + pair scientificModeMaximums = make_pair(m_CopyPasteManager.MaxScientificOperandLength, 0); + pair converterModeMaximums = make_pair(m_CopyPasteManager.MaxConverterInputLength, 0); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Standard, CategoryGroupType::None, -1, -1), standardModeMaximums, L"Verify Standard mode maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Scientific, CategoryGroupType::None, -1, -1), scientificModeMaximums, L"Verify Scientific mode maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::None, CategoryGroupType::Converter, -1, -1), converterModeMaximums, L"Verify Converter mode maximum values"); + + unsigned long long int ullQwordMax = UINT64_MAX; + unsigned long long int ullDwordMax = UINT32_MAX; + unsigned long long int ullWordMax = UINT16_MAX; + unsigned long long int ullByteMax = UINT8_MAX; + Log::Comment(L"Verify Programmer Mode HexBase maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, HexBase, QwordType), make_pair((size_t)16u, ullQwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, HexBase, DwordType), make_pair((size_t)8u, ullDwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, HexBase, WordType), make_pair((size_t)4u, ullWordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, HexBase, ByteType), make_pair((size_t)2u, ullByteMax)); + + Log::Comment(L"Verify Programmer Mode DecBase maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, DecBase, QwordType), make_pair((size_t)19u, ullQwordMax >> 1)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, DecBase, DwordType), make_pair((size_t)10u, ullDwordMax >> 1)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, DecBase, WordType), make_pair((size_t)5u, ullWordMax >> 1)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, DecBase, ByteType), make_pair((size_t)3u, ullByteMax >> 1)); + + Log::Comment(L"Verify Programmer Mode OctBase maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, OctBase, QwordType), make_pair((size_t)22u, ullQwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, OctBase, DwordType), make_pair((size_t)11u, ullDwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, OctBase, WordType), make_pair((size_t)6u, ullWordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, OctBase, ByteType), make_pair((size_t)3u, ullByteMax)); + + Log::Comment(L"Verify Programmer Mode BinBase maximum values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, BinBase, QwordType), make_pair((size_t)64u, ullQwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, BinBase, DwordType), make_pair((size_t)32u, ullDwordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, BinBase, WordType), make_pair((size_t)16u, ullWordMax)); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::Programmer, CategoryGroupType::None, BinBase, ByteType), make_pair((size_t)8u, ullByteMax)); + + Log::Comment(L"Verify invalid ViewModes/Categories return 0 for max values"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.GetMaxOperandLengthAndValue(ViewMode::None, CategoryGroupType::None, -1, -1), make_pair((size_t)0u, 0ull)); + }; + + TEST_METHOD(ValidateSanitizeOperand) + { + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"((1234"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"1234))"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"-1234"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"12-34"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"((((1234))))"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"1'2'3'4"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"'''''1234''''"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"1_2_3_4"), L"1234"); + VERIFY_ARE_EQUAL(m_CopyPasteManager.SanitizeOperand(L"______1234___"), L"1234"); + }; + + TEST_METHOD(ValidateTryOperandToULL) + { + unsigned long long int result = 0; + + Log::Comment(L"Verify TryOperandToULL HexBase conversion"); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1234", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0x1234ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"FF", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"FFFFFFFFFFFFFFFF", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"0xFFFFFFFFFFFFFFFF", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"0XFFFFFFFFFFFFFFFF", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"0X0FFFFFFFFFFFFFFFF", HexBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + + Log::Comment(L"Verify TryOperandToULL DecBase conversion"); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1234", DecBase, result)); + VERIFY_ARE_EQUAL(result, 1234ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"18446744073709551615", DecBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"018446744073709551615", DecBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + + Log::Comment(L"Verify TryOperandToULL OctBase conversion"); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"777", OctBase, result)); + VERIFY_ARE_EQUAL(result, 0777ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"0777", OctBase, result)); + VERIFY_ARE_EQUAL(result, 0777ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1777777777777777777777", OctBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"01777777777777777777777", OctBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + + Log::Comment(L"Verify TryOperandToULL BinBase conversion"); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1111", BinBase, result)); + VERIFY_ARE_EQUAL(result, 0b1111ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"0010", BinBase, result)); + VERIFY_ARE_EQUAL(result, 0b10ull); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1111111111111111111111111111111111111111111111111111111111111111", BinBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"01111111111111111111111111111111111111111111111111111111111111111", BinBase, result)); + VERIFY_ARE_EQUAL(result, 0xFFFF'FFFF'FFFF'FFFF); + + Log::Comment(L"Verify TryOperandToULL invalid numberBase defaults to DecBase"); + VERIFY_IS_TRUE(m_CopyPasteManager.TryOperandToULL(L"1234", 128, result)); + VERIFY_ARE_EQUAL(result, 1234ull); + + Log::Comment(L"Verify TryOperandToULL returns false when input is invalid or strtoull throws exceptions"); + // Max values + 1 + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"0xFFFFFFFFFFFFFFFFF1", HexBase, result)); + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"18446744073709551616", DecBase, result)); + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"2000000000000000000000", OctBase, result)); + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"11111111111111111111111111111111111111111111111111111111111111111", BinBase, result)); + // Invalid values/characters + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"-1", DecBase, result)); + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"5555", BinBase, result)); + VERIFY_IS_FALSE(m_CopyPasteManager.TryOperandToULL(L"xyz", BinBase, result)); + + }; + + TEST_METHOD(ValidateStandardScientificOperandLength) + { + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L""), 0); + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L"0.2"), 1); + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L"1.2"), 2); + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L"0."), 0); + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L"12345"), 5); + VERIFY_ARE_EQUAL(m_CopyPasteManager.StandardScientificOperandLength(L"-12345"), 6); + + }; + + TEST_METHOD(ValidateProgrammerOperandLength) + { + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"1001", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"1001b", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"1001B", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0b1001", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0B1001", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0y1001", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0Y1001", BinBase), 4); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0b", BinBase), 1); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"123456", OctBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0t123456", OctBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0T123456", OctBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0o123456", OctBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0O123456", OctBase), 6); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"", DecBase), 0); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"-", DecBase), 0); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"12345", DecBase), 5); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"-12345", DecBase), 5); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0n12345", DecBase), 5); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0N12345", DecBase), 5); + + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"123ABC", HexBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0x123ABC", HexBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"0X123ABC", HexBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"123ABCh", HexBase), 6); + VERIFY_ARE_EQUAL(m_CopyPasteManager.ProgrammerOperandLength(L"123ABCH", HexBase), 6); + }; + + private: + CopyPasteManager m_CopyPasteManager; + String^ ValidateStandardPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Standard, -1/*number base*/, -1/*bitlength Type*/); + } + + String^ ValidateScientificPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Scientific, -1/*number base*/, -1/*bitlength Type*/); + } + + String^ ValidateConverterPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::None, CategoryGroupType::Converter, -1/*number base*/, -1/*bitlength Type*/); + } + + String^ ValidateProgrammerHexQwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, HexBase/*number base*/, QwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerHexDwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, HexBase/*number base*/, DwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerHexWordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, HexBase/*number base*/, WordType/*bitlength Type*/); + } + + String^ ValidateProgrammerHexBytePasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, HexBase/*number base*/, ByteType/*bitlength Type*/); + } + + String^ ValidateProgrammerDecQwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, DecBase/*number base*/, QwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerDecDwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, DecBase/*number base*/, DwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerDecWordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, DecBase/*number base*/, WordType/*bitlength Type*/); + } + + String^ ValidateProgrammerDecBytePasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, DecBase/*number base*/, ByteType/*bitlength Type*/); + } + + String^ ValidateProgrammerOctQwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, OctBase/*number base*/, QwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerOctDwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, OctBase/*number base*/, DwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerOctWordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, OctBase/*number base*/, WordType/*bitlength Type*/); + } + + String^ ValidateProgrammerOctBytePasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, OctBase/*number base*/, ByteType/*bitlength Type*/); + } + + String^ ValidateProgrammerBinQwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, BinBase/*number base*/, QwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerBinDwordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, BinBase/*number base*/, DwordType/*bitlength Type*/); + } + + String^ ValidateProgrammerBinWordPasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, BinBase/*number base*/, WordType/*bitlength Type*/); + } + + String^ ValidateProgrammerBinBytePasteExpression(_In_ String^ pastedText) + { + return m_CopyPasteManager.ValidatePasteExpression(pastedText, ViewMode::Programmer, BinBase/*number base*/, ByteType/*bitlength Type*/); + } + + + }; + + /************************************* + standard: + can paste simple numbers / expressions not exponential numbers / expressions + + scientific : + can paste exponential numbers / expressions too + + programmer : + can paste specific numbers / expressions based on radixes.Hex numbers such 13abe is allowed when radix is set to hex, but not allowed otherwise. + + converter : + can paste simple numbers not expressions + + List of test cases: + 1. simple unsigned number + 2. negative number + 3. exponential number with positive exp + 4. exponential number with negative exp + 5. exponential number with unsigned exp + 6. exponential number with very large(larger than 4 digit) exp + 7. expression involving simple numbers + 8. expression involving exponential numbers + 9. number with random text like xyz + 10. hex numbers + 11. binary numbers + 12. octal numbers + 13. very large number + 14. number with some escape characters in b/w like ",/. \n\r ", '" + 15. expression involving sin, cos or other mathematic functions + 16. expression having more than one operator in b/w operands + 17. expression involving open and close parenthesis (, ) + + ****************************************/ + + void CopyPasteManagerTest::FunctionalCopyPasteTest() + { + // Doesn't have test where converter is involved. Will add such a test later. + StandardCalculatorViewModel^ scvm = ref new StandardCalculatorViewModel(); + scvm->IsStandard = true; + String^ input[] = { L"123", L"12345", L"123+456", L"1,234", L"1 2 3", L"\n\r1,234\n", L"\n 1+\n2 ", L"1\"2" }; + + START_LOOP(input) + // paste number in standard mode and then validate the pastability of displayed number for other modes + scvm->OnPaste(input[size], ViewMode::Standard); + VERIFY_ARE_EQUAL(ValidateStandardPasteExpression(scvm->DisplayValue), scvm->DisplayValue); + VERIFY_ARE_EQUAL(ValidateScientificPasteExpression(scvm->DisplayValue), scvm->DisplayValue); + VERIFY_ARE_EQUAL(ValidateProgrammerHexQwordPasteExpression(scvm->DisplayValue), scvm->DisplayValue); + END_LOOP + } + + + void CopyPasteManagerTest::ValidateStandardPasteExpressionTest() + { + String^ positiveInput[] = { L"123", L"+123", L"-133", L"12345.", L"+12.34", L"12.345", L"012.034", L"-23.032", L"-.123", L".1234", L"012.012", L"123+456", L"123+-234", L"123*-345", L"123*4*-3", L"123*+4*-3", L"1,234", L"1 2 3", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"1234567891234567"/*boundary condition <=16 digits*/, L"2+2=", L"2+2= " }; + String^ negativeInput[] = { L"(123)+(456)", L"1.2e23"/*unsigned exponent*/, L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"12345678912345678"/*boundary condition: greater than 16 digits*/, L"SIN(2)", L"2+2==", L"2=+2" }; + + ASSERT_POSITIVE_TESTCASES(ValidateStandardPasteExpression, positiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateStandardPasteExpression, negativeInput); + } + + void CopyPasteManagerTest::ValidateScientificPasteExpressionTest() + { + String^ positiveInput[] = { L"123", L"+123", L"-133", L"123+456", L"12345e+023", L"1,234", L"1.23", L"-.123", L".1234", L"012.012", L"123+-234", L"123*-345", L"123*4*-3", L"123*+4*-3", L"1 2 3", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"1.2e+023", L"12345e-23", L"(123)+(456)", L"12345678912345678123456789012345", L"(123)+(456)=", L"2+2= " }; + String^ negativeInput[] = { L"1.2e23"/*unsigned exponent*/, L"abcdef", L"xyz", L"ABab", L"e+234", L"123456789123456781234567890123456"/*boundary condition: greater than 32 digits*/, L"SIN(2)", L"2+2==", L"2=+2" }; + + ASSERT_POSITIVE_TESTCASES(ValidateScientificPasteExpression, positiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateScientificPasteExpression, negativeInput); + } + + void CopyPasteManagerTest::ValidateProgrammerHexPasteExpressionTest() + { + String^ qwordPositiveInput[] = { L"123", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"12345e-23"/*note: here is considered as E of hex*/, L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"e+234", L"1\"2", L"(123)+(456)", L"abcdef", L"ABab", L"ABCDF21abc41a"/*within boundary*/, L"0x1234", L"0xab12", L"0X1234", L"AB12h", L"BC34H", L"1234u", L"1234ul", L"1234ULL", L"2+2=", L"2+2= " }; + String^ qwordNegativeInput[] = { L"+123", L"1.23"/*floating number*/, L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e+023", L"1.2e23"/*unsigned exponent*/, L"xyz", L"ABCDEF21abc41abc7"/*outside boundary of 16 digitis*/, L"SIN(2)", L"123+-234", L"1234x", L"A0x1234", L"0xx1234", L"1234uu", L"1234ulll", L"2+2==", L"2=+2" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerHexQwordPasteExpression, qwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerHexQwordPasteExpression, qwordNegativeInput); + + String^ dwordPositiveInput[] = { L"123", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"12345e-23"/*note: here is considered as E of hex*/, L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"e+234", L"1\"2", L"(123)+(456)", L"abcdef", L"ABab", L"ABCD123a"/*within boundary*/, L"0x1234", L"0xab12", L"0X1234", L"AB12h", L"BC34H", L"1234u", L"1234ul", L"1234ULL" }; + String^ dwordNegativeInput[] = { L"+123", L"1.23"/*floating number*/, L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e+023", L"1.2e23"/*unsigned exponent*/, L"xyz", L"ABCD123ab"/*outside boundary of 8 digitis*/, L"SIN(2)", L"123+-234", L"1234x", L"A0x1234", L"0xx1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerHexDwordPasteExpression, dwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerHexDwordPasteExpression, dwordNegativeInput); + + String^ wordPositiveInput[] = { L"123", L"13+456", L"1,34", L"12 3", L"1'2'3'4", L"1_2_3_4", L"15e-23"/*note: here is considered as E of hex*/, L"\r1", L"\n\r1,4", L"\n1,4\n", L"\f\n1+2\t\r\v", L"\n 1+\n2 ", L"e+24", L"1\"2", L"(23)+(4)", L"aef", L"ABab", L"A1a3"/*within boundary*/, L"0x1234", L"0xab12", L"0X1234", L"AB12h", L"BC34H", L"1234u", L"1234ul", L"1234ULL" }; + String^ wordNegativeInput[] = { L"+123", L"1.23"/*floating number*/, L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e+023", L"1.2e23"/*unsigned exponent*/, L"xyz", L"A1a3b"/*outside boundary of 4 digitis*/, L"SIN(2)", L"123+-234", L"1234x", L"A0x1234", L"0xx1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerHexWordPasteExpression, wordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerHexWordPasteExpression, wordNegativeInput); + + String^ bytePositiveInput[] = { L"13", L"13+6", L"1,4", L"2 3", L"1'2", L"1_2", L"5e-3"/*note: here is considered as E of hex*/, L"\r1", L"a", L"ab", L"A1"/*within boundary*/, L"0x12", L"0xab", L"0X12", L"A9h", L"B8H", L"12u", L"12ul", L"12ULL" }; + String^ byteNegativeInput[] = { L"+3", L"1.2"/*floating number*/, L"1''2", L"'12", L"12'", L"1__2", L"_12", L"12_", L"-3", L"1.1e+02", L"1.2e3"/*unsigned exponent*/, L"xz", L"A3a"/*outside boundary of 2 digitis*/, L"SIN(2)", L"13+-23", L"12x", L"A0x1", L"0xx12", L"12uu", L"12ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerHexBytePasteExpression, bytePositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerHexBytePasteExpression, byteNegativeInput); + } + + void CopyPasteManagerTest::ValidateProgrammerDecPasteExpressionTest() + { + String^ qwordPositiveInput[] = { L"123", L"+123", L"-133", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"(123)+(456)", L"123+-234", L"123*-345", L"123*4*-3", L"123*+4*-3", L"9223372036854775807", L"-9223372036854775807"/*boundary condition: max/min allowed number*/, L"0n1234", L"0N1234", L"1234u", L"1234ul", L"1234ULL", L"2+2=", L"2+2= " }; + String^ qwordNegativeInput[] = { L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"9223372036854775809"/*boundary condition: greater than max allowed number 9223372036854775807*/, L"SIN(2)", L"-0n123", L"0nn1234", L"1234uu", L"1234ulll", L"2+2==", L"2=+2" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerDecQwordPasteExpression, qwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerDecQwordPasteExpression, qwordNegativeInput); + + String^ dwordPositiveInput[] = { L"123", L"+123", L"-133", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"(123)+(456)", L"123+-234", L"123*-345", L"123*4*-3", L"123*+4*-3", L"2147483647", L"-2147483647"/*boundary condition: max/min allowed number*/, L"0n1234", L"0N1234", L"1234u", L"1234ul", L"1234ULL" }; + String^ dwordNegativeInput[] = { L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"2147483649"/*boundary condition: greater than max allowed number 2147483647*/, L"SIN(2)", L"-0n123", L"0nn1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerDecDwordPasteExpression, dwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerDecDwordPasteExpression, dwordNegativeInput); + + String^ wordPositiveInput[] = { L"123", L"+123", L"-133", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\f\n1+2\t\r\v\x85", L"1\"2", L"(123)+(456)", L"123+-234", L"123*-345", L"123*4*-3", L"123*+4*-3", L"32767", L"-32767"/*boundary condition: max/min allowed number*/, L"0n1234", L"0N1234", L"1234u", L"1234ul", L"1234ULL" }; + String^ wordNegativeInput[] = { L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"32769"/*boundary condition: greater than max allowed number 32769*/, L"SIN(2)", L"-0n123", L"0nn1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerDecWordPasteExpression, wordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerDecWordPasteExpression, wordNegativeInput); + + String^ bytePositiveInput[] = { L"13", L"+13", L"-13", L"13+46", L"13+-34", L"13*-3", L"3*4*-3", L"3*+4*-3", L"1,3", L"1 3", L"1'2'3", L"1_2_3", L"1\"2", L"127", L"-127"/*boundary condition: max/min allowed number*/, L"0n123", L"0N123", L"123u", L"123ul", L"123ULL" }; + String^ byteNegativeInput[] = { L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"15e-23", L"abcdef", L"xyz", L"ABab", L"e+24", L"129"/*boundary condition: greater than max allowed number 127*/, L"SIN(2)", L"-0n123", L"0nn1234", L"123uu", L"123ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerDecBytePasteExpression, bytePositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerDecBytePasteExpression, byteNegativeInput); + } + + void CopyPasteManagerTest::ValidateProgrammerOctPasteExpressionTest() + { + String^ qwordPositiveInput[] = { L"123", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"(123)+(456)", L"0t1234", L"0T1234", L"0o1234", L"0O1234", L"1234u", L"1234ul", L"1234ULL", L"2+2=", L"2+2= " }; + String^ qwordNegativeInput[] = { L"+123", L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"12345678901234567890123"/*boundary condition: greater than max allowed digits 22*/, L"SIN(2)", L"123+-234", L"0ot1234", L"1234uu", L"1234ulll", L"2+2==", L"2=+2" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerOctQwordPasteExpression, qwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerOctQwordPasteExpression, qwordNegativeInput); + + String^ dwordPositiveInput[] = { L"123", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\n\r1,234\n", L"\f\n1+2\t\r\v\x85", L"\n 1+\n2 ", L"1\"2", L"(123)+(456)", L"37777777777"/*boundary condition: max allowed number*/, L"0t1234", L"0T1234", L"0o1234", L"0O1234", L"1234u", L"1234ul", L"1234ULL" }; + String^ dwordNegativeInput[] = { L"+123", L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"377777777771"/*boundary condition: greater than max allowed number 37777777777*/, L"SIN(2)", L"123+-234", L"0ot1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerOctDwordPasteExpression, dwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerOctDwordPasteExpression, dwordNegativeInput); + + String^ wordPositiveInput[] = { L"123", L"123+456", L"1,234", L"1 2 3", L"1'2'3'4", L"1_2_3_4", L"\f\n1+2\t\r\v\x85", L"1\"2", L"(123)+(456)", L"177777"/*boundary condition: max allowed number*/, L"0t1234", L"0T1234", L"0o1234", L"0O1234", L"1234u", L"1234ul", L"1234ULL" }; + String^ wordNegativeInput[] = { L"+123", L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-133", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"12345e-23", L"abcdef", L"xyz", L"ABab", L"e+234", L"1777771"/*boundary condition: greater than max allowed number 177777*/, L"SIN(2)", L"123+-234", L"0ot1234", L"1234uu", L"1234ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerOctWordPasteExpression, wordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerOctWordPasteExpression, wordNegativeInput); + + String^ bytePositiveInput[] = { L"13", L"13+46", L"1,3", L"1 3", L"1'2'3", L"1_2_3", L"1\"2", L"377"/*boundary condition: max allowed number*/, L"0t123", L"0T123", L"0o123", L"0O123", L"123u", L"123ul", L"123ULL" }; + String^ byteNegativeInput[] = { L"+123", L"1.23", L"1''2", L"'123", L"123'", L"1__2", L"_123", L"123_", L"-13", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"15e-23", L"abcdef", L"xyz", L"ABab", L"e+24", L"477"/*boundary condition: greater than max allowed number 377*/, L"SIN(2)", L"123+-34", L"0ot123", L"123uu", L"123ulll" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerOctBytePasteExpression, bytePositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerOctBytePasteExpression, byteNegativeInput); + } + + void CopyPasteManagerTest::ValidateProgrammerBinPasteExpressionTest() + { + String^ qwordPositiveInput[] = { L"100", L"100+101", L"1,001", L"1 0 1", L"1'0'0'1", L"1_0_0_1", L"\n\r1,010\n", L"\f\n1+11\t\r\v\x85", L"\n 1+\n1 ", L"1\"1", L"(101)+(10)", L"0b1001", L"0B1111", L"0y1001", L"0Y1001", L"1100b", L"1101B", L"1111u", L"1111ul", L"1111ULL", L"1010101010101010101010101011110110100100101010101001010101001010"/*boundary condition: max allowed digits 64*/, L"1+10=", L"1+10= " }; + String^ qwordNegativeInput[] = { L"+10101", L"1.01", L"1''0", L"'101", L"101'", L"1__0", L"_101", L"101_", L"-10101001", L"123", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"101010e-1010", L"abcdef", L"xyz", L"ABab", L"e+10101", L"b1001", L"10b01", L"0x10", L"1001x", L"1001h", L"0bb1111", L"1111uu", L"1111ulll", L"10101010101010101010101010111101101001001010101010010101010010100"/*boundary condition: greater than max allowed digits 64*/, L"SIN(01010)", L"10+-10101010101", L"1+10==", L"1=+10" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerBinQwordPasteExpression, qwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerBinQwordPasteExpression, qwordNegativeInput); + + String^ dwordPositiveInput[] = { L"100", L"100+101", L"1,001", L"1 0 1", L"1'0'0'1", L"1_0_0_1", L"\n\r1,010\n", L"\f\n1+11\t\r\v\x85", L"\n 1+\n1 ", L"1\"1", L"(101)+(10)", L"0b1001", L"0B1111", L"0y1001", L"0Y1001", L"1100b", L"1101B", L"1111u", L"1111ul", L"1111ULL", L"10101001001010101101010111111100"/*boundary condition: max allowed number*/ }; + String^ dwordNegativeInput[] = { L"+10101", L"1.01", L"1''0", L"'101", L"101'", L"1__0", L"_101", L"101_", L"-10101001", L"123", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"101010e-1010", L"abcdef", L"xyz", L"ABab", L"e+10101", L"b1001", L"10b01", L"0x10", L"1001x", L"1001h", L"0bb1111", L"1111uu", L"1111ulll", L"101010010010101011010101111111001"/*boundary condition: greater than max allowed digits 32*/, L"SIN(01010)", L"10+-10101010101" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerBinDwordPasteExpression, dwordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerBinDwordPasteExpression, dwordNegativeInput); + + String^ wordPositiveInput[] = { L"100", L"100+101", L"1,001", L"1 0 1", L"1'0'0'1", L"1_0_0_1", L"\n\r1,010\n", L"\f\n1+11\t\r\v\x85", L"\n 1+\n1 ", L"1\"1", L"(101)+(10)", L"0b1001", L"0B1111", L"0y1001", L"0Y1001", L"1100b", L"1101B", L"1111u", L"1111ul", L"1111ULL", L"1010101010010010"/*boundary condition: max allowed number*/ }; + String^ wordNegativeInput[] = { L"+10101", L"1.01", L"1''0", L"'101", L"101'", L"1__0", L"_101", L"101_", L"-10101001", L"123", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"101010e-1010", L"abcdef", L"xyz", L"ABab", L"e+10101", L"b1001", L"10b01", L"0x10", L"1001x", L"1001h", L"0bb1111", L"1111uu", L"1111ulll", L"10101010100100101"/*boundary condition: greater than max allowed digits 16*/, L"SIN(01010)", L"10+-10101010101" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerBinWordPasteExpression, wordPositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerBinWordPasteExpression, wordNegativeInput); + + String^ bytePositiveInput[] = { L"100", L"100+101", L"1,001", L"1 0 1", L"1'0'0'1", L"1_0_0_1", L"\n\r1,010\n", L"\n 1+\n1 ", L"1\"1", L"(101)+(10)", L"0b1001", L"0B1111", L"0y1001", L"0Y1001", L"1100b", L"1101B", L"1111u", L"1111ul", L"1111ULL", L"10100010"/*boundary condition: max allowed number*/ }; + String^ byteNegativeInput[] = { L"+10101", L"1.01", L"1''0", L"'101", L"101'", L"1__0", L"_101", L"101_", L"-10101001", L"123", L"1.2e23"/*unsigned exponent*/, L"1.2e+023", L"101010e-1010", L"abcdef", L"xyz", L"ABab", L"e+10101", L"b1001", L"10b01", L"0x10", L"1001x", L"1001h", L"0bb1111", L"1111uu", L"1111ulll", L"101000101"/*boundary condition: greater than max allowed digits 8*/, L"SIN(01010)", L"10+-1010101" }; + + ASSERT_POSITIVE_TESTCASES(ValidateProgrammerBinBytePasteExpression, bytePositiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateProgrammerBinBytePasteExpression, byteNegativeInput); + } + + void CopyPasteManagerTest::ValidateConverterPasteExpressionTest() + { + String^ positiveInput[] = { L"123", L"+123", L"-133", L"12345.", L"012.012", L"1,234", L"1 2 3", L"\n\r1,234\n", L"\f\n12\t\r\v\x85", L"1\"2", L"100=", L"100= " }; + String^ negativeInput[] = { L"(123)+(456)", L"1.2e23"/*unsigned exponent*/, L"12345e-23", L"\n 1+\n2 ", L"123+456", L"abcdef", L"\n 1+\n2 ", L"xyz", L"ABab", L"e+234", L"12345678912345678"/*boundary condition: greater than 16 bits*/, L"SIN(2)", L"123+-234", L"100==", L"=100" }; + + ASSERT_POSITIVE_TESTCASES(ValidateConverterPasteExpression, positiveInput); + ASSERT_NEGATIVE_TESTCASES(ValidateConverterPasteExpression, negativeInput); + } +} + diff --git a/internal/CalculatorUnitTests/CurrencyConverterUnitTests.cpp b/internal/CalculatorUnitTests/CurrencyConverterUnitTests.cpp new file mode 100644 index 00000000..04aaa7da --- /dev/null +++ b/internal/CalculatorUnitTests/CurrencyConverterUnitTests.cpp @@ -0,0 +1,605 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +#include "CalcViewModel\DataLoaders\CurrencyDataLoader.h" +#include "CalcViewModel\Common\LocalizationService.h" + +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::LocalizationServiceProperties; +using namespace CalculatorApp::DataLoaders; +using namespace CalculatorApp::ViewModel; +using namespace CalculatorUnitTests; +using namespace Concurrency; +using namespace Platform; +using namespace std; +using namespace UnitConversionManager; +using namespace Windows::Foundation; +using namespace Windows::Storage; +using namespace Windows::Web::Http; + +namespace CalculatorApp +{ + namespace DataLoaders + { + class MockCurrencyHttpClientWithResult : public CurrencyHttpClient + { + public: + MockCurrencyHttpClientWithResult(String^ staticResponse, String^ allRatiosResponse) : + m_staticResponse(staticResponse), + m_allRatiosResponse(allRatiosResponse) + { + } + + IAsyncOperationWithProgress^ GetCurrencyMetadata() override + { + return ref new MockAsyncOperationWithProgress(m_staticResponse); + } + + IAsyncOperationWithProgress^ GetCurrencyRatios() override + { + return ref new MockAsyncOperationWithProgress(m_allRatiosResponse); + } + + private: + String^ m_staticResponse; + String^ m_allRatiosResponse; + }; + + class MockCurrencyHttpClientThrowsException : public CurrencyHttpClient + { + public: + MockCurrencyHttpClientThrowsException() {} + + IAsyncOperationWithProgress^ GetCurrencyMetadata() override + { + throw ref new NotImplementedException(); + } + + IAsyncOperationWithProgress^ GetCurrencyRatios() override + { + throw ref new NotImplementedException(); + } + }; + } +} + +class DataLoadedCallback : public UnitConversionManager::IViewModelCurrencyCallback +{ +public: + DataLoadedCallback(task_completion_event tce) : + m_task_completion_event{ tce } + {} + + void CurrencyDataLoadFinished(bool didLoad) override + { + m_task_completion_event.set(); + } + + void CurrencySymbolsCallback(_In_ const wstring& fromSymbol, _In_ const wstring& toSymbol) override {} + void CurrencyRatiosCallback(_In_ const wstring& ratioEquality, _In_ const wstring& accRatioEquality) override {} + void CurrencyTimestampCallback(_In_ const std::wstring& timestamp, bool isWeekOldData) override {} + void NetworkBehaviorChanged(_In_ int newBehavior) override {} + +private: + Concurrency::task_completion_event m_task_completion_event; +}; + +namespace CalculatorUnitTests +{ + constexpr auto sc_Language_EN = L"en-US"; + + const UCM::Category CURRENCY_CATEGORY = { NavCategory::Serialize(ViewMode::Currency), L"Currency", false /*supportsNegative*/ }; + + unique_ptr MakeLoaderWithResults(String^ staticResponse, String^ allRatiosResponse) + { + auto client = make_unique(staticResponse, allRatiosResponse); + client->SetSourceCurrencyCode(StringReference(DefaultCurrencyCode.data())); + return make_unique(move(client)); + } + + String^ SerializeContent(const vector& data) + { + String^ result = L""; + String^ delimiter = CurrencyDataLoaderConstants::CacheDelimiter; + for (String^ content : data) + { + result += (delimiter + content); + } + + return result; + } + + bool WriteToFileInLocalCacheFolder(String^ filename, String^ content) + { + try + { + StorageFolder^ localFolder = ApplicationData::Current->LocalCacheFolder; + StorageFile^ file = create_task(localFolder->CreateFileAsync(filename, CreationCollisionOption::ReplaceExisting)).get(); + create_task(FileIO::WriteTextAsync(file, content)).wait(); + return true; + } + catch (Exception^ ex) + { + return false; + } + } + + bool DeleteFileFromLocalCacheFolder(String^ filename) + { + try + { + StorageFolder^ folder = ApplicationData::Current->LocalCacheFolder; + IAsyncOperation^ fileOperation = folder->GetFileAsync(filename); + StorageFile^ file = create_task(fileOperation).get(); + create_task(file->DeleteAsync()).get(); + return true; + } + catch (Platform::Exception^ ex) + { + // FileNotFoundException is a valid result + return ex->HResult == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + catch (...) + { + return false; + } + } + + bool DeleteCurrencyCacheFiles() + { + try + { + bool deletedStaticData = DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename); + bool deletedAllRatiosData = DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename); + + return deletedStaticData && deletedAllRatiosData; + } + catch (...) + { + return false; + } + } + + void InsertToLocalSettings(String^ key, Object^ value) + { + ApplicationData::Current->LocalSettings->Values->Insert(key, value); + } + + void RemoveFromLocalSettings(String^ key) + { + // Safe to call, even if the key does not exist. + ApplicationData::Current->LocalSettings->Values->Remove(key); + } + + void StandardCacheSetup() + { + // Insert current time so data is less than a day old. + DateTime now = Utils::GetUniversalSystemTime(); + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheLangcodeKey, StringReference(sc_Language_EN)); + + VERIFY_IS_TRUE(DeleteCurrencyCacheFiles()); + + VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); + VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename, CurrencyHttpClient::GetRawAllRatiosDataResponse())); + } + + class CurrencyConverterLoadTests + { + public: + TEST_CLASS(CurrencyConverterLoadTests); + + + TEST_METHOD_SETUP(DeleteCacheFiles) + { + return DeleteCurrencyCacheFiles(); + } + + TEST_METHOD(LoadFromCache_Fail_NoCacheKey) + { + RemoveFromLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromCache_Fail_OlderThanADay) + { + // Insert 24 hours ago so data is considered stale. + // This will cause the load from cache to fail. + DateTime now = Utils::GetUniversalSystemTime(); + DateTime dayOld; + dayOld.UniversalTime = now.UniversalTime - CurrencyDataLoaderConstants::DayDuration - 1; + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, dayOld); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromCache_Fail_StaticDataFileDoesNotExist) + { + // Insert current time so data is less than a day old. + // This will cause the load to continue to attempt to load the file. + DateTime now = Utils::GetUniversalSystemTime(); + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); + + VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename)); + VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename, CurrencyHttpClient::GetRawAllRatiosDataResponse())); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromCache_Fail_AllRatiosDataFileDoesNotExist) + { + // Insert current time so data is less than a day old. + // This will cause the load to continue to attempt to load the file. + DateTime now = Utils::GetUniversalSystemTime(); + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); + + VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); + VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename)); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromCache_Fail_ResponseLanguageChanged) + { + DateTime now = Utils::GetUniversalSystemTime(); + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); + + // Tests always use en-US as response language. Insert a different lang-code to fail the test. + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheLangcodeKey, L"ar-SA"); + + VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); + VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename)); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromCache_Success) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromCacheAsync().get(); + + VERIFY_IS_TRUE(didLoad); + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + } + + TEST_METHOD(LoadFromWeb_Fail_ClientIsNullptr) + { + CurrencyDataLoader loader{ nullptr }; + + bool didLoad = loader.TryLoadDataFromWebAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + } + + TEST_METHOD(LoadFromWeb_Fail_WebException) + { + CurrencyDataLoader loader{ make_unique() }; + + bool didLoad = loader.TryLoadDataFromWebAsync().get(); + + VERIFY_IS_FALSE(didLoad); + VERIFY_IS_FALSE(loader.LoadFinished()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + } + + TEST_METHOD(LoadFromWeb_Success) + { + String^ staticResponse = CurrencyHttpClient::GetRawStaticDataResponse(); + String^ allRatiosResponse = CurrencyHttpClient::GetRawAllRatiosDataResponse(); + unique_ptr loader = MakeLoaderWithResults(staticResponse, allRatiosResponse); + + bool didLoad = loader->TryLoadDataFromWebAsync().get(); + + VERIFY_IS_TRUE(didLoad); + VERIFY_IS_TRUE(loader->LoadFinished()); + VERIFY_IS_TRUE(loader->LoadedFromWeb()); + } + + TEST_METHOD(Load_Success_LoadedFromCache) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + } + + TEST_METHOD(Load_Success_LoadedFromWeb) + { + // Insert 24 hours ago so data is considered stale. + // This will cause the load from cache to fail. + DateTime now = Utils::GetUniversalSystemTime(); + DateTime dayOld; + dayOld.UniversalTime = now.UniversalTime - CurrencyDataLoaderConstants::DayDuration - 1; + InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, dayOld); + + String^ staticResponse = CurrencyHttpClient::GetRawStaticDataResponse(); + String^ allRatiosResponse = CurrencyHttpClient::GetRawAllRatiosDataResponse(); + unique_ptr loader = MakeLoaderWithResults(staticResponse, allRatiosResponse); + + auto data_loaded_event = task_completion_event(); + loader->SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader->LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader->LoadFinished()); + VERIFY_IS_FALSE(loader->LoadedFromCache()); + VERIFY_IS_TRUE(loader->LoadedFromWeb()); + } + }; + + class CurrencyConverterUnitTests + { + TEST_CLASS(CurrencyConverterUnitTests); + + const UCM::Unit GetUnit(const vector& unitList, const wstring& target) + { + return *find_if(begin(unitList), end(unitList), [&target](const UCM::Unit& u) { return u.abbreviation == target; }); + } + + TEST_METHOD(Loaded_LoadOrderedUnits) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); + + VERIFY_ARE_EQUAL(StringReference(L"United States - Dollar"), ref new String(usdUnit.name.c_str())); + VERIFY_ARE_EQUAL(StringReference(L"USD"), ref new String(usdUnit.abbreviation.c_str())); + + VERIFY_ARE_EQUAL(StringReference(L"Europe - Euro"), ref new String(eurUnit.name.c_str())); + VERIFY_ARE_EQUAL(StringReference(L"EUR"), ref new String(eurUnit.abbreviation.c_str())); + } + + TEST_METHOD(Loaded_LoadOrderedRatios) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); + + unordered_map ratios = loader.LoadOrderedRatios(usdUnit); + VERIFY_ARE_EQUAL(size_t{ 2 }, ratios.size()); + + UCM::ConversionData usdRatioData = ratios[usdUnit]; + VERIFY_IS_TRUE((std::abs(1.0 - usdRatioData.ratio) < 1e-1)); + + UCM::ConversionData eurRatioData = ratios[eurUnit]; + VERIFY_IS_TRUE((std::abs(0.920503 - eurRatioData.ratio) < 1e-6)); + } + + TEST_METHOD(Loaded_GetCurrencySymbols_Valid) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); + + const pair symbols = loader.GetCurrencySymbols(usdUnit, eurUnit); + + VERIFY_ARE_EQUAL(ref new String(L"$"), StringReference(symbols.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L"€"), StringReference(symbols.second.c_str())); + } + + TEST_METHOD(Loaded_GetCurrencySymbols_Invalid) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + const UCM::Unit fakeUnit1 = { + 1, L"fakeUnit1", L"FUD1", false, false, false + }; + + const UCM::Unit fakeUnit2 = { + 2, L"fakeUnit2", L"FUD2", false, false, false + }; + + pair symbols = loader.GetCurrencySymbols(fakeUnit1, fakeUnit2); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); + + // Verify that when only one unit is valid, both symbols return as empty string. + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + + symbols = loader.GetCurrencySymbols(fakeUnit1, usdUnit); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); + + symbols = loader.GetCurrencySymbols(usdUnit, fakeUnit1); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); + } + + TEST_METHOD(Loaded_GetCurrencyRatioEquality_Valid) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); + + const pair ratio = loader.GetCurrencyRatioEquality(usdUnit, eurUnit); + + VERIFY_ARE_EQUAL(ref new String(L"1 USD = 0.9205 EUR"), StringReference(ratio.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L"1 United States Dollar = 0.9205 Europe Euro"), StringReference(ratio.second.c_str())); + } + + TEST_METHOD(Loaded_GetCurrencyRatioEquality_Invalid) + { + StandardCacheSetup(); + + CurrencyDataLoader loader{ nullptr }; + + auto data_loaded_event = task_completion_event(); + loader.SetViewModelCallback(make_shared(data_loaded_event)); + + auto data_loaded_task = create_task(data_loaded_event); + loader.LoadData(); + data_loaded_task.wait(); + + VERIFY_IS_TRUE(loader.LoadFinished()); + VERIFY_IS_TRUE(loader.LoadedFromCache()); + VERIFY_IS_FALSE(loader.LoadedFromWeb()); + + const UCM::Unit fakeUnit1 = { + 1, L"fakeUnit1", L"fakeCountry1", L"FUD1", false, false, false + }; + const UCM::Unit fakeUnit2 = { + 2, L"fakeUnit2", L"fakeCountry2", L"FUD2", false, false, false + }; + + pair ratio = loader.GetCurrencyRatioEquality(fakeUnit1, fakeUnit2); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); + + // Verify that when only one unit is valid, both symbols return as empty string. + vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); + VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); + + const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); + + ratio = loader.GetCurrencyRatioEquality(fakeUnit1, usdUnit); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); + + ratio = loader.GetCurrencyRatioEquality(usdUnit, fakeUnit1); + + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); + VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); + } + }; +} diff --git a/internal/CalculatorUnitTests/DateCalculatorUnitTests.cpp b/internal/CalculatorUnitTests/DateCalculatorUnitTests.cpp new file mode 100644 index 00000000..d6df4390 --- /dev/null +++ b/internal/CalculatorUnitTests/DateCalculatorUnitTests.cpp @@ -0,0 +1,582 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include "DateUtils.h" + +#include "CalcViewModel\Common\DateCalculator.h" +#include "CalcViewModel\DateCalculatorViewModel.h" + +using namespace Platform; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Globalization; +using namespace Windows::Globalization::DateTimeFormatting; +using namespace CalculatorApp::Common::DateCalculation; +using namespace CalculatorApp::ViewModel; + +namespace DateCalculationUnitTests +{ + const int c_numDate = 15; + const int c_diffTestCase = 9; + const int c_numAddOobDate = 2; + const int c_numSubtractOobDate = 2; + const int c_addCases = 3; + const int c_subtractCases = 3; + const int c_dateDiff = 14; + + DateCalculationEngine m_DateCalcEngine(CalendarIdentifiers::Gregorian); + + typedef struct + { + SYSTEMTIME startDate; + SYSTEMTIME endDate; + DateDifference dateDiff; + } DateTimeTestCase; + + SYSTEMTIME date[c_numDate]; + DateDifference dateDifference[c_dateDiff]; + DateTimeTestCase datetimeDifftest[c_diffTestCase]; + DateTimeTestCase datetimeBoundAdd[c_numAddOobDate]; + DateTimeTestCase datetimeBoundSubtract[c_numSubtractOobDate]; + DateTimeTestCase datetimeAddCase[c_addCases]; + DateTimeTestCase datetimeSubtractCase[c_subtractCases]; + + + // Test Class + class DateCalculatorUnitTests + { + public: + TEST_CLASS(DateCalculatorUnitTests); + + TEST_CLASS_SETUP(TestClassSetup) + { + /* Test Case Data */ + + // Dates - DD.MM.YYYY + /*31.12.9999*/ date[0].wYear = 9999; date[0].wMonth = 12; date[0].wDayOfWeek = 5; date[0].wDay = 31; date[0].wHour = 0; date[0].wMinute = 0; date[0].wSecond = 0; date[0].wMilliseconds = 0; + /*30.12.9999*/ date[1].wYear = 9999; date[1].wMonth = 12; date[1].wDayOfWeek = 4; date[1].wDay = 30; date[1].wHour = 0; date[1].wMinute = 0; date[1].wSecond = 0; date[1].wMilliseconds = 0; + /*31.12.9998*/ date[2].wYear = 9998; date[2].wMonth = 12; date[2].wDayOfWeek = 4; date[2].wDay = 31; date[2].wHour = 0; date[2].wMinute = 0; date[2].wSecond = 0; date[2].wMilliseconds = 0; + /*01.01.1601*/ date[3].wYear = 1601; date[3].wMonth = 1; date[3].wDayOfWeek = 1; date[3].wDay = 1; date[3].wHour = 0; date[3].wMinute = 0; date[3].wSecond = 0; date[3].wMilliseconds = 0; + /*02.01.1601*/ date[4].wYear = 1601; date[4].wMonth = 1; date[4].wDayOfWeek = 2; date[4].wDay = 2; date[4].wHour = 0; date[4].wMinute = 0; date[4].wSecond = 0; date[4].wMilliseconds = 0; + /*10.05.2008*/ date[5].wYear = 2008; date[5].wMonth = 5; date[5].wDayOfWeek = 6; date[5].wDay = 10; date[5].wHour = 0; date[5].wMinute = 0; date[5].wSecond = 0; date[5].wMilliseconds = 0; + /*10.03.2008*/ date[6].wYear = 2008; date[6].wMonth = 3; date[6].wDayOfWeek = 1; date[6].wDay = 10; date[6].wHour = 0; date[6].wMinute = 0; date[6].wSecond = 0; date[6].wMilliseconds = 0; + /*29.02.2008*/ date[7].wYear = 2008; date[7].wMonth = 2; date[7].wDayOfWeek = 5; date[7].wDay = 29; date[7].wHour = 0; date[7].wMinute = 0; date[7].wSecond = 0; date[7].wMilliseconds = 0; + /*28.02.2007*/ date[8].wYear = 2007; date[8].wMonth = 2; date[8].wDayOfWeek = 3; date[8].wDay = 28; date[8].wHour = 0; date[8].wMinute = 0; date[8].wSecond = 0; date[8].wMilliseconds = 0; + /*10.03.2007*/ date[9].wYear = 2007; date[9].wMonth = 3; date[9].wDayOfWeek = 6; date[9].wDay = 10; date[9].wHour = 0; date[9].wMinute = 0; date[9].wSecond = 0; date[9].wMilliseconds = 0; + /*10.05.2007*/ date[10].wYear = 2007; date[10].wMonth = 5; date[10].wDayOfWeek = 4; date[10].wDay = 10; date[10].wHour = 0; date[10].wMinute = 0; date[10].wSecond = 0; date[10].wMilliseconds = 0; + /*29.01.2008*/ date[11].wYear = 2008; date[11].wMonth = 1; date[11].wDayOfWeek = 2; date[11].wDay = 29; date[11].wHour = 0; date[11].wMinute = 0; date[11].wSecond = 0; date[11].wMilliseconds = 0; + /*28.01.2007*/ date[12].wYear = 2007; date[12].wMonth = 1; date[12].wDayOfWeek = 0; date[12].wDay = 28; date[12].wHour = 0; date[12].wMinute = 0; date[12].wSecond = 0; date[12].wMilliseconds = 0; + /*31.01.2008*/ date[13].wYear = 2008; date[13].wMonth = 1; date[13].wDayOfWeek = 4; date[13].wDay = 31; date[13].wHour = 0; date[13].wMinute = 0; date[13].wSecond = 0; date[13].wMilliseconds = 0; + /*31.03.2008*/ date[14].wYear = 2008; date[14].wMonth = 3; date[14].wDayOfWeek = 1; date[14].wDay = 31; date[14].wHour = 0; date[14].wMinute = 0; date[14].wSecond = 0; date[14].wMilliseconds = 0; + + // Date Differences + dateDifference[0].year = 1; dateDifference[0].month = 1; + dateDifference[1].month = 1; dateDifference[1].day = 10; + dateDifference[2].day = 2; + /*date[2]-[0]*/ dateDifference[3].week = 52; dateDifference[3].day = 1; + /*date[2]-[0]*/ dateDifference[4].year = 1; + dateDifference[5].day = 365; + dateDifference[6].month = 1; + dateDifference[7].month = 1; dateDifference[7].day = 2; + dateDifference[8].day = 31; + dateDifference[9].month = 11; dateDifference[9].day = 1; + dateDifference[10].year = 8398; dateDifference[10].month = 11; dateDifference[10].day = 30; + dateDifference[11].year = 2008; + dateDifference[12].year = 7991; dateDifference[12].month = 11; + dateDifference[13].week = 416998; dateDifference[13].day = 1; + + + + /* Test Cases */ + + // Date Difference test cases + datetimeDifftest[0].startDate = date[0]; datetimeDifftest[0].endDate = date[3]; datetimeDifftest[0].dateDiff = dateDifference[10]; + datetimeDifftest[1].startDate = date[0]; datetimeDifftest[1].endDate = date[2]; datetimeDifftest[1].dateDiff = dateDifference[5]; + datetimeDifftest[2].startDate = date[0]; datetimeDifftest[2].endDate = date[2]; datetimeDifftest[2].dateDiff = dateDifference[4]; + datetimeDifftest[3].startDate = date[0]; datetimeDifftest[3].endDate = date[2]; datetimeDifftest[3].dateDiff = dateDifference[3]; + datetimeDifftest[4].startDate = date[14]; datetimeDifftest[4].endDate = date[7]; datetimeDifftest[4].dateDiff = dateDifference[7]; + datetimeDifftest[5].startDate = date[14]; datetimeDifftest[5].endDate = date[7]; datetimeDifftest[5].dateDiff = dateDifference[8]; + datetimeDifftest[6].startDate = date[11]; datetimeDifftest[6].endDate = date[8]; datetimeDifftest[6].dateDiff = dateDifference[9]; + datetimeDifftest[7].startDate = date[13]; datetimeDifftest[7].endDate = date[0]; datetimeDifftest[7].dateDiff = dateDifference[12]; + datetimeDifftest[8].startDate = date[13]; datetimeDifftest[8].endDate = date[0]; datetimeDifftest[8].dateDiff = dateDifference[13]; + + // Date Add Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundAdd[0].startDate = date[1]; datetimeBoundAdd[0].endDate = date[0]; datetimeBoundAdd[0].dateDiff = dateDifference[2]; // on Add date[0] not used + /*OutofBound*/ datetimeBoundAdd[1].startDate = date[2]; datetimeBoundAdd[1].endDate = date[0]; datetimeBoundAdd[1].dateDiff = dateDifference[11]; // on Add date[0] not used + + // Date Subtract Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundSubtract[0].startDate = date[3]; datetimeBoundSubtract[0].endDate = date[0]; datetimeBoundSubtract[0].dateDiff = dateDifference[2]; // on subtract date[0] not used + /*OutofBound*/ datetimeBoundSubtract[1].startDate = date[14]; datetimeBoundSubtract[1].endDate = date[0]; datetimeBoundSubtract[1].dateDiff = dateDifference[11]; // on subtract date[0] not used + + // Date Add test cases (Positive tests) + datetimeAddCase[0].startDate = date[13]; datetimeAddCase[0].endDate = date[7]; datetimeAddCase[0].dateDiff = dateDifference[6];// add + datetimeAddCase[1].startDate = date[14]; datetimeAddCase[1].endDate = date[5]; datetimeAddCase[1].dateDiff = dateDifference[1];// add + datetimeAddCase[2].startDate = date[13]; datetimeAddCase[2].endDate = date[6]; datetimeAddCase[2].dateDiff = dateDifference[1];// add + + // Date Subtract test cases (Positive tests) + datetimeSubtractCase[0].startDate = date[14]; datetimeSubtractCase[0].endDate = date[7]; datetimeSubtractCase[0].dateDiff = dateDifference[6];// subtract + datetimeSubtractCase[1].startDate = date[6]; datetimeSubtractCase[1].endDate = date[11]; datetimeSubtractCase[1].dateDiff = dateDifference[1];// subtract + datetimeSubtractCase[2].startDate = date[9]; datetimeSubtractCase[2].endDate = date[12]; datetimeSubtractCase[2].dateDiff = dateDifference[1];// subtract + + return true; + } + + + /* Duration Between Two Date Tests -- Timediff obtained after calculation should be checked to be identical */ + TEST_METHOD(TestDateDiff) + { + // TODO - MSFT 10331900, fix this test + + //for (int testIndex = 0; testIndex < c_diffTestCase; testIndex++) + //{ + // DateDifference diff; + // DateUnit dateOutputFormat; + + // switch (testIndex) + // { + // case 0: + // case 2: + // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; + // break; + // case 1: + // dateOutputFormat = DateUnit::Day; + // break; + // case 3: + // case 8: + // dateOutputFormat = DateUnit::Week | DateUnit::Day; + // break; + // case 7: + // dateOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Day; + // break; + // case 4: + // case 6: + // dateOutputFormat = DateUnit::Month | DateUnit::Day; + // break; + // case 5: + // dateOutputFormat = DateUnit::Day; + // break; + // } + + // // Calculate the difference + // m_DateCalcEngine.GetDateDifference(DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].startDate), DateUtils::SystemTimeToDateTime(datetimeDifftest[testIndex].endDate), dateOutputFormat, &diff); + + // // Assert for the result + // bool areIdentical = true; + // if (diff.year != datetimeDifftest[testIndex].dateDiff.year || + // diff.month != datetimeDifftest[testIndex].dateDiff.month || + // diff.week != datetimeDifftest[testIndex].dateDiff.week || + // diff.day != datetimeDifftest[testIndex].dateDiff.day) + // { + // areIdentical = false; + // } + + // VERIFY_IS_TRUE(areIdentical); + //} + } + + /*Add Out of bound Tests*/ + TEST_METHOD(TestAddOob) + { + // TODO - MSFT 10331900, fix this test + + //for (int testIndex = 0; testIndex< c_numAddOobDate; testIndex++) + //{ + // DateTime endDate; + + // // Add Duration + // bool isValid = m_DateCalcEngine.AddDuration(DateUtils::SystemTimeToDateTime(datetimeBoundAdd[testIndex].startDate), datetimeBoundAdd[testIndex].dateDiff, &endDate); + + // // Assert for the result + // VERIFY_IS_FALSE(isValid); + //} + } + + /*Subtract Out of bound Tests*/ + TEST_METHOD(TestSubtractOob) + { + for (int testIndex = 0; testIndex< c_numSubtractOobDate; testIndex++) + { + DateTime endDate; + + // Subtract Duration + bool isValid = m_DateCalcEngine.SubtractDuration(DateUtils::SystemTimeToDateTime(datetimeBoundSubtract[testIndex].startDate), datetimeBoundSubtract[testIndex].dateDiff, &endDate); + + // Assert for the result + VERIFY_IS_FALSE(isValid); + } + } + + // Add Tests + TEST_METHOD(TestAddition) + { + // TODO - MSFT 10331900, fix this test + + //for (int testIndex = 0; testIndex < c_addCases; testIndex++) + //{ + // DateTime endDate; + + // // Add Duration + // bool isValid = m_DateCalcEngine.AddDuration(DateUtils::SystemTimeToDateTime(datetimeAddCase[testIndex].startDate), datetimeAddCase[testIndex].dateDiff, &endDate); + + // // Assert for the result + // VERIFY_IS_TRUE(isValid); + + // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); + // if (systemTime.wYear != datetimeAddCase[testIndex].endDate.wYear || + // systemTime.wMonth != datetimeAddCase[testIndex].endDate.wMonth || + // systemTime.wDay != datetimeAddCase[testIndex].endDate.wDay || + // systemTime.wDayOfWeek != datetimeAddCase[testIndex].endDate.wDayOfWeek) + // { + // isValid = false; + // } + + // VERIFY_IS_TRUE(isValid); + //} + } + + // Subtract Tests + TEST_METHOD(TestSubtraction) + { + // TODO - MSFT 10331900, fix this test + + //for (int testIndex = 0; testIndex < c_subtractCases; testIndex++) + //{ + // DateTime endDate; + + // // Subtract Duration + // bool isValid = m_DateCalcEngine.SubtractDuration(DateUtils::SystemTimeToDateTime(datetimeSubtractCase[testIndex].startDate), datetimeSubtractCase[testIndex].dateDiff, &endDate); + + // // assert for the result + // VERIFY_IS_TRUE(isValid); + + // SYSTEMTIME systemTime = DateUtils::DateTimeToSystemTime(endDate); + // if (systemTime.wYear != datetimeSubtractCase[testIndex].endDate.wYear || + // systemTime.wMonth != datetimeSubtractCase[testIndex].endDate.wMonth || + // systemTime.wDay != datetimeSubtractCase[testIndex].endDate.wDay || + // systemTime.wDayOfWeek != datetimeSubtractCase[testIndex].endDate.wDayOfWeek) + // { + // isValid = false; + // } + + // VERIFY_IS_TRUE(isValid); + //} + } + + private: + + }; + + class DateCalculatorViewModelTests + { + public: + TEST_CLASS(DateCalculatorViewModelTests); + + TEST_CLASS_SETUP(TestClassSetup) + { + /* Test Case Data */ + + // Dates - DD.MM.YYYY + /*31.12.9999*/ date[0].wYear = 9999; date[0].wMonth = 12; date[0].wDayOfWeek = 5; date[0].wDay = 31; date[0].wHour = 0; date[0].wMinute = 0; date[0].wSecond = 0; date[0].wMilliseconds = 0; + /*30.12.9999*/ date[1].wYear = 9999; date[1].wMonth = 12; date[1].wDayOfWeek = 4; date[1].wDay = 30; date[1].wHour = 0; date[1].wMinute = 0; date[1].wSecond = 0; date[1].wMilliseconds = 0; + /*31.12.9998*/ date[2].wYear = 9998; date[2].wMonth = 12; date[2].wDayOfWeek = 4; date[2].wDay = 31; date[2].wHour = 0; date[2].wMinute = 0; date[2].wSecond = 0; date[2].wMilliseconds = 0; + /*01.01.1601*/ date[3].wYear = 1601; date[3].wMonth = 1; date[3].wDayOfWeek = 1; date[3].wDay = 1; date[3].wHour = 0; date[3].wMinute = 0; date[3].wSecond = 0; date[3].wMilliseconds = 0; + /*02.01.1601*/ date[4].wYear = 1601; date[4].wMonth = 1; date[4].wDayOfWeek = 2; date[4].wDay = 2; date[4].wHour = 0; date[4].wMinute = 0; date[4].wSecond = 0; date[4].wMilliseconds = 0; + /*10.05.2008*/ date[5].wYear = 2008; date[5].wMonth = 5; date[5].wDayOfWeek = 6; date[5].wDay = 10; date[5].wHour = 0; date[5].wMinute = 0; date[5].wSecond = 0; date[5].wMilliseconds = 0; + /*10.03.2008*/ date[6].wYear = 2008; date[6].wMonth = 3; date[6].wDayOfWeek = 1; date[6].wDay = 10; date[6].wHour = 0; date[6].wMinute = 0; date[6].wSecond = 0; date[6].wMilliseconds = 0; + /*29.02.2008*/ date[7].wYear = 2008; date[7].wMonth = 2; date[7].wDayOfWeek = 5; date[7].wDay = 29; date[7].wHour = 0; date[7].wMinute = 0; date[7].wSecond = 0; date[7].wMilliseconds = 0; + /*28.02.2007*/ date[8].wYear = 2007; date[8].wMonth = 2; date[8].wDayOfWeek = 3; date[8].wDay = 28; date[8].wHour = 0; date[8].wMinute = 0; date[8].wSecond = 0; date[8].wMilliseconds = 0; + /*10.03.2007*/ date[9].wYear = 2007; date[9].wMonth = 3; date[9].wDayOfWeek = 6; date[9].wDay = 10; date[9].wHour = 0; date[9].wMinute = 0; date[9].wSecond = 0; date[9].wMilliseconds = 0; + /*10.05.2007*/ date[10].wYear = 2007; date[10].wMonth = 5; date[10].wDayOfWeek = 4; date[10].wDay = 10; date[10].wHour = 0; date[10].wMinute = 0; date[10].wSecond = 0; date[10].wMilliseconds = 0; + /*29.01.2008*/ date[11].wYear = 2008; date[11].wMonth = 1; date[11].wDayOfWeek = 2; date[11].wDay = 29; date[11].wHour = 0; date[11].wMinute = 0; date[11].wSecond = 0; date[11].wMilliseconds = 0; + /*28.01.2007*/ date[12].wYear = 2007; date[12].wMonth = 1; date[12].wDayOfWeek = 0; date[12].wDay = 28; date[12].wHour = 0; date[12].wMinute = 0; date[12].wSecond = 0; date[12].wMilliseconds = 0; + /*31.01.2008*/ date[13].wYear = 2008; date[13].wMonth = 1; date[13].wDayOfWeek = 4; date[13].wDay = 31; date[13].wHour = 0; date[13].wMinute = 0; date[13].wSecond = 0; date[13].wMilliseconds = 0; + /*31.03.2008*/ date[14].wYear = 2008; date[14].wMonth = 3; date[14].wDayOfWeek = 1; date[14].wDay = 31; date[14].wHour = 0; date[14].wMinute = 0; date[14].wSecond = 0; date[14].wMilliseconds = 0; + + // Date Differences + dateDifference[0].year = 1; dateDifference[0].month = 1; + dateDifference[1].month = 1; dateDifference[1].day = 10; + dateDifference[2].day = 2; + /*date[2]-[0]*/ dateDifference[3].week = 52; dateDifference[3].day = 1; + /*date[2]-[0]*/ dateDifference[4].year = 1; + dateDifference[5].day = 365; + dateDifference[6].month = 1; + dateDifference[7].month = 1; dateDifference[7].day = 2; + dateDifference[8].day = 31; + dateDifference[9].month = 11; dateDifference[9].day = 1; + dateDifference[10].year = 8398; dateDifference[10].month = 11; dateDifference[10].day = 30; + dateDifference[11].year = 2008; + dateDifference[12].year = 7991; dateDifference[12].month = 11; + dateDifference[13].week = 416998; dateDifference[13].day = 1; + + + + /* Test Cases */ + + // Date Difference test cases + datetimeDifftest[0].startDate = date[0]; datetimeDifftest[0].endDate = date[3]; datetimeDifftest[0].dateDiff = dateDifference[10]; + datetimeDifftest[1].startDate = date[0]; datetimeDifftest[1].endDate = date[2]; datetimeDifftest[1].dateDiff = dateDifference[5]; + datetimeDifftest[2].startDate = date[0]; datetimeDifftest[2].endDate = date[2]; datetimeDifftest[2].dateDiff = dateDifference[4]; + datetimeDifftest[3].startDate = date[0]; datetimeDifftest[3].endDate = date[2]; datetimeDifftest[3].dateDiff = dateDifference[3]; + datetimeDifftest[4].startDate = date[14]; datetimeDifftest[4].endDate = date[7]; datetimeDifftest[4].dateDiff = dateDifference[7]; + datetimeDifftest[5].startDate = date[14]; datetimeDifftest[5].endDate = date[7]; datetimeDifftest[5].dateDiff = dateDifference[8]; + datetimeDifftest[6].startDate = date[11]; datetimeDifftest[6].endDate = date[8]; datetimeDifftest[6].dateDiff = dateDifference[9]; + datetimeDifftest[7].startDate = date[13]; datetimeDifftest[7].endDate = date[0]; datetimeDifftest[7].dateDiff = dateDifference[12]; + datetimeDifftest[8].startDate = date[13]; datetimeDifftest[8].endDate = date[0]; datetimeDifftest[8].dateDiff = dateDifference[13]; + + // Date Add Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundAdd[0].startDate = date[1]; datetimeBoundAdd[0].endDate = date[0]; datetimeBoundAdd[0].dateDiff = dateDifference[2]; // on Add date[0] not used + /*OutofBound*/ datetimeBoundAdd[1].startDate = date[2]; datetimeBoundAdd[1].endDate = date[0]; datetimeBoundAdd[1].dateDiff = dateDifference[11]; // on Add date[0] not used + + // Date Subtract Out of Bound test cases (Negative tests) + /*OutofBound*/ datetimeBoundSubtract[0].startDate = date[3]; datetimeBoundSubtract[0].endDate = date[0]; datetimeBoundSubtract[0].dateDiff = dateDifference[2]; // on subtract date[0] not used + /*OutofBound*/ datetimeBoundSubtract[1].startDate = date[14]; datetimeBoundSubtract[1].endDate = date[0]; datetimeBoundSubtract[1].dateDiff = dateDifference[11]; // on subtract date[0] not used + + // Date Add test cases (Positive tests) + datetimeAddCase[0].startDate = date[13]; datetimeAddCase[0].endDate = date[7]; datetimeAddCase[0].dateDiff = dateDifference[6];// add + datetimeAddCase[1].startDate = date[14]; datetimeAddCase[1].endDate = date[5]; datetimeAddCase[1].dateDiff = dateDifference[1];// add + datetimeAddCase[2].startDate = date[13]; datetimeAddCase[2].endDate = date[6]; datetimeAddCase[2].dateDiff = dateDifference[1];// add + + // Date Subtract test cases (Positive tests) + datetimeSubtractCase[0].startDate = date[14]; datetimeSubtractCase[0].endDate = date[7]; datetimeSubtractCase[0].dateDiff = dateDifference[6];// subtract + datetimeSubtractCase[1].startDate = date[6]; datetimeSubtractCase[1].endDate = date[11]; datetimeSubtractCase[1].dateDiff = dateDifference[1];// subtract + datetimeSubtractCase[2].startDate = date[9]; datetimeSubtractCase[2].endDate = date[12]; datetimeSubtractCase[2].dateDiff = dateDifference[1];// subtract + return true; + } + + TEST_METHOD(DateCalcViewModelInitializationTest) + { + auto viewModel = ref new DateCalculatorViewModel(); + + // Check for the initialized values + VERIFY_IS_TRUE(viewModel->IsDateDiffMode); + VERIFY_IS_TRUE(viewModel->IsAddMode); + + VERIFY_IS_TRUE(0 != viewModel->FromDate.UniversalTime); + VERIFY_IS_TRUE(0 != viewModel->ToDate.UniversalTime); + VERIFY_IS_TRUE(0 != viewModel->StartDate.UniversalTime); + + VERIFY_ARE_EQUAL(0, viewModel->DaysOffset); + VERIFY_ARE_EQUAL(0, viewModel->MonthsOffset); + VERIFY_ARE_EQUAL(0, viewModel->YearsOffset); + + VERIFY_IS_TRUE(viewModel->IsDiffInDays); + VERIFY_ARE_EQUAL(StringReference(L"Same dates"), viewModel->StrDateDiffResult); + VERIFY_IS_NULL(viewModel->StrDateDiffResultInDays); + + VERIFY_IS_NULL(viewModel->StrDateResult); + } + + TEST_METHOD(DateCalcViewModelAddSubtractInitTest) + { + auto viewModel = ref new DateCalculatorViewModel(); + viewModel->IsDateDiffMode = false; + + // Check for the initialized values + VERIFY_IS_FALSE(viewModel->IsDateDiffMode); + VERIFY_IS_TRUE(viewModel->IsAddMode); + + VERIFY_IS_TRUE(0 != viewModel->FromDate.UniversalTime); + VERIFY_IS_TRUE(0 != viewModel->ToDate.UniversalTime); + VERIFY_IS_TRUE(0 != viewModel->StartDate.UniversalTime); + + VERIFY_ARE_EQUAL(0, viewModel->DaysOffset); + VERIFY_ARE_EQUAL(0, viewModel->MonthsOffset); + VERIFY_ARE_EQUAL(0, viewModel->YearsOffset); + + VERIFY_IS_TRUE(viewModel->IsDiffInDays); + VERIFY_ARE_EQUAL(StringReference(L"Same dates"), viewModel->StrDateDiffResult); + VERIFY_IS_NULL(viewModel->StrDateDiffResultInDays); + + VERIFY_IS_NOT_NULL(viewModel->StrDateResult); + VERIFY_IS_TRUE(StringReference(L"") != viewModel->StrDateResult); + } + + TEST_METHOD(DateCalcViewModelAddTest) + { + // TODO - MSFT 10331900, fix this test + // A few issues to be investigated.. + // The date returned by DateUtils::GetLongDate can be a different string than expected + // based on the values of date[7]. This is because date[7] is in UTC but GetLongDate + // doesn't format according to UTC. If it did, the test would still be incorrect because + // the ViewModel is not necessarily in UTC. + // + // The DateTime value assigned to StartDate after the conversion SystemTimeToDateTime is not + // the same DateTime value as if the user were to select the same date from the CalendarDatePicker. + // This means testing a specific date here, is *not* the same as selecting that date in the app. + + //auto viewModel = ref new DateCalculatorViewModel(); + //viewModel->Initialize(); + + //viewModel->IsDateDiffMode = false; + //viewModel->IsAddMode = true; + //VERIFY_IS_FALSE(viewModel->IsDateDiffMode); + //VERIFY_IS_TRUE(viewModel->IsAddMode); + + //viewModel->StartDate = DateUtils::SystemTimeToDateTime(datetimeAddCase[0].startDate); + //viewModel->DaysOffset = datetimeAddCase[0].dateDiff.day; + //viewModel->MonthsOffset = datetimeAddCase[0].dateDiff.month; + //viewModel->YearsOffset = datetimeAddCase[0].dateDiff.year; + + //// Assert for the result + //VERIFY_ARE_EQUAL(DateUtils::GetLongDate(date[7]), viewModel->StrDateResult); + } + + TEST_METHOD(DateCalcViewModelSubtractTest) + { + // TODO - MSFT 10331900, fix this test + // A few issues to be investigated.. + // The date returned by DateUtils::GetLongDate can be a different string than expected + // based on the values of date[7]. This is because date[7] is in UTC but GetLongDate + // doesn't format according to UTC. If it did, the test would still be incorrect because + // the ViewModel is not necessarily in UTC. + // + // The DateTime value assigned to StartDate after the conversion SystemTimeToDateTime is not + // the same DateTime value as if the user were to select the same date from the CalendarDatePicker. + // This means testing a specific date here, is *not* the same as selecting that date in the app. + + //auto viewModel = ref new DateCalculatorViewModel(); + //viewModel->Initialize(); + + //viewModel->IsDateDiffMode = false; + //viewModel->IsAddMode = false; + //VERIFY_IS_FALSE(viewModel->IsDateDiffMode); + //VERIFY_IS_FALSE(viewModel->IsAddMode); + + //viewModel->StartDate = DateUtils::SystemTimeToDateTime(datetimeSubtractCase[0].startDate); + //viewModel->DaysOffset = datetimeSubtractCase[0].dateDiff.day; + //viewModel->MonthsOffset = datetimeSubtractCase[0].dateDiff.month; + //viewModel->YearsOffset = datetimeSubtractCase[0].dateDiff.year; + + //// Assert for the result + //VERIFY_ARE_EQUAL(DateUtils::GetLongDate(date[7]), viewModel->StrDateResult); + } + + TEST_METHOD(DateCalcViewModelAddOobTest) + { + // TODO - MSFT 10331900, fix this test + // Curiously enough, this test fails because it fails to go Oob. + // Possibly need to update test to use a new max date. + + //auto viewModel = ref new DateCalculatorViewModel(); + //viewModel->Initialize(); + + //viewModel->IsDateDiffMode = false; + //viewModel->IsAddMode = true; + //VERIFY_IS_FALSE(viewModel->IsDateDiffMode); + //VERIFY_IS_TRUE(viewModel->IsAddMode); + + //for (int testIndex = 0; testIndex< c_numAddOobDate; testIndex++) + //{ + // viewModel->StartDate = DateUtils::SystemTimeToDateTime(datetimeBoundAdd[testIndex].startDate); + // viewModel->DaysOffset = datetimeBoundAdd[testIndex].dateDiff.day; + // viewModel->MonthsOffset = datetimeBoundAdd[testIndex].dateDiff.month; + // viewModel->YearsOffset = datetimeBoundAdd[testIndex].dateDiff.year; + + // // Assert for the result + // VERIFY_ARE_EQUAL(StringReference(L"Date out of Bound"), viewModel->StrDateResult); + //} + } + + TEST_METHOD(DateCalcViewModelSubtractOobTest) + { + auto viewModel = ref new DateCalculatorViewModel(); + + viewModel->IsDateDiffMode = false; + viewModel->IsAddMode = false; + VERIFY_IS_FALSE(viewModel->IsDateDiffMode); + VERIFY_IS_FALSE(viewModel->IsAddMode); + + for (int testIndex = 0; testIndex < c_numSubtractOobDate; testIndex++) + { + viewModel->StartDate = DateUtils::SystemTimeToDateTime(datetimeBoundSubtract[testIndex].startDate); + viewModel->DaysOffset = datetimeBoundSubtract[testIndex].dateDiff.day; + viewModel->MonthsOffset = datetimeBoundSubtract[testIndex].dateDiff.month; + viewModel->YearsOffset = datetimeBoundSubtract[testIndex].dateDiff.year; + + // Assert for the result + VERIFY_ARE_EQUAL(StringReference(L"Date out of Bound"), viewModel->StrDateResult); + } + } + + TEST_METHOD(DateCalcViewModelDateDiffTest) + { + // TODO - MSFT 10331900, fix this test + // The last VERIFY checks with expected value "8398 years, 11 months, 4 weeks, 2 days" + // The viewmodel result is something like "8398 years, 12 months, 6568892 weeks, 1 day", + // which shows there is a problem with the viewmodel's reduction algorithm. + + //auto viewModel = ref new DateCalculatorViewModel(); + //viewModel->Initialize(); + + //viewModel->IsDateDiffMode = true; + //VERIFY_IS_TRUE(viewModel->IsDateDiffMode); + + //viewModel->FromDate = DateUtils::SystemTimeToDateTime(datetimeDifftest[0].startDate); + //viewModel->ToDate = DateUtils::SystemTimeToDateTime(datetimeDifftest[0].endDate); + + //// Assert for the result + //VERIFY_IS_FALSE(viewModel->IsDiffInDays); + //VERIFY_ARE_EQUAL(StringReference(L"3067670 days"), viewModel->StrDateDiffResultInDays); + //VERIFY_ARE_EQUAL(StringReference(L"8398 years, 11 months, 4 weeks, 2 days"), viewModel->StrDateDiffResult); + } + + TEST_METHOD(DateCalcViewModelDateDiffResultInDaysTest) + { + auto viewModel = ref new DateCalculatorViewModel(); + + viewModel->IsDateDiffMode = true; + VERIFY_IS_TRUE(viewModel->IsDateDiffMode); + + viewModel->FromDate = DateUtils::SystemTimeToDateTime(date[0]); + viewModel->ToDate = DateUtils::SystemTimeToDateTime(date[1]); + + // Assert for the result + VERIFY_IS_TRUE(viewModel->IsDiffInDays); + VERIFY_ARE_EQUAL(StringReference(L"1 day"), viewModel->StrDateDiffResult); + VERIFY_IS_NULL(viewModel->StrDateDiffResultInDays); + } + + // Tests that the automation name for the resulting date in Add Mode + // contains the DayOfWeek, Day, Month, and Year + TEST_METHOD(DateCalcViewModelAddSubtractResultAutomationNameTest) + { + auto viewModel = ref new DateCalculatorViewModel(); + + auto cal = ref new Calendar(); + cal->Year = 2007; + cal->Month = 5; + cal->Day = 10; + cal->Hour = 12; + cal->Period = 2; + cal->Nanosecond = 0; + cal->Second = 0; + + DateTime startDate = cal->GetDateTime(); + viewModel->StartDate = startDate; + + viewModel->IsDateDiffMode = false; + viewModel->IsAddMode = true; + + wstring actualValue = viewModel->StrDateResultAutomationName->Data(); + + // Verify each component is present in the result + wstring components[] = { + L"dayofweek.full", + L"month.full", + L"year.full", + L"day" + }; + + for (const wstring &component : components) + { + auto formatter = ref new DateTimeFormatter(ref new String(component.c_str())); + wstring expectedValue = formatter->Format(startDate)->Data(); + wstring message = L"Verifying " + component + L" is present in the result"; + VERIFY_IS_TRUE(actualValue.find(expectedValue) != wstring::npos, message.c_str()); + } + } + }; +} + diff --git a/internal/CalculatorUnitTests/DateUtils.h b/internal/CalculatorUnitTests/DateUtils.h new file mode 100644 index 00000000..c2625bd5 --- /dev/null +++ b/internal/CalculatorUnitTests/DateUtils.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +namespace DateCalculationUnitTests +{ + /** Date Utils **/ + class DateUtils + { + public: + // Converts SYSTEMTIME structure to DateTime value + // Converts: SYSTEMTIME -> FILETIME -> DateTime + static Windows::Foundation::DateTime SystemTimeToDateTime(SYSTEMTIME systemTime) + { + LPFILETIME lpFileTime = new FILETIME(); + SystemTimeToFileTime(&systemTime, lpFileTime); + + Windows::Foundation::DateTime dateTime; + dateTime.UniversalTime = (DWORD)lpFileTime->dwHighDateTime; + dateTime.UniversalTime <<= 32; + dateTime.UniversalTime |= (DWORD)lpFileTime->dwLowDateTime; + + return dateTime; + } + + // Converts DateTime value to SYSTEMTIME structure + // Converts: DateTime -> FILETIME -> SYSTEMTIME + static SYSTEMTIME DateTimeToSystemTime(Windows::Foundation::DateTime dateTime) + { + FILETIME fileTime; + fileTime.dwLowDateTime = (DWORD)(dateTime.UniversalTime & 0xffffffff); + fileTime.dwHighDateTime = (DWORD)(dateTime.UniversalTime >> 32); + + SYSTEMTIME systemTime; + FileTimeToSystemTime(&fileTime, &systemTime); + + return systemTime; + } + + // Returns long date format for a date + static Platform::String^ GetLongDate(SYSTEMTIME systemTime) + { + auto formatter = ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter( + L"longdate", + Windows::Globalization::ApplicationLanguages::Languages, + Windows::System::UserProfile::GlobalizationPreferences::HomeGeographicRegion, + Windows::Globalization::CalendarIdentifiers::Gregorian, + Windows::Globalization::ClockIdentifiers::TwentyFourHour); + + Windows::Foundation::DateTime dateTime = SystemTimeToDateTime(systemTime); + return formatter->Format(dateTime); + } + }; +} + diff --git a/internal/CalculatorUnitTests/Helpers.h b/internal/CalculatorUnitTests/Helpers.h new file mode 100644 index 00000000..c7df9622 --- /dev/null +++ b/internal/CalculatorUnitTests/Helpers.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +#pragma once +#include "CalcViewModel\Common\CalculatorButtonUser.h" + +namespace CalculatorUnitTests +{ + #define StandardModePrecision 16 + #define ScientificModePrecision 32 + #define ProgrammerModePrecision 64 + + typedef struct item + { + CalculatorApp::NumbersAndOperatorsEnum command; + std::wstring expectedPrimaryDisplay; + std::wstring expectedExpressions; + } TESTITEM; + + namespace UtfUtils { + constexpr wchar_t LRE = 0x202a; // Left-to-Right Embedding + constexpr wchar_t PDF = 0x202c; // Pop Directional Formatting + constexpr wchar_t LRO = 0x202d; // Left-to-Right Override + constexpr wchar_t MUL = 0x00d7; // Multiplication Symbol + } + + +} + diff --git a/internal/CalculatorUnitTests/HistoryTests.cpp b/internal/CalculatorUnitTests/HistoryTests.cpp new file mode 100644 index 00000000..53012fa0 --- /dev/null +++ b/internal/CalculatorUnitTests/HistoryTests.cpp @@ -0,0 +1,533 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +#include "CalcViewModel\HistoryViewModel.h" +#include "CalcViewModel\StandardCalculatorViewModel.h" + +using namespace CalculationManager; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace CalculatorUnitTests; +using namespace Platform; +using namespace std; +using namespace Windows::Storage; +using namespace Windows::ApplicationModel::Resources; + +namespace CalculatorFunctionalTests +{ + class HistoryTests + { + public: + TEST_CLASS(HistoryTests); + TEST_METHOD(TestHistoryItemClicked); + TEST_METHOD(TestHistoryItemAddSingleItem); + TEST_METHOD(TestHistoryItemAddMaxItems); + TEST_METHOD(TestHistoryClearCommand); + TEST_METHOD(TestHistoryClearCommandWithEmptyHistory); + TEST_METHOD(TestReLoadHistory); + TEST_METHOD(TestSaveAndReloadHistory); + TEST_METHOD(TestSerializeDeSerializeHistoryItem); + TEST_METHOD(TestHistoryItemWithPrettyExpressions); + TEST_METHOD(TestHistoryItemWithPrettyExpressionsMixedRadix); + TEST_METHOD(TestHistoryItemLoadAndContinueCalculation); + TEST_METHOD(TestDisplayValueAutomationNames); + TEST_METHOD(TestRadixAutomationName); + TEST_METHOD(TestHistoryEmpty); + + private: + HistoryViewModel^ m_historyViewModel; + StandardCalculatorViewModel^ m_standardViewModel; + + void Initialize(unsigned int windowId = 0) + { + m_standardViewModel = ref new StandardCalculatorViewModel(); + m_standardViewModel->IsStandard = true; + m_historyViewModel = ref new HistoryViewModel(m_standardViewModel->m_standardCalculatorManager.get()); + m_historyViewModel->SetCalculatorDisplay(m_standardViewModel->m_calculatorDisplay); + } + + void Cleanup(unsigned int windowId = 0) + { + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + m_historyViewModel->OnClearCommand(nullptr); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_historyViewModel->OnClearCommand(nullptr); + m_standardViewModel->m_standardCalculatorManager->Reset(); + } + + bool IsHistoryContainerEmpty(_In_ String^ historyContainerKey) + { + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + return !(localSettings->Containers->HasKey(historyContainerKey)); + } + + String^ GetHistoryContainerKeyHelper(CalculationManager::CALCULATOR_MODE cMode) + { + ValueType^ modeValue = static_cast(cMode); + return String::Concat(modeValue->ToString(), L"_History"); + } + + void MockOnHistoryItemClicked(CalculatorApp::ViewModel::HistoryItemViewModel^ e) + { + m_standardViewModel->SetHistoryExpressionDisplay(e->GetTokens(), e->GetCommands()); + m_standardViewModel->SetExpressionDisplay(e->GetTokens(), e->GetCommands()); + m_standardViewModel->SetPrimaryDisplay(e->Result->Data(), false/*IsError*/); + m_standardViewModel->IsFToEEnabled = false; + } + + void AddSingleHistoryItem(unsigned int windowId = 0) + { + Initialize(windowId); + int initialSize = m_historyViewModel->ItemSize; + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command8); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + int sizeAfterItemAdd = m_historyViewModel->ItemSize; + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + String^ expression = UtfUtils::LRO + L"1 + 8 =" + UtfUtils::PDF; + String ^result = StringReference(L"9"); + VERIFY_ARE_EQUAL(initialSize + 1, sizeAfterItemAdd); + VERIFY_ARE_EQUAL(expression, StringReference(historyItem->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + Cleanup(windowId); + } + + void AddMaxHistoryItems(unsigned int windowId = 0) + { + Initialize(windowId); + int initialSize = m_historyViewModel->ItemSize; + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + for (int i = 1; i < m_standardViewModel->m_standardCalculatorManager->MaxHistorySize(); i++) + { + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + } + VERIFY_ARE_EQUAL(m_historyViewModel->ItemSize, m_standardViewModel->m_standardCalculatorManager->MaxHistorySize()); + String ^expression = UtfUtils::LRO + L"1 + 1 =" + UtfUtils::PDF; + int output = 2; + String ^result = output.ToString(); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + VERIFY_ARE_EQUAL(expression, StringReference(historyItem->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command5); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + VERIFY_ARE_EQUAL(m_historyViewModel->ItemSize, m_standardViewModel->m_standardCalculatorManager->MaxHistorySize()); + expression = UtfUtils::LRO + L"1 + 2 =" + UtfUtils::PDF; + output = 3; + result = output.ToString(); + historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + VERIFY_ARE_EQUAL(expression, StringReference(historyItem->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + Cleanup(windowId); + } + + void ReloadHistory(unsigned int windowId = 0) + { + Initialize(windowId); + + m_standardViewModel->m_standardCalculatorManager->Reset(); + int scientificItems = 5; + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + for (int i = 0; i < scientificItems; i++) + { + Command nextCommand = Command(130 + i); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(nextCommand); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + } + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + int standardItems = 2; + for (int i = 0; i < standardItems; i++) + { + Command nextCommand = Command(130 + i); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(nextCommand); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + } + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_historyViewModel->ReloadHistory(ViewMode::Scientific); + VERIFY_ARE_EQUAL(scientificItems, m_historyViewModel->ItemSize); + for (int i = 0; i < scientificItems; i++) + { + wstring expr = L"1 + " + wstring(i.ToString()->Data()) + L" ="; + expr = UtfUtils::LRO + expr + UtfUtils::PDF; + int output = 1 + i; + String ^result = output.ToString(); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(i); + VERIFY_ARE_EQUAL(expr, historyItem->historyItemVector.expression); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + } + + m_historyViewModel->ReloadHistory(ViewMode::Standard); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + VERIFY_ARE_EQUAL(standardItems, m_historyViewModel->ItemSize); + for (int i = 0; i < standardItems; i++) + { + wstring expr = L"1 + " + wstring(i.ToString()->Data()) + L" ="; + expr = UtfUtils::LRO + expr + UtfUtils::PDF; + int output = 1 + i; + String ^result = output.ToString(); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(i); + VERIFY_ARE_EQUAL(expr, historyItem->historyItemVector.expression); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + } + Cleanup(windowId); + } + + void ClearHistory(unsigned int windowId = 0) + { + Initialize(windowId); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + m_historyViewModel->OnClearCommand(nullptr); + VERIFY_ARE_EQUAL(0, m_historyViewModel->ItemSize); + VERIFY_IS_TRUE(IsHistoryContainerEmpty(GetHistoryContainerKeyHelper(CM_STD))); + VERIFY_IS_TRUE(IsHistoryContainerEmpty(GetHistoryContainerKeyHelper(CM_SCI))); + Cleanup(windowId); + } + + void SerializeDeSerializeHistoryItem(unsigned int windowId = 0) + { + Initialize(windowId); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + auto itemBeforeSerializeDeserialize = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + m_historyViewModel->SaveHistory(); + m_historyViewModel->ReloadHistory(ViewMode::Scientific); + auto itemAfterSerializeDeserialize = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + VERIFY_IS_TRUE((itemBeforeSerializeDeserialize->historyItemVector.expression == itemAfterSerializeDeserialize->historyItemVector.expression) && (itemBeforeSerializeDeserialize->historyItemVector.result == itemAfterSerializeDeserialize->historyItemVector.result) && (itemBeforeSerializeDeserialize->historyItemVector.spCommands == itemAfterSerializeDeserialize->historyItemVector.spCommands) && (itemBeforeSerializeDeserialize->historyItemVector.spTokens == itemAfterSerializeDeserialize->historyItemVector.spTokens)); + Cleanup(windowId); + } + + void SaveAndReloadHistory(unsigned int windowid = 0) + { + Initialize(windowid); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command8); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command6); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + int itemsBeforeSaveAndReload = m_historyViewModel->ItemSize; + m_historyViewModel->SaveHistory(); + m_historyViewModel->ReloadHistory(ViewMode::Scientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + wstring expr = L"1 + 8 ="; + // add double quotes around the expression + expr = UtfUtils::LRO + expr + UtfUtils::PDF; + String ^result = StringReference(L"9"); + int itemsAfterSaveAndReload = m_historyViewModel->ItemSize; + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + + VERIFY_ARE_EQUAL(expr, historyItem->historyItemVector.expression); + VERIFY_ARE_EQUAL(result, StringReference(historyItem->historyItemVector.result.c_str())); + VERIFY_ARE_NOT_EQUAL(itemsBeforeSaveAndReload, itemsAfterSaveAndReload); + VERIFY_ARE_EQUAL(itemsBeforeSaveAndReload, itemsAfterSaveAndReload + 1); + Cleanup(windowid); + } + + void HistoryItemWithPrettyExpressions(unsigned int windowId = 0) + { + Initialize(windowId); + Command commands[] = { Command::CommandSIN, Command::CommandCOS, Command::CommandTAN, Command::CommandASIN, Command::CommandACOS, Command::CommandATAN }; + Command mode[] = { Command::CommandDEG, Command::CommandRAD, Command::CommandGRAD }; + int modes = sizeof(mode) / sizeof(Command); + int commandsSize = sizeof(commands) / sizeof(Command); + ResourceLoader^ m_uiResourceLoader = ResourceLoader::GetForViewIndependentUse(L"CEngineStrings"); + int itemIndex = 0; + int commandResource = 67; + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + for (int index = 0; index < modes; index++) + { + m_standardViewModel->m_standardCalculatorManager->SendCommand(mode[index]); + for (int command = 0; command < commandsSize; command++) + { + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(commands[command]); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(itemIndex); + String^ expression = m_uiResourceLoader->GetString(commandResource.ToString()); + expression += L"( 1 ) ="; + wstring expr = wstring(expression->Data()); + expr = UtfUtils::LRO + expr + UtfUtils::PDF; + VERIFY_ARE_EQUAL(historyItem->historyItemVector.expression, expr); + commandResource++; + itemIndex++; + } + } + Cleanup(windowId); + } + + void HistoryItemWithPrettyExpressionsMixedRadix(unsigned int windowId = 0) + { + Initialize(windowId); + ResourceLoader^ m_uiResourceLoader = ResourceLoader::GetForViewIndependentUse(L"CEngineStrings"); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandDEG); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandSIN); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandRAD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandSIN); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandGRAD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandSIN); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + String^ expression = m_uiResourceLoader->GetString(L"67"); + expression += L"( 1 ) + "; + expression += m_uiResourceLoader->GetString(L"73"); + expression += L"( 1 ) + "; + expression += m_uiResourceLoader->GetString(L"79"); + expression += L"( 1 ) ="; + wstring expr = wstring(expression->Data()); + expr = UtfUtils::LRO + expr + UtfUtils::PDF; + VERIFY_ARE_EQUAL(historyItem->historyItemVector.expression,expr); + + Cleanup(windowId); + } + + void HistoryItemClicked(unsigned int windowId = 0) + { + Initialize(windowId); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command5); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command3); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + String^ expression = StringReference(historyItem->historyItemVector.expression.c_str()); + String^ result = StringReference(historyItem->historyItemVector.result.c_str()); + HistoryItemViewModel ^ item = ref new HistoryItemViewModel(expression, result, historyItem->historyItemVector.spTokens, historyItem->historyItemVector.spCommands); + MockOnHistoryItemClicked(item); + VERIFY_ARE_EQUAL(StringReference(L"9"), m_standardViewModel->DisplayValue); + VERIFY_ARE_EQUAL(StringReference(L"1"), m_standardViewModel->ExpressionTokens->GetAt(0)->Token); + VERIFY_ARE_EQUAL(StringReference(L" "), m_standardViewModel->ExpressionTokens->GetAt(1)->Token); + VERIFY_ARE_EQUAL(StringReference(L"+"), m_standardViewModel->ExpressionTokens->GetAt(2)->Token); + VERIFY_ARE_EQUAL(StringReference(L" "), m_standardViewModel->ExpressionTokens->GetAt(3)->Token); + VERIFY_ARE_EQUAL(StringReference(L"5"), m_standardViewModel->ExpressionTokens->GetAt(4)->Token); + VERIFY_ARE_EQUAL(StringReference(L" "), m_standardViewModel->ExpressionTokens->GetAt(5)->Token); + VERIFY_ARE_EQUAL(StringReference(L"+"), m_standardViewModel->ExpressionTokens->GetAt(6)->Token); + VERIFY_ARE_EQUAL(StringReference(L" "), m_standardViewModel->ExpressionTokens->GetAt(7)->Token); + Cleanup(windowId); + } + + void HistoryItemLoadAndContinueCalculation(unsigned int windowId = 0) + { + Initialize(windowId); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeBasic); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command5); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command3); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + + auto historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + String^ expression = StringReference(historyItem->historyItemVector.expression.c_str()); + String^ result = StringReference(historyItem->historyItemVector.result.c_str()); + HistoryItemViewModel ^ item = ref new HistoryItemViewModel(expression, result, historyItem->historyItemVector.spTokens, historyItem->historyItemVector.spCommands); + MockOnHistoryItemClicked(item); + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command5); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + VERIFY_ARE_EQUAL(StringReference(L"14"), m_standardViewModel->DisplayValue); + historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(0); + expression = StringReference(historyItem->historyItemVector.expression.c_str()); + result = StringReference(historyItem->historyItemVector.result.c_str()); + item = ref new HistoryItemViewModel(expression, result, historyItem->historyItemVector.spTokens, historyItem->historyItemVector.spCommands); + MockOnHistoryItemClicked(item); + VERIFY_ARE_EQUAL(StringReference(L"9"), m_standardViewModel->DisplayValue); + + historyItem = m_standardViewModel->m_standardCalculatorManager->GetHistoryItem(1); + expression = StringReference(historyItem->historyItemVector.expression.c_str()); + result = StringReference(historyItem->historyItemVector.result.c_str()); + item = ref new HistoryItemViewModel(expression, result, historyItem->historyItemVector.spTokens, historyItem->historyItemVector.spCommands); + MockOnHistoryItemClicked(item); + VERIFY_ARE_EQUAL(StringReference(L"14"), m_standardViewModel->DisplayValue); + Cleanup(windowId); + } + + void DisplayValueAutomationNames(unsigned int windowId = 0) + { + Initialize(windowId); + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command8); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + String ^expression = StringReference(L"Display is 9"); + VERIFY_ARE_EQUAL(expression, m_standardViewModel->CalculationResultAutomationName); + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command5); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + expression = StringReference(L"Display is 6"); + VERIFY_ARE_EQUAL(expression, m_standardViewModel->CalculationResultAutomationName); + + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeProgrammer); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command2); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + expression = StringReference(L"Display is 3"); + VERIFY_ARE_EQUAL(expression, m_standardViewModel->CalculationResultAutomationName); + + Cleanup(windowId); + } + + void RadixAutomationName(unsigned int windowId = 0) + { + Initialize(windowId); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeProgrammer); + m_standardViewModel->IsProgrammer = true; + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command1); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandADD); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::Command7); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::CommandEQU); + String ^expression = L"HexaDecimal" + L" 8"; + String ^result = L"HexaDecimal " + Utils::GetStringValue(m_standardViewModel->HexDisplayValue); + VERIFY_ARE_EQUAL(expression, result); + expression = StringReference(L"Octal 10"); + result = L"Octal " + Utils::GetStringValue(m_standardViewModel->OctalDisplayValue); + VERIFY_ARE_EQUAL(expression, result); + expression = StringReference(L"Binary 1000"); + result = L"Binary " + Utils::GetStringValue(m_standardViewModel->BinaryDisplayValue); + VERIFY_ARE_EQUAL(expression, result); + Cleanup(windowId); + } + + void HistoryEmpty(unsigned int windowId = 0) + { + Initialize(windowId); + VERIFY_ARE_EQUAL(0, m_historyViewModel->ItemSize); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + VERIFY_ARE_EQUAL(0, m_historyViewModel->ItemSize); + Cleanup(windowId); + } + + void HistoryClearCommandWithEmptyHistory(unsigned int windowId = 0) + { + Initialize(windowId); + VERIFY_ARE_EQUAL(0, m_historyViewModel->ItemSize); + m_standardViewModel->m_standardCalculatorManager->SendCommand(Command::ModeScientific); + m_historyViewModel->OnClearCommand(nullptr); + VERIFY_ARE_EQUAL(0, m_historyViewModel->ItemSize); + Cleanup(windowId); + + } + }; + + void HistoryTests::TestHistoryItemAddSingleItem() + { + AddSingleHistoryItem(); + } + + void HistoryTests::TestHistoryItemAddMaxItems() + { + AddMaxHistoryItems(); + } + + void HistoryTests::TestReLoadHistory() + { + ReloadHistory(); + } + + void HistoryTests::TestHistoryClearCommand() + { + ClearHistory(); + } + + void HistoryTests::TestSerializeDeSerializeHistoryItem() + { + SerializeDeSerializeHistoryItem(); + } + + void HistoryTests::TestSaveAndReloadHistory() + { + SaveAndReloadHistory(); + } + + void HistoryTests::TestHistoryItemWithPrettyExpressions() + { + HistoryItemWithPrettyExpressions(); + } + + void HistoryTests::TestHistoryItemWithPrettyExpressionsMixedRadix() + { + HistoryItemWithPrettyExpressionsMixedRadix(); + } + + void HistoryTests::TestHistoryItemClicked() + { + HistoryItemClicked(); + } + + void HistoryTests::TestHistoryItemLoadAndContinueCalculation() + { + HistoryItemLoadAndContinueCalculation(); + } + + void HistoryTests::TestDisplayValueAutomationNames() + { + DisplayValueAutomationNames(); + } + + void HistoryTests::TestRadixAutomationName() + { + RadixAutomationName(); + } + + void HistoryTests::TestHistoryEmpty() + { + HistoryEmpty(); + } + + void HistoryTests::TestHistoryClearCommandWithEmptyHistory() + { + HistoryClearCommandWithEmptyHistory(); + } +} + diff --git a/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.cpp b/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.cpp new file mode 100644 index 00000000..9eb5d06f --- /dev/null +++ b/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.cpp @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "pch.h" +#include "CurrencyHttpClient.h" + +#include "CalcViewModel\Common\NetworkManager.h" + +using namespace CalculatorApp::DataLoaders; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::System::UserProfile; +using namespace Windows::Web::Http; + +// Generic responses so unit tests will pass. +static constexpr auto STATIC_DATA_RESPONSE = LR"([{"CountryCode":"USA","CountryName":"United States","CurrencyCode":"USD","CurrencyName":"Dollar","CurrencySymbol":"$"},{"CountryCode":"EUR","CountryName":"Europe","CurrencyCode":"EUR","CurrencyName":"Euro","CurrencySymbol":"€"}])"; +static constexpr auto ALL_RATIOS_RESPONSE = LR"([{"An":"USD","Ch":0,"Pc":0,"Rt":1},{"An":"EUR","Ch":0.003803,"Pc":0.4149,"Rt":0.920503,"Yh":0.9667,"Yl":0.86701}])"; + +CurrencyHttpClient::CurrencyHttpClient() +{ +} + +String^ CurrencyHttpClient::GetRawStaticDataResponse() +{ + return StringReference(STATIC_DATA_RESPONSE); +} + +String^ CurrencyHttpClient::GetRawAllRatiosDataResponse() +{ + return StringReference(ALL_RATIOS_RESPONSE); +} + +IAsyncOperationWithProgress^ CurrencyHttpClient::GetCurrencyMetadata() +{ + return ref new MockAsyncOperationWithProgress(StringReference(STATIC_DATA_RESPONSE)); +} + +IAsyncOperationWithProgress^ CurrencyHttpClient::GetCurrencyRatios() +{ + return ref new MockAsyncOperationWithProgress(StringReference(ALL_RATIOS_RESPONSE)); +} + +MockAsyncOperationWithProgress::MockAsyncOperationWithProgress(String^ result) : + m_result(result) +{ +} + +HResult MockAsyncOperationWithProgress::ErrorCode::get() +{ + HResult okayResult; + okayResult.Value = S_OK; + return okayResult; +} + diff --git a/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.h b/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.h new file mode 100644 index 00000000..e9bff412 --- /dev/null +++ b/internal/CalculatorUnitTests/Mocks/CurrencyHttpClient.h @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "CalcViewModel\DataLoaders\ICurrencyHttpClient.h" + +namespace CalculatorApp +{ + namespace DataLoaders + { + class CurrencyHttpClient : public ICurrencyHttpClient + { + public: + CurrencyHttpClient(); + + static Platform::String^ GetRawStaticDataResponse(); + static Platform::String^ GetRawAllRatiosDataResponse(); + + // ICurrencyHttpClient + void SetSourceCurrencyCode(Platform::String^ sourceCurrencyCode) override {} + void SetResponseLanguage(Platform::String^ responseLanguage) override {} + + virtual Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyMetadata() override; + virtual Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyRatios() override; + // ICurrencyHttpClient + }; + + public ref class MockAsyncOperationWithProgress sealed : + public Windows::Foundation::IAsyncOperationWithProgress + { + public: + MockAsyncOperationWithProgress(Platform::String^ result); + + // IAsyncInfo + virtual property Windows::Foundation::HResult ErrorCode + { + Windows::Foundation::HResult get(); + } + + virtual property unsigned int Id + { + unsigned int get() { return 128u; } + } + + virtual property Windows::Foundation::AsyncStatus Status + { + Windows::Foundation::AsyncStatus get() { return Windows::Foundation::AsyncStatus::Completed; } + } + + virtual void Cancel() {} + virtual void Close() {} + // IAsyncInfo + + // IAsyncOperationWithProgress + virtual property Windows::Foundation::AsyncOperationProgressHandler^ Progress + { + Windows::Foundation::AsyncOperationProgressHandler^ get() { return nullptr; } + void set(Windows::Foundation::AsyncOperationProgressHandler^ handler) {} + } + + virtual property Windows::Foundation::AsyncOperationWithProgressCompletedHandler^ Completed + { + Windows::Foundation::AsyncOperationWithProgressCompletedHandler^ get() { return nullptr; } + void set(Windows::Foundation::AsyncOperationWithProgressCompletedHandler^ handler) {} + } + + virtual Platform::String^ GetResults() { return m_result; } + // IAsyncOperationWithProgress + + private: + Platform::String^ m_result; + }; + } +} diff --git a/internal/CalculatorUnitTests/Module.cpp b/internal/CalculatorUnitTests/Module.cpp new file mode 100644 index 00000000..3cb0d4c3 --- /dev/null +++ b/internal/CalculatorUnitTests/Module.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +namespace CalculatorUnitTests +{ + BEGIN_MODULE() + MODULE_PROPERTY(L"APPX:CertificateFileName", L"CalculatorUnitTests.cer:TrustedPeople") + END_MODULE() + + MODULE_SETUP(ModuleSetup) + { + return true; + } + + MODULE_CLEANUP(ModuleCleanup) + { + return true; + } +} diff --git a/internal/CalculatorUnitTests/MultiWindowUnitTests.cpp b/internal/CalculatorUnitTests/MultiWindowUnitTests.cpp new file mode 100644 index 00000000..6b8887c0 --- /dev/null +++ b/internal/CalculatorUnitTests/MultiWindowUnitTests.cpp @@ -0,0 +1,1012 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include "UnitConverterViewModelUnitTests.h" +#include "DateUtils.h" + +#include "CalcViewModel\StandardCalculatorViewModel.h" +#include "CalcViewModel\UnitConverterViewModel.h" +#include "CalcViewModel\DateCalculatorViewModel.h" +#include "CalcViewModel\DataLoaders\UnitConverterDataLoader.h" + +using namespace Platform; +using namespace Platform::Collections; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace CalculationManager; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Devices::Input; +using namespace Windows::Foundation::Collections; +using namespace Windows::Globalization; +using namespace Utils; +using namespace DateCalculationUnitTests; + +namespace CalculatorUnitTests +{ + extern void ChangeMode(StandardCalculatorViewModel^ viewModel, int mode); + extern void ValidateViewModelByCommands(StandardCalculatorViewModel^ viewModel, TESTITEM item[], bool doReset = false); + + // Validated the Mode set for a given instance of Standard Calculator View Model + void ValidateViewModelMode(StandardCalculatorViewModel^ viewModel, int mode) + { + // Standard + if (mode == 0) + { + VERIFY_IS_TRUE(viewModel->IsStandard); + VERIFY_IS_FALSE(viewModel->IsScientific); + VERIFY_IS_FALSE(viewModel->IsProgrammer); + } + // Scientific + else if (mode == 1) + { + VERIFY_IS_FALSE(viewModel->IsStandard); + VERIFY_IS_TRUE(viewModel->IsScientific); + VERIFY_IS_FALSE(viewModel->IsProgrammer); + } + // Programmer + else if (mode == 2) + { + VERIFY_IS_FALSE(viewModel->IsStandard); + VERIFY_IS_FALSE(viewModel->IsScientific); + VERIFY_IS_TRUE(viewModel->IsProgrammer); + } + } + + // Test class containing Test Methods to validate Multi Window + class MultiWindowUnitTests + { + public: + TEST_CLASS(MultiWindowUnitTests); + + // Create 3 instances of StandardCalculatorViewModel + TEST_METHOD(InitializeMultipleInstancesTest) + { + std::vector viewModels(3); + + // Create 3 instances of StandardCalculatorViewModel + for (int i = 0; i < 3; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + viewModels[i]->IsStandard = true; + } + + // Assert that the Display Value is "0" for all instances + for (int i = 0; i < 3; i++) + { + VERIFY_IS_TRUE("0" == viewModels[i]->DisplayValue); + } + } + + // Create 3 separate instances of Calulator in different modes + TEST_METHOD(InitializeMultipleModeInstancesTest) + { + std::vector viewModels(3); + + // Create 3 instances of StandardCalculatorViewModel in different modes + for (int i = 0; i < 3; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + ChangeMode(viewModels[i], i); + } + + // Assert for the Modes and DisplayValues + for (int i = 0; i < 3; i++) + { + ValidateViewModelMode(viewModels[i], i); + + VERIFY_IS_TRUE("0" == viewModels[i]->DisplayValue); + } + } + + // Perform calculations on diferent calculator modes and verify that they work independently + TEST_METHOD(MultipleModesCalculationTest) + { + std::vector viewModels(3); + + // Create 3 instances of StandardCalculatorViewModel in different modes + for (int i = 0; i < 3; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + ChangeMode(viewModels[i], i); + } + + // Perform Calculations on all instances and check that they work independently + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[0], standardModeTestItems, true); + + // Scientific Mode: Expression 1+2*3 + TESTITEM scientificModeTestItems[] = { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[1], scientificModeTestItems, true); + + // Programmer Mode: Expression F + TESTITEM programmerModeTestItems[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[2], programmerModeTestItems, false); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[2]->HexDisplayValue), StringReference(L"F")); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[2]->DecimalDisplayValue), StringReference(L"15")); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[2]->OctalDisplayValue), StringReference(L"17")); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[2]->BinaryDisplayValue), StringReference(L"1111")); + + // Assert that Standard and Scientific mode Display Values are unchanged + VERIFY_ARE_EQUAL(GetStringValue(viewModels[0]->DisplayValue), StringReference(L"3")); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[1]->DisplayValue), StringReference(L"7")); + } + + // Perform calculations on 2 instances of Calculator in Standard Mode and verify that they work independently + TEST_METHOD(MultipleStandardModeCalculationTest) + { + // Create 2 instances of Standard Mode + StandardCalculatorViewModel^ standardViewModel1 = ref new StandardCalculatorViewModel(); + StandardCalculatorViewModel^ standardViewModel2 = ref new StandardCalculatorViewModel(); + + ChangeMode(standardViewModel1, 0); + ChangeMode(standardViewModel2, 0); + + ValidateViewModelMode(standardViewModel1, 0); + ValidateViewModelMode(standardViewModel2, 0); + + // Perform Calculations on the 2 instances and check that they work independently + + // Standard Mode 1: Expression 3-2= + TESTITEM standardModeTestItems1[] = { + { NumbersAndOperatorsEnum::Three, L"3", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"3", L"3 - " }, + { NumbersAndOperatorsEnum::Two, L"2", L"3 - " }, + { NumbersAndOperatorsEnum::Equals, L"1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(standardViewModel1, standardModeTestItems1, true); + + // Standard Mode 2: Expression 4*5= + TESTITEM standardModeTestItems2[] = { + { NumbersAndOperatorsEnum::Four, L"4", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"4", L"4 * " }, + { NumbersAndOperatorsEnum::Five, L"5", L"4 * " }, + { NumbersAndOperatorsEnum::Equals, L"20", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(standardViewModel2, standardModeTestItems2, true); + + // Assert that the Display Value of 1st instance is unchanged + VERIFY_ARE_EQUAL(GetStringValue(standardViewModel1->DisplayValue), StringReference(L"1")); + } + + // Perform calculations on 2 instances of Calculator in Scientific Mode and verify that they work independently + TEST_METHOD(MultipleScientificModeCalculationTest) + { + // Create 2 instances of Standard Mode + StandardCalculatorViewModel^ scientificViewModel1 = ref new StandardCalculatorViewModel(); + StandardCalculatorViewModel^ scientificViewModel2 = ref new StandardCalculatorViewModel(); + + ChangeMode(scientificViewModel1, 1); + ChangeMode(scientificViewModel2, 1); + + ValidateViewModelMode(scientificViewModel1, 1); + ValidateViewModelMode(scientificViewModel2, 1); + + // Perform Calculations on the 2 instances and check that they work independently + + // Standard Mode 1: Expression 3-2= + TESTITEM scientificModeTestItems1[] = { + { NumbersAndOperatorsEnum::Three, L"3", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"3", L"3 - " }, + { NumbersAndOperatorsEnum::Two, L"2", L"3 - " }, + { NumbersAndOperatorsEnum::Equals, L"1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel1, scientificModeTestItems1, true); + + // Standard Mode 2: Expression 4*5= + TESTITEM scientificModeTestItems2[] = { + { NumbersAndOperatorsEnum::Four, L"4", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"4", L"4 * " }, + { NumbersAndOperatorsEnum::Five, L"5", L"4 * " }, + { NumbersAndOperatorsEnum::Equals, L"20", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel2, scientificModeTestItems2, true); + + // Assert that the Display Value of 1st instance is unchanged + VERIFY_ARE_EQUAL(GetStringValue(scientificViewModel1->DisplayValue), StringReference(L"1")); + } + + // Perform calculations on 2 instances of Calculator in Scientific Mode + // (with different Angle types, HYP and F-E settings) and verify that they work independently + TEST_METHOD(MultipleScientificModeWithDifferentSettingsTest) + { + // Create 2 instances of Standard Mode + StandardCalculatorViewModel^ scientificViewModel1 = ref new StandardCalculatorViewModel(); + StandardCalculatorViewModel^ scientificViewModel2 = ref new StandardCalculatorViewModel(); + + ChangeMode(scientificViewModel1, 1); + ChangeMode(scientificViewModel2, 1); + + ValidateViewModelMode(scientificViewModel1, 1); + ValidateViewModelMode(scientificViewModel2, 1); + + // Perform Calculations on the 2 instances and check that they work independently + + // Scientific Mode 1: Degrees with HYP checked + TESTITEM scientificModeInitializeItems1[] = { + { NumbersAndOperatorsEnum::Degree, L"0", L"" }, + { NumbersAndOperatorsEnum::Hyp, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel1, scientificModeInitializeItems1, true); + + // Scientific Mode 2: Radians with F-E checked + TESTITEM scientificModeInitializeItems2[] = { + { NumbersAndOperatorsEnum::Radians, L"0", L"" }, + { NumbersAndOperatorsEnum::FToE, L"0.e+0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel2, scientificModeInitializeItems2, true); + + // Scientific Mode 1: Expression CosH(0 degrees) + TESTITEM scientificModeTestItems1[] = { + { NumbersAndOperatorsEnum::Zero, L"0", L"" }, + { NumbersAndOperatorsEnum::Cosh, L"1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel1, scientificModeTestItems1, true); + + // Scientific Mode 2: Expression Cos(pi radians) + TESTITEM scientificModeTestItems2[] = { + { NumbersAndOperatorsEnum::Pi, L"3.1415926535897932384626433832795e+0", L"" }, + { NumbersAndOperatorsEnum::Cos, L"-1.e+0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(scientificViewModel2, scientificModeTestItems2, true); + } + + // Perform calculations on 2 instances of Calculator in Programmer Mode and verify that they work independently + TEST_METHOD(MultipleProgrammerModeCalculationTest) + { + // Create 2 instances of Standard Mode + StandardCalculatorViewModel^ programmerViewModel1 = ref new StandardCalculatorViewModel(); + StandardCalculatorViewModel^ programmerViewModel2 = ref new StandardCalculatorViewModel(); + + ChangeMode(programmerViewModel1, 2); + ChangeMode(programmerViewModel2, 2); + + ValidateViewModelMode(programmerViewModel1, 2); + ValidateViewModelMode(programmerViewModel2, 2); + + // Perform Calculations on the 2 instances and check that they work independently + + // Radix Type: Hexadecimal, Expression: F + TESTITEM programmerModeTestItems1[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel1, programmerModeTestItems1, false); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->HexDisplayValue), StringReference(L"F")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->DecimalDisplayValue), StringReference(L"15")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->OctalDisplayValue), StringReference(L"17")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->BinaryDisplayValue), StringReference(L"1111")); + + // Radix Type: Octal, Expression: 7 + TESTITEM programmerModeTestItems2[] = { + { NumbersAndOperatorsEnum::OctButton, L"0", L"" }, + { NumbersAndOperatorsEnum::Seven, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel2, programmerModeTestItems2, false); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->HexDisplayValue), StringReference(L"7")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->DecimalDisplayValue), StringReference(L"7")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->OctalDisplayValue), StringReference(L"7")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->BinaryDisplayValue), StringReference(L"0111")); + + // Assert that displayed values of 1st instance are unchanged + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->DisplayValue), StringReference(L"F")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->HexDisplayValue), StringReference(L"F")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->DecimalDisplayValue), StringReference(L"15")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->OctalDisplayValue), StringReference(L"17")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->BinaryDisplayValue), StringReference(L"1111")); + } + + // Perform calculations on 2 instances of Calculator in Programmer Mode + // (with different Bit lengths and Radix types) and verify that they work independently + TEST_METHOD(MultipleProgrammerModeWithDifferentSettingsTest) + { + // Create 2 instances of Standard Mode + StandardCalculatorViewModel^ programmerViewModel1 = ref new StandardCalculatorViewModel(); + StandardCalculatorViewModel^ programmerViewModel2 = ref new StandardCalculatorViewModel(); + + ChangeMode(programmerViewModel1, 2); + ChangeMode(programmerViewModel2, 2); + + ValidateViewModelMode(programmerViewModel1, 2); + ValidateViewModelMode(programmerViewModel2, 2); + + // Perform Calculations on the 2 instances and check that they work independently + + // Bit length: Byte & Radix Type: Hex + TESTITEM programmerModeInitializeItems1[] = { + { NumbersAndOperatorsEnum::Byte, L"0", L"" }, + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel1, programmerModeInitializeItems1, false); + + // Bit Length: Word & Radix Type: Oct + TESTITEM programmerModeInitializeItems2[] = { + { NumbersAndOperatorsEnum::Word, L"0", L"" }, + { NumbersAndOperatorsEnum::OctButton, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel2, programmerModeInitializeItems2, false); + + TESTITEM programmerModeTestItems1[] = { + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::F, L"FF", L"" }, + // One more F shouldn't have any effect, testing for precision + { NumbersAndOperatorsEnum::F, L"FF", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel1, programmerModeTestItems1, false); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->HexDisplayValue), StringReference(L"FF")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->DecimalDisplayValue), StringReference(L"-1")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->OctalDisplayValue), StringReference(L"377")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel1->BinaryDisplayValue), StringReference(L"1111 1111")); + + TESTITEM programmerModeTestItems2[] = { + { NumbersAndOperatorsEnum::Seven, L"7", L"" }, + { NumbersAndOperatorsEnum::Seven, L"77", L"" }, + { NumbersAndOperatorsEnum::Seven, L"777", L"" }, + { NumbersAndOperatorsEnum::Seven, L"7 777", L"" }, + { NumbersAndOperatorsEnum::Seven, L"77 777", L"" }, + // One more '7' shouldn't have any effect, testing for precision + { NumbersAndOperatorsEnum::Seven, L"77 777", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + + ValidateViewModelByCommands(programmerViewModel2, programmerModeTestItems2, false); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->HexDisplayValue), StringReference(L"7FFF")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->DecimalDisplayValue), StringReference(L"32,767")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->OctalDisplayValue), StringReference(L"77 777")); + VERIFY_ARE_EQUAL(GetStringValue(programmerViewModel2->BinaryDisplayValue), StringReference(L"0111 1111 1111 1111")); + } + + // Perform calculations on 2 separate instances of Calculator and verify that their History list items are maintained separately + TEST_METHOD(MultipleModesHistoryAddItemTest) + { + std::vector viewModels(2); + + // Create 3 instances of StandardCalculatorViewModel in Standard and Scientific mode + for (int i = 0; i < 2; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + ChangeMode(viewModels[i], i); + + // Validate that the history items list is initially empty + VERIFY_IS_TRUE(0 == viewModels[i]->m_standardCalculatorManager->GetHistoryItems().size()); + } + + // Perform Calculations on both the instances and check that the History items work independently + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[0], standardModeTestItems, true); + + // Scientific Mode: Expression 1+2*3 + TESTITEM scientificModeTestItems[] = { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[1], scientificModeTestItems, true); + + // Assert for the history list items of 1st instance + VERIFY_IS_TRUE(1 == viewModels[0]->m_standardCalculatorManager->GetHistoryItems().size()); + + auto item1 = viewModels[0]->m_standardCalculatorManager->GetHistoryItem(0); + String ^expression1 = UtfUtils::LRO + L"1 + 2 =" + UtfUtils::PDF; + String^ result1 = L"3"; + + VERIFY_ARE_EQUAL(expression1, StringReference(item1->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result1, StringReference(item1->historyItemVector.result.c_str())); + + // Assert for the history list items of 2nd instance + VERIFY_IS_TRUE(1 == viewModels[1]->m_standardCalculatorManager->GetHistoryItems().size()); + + auto item2 = viewModels[1]->m_standardCalculatorManager->GetHistoryItem(0); + String^ expression2 = UtfUtils::LRO + L"1 + 2 " + UtfUtils::MUL + L" 3 =" + UtfUtils::PDF; + String^ result2 = L"7"; + + VERIFY_ARE_EQUAL(expression2, StringReference(item2->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result2, StringReference(item2->historyItemVector.result.c_str())); + } + + // Perform calculations on 2 separate instances of Standard Modes and verify that their History list items are maintained separately + TEST_METHOD(MultipleStandardModesHistoryAddItemTest) + { + std::vector viewModels(2); + + // Create 3 instances of StandardCalculatorViewModel in Standard and Scientific mode + for (int i = 0; i < 2; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + // Standard Mode + ChangeMode(viewModels[i], 0); + + // Validate that the history items list is initially empty + VERIFY_IS_TRUE(0 == viewModels[i]->m_standardCalculatorManager->GetHistoryItems().size()); + } + + // Perform Calculations on both the instances and check that the History items work independently + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems[2][8] = { + { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }, + { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + } + }; + + // Run the commands + for (int i = 0; i < 2; i++) + { + ValidateViewModelByCommands(viewModels[i], standardModeTestItems[i], true); + } + + String^ expression[] = { UtfUtils::LRO + L"1 + 2 =" + UtfUtils::PDF, UtfUtils::LRO + L"1 + 2 " + UtfUtils::MUL + L" 3 =" + UtfUtils::PDF }; + String^ result[] = { L"3", L"7" }; + + // Assert for the history list items of the instances + for (int i = 0; i < 2; i++) + { + VERIFY_IS_TRUE(1 == viewModels[i]->m_standardCalculatorManager->GetHistoryItems().size()); + + auto item = viewModels[i]->m_standardCalculatorManager->GetHistoryItem(0); + + VERIFY_ARE_EQUAL(expression[i], StringReference(item->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result[i], StringReference(item->historyItemVector.result.c_str())); + } + } + + // Perform calculations on 2 separate instances of Scientific Modes and verify that their History list items are maintained separately + TEST_METHOD(MultipleScientificModesHistoryAddItemTest) + { + std::vector viewModels(2); + + // Create 3 instances of StandardCalculatorViewModel in Standard and Scientific mode + for (int i = 0; i < 2; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + // Scientific Mode + ChangeMode(viewModels[i], 1); + + // Validate that the history items list is initially empty + VERIFY_IS_TRUE(0 == viewModels[i]->m_standardCalculatorManager->GetHistoryItems().size()); + } + + // Perform Calculations on both the instances and check that the History items work independently + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems[2][8] = { + { + { NumbersAndOperatorsEnum::IsStandardMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }, + { + { NumbersAndOperatorsEnum::IsStandardMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"9", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + } + }; + + // Run the commands + for (int i = 0; i < 2; i++) + { + ValidateViewModelByCommands(viewModels[i], standardModeTestItems[i], true); + } + + String^ expression[] = { UtfUtils::LRO + L"1 + 2 =" + Utils::PDF, UtfUtils::LRO + L"1 + 2 " + UtfUtils::MUL + L" 3 =" + Utils::PDF }; + String^ result[] = { L"3", L"9" }; + + // Assert for the history list items of the instances + for (int i = 0; i < 2; i++) + { + VERIFY_IS_TRUE(1 == viewModels[i]->m_standardCalculatorManager->GetHistoryItems().size()); + + auto item = viewModels[i]->m_standardCalculatorManager->GetHistoryItem(0); + + VERIFY_ARE_EQUAL(expression[i], StringReference(item->historyItemVector.expression.c_str())); + VERIFY_ARE_EQUAL(result[i], StringReference(item->historyItemVector.result.c_str())); + } + } + + // Perform calculations on 3 separate instances of Calcuator and verify that their Memory List items are maintained separately + TEST_METHOD(MultipleModesMemoryAddItemTest) + { + std::vector viewModels(3); + + // Create 3 instances of StandardCalculatorViewModel in Standard and Scientific mode + for (int i = 0; i < 3; i++) + { + viewModels[i] = ref new StandardCalculatorViewModel(); + + ChangeMode(viewModels[i], i); + + // Validate that the history items list is initially empty + VERIFY_IS_TRUE(0 == viewModels[i]->MemorizedNumbers->Size); + } + + // Perform Calculations on both the instances and check that the Memory items work independently + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[0], standardModeTestItems, true); + + // Scientific Mode: Expression 1+2*3 + TESTITEM scientificModeTestItems[] = { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[1], scientificModeTestItems, true); + + // Programmer Mode: Expression F + TESTITEM programmerModeTestItems[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[2], programmerModeTestItems, false); + + // Press the Memory Button to save Values to Memory + for (int i = 0; i < 3; i++) + { + viewModels[i]->ButtonPressed->Execute(NumbersAndOperatorsEnum::Memory); + } + + String^ expectedMemoryValues[] = { + UtfUtils::LRO + L"3" + UtfUtils::PDF, + UtfUtils::LRO + L"7" + UtfUtils::PDF, + UtfUtils::LRO + L"F" + UtfUtils::PDF}; + + // Validate that only one item is present in the memory + // Also assert for their value + for (int i = 0; i < 3; i++) + { + VERIFY_IS_TRUE(1 == viewModels[i]->MemorizedNumbers->Size); + auto memorySlot = viewModels[i]->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(expectedMemoryValues[i], memorySlot->Value); + } + } + + TEST_METHOD(MultipleDateCalculatorTest) + { + // TODO - MSFT 10331900, fix this test + // This test mostly passes, but the last set of VERIFYs succeeds for + // the check of viewModels[2]->StrDateResult when in a UTC- time, + // however, both the expected and actual results are incorrect. Test + // needs to be updated with correct expected value and viewmodel needs + // to be updated to calculate correct value. + + //DateCalculatorViewModel^ viewModels[4]; + + //// Initialize the view models + //for (int i = 0; i < 4; i++) + //{ + // auto vm = ref new DateCalculatorViewModel(); + // vm->Initialize(); + + // viewModels[i] = vm; + //} + + //viewModels[2]->IsDateDiffMode = false; + //viewModels[3]->IsDateDiffMode = false; + + //viewModels[2]->IsAddMode = true; + //viewModels[3]->IsAddMode = false; + + //// Verify the initialization + //for (int i = 0; i < 2; i++) + //{ + // VERIFY_IS_TRUE(viewModels[i]->IsDateDiffMode); + // VERIFY_IS_TRUE(viewModels[i]->FromDate.UniversalTime != 0); + // VERIFY_IS_TRUE(viewModels[i]->ToDate.UniversalTime != 0); + // VERIFY_ARE_EQUAL(StringReference(L"Same dates"), viewModels[i]->StrDateDiffResult); + // VERIFY_IS_NULL(viewModels[i]->StrDateDiffResultInDays); + //} + + //for (int i = 2; i < 4; i++) + //{ + // VERIFY_IS_FALSE(viewModels[i]->IsDateDiffMode); + // VERIFY_IS_TRUE(viewModels[i]->DaysOffset == 0); + // VERIFY_IS_TRUE(viewModels[i]->MonthsOffset == 0); + // VERIFY_IS_TRUE(viewModels[i]->YearsOffset == 0); + // VERIFY_IS_NOT_NULL(viewModels[i]->StrDateResult); + // VERIFY_IS_TRUE(StringReference(L"") != viewModels[i]->StrDateResult); + //} + + //VERIFY_IS_TRUE(viewModels[2]->IsAddMode); + //VERIFY_IS_FALSE(viewModels[3]->IsAddMode); + + //// Perform some calculations + //// Diff in viewModels[0] + //SYSTEMTIME date1, date2, resultDate; + ///* 01-10-2015 */ date1.wDay = 1; date1.wMonth = 10; date1.wYear = 2015; date1.wDayOfWeek = 4; date1.wHour = 0; date1.wMinute = 0; date1.wSecond = 0; date1.wMilliseconds = 0; + ///* 15-02-2016 */ date2.wDay = 15; date2.wMonth = 2; date2.wYear = 2016; date2.wDayOfWeek = 1; date2.wHour = 0; date2.wMinute = 0; date2.wSecond = 0; date2.wMilliseconds = 0; + + //viewModels[0]->FromDate = DateUtils::SystemTimeToDateTime(date1); + //viewModels[0]->ToDate = DateUtils::SystemTimeToDateTime(date2); + + //// Diff in viewModels[1] + ///* 12-12-2015 */ date1.wDay = 12; date1.wMonth = 12; date1.wYear = 2015; date1.wDayOfWeek = 6; + ///* 15-12-2015 */ date2.wDay = 15; date2.wMonth = 12; date2.wYear = 2015; date2.wDayOfWeek = 2; + ///* 17-01-2018 */ resultDate.wDay = 17; resultDate.wMonth = 1; resultDate.wYear = 2018; resultDate.wDayOfWeek = 3; resultDate.wHour = 0; resultDate.wMinute = 0; resultDate.wSecond = 0; resultDate.wMilliseconds = 0; + + //viewModels[1]->FromDate = DateUtils::SystemTimeToDateTime(date1); + //viewModels[1]->ToDate = DateUtils::SystemTimeToDateTime(date2); + + //// Add in viewModels[2] + //viewModels[2]->StartDate = DateUtils::SystemTimeToDateTime(date1); + //viewModels[2]->DaysOffset = 5; + //viewModels[2]->MonthsOffset = 1; + //viewModels[2]->YearsOffset = 2; + + //// Subtract OOB in viewModels[3] + //viewModels[3]->StartDate = DateUtils::SystemTimeToDateTime(date2); + //viewModels[3]->DaysOffset = 5; + //viewModels[3]->MonthsOffset = 1; + //viewModels[3]->YearsOffset = 500; + + //// Assert for the result + //VERIFY_IS_FALSE(viewModels[0]->IsDiffInDays); + //VERIFY_ARE_EQUAL(StringReference(L"137 days"), viewModels[0]->StrDateDiffResultInDays); + //VERIFY_ARE_EQUAL(StringReference(L"4 months, 2 weeks"), viewModels[0]->StrDateDiffResult); + + //VERIFY_IS_TRUE(viewModels[1]->IsDiffInDays); + //VERIFY_ARE_EQUAL(StringReference(L"3 days"), viewModels[1]->StrDateDiffResult); + //VERIFY_IS_NULL(viewModels[1]->StrDateDiffResultInDays); + + //// TODO - MSFT 10331900 : both GetLongDate and viewmodel return incorrect values! + //VERIFY_ARE_EQUAL(DateUtils::GetLongDate(resultDate), viewModels[2]->StrDateResult); + //VERIFY_ARE_EQUAL(StringReference(L"Date out of Bound"), viewModels[3]->StrDateResult); + } + + TEST_METHOD(InitializeMultipleConverterTest) + { + std::shared_ptr unitConverterMocks[3]; + UnitConverterViewModel^ viewModels[3]; + + for (int i = 0; i < 3; i++) + { + unitConverterMocks[i] = std::make_shared(); + viewModels[i] = ref new UnitConverterViewModel(unitConverterMocks[i]); + IObservableVector^ cats = viewModels[i]->Categories; + VERIFY_ARE_EQUAL((UINT)1, unitConverterMocks[i]->m_getCategoriesCallCount); + VERIFY_ARE_EQUAL((UINT)3, cats->Size); + // Verify that we match current category + VERIFY_IS_TRUE(CAT2 == viewModels[i]->CurrentCategory->GetModelCategory()); + } + + // Change category of 1st instance to CAT1 and that of 2nd instance to CAT2 + viewModels[0]->CurrentCategory = viewModels[0]->Categories->GetAt(0); + viewModels[2]->CurrentCategory = viewModels[1]->Categories->GetAt(2); + + // Verify that the instance properties were set independently + for (int i = 0; i < 2; i++) + { + VERIFY_ARE_EQUAL((UINT)3, viewModels[i]->Categories->Size); + VERIFY_ARE_EQUAL((UINT)3, viewModels[i]->Units->Size); + } + + VERIFY_IS_TRUE(CAT1 == viewModels[0]->CurrentCategory->GetModelCategory()); + VERIFY_IS_TRUE(CAT2 == viewModels[1]->CurrentCategory->GetModelCategory()); + VERIFY_IS_TRUE(CAT3 == viewModels[2]->CurrentCategory->GetModelCategory()); + + VERIFY_IS_TRUE(UNIT1 == viewModels[0]->Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT2 == viewModels[0]->Unit2->GetModelUnit()); + + VERIFY_IS_TRUE(UNIT4 == viewModels[1]->Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT6 == viewModels[1]->Unit2->GetModelUnit()); + + VERIFY_IS_TRUE(UNIT9 == viewModels[2]->Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT7 == viewModels[2]->Unit2->GetModelUnit()); + } + + TEST_METHOD(MultipleConverterModeCalculationTest) + { + UnitConverterViewModel^ viewModels[3]; + ResourceLoader^ resLoader = ResourceLoader::GetForViewIndependentUse("Test"); + + for (int i = 0; i < 3; i++) + { + viewModels[i] = ref new UnitConverterViewModel(std::make_shared(std::make_shared(ref new GeographicRegion()), nullptr)); + IObservableVector^ categories = viewModels[i]->Categories; + VERIFY_ARE_EQUAL((UINT)13, categories->Size); + } + + IObservableVector^ categoryList = viewModels[0]->Categories; + IObservableVector^ unitsList[3]; + + // viewModels 0 & 1 have same category(Volume) and viewModel 2 has different category(Length) + int volumeIndex = NavCategory::GetIndexInGroup(ViewMode::Volume, CategoryGroupType::Converter); + int lengthIndex = NavCategory::GetIndexInGroup(ViewMode::Length, CategoryGroupType::Converter); + viewModels[0]->CurrentCategory = categoryList->GetAt(volumeIndex); + viewModels[1]->CurrentCategory = categoryList->GetAt(volumeIndex); + viewModels[2]->CurrentCategory = categoryList->GetAt(lengthIndex); + + for (int i = 0; i < 3; i++) + { + unitsList[i] = viewModels[i]->Units; + } + + // Milliliters + viewModels[0]->Unit1 = unitsList[0]->GetAt(0); + // Cubic centimeters + viewModels[0]->Unit2 = unitsList[0]->GetAt(1); + + // Liters + viewModels[1]->Unit1 = unitsList[1]->GetAt(2); + // Cubic meters + viewModels[1]->Unit2 = unitsList[1]->GetAt(3); + + // Nanometers + viewModels[2]->Unit1 = unitsList[2]->GetAt(0); + // Microns + viewModels[2]->Unit2 = unitsList[2]->GetAt(1); + + // Check that the units in multiple instances got set correctly + VERIFY_IS_TRUE(unitsList[0]->GetAt(0) == viewModels[0]->Unit1); + VERIFY_IS_TRUE(unitsList[0]->GetAt(1) == viewModels[0]->Unit2); + + VERIFY_IS_TRUE(unitsList[1]->GetAt(2) == viewModels[1]->Unit1); + VERIFY_IS_TRUE(unitsList[1]->GetAt(3) == viewModels[1]->Unit2); + + VERIFY_IS_TRUE(unitsList[2]->GetAt(0) == viewModels[2]->Unit1); + VERIFY_IS_TRUE(unitsList[2]->GetAt(1) == viewModels[2]->Unit2); + + // Perform conversions + viewModels[0]->ButtonPressed->Execute(NumbersAndOperatorsEnum::One); + + viewModels[1]->ButtonPressed->Execute(NumbersAndOperatorsEnum::Two); + + viewModels[2]->ButtonPressed->Execute(NumbersAndOperatorsEnum::Three); + + // Validate Value1 and Value2 which is the result + for (int i = 0; i < 3; i++) + { + auto expectedValue1 = (i + 1).ToString(); + auto actualValue1 = viewModels[i]->Value1; + + VERIFY_ARE_EQUAL(expectedValue1, GetStringValue(actualValue1)); + + std::wstring unit1Name = viewModels[i]->Unit1->Name->Data(); + std::wstring unit2Name = viewModels[i]->Unit2->Name->Data(); + + auto resKey = String::Concat(ref new String((unit1Name + L"-" + unit2Name + L"-").c_str()), expectedValue1); + String^ expectedResult = resLoader->GetString(resKey); + + String^ actualResult = GetStringValue(viewModels[i]->Value2); + + VERIFY_ARE_EQUAL(expectedResult, actualResult); + } + } + + TEST_METHOD(TestStandardUnitConverterAndDateViewModels) + { + // Create instances of SCVM in Standard Mode and UnitConverterViewModel + StandardCalculatorViewModel^ standardViewModel = ref new StandardCalculatorViewModel(); + UnitConverterViewModel^ unitConverterViewModel = ref new UnitConverterViewModel(std::make_shared(std::make_shared(ref new GeographicRegion()), nullptr)); + DateCalculatorViewModel^ dateCalcViewModel = ref new DateCalculatorViewModel(); + + // Initialize Standard Calculator + ChangeMode(standardViewModel, 0); + + // Initialize Date Calculator + dateCalcViewModel->IsDateDiffMode = false; + dateCalcViewModel->IsAddMode = true; + + // Initialize Unit Converter + int volumeCategoryIndex = NavCategory::GetIndexInGroup(ViewMode::Volume, CategoryGroupType::Converter); + IObservableVector^ categories = unitConverterViewModel->Categories; + unitConverterViewModel->CurrentCategory = categories->GetAt(volumeCategoryIndex); + unitConverterViewModel->Unit1 = unitConverterViewModel->Units->GetAt(2); + unitConverterViewModel->Unit2 = unitConverterViewModel->Units->GetAt(3); + + // Validate Calculator mode and UC Category and Units + ValidateViewModelMode(standardViewModel, 0); + + VERIFY_ARE_EQUAL(categories->GetAt(volumeCategoryIndex), unitConverterViewModel->CurrentCategory); + VERIFY_ARE_EQUAL(unitConverterViewModel->Units->GetAt(2), unitConverterViewModel->Unit1); + VERIFY_ARE_EQUAL(unitConverterViewModel->Units->GetAt(3), unitConverterViewModel->Unit2); + + // Perform Calculations on these instances and check that they work independently + + // Standard Mode 1: Expression 3-2= + TESTITEM standardModeTestItems1[] = { + { NumbersAndOperatorsEnum::Three, L"3", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"3", L"3 - " }, + { NumbersAndOperatorsEnum::Two, L"2", L"3 - " }, + { NumbersAndOperatorsEnum::Equals, L"1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(standardViewModel, standardModeTestItems1, true); + + SYSTEMTIME startDate, endDate; + /* 01-10-2015 */ startDate.wDay = 1; startDate.wMonth = 10; startDate.wYear = 2015; startDate.wDayOfWeek = 4; startDate.wHour = 0; startDate.wMinute = 0; startDate.wSecond = 0; startDate.wMilliseconds = 0; + /* 02-12-2018 */ endDate.wDay = 2; endDate.wMonth = 12; endDate.wYear = 2018; endDate.wDayOfWeek = 0; endDate.wHour = 0; endDate.wMinute = 0; endDate.wSecond = 0; endDate.wMilliseconds = 0; + + dateCalcViewModel->StartDate = DateUtils::SystemTimeToDateTime(startDate); + dateCalcViewModel->DaysOffset = 1; + dateCalcViewModel->MonthsOffset = 2; + dateCalcViewModel->YearsOffset = 3; + + unitConverterViewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Two); + + // Validate the Result + VERIFY_ARE_EQUAL(StringReference(L"2"), GetStringValue(unitConverterViewModel->Value1)); + VERIFY_ARE_EQUAL(StringReference(L"0.002"), GetStringValue(unitConverterViewModel->Value2)); + + // Assert that the Display Value of Standard Calc instance is unchanged + VERIFY_ARE_EQUAL(GetStringValue(standardViewModel->DisplayValue), StringReference(L"1")); + + // Again perform calculations on Standard Calc instance and validate that the Converter remains unaffected + + // Standard Mode: Expression 1+2= + TESTITEM standardModeTestItems2[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(standardViewModel, standardModeTestItems2, true); + VERIFY_ARE_EQUAL(StringReference(L"3"), GetStringValue(standardViewModel->DisplayValue)); + + // Validate the Date Calculator + VERIFY_ARE_EQUAL(DateUtils::GetLongDate(endDate), dateCalcViewModel->StrDateResult); + + // Validate the Unit Converter + VERIFY_ARE_EQUAL(StringReference(L"2"), GetStringValue(unitConverterViewModel->Value1)); + VERIFY_ARE_EQUAL(StringReference(L"0.002"), GetStringValue(unitConverterViewModel->Value2)); + } + + // Change the radix in programmer mode and check that other modes are not affected + TEST_METHOD(MultipleModesWithChangeInProgrammerRadix) + { + std::vector viewModels(3); + + // Create 2 instances of StandardCalculatorViewModel in Standard and Programmer modes + viewModels[0] = ref new StandardCalculatorViewModel(); + ChangeMode(viewModels[0], 0); + + viewModels[1] = ref new StandardCalculatorViewModel(); + ChangeMode(viewModels[1], 2); + + // Change the programmer mode radix to hex + TESTITEM programmerModeTestItems[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[1], programmerModeTestItems, false /*doReset*/); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[1]->HexDisplayValue), StringReference(L"F")); + + // Standard Mode: Expression 10+2= + // Radix should be decimal, not hex + TESTITEM standardModeTestItems[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Add, L"10", L"10 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"10 + " }, + { NumbersAndOperatorsEnum::Equals, L"12", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[0], standardModeTestItems, false /*doReset*/); + + //Launch a new instance in standard mode + viewModels[2] = ref new StandardCalculatorViewModel(); + ChangeMode(viewModels[2], 0); + + // Standard Mode: Expression 10+2= + // Radix will be decimal by default + TESTITEM standardModeTestItemsNew[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Add, L"10", L"10 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"10 + " }, + { NumbersAndOperatorsEnum::Equals, L"12", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[2], standardModeTestItemsNew, false /*doReset*/); + + //Radix in the programmer mode launched should still be hex. + // A + 1 = B + TESTITEM programmerModeTestItemsNew[] = { + { NumbersAndOperatorsEnum::A, L"A", L"" }, + { NumbersAndOperatorsEnum::Add, L"A", L"A + " }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Equals, L"B", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(viewModels[1], programmerModeTestItemsNew, true /*doReset*/); + VERIFY_ARE_EQUAL(GetStringValue(viewModels[1]->HexDisplayValue), StringReference(L"B")); + } + }; +} + diff --git a/internal/CalculatorUnitTests/NavCategoryUnitTests.cpp b/internal/CalculatorUnitTests/NavCategoryUnitTests.cpp new file mode 100644 index 00000000..e0ecebb3 --- /dev/null +++ b/internal/CalculatorUnitTests/NavCategoryUnitTests.cpp @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +using namespace CalculatorApp::Common; +using namespace CalculatorUnitTests; +using namespace Platform; +using namespace std; +using namespace Windows::Foundation::Collections; + +namespace CalculatorUnitTests +{ + class NavCategoryUnitTests + { + public: + TEST_CLASS(NavCategoryUnitTests); + + TEST_METHOD(Serialize); + TEST_METHOD(Deserialize_AllValid); + TEST_METHOD(Deserialize_AllInvalid); + + TEST_METHOD(IsValidViewMode_AllValid); + TEST_METHOD(IsValidViewMode_AllInvalid); + + TEST_METHOD(IsCalculatorViewMode); + TEST_METHOD(IsDateCalculatorViewMode); + TEST_METHOD(IsConverterViewMode); + + TEST_METHOD(GetFriendlyName); + TEST_METHOD(GetGroupType); + TEST_METHOD(GetIndex); + TEST_METHOD(GetPosition); + TEST_METHOD(GetIndex_GetPosition_Relationship); + TEST_METHOD(GetIndexInGroup); + + TEST_METHOD(GetViewModeForVirtualKey); + }; + + void NavCategoryUnitTests::Serialize() + { + // While values in other tests may change (for example, the order + // of a navigation item might change), these values should NEVER + // change. We are validating the unique ID for each mode, not + // it's position or index. + VERIFY_ARE_EQUAL(0, NavCategory::Serialize(ViewMode::Standard)); + VERIFY_ARE_EQUAL(1, NavCategory::Serialize(ViewMode::Scientific)); + VERIFY_ARE_EQUAL(2, NavCategory::Serialize(ViewMode::Programmer)); + VERIFY_ARE_EQUAL(3, NavCategory::Serialize(ViewMode::Date)); + VERIFY_ARE_EQUAL(16, NavCategory::Serialize(ViewMode::Currency)); + VERIFY_ARE_EQUAL(4, NavCategory::Serialize(ViewMode::Volume)); + VERIFY_ARE_EQUAL(5, NavCategory::Serialize(ViewMode::Length)); + VERIFY_ARE_EQUAL(6, NavCategory::Serialize(ViewMode::Weight)); + VERIFY_ARE_EQUAL(7, NavCategory::Serialize(ViewMode::Temperature)); + VERIFY_ARE_EQUAL(8, NavCategory::Serialize(ViewMode::Energy)); + VERIFY_ARE_EQUAL(9, NavCategory::Serialize(ViewMode::Area)); + VERIFY_ARE_EQUAL(10, NavCategory::Serialize(ViewMode::Speed)); + VERIFY_ARE_EQUAL(11, NavCategory::Serialize(ViewMode::Time)); + VERIFY_ARE_EQUAL(12, NavCategory::Serialize(ViewMode::Power)); + VERIFY_ARE_EQUAL(13, NavCategory::Serialize(ViewMode::Data)); + VERIFY_ARE_EQUAL(14, NavCategory::Serialize(ViewMode::Pressure)); + VERIFY_ARE_EQUAL(15, NavCategory::Serialize(ViewMode::Angle)); + + VERIFY_ARE_EQUAL(-1, NavCategory::Serialize(ViewMode::None)); + } + + void NavCategoryUnitTests::Deserialize_AllValid() + { + // While values in other tests may change (for example, the order + // of a navigation item might change), these values should NEVER + // change. We are validating the unique ID for each mode, not + // it's position or index. + VERIFY_ARE_EQUAL(ViewMode::Standard, NavCategory::Deserialize(ref new Box(0))); + VERIFY_ARE_EQUAL(ViewMode::Scientific, NavCategory::Deserialize(ref new Box(1))); + VERIFY_ARE_EQUAL(ViewMode::Programmer, NavCategory::Deserialize(ref new Box(2))); + VERIFY_ARE_EQUAL(ViewMode::Date, NavCategory::Deserialize(ref new Box(3))); + VERIFY_ARE_EQUAL(ViewMode::Currency, NavCategory::Deserialize(ref new Box(16))); + VERIFY_ARE_EQUAL(ViewMode::Volume, NavCategory::Deserialize(ref new Box(4))); + VERIFY_ARE_EQUAL(ViewMode::Length, NavCategory::Deserialize(ref new Box(5))); + VERIFY_ARE_EQUAL(ViewMode::Weight, NavCategory::Deserialize(ref new Box(6))); + VERIFY_ARE_EQUAL(ViewMode::Temperature, NavCategory::Deserialize(ref new Box(7))); + VERIFY_ARE_EQUAL(ViewMode::Energy, NavCategory::Deserialize(ref new Box(8))); + VERIFY_ARE_EQUAL(ViewMode::Area, NavCategory::Deserialize(ref new Box(9))); + VERIFY_ARE_EQUAL(ViewMode::Speed, NavCategory::Deserialize(ref new Box(10))); + VERIFY_ARE_EQUAL(ViewMode::Time, NavCategory::Deserialize(ref new Box(11))); + VERIFY_ARE_EQUAL(ViewMode::Power, NavCategory::Deserialize(ref new Box(12))); + VERIFY_ARE_EQUAL(ViewMode::Data, NavCategory::Deserialize(ref new Box(13))); + VERIFY_ARE_EQUAL(ViewMode::Pressure, NavCategory::Deserialize(ref new Box(14))); + VERIFY_ARE_EQUAL(ViewMode::Angle, NavCategory::Deserialize(ref new Box(15))); + } + + void NavCategoryUnitTests::Deserialize_AllInvalid() + { + VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(nullptr)); + VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new String(L"fail"))); + + // Boundary testing + VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new Box(-1))); + VERIFY_ARE_EQUAL(ViewMode::None, NavCategory::Deserialize(ref new Box(17))); + } + + void NavCategoryUnitTests::IsValidViewMode_AllValid() + { + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Standard)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Scientific)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Programmer)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Date)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Currency)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Volume)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Length)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Weight)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Temperature)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Energy)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Area)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Speed)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Time)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Power)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Data)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Pressure)); + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(ViewMode::Angle)); + } + + void NavCategoryUnitTests::IsValidViewMode_AllInvalid() + { + VERIFY_IS_FALSE(NavCategory::IsValidViewMode(ViewMode::None)); + + // There are 17 total options so int 17 should be the first invalid + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(16))); + VERIFY_IS_FALSE(NavCategory::IsValidViewMode(static_cast(17))); + + // Also verify the lower bound + VERIFY_IS_TRUE(NavCategory::IsValidViewMode(static_cast(0))); + VERIFY_IS_FALSE(NavCategory::IsValidViewMode(static_cast(-1))); + } + + void NavCategoryUnitTests::IsCalculatorViewMode() + { + VERIFY_IS_TRUE(NavCategory::IsCalculatorViewMode(ViewMode::Standard)); + VERIFY_IS_TRUE(NavCategory::IsCalculatorViewMode(ViewMode::Scientific)); + VERIFY_IS_TRUE(NavCategory::IsCalculatorViewMode(ViewMode::Programmer)); + + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Date)); + + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Currency)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Volume)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Length)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Weight)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Temperature)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Energy)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Area)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Speed)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Time)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Power)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Data)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Pressure)); + VERIFY_IS_FALSE(NavCategory::IsCalculatorViewMode(ViewMode::Angle)); + } + + void NavCategoryUnitTests::IsDateCalculatorViewMode() + { + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Standard)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Scientific)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Programmer)); + + VERIFY_IS_TRUE(NavCategory::IsDateCalculatorViewMode(ViewMode::Date)); + + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Currency)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Volume)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Length)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Weight)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Temperature)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Energy)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Area)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Speed)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Time)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Power)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Data)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Pressure)); + VERIFY_IS_FALSE(NavCategory::IsDateCalculatorViewMode(ViewMode::Angle)); + } + + void NavCategoryUnitTests::IsConverterViewMode() + { + VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Standard)); + VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Scientific)); + VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Programmer)); + + VERIFY_IS_FALSE(NavCategory::IsConverterViewMode(ViewMode::Date)); + + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Currency)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Volume)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Length)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Weight)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Temperature)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Energy)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Area)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Speed)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Time)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Power)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Data)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Pressure)); + VERIFY_IS_TRUE(NavCategory::IsConverterViewMode(ViewMode::Angle)); + } + + void NavCategoryUnitTests::GetFriendlyName() + { + VERIFY_ARE_EQUAL(StringReference(L"Standard"), NavCategory::GetFriendlyName(ViewMode::Standard)); + VERIFY_ARE_EQUAL(StringReference(L"Scientific"), NavCategory::GetFriendlyName(ViewMode::Scientific)); + VERIFY_ARE_EQUAL(StringReference(L"Programmer"), NavCategory::GetFriendlyName(ViewMode::Programmer)); + VERIFY_ARE_EQUAL(StringReference(L"Date"), NavCategory::GetFriendlyName(ViewMode::Date)); + VERIFY_ARE_EQUAL(StringReference(L"Currency"), NavCategory::GetFriendlyName(ViewMode::Currency)); + VERIFY_ARE_EQUAL(StringReference(L"Volume"), NavCategory::GetFriendlyName(ViewMode::Volume)); + VERIFY_ARE_EQUAL(StringReference(L"Length"), NavCategory::GetFriendlyName(ViewMode::Length)); + VERIFY_ARE_EQUAL(StringReference(L"Weight and Mass"), NavCategory::GetFriendlyName(ViewMode::Weight)); + VERIFY_ARE_EQUAL(StringReference(L"Temperature"), NavCategory::GetFriendlyName(ViewMode::Temperature)); + VERIFY_ARE_EQUAL(StringReference(L"Energy"), NavCategory::GetFriendlyName(ViewMode::Energy)); + VERIFY_ARE_EQUAL(StringReference(L"Area"), NavCategory::GetFriendlyName(ViewMode::Area)); + VERIFY_ARE_EQUAL(StringReference(L"Speed"), NavCategory::GetFriendlyName(ViewMode::Speed)); + VERIFY_ARE_EQUAL(StringReference(L"Time"), NavCategory::GetFriendlyName(ViewMode::Time)); + VERIFY_ARE_EQUAL(StringReference(L"Power"), NavCategory::GetFriendlyName(ViewMode::Power)); + VERIFY_ARE_EQUAL(StringReference(L"Data"), NavCategory::GetFriendlyName(ViewMode::Data)); + VERIFY_ARE_EQUAL(StringReference(L"Pressure"), NavCategory::GetFriendlyName(ViewMode::Pressure)); + VERIFY_ARE_EQUAL(StringReference(L"Angle"), NavCategory::GetFriendlyName(ViewMode::Angle)); + + VERIFY_ARE_EQUAL(StringReference(L"None"), NavCategory::GetFriendlyName(ViewMode::None)); + } + + void NavCategoryUnitTests::GetGroupType() + { + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Standard)); + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Scientific)); + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Programmer)); + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, NavCategory::GetGroupType(ViewMode::Date)); + + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Currency)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Volume)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Length)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Weight)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Temperature)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Energy)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Area)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Speed)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Time)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Power)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Data)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Pressure)); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, NavCategory::GetGroupType(ViewMode::Angle)); + } + + void NavCategoryUnitTests::GetIndex() + { + // Index is the 0-based ordering of modes + vector orderedModes = { + ViewMode::Standard, + ViewMode::Scientific, + ViewMode::Programmer, + ViewMode::Date, + ViewMode::Currency, + ViewMode::Volume, + ViewMode::Length, + ViewMode::Weight, + ViewMode::Temperature, + ViewMode::Energy, + ViewMode::Area, + ViewMode::Speed, + ViewMode::Time, + ViewMode::Power, + ViewMode::Data, + ViewMode::Pressure, + ViewMode::Angle + }; + + for (size_t index = 0; index < orderedModes.size(); index++) + { + ViewMode mode = orderedModes[index]; + VERIFY_ARE_EQUAL(index, NavCategory::GetIndex(mode)); + } + + VERIFY_ARE_EQUAL(-1, NavCategory::GetIndex(ViewMode::None)); + } + + void NavCategoryUnitTests::GetPosition() + { + // Position is the 1-based ordering of modes + vector orderedModes = { + ViewMode::Standard, + ViewMode::Scientific, + ViewMode::Programmer, + ViewMode::Date, + ViewMode::Currency, + ViewMode::Volume, + ViewMode::Length, + ViewMode::Weight, + ViewMode::Temperature, + ViewMode::Energy, + ViewMode::Area, + ViewMode::Speed, + ViewMode::Time, + ViewMode::Power, + ViewMode::Data, + ViewMode::Pressure, + ViewMode::Angle + }; + + for (size_t pos = 1; pos <= orderedModes.size(); pos++) + { + ViewMode mode = orderedModes[pos - 1]; + VERIFY_ARE_EQUAL(pos, NavCategory::GetPosition(mode)); + } + + VERIFY_ARE_EQUAL(-1, NavCategory::GetPosition(ViewMode::None)); + } + + void NavCategoryUnitTests::GetIndex_GetPosition_Relationship() + { + // Index should be 1 less than Position. + // The other checks verify the order of Index and Position. + // Just verify the relationship here. + VERIFY_ARE_EQUAL(NavCategory::GetIndex(ViewMode::Standard) + 1, NavCategory::GetPosition(ViewMode::Standard)); + VERIFY_ARE_EQUAL(NavCategory::GetPosition(ViewMode::Volume) - 1, NavCategory::GetIndex(ViewMode::Volume)); + } + + void NavCategoryUnitTests::GetIndexInGroup() + { + VERIFY_ARE_EQUAL(0, NavCategory::GetIndexInGroup(ViewMode::Standard, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(1, NavCategory::GetIndexInGroup(ViewMode::Scientific, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Programmer, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(3, NavCategory::GetIndexInGroup(ViewMode::Date, CategoryGroupType::Calculator)); + + VERIFY_ARE_EQUAL(0, NavCategory::GetIndexInGroup(ViewMode::Currency, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(1, NavCategory::GetIndexInGroup(ViewMode::Volume, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(2, NavCategory::GetIndexInGroup(ViewMode::Length, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(3, NavCategory::GetIndexInGroup(ViewMode::Weight, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(4, NavCategory::GetIndexInGroup(ViewMode::Temperature, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(5, NavCategory::GetIndexInGroup(ViewMode::Energy, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(6, NavCategory::GetIndexInGroup(ViewMode::Area, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(7, NavCategory::GetIndexInGroup(ViewMode::Speed, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(8, NavCategory::GetIndexInGroup(ViewMode::Time, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(9, NavCategory::GetIndexInGroup(ViewMode::Power, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(10, NavCategory::GetIndexInGroup(ViewMode::Data, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(11, NavCategory::GetIndexInGroup(ViewMode::Pressure, CategoryGroupType::Converter)); + VERIFY_ARE_EQUAL(12, NavCategory::GetIndexInGroup(ViewMode::Angle, CategoryGroupType::Converter)); + + VERIFY_ARE_EQUAL(-1, NavCategory::GetIndexInGroup(ViewMode::None, CategoryGroupType::Calculator)); + VERIFY_ARE_EQUAL(-1, NavCategory::GetIndexInGroup(ViewMode::None, CategoryGroupType::Converter)); + } + + void NavCategoryUnitTests::GetViewModeForVirtualKey() + { + VERIFY_ARE_EQUAL(ViewMode::Standard, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number1)); + VERIFY_ARE_EQUAL(ViewMode::Scientific, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number2)); + VERIFY_ARE_EQUAL(ViewMode::Programmer, NavCategory::GetViewModeForVirtualKey(MyVirtualKey::Number3)); + } + + class NavCategoryGroupUnitTests + { + public: + TEST_CLASS(NavCategoryGroupUnitTests); + + TEST_METHOD(CreateNavCategoryGroup); + + private: + void ValidateNavCategory(IObservableVector^ categories, unsigned int index, ViewMode expectedMode, int expectedPosition) + { + VERIFY_IS_LESS_THAN(0u, categories->Size); + VERIFY_IS_GREATER_THAN(categories->Size, index); + + NavCategory^ category = categories->GetAt(index); + VERIFY_ARE_EQUAL(expectedMode, category->Mode); + VERIFY_ARE_EQUAL(expectedPosition, category->Position); + } + }; + + void NavCategoryGroupUnitTests::CreateNavCategoryGroup() + { + IObservableVector^ menuOptions = NavCategoryGroup::CreateMenuOptions(); + + VERIFY_ARE_EQUAL(2, menuOptions->Size); + + NavCategoryGroup^ calculatorGroup = menuOptions->GetAt(0); + VERIFY_ARE_EQUAL(CategoryGroupType::Calculator, calculatorGroup->GroupType); + + IObservableVector^ calculatorCategories = calculatorGroup->Categories; + VERIFY_ARE_EQUAL(4, calculatorCategories->Size); + ValidateNavCategory(calculatorCategories, 0u, ViewMode::Standard, 1); + ValidateNavCategory(calculatorCategories, 1u, ViewMode::Scientific, 2); + ValidateNavCategory(calculatorCategories, 2u, ViewMode::Programmer, 3); + ValidateNavCategory(calculatorCategories, 3u, ViewMode::Date, 4); + + NavCategoryGroup^ converterGroup = menuOptions->GetAt(1); + VERIFY_ARE_EQUAL(CategoryGroupType::Converter, converterGroup->GroupType); + + IObservableVector^ converterCategories = converterGroup->Categories; + VERIFY_ARE_EQUAL(13, converterCategories->Size); + ValidateNavCategory(converterCategories, 0u, ViewMode::Currency, 5); + ValidateNavCategory(converterCategories, 1u, ViewMode::Volume, 6); + ValidateNavCategory(converterCategories, 2u, ViewMode::Length, 7); + ValidateNavCategory(converterCategories, 3u, ViewMode::Weight, 8); + ValidateNavCategory(converterCategories, 4u, ViewMode::Temperature, 9); + ValidateNavCategory(converterCategories, 5u, ViewMode::Energy, 10); + ValidateNavCategory(converterCategories, 6u, ViewMode::Area, 11); + ValidateNavCategory(converterCategories, 7u, ViewMode::Speed, 12); + ValidateNavCategory(converterCategories, 8u, ViewMode::Time, 13); + ValidateNavCategory(converterCategories, 9u, ViewMode::Power, 14); + ValidateNavCategory(converterCategories, 10u, ViewMode::Data, 15); + ValidateNavCategory(converterCategories, 11u, ViewMode::Pressure, 16); + ValidateNavCategory(converterCategories, 12u, ViewMode::Angle, 17); + } +} diff --git a/internal/CalculatorUnitTests/Package.appxmanifest b/internal/CalculatorUnitTests/Package.appxmanifest new file mode 100644 index 00000000..62c0b679 --- /dev/null +++ b/internal/CalculatorUnitTests/Package.appxmanifest @@ -0,0 +1,29 @@ + + + + + + CalculatorUnitTests + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/StandardViewModelUnitTests.cpp b/internal/CalculatorUnitTests/StandardViewModelUnitTests.cpp new file mode 100644 index 00000000..3164303f --- /dev/null +++ b/internal/CalculatorUnitTests/StandardViewModelUnitTests.cpp @@ -0,0 +1,1144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +#include "CalcViewModel/StandardCalculatorViewModel.h" +#include "CalcViewModel/Common/CalculatorButtonPressedEventArgs.h" + +#include "CalcViewModel/StandardCalculatorViewModel.h" +#include "CalcViewModel/Common/CalculatorButtonPressedEventArgs.h" + +using namespace CalculationManager; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace Platform; + +namespace CalculatorUnitTests +{ + + void ChangeMode(StandardCalculatorViewModel^ viewModel, int mode) + { + if (mode == 0) + { + viewModel->IsStandard = true; + viewModel->IsScientific = false; + viewModel->IsProgrammer = false; + viewModel->ResetDisplay(); + viewModel->SetPrecision(StandardModePrecision); + } + else if (mode == 1) + { + viewModel->IsScientific = true; + viewModel->IsProgrammer = false; + viewModel->IsStandard = false; + viewModel->ResetDisplay(); + viewModel->SetPrecision(ScientificModePrecision); + } + else if (mode == 2) + { + viewModel->IsProgrammer = true; + viewModel->IsScientific = false; + viewModel->IsStandard = false; + viewModel->ResetDisplay(); + viewModel->SetPrecision(ProgrammerModePrecision); + } + } + + Object^ CommandParameterFromTestItem(TESTITEM* item) + { + // Need to wrap Binary Operators with their feedback + static const std::vector> binOps = { + { NumbersAndOperatorsEnum::Add, L"plus" }, + { NumbersAndOperatorsEnum::Subtract, L"minus" }, + { NumbersAndOperatorsEnum::Multiply, L"times" }, + { NumbersAndOperatorsEnum::Divide, L"divided by" } + }; + + for (auto& binOp : binOps) + { + if (item->command == binOp.first) + { + return ref new CalculatorButtonPressedEventArgs(binOp.second, binOp.first); + } + } + + return item->command; + } + + void ValidateViewModelByCommands(StandardCalculatorViewModel^ viewModel, TESTITEM item[], bool doReset = false) + { + if (doReset) + { + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Clear); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::ClearEntry); + viewModel->ClearMemoryCommand->Execute(nullptr); + viewModel->Deserialize(ref new Platform::Array(0)); + } + + TESTITEM* currentItem = item; + + while (currentItem->command != NumbersAndOperatorsEnum::None) + { + Object^ commandParam = CommandParameterFromTestItem(currentItem); + viewModel->ButtonPressed->Execute(commandParam); + + if (currentItem->expectedPrimaryDisplay != L"N/A") + { + VERIFY_ARE_EQUAL(Platform::StringReference(currentItem->expectedPrimaryDisplay.c_str()), viewModel->DisplayValue); + } + if (currentItem->expectedExpressions != L"N/A" && viewModel->DisplayStringExpression != nullptr) + { + VERIFY_ARE_EQUAL(Platform::StringReference(currentItem->expectedExpressions.c_str()), viewModel->DisplayStringExpression); + } + currentItem++; + } + } + + class StandardViewModelUnitTests + { + public: + TEST_CLASS(StandardViewModelUnitTests); + + TEST_METHOD_SETUP(InitializeViewModel) + { + m_viewModel = ref new StandardCalculatorViewModel(); + m_viewModel->IsStandard = true; + m_engineResourceProvider = std::make_shared(); + m_decimalSeparator = ref new Platform::String(m_engineResourceProvider->GetCEngineString(L"sDecimal").c_str()); + return true; + } + + void ValidateViewModelValueAndExpression(String ^ value, String ^expression = nullptr) + { + String^ displayValue = m_viewModel->DisplayValue; + String^ displayExpression = m_viewModel->DisplayStringExpression; + if (value != nullptr) + { + VERIFY_ARE_EQUAL(value, displayValue); + } + + if (expression != nullptr) + { + VERIFY_ARE_EQUAL(expression, displayExpression); + } + } + + void ValidateViewModelValueAndSecondaryExpression(String ^ value, String ^expression = nullptr) + { + String^ displayValue = m_viewModel->DisplayValue; + + unsigned int nTokens = m_viewModel->ExpressionTokens->Size; + String^ displaySecondaryExpression = ref new String(); + + for (unsigned int i = 0; i < nTokens; ++i) + { + DisplayExpressionToken^ currentToken; + currentToken = m_viewModel->ExpressionTokens->GetAt(i); + displaySecondaryExpression = String::Concat(displaySecondaryExpression, currentToken->Token); + } + + if (value != nullptr) + { + VERIFY_ARE_EQUAL(value, displayValue); + } + + if (expression != nullptr) + { + VERIFY_ARE_EQUAL(expression, displaySecondaryExpression); + } + } + + TEST_METHOD(ViewModelConstructorDisplayValueAndExpressionInitializedTest) + { + StandardCalculatorViewModel^ vmconstructortest = ref new StandardCalculatorViewModel(); + vmconstructortest->IsStandard = true; + String^ displayValue = vmconstructortest->DisplayValue; + String^ displayExpression = vmconstructortest->DisplayStringExpression; + String^ calculationResultAutomationName = vmconstructortest->CalculationResultAutomationName; + + VERIFY_ARE_EQUAL(StringReference(L"0"), displayValue); + VERIFY_ARE_EQUAL(StringReference(L"Display is 0"), calculationResultAutomationName); + } + + TEST_METHOD(ViewModelConstructorButtonPressedInitializedTest) + { + StandardCalculatorViewModel^ vmconstructortest = ref new StandardCalculatorViewModel(); + vmconstructortest->IsStandard = true; + VERIFY_IS_NOT_NULL(vmconstructortest->ButtonPressed); + } + + /// Expression : 135 + TEST_METHOD(ButtonPressedLeftHandOperandEnteredTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Three, L"13", L"" }, + { NumbersAndOperatorsEnum::Five, L"135", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 13. + TEST_METHOD(ButtonPressedLeftHandOperandAndDecimalEnteredTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Three, L"13", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"13" + std::wstring(m_decimalSeparator->Data()), L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 13== + TEST_METHOD(ButtonPressedLeftHandOperandAndEqualsEnteredTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Three, L"13", L"" }, + { NumbersAndOperatorsEnum::Equals, L"13", L"" }, + { NumbersAndOperatorsEnum::Equals, L"13", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 13+ + TEST_METHOD(ButtonPressedLeftHandOperandAndOperationEnteredTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Three, L"13", L"" }, + { NumbersAndOperatorsEnum::Add, L"13", L"13 + " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 13+801 + TEST_METHOD(ButtonPressedRightHandOperandEnteredTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Three, L"13", L"" }, + { NumbersAndOperatorsEnum::Add, L"13", L"13 + " }, + { NumbersAndOperatorsEnum::Eight, L"8", L"13 + " }, + { NumbersAndOperatorsEnum::Zero, L"80", L"13 + " }, + { NumbersAndOperatorsEnum::One, L"801", L"13 + " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 1+2= + TEST_METHOD(ButtonPressedAdditionWithEqualsTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 1-2= + TEST_METHOD(ButtonPressedSubtractionWithEqualsTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"1", L"1 - " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 - " }, + { NumbersAndOperatorsEnum::Equals, L"-1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 3*5= + TEST_METHOD(ButtonPressedMultiplyWithEqualsTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Three, L"3", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"3", L"3 * " }, + { NumbersAndOperatorsEnum::Five, L"5", L"3 * " }, + { NumbersAndOperatorsEnum::Equals, L"15", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 9/3= + TEST_METHOD(ButtonPressedDivideTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Nine, L"9", L"" }, + { NumbersAndOperatorsEnum::Divide, L"9", L"9 / " }, + { NumbersAndOperatorsEnum::Three, L"3", L"9 / " }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 7.555*3= + TEST_METHOD(ButtonPressedDecimalOperationTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Seven, L"7", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"7" + std::wstring(m_decimalSeparator->Data()), L"" }, + { NumbersAndOperatorsEnum::Five, L"7" + std::wstring(m_decimalSeparator->Data()) + L"5", L"" }, + { NumbersAndOperatorsEnum::Five, L"7" + std::wstring(m_decimalSeparator->Data()) + L"55", L"" }, + { NumbersAndOperatorsEnum::Five, L"7" + std::wstring(m_decimalSeparator->Data()) + L"555", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"7" + std::wstring(m_decimalSeparator->Data()) + L"555", L"7" + std::wstring(m_decimalSeparator->Data()) + L"555 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"7" + std::wstring(m_decimalSeparator->Data()) + L"555 * " }, + { NumbersAndOperatorsEnum::Equals, L"22" + std::wstring(m_decimalSeparator->Data()) + L"665", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 7/0 + TEST_METHOD(ButtonPressedDivideByZeroNegativeTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Seven, L"7", L"" }, + { NumbersAndOperatorsEnum::Divide, L"7", L"7 / " }, + { NumbersAndOperatorsEnum::Zero, L"0", L"7 / " }, + { NumbersAndOperatorsEnum::Equals, L"Cannot divide by zero", L"7 / " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 8/2* + TEST_METHOD(ButtonPressedExpressionWithMultipleOperatorsTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Eight, L"8", L"" }, + { NumbersAndOperatorsEnum::Divide, L"8", L"8 / " }, + { NumbersAndOperatorsEnum::Two, L"2", L"8 / " }, + { NumbersAndOperatorsEnum::Multiply, L"4", L"8 / 2 * " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 8/+*2* + TEST_METHOD(ButtonPressedExpressionWithMultipleOperatorsInSuccessionTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Eight, L"8", L"" }, + { NumbersAndOperatorsEnum::Divide, L"8", L"8 / " }, + { NumbersAndOperatorsEnum::Add, L"8", L"8 + " }, + { NumbersAndOperatorsEnum::Multiply, L"8", L"8 * " }, + { NumbersAndOperatorsEnum::Two, L"2", L"8 * " }, + { NumbersAndOperatorsEnum::Multiply, L"16", L"8 * 2 * " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 8*2== + TEST_METHOD(ButtonPressedExpressionWithMultipleEqualsAfterEvaluateTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Eight, L"8", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"8", L"8 * " }, + { NumbersAndOperatorsEnum::Two, L"2", L"8 * " }, + { NumbersAndOperatorsEnum::Equals, L"16", L"" }, + { NumbersAndOperatorsEnum::Equals, L"32", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 7-6 and Backspace + TEST_METHOD(ButtonPressedExpressionWithBackSpaceTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Seven, L"7", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"7", L"7 - " }, + { NumbersAndOperatorsEnum::Six, L"6", L"7 - " }, + { NumbersAndOperatorsEnum::Backspace, L"0", L"7 - " }, + { NumbersAndOperatorsEnum::Backspace, L"0", L"7 - " }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// Expression : 91-68 and Clear + TEST_METHOD(ButtonPressedExpressionWithClearTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::Nine, L"9", L"" }, + { NumbersAndOperatorsEnum::One, L"91", L"" }, + { NumbersAndOperatorsEnum::Subtract, L"91", L"91 - " }, + { NumbersAndOperatorsEnum::Six, L"6", L"91 - " }, + { NumbersAndOperatorsEnum::Eight, L"68", L"91 - " }, + { NumbersAndOperatorsEnum::Clear, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + } + + /// + /// Paste tests + /// + template + void ValidateIntegralsAreEqual(NumbersAndOperatorsEnum a, NumbersAndOperatorsEnum b) + { + VERIFY_ARE_EQUAL(static_cast(a), static_cast(b)); + } + + void ValidateNumbersAndOperatorsAreEqual(NumbersAndOperatorsEnum a, NumbersAndOperatorsEnum b) + { + ValidateIntegralsAreEqual(a, b); + } + + /// Low-level test of character mapping + TEST_METHOD(VerifyCorrectCharacterMapping) + { + bool canSendNegate = false; + + // Valid numbers + NumbersAndOperatorsEnum n = m_viewModel->MapCharacterToButtonId(L'0', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::Zero); + + n = m_viewModel->MapCharacterToButtonId(L'1', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::One); + + // Valid operators + n = m_viewModel->MapCharacterToButtonId(L'+', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::Add); + + n = m_viewModel->MapCharacterToButtonId(L'=', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::Equals); + + n = m_viewModel->MapCharacterToButtonId(L'a', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::A); + + // Invalid character + n = m_viewModel->MapCharacterToButtonId(L'$', canSendNegate); + ValidateNumbersAndOperatorsAreEqual(n, NumbersAndOperatorsEnum::None); + } + + /// Various strings get pasted + TEST_METHOD(PasteExpressions) + { + m_viewModel->IsScientific = false; + + m_viewModel->OnPaste("-0.99", ViewMode::Standard); + ValidateViewModelValueAndExpression("-0" + m_decimalSeparator + "99", ""); + + m_viewModel->OnPaste("1+1=", ViewMode::Standard); + ValidateViewModelValueAndExpression("2", ""); + + // This result is not obvious: it's the result of the previous operation + m_viewModel->OnPaste("0=", ViewMode::Standard); + ValidateViewModelValueAndExpression("1", ""); + + // Negative value + m_viewModel->OnPaste("-1", ViewMode::Standard); + ValidateViewModelValueAndExpression("-1", ""); + + // Negated expression + m_viewModel->OnPaste("-(1+1)", ViewMode::Standard); + ValidateViewModelValueAndSecondaryExpression("-2", "negate(1 + 1)"); + + // More complicated Negated expression + m_viewModel->OnPaste("-(-(-1))", ViewMode::Standard); + ValidateViewModelValueAndSecondaryExpression("-1", "negate(0 - (0 - 1))"); + + // Switch to scientific mode + m_viewModel->IsScientific = true; + + VERIFY_IS_FALSE(m_viewModel->IsFToEChecked); + + //// Positive exponent + m_viewModel->OnPaste("1.23e+10", ViewMode::Scientific); + ValidateViewModelValueAndExpression("1" + m_decimalSeparator + "23e+10", ""); + + //// Negative exponent + m_viewModel->OnPaste("1.23e-10", ViewMode::Scientific); + ValidateViewModelValueAndExpression("1" + m_decimalSeparator + "23e-10", ""); + + //// Uppercase E (for exponent) + m_viewModel->OnPaste("1.23E-10", ViewMode::Scientific); + ValidateViewModelValueAndExpression("1" + m_decimalSeparator + "23e-10", ""); + } + + // Verify Calculator CalculationResultAutomationName is set correctly + TEST_METHOD(CalculationResultAutomationNameVerification) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::IsStandardMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Two, L"12", L"" }, + { NumbersAndOperatorsEnum::Three, L"123", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + VERIFY_ARE_EQUAL(StringReference(L"Display is 123"), m_viewModel->CalculationResultAutomationName); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, true); + VERIFY_ARE_EQUAL(StringReference(L"Display is 7"), m_viewModel->CalculationResultAutomationName); + + TESTITEM items3[] = { + { NumbersAndOperatorsEnum::Clear, L"0", L"" }, + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::Five, L"5", L"" }, + { NumbersAndOperatorsEnum::InvSin, L"Invalid input", L"asind(5)" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items3, false); + VERIFY_ARE_EQUAL(StringReference(L"Display is Invalid input"), m_viewModel->CalculationResultAutomationName); + } + + // Switch Calculator Mode and verify if mode switch is happening as expected. + // Test precendence to check if mode switch is happening + // Standard mode 1+2*3 = 9; Scientific mode 1+2*3 = 7 + // Intermediate value is also different. after 1 + 2 * , standard shows 3, scientific shows 2 + TEST_METHOD(ButtonPressedCalculatorModeSwitch) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::IsStandardMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"9", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Add, L"1", L"1 + " }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 + " }, + { NumbersAndOperatorsEnum::Multiply, L"2", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Three, L"3", L"1 + 2 * " }, + { NumbersAndOperatorsEnum::Equals, L"7", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, true); + } + + // Test AutoConvertedValue + TEST_METHOD(ProgrammerModeAutoConvertedValue) + { + TESTITEM none[] = { + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, none, true); + m_viewModel->IsProgrammer = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, false); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->HexDisplayValue), StringReference(L"F")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->DecimalDisplayValue), StringReference(L"15")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->OctalDisplayValue), StringReference(L"17")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->BinaryDisplayValue), StringReference(L"1111")); + } + + // Test Button disabling in different Radixes + TEST_METHOD(ProgrammerModeButtonsDisable) + { + /* m_viewModel->IsProgrammer = true; + m_viewModel->SwitchProgrammerModeBase(OCT_RADIX); + VERIFY_IS_TRUE(m_viewModel->AreOCTButtonsEnabled); + VERIFY_IS_FALSE(m_viewModel->AreHEXButtonsEnabled); + VERIFY_IS_FALSE(m_viewModel->AreDECButtonsEnabled); + m_viewModel->SwitchProgrammerModeBase(DEC_RADIX); + VERIFY_IS_FALSE(m_viewModel->AreHEXButtonsEnabled); + VERIFY_IS_TRUE(m_viewModel->AreDECButtonsEnabled); + VERIFY_IS_TRUE(m_viewModel->AreOCTButtonsEnabled); + m_viewModel->SwitchProgrammerModeBase(HEX_RADIX); + VERIFY_IS_TRUE(m_viewModel->AreHEXButtonsEnabled); + VERIFY_IS_TRUE(m_viewModel->AreDECButtonsEnabled); + VERIFY_IS_TRUE(m_viewModel->AreOCTButtonsEnabled); + m_viewModel->SwitchProgrammerModeBase(BIN_RADIX); + VERIFY_IS_FALSE(m_viewModel->AreHEXButtonsEnabled); + VERIFY_IS_FALSE(m_viewModel->AreDECButtonsEnabled); + VERIFY_IS_FALSE(m_viewModel->AreOCTButtonsEnabled);*/ + + } + + // Test digit grouping for different radix in programmer mode + TEST_METHOD(ProgrammerModeRadixGrouping) + { + TESTITEM none[] = { + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, none, true); + m_viewModel->IsProgrammer = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Two, L"12", L"" }, + { NumbersAndOperatorsEnum::Three, L"123", L"" }, + { NumbersAndOperatorsEnum::Four, L"1,234", L"" }, + { NumbersAndOperatorsEnum::Five, L"12,345", L"" }, + { NumbersAndOperatorsEnum::Six, L"123,456", L"" }, + { NumbersAndOperatorsEnum::Seven, L"1,234,567", L"" }, + { NumbersAndOperatorsEnum::Eight, L"12,345,678", L"" }, + { NumbersAndOperatorsEnum::Nine, L"123,456,789", L"" }, + { NumbersAndOperatorsEnum::None, L"1", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->HexDisplayValue), StringReference(L"75B CD15")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->DecimalDisplayValue), StringReference(L"123,456,789")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->OctalDisplayValue), StringReference(L"726 746 425")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->BinaryDisplayValue), StringReference(L"0111 0101 1011 1100 1101 0001 0101")); + } + + // Test Not functionality + TEST_METHOD(ProgrammerModeNot) + { + TESTITEM none[] = { + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, none, true); + m_viewModel->IsProgrammer = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Not, L"-2", L"~(1)" }, + { NumbersAndOperatorsEnum::None, L"N/A", L"N/A" } + }; + ValidateViewModelByCommands(m_viewModel, items, false); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->HexDisplayValue), StringReference(L"FFFF FFFF FFFF FFFE")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->DecimalDisplayValue), StringReference(L"-2")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->OctalDisplayValue), StringReference(L"1 777 777 777 777 777 777 776")); + VERIFY_ARE_EQUAL(Utils::GetStringValue(m_viewModel->BinaryDisplayValue), StringReference(L"1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110")); + VERIFY_ARE_EQUAL(m_viewModel->DisplayValue, StringReference(L"-2")); + } + + // Test And Or functionality + TEST_METHOD(ProgrammerModeAndOr) + { + TESTITEM none[] = { + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, none, true); + m_viewModel->IsProgrammer = true; + + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Or, L"1", L"1 ||" }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 ||" }, + { NumbersAndOperatorsEnum::Equals, L"3", L"" }, + { NumbersAndOperatorsEnum::None, L"3", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, false); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::And, L"1", L"1 &" }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 &" }, + { NumbersAndOperatorsEnum::Equals, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"0", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + } + + // Test CE and C buttons functionality + TEST_METHOD(ProgrammerModeClear) + { + TESTITEM none[] = { + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, none, true); + m_viewModel->IsProgrammer = true; + + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Or, L"1", L"1 ||" }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 ||" }, + { NumbersAndOperatorsEnum::ClearEntry, L"0", L"1 ||" }, + { NumbersAndOperatorsEnum::None, L"", L"1 ||" } + }; + ValidateViewModelByCommands(m_viewModel, items, false); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::And, L"1", L"1 &" }, + { NumbersAndOperatorsEnum::Two, L"2", L"1 &" }, + { NumbersAndOperatorsEnum::Clear, L"0", L"" }, + { NumbersAndOperatorsEnum::None, L"0", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + } + + // Test unary operators + TEST_METHOD(ButtonPressedUnaryOperatorTest) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::IsStandardMode, L"0", L"" }, + { NumbersAndOperatorsEnum::Five, L"5", L"" }, + { NumbersAndOperatorsEnum::Invert, L"0" + std::wstring(m_decimalSeparator->Data()) + L"2", L"reciproc(5)" }, + { NumbersAndOperatorsEnum::Equals, L"0" + std::wstring(m_decimalSeparator->Data()) + L"2", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Six, L"16", L"" }, + { NumbersAndOperatorsEnum::Sqrt, L"4", L"sqrt(16)" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + + TESTITEM items3[] = { + { NumbersAndOperatorsEnum::Six, L"6", L"" }, + { NumbersAndOperatorsEnum::Negate, L"-6", L"" }, + { NumbersAndOperatorsEnum::Nine, L"-69", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items3, false); + + TESTITEM items4[] = { + { NumbersAndOperatorsEnum::Clear, L"0", L"" }, + { NumbersAndOperatorsEnum::IsScientificMode, L"0", L"" }, + { NumbersAndOperatorsEnum::Five, L"5", L"" }, + { NumbersAndOperatorsEnum::InvSin, L"Invalid input", L"asind(5)" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items4, false); + + TESTITEM items5[] = { + { NumbersAndOperatorsEnum::Clear, L"0", L"" }, + { NumbersAndOperatorsEnum::Four, L"4", L"" }, + { NumbersAndOperatorsEnum::Factorial, L"24", L"fact(4)" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items5, false); + } + + // IsMemoryEmpty Property + TEST_METHOD(IsMemoryEmptyTest) + { + StandardCalculatorViewModel^ viewModel = ref new StandardCalculatorViewModel(); + viewModel->IsStandard = true; + VERIFY_ARE_EQUAL(static_cast(0), viewModel->MemorizedNumbers->Size); + viewModel->OnMemoryButtonPressed(); + VERIFY_ARE_EQUAL(static_cast(1), viewModel->MemorizedNumbers->Size); + viewModel->ClearMemoryCommand->Execute(nullptr); + VERIFY_ARE_EQUAL(static_cast(0), viewModel->MemorizedNumbers->Size); + } + + // IsOperatorCommand Property + TEST_METHOD(IsOperatorCommandTest) + { + StandardCalculatorViewModel^ viewModel = ref new StandardCalculatorViewModel(); + viewModel->IsStandard = true; + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::One); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Two); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Three); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Four); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Five); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Six); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Seven); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Eight); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Nine); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Decimal); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Zero); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Multiply); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, true); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Add); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, true); + viewModel->ButtonPressed->Execute(NumbersAndOperatorsEnum::Zero); + VERIFY_ARE_EQUAL(viewModel->IsOperatorCommand, false); + } + + //When memory button is pressed - verify if display value is being stored in vector + TEST_METHOD(OnMemoryButtonPressed) + { + StandardCalculatorViewModel^ viewModel = ref new StandardCalculatorViewModel(); + viewModel->IsStandard = true; + viewModel->DisplayValue = L"1001"; + viewModel->OnMemoryButtonPressed(); + viewModel->OnMemoryButtonPressed(); + VERIFY_ARE_EQUAL((int)viewModel->MemorizedNumbers->Size, 2); + } + + //when memory list is empty and M+ is pressed + TEST_METHOD(OnMemoryAddWhenMemoryEmpty) + { + m_viewModel->IsStandard = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryAdd(ref new Platform::Box(0)); + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001"), m_viewModel->DisplayValue); + } + + + //when memory list is empty and M- is pressed + TEST_METHOD(OnMemorySubtractWhenMemoryEmpty) + { + m_viewModel->IsStandard = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemorySubtract(ref new Platform::Box(0)); + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001"), m_viewModel->DisplayValue); + } + + //when negative number is saved in memory + TEST_METHOD(OnNegativeEntryInMemory) + { + ChangeMode(m_viewModel, 0/*Standard*/); + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Negate, L"N/A", L"N/A" }, + { NumbersAndOperatorsEnum::None, L"", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001"), m_viewModel->DisplayValue); + MemoryItemViewModel^ memorySlotStandard = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001"), Utils::GetStringValue(memorySlotStandard->Value)); + ChangeMode(m_viewModel, 1/*scientific*/); + MemoryItemViewModel^ memorySlotScientific = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001"), Utils::GetStringValue(memorySlotScientific->Value)); + ChangeMode(m_viewModel, 2/*Programmer*/); + MemoryItemViewModel^ memorySlotProgrammer = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001"), Utils::GetStringValue(memorySlotProgrammer->Value)); + } + + //when decimal number is saved in memory + TEST_METHOD(OnDecimalEntryInMemory) + { + ChangeMode(m_viewModel, 0/*Standard*/); + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"1,001.", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001.1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001.1"), m_viewModel->DisplayValue); + MemoryItemViewModel^ memorySlotStandard = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001.1"), Utils::GetStringValue(memorySlotStandard->Value)); + ChangeMode(m_viewModel, 1/*Scientific*/); + MemoryItemViewModel^ memorySlotScientific = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001.1"), Utils::GetStringValue(memorySlotScientific->Value)); + ChangeMode(m_viewModel, 2/*Programmer*/); + MemoryItemViewModel^ memorySlotProgrammer = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001"), Utils::GetStringValue(memorySlotProgrammer->Value)); + } + + //when negative decimal number is saved in memory + TEST_METHOD(OnNegativeDecimalInMemory) + { + m_viewModel->IsStandard = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"1,001.", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001.1", L"" }, + { NumbersAndOperatorsEnum::Negate, L"N/A", L"N/A" }, + { NumbersAndOperatorsEnum::None, L"", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"-1,001.1"), m_viewModel->DisplayValue); + } + + //when decimal number is added to the memory + TEST_METHOD(OnDecimalAddedToMemory) + { + m_viewModel->IsStandard = true; + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"1,001.", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001.1", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Decimal, L"1,001.", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryAdd(1); + m_viewModel->OnMemoryItemPressed(1); + VERIFY_ARE_EQUAL(Platform::StringReference(L"2,002.1"), m_viewModel->DisplayValue); + } + + //when memory is saved in programmer as Hex value and then we switch to standard mode, test to see that memory gets converted to decimal + TEST_METHOD(OnMemorySavedInHexRadixAndSwitchedToStandardMode) + { + ChangeMode(m_viewModel, 2/*programmer*/); + TESTITEM items[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::F, L"FF", L"" }, + { NumbersAndOperatorsEnum::None, L"FF", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + ChangeMode(m_viewModel, 1/*Scientific*/); + m_viewModel->OnMemoryItemPressed(ref new Box(0)); + VERIFY_ARE_EQUAL(Platform::StringReference(L"255"), m_viewModel->DisplayValue); + MemoryItemViewModel^ memorySlot = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"255"), Utils::GetStringValue(memorySlot->Value)); + } + + + TEST_METHOD(OnMemorySavedInHexRadixAndRadixChanges) + { + ChangeMode(m_viewModel, 2/*programmer*/); + TESTITEM items[] = { + { NumbersAndOperatorsEnum::HexButton, L"0", L"" }, + { NumbersAndOperatorsEnum::F, L"F", L"" }, + { NumbersAndOperatorsEnum::F, L"FF", L"" }, + { NumbersAndOperatorsEnum::None, L"FF", L"" }, + }; + ValidateViewModelByCommands(m_viewModel, items, true); + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->SwitchProgrammerModeBase(RADIX_TYPE::OCT_RADIX); + MemoryItemViewModel^ memorySlotOct = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"377"), Utils::GetStringValue(memorySlotOct->Value)); + m_viewModel->SwitchProgrammerModeBase(RADIX_TYPE::DEC_RADIX); + MemoryItemViewModel^ memorySlotDec = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"255"), Utils::GetStringValue(memorySlotDec->Value)); + m_viewModel->SwitchProgrammerModeBase(RADIX_TYPE::BIN_RADIX); + MemoryItemViewModel^ memorySlotBin = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"1111 1111"), Utils::GetStringValue(memorySlotBin->Value)); + + } + + //When memory button is pressed more than max number of slots allowed, + //the MemorizedNumbers vector size should not increase. + TEST_METHOD(OnMemoryButtonPressedMaxTimes) + { + StandardCalculatorViewModel^ viewModel = ref new StandardCalculatorViewModel(); + viewModel->IsStandard = true; + viewModel->DisplayValue = L"1001"; + for (int i = 0; i < 110; i++) + { + viewModel->OnMemoryButtonPressed(); + } + VERIFY_ARE_EQUAL((int)viewModel->MemorizedNumbers->Size, 100); + } + + // When memory slot is pressed verify if the display value is updated correctly + TEST_METHOD(OnMemoryItemPressed) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + m_viewModel->OnMemoryButtonPressed(); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::Two, L"2", L"" }, + { NumbersAndOperatorsEnum::Zero, L"20", L"" }, + { NumbersAndOperatorsEnum::Zero, L"200", L"" }, + { NumbersAndOperatorsEnum::Two, L"2,002", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryItemPressed(1); + + VERIFY_ARE_EQUAL(Platform::StringReference(L"1,001"), m_viewModel->DisplayValue); + } + + // verify nothing happens if there is no memory and the memory slot pressed action is taken + TEST_METHOD(OnMemoryItemPressedNoMemory) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Two, L"12", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + m_viewModel->OnMemoryItemPressed(ref new Platform::Box(0)); + + VERIFY_ARE_EQUAL(StringReference(L"12"), m_viewModel->DisplayValue); + VERIFY_ARE_EQUAL((UINT)0, m_viewModel->MemorizedNumbers->Size); + } + + // When memory slot is pressed verify if the display value is updated correctly + TEST_METHOD(OnMemoryItemAddAndSubtract) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + m_viewModel->OnMemoryButtonPressed(); + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::Two, L"2", L"" }, + { NumbersAndOperatorsEnum::Zero, L"20", L"" }, + { NumbersAndOperatorsEnum::Zero, L"200", L"" }, + { NumbersAndOperatorsEnum::Two, L"2,002", L"" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false); + + m_viewModel->OnMemoryButtonPressed(); + m_viewModel->OnMemoryItemPressed(1); + m_viewModel->OnMemoryAdd(ref new Platform::Box(0)); + MemoryItemViewModel^ memorySlot = (MemoryItemViewModel^)m_viewModel->MemorizedNumbers->GetAt(0); + VERIFY_ARE_EQUAL(Platform::StringReference(L"3,003"), Utils::GetStringValue(memorySlot->Value)); + } + + // Verify that raw, unformatted numbers are provided correctly + TEST_METHOD(VerifyRawFormatting) + { + m_viewModel->DisplayValue = L"1,001"; + VERIFY_ARE_EQUAL(StringReference(L"1001"), m_viewModel->GetRawDisplayValue()); + + m_viewModel->DisplayValue = L"999"; + VERIFY_ARE_EQUAL(StringReference(L"999"), m_viewModel->GetRawDisplayValue()); + + m_viewModel->DisplayValue = L"1,001,001"; + VERIFY_ARE_EQUAL(StringReference(L"1001001"), m_viewModel->GetRawDisplayValue()); + + m_viewModel->DisplayValue = L"1,001, 001"; + VERIFY_ARE_EQUAL(StringReference(L"1001001"), m_viewModel->GetRawDisplayValue()); + + } + + TEST_METHOD(VerifyAnnouncementAfterBinaryOperatorReceived) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"1,001", L"1001 x" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + VERIFY_ARE_EQUAL(StringReference(L"Display is 1,001 times"), m_viewModel->Announcement->Announcement); + } + + TEST_METHOD(VerifyAnnouncementAfterMultipleBinaryOperatorsReceived) + { + TESTITEM items[] = { + { NumbersAndOperatorsEnum::One, L"1", L"" }, + { NumbersAndOperatorsEnum::Zero, L"10", L"" }, + { NumbersAndOperatorsEnum::Zero, L"100", L"" }, + { NumbersAndOperatorsEnum::One, L"1,001", L"" }, + { NumbersAndOperatorsEnum::Multiply, L"1,001", L"1001 x" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items, true); + + VERIFY_ARE_EQUAL(StringReference(L"Display is 1,001 times"), m_viewModel->Announcement->Announcement); + + + TESTITEM items2[] = { + { NumbersAndOperatorsEnum::Divide, L"1,001", L"1001 ÷" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items2, false /*reset*/); + + VERIFY_ARE_EQUAL(StringReference(L"Display is 1,001 divided by"), m_viewModel->Announcement->Announcement); + + TESTITEM items3[] = { + { NumbersAndOperatorsEnum::Add, L"1,001", L"1001 +" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items3, false /*reset*/); + + VERIFY_ARE_EQUAL(StringReference(L"Display is 1,001 plus"), m_viewModel->Announcement->Announcement); + + + TESTITEM items4[] = { + { NumbersAndOperatorsEnum::Subtract, L"1,001", L"1001 x" }, + { NumbersAndOperatorsEnum::None, L"", L"" } + }; + ValidateViewModelByCommands(m_viewModel, items4, false /*reset*/); + + VERIFY_ARE_EQUAL(StringReference(L"Display is 1,001 minus"), m_viewModel->Announcement->Announcement); + } + + private: + StandardCalculatorViewModel^ m_viewModel; + std::shared_ptr m_engineResourceProvider; + Platform::String^ m_decimalSeparator; + }; +} + diff --git a/internal/CalculatorUnitTests/Test.resw b/internal/CalculatorUnitTests/Test.resw new file mode 100644 index 00000000..5cb77d94 --- /dev/null +++ b/internal/CalculatorUnitTests/Test.resw @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + +4046.8564224 + +1 + +0.09290304 + +0.83612736 + +0.000001 + +0.0001 + +0.00064516 + +2589988.110336 + +1000000 + +10000 + +0.012516104 + +0.06032246 + +10869.66 + +100000 + +0.000000125 + +0.000001 + +0.001 + +1 + +1000 + +1000000 + +1000000000 + +1000000000000 + +1000000000000000 + +1000000000000000000 + +0.000125 + +0.125 + +125 + +125000 + +125000000 + +125000000000 + +125000000000000 + +125000000000000000 + +134.217728 + +1073.741824 + +0.000128 + +0.001024 + +0.131072 + +1.048576 + +140737488.355328 + +1125899906.842624 + +137438.953472 + +1099511.627776 + +144115188075.855872 + +1152921504606.846976 + +147573952589676.412928 + +1180591620717411.303424 + +151115727451828646.838272 + +1208925819614629174.706176 + +1.509949 + +734.003200 + +5046.586573 + +4.184 + +4184 + +1055.056 + +1000 + +0.0000000000000000001602176565 + +1 + +1.3558179483314 + +9000 + +439614 + +1046700 + +0.0254 + +0.3048 + +0.9144 + +1609.344 + +0.000001 + +0.001 + +0.000000001 + +0.01 + +1 + +1000 + +1852 + +0.035052 + +0.18669 + +76 + +17.58426666666667 + +0.0225969658055233 + +1 + +1000 + +745.69987158227022 + +60 + +745.7 + +2982799.486329081 + +86400 + +1 + +604800 + +31557600 + +0.001 + +0.000001 + +60 + +3600 + +236.588237 + +473.176473 + +568.26125 + +946.352946 + +1136.5225 + +3785.411784 + +4546.09 + +1000 + +4.928922 + +14.786765 + +1 + +764554.857984 + +1000000 + +1 + +16.387064 + +28316.846592 + +29.5735295625 + +28.4130625 + +5.91938802083333333333 + +17.7581640625 + +236.5882 + +378541.2 + +3750000000 + +1 + +0.1 + +0.01 + +0.001 + +0.45359237 + +0.028349523125 + +0.000001 + +0.00001 + +0.0001 + +1016.0469088 + +1000 + +6.35029318 + +0.0002 + +907.18474 + +0.000002 + +0.4325 + +4000 + +90000 + +1 + +30.48 + +27.777777777777777777778 + +51.44 + +34030 + +100 + +44.7 + +8.94 + +2011.5 + +24585 + +1 + +57.29577951308233 + +0.9 + +1 + +0.9869232667160128 + +0.0098692326671601 + +0.0013155687145324 + +9.869232667160128e-6 + +0.068045961016531 + + 0.002 + + + 1 + + + 1 + + + 1000 + + + 1 + + + 0.003 + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/UnitConverterTest.cpp b/internal/CalculatorUnitTests/UnitConverterTest.cpp new file mode 100644 index 00000000..8c002d63 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitConverterTest.cpp @@ -0,0 +1,600 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +#include + +using namespace UnitConversionManager; +using namespace std; + +namespace UnitConverterUnitTests +{ + + void SetUnitParams(Unit* type, int id, wstring name, wstring abbreviation, bool conversionSource, bool conversionTarget, bool isWhimsical) + { + type->id = id; + type->name = name; + type->abbreviation = abbreviation; + type->isConversionSource = conversionSource; + type->isConversionTarget = conversionTarget; + type->isWhimsical = isWhimsical; + } + + void SetCategoryParams(Category* type, int id, wstring name, bool supportsNegative) + { + type->id = id; + type->name = name; + type->supportsNegative = supportsNegative; + } + + void SetConversionDataParams(ConversionData* type, double ratio, double offset, bool offsetFirst) + { + type->ratio = ratio; + type->offset = offset; + type->offsetFirst = offsetFirst; + } + + class TestUnitConverterConfigLoader : public IConverterDataLoader + { + public: + TestUnitConverterConfigLoader() : + m_loadDataCallCount(0) + { + Category c1, c2; + SetCategoryParams(&c1, 1, L"Length", true); + SetCategoryParams(&c2, 2, L"Weight", false); + m_categories.push_back(c1); + m_categories.push_back(c2); + + Unit u1, u2, u3, u4; + SetUnitParams(&u1, 1, L"Inches", L"In", true, true, false); + SetUnitParams(&u2, 2, L"Feet", L"Ft", false, false, false); + SetUnitParams(&u3, 3, L"Pounds", L"Lb", true, true, false); + SetUnitParams(&u4, 4, L"Kilograms", L"Kg", false, false, false); + + vector c1units = vector(); + vector c2units = vector(); + c1units.push_back(u1); + c1units.push_back(u2); + c2units.push_back(u3); + c2units.push_back(u4); + + m_units[c1] = c1units; + m_units[c2] = c2units; + + unordered_map unit1Map = unordered_map(); + unordered_map unit2Map = unordered_map(); + unordered_map unit3Map = unordered_map(); + unordered_map unit4Map = unordered_map(); + + ConversionData conversion1, conversion2, conversion3, conversion4, conversion5; + SetConversionDataParams(&conversion1, 1.0, 0, false); + SetConversionDataParams(&conversion2, 0.08333333333333333333333333333333, 0, false); + SetConversionDataParams(&conversion3, 12.0, 0, false); + SetConversionDataParams(&conversion4, 0.453592, 0, false); + SetConversionDataParams(&conversion5, 2.20462, 0, false); + + //Setting the conversion ratios for testing + unit1Map[u1] = conversion1; + unit1Map[u2] = conversion2; + + unit2Map[u1] = conversion3; + unit2Map[u2] = conversion1; + + unit3Map[u3] = conversion1; + unit3Map[u4] = conversion4; + + unit4Map[u3] = conversion5; + unit4Map[u4] = conversion1; + + + m_ratioMaps[u1] = unit1Map; + m_ratioMaps[u2] = unit2Map; + m_ratioMaps[u3] = unit3Map; + m_ratioMaps[u4] = unit4Map; + } + + void LoadData() + { + m_loadDataCallCount++; + } + + vector LoadOrderedCategories() + { + return m_categories; + } + + vector LoadOrderedUnits(const Category& c) + { + return m_units[c]; + } + + unordered_map LoadOrderedRatios(const Unit& u) + { + return m_ratioMaps[u]; + } + + bool SupportsCategory(const Category& target) + { + return true; + } + + UINT m_loadDataCallCount; + private: + vector m_categories; + CategoryToUnitVectorMap m_units; + UnitToUnitToConversionDataMap m_ratioMaps; + }; + + class TestUnitConverterVMCallback : public IUnitConverterVMCallback + { + public: + void Reset() + { + m_maxDigitsReachedCallCount = 0; + } + + void DisplayCallback(const wstring& from, const wstring& to) override + { + m_lastFrom = from; + m_lastTo = to; + } + + void SuggestedValueCallback(const vector>& suggestedValues) override + { + m_lastSuggested = suggestedValues; + } + + void MaxDigitsReached() override + { + m_maxDigitsReachedCallCount++; + } + + int GetMaxDigitsReachedCallCount() + { + return m_maxDigitsReachedCallCount; + } + + bool CheckDisplayValues(wstring from, wstring to) + { + return (from == m_lastFrom && to == m_lastTo); + } + + bool CheckSuggestedValues(vector> suggested) + { + if (suggested.size() != m_lastSuggested.size()) + { + return false; + } + bool returnValue = true; + for (unsigned int i = 0; i < suggested.size(); i++) + { + if (suggested[i] != m_lastSuggested[i]) + { + returnValue = false; + break; + } + } + return returnValue; + } + private: + wstring m_lastFrom; + wstring m_lastTo; + vector> m_lastSuggested; + int m_maxDigitsReachedCallCount = 0; + }; + + class UnitConverterTest + { + public: + // Declare this class as a TestClass, and supply metadata if necessary. + TEST_CLASS(UnitConverterTest); + TEST_CLASS_SETUP(CommonSetup); + + TEST_METHOD_CLEANUP(Cleanup); + + TEST_METHOD(UnitConverterTestInit); + TEST_METHOD(UnitConverterTestBasic); + TEST_METHOD(UnitConverterTestGetters); + TEST_METHOD(UnitConverterTestGetCategory); + TEST_METHOD(UnitConverterTestUnitTypeSwitching); + TEST_METHOD(UnitConverterTestSerialization); + TEST_METHOD(UnitConverterTestDeSerialization); + TEST_METHOD(UnitConverterTestQuote); + TEST_METHOD(UnitConverterTestUnquote); + TEST_METHOD(UnitConverterTestBackspace); + TEST_METHOD(UnitConverterTestScientificInputs); + TEST_METHOD(UnitConverterTestSupplementaryResultRounding); + TEST_METHOD(UnitConverterTestMaxDigitsReached); + TEST_METHOD(UnitConverterTestMaxDigitsReached_LeadingDecimal); + TEST_METHOD(UnitConverterTestMaxDigitsReached_TrailingDecimal); + TEST_METHOD(UnitConverterTestMaxDigitsReached_MultipleTimes); + private: + static void ExecuteCommands(vector commands); + + static shared_ptr s_unitConverter; + static shared_ptr s_xmlLoader; + static shared_ptr s_testVMCallback; + static Category s_testLength; + static Category s_testWeight; + static Unit s_testInches; + static Unit s_testFeet; + static Unit s_testPounds; + static Unit s_testKilograms; + }; + + shared_ptr UnitConverterTest::s_unitConverter; + shared_ptr UnitConverterTest::s_xmlLoader; + shared_ptr UnitConverterTest::s_testVMCallback; + Category UnitConverterTest::s_testLength; + Category UnitConverterTest::s_testWeight; + Unit UnitConverterTest::s_testInches; + Unit UnitConverterTest::s_testFeet; + Unit UnitConverterTest::s_testPounds; + Unit UnitConverterTest::s_testKilograms; + + // Creates instance of UnitConverter before running tests + bool UnitConverterTest::CommonSetup() + { + s_testVMCallback = make_shared(); + s_xmlLoader = make_shared(); + s_unitConverter = make_shared(s_xmlLoader); + s_unitConverter->SetViewModelCallback(s_testVMCallback); + SetCategoryParams(&s_testLength, 1, L"Length", true); + SetCategoryParams(&s_testWeight, 2, L"Weight", false); + SetUnitParams(&s_testInches, 1, L"Inches", L"In", true, true, false); + SetUnitParams(&s_testFeet, 2, L"Feet", L"Ft", false, false, false); + SetUnitParams(&s_testPounds, 3, L"Pounds", L"Lb", true, true, false); + SetUnitParams(&s_testKilograms, 4, L"Kilograms", L"Kg", false, false, false); + return true; + } + + // Resets calculator state to start state after each test + bool UnitConverterTest::Cleanup() + { + s_unitConverter->DeSerialize(wstring()); + s_testVMCallback->Reset(); + return true; + } + + void UnitConverterTest::ExecuteCommands(vector commands) + { + for (size_t i = 0; i < commands.size() && commands[i] != Command::None; i++) + { + s_unitConverter->SendCommand(commands[i]); + } + } + + // Test ctor/initialization states + void UnitConverterTest::UnitConverterTestInit() + { + VERIFY_ARE_EQUAL((UINT)0, s_xmlLoader->m_loadDataCallCount); // shouldn't have initialized the loader yet + s_unitConverter->Initialize(); + VERIFY_ARE_EQUAL((UINT)1, s_xmlLoader->m_loadDataCallCount); // now we should have loaded + } + + // Verify a basic input command stream.'3', '2', '.', '0' + void UnitConverterTest::UnitConverterTestBasic() + { + tuple test1[] = {tuple(wstring(L"0.25"), s_testFeet)}; + tuple test2[] = {tuple(wstring(L"2.5"), s_testFeet)}; + + s_unitConverter->SendCommand(Command::Three); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"3"), wstring(L"3"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test1),end(test1)))); + s_unitConverter->SendCommand(Command::Zero); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30"), wstring(L"30"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test2),end(test2)))); + s_unitConverter->SendCommand(Command::Decimal); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30."), wstring(L"30"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test2),end(test2)))); + s_unitConverter->SendCommand(Command::Zero); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30.0"), wstring(L"30"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test2),end(test2)))); + } + + // Check the getter functions + void UnitConverterTest::UnitConverterTestGetters() + { + Category test1[] = {s_testLength, s_testWeight}; + Unit test2[] = {s_testInches, s_testFeet}; + + VERIFY_IS_TRUE(s_unitConverter->GetCategories() == vector(begin(test1),end(test1))); + VERIFY_IS_TRUE(get<0>(s_unitConverter->SetCurrentCategory(test1[0])) == vector(begin(test2),end(test2))); + } + + // Test getting category after it has been set. + void UnitConverterTest::UnitConverterTestGetCategory() + { + s_unitConverter->SetCurrentCategory(s_testWeight); + VERIFY_IS_TRUE(s_unitConverter->GetCurrentCategory() == s_testWeight); + } + + // Test switching of unit types + void UnitConverterTest::UnitConverterTestUnitTypeSwitching() + { + // Enter 57 into the from field, then switch focus to the to field (making it the new from field) + s_unitConverter->SendCommand(Command::Five); + s_unitConverter->SendCommand(Command::Seven); + s_unitConverter->SwitchActive(wstring(L"57")); + // Now set unit conversion to go from kilograms to pounds + s_unitConverter->SetCurrentCategory(s_testWeight); + s_unitConverter->SetCurrentUnitTypes(s_testKilograms, s_testPounds); + s_unitConverter->SendCommand(Command::Five); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"5"), wstring(L"11.0231"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>())); + } + + // Test serialization + void UnitConverterTest::UnitConverterTestSerialization() + { + wstring test1 = wstring(L"4;Kilograms;Kg;0;0;0;|3;Pounds;Lb;1;1;0;|2;0;Weight;|1;1;0;52.8;116.4039;|1;1;Length;,2;0;Weight;,|1;1;Length;[1;Inches;In;1;1;0;,2;Feet;Ft;0;0;0;,[]2;0;Weight;[3;Pounds;Lb;1;1;0;,4;Kilograms;Kg;0;0;0;,[]|1;Inches;In;1;1;0;[1;Inches;In;1;1;0;:1;0;0;:,2;Feet;Ft;0;0;0;:0.08333333333333332870740406406185;0;0;:,[]2;Feet;Ft;0;0;0;[1;Inches;In;1;1;0;:12;0;0;:,2;Feet;Ft;0;0;0;:1;0;0;:,[]3;Pounds;Lb;1;1;0;[3;Pounds;Lb;1;1;0;:1;0;0;:,4;Kilograms;Kg;0;0;0;:0.45359199999999999519673110626172;0;0;:,[]4;Kilograms;Kg;0;0;0;[3;Pounds;Lb;1;1;0;:2.20461999999999980204279381723609;0;0;:,4;Kilograms;Kg;0;0;0;:1;0;0;:,[]|"); + s_unitConverter->SendCommand(Command::Five); + s_unitConverter->SendCommand(Command::Two); + s_unitConverter->SendCommand(Command::Decimal); + s_unitConverter->SendCommand(Command::Eight); + s_unitConverter->SetCurrentCategory(s_testWeight); + s_unitConverter->SetCurrentUnitTypes(s_testKilograms, s_testPounds); + VERIFY_IS_TRUE(s_unitConverter->Serialize().compare(test1) == 0); + } + + // Test input escaping + void UnitConverterTest::UnitConverterTestQuote() + { + wstring input1 = L"Weight"; + wstring output1 = L"Weight"; + wstring input2 = L"{p}Weig;[ht|"; + wstring output2 = L"{lb}p{rb}Weig{sc}{lc}ht{p}"; + wstring input3 = L"{{{t;s}}},:]"; + wstring output3 = L"{lb}{lb}{lb}t{sc}s{rb}{rb}{rb}{cm}{co}{rc}"; + VERIFY_IS_TRUE(UnitConverter::Quote(input1) == output1); + VERIFY_IS_TRUE(UnitConverter::Quote(input2) == output2); + VERIFY_IS_TRUE(UnitConverter::Quote(input3) == output3); + } + + // Test output unescaping + void UnitConverterTest::UnitConverterTestUnquote() + { + wstring input1 = L"Weight"; + wstring input2 = L"{p}Weig;[ht|"; + wstring input3 = L"{{{t;s}}},:]"; + VERIFY_IS_TRUE(UnitConverter::Unquote(input1) == input1); + VERIFY_IS_TRUE(UnitConverter::Unquote(UnitConverter::Quote(input1)) == input1); + VERIFY_IS_TRUE(UnitConverter::Unquote(UnitConverter::Quote(input2)) == input2); + VERIFY_IS_TRUE(UnitConverter::Unquote(UnitConverter::Quote(input3)) == input3); + } + + // Test de-serialization + void UnitConverterTest::UnitConverterTestDeSerialization() + { + wstring test1 = wstring(L"4;Kilograms;Kg;0;0;0;|3;Pounds;Lb;1;1;0;|2;0;Weight;|1;1;0;52.8;116.4039;|1;1;Length;,2;0;Weight;,|1;1;Length;[1;Inches;In;1;1;0;,2;Feet;Ft;0;0;0;,[]2;0;Weight;[3;Pounds;Lb;1;1;0;,4;Kilograms;Kg;0;0;0;,[]|1;Inches;In;1;1;0;[1;Inches;In;1;1;0;:1;0;0;:,2;Feet;Ft;0;0;0;:0.08333333333333332870740406406185;0;0;:,[]2;Feet;Ft;0;0;0;[1;Inches;In;1;1;0;:12;0;0;:,2;Feet;Ft;0;0;0;:1;0;0;:,[]3;Pounds;Lb;1;1;0;[3;Pounds;Lb;1;1;0;:1;0;0;:,4;Kilograms;Kg;0;0;0;:0.45359199999999999519673110626172;0;0;:,[]4;Kilograms;Kg;0;0;0;[3;Pounds;Lb;1;1;0;:2.20461999999999980204279381723609;0;0;:,4;Kilograms;Kg;0;0;0;:1;0;0;:,[]|"); + s_unitConverter->DeSerialize(test1); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"52.8"), wstring(L"116.4039"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>())); + } + + // Test backspace commands + void UnitConverterTest::UnitConverterTestBackspace() + { + tuple test1[] = {tuple(wstring(L"13.66"), s_testKilograms)}; + tuple test2[] = {tuple(wstring(L"13.65"), s_testKilograms)}; + tuple test3[] = {tuple(wstring(L"13.61"), s_testKilograms)}; + tuple test4[] = {tuple(wstring(L"1.36"), s_testKilograms)}; + + s_unitConverter->SetCurrentCategory(s_testWeight); + s_unitConverter->SetCurrentUnitTypes(s_testPounds, s_testPounds); + s_unitConverter->SendCommand(Command::Three); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Decimal); + s_unitConverter->SendCommand(Command::One); + s_unitConverter->SendCommand(Command::Two); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30.12"), wstring(L"30.12"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test1),end(test1)))); + s_unitConverter->SendCommand(Command::Backspace); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30.1"), wstring(L"30.1"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test2),end(test2)))); + s_unitConverter->SendCommand(Command::Backspace); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30."), wstring(L"30"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test3),end(test3)))); + s_unitConverter->SendCommand(Command::Backspace); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"30"), wstring(L"30"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test3),end(test3)))); + s_unitConverter->SendCommand(Command::Backspace); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"3"), wstring(L"3"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test4),end(test4)))); + s_unitConverter->SendCommand(Command::Backspace); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"0"), wstring(L"0"))); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>())); + } + + // Test large values + void UnitConverterTest::UnitConverterTestScientificInputs() + { + s_unitConverter->SetCurrentCategory(s_testWeight); + s_unitConverter->SetCurrentUnitTypes(s_testPounds, s_testKilograms); + s_unitConverter->SendCommand(Command::Decimal); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::Zero); + s_unitConverter->SendCommand(Command::One); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"0.00000000000001"), wstring(L"4.535920e-15"))); + s_unitConverter->SwitchActive(wstring(L"4.535920e-15")); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + s_unitConverter->SendCommand(Command::Nine); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"999999999999999"), wstring(L"2.204620e+15"))); + s_unitConverter->SwitchActive(wstring(L"2.20463e+15")); + s_unitConverter->SendCommand(Command::One); + s_unitConverter->SendCommand(Command::Two); + s_unitConverter->SendCommand(Command::Three); + s_unitConverter->SendCommand(Command::Four); + s_unitConverter->SendCommand(Command::Five); + s_unitConverter->SendCommand(Command::Six); + s_unitConverter->SendCommand(Command::Seven); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"1234567"), wstring(L"559989.7"))); + s_unitConverter->SwitchActive(wstring(L"559989.7")); + s_unitConverter->SendCommand(Command::One); + s_unitConverter->SendCommand(Command::Two); + s_unitConverter->SendCommand(Command::Three); + s_unitConverter->SendCommand(Command::Four); + s_unitConverter->SendCommand(Command::Five); + s_unitConverter->SendCommand(Command::Six); + s_unitConverter->SendCommand(Command::Seven); + s_unitConverter->SendCommand(Command::Eight); + VERIFY_IS_TRUE(s_testVMCallback->CheckDisplayValues(wstring(L"12345678"), wstring(L"27217528.63236"))); + } + + // Test large values + void UnitConverterTest::UnitConverterTestSupplementaryResultRounding() + { + tuple test1[] = {tuple(wstring(L"27.75"), s_testFeet)}; + tuple test2[] = {tuple(wstring(L"277.8"), s_testFeet)}; + tuple test3[] = {tuple(wstring(L"2778"), s_testFeet)}; + s_unitConverter->SendCommand(Command::Three); + s_unitConverter->SendCommand(Command::Three); + s_unitConverter->SendCommand(Command::Three); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test1),end(test1)))); + s_unitConverter->SendCommand(Command::Three); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test2),end(test2)))); + s_unitConverter->SendCommand(Command::Three); + VERIFY_IS_TRUE(s_testVMCallback->CheckSuggestedValues(vector>(begin(test3),end(test3)))); + } + + void UnitConverterTest::UnitConverterTestMaxDigitsReached() + { + ExecuteCommands({ + Command::One, + Command::Two, + Command::Three, + Command::Four, + Command::Five, + Command::Six, + Command::Seven, + Command::Eight, + Command::Nine, + Command::One, + Command::Zero, + Command::One, + Command::One, + Command::One, + Command::Two + }); + + VERIFY_ARE_EQUAL(0, s_testVMCallback->GetMaxDigitsReachedCallCount()); + + ExecuteCommands({ Command::One }); + + VERIFY_ARE_EQUAL(1, s_testVMCallback->GetMaxDigitsReachedCallCount()); + } + + void UnitConverterTest::UnitConverterTestMaxDigitsReached_LeadingDecimal() + { + ExecuteCommands({ + Command::Zero, + Command::Decimal, + Command::One, + Command::Two, + Command::Three, + Command::Four, + Command::Five, + Command::Six, + Command::Seven, + Command::Eight, + Command::Nine, + Command::One, + Command::Zero, + Command::One, + Command::One, + Command::One + }); + + VERIFY_ARE_EQUAL(0, s_testVMCallback->GetMaxDigitsReachedCallCount()); + + ExecuteCommands({ Command::Two }); + + VERIFY_ARE_EQUAL(1, s_testVMCallback->GetMaxDigitsReachedCallCount()); + } + + void UnitConverterTest::UnitConverterTestMaxDigitsReached_TrailingDecimal() + { + ExecuteCommands({ + Command::One, + Command::Two, + Command::Three, + Command::Four, + Command::Five, + Command::Six, + Command::Seven, + Command::Eight, + Command::Nine, + Command::One, + Command::Zero, + Command::One, + Command::One, + Command::One, + Command::Two, + Command::Decimal + }); + + VERIFY_ARE_EQUAL(0, s_testVMCallback->GetMaxDigitsReachedCallCount()); + + ExecuteCommands({ Command::One }); + + VERIFY_ARE_EQUAL(1, s_testVMCallback->GetMaxDigitsReachedCallCount()); + } + + void UnitConverterTest::UnitConverterTestMaxDigitsReached_MultipleTimes() + { + ExecuteCommands({ + Command::One, + Command::Two, + Command::Three, + Command::Four, + Command::Five, + Command::Six, + Command::Seven, + Command::Eight, + Command::Nine, + Command::One, + Command::Zero, + Command::One, + Command::One, + Command::One, + Command::Two + }); + + VERIFY_ARE_EQUAL(0, s_testVMCallback->GetMaxDigitsReachedCallCount()); + + for (auto count = 1; count <= 10; count++) + { + ExecuteCommands({ Command::Three }); + + VERIFY_ARE_EQUAL(count, s_testVMCallback->GetMaxDigitsReachedCallCount(), to_wstring(count).c_str()); + } + } +} + diff --git a/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp b/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp new file mode 100644 index 00000000..7d4d18e2 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp @@ -0,0 +1,1115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include "UnitConverterViewModelUnitTests.h" + +#include "CalcViewModel\UnitConverterViewModel.h" +#include "CalcViewModel\DataLoaders\UnitConverterDataLoader.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Utils; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Data; + +namespace UCM = UnitConversionManager; +namespace VM = CalculatorApp::ViewModel; +using namespace Windows::Globalization; + +namespace CalculatorUnitTests +{ + String^ AddUnicodeLTRMarkers(wstring str) + { + str.insert(str.begin(), L'\u202a'); + str.push_back(L'\u202c'); + return ref new String(str.c_str()); + } + + class CategoryViewModelTests + { + public: + TEST_CLASS(CategoryViewModelTests); + + // Test all binding values: + + TEST_METHOD(TestGetNameReturnsCorrectName) + { + wstring name = L"TestName"; + UCM::Category cat; + cat.name = name; + VM::Category^ vmcat = ref new VM::Category(cat); + VERIFY_ARE_EQUAL(vmcat->Name->Data(), name); + } + + TEST_METHOD(TestGetVisibilityReturnsVisible) + { + UCM::Category cat; + cat.supportsNegative = true; + VM::Category^ vmcat = ref new VM::Category(cat); + VERIFY_IS_TRUE(Visibility::Visible == vmcat->NegateVisibility); + } + + TEST_METHOD(TestGetVisibilityReturnsCollapsed) + { + UCM::Category cat; + cat.supportsNegative = false; + VM::Category^ vmcat = ref new VM::Category(cat); + VERIFY_IS_TRUE(Visibility::Collapsed == vmcat->NegateVisibility); + } + }; + + class UnitViewModelTests + { + public: + TEST_CLASS(UnitViewModelTests); + + // Test all binding values: + + TEST_METHOD(TestGetNameReturnsCorrectName) + { + wstring name = L"TestName"; + UCM::Unit unit; + unit.name = name; + VM::Unit^ vmunit = ref new VM::Unit(unit); + VERIFY_ARE_EQUAL(vmunit->Name->Data(), name); + } + + TEST_METHOD(TestGetAbbreviationReturnsCorrectAbbreviation) + { + wstring abbr = L"TestName"; + UCM::Unit unit; + unit.abbreviation = abbr; + VM::Unit^ vmunit = ref new VM::Unit(unit); + VERIFY_ARE_EQUAL(vmunit->Abbreviation->Data(), abbr); + } + }; + + class SupplementaryResultsViewModelTests + { + public: + TEST_CLASS(SupplementaryResultsViewModelTests); + + // Test all binding values: + + TEST_METHOD(TestGetValueReturnsCorrectValue) + { + wstring value = L"TestName"; + UCM::Unit unit = { 1, L"", L"", false, false, false }; + VM::Unit^ vmunit = ref new VM::Unit(unit); + VM::SupplementaryResult^ vmsupp = ref new VM::SupplementaryResult(ref new String(value.c_str()), vmunit); + VERIFY_ARE_EQUAL(vmsupp->Value->Data(), value); + } + + TEST_METHOD(TestGetUnitNameReturnsCorrectValue) + { + wstring name = L"TestName"; + UCM::Unit unit = { 1, name, L"", false, false, false }; + unit.name = name; + VM::Unit^ vmunit = ref new VM::Unit(unit); + VM::SupplementaryResult^ vmsupp = ref new VM::SupplementaryResult(L"", vmunit); + VERIFY_ARE_EQUAL(vmsupp->Unit->Name->Data(), name); + } + + TEST_METHOD(TestGetIsWhimsicalReturnsCorrectValue) + { + UCM::Unit unit = { 1, L"", L"", false, false, false }; + UCM::Unit unitW = { 2, L"", L"", false, false, true }; + VM::Unit^ vmUnit = ref new VM::Unit(unit); + VM::Unit^ vmUnitW = ref new VM::Unit(unitW); + VM::SupplementaryResult^ vmsupp = ref new VM::SupplementaryResult(L"", vmUnit); + VM::SupplementaryResult^ vmsuppW = ref new VM::SupplementaryResult(L"", vmUnitW); + VERIFY_IS_FALSE(vmsupp->IsWhimsical()); + VERIFY_IS_TRUE(vmsuppW->IsWhimsical()); + } + }; + + UnitConverterMock::UnitConverterMock() : + m_initCallCount(0), + m_getCategoriesCallCount(0), + m_setCurrentCategoryCallCount(0), + m_setCurUnitTypesCallCount(0), + m_switchActiveCallCount(0), + m_sendCommandCallCount(0), + m_setVMCallbackCallCount(0), + m_serializeCallCount(0), + m_deSerializeCallCount(0) + { } + + // IUnitConverter + + void UnitConverterMock::Initialize() + { + m_initCallCount++; + } + + vector UnitConverterMock::GetCategories() + { + m_getCategoriesCallCount++; + + vector cats; + cats.push_back(CAT1); + cats.push_back(CAT2); + cats.push_back(CAT3); + + m_curCategory = CAT2; + + return cats; + } + + UCM::CategorySelectionInitializer UnitConverterMock::SetCurrentCategory(const UCM::Category& input) + { + m_setCurrentCategoryCallCount++; + m_curCategory = input; + vector units; + switch (input.id) + { + case 1: + { + units.push_back(UNIT1); + units.push_back(UNIT2); + units.push_back(UNIT3); + break; + } + case 2: + { + units.push_back(UNIT4); + units.push_back(UNIT5); + units.push_back(UNIT6); + break; + } + case 3: + { + units.push_back(UNIT7); + units.push_back(UNIT8); + units.push_back(UNIT9); + break; + } + default: + throw; + } + + for (const UCM::Unit& unit : units) + { + if (unit.isConversionSource) + { + m_curFrom = unit; + } + + if (unit.isConversionTarget) + { + m_curTo = unit; + } + } + + units.push_back(UNITWHIMSY); // needs to be filtered out + + return make_tuple(units, m_curFrom, m_curTo); + } + + UCM::Category UnitConverterMock::GetCurrentCategory() + { + return m_curCategory; + } + + void UnitConverterMock::SetCurrentUnitTypes(const UCM::Unit& fromType, const UCM::Unit& toType) + { + m_setCurUnitTypesCallCount++; + m_curFrom = fromType; + m_curTo = toType; + m_vmCallback->SuggestedValueCallback(m_suggestedList); + } + + void UnitConverterMock::SwitchActive(const std::wstring& newValue) + { + m_switchActiveCallCount++; + m_curValue = newValue; + } + + wstring UnitConverterMock::Serialize() + { + m_serializeCallCount++; + return wstring(L""); + } + + void UnitConverterMock::DeSerialize(const wstring& serializedData) + { + m_deSerializeCallCount++; + } + + std::wstring UnitConverterMock::SaveUserPreferences() + { + return L"TEST"; + }; + + void UnitConverterMock::RestoreUserPreferences(_In_ const std::wstring& userPreferences) + { + }; + + void UnitConverterMock::SendCommand(UCM::Command command) + { + m_sendCommandCallCount++; + m_lastCommand = command; + + m_vmCallback->SuggestedValueCallback(m_suggestedList); + } + + void UnitConverterMock::SetViewModelCallback(const shared_ptr& newCallback) + { + m_setVMCallbackCallCount++; + m_vmCallback = newCallback; + } + + ref class MockActivatable sealed : public VM::IActivatable + { + private: + bool m_active; + + public: + MockActivatable(bool active) : m_active(active) + { } + + virtual property bool IsActive + { + bool get() { return m_active; } + void set(bool value) { m_active = value; } + } + }; + + class UnitConverterDataLoaderTests + { + public: + TEST_CLASS(UnitConverterDataLoaderTests); + + TEST_METHOD(FuncUnitConverterAllUnitCombinations) + { + VM::UnitConverterViewModel^ vm = ref new UnitConverterViewModel(make_shared(make_shared(ref new GeographicRegion()), nullptr)); + IObservableVector^ categoryList = vm->Categories; + ResourceLoader^ m_resLoader = ResourceLoader::GetForViewIndependentUse("Test"); + double epsilon = 0.1; //Could be more precise like 0.001 except atm to pascal conversion + + for (unsigned int k = 0; k < categoryList->Size; k++) + { + vm->CurrentCategory = categoryList->GetAt(k); + IObservableVector^ unitList = vm->Units; + for (unsigned int i = 0; i < unitList->Size; i++) + { + vm->Unit1 = unitList->GetAt(i); + wstring unit1Name = vm->Unit1->Name->Data(); + for (unsigned int j = 0; j < unitList->Size; j++) + { + vm->Value2Active = true; + vm->Value1Active = false; + vm->Unit2 = unitList->GetAt(j); + wstring unit2Name = vm->Unit2->Name->Data(); + //change value2 as 1. + vm->ButtonPressed->Execute(NumbersAndOperatorsEnum::One); + String^ expectedResult = m_resLoader->GetString(ref new String((unit1Name + L"-" + unit2Name).c_str())); + + // if the corresponding conversion ratio is present in Test.resw file + if (expectedResult != nullptr) + { + wstring expResult = expectedResult->Data(); + + double expectedConversion = GetDoubleFromWstring(expResult); + double actualConversion = GetDoubleFromWstring(GetStringValue(vm->Value1)->Data()); + double diff = abs(expectedConversion - actualConversion); + + // assert for diff less than epsilonth fraction of expected conversion result + VERIFY_IS_LESS_THAN_OR_EQUAL(diff, epsilon*expectedConversion); + } + //clearing the value1 + vm->ButtonPressed->Execute(NumbersAndOperatorsEnum::Clear); + } + } + } + } + }; + + class UnitConverterViewModelTests + { + public: + TEST_CLASS(UnitConverterViewModelTests); + + // Test that the ctor makes value1 active and value2 passive + TEST_METHOD(TestUnitConverterCtorSetsUpCorrectActiveValue) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + VERIFY_IS_TRUE(vm.Value1Active); + VERIFY_IS_FALSE(vm.Value2Active); + } + + // Test that we've created all the vectors on init. + TEST_METHOD(TestUnitConverterCtorSetsUpVectors) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + VERIFY_IS_NOT_NULL(vm.Categories); + VERIFY_IS_NOT_NULL(vm.Units); + VERIFY_IS_NOT_NULL(vm.SupplementaryResults); + } + + // Test that we've set up all the display callbacks on init. + TEST_METHOD(TestUnitConverterLoadSetsUpCallbacks) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setVMCallbackCallCount); + } + + // Test that we've set up all categories on load and that the first + // category is selected. + TEST_METHOD(TestUnitConverterLoadSetsUpCategories) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + IObservableVector^ cats = vm.Categories; + VERIFY_ARE_EQUAL((UINT)1, mock->m_getCategoriesCallCount); + VERIFY_ARE_EQUAL((UINT)3, cats->Size); + // Verify that we match current category + VERIFY_IS_TRUE(CAT2 == vm.CurrentCategory->GetModelCategory()); + } + + // Test that we've set up all units on load and that the default + // units are selected. + TEST_METHOD(TestUnitConverterLoadSetsUpUnits) + { + shared_ptr mock = make_shared(); + + VM::UnitConverterViewModel vm(mock); + IObservableVector^ units = vm.Units; + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurrentCategoryCallCount); + VERIFY_ARE_EQUAL((UINT)3, units->Size); + VERIFY_IS_TRUE(UNIT4 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT6 == vm.Unit2->GetModelUnit()); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurUnitTypesCallCount); + VERIFY_IS_TRUE(mock->m_curFrom == UNIT4); + VERIFY_IS_TRUE(mock->m_curTo == UNIT6); + } + + // Test that changing units updates the current unit types + // in the model + TEST_METHOD(TestUnitSelectionChangeUpdatesModel) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.Unit1 = vm.Units->GetAt(1); // Change from u4 to u5 + // count will be 2 here since it was already called once at init + VERIFY_ARE_EQUAL((UINT)2, mock->m_setCurUnitTypesCallCount); + VERIFY_IS_TRUE(UNIT5 == mock->m_curFrom); + VERIFY_IS_TRUE(UNIT6 == mock->m_curTo); + vm.Unit2 = vm.Units->GetAt(0); // Change from u3 to u1 + VERIFY_ARE_EQUAL((UINT)3, mock->m_setCurUnitTypesCallCount); + VERIFY_IS_TRUE(UNIT5 == mock->m_curFrom); + VERIFY_IS_TRUE(UNIT4 == mock->m_curTo); + } + + // Test that changing categories updates the unit list + TEST_METHOD(TestCategorySelectionChangeUpdatesUnits) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.CurrentCategory = vm.Categories->GetAt(2); // Change from cat1 to cat3 + // counts will be 2 here since the first call should have happened during init + VERIFY_IS_GREATER_THAN_OR_EQUAL(2u, mock->m_setCurrentCategoryCallCount); + VERIFY_ARE_EQUAL((UINT)3, vm.Units->Size); + VERIFY_IS_TRUE(UNIT7 == vm.Units->GetAt(0)->GetModelUnit()); + VERIFY_IS_TRUE(UNIT8 == vm.Units->GetAt(1)->GetModelUnit()); + VERIFY_IS_TRUE(UNIT9 == vm.Units->GetAt(2)->GetModelUnit()); + } + + // Test that changing categories updates the current unit types + // in the model + TEST_METHOD(TestCategorySelectionChangeUpdatesModel) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.CurrentCategory = vm.Categories->GetAt(2); // Change from cat1 to cat3 + // counts will be 2 here since the first call should have happened during init + VERIFY_IS_GREATER_THAN_OR_EQUAL(2u, mock->m_setCurrentCategoryCallCount); + VERIFY_IS_TRUE(UNIT9 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT7 == vm.Unit2->GetModelUnit()); + VERIFY_IS_GREATER_THAN_OR_EQUAL(2u, mock->m_setCurUnitTypesCallCount); + VERIFY_IS_TRUE(UNIT9 == mock->m_curFrom); + VERIFY_IS_TRUE(UNIT7 == mock->m_curTo); + } + + // Test that the displaycallback updates the display values + TEST_METHOD(TestDisplayCallbackUpdatesDisplayValues) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1234", *vTo = L"56.78"; + vm.UpdateDisplay(vFrom, vTo); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(L"1,234")); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(vTo)); + } + + // Test that the calculator button command correctly fires + // commands to the model. + TEST_METHOD(TestButtonCommandFiresModelCommands) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + // Call count is being set to 1 because we send 'CE' command as the first call + UINT callCount = 1; + + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Zero); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Zero == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::One); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::One == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Two); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Two == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Three); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Three == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Four); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Four == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Five); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Five == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Six); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Six == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Seven); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Seven == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Eight); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Eight == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Nine); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Nine == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Decimal); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Decimal == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Negate); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Negate == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Backspace); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Backspace == mock->m_lastCommand); + vm.ButtonPressed->Execute(CalculatorApp::NumbersAndOperatorsEnum::Clear); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Clear == mock->m_lastCommand); + + for (NumbersAndOperatorsEnum button = NumbersAndOperatorsEnum::Add; button <= NumbersAndOperatorsEnum::None; button++) + { + if (button == NumbersAndOperatorsEnum::Decimal || + button == NumbersAndOperatorsEnum::Negate || + button == NumbersAndOperatorsEnum::Backspace) + { + continue; + } + vm.ButtonPressed->Execute(button); + VERIFY_ARE_EQUAL(++callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::None == mock->m_lastCommand); + } + } + + // Tests that when we fire the OnGotFocus, it activates the given control + TEST_METHOD(TestOnValueGotFocusActivatesControl) + { + //shared_ptr mock = make_shared(); + //VM::UnitConverterViewModel vm(mock); + //MockActivatable^ activatable = ref new MockActivatable(false); + //vm.OnValueGotFocus(AsActivatable(activatable)); + //VERIFY_IS_TRUE(activatable->IsActive); + //vm.OnValueGotFocus(AsActivatable(activatable)); // try again, starting with true + //VERIFY_IS_TRUE(activatable->IsActive); + } + + // Tests that when we select the currently active value, nothing + // happens + TEST_METHOD(TestReselectCurrentlyActiveValueDoesNothing) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + // Establish base condition + VERIFY_ARE_EQUAL((UINT)0, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_sendCommandCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurUnitTypesCallCount); + vm.Value1Active = true; + VERIFY_ARE_EQUAL((UINT)0, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_sendCommandCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurUnitTypesCallCount); + } + + // Tests that when we set switch the active value, it sets the oppsite value + // to inactive + TEST_METHOD(TestActivatingValueDeactivatesOther) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.Value1Active = true; // base + VERIFY_IS_TRUE(vm.Value1Active); + VERIFY_IS_FALSE(vm.Value2Active); + vm.Value2Active = true; // change + VERIFY_IS_TRUE(vm.Value2Active); + VERIFY_IS_FALSE(vm.Value1Active); + vm.Value2Active = true; // setting it to true again doesnt change anything + VERIFY_IS_TRUE(vm.Value2Active); + VERIFY_IS_FALSE(vm.Value1Active); + vm.Value1Active = true; // back to 1 + VERIFY_IS_TRUE(vm.Value1Active); + VERIFY_IS_FALSE(vm.Value2Active); + } + + // Tests that when we switch the active value, the active value + // gets updated in the model + TEST_METHOD(TestSwitchActiveValueUpdatesActiveValueInModel) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + VERIFY_ARE_EQUAL((UINT)1, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL(0, mock->m_curValue.compare(vTo)); + } + + // Tests that the suggested visibility value gets updated correctly + TEST_METHOD(TestSuggestedVisibilityIsUpdated) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + VERIFY_IS_TRUE(Visibility::Collapsed == vm.SupplementaryVisibility); + vector> supp; + supp.push_back(tuple(L"1", UNIT1)); + vm.UpdateSupplementaryResults(supp); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_IS_TRUE(Visibility::Visible == vm.SupplementaryVisibility); + vm.UpdateSupplementaryResults(vector>()); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_IS_TRUE(Visibility::Collapsed == vm.SupplementaryVisibility); + } + + // Tests that when we switch the active field and get display + // updates, the correct values are being updated. + TEST_METHOD(TestDisplayValueUpdatesAfterSwitchingActiveUpdateTheRightDisplay) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + const WCHAR * newvFrom = L"3", *newvTo = L"57"; + vm.UpdateDisplay(newvFrom, newvTo); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(newvFrom)); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(newvTo)); + } + + // Tests that when we switch the active field and get change units, + // the correct unit values are being passed. + TEST_METHOD(TestUnitChangeAfterSwitchingActiveUpdateUnitsCorrectly) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.Unit2 = vm.Units->GetAt(0); + VERIFY_IS_TRUE(UNIT4 == mock->m_curFrom); + vm.Unit1 = vm.Units->GetAt(2); + VERIFY_IS_TRUE(UNIT6 == mock->m_curTo); + } + + // Test that if we switch categories and come back, our first + // and second units are still the same + TEST_METHOD(TestCategorySwitchAndBackKeepsUnitsUnchanged) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.CurrentCategory = vm.Categories->GetAt(2); + vm.CurrentCategory = vm.Categories->GetAt(0); + VERIFY_IS_TRUE(UNIT1 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT2 == vm.Unit2->GetModelUnit()); + } + + // Test that if we switch categories and active, and then + // come back, our first and second units are swapped + // and second unit is active + TEST_METHOD(TestCategoryAndActiveSwitchAndBack) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.CurrentCategory = vm.Categories->GetAt(2); + vm.Value2Active = true; + vm.CurrentCategory = vm.Categories->GetAt(0); + VERIFY_IS_TRUE(UNIT2 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT1 == vm.Unit2->GetModelUnit()); + VERIFY_IS_TRUE(vm.Value2Active); + } + + // Tests that when we switch the active field and then change + // category, the correct units are displayed. + TEST_METHOD(TestCategoryChangeAfterSwitchingActiveUpdatesDisplayCorrectly) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.CurrentCategory = vm.Categories->GetAt(2); + VERIFY_IS_TRUE(UNIT7 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT9 == vm.Unit2->GetModelUnit()); + VERIFY_IS_TRUE(UNIT9 == mock->m_curFrom); + VERIFY_IS_TRUE(UNIT7 == mock->m_curTo); + VERIFY_ARE_EQUAL((UINT)1, mock->m_switchActiveCallCount); + const wchar_t * newvFrom = L"5", *newvTo = L"7"; + vm.UpdateDisplay(newvFrom, newvTo); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(newvFrom)); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(newvTo)); + } + + // Repeat above active switch tests but with a second switch to ensure + // transitions work both ways. + TEST_METHOD(TestSwitchAndReselectCurrentlyActiveValueDoesNothing) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + // Establish base condition + VERIFY_ARE_EQUAL((UINT)1, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_sendCommandCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurUnitTypesCallCount); + vm.Value2Active = true; + VERIFY_ARE_EQUAL((UINT)1, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_sendCommandCallCount); + VERIFY_ARE_EQUAL((UINT)1, mock->m_setCurUnitTypesCallCount); + } + + TEST_METHOD(TestSwitchActiveValueTwiceUpdatesActiveValueInModel) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.Value1Active = true; + VERIFY_ARE_EQUAL((UINT)2, mock->m_switchActiveCallCount); + VERIFY_ARE_EQUAL(0, mock->m_curValue.compare(vFrom)); + } + + TEST_METHOD(TestDisplayValueUpdatesAfterSwitchingActiveTwiceUpdateTheRightDisplay) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.Value1Active = true; + const WCHAR * newvFrom = L"3", *newvTo = L"57"; + vm.UpdateDisplay(newvFrom, newvTo); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(newvFrom)); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(newvTo)); + } + + TEST_METHOD(TestUnitChangeAfterSwitchingActiveTwiceUpdateUnitsCorrectly) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.Value1Active = true; + vm.Unit2 = vm.Units->GetAt(0); + VERIFY_IS_TRUE(UNIT4 == mock->m_curTo); + vm.Unit1 = vm.Units->GetAt(2); + VERIFY_IS_TRUE(UNIT6 == mock->m_curFrom); + } + + TEST_METHOD(TestCategoryChangeAfterSwitchingActiveTwiceUpdatesDisplayCorrectly) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"1", *vTo = L"234"; + vm.UpdateDisplay(vFrom, vTo); + vm.Value2Active = true; + vm.Value1Active = true; + vm.CurrentCategory = vm.Categories->GetAt(2); + VERIFY_IS_TRUE(UNIT9 == vm.Unit1->GetModelUnit()); + VERIFY_IS_TRUE(UNIT7 == vm.Unit2->GetModelUnit()); + VERIFY_IS_TRUE(UNIT9 == mock->m_curFrom); + VERIFY_IS_TRUE(UNIT7 == mock->m_curTo); + VERIFY_ARE_EQUAL((UINT)2, mock->m_switchActiveCallCount); + const wchar_t * newvFrom = L"5", *newvTo = L"7"; + vm.UpdateDisplay(newvFrom, newvTo); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(newvFrom)); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(newvTo)); + } + + // There is a 100 ms fudge time for the time based tests below + + // Test that UpdateSupplementaryResults updates the supplementary results + // after a delay + TEST_METHOD(TestSuggestedValuesCallbackUpdatesSupplementaryResults) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vector> supp; + supp.push_back(tuple(L"1", UNIT1)); + supp.push_back(tuple(L"2", UNIT2)); + supp.push_back(tuple(L"3", UNIT3)); + vm.UpdateSupplementaryResults(supp); + VERIFY_ARE_EQUAL((UINT)0, vm.SupplementaryResults->Size); + WaitForSingleObjectEx(GetCurrentThread(), 200, FALSE); + // Now we should see it + VERIFY_ARE_EQUAL((UINT)3, vm.SupplementaryResults->Size); + VERIFY_IS_TRUE((AddUnicodeLTRMarkers(L"1")) == vm.SupplementaryResults->GetAt(0)->Value); + VERIFY_IS_TRUE(UNIT1 == vm.SupplementaryResults->GetAt(0)->Unit->GetModelUnit()); + VERIFY_IS_TRUE((AddUnicodeLTRMarkers(L"2")) == vm.SupplementaryResults->GetAt(1)->Value); + VERIFY_IS_TRUE(UNIT2 == vm.SupplementaryResults->GetAt(1)->Unit->GetModelUnit()); + VERIFY_IS_TRUE((AddUnicodeLTRMarkers(L"3")) == vm.SupplementaryResults->GetAt(2)->Value); + VERIFY_IS_TRUE(UNIT3 == vm.SupplementaryResults->GetAt(2)->Unit->GetModelUnit()); + } + + // Test that changing category immediately updates supplementary results + TEST_METHOD(TestCategoryChangeImmediatelyUpdatesSupplementaryResults) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vector> supp; + supp.push_back(tuple(L"1", UNIT1)); + supp.push_back(tuple(L"2", UNIT2)); + supp.push_back(tuple(L"3", UNIT3)); + vm.UpdateSupplementaryResults(supp); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_ARE_EQUAL((UINT)3, vm.SupplementaryResults->Size); // Verify we're in the state we expect as a pre condition + + vm.CurrentCategory = vm.Categories->GetAt(2); + VERIFY_ARE_EQUAL((UINT)0, vm.SupplementaryResults->Size); + } + + // Test that changing category immediately updates supplementary results + TEST_METHOD(TestCategoryChangeImmediatelyUpdatesSupplementaryResultsWhimsy) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vector> supp; + supp.push_back(tuple(L"1", UNIT1)); + supp.push_back(tuple(L"2", UNIT2)); + supp.push_back(tuple(L"3", UNIT3)); + supp.push_back(tuple(L"4", UNITWHIMSY)); + vm.UpdateSupplementaryResults(supp); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_ARE_EQUAL((UINT)4, vm.SupplementaryResults->Size); // Verify we're in the state we expect as a pre condition + + VERIFY_IS_TRUE(vm.SupplementaryResults->GetAt(3)->Unit->GetModelUnit().isWhimsical); + VERIFY_IS_FALSE(vm.SupplementaryResults->GetAt(0)->Unit->GetModelUnit().isWhimsical); + } + + // Test that changing units immediately updates supplementary results + TEST_METHOD(TestUnitChangeImmediatelyUpdatesSupplementaryResults) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vector> supp; + supp.push_back(tuple(L"1", UNIT1)); + supp.push_back(tuple(L"2", UNIT2)); + supp.push_back(tuple(L"3", UNIT3)); + vm.UpdateSupplementaryResults(supp); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_ARE_EQUAL((UINT)3, vm.SupplementaryResults->Size); // Verify we're in the state we expect as a pre condition + + vm.Unit1 = vm.Units->GetAt(2); + VERIFY_ARE_EQUAL((UINT)0, vm.SupplementaryResults->Size); + + // Reset and try with other unit + vm.UpdateSupplementaryResults(supp); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_ARE_EQUAL((UINT)3, vm.SupplementaryResults->Size); // Verify we're in the state we expect as a pre condition + + vm.Unit2 = vm.Units->GetAt(0); + VERIFY_ARE_EQUAL((UINT)0, vm.SupplementaryResults->Size); + } + + // Test that only the first whimsical unit is selected and is appended + // to end of supplementary results. + TEST_METHOD(TestSupplementaryResultsWhimsicalUnits) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + UCM::Unit unit; + unit.isWhimsical = false; + + UCM::Unit unitWhimSubsequent; + unitWhimSubsequent.isWhimsical = true; + + vector> suggestedList; + suggestedList.push_back(tuple(L"", unit)); + suggestedList.push_back(tuple(L"", unit)); + suggestedList.push_back(tuple(L"", UNITWHIMSY)); + suggestedList.push_back(tuple(L"", unitWhimSubsequent)); + suggestedList.push_back(tuple(L"", unit)); + suggestedList.push_back(tuple(L"", unit)); + suggestedList.push_back(tuple(L"", unitWhimSubsequent)); + suggestedList.push_back(tuple(L"", unit)); + + vm.UpdateSupplementaryResults(suggestedList); + WaitForSingleObjectEx(GetCurrentThread(), 1100, FALSE); + VERIFY_ARE_EQUAL((UINT)6, vm.SupplementaryResults->Size); + while (vm.SupplementaryResults->Size > 1) + { + VERIFY_IS_FALSE(vm.SupplementaryResults->GetAt(0)->IsWhimsical()); + vm.SupplementaryResults->RemoveAt(0); + } + // Last item + VERIFY_IS_TRUE(vm.SupplementaryResults->GetAt(0)->Unit->GetModelUnit() == UNITWHIMSY); + } + + // Test deserialization + TEST_METHOD(TestUnitConverterViewModelDeserialization) + { + String ^ serializedTest = L"0[;;;]0[;;;]0[;;;]1[;;;]1.5[;;;]25[;;;]1.5[;;;]25[;;;][###][###]"; + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.Deserialize(serializedTest); + VERIFY_IS_TRUE(vm.Value1 == L"1.5"); + VERIFY_IS_TRUE(vm.Value2 == L"25"); + VERIFY_IS_TRUE(vm.GetValueFromUnlocalized() == L"1.5"); + VERIFY_IS_TRUE(vm.GetValueToUnlocalized() == L"25"); + VERIFY_ARE_EQUAL(vm.Value1Active, false); + VERIFY_ARE_EQUAL(vm.Value2Active, true); + VERIFY_IS_TRUE(mock->m_deSerializeCallCount == 1); + } + + // Test serialization + /*TEST_METHOD(TestUnitConverterViewModelSerialization) + { + String ^ serializedTest = L"0[;;;]0[;;;]0[;;;]1[;;;];;;]1.5[;;;[;;;]25[###[;;;]0[;;;]0[;;;][###][###]"; + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.Value1 = L";;;]1.5[;;;"; + vm.Value2 = L"25[###"; + vm.Value1Active = false; + vm.Value2Active = true; + String ^ test = vm.Serialize(); + VERIFY_IS_TRUE(serializedTest == test); + VERIFY_IS_TRUE(mock->m_serializeCallCount == 1); + }*/ + + TEST_METHOD(TestOnPaste) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + // Call count is being set to 1 because we send 'CE' command as the first call in OnPaste method of the ViewModel + UINT callCount = 1; + ViewMode mode = ViewMode::Volume; // Some temp mode for UnitConverter + + // Paste an invalid character - verify that call count doesn't increment + vm.OnPaste("z", mode); + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + + + // Test all valid characters. Verify that two commands are sent for each character + vm.OnPaste("0", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Zero == mock->m_lastCommand); + + vm.OnPaste("1", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::One == mock->m_lastCommand); + + vm.OnPaste("2", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Two == mock->m_lastCommand); + + vm.OnPaste("3", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Three == mock->m_lastCommand); + + vm.OnPaste("4", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Four == mock->m_lastCommand); + + vm.OnPaste("5", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Five == mock->m_lastCommand); + + vm.OnPaste("6", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Six == mock->m_lastCommand); + + vm.OnPaste("7", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Seven == mock->m_lastCommand); + + vm.OnPaste("8", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Eight == mock->m_lastCommand); + + vm.OnPaste("9", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Nine == mock->m_lastCommand); + + vm.OnPaste(".", mode); + callCount += 2; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + VERIFY_IS_TRUE(UCM::Command::Decimal == mock->m_lastCommand); + + vm.OnPaste("-", mode); + // Call count should increment by one (the Clear command) since negate isn't + // sent by itself, only after another legal character + ++callCount; + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + + // Send an invalid character + + vm.OnPaste("a", mode); + // Count should remain the same + VERIFY_ARE_EQUAL(callCount, mock->m_sendCommandCallCount); + // Last command should remain the same + VERIFY_IS_TRUE(UCM::Command::Clear == mock->m_lastCommand); + } + + TEST_METHOD(TestDecimalFormattingLogic) + { + // verify that a dangling decimal is left alone until the user switches active fields, and that trailing zeroes + // entered by the user remain in the display even after a switch. + + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + const WCHAR * vFrom = L"3.", *vTo = L"2.50"; + vm.UpdateDisplay(vFrom, vTo); + // Establish base condition + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(L"3.")); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(L"2.50")); + vm.SwitchActive->Execute(nullptr); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(L"3")); // dangling decimal now removed + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(L"2.50")); + vm.SwitchActive->Execute(nullptr); + VERIFY_IS_TRUE(vm.Value1 == AddUnicodeLTRMarkers(L"3")); + VERIFY_IS_TRUE(vm.Value2 == AddUnicodeLTRMarkers(L"2.50")); + } + // Tests that when we switch the active field and get display + // updates, the correct automation names are are being updated. + TEST_METHOD(TestValue1AndValue2AutomationNameChanges) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + vm.Value1 = L"1"; + vm.Unit1 = vm.Units->GetAt(1); + VERIFY_IS_TRUE(vm.Value1AutomationName == L"Convert from 1 UNIT5"); + VERIFY_IS_TRUE(vm.Value2AutomationName == L"Converts into 0 UNIT6"); + vm.Value2 = L"234"; + vm.Value2Active = true; + VERIFY_IS_TRUE(vm.Value2AutomationName == L"Convert from 234 UNIT6"); + VERIFY_IS_TRUE(vm.Value1AutomationName == L"Converts into 1 UNIT5"); + vm.Value2 = L"3"; + vm.Unit2 = vm.Units->GetAt(0); + VERIFY_IS_TRUE(vm.Value2AutomationName == L"Convert from 3 UNIT4"); + VERIFY_IS_TRUE(vm.Value1AutomationName == L"Converts into 1 UNIT5"); + vm.Value1Active = true; + VERIFY_IS_TRUE(vm.Value1AutomationName == L"Convert from 1 UNIT5"); + VERIFY_IS_TRUE(vm.Value2AutomationName == L"Converts into 3 UNIT4"); + } + + // Test that an invalid model unit list results in a Units list of size 1 + // containing EMPTY_UNIT. + TEST_METHOD(TestUnitsListBuildsFromEmptyModelUnitList) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vm.BuildUnitList({}); + + VERIFY_ARE_EQUAL(1u, vm.Units->Size); + VERIFY_ARE_EQUAL((UCM::EMPTY_UNIT).id, (vm.Units->GetAt(0)->GetModelUnit()).id); + } + + // Test that a valid model unit list vuilds + TEST_METHOD(TestUnitsListBuildsFromValidModelUnitList) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vm.BuildUnitList({ + UNIT1, + UNIT2, + UNIT3 + }); + + VERIFY_ARE_EQUAL(3u, vm.Units->Size); + VERIFY_ARE_EQUAL(UNIT1.id, (vm.Units->GetAt(0)->GetModelUnit()).id); + VERIFY_ARE_EQUAL(UNIT2.id, (vm.Units->GetAt(1)->GetModelUnit()).id); + VERIFY_ARE_EQUAL(UNIT3.id, (vm.Units->GetAt(2)->GetModelUnit()).id); + } + + TEST_METHOD(TestFindInListWhenListIsValid) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vm.BuildUnitList({ + UNIT1, + UNIT2, + UNIT3 + }); + + Unit^ foundUnit = vm.FindUnitInList(UNIT1); + VERIFY_ARE_EQUAL(UNIT1.id, foundUnit->GetModelUnit().id); + } + + TEST_METHOD(TestFindInListWhenListIsInvalid) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vm.BuildUnitList({}); + + Unit^ foundUnit = vm.FindUnitInList(UNIT1); + + VERIFY_ARE_EQUAL(UCM::EMPTY_UNIT.id, foundUnit->GetModelUnit().id); + } + + TEST_METHOD(TestSetSelectedUnits) + { + shared_ptr mock = make_shared(); + VM::UnitConverterViewModel vm(mock); + + vm.CurrentCategory = vm.Categories->GetAt(0); + + vm.SetSelectedUnits(); + VERIFY_ARE_NOT_EQUAL(UCM::EMPTY_UNIT.id, vm.Unit1->GetModelUnit().id); + VERIFY_ARE_NOT_EQUAL(UCM::EMPTY_UNIT.id, vm.Unit2->GetModelUnit().id); + } + }; +} + diff --git a/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.h b/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.h new file mode 100644 index 00000000..2585f758 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitConverterViewModelUnitTests.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +namespace UCM = UnitConversionManager; + +#pragma once + +namespace CalculatorUnitTests +{ + static UCM::Unit UNIT1 = { 1, L"UNIT1", L"U1", true, false, false }; + static UCM::Unit UNIT2 = { 2, L"UNIT2", L"U2", false, true, false }; + static UCM::Unit UNIT3 = { 3, L"UNIT3", L"U3", false, false, false }; + static UCM::Unit UNIT4 = { 4, L"UNIT4", L"U4", true, false, false }; + static UCM::Unit UNIT5 = { 5, L"UNIT5", L"U5", false, false, false }; + static UCM::Unit UNIT6 = { 6, L"UNIT6", L"U6", false, true, false }; + static UCM::Unit UNIT7 = { 7, L"UNIT7", L"U7", false, true, false }; + static UCM::Unit UNIT8 = { 8, L"UNIT8", L"U8", false, false, false }; + static UCM::Unit UNIT9 = { 9, L"UNIT9", L"U9", true, false, false }; + static UCM::Unit UNITWHIMSY = { 10, L"Whimsy", L"UW", true, false, true }; + + static UCM::Category CAT1 = { 1, L"CAT1", false }; // contains Unit1 - Unit3 + static UCM::Category CAT2 = { 2, L"CAT2", false }; // contains Unit4 - Unit6 + static UCM::Category CAT3 = { 3, L"CAT3", false }; // contains Unit7 - Unit9 + + + class UnitConverterMock : public UnitConversionManager::IUnitConverter + { + public: + UnitConverterMock(); + void Initialize() override; + std::vector GetCategories() override; + UCM::CategorySelectionInitializer SetCurrentCategory(const UCM::Category& input) override; + UCM::Category GetCurrentCategory(); + void SetCurrentUnitTypes(const UCM::Unit& fromType, const UCM::Unit& toType) override; + void SwitchActive(const std::wstring& newValue); + std::wstring Serialize() override; + void DeSerialize(const std::wstring& serializedData) override; + std::wstring SaveUserPreferences() override; + void RestoreUserPreferences(_In_ const std::wstring& userPreferences) override; + void SendCommand(UCM::Command command) override; + void SetViewModelCallback(const std::shared_ptr& newCallback) override; + void SetViewModelCurrencyCallback(_In_ const std::shared_ptr& newCallback) override {} + concurrency::task> RefreshCurrencyRatios() override + { + co_return std::make_pair(L"", L""); + } + + UINT m_initCallCount; + UINT m_getCategoriesCallCount; + UINT m_setCurrentCategoryCallCount; + UINT m_setCurUnitTypesCallCount; + UINT m_switchActiveCallCount; + UINT m_sendCommandCallCount; + UINT m_setVMCallbackCallCount; + UINT m_serializeCallCount; + UINT m_deSerializeCallCount; + + UCM::Category m_curCategory; + UCM::Unit m_curFrom; + UCM::Unit m_curTo; + UCM::Command m_lastCommand; + + std::shared_ptr m_vmCallback; + std::vector> m_suggestedList; + std::wstring m_curValue; + }; +} + diff --git a/internal/CalculatorUnitTests/UnitTestApp.rd.xml b/internal/CalculatorUnitTests/UnitTestApp.rd.xml new file mode 100644 index 00000000..74506cfd --- /dev/null +++ b/internal/CalculatorUnitTests/UnitTestApp.rd.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/UnitTestApp.xaml b/internal/CalculatorUnitTests/UnitTestApp.xaml new file mode 100644 index 00000000..54e34549 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitTestApp.xaml @@ -0,0 +1,8 @@ + + + diff --git a/internal/CalculatorUnitTests/UnitTestApp.xaml.cpp b/internal/CalculatorUnitTests/UnitTestApp.xaml.cpp new file mode 100644 index 00000000..88cfcb11 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitTestApp.xaml.cpp @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// App.xaml.cpp +// Implementation of the App class. +// + +#include "pch.h" +#include "UnitTestApp.xaml.h" + +using namespace CalculatorUnitTests; + +using namespace Platform; +using namespace Windows::ApplicationModel; +using namespace Windows::ApplicationModel::Activation; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Interop; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Navigation; + +// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=402347&clcid=0x409 + +/// +/// Initializes the singleton application object. This is the first line of authored code +/// executed, and as such is the logical equivalent of main() or WinMain(). +/// +App::App() +{ + InitializeComponent(); + Suspending += ref new SuspendingEventHandler(this, &App::OnSuspending); +} + +/// +/// Invoked when the application is launched normally by the end user. Other entry points +/// will be used such as when the application is launched to open a specific file. +/// +/// Details about the launch request and process. +void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ e) +{ + +#if _DEBUG + // Show graphics profiling information while debugging. + if (IsDebuggerPresent()) + { + // Display the current frame rate counters + DebugSettings->EnableFrameRateCounter = true; + } +#endif + + auto rootFrame = dynamic_cast(Window::Current->Content); + + // Do not repeat app initialization when the Window already has content, + // just ensure that the window is active + if (rootFrame == nullptr) + { + // Create a Frame to act as the navigation context and associate it with + // a SuspensionManager key + rootFrame = ref new Frame(); + + rootFrame->NavigationFailed += ref new Windows::UI::Xaml::Navigation::NavigationFailedEventHandler(this, &App::OnNavigationFailed); + + if (e->PreviousExecutionState == ApplicationExecutionState::Terminated) + { + // TODO: Restore the saved session state only when appropriate, scheduling the + // final launch steps after the restore is complete + + } + + // Place the frame in the current Window + Window::Current->Content = rootFrame; + } + + Microsoft::VisualStudio::TestPlatform::TestExecutor::WinRTCore::UnitTestClient::CreateDefaultUI(); + + Window::Current->Activate(); + + Microsoft::VisualStudio::TestPlatform::TestExecutor::WinRTCore::UnitTestClient::Run(e->Arguments); +} + +/// +/// Invoked when application execution is being suspended. Application state is saved +/// without knowing whether the application will be terminated or resumed with the contents +/// of memory still intact. +/// +/// The source of the suspend request. +/// Details about the suspend request. +void App::OnSuspending(Object^ sender, SuspendingEventArgs^ e) +{ + (void) sender; // Unused parameter + (void) e; // Unused parameter + + //TODO: Save application state and stop any background activity +} + +/// +/// Invoked when Navigation to a certain page fails +/// +/// The Frame which failed navigation +/// Details about the navigation failure +void App::OnNavigationFailed(Platform::Object ^sender, Windows::UI::Xaml::Navigation::NavigationFailedEventArgs ^e) +{ + throw ref new FailureException("Failed to load Page " + e->SourcePageType.Name); +} + diff --git a/internal/CalculatorUnitTests/UnitTestApp.xaml.h b/internal/CalculatorUnitTests/UnitTestApp.xaml.h new file mode 100644 index 00000000..f514d6d7 --- /dev/null +++ b/internal/CalculatorUnitTests/UnitTestApp.xaml.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// App.xaml.h +// Declaration of the App class. +// + +#pragma once + +#include "UnitTestApp.g.h" +#include "CalcViewModel\DateCalculatorViewModel.h" +#include "CalcViewModel\StandardCalculatorViewModel.h" +#include "CalcViewModel\UnitConverterViewModel.h" +#include "CalcViewModel\MemoryItemViewModel.h" + +namespace CalculatorUnitTests +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + ref class App sealed + { + protected: + virtual void OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ e) override; + + internal: + App(); + + private: + void OnSuspending(Platform::Object^ sender, Windows::ApplicationModel::SuspendingEventArgs^ e); + void OnNavigationFailed(Platform::Object ^sender, Windows::UI::Xaml::Navigation::NavigationFailedEventArgs ^e); + }; +} + diff --git a/internal/CalculatorUnitTests/UtilsTests.cpp b/internal/CalculatorUnitTests/UtilsTests.cpp new file mode 100644 index 00000000..7b53a0c0 --- /dev/null +++ b/internal/CalculatorUnitTests/UtilsTests.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include + +namespace CalculatorUnitTests +{ + class UtilsTests + { + public: + TEST_CLASS(UtilsTests); + + TEST_METHOD(IsLastCharacterSuccess) + { + VERIFY_IS_TRUE(Utils::IsLastCharacterTarget(L"Test.", L'.')); + } + + TEST_METHOD(IsLastCharacterSuccessMultipleSuffices) + { + VERIFY_IS_TRUE(Utils::IsLastCharacterTarget(L"Test..", L'.')); + } + + TEST_METHOD(IsLastCharacterFailure) + { + VERIFY_IS_FALSE(Utils::IsLastCharacterTarget(L"Test", L'.')); + } + + TEST_METHOD(IsLastCharacterFailureAllButLastMatch) + { + VERIFY_IS_FALSE(Utils::IsLastCharacterTarget(L".....T", L'.')); + } + + TEST_METHOD(IsLastCharacterFailureEmptyInput) + { + VERIFY_IS_FALSE(Utils::IsLastCharacterTarget({}, L'.')); + } + + TEST_METHOD(IsLastCharacterFailureNullTarget) + { + VERIFY_IS_FALSE(Utils::IsLastCharacterTarget({}, NULL)); + } + }; +} diff --git a/internal/CalculatorUnitTests/packages.config b/internal/CalculatorUnitTests/packages.config new file mode 100644 index 00000000..6b2ab24a --- /dev/null +++ b/internal/CalculatorUnitTests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/internal/CalculatorUnitTests/pch.cpp b/internal/CalculatorUnitTests/pch.cpp new file mode 100644 index 00000000..22973364 --- /dev/null +++ b/internal/CalculatorUnitTests/pch.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// pch.cpp +// Include the standard header and generate the precompiled header. +// + +#include "pch.h" + diff --git a/internal/CalculatorUnitTests/pch.h b/internal/CalculatorUnitTests/pch.h new file mode 100644 index 00000000..fd760ad6 --- /dev/null +++ b/internal/CalculatorUnitTests/pch.h @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// pch.h +// Header for standard system include files. +// + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#define UNIT_TESTS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// C++\WinRT Headers +#include "winrt\base.h" +#include "winrt\Windows.Foundation.Diagnostics.h" +#include "winrt\Windows.Globalization.h" +#include "winrt\Windows.Globalization.DateTimeFormatting.h" +#include "winrt\Windows.System.UserProfile.h" + +namespace CalculatorApp +{ + namespace WF = Windows::Foundation; + namespace WUC = Windows::UI::Core; + namespace WX = Windows::UI::Xaml; + namespace WXC = Windows::UI::Xaml::Controls; + namespace WXCP = Windows::UI::Xaml::Controls::Primitives; + namespace P = Platform; + namespace PC = Platform::Collections; + namespace WXI = Windows::UI::Xaml::Input; + namespace WFC = Windows::Foundation::Collections; + namespace WS = Windows::System; + namespace WAR = Windows::ApplicationModel::Resources; + namespace WXMA = Windows::UI::Xaml::Media::Animation; + namespace WXD = Windows::UI::Xaml::Data; + namespace WXInt = Windows::UI::Xaml::Interop; + namespace WXM = Windows::UI::Xaml::Markup; + namespace WXA = Windows::UI::Xaml::Automation; +} + +// The following namespaces exist as a convenience to resolve +// ambiguity for Windows types in the Windows::UI::Xaml::Automation::Peers +// namespace that only exist on RS3. +// Once the app switches to min version RS3, the namespaces can be removed. +// TODO - MSFT 12735088 +namespace StandardPeers = Windows::UI::Xaml::Automation::Peers; +namespace CalculatorApp::Common::Automation {} +namespace CustomPeers = CalculatorApp::Common::Automation; + +//CalcManager Headers +#include "CalcManager\CalculatorVector.h" +#include "CalcManager\ExpressionCommand.h" +#include "CalcManager\CalculatorResource.h" +#include "CalcManager\CalculatorManager.h" +#include "CalcManager\UnitConverter.h" + +// CalcViewModel Headers +#include "CalcViewModel\Common\DelegateCommand.h" +#include "CalcViewModel\Common\Utils.h" +#include "CalcViewModel\Common\MyVirtualKey.h" +#include "CalcViewModel\Common\NavCategory.h" +#include "CalcViewModel\Common\CalculatorButtonUser.h" +#include "CalcViewModel\Common\NetworkManager.h" + +#include "Mocks\CurrencyHttpClient.h" +#include "Helpers.h" + +#include "UnitTestApp.xaml.h" + +#define TEST_METHOD_IGNORE();\ + BEGIN_TEST_METHOD_PROPERTIES()\ + TEST_METHOD_PROPERTY(L"Ignore", L"true")\ + END_TEST_METHOD_PROPERTIES() + +#define VERIFY_THROWS_WINRT(__operation, __exception, ...) \ +{ \ + bool __exceptionHit = false; \ + try \ + { \ + __operation; \ + } \ + catch(__exception __e) \ + { \ + WEX::TestExecution::Private::MacroVerify::ExpectedExceptionThrown(__e, L#__exception, L#__operation, __VA_ARGS__); \ + __exceptionHit = true; \ + } \ + \ + if (!__exceptionHit) \ + { \ + WEX::TestExecution::Private::MacroVerify::ExpectedExceptionNotThrown(L#__exception, L#__operation, PRIVATE_VERIFY_ERROR_INFO, __VA_ARGS__); \ + } \ +} + + diff --git a/internal/CalculatorUnitTests/testmd.definition b/internal/CalculatorUnitTests/testmd.definition new file mode 100644 index 00000000..d3ca47d5 --- /dev/null +++ b/internal/CalculatorUnitTests/testmd.definition @@ -0,0 +1,34 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-4.json", + "Package": { + "ComponentName": "Calculator", + "SubComponentName": "UnitTests" + }, + "SupportedArchitectures": [ "All" ], + "Execution": { + "Type": "TAEF", + "Parameter": "/APPX:CertificateFileName=CalculatorUnitTests.cer:TrustedPeople /screenCaptureOnError /TestMode:EnsureLoggedOnUser /TestMode:EtwLogger", + "ExecutionTimeoutInMinutes": "30" + }, + "Dependencies": { + "Files": [ + { + "SourcePath": "$(AppxPackagePublicKeyFile)", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + }, + { + "SourcePath": "$(AppxPackageVCLibsDependency)", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + } + ], + "Packages": [ + "Microsoft-Windows-Test-Taef", + "Microsoft-Windows-Test-EtwProcessor", + "Microsoft-Test-Taef-EnsureLoggedOnUserTestMode", + "Microsoft-Test-Taef-EtwLoggerTestMode" + ] + }, + "Logs": [], + "Plugins": [], + "Profiles": [] +} diff --git a/internal/Readme.txt b/internal/Readme.txt new file mode 100644 index 00000000..b5154325 --- /dev/null +++ b/internal/Readme.txt @@ -0,0 +1 @@ +This directory contains projects that have dependencies on internal Microsoft tools and that are not necessary to build Calculator. \ No newline at end of file diff --git a/internal/Settings.XamlStyler b/internal/Settings.XamlStyler new file mode 100644 index 00000000..6fc7c481 --- /dev/null +++ b/internal/Settings.XamlStyler @@ -0,0 +1,39 @@ +{ + "AttributesTolerance": 2, + "KeepFirstAttributeOnSameLine": true, + "MaxAttributeCharatersPerLine": 0, + "MaxAttributesPerLine": 1, + "NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransfom, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter", + "SeparateByGroups": false, + "EnableAttributeReordering": true, + "AttributeOrderingRuleGroups": [ + "x:Class*", + "xmlns, xmlns:x", + "xmlns:*", + "x:Key, Key, x:Name, Name, x:Uid, Uid, Title", + "Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom", + "Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight", + "Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex", + "Style, Background, Foreground, Fill, BorderBrush, BorderThickness, Stroke, StrokeThickness, Opacity", + "FontFamily, FontSize, LineHeight, FontWeight, FontStyle, FontStretch", + "*:*, *", + "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", + "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText" + ], + "OrderAttributesByName": true, + "PutEndingBracketOnNewLine": false, + "RemoveEndingTagOfEmptyElement": true, + "SpaceBeforeClosingSlash": false, + "RootElementLineBreakRule": 0, + "ReorderVSM": 1, + "ReorderGridChildren": false, + "ReorderCanvasChildren": false, + "ReorderSetters": 0, + "FormatMarkupExtension": true, + "NoNewLineMarkupExtensions": "x:Bind, Binding", + "ThicknessSeparator": 2, + "ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin", + "FormatOnSave": true, + "CommentPadding": 1, + "IndentSize": 4 +} \ No newline at end of file diff --git a/internal/nuget.config b/internal/nuget.config new file mode 100644 index 00000000..bd1364ee --- /dev/null +++ b/internal/nuget.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..0286336f --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/CalcManager/CEngine/CalcInput.cpp b/src/CalcManager/CEngine/CalcInput.cpp new file mode 100644 index 00000000..c4f9c9d8 --- /dev/null +++ b/src/CalcManager/CEngine/CalcInput.cpp @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace std; +using namespace CalcEngine; + +constexpr int C_NUM_MAX_DIGITS = MAX_STRLEN; +constexpr int C_EXP_MAX_DIGITS = 4; + +void CalcNumSec::Clear() +{ + value.clear(); + m_isNegative = false; +} + +void CalcInput::Clear() +{ + m_base.Clear(); + m_exponent.Clear(); + m_hasExponent = false; + m_hasDecimal = false; + m_decPtIndex = 0; +} + +bool CalcInput::TryToggleSign(bool isIntegerMode, wstring_view maxNumStr) +{ + // Zero is always positive + if (m_base.IsEmpty()) + { + m_base.IsNegative(false); + m_exponent.IsNegative(false); + } + else if (m_hasExponent) + { + m_exponent.IsNegative(!m_exponent.IsNegative()); + } + else + { + // When in integer only mode, it isnt always allowed to toggle, as toggling can cause the num to be out of + // bounds. For eg. in byte -128 is valid, but when it toggled it becomes 128, which is more than 127. + if (isIntegerMode && m_base.IsNegative()) + { + // Decide if this additional digit will fit for the given bit width + if (m_base.value.size() >= maxNumStr.size() && m_base.value.back() > maxNumStr.back()) + { + // Last digit is more than the allowed positive number. Fail + return false; + } + } + m_base.IsNegative(!m_base.IsNegative()); + } + + return true; +} + +bool CalcInput::TryAddDigit(unsigned int value, uint32_t radix, bool isIntegerMode, wstring_view maxNumStr, long wordBitWidth, int maxDigits) +{ + // Convert from an integer into a character + // This includes both normal digits and alpha 'digits' for radices > 10 + auto chDigit = static_cast((value < 10) ? (L'0' + value) : (L'A' + value - 10)); + + CalcNumSec* pNumSec; + size_t maxCount; + if (m_hasExponent) + { + pNumSec = &m_exponent; + maxCount = C_EXP_MAX_DIGITS; + } + else + { + pNumSec = &m_base; + maxCount = maxDigits; + // Don't include the decimal point in the count. In that way you can enter the maximum allowed precision. + // Precision doesnt include decimal point. + if (HasDecimalPt()) + { + maxCount++; + } + // First leading 0 is not counted in input restriction as the output can be of that form + // See NumberToString algorithm. REVIEW: We dont have such input restriction mimicking based on output of NumberToString for exponent + // NumberToString can give 10 digit exponent, but we still restrict the exponent here to be only 4 digits. + if (!pNumSec->IsEmpty() && pNumSec->value.front() == L'0') + { + maxCount++; + } + } + + // Ignore leading zeros + if (pNumSec->IsEmpty() && (value == 0)) + { + return true; + } + + if (pNumSec->value.size() < maxCount) + { + pNumSec->value += chDigit; + return true; + } + + // if we are in integer mode, within the base, and we're on the last digit then + // there are special cases where we can actually add one more digit. + if (isIntegerMode && pNumSec->value.size() == maxCount && !m_hasExponent) + { + bool allowExtraDigit = false; + + if (radix == 8) + { + switch (wordBitWidth % 3) + { + case 1: + // in 16 or 64bit word size, if the first digit is a 1 we can enter 6 (16bit) or 22 (64bit) digits + allowExtraDigit = (pNumSec->value.front() == L'1'); + break; + + case 2: + // in 8 or 32bit word size, if the first digit is a 3 or less we can enter 3 (8bit) or 11 (32bit) digits + allowExtraDigit = (pNumSec->value.front() <= L'3'); + break; + } + } + else if (radix == 10) + { + // If value length is at least the max, we know we can't add another digit. + if(pNumSec->value.size() < maxNumStr.size()) + { + // Compare value to substring of maxNumStr of value.size() length. + // If cmpResult > 0: + // eg. max is "127", and the current number is "20". first digit itself says we are out. + // Additional digit is not possible + + // If cmpResult < 0: + // Success case. eg. max is "127", and current number is say "11". The second digit '1' being < + // corresponding digit '2', means all digits are possible to append, like 119 will still be < 127 + + // If cmpResult == 0: + // Undecided still. The case when max is "127", and current number is "12". Look for the new number being 7 or less to allow + auto cmpResult = pNumSec->value.compare(0, wstring::npos, maxNumStr, 0, pNumSec->value.size()); + if (cmpResult < 0) + { + allowExtraDigit = true; + } + else if (cmpResult == 0) + { + auto lastChar = maxNumStr[pNumSec->value.size()]; + if (chDigit <= lastChar) + { + allowExtraDigit = true; + } + else if (pNumSec->IsNegative() && chDigit <= lastChar + 1) + { + // Negative value case, eg. max is "127", and current number is "-12". Then 8 is also valid, as the range + // is always from -(max+1)...max in signed mode + allowExtraDigit = true; + } + } + } + } + + if (allowExtraDigit) + { + pNumSec->value += chDigit; + return true; + } + } + + return false; +} + +bool CalcInput::TryAddDecimalPt() +{ + // Already have a decimal pt or we're in the exponent + if (m_hasDecimal || m_hasExponent) + { + return false; + } + + if (m_base.IsEmpty()) + { + m_base.value += L"0"; // Add a leading zero + } + + m_decPtIndex = m_base.value.size(); + m_base.value += m_decSymbol; + m_hasDecimal = true; + + return true; +} + +bool CalcInput::HasDecimalPt() +{ + return m_hasDecimal; +} + +bool CalcInput::TryBeginExponent() +{ + // For compatibility, add a trailing dec point to base num if it doesn't have one + TryAddDecimalPt(); + + if (m_hasExponent) // Already entering exponent + { + return false; + } + + m_hasExponent = true; // Entering exponent + return true; +} + +void CalcInput::Backspace() +{ + if (m_hasExponent) + { + if (!m_exponent.IsEmpty()) + { + m_exponent.value.pop_back(); + + if (m_exponent.IsEmpty()) + { + m_exponent.Clear(); + } + } + else + { + m_hasExponent = false; + } + } + else + { + if (!m_base.IsEmpty()) + { + m_base.value.pop_back(); + } + + if (m_base.value.size() <= m_decPtIndex) + { + // Backed up over decimal point + m_hasDecimal = false; + m_decPtIndex = 0; + } + + if (m_base.IsEmpty()) + { + m_base.Clear(); + } + } +} + +void CalcInput::SetDecimalSymbol(wchar_t decSymbol) +{ + if (m_decSymbol != decSymbol) + { + m_decSymbol = decSymbol; + + if (m_hasDecimal) + { + // Change to new decimal pt + m_base.value[m_decPtIndex] = m_decSymbol; + } + } +} + +wstring CalcInput::ToString(uint32_t radix, bool isIntegerMode) +{ + // In theory both the base and exponent could be C_NUM_MAX_DIGITS long. + wstringstream resStream; + + if ((m_base.value.size() > MAX_STRLEN) || (m_hasExponent && m_exponent.value.size() > MAX_STRLEN)) + { + return wstring(); + } + + if (m_base.IsNegative()) + { + resStream << L'-'; + } + + resStream << (m_base.IsEmpty() ? L"0" : m_base.value); + + if (m_hasExponent) + { + // Add a decimal point if it is not already there + if (!m_hasDecimal) + { + resStream << m_decSymbol; + } + + resStream << ((radix == 10) ? L'e' : L'^'); + resStream << (m_exponent.IsNegative() ? L'-' : L'+'); + resStream << (m_exponent.IsEmpty() ? L"0" : m_exponent.value); + } + + auto result = resStream.str(); + + // Base and Exp can each be up to C_NUM_MAX_DIGITS in length, plus 4 characters for sign, dec, exp, and expSign. + if (result.size() > C_NUM_MAX_DIGITS * 2 + 4) + { + return wstring(); + } + + return result; +} + +Rational CalcInput::ToRational(uint32_t radix, int32_t precision) +{ + PRAT rat = StringToRat(m_base.IsNegative(), m_base.value, m_exponent.IsNegative(), m_exponent.value, radix, precision); + if (rat == nullptr) + { + return Rational{}; + } + + Rational result{ rat }; + destroyrat(rat); + + return result; +} diff --git a/src/CalcManager/CEngine/CalcUtils.cpp b/src/CalcManager/CEngine/CalcUtils.cpp new file mode 100644 index 00000000..e398d8d9 --- /dev/null +++ b/src/CalcManager/CEngine/CalcUtils.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files/CalcEngine.h" + +bool IsOpInRange(WPARAM op, uint32_t x, uint32_t y) +{ + return ((op >= x) && (op <= y)); +} + +bool IsBinOpCode(WPARAM opCode) +{ + return IsOpInRange(opCode, IDC_AND, IDC_PWR); +} + +// WARNING: IDC_SIGN is a special unary op but still this doesnt catch this. Caller has to be aware +// of it and catch it themself or not needing this +bool IsUnaryOpCode(WPARAM opCode) +{ + return IsOpInRange(opCode, IDC_UNARYFIRST, IDC_UNARYLAST); +} + +bool IsDigitOpCode(WPARAM opCode) +{ + return IsOpInRange(opCode, IDC_0, IDC_F); +} + +// Some commands are not affecting the state machine state of the calc flow. But these are more of +// some gui mode kind of settings (eg Inv button, or Deg,Rad , Back etc.). This list is getting bigger & bigger +// so we abstract this as a separate routine. Note: There is another side to this. Some commands are not +// gui mode setting to begin with, but once it is discovered it is invalid and we want to behave as though it +// was never inout, we need to revert the state changes made as a result of this test +bool IsGuiSettingOpCode(WPARAM opCode) +{ + if (IsOpInRange(opCode, IDM_HEX, IDM_BIN) || + IsOpInRange(opCode, IDM_QWORD, IDM_BYTE) || + IsOpInRange(opCode, IDM_DEG, IDM_GRAD)) + { + return true; + } + + switch (opCode) + { + case IDC_INV: + case IDC_FE: + case IDC_MCLEAR: + case IDC_BACK: + case IDC_EXP: + case IDC_STORE: + case IDC_MPLUS: + case IDC_MMINUS: + return true; + } + + // most of the commands + return false; +} diff --git a/src/CalcManager/CEngine/History.cpp b/src/CalcManager/CEngine/History.cpp new file mode 100644 index 00000000..092b4d9a --- /dev/null +++ b/src/CalcManager/CEngine/History.cpp @@ -0,0 +1,490 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#pragma once +#include "Header Files/CalcEngine.h" +#include "Command.h" +#include "CalculatorVector.h" +#include "ExpressionCommand.h" +#include "CalcException.h" + +constexpr int ASCII_0 = 48; + +using namespace std; + +void CHistoryCollector::ReinitHistory() +{ + m_lastOpStartIndex = -1; + m_lastBinOpStartIndex = -1; + m_curOperandIndex = 0; + m_bLastOpndBrace = false; + if (m_spTokens != nullptr) + { + m_spTokens->Clear(); + } + if (m_spCommands != nullptr) + { + m_spCommands->Clear(); + } +} + +// Constructor +// Can throw Out of memory error +CHistoryCollector::CHistoryCollector(ICalcDisplay *pCalcDisplay, std::shared_ptr pHistoryDisplay, wchar_t decimalSymbol) : + m_pHistoryDisplay(pHistoryDisplay), + m_pCalcDisplay(pCalcDisplay), + m_decimalSymbol(decimalSymbol), + m_iCurLineHistStart(-1) +{ + ReinitHistory(); +} + +CHistoryCollector::~CHistoryCollector() +{ + m_pHistoryDisplay = nullptr; + m_pCalcDisplay = nullptr; + + if (m_spTokens != nullptr) + { + m_spTokens->Clear(); + } +} + +void CHistoryCollector::AddOpndToHistory(wstring_view numStr, PRAT hNoNum, bool fRepetition) +{ + std::shared_ptr> commands = std::make_shared>(); + // Check for negate + bool fNegative = (numStr[0] == L'-'); + bool fSciFmt = false; + bool fDecimal = false; + + for (size_t i = (fNegative ? 1 : 0); i < numStr.length(); i++) + { + if (numStr[i] == m_decimalSymbol) + { + IFT(commands->Append(IDC_PNT)); + if (!fSciFmt) + { + fDecimal = true; + } + } + else if (numStr[i] == L'e') + { + IFT(commands->Append(IDC_EXP)); + fSciFmt = true; + } + else if (numStr[i] == L'-') + { + IFT(commands->Append(IDC_SIGN)); + } + else if (numStr[i] == L'+') + { + // Ignore. + } + // Number + else + { + int num = static_cast(numStr[i]) - ASCII_0; + num += IDC_0; + IFT(commands->Append(num)); + } + } + + auto operandCommand = std::make_shared(commands, fNegative, fDecimal, fSciFmt); + operandCommand->Initialize(hNoNum); + int iCommandEnd = AddCommand(operandCommand); + m_lastOpStartIndex = IchAddSzToEquationSz(numStr, iCommandEnd); + + if (fRepetition) + { + SetExpressionDisplay(); + } + m_bLastOpndBrace = false; + m_lastBinOpStartIndex = -1; +} + +void CHistoryCollector::RemoveLastOpndFromHistory() +{ + TruncateEquationSzFromIch(m_lastOpStartIndex); + SetExpressionDisplay(); + m_lastOpStartIndex = -1; + // This will not restore the m_lastBinOpStartIndex, as it isnt possible to remove that also later +} + +void CHistoryCollector::AddBinOpToHistory(int nOpCode, bool fNoRepetition) +{ + int iCommandEnd = AddCommand(std::make_shared(nOpCode)); + m_lastBinOpStartIndex = IchAddSzToEquationSz(L" ", -1); + + IchAddSzToEquationSz(CCalcEngine::OpCodeToString(nOpCode), iCommandEnd); + IchAddSzToEquationSz(L" ", -1); + + if (fNoRepetition) + { + SetExpressionDisplay(); + } + m_lastOpStartIndex = -1; +} + +// This is expected to be called when a binary op in the last say 1+2+ is changing to another one say 1+2* (+ changed to *) +// It needs to know by this change a Precedence inversion happenned. i.e. previous op was lower or equal to its previous op, but the new +// one isn't. (Eg. 1*2* to 1*2^). It can add explicit brackets to ensure the precedence is inverted. (Eg. (1*2) ^) +void CHistoryCollector::ChangeLastBinOp(int nOpCode, bool fPrecInvToHigher) +{ + TruncateEquationSzFromIch(m_lastBinOpStartIndex); + if (fPrecInvToHigher) + { + EnclosePrecInvertionBrackets(); + } + AddBinOpToHistory(nOpCode); +} + +void CHistoryCollector::PushLastOpndStart(int ichOpndStart) +{ + int ich = (ichOpndStart == -1) ? m_lastOpStartIndex : ichOpndStart; + + if (m_curOperandIndex < static_cast(m_operandIndices.size())) + { + m_operandIndices[m_curOperandIndex++] = ich; + } +} + +void CHistoryCollector::PopLastOpndStart() +{ + if (m_curOperandIndex > 0) + { + m_lastOpStartIndex = m_operandIndices[--m_curOperandIndex]; + } +} + +void CHistoryCollector::AddOpenBraceToHistory() +{ + int iCommandEnd = AddCommand(std::make_shared(IDC_OPENP)); + int ichOpndStart = IchAddSzToEquationSz(CCalcEngine::OpCodeToString(IDC_OPENP), -1); + PushLastOpndStart(ichOpndStart); + + SetExpressionDisplay(); + m_lastBinOpStartIndex = -1; +} + +void CHistoryCollector::AddCloseBraceToHistory() +{ + int iCommandEnd = AddCommand(std::make_shared(IDC_CLOSEP)); + IchAddSzToEquationSz(CCalcEngine::OpCodeToString(IDC_CLOSEP), -1); + SetExpressionDisplay(); + PopLastOpndStart(); + + m_lastBinOpStartIndex = -1; + m_bLastOpndBrace = true; +} + +void CHistoryCollector::EnclosePrecInvertionBrackets() +{ + // Top of the Opnd starts index or 0 is nothing is in top + int ichStart = (m_curOperandIndex > 0) ? m_operandIndices[m_curOperandIndex - 1] : 0; + + InsertSzInEquationSz(CCalcEngine::OpCodeToString(IDC_OPENP), -1, ichStart); + IchAddSzToEquationSz(CCalcEngine::OpCodeToString(IDC_CLOSEP), -1); +} + +bool CHistoryCollector::FOpndAddedToHistory() +{ + return (-1 != m_lastOpStartIndex); +} + +// AddUnaryOpToHistory +// +// This is does the postfix to prefix transalation of the input and adds the text to the history. Eg. doing 2 + 4 (sqrt), +// this routine will ensure the last sqrt call unary operator, actually goes back in history and wraps 4 in sqrt(4) +// +void CHistoryCollector::AddUnaryOpToHistory(int nOpCode, bool fInv, ANGLE_TYPE angletype) +{ + int iCommandEnd; + // When successfully applying a unary op, there should be an opnd already + // A very special case of % which is a funny post op unary op. + if (IDC_PERCENT == nOpCode) + { + iCommandEnd = AddCommand(std::make_shared(nOpCode)); + IchAddSzToEquationSz(CCalcEngine::OpCodeToString(nOpCode), iCommandEnd); + } + else // all the other unary ops + { + std::shared_ptr spExpressionCommand; + if (IDC_SIGN == nOpCode) + { + spExpressionCommand = std::make_shared(nOpCode); + } + else + { + CalculationManager::Command angleOpCode; + if (angletype == ANGLE_DEG) + { + angleOpCode = CalculationManager::Command::CommandDEG; + } + if (angletype == ANGLE_RAD) + { + angleOpCode = CalculationManager::Command::CommandRAD; + } + if (angletype == ANGLE_GRAD) + { + angleOpCode = CalculationManager::Command::CommandGRAD; + } + + int command = nOpCode; + switch (nOpCode) + { + case IDC_SIN: + command = fInv ? static_cast(CalculationManager::Command::CommandASIN) : IDC_SIN; + spExpressionCommand = std::make_shared(static_cast(angleOpCode), command); + break; + case IDC_COS: + command = fInv ? static_cast(CalculationManager::Command::CommandACOS) : IDC_COS; + spExpressionCommand = std::make_shared(static_cast(angleOpCode), command); + break; + case IDC_TAN: + command = fInv ? static_cast(CalculationManager::Command::CommandATAN) : IDC_TAN; + spExpressionCommand = std::make_shared(static_cast(angleOpCode), command); + break; + case IDC_SINH: + command = fInv ? static_cast(CalculationManager::Command::CommandASINH) : IDC_SINH; + spExpressionCommand = std::make_shared(command); + break; + case IDC_COSH: + command = fInv ? static_cast(CalculationManager::Command::CommandACOSH) : IDC_COSH; + spExpressionCommand = std::make_shared(command); + break; + case IDC_TANH: + command = fInv ? static_cast(CalculationManager::Command::CommandATANH) : IDC_TANH; + spExpressionCommand = std::make_shared(command); + break; + case IDC_LN: + command = fInv ? static_cast(CalculationManager::Command::CommandPOWE) : IDC_LN; + spExpressionCommand = std::make_shared(command); + break; + default: + spExpressionCommand = std::make_shared(nOpCode); + } + } + + iCommandEnd = AddCommand(spExpressionCommand); + + wstring operandStr{ CCalcEngine::OpCodeToUnaryString(nOpCode, fInv, angletype) }; + if (!m_bLastOpndBrace) // The opnd is already covered in braces. No need for additional braces around it + { + operandStr.append(CCalcEngine::OpCodeToString(IDC_OPENP)); + } + InsertSzInEquationSz(operandStr, iCommandEnd, m_lastOpStartIndex); + + if (!m_bLastOpndBrace) + { + IchAddSzToEquationSz(CCalcEngine::OpCodeToString(IDC_CLOSEP), -1); + } + } + + SetExpressionDisplay(); + m_bLastOpndBrace = false; + // m_lastOpStartIndex remains the same as last opnd is just replaced by unaryop(lastopnd) + m_lastBinOpStartIndex = -1; +} + +// Called after = with the result of the equation +// Responsible for clearing the top line of current running history display, as well as adding yet another element to +// history of equations +void CHistoryCollector::CompleteHistoryLine(wstring_view numStr) +{ + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetExpressionDisplay(std::make_shared>>(), std::make_shared>>()); + } + + if (nullptr != m_pHistoryDisplay) + { + unsigned int addedItemIndex = m_pHistoryDisplay->AddToHistory(m_spTokens, m_spCommands, numStr); + m_pCalcDisplay->OnHistoryItemAdded(addedItemIndex); + } + + m_spTokens = nullptr; + m_spCommands = nullptr; + m_iCurLineHistStart = -1; // It will get recomputed at the first Opnd + ReinitHistory(); +} + +void CHistoryCollector::ClearHistoryLine(wstring_view errStr) +{ + if (errStr.empty()) // in case of error let the display stay as it is + { + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetExpressionDisplay(std::make_shared>>(), std::make_shared>>()); + } + m_iCurLineHistStart = -1; // It will get recomputed at the first Opnd + ReinitHistory(); + } +} + + +// Adds the given string psz to the globally maintained current equation string at the end. +// Also returns the 0 based index in the string just added. Can throw out of memory error +int CHistoryCollector::IchAddSzToEquationSz(wstring_view str, int icommandIndex) +{ + if (m_spTokens == nullptr) + { + m_spTokens = std::make_shared>>(); + } + + if (FAILED(m_spTokens->Append(std::make_pair(wstring(str), icommandIndex)))) + { + throw(CALC_E_OUTOFMEMORY); + } + + unsigned int nTokens; + m_spTokens->GetSize(&nTokens); + return nTokens - 1; +} + +// Inserts a given string into the global m_pszEquation at the given index ich taking care of reallocations etc. +void CHistoryCollector::InsertSzInEquationSz(wstring_view str, int icommandIndex, int ich) +{ + if (FAILED(m_spTokens->InsertAt(ich, std::make_pair(wstring(str), icommandIndex)))) + { + throw(CALC_E_OUTOFMEMORY); + } +} + +// Chops off the current equation string from the given index +void CHistoryCollector::TruncateEquationSzFromIch(int ich) +{ + // Truncate commands + int minIdx = -1; + unsigned int nTokens = 0; + std::pair currentPair; + m_spTokens->GetSize(&nTokens); + + for (unsigned int i = ich; i < nTokens; i++) + { + IFT(m_spTokens->GetAt(i, ¤tPair)); + int curTokenId = currentPair.second; + if (curTokenId != -1) + { + if ((minIdx != -1) || (curTokenId < minIdx)) + { + minIdx = curTokenId; + IFT(m_spCommands->Truncate(minIdx)); + } + } + } + + IFT(m_spTokens->Truncate(ich)); +} + +// Adds the m_pszEquation into the running history text +void CHistoryCollector::SetExpressionDisplay() +{ + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetExpressionDisplay(m_spTokens, m_spCommands); + } + +} + +int CHistoryCollector::AddCommand(_In_ const std::shared_ptr & spCommand) +{ + if (m_spCommands == nullptr) + { + m_spCommands = std::make_shared >>(); + } + + if (FAILED(m_spCommands->Append(spCommand))) + { + throw(CALC_E_OUTOFMEMORY); + } + + unsigned int nCommmands = 0; + m_spCommands->GetSize(&nCommmands); + return nCommmands - 1; +} + +//To Update the operands in the Expression according to the current Radix +void CHistoryCollector::UpdateHistoryExpression(uint32_t radix, int32_t precision) +{ + if (m_spTokens != nullptr) + { + unsigned int size; + IFT(m_spTokens->GetSize(&size)); + + for (unsigned int i = 0; i < size; ++i) + { + std::pair token; + IFT(m_spTokens->GetAt(i, &token)); + int commandPosition = token.second; + if (commandPosition != -1) + { + std::shared_ptr expCommand; + IFT(m_spCommands->GetAt(commandPosition, &expCommand)); + if (expCommand != nullptr && CalculationManager::CommandType::OperandCommand == expCommand->GetCommandType()) + { + std::shared_ptr opndCommand = std::static_pointer_cast(expCommand); + if (opndCommand != nullptr) + { + token.first = opndCommand->GetString(radix, precision, m_decimalSymbol); + IFT(m_spTokens->SetAt(i, token)); + opndCommand->SetCommands(GetOperandCommandsFromString(token.first)); + } + } + } + } + SetExpressionDisplay(); + } +} + +void CHistoryCollector::SetDecimalSymbol(wchar_t decimalSymbol) +{ + m_decimalSymbol = decimalSymbol; +} + +//Update the commands corresponding to the passed string Number +std::shared_ptr> CHistoryCollector::GetOperandCommandsFromString(wstring_view numStr) +{ + std::shared_ptr> commands = std::make_shared>(); + // Check for negate + bool fNegative = (numStr[0] == L'-'); + bool fSciFmt = false; + bool fDecimal = false; + + for (size_t i = (fNegative ? 1 : 0); i < numStr.length(); i++) + { + if (numStr[i] == m_decimalSymbol) + { + IFT(commands->Append(IDC_PNT)); + fDecimal = true; + } + else if (numStr[i] == L'e') + { + IFT(commands->Append(IDC_EXP)); + fSciFmt = true; + } + else if (numStr[i] == L'-') + { + IFT(commands->Append(IDC_SIGN)); + } + else if (numStr[i] == L'+') + { + // Ignore. + } + // Number + else + { + int num = static_cast(numStr[i]) - ASCII_0; + num += IDC_0; + IFT(commands->Append(num)); + } + } + + // If the number is negative, append a sign command at the end. + if (fNegative) + { + IFT(commands->Append(IDC_SIGN)); + } + return commands; +} diff --git a/src/CalcManager/CEngine/Number.cpp b/src/CalcManager/CEngine/Number.cpp new file mode 100644 index 00000000..0d809917 --- /dev/null +++ b/src/CalcManager/CEngine/Number.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#include "pch.h" +#include "Header Files/Number.h" + +using namespace std; + +namespace CalcEngine +{ + Number::Number() noexcept : + Number(1, 0, { 0 }) + {} + + Number::Number(int32_t sign, int32_t exp, vector const& mantissa) noexcept : + m_sign{ sign }, + m_exp{ exp }, + m_mantissa{ mantissa } + {} + + Number::Number(PNUMBER p) noexcept : + m_sign{ p->sign }, + m_exp{ p->exp }, + m_mantissa{} + { + m_mantissa.reserve(p->cdigit); + copy(p->mant, p->mant + p->cdigit, back_inserter(m_mantissa)); + } + + PNUMBER Number::ToPNUMBER() const + { + PNUMBER ret = _createnum(static_cast(this->Mantissa().size()) + 1); + ret->sign = this->Sign(); + ret->exp = this->Exp(); + ret->cdigit = static_cast(this->Mantissa().size()); + + MANTTYPE *ptrRet = ret->mant; + for (auto const& digit : this->Mantissa()) + { + *ptrRet++ = digit; + } + + return ret; + } + + int32_t const& Number::Sign() const + { + return m_sign; + } + + int32_t const& Number::Exp() const + { + return m_exp; + } + + vector const& Number::Mantissa() const + { + return m_mantissa; + } + + bool Number::IsZero() const + { + return all_of(m_mantissa.begin(), m_mantissa.end(), [](auto &&i) { return i == 0; }); + } +} diff --git a/src/CalcManager/CEngine/Rational.cpp b/src/CalcManager/CEngine/Rational.cpp new file mode 100644 index 00000000..eceace43 --- /dev/null +++ b/src/CalcManager/CEngine/Rational.cpp @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#include "pch.h" +#include "Header Files/Rational.h" + +using namespace std; + +namespace CalcEngine +{ + Rational::Rational() noexcept : + m_p{}, + m_q{ 1, 0, { 1 } } + {} + + Rational::Rational(Number const& n) noexcept + { + int32_t qExp = 0; + if (n.Exp() < 0) + { + qExp -= n.Exp(); + } + + m_p = Number(n.Sign(), 0, n.Mantissa()); + m_q = Number(1, qExp, { 1 }); + } + + Rational::Rational(Number const& p, Number const& q) noexcept : + m_p{ p }, + m_q{ q } + {} + + Rational::Rational(PRAT prat) noexcept : + m_p{ Number{prat->pp} }, + m_q{ Number{prat->pq} } + {} + + PRAT Rational::ToPRAT() const + { + PRAT ret = _createrat(); + + ret->pp = this->P().ToPNUMBER(); + ret->pq = this->Q().ToPNUMBER(); + + return ret; + } + + Number const& Rational::P() const + { + return m_p; + } + + Number const& Rational::Q() const + { + return m_q; + } + + bool Rational::IsZero() const + { + return this->P().IsZero(); + } +} diff --git a/src/CalcManager/CEngine/calc.cpp b/src/CalcManager/CEngine/calc.cpp new file mode 100644 index 00000000..d35a74ec --- /dev/null +++ b/src/CalcManager/CEngine/calc.cpp @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/**************************************************************************\ + *** SCICALC Scientific Calculator for Windows 3.00.12 + *** (c)1989 Microsoft Corporation. All Rights Reserved. + *** + *** scimain.c + *** + *** Definitions of all globals, WinMain procedure + *** + *** Last modification + *** Fri 22-Nov-1996 + *** + *** 22-Nov-1996 + *** Converted Calc from floating point to infinite precision. + *** The new math engine is in ..\ratpak + *** + *** + *** 05-Jan-1990 + *** Calc did not have a floating point exception signal handler. This + *** would cause CALC to be forced to exit on a FP exception as that's + *** the default. + *** The signal handler is defined in SCIFUNC.C, in WinMain we hook the + *** the signal. +\**************************************************************************/ +#include "pch.h" +#include "Header Files/CalcEngine.h" + +#include "CalculatorResource.h" + +using namespace std; +using namespace CalcEngine; + +/**************************************************************************/ +/*** Global variable declarations and initializations ***/ +/**************************************************************************/ + +static constexpr int DEFAULT_MAX_DIGITS = 32; +static constexpr int DEFAULT_PRECISION = 32; +static constexpr long DEFAULT_RADIX = 10; + +static constexpr wchar_t DEFAULT_DEC_SEPARATOR = L'.'; +static constexpr wchar_t DEFAULT_GRP_SEPARATOR = L','; +static constexpr wstring_view DEFAULT_GRP_STR = L"3;0"; +static constexpr wstring_view DEFAULT_NUMBER_STR = L"0"; + +// Read strings for keys, errors, trig types, etc. +// These will be copied from the resources to local memory. A larger +// than needed block is allocated first and then reallocated once we +// know how much is actually used. + +array CCalcEngine::s_engineStrings; + +void CCalcEngine::LoadEngineStrings(CalculationManager::IResourceProvider& resourceProvider) +{ + for (size_t i = 0; i < s_engineStrings.size(); i++) + { + s_engineStrings[i] = resourceProvider.GetCEngineString(g_sids[i]); + } +} + +////////////////////////////////////////////////// +// +// InitialOneTimeOnlyNumberSetup +// +////////////////////////////////////////////////// +void CCalcEngine::InitialOneTimeOnlySetup(CalculationManager::IResourceProvider& resourceProvider) +{ + LoadEngineStrings(resourceProvider); + + // we must now setup all the ratpak constants and our arrayed pointers + // to these constants. + ChangeBaseConstants(DEFAULT_RADIX, DEFAULT_MAX_DIGITS, DEFAULT_PRECISION); +} + +////////////////////////////////////////////////// +// +// CCalcEngine::CCalcEngine +// +////////////////////////////////////////////////// +CCalcEngine::CCalcEngine(bool fPrecedence, bool fIntegerMode, CalculationManager::IResourceProvider* const pResourceProvider, __in_opt ICalcDisplay *pCalcDisplay, __in_opt shared_ptr pHistoryDisplay) : + m_HistoryCollector(pCalcDisplay, pHistoryDisplay, DEFAULT_DEC_SEPARATOR), + m_resourceProvider(pResourceProvider), + m_bSetCalcState(false), + m_fPrecedence(fPrecedence), + m_fIntegerMode(fIntegerMode), + m_pCalcDisplay(pCalcDisplay), + m_input(DEFAULT_DEC_SEPARATOR), + m_nOpCode(0), + m_nPrevOpCode(0), + m_openParenCount(0), + m_nPrecNum(0), + m_nTempCom(0), + m_nLastCom(0), + m_parenVals{}, + m_precedenceVals{}, + m_bChangeOp(false), + m_bRecord(false), + m_bError(false), + m_bInv(false), + m_nFE(FMT_FLOAT), + m_bNoPrevEqu(true), + m_numwidth(QWORD_WIDTH), + m_angletype(ANGLE_DEG), + m_radix(DEFAULT_RADIX), + m_precision(DEFAULT_PRECISION), + m_cIntDigitsSav(DEFAULT_MAX_DIGITS), + m_decGrouping(), + m_groupSeparator(DEFAULT_GRP_SEPARATOR), + m_numberString(DEFAULT_NUMBER_STR), + m_nOp(), + m_nPrecOp(), + m_memoryValue{make_unique()}, + m_holdVal{}, + m_currentVal{}, + m_lastVal{} +{ + InitChopNumbers(); + + m_dwWordBitWidth = DwWordBitWidthFromeNumWidth(m_numwidth); + + PRAT maxTrig = longtorat(10L); + PRAT hundred = longtorat(100L); + powrat(&maxTrig, hundred, m_radix, m_precision); + m_maxTrigonometricNum = Rational{ maxTrig }; + destroyrat(maxTrig); + destroyrat(hundred); + + SetRadixTypeAndNumWidth(DEC_RADIX, m_numwidth); + SettingsChanged(); + DisplayNum(); +} + +void CCalcEngine::InitChopNumbers() +{ + // these rat numbers are set only once and then never change regardless of + // base or precision changes + assert(m_chopNumbers.size() >= 4); + m_chopNumbers[0] = Rational{ rat_qword }; + m_chopNumbers[1] = Rational{ rat_dword }; + m_chopNumbers[2] = Rational{ rat_word }; + m_chopNumbers[3] = Rational{ rat_byte }; + + // initialize the max dec number you can support for each of the supported bit length + // this is basically max num in that width / 2 in integer + assert(m_chopNumbers.size() == m_maxDecimalValueStrings.size()); + for (size_t i = 0; i < m_chopNumbers.size(); i++) + { + PRAT hno = m_chopNumbers[i].ToPRAT(); + + divrat(&hno, rat_two, m_precision); + intrat(&hno, m_radix, m_precision); + + m_maxDecimalValueStrings[i] = NumObjToString(hno, 10, FMT_FLOAT, m_precision); + + NumObjDestroy(&hno); + } +} + +// Gets the number in memory for UI to keep it persisted and set it again to a different instance +// of CCalcEngine. Otherwise it will get destructed with the CalcEngine +unique_ptr CCalcEngine::PersistedMemObject() +{ + return move(m_memoryValue); +} + +void CCalcEngine::PersistedMemObject(Rational const& memObject) +{ + m_memoryValue = make_unique(memObject); +} + +void CCalcEngine::SettingsChanged() +{ + wchar_t lastDec = m_decimalSeparator; + wstring decStr = m_resourceProvider->GetCEngineString(L"sDecimal"); + m_decimalSeparator = decStr.empty() ? DEFAULT_DEC_SEPARATOR : decStr.at(0); + // Until it can be removed, continue to set ratpak decimal here + SetDecimalSeparator(m_decimalSeparator); + + wchar_t lastSep = m_groupSeparator; + wstring sepStr = m_resourceProvider->GetCEngineString(L"sThousand"); + m_groupSeparator = sepStr.empty() ? DEFAULT_GRP_SEPARATOR : sepStr.at(0); + + auto lastDecGrouping = m_decGrouping; + wstring grpStr = m_resourceProvider->GetCEngineString(L"sGrouping"); + m_decGrouping = DigitGroupingStringToGroupingVector(grpStr.empty() ? DEFAULT_GRP_STR : grpStr); + + bool numChanged = false; + + // if the grouping pattern or thousands symbol changed we need to refresh the display + if (m_decGrouping != lastDecGrouping || m_groupSeparator != lastSep) + { + numChanged = true; + } + + // if the decimal symbol has changed we always do the following things + if (m_decimalSeparator != lastDec) + { + // Re-initialize member variables' decimal point. + m_input.SetDecimalSymbol(m_decimalSeparator); + m_HistoryCollector.SetDecimalSymbol(m_decimalSeparator); + + // put the new decimal symbol into the table used to draw the decimal key + s_engineStrings[IDS_DECIMAL] = m_decimalSeparator; + + // we need to redraw to update the decimal point button + numChanged = true; + } + + if (numChanged) + { + DisplayNum(); + } +} + +wchar_t CCalcEngine::DecimalSeparator() const +{ + return m_decimalSeparator; +} diff --git a/src/CalcManager/CEngine/scicomm.cpp b/src/CalcManager/CEngine/scicomm.cpp new file mode 100644 index 00000000..354fb37d --- /dev/null +++ b/src/CalcManager/CEngine/scicomm.cpp @@ -0,0 +1,1107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/****************************Module*Header***********************************\ +* Module Name: SCICOMM.C +* +* Module Descripton: +* +* Warnings: +* +* Created: +* +* Author: +\****************************************************************************/ +#include "pch.h" +#include "Header Files/CalcEngine.h" +#include "Header Files/CalcUtils.h" + +#define IDC_RADSIN IDC_UNARYLAST+1 +#define IDC_RADCOS IDC_UNARYLAST+2 +#define IDC_RADTAN IDC_UNARYLAST+3 +#define IDC_GRADSIN IDC_UNARYLAST+4 +#define IDC_GRADCOS IDC_UNARYLAST+5 +#define IDC_GRADTAN IDC_UNARYLAST+6 + +using namespace std; +using namespace CalcEngine; + +// NPrecedenceOfOp +// +// returns a virtual number for precendence for the operator. We expect binary operator only, otherwise the lowest number +// 0 is returned. Higher the number, higher the precendence of the operator. +INT NPrecedenceOfOp(int nopCode) +{ + static BYTE rgbPrec[]={ 0,0, IDC_OR,0, IDC_XOR,0, IDC_AND,1, + IDC_ADD,2, IDC_SUB,2, IDC_RSHF,3, IDC_LSHF,3, + IDC_MOD,3, IDC_DIV,3, IDC_MUL,3, IDC_PWR,4, IDC_ROOT, 4}; + int iPrec; + + iPrec = 0; + while ((iPrec < ARRAYSIZE(rgbPrec)) && (nopCode != rgbPrec[iPrec])) + { + iPrec += 2; + } + if (iPrec >= ARRAYSIZE(rgbPrec)) + { + iPrec = 0; + } + return rgbPrec[iPrec+1]; + +} + +// HandleErrorCommand +// +// When it is discovered by the state machine that at this point the input is not valid (eg. "1+)"), we want to proceed as though this input never +// occured and may be some feedback to user like Beep. The rest of input can then continue by just ignoring this command. +void CCalcEngine::HandleErrorCommand(WPARAM idc) +{ + if (!IsGuiSettingOpCode(idc)) + { + // we would have saved the prev command. Need to unremember this state + m_nTempCom = m_nLastCom; + } +} + +void CCalcEngine::HandleMaxDigitsReached() +{ + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->MaxDigitsReached(); + } +} + +void CCalcEngine::ClearTemporaryValues() +{ + m_bInv = false; + m_input.Clear(); + m_bRecord = true; + CheckAndAddLastBinOpToHistory(); + DisplayNum (); + m_bError=false; +} + +void CCalcEngine::ProcessCommand(WPARAM wParam) +{ + if(wParam == IDC_SET_RESULT) + { + wParam = IDC_RECALL; + m_bSetCalcState = true; + } + + ProcessCommandWorker(wParam); +} + +void CCalcEngine::ProcessCommandWorker(WPARAM wParam) +{ + INT nx, ni; + + // Save the last command. Some commands are not saved in this manor, these + // commands are: + // Inv, Deg, Rad, Grad, Stat, FE, MClear, Back, and Exp. The excluded + // commands are not + // really mathematical operations, rather they are GUI mode settings. + + if (!IsGuiSettingOpCode(wParam)) + { + m_nLastCom=m_nTempCom; + m_nTempCom=(INT)wParam; + } + + if (m_bError) + { + if(wParam == IDC_CLEAR) + { + // handle "C" normally + } + else if (wParam == IDC_CENTR) + { + // treat "CE" as "C" + wParam = IDC_CLEAR; + } + else + { + HandleErrorCommand(wParam); + return; + } + } + + // Toggle Record/Display mode if appropriate. + if (m_bRecord) + { + if (IsOpInRange(wParam, IDC_AND, IDC_MMINUS) || + IsOpInRange(wParam, IDC_OPENP, IDC_CLOSEP) || + IsOpInRange(wParam, IDM_HEX, IDM_BIN) || + IsOpInRange(wParam, IDM_QWORD, IDM_BYTE) || + IsOpInRange(wParam, IDM_DEG, IDM_GRAD) || + IsOpInRange(wParam, IDC_BINEDITSTART, IDC_BINEDITSTART+63) || + (IDC_INV == wParam) || + (IDC_SIGN == wParam && 10 != m_radix)) + { + m_bRecord = false; + m_currentVal = m_input.ToRational(m_radix, m_precision); + DisplayNum(); // Causes 3.000 to shrink to 3. on first op. + } + } + else + { + if ( IsDigitOpCode(wParam)|| wParam == IDC_PNT) + { + m_bRecord = true; + m_input.Clear(); + CheckAndAddLastBinOpToHistory(); + } + } + + // Interpret digit keys. + if (IsDigitOpCode(wParam)) + { + unsigned int iValue = static_cast(wParam-IDC_0); + + // this is redundant, illegal keys are disabled + if (iValue >= static_cast(m_radix)) + { + HandleErrorCommand(wParam); + return; + } + + if (!m_input.TryAddDigit(iValue, m_radix, m_fIntegerMode, m_maxDecimalValueStrings[m_numwidth], m_dwWordBitWidth, m_cIntDigitsSav)) + { + HandleErrorCommand(wParam); + HandleMaxDigitsReached(); + return; + } + + DisplayNum(); + + return; + } + + // BINARY OPERATORS: + if (IsBinOpCode(wParam)) + { + /* Change the operation if last input was operation. */ + if (IsBinOpCode(m_nLastCom)) + { + INT nPrev; + bool fPrecInvToHigher = false; // Is Precedence Invertion from lower to higher precedence happenning ?? + + m_nOpCode =(INT)wParam; + + // Check to see if by changing this binop, a Precedence invertion is happenning. + // Eg. 1 * 2 + and + is getting changed to ^. The previous precedence rules would have already computed + // 1*2, so we will put additional brackets to cover for precedence invertion and it will become (1 * 2) ^ + // Here * is m_nPrevOpCode, m_currentVal is 2 (by 1*2), m_nLastCom is +, m_nOpCode is ^ + if (m_fPrecedence && 0 != m_nPrevOpCode) + { + nPrev = NPrecedenceOfOp(m_nPrevOpCode); + nx = NPrecedenceOfOp(m_nLastCom); + ni = NPrecedenceOfOp(m_nOpCode); + if (nx <= nPrev && ni > nPrev) // condition for Precedence Invertion + { + fPrecInvToHigher = true; + m_nPrevOpCode = 0; // Once the precedence invertion has put additional brackets, its no longer required + } + } + m_HistoryCollector.ChangeLastBinOp(m_nOpCode, fPrecInvToHigher); + DisplayAnnounceBinaryOperator(); + return; + } + + if (!m_HistoryCollector.FOpndAddedToHistory()) + { + // if the prev command was ) or unop then it is already in history as a opnd form (...) + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); + destroyrat(curRat); + } + + /* m_bChangeOp is true if there was an operation done and the */ + /* current m_currentVal is the result of that operation. This is so */ + /* entering 3+4+5= gives 7 after the first + and 12 after the */ + /* the =. The rest of this stuff attempts to do precedence in*/ + /* Scientific mode. */ + if (m_bChangeOp) + { +DoPrecedenceCheckAgain: + + nx = NPrecedenceOfOp((int) wParam); + ni = NPrecedenceOfOp(m_nOpCode); + + if ((nx > ni) && m_fPrecedence) + { + if (m_nPrecNum < MAXPRECDEPTH) + { + m_precedenceVals[m_nPrecNum] = m_lastVal; + + m_nPrecOp[m_nPrecNum] = m_nOpCode; + m_HistoryCollector.PushLastOpndStart(); // Eg. 1 + 2 *, Need to remember the start of 2 to do Precedence invertion if need to + } + else + { + m_nPrecNum = MAXPRECDEPTH-1; + HandleErrorCommand(wParam); + } + m_nPrecNum++; + } + else + { + /* do the last operation and then if the precedence array is not + * empty or the top is not the '(' demarcator then pop the top + * of the array and recheck precedence against the new operator + */ + m_currentVal = DoOperation(m_nOpCode, m_currentVal, m_lastVal); + m_nPrevOpCode = m_nOpCode; + + if (!m_bError) + { + DisplayNum (); + } + + if ((m_nPrecNum !=0) && (m_nPrecOp[m_nPrecNum-1])) + { + m_nPrecNum--; + m_nOpCode=m_nPrecOp[m_nPrecNum] ; + + m_lastVal = m_precedenceVals[m_nPrecNum]; + + nx = NPrecedenceOfOp(m_nOpCode); + // Precedence Invertion Higher to lower can happen which needs explicit enclosure of brackets + // Eg. 1 + 2 * Or 3 Or. We would have pushed 1+ before, and now last + forces 2 Or 3 to be evaluated + // because last Or is less or equal to first + (after 1). But we see that 1+ is in stack and we evaluated to 2 Or 3 + // This is precedence invertion happenned because of operator changed in between. We put extra brackets like + // 1 + (2 Or 3) + if (ni <= nx) + { + m_HistoryCollector.EnclosePrecInvertionBrackets(); + } + m_HistoryCollector.PopLastOpndStart(); + goto DoPrecedenceCheckAgain ; + } + + } + } + + DisplayAnnounceBinaryOperator(); + + m_lastVal = m_currentVal; + m_nOpCode=(INT)wParam; + m_HistoryCollector.AddBinOpToHistory(m_nOpCode); + m_bNoPrevEqu = m_bChangeOp = true; + return; + } + + // UNARY OPERATORS: + if (IsUnaryOpCode(wParam) || (wParam == IDC_DEGREES)) + { + /* Functions are unary operations. */ + /* If the last thing done was an operator, m_currentVal was cleared. */ + /* In that case we better use the number before the operator */ + /* was entered, otherwise, things like 5+ 1/x give Divide By */ + /* zero. This way 5+=gives 10 like most calculators do. */ + if (IsBinOpCode(m_nLastCom)) + { + m_currentVal = m_lastVal; + } + + // we do not add percent sign to history or to two line display. + // instead, we add the result of applying %. + if(wParam != IDC_PERCENT) + { + if (!m_HistoryCollector.FOpndAddedToHistory()) + { + + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); + destroyrat(curRat); + } + + m_HistoryCollector.AddUnaryOpToHistory((INT)wParam, m_bInv, m_angletype); + } + + if ((wParam == IDC_SIN) || (wParam == IDC_COS) || (wParam == IDC_TAN) || (wParam == IDC_SINH) || (wParam == IDC_COSH) || (wParam == IDC_TANH)) + { + if (IsCurrentTooBigForTrig()) + { + DisplayError(CALC_E_DOMAIN); + return; + } + } + + m_currentVal = SciCalcFunctions(m_currentVal, (DWORD)wParam); + + if (m_bError) + return; + + /* Display the result, reset flags, and reset indicators. */ + DisplayNum (); + + if(wParam == IDC_PERCENT) + { + CheckAndAddLastBinOpToHistory(); + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat, true); + destroyrat(curRat); + } + + /* reset the m_bInv flag and indicators if it is set + and have been used */ + + if (m_bInv && + ((wParam == IDC_CHOP) || + (wParam == IDC_SIN) || (wParam == IDC_COS) || (wParam == IDC_TAN) || + (wParam == IDC_LN) || (wParam == IDC_DMS) || (wParam == IDC_DEGREES) || + (wParam == IDC_SINH) || (wParam == IDC_COSH) || (wParam == IDC_TANH))) + { + m_bInv = false; + } + + return; + } + + // Tiny binary edit windows clicked. Toggle that bit and update display + if (IsOpInRange(wParam, IDC_BINEDITSTART, IDC_BINEDITSTART+63)) + { + // Same reasoning as for unary operators. We need to seed it previous number + if (m_nLastCom >= IDC_AND && m_nLastCom <= IDC_PWR) + { + m_currentVal = m_lastVal; + } + + CheckAndAddLastBinOpToHistory(); + + if (TryToggleBit(m_currentVal, (DWORD)wParam - IDC_BINEDITSTART)) + { + DisplayNum(); + } + + return; + } + + /* Now branch off to do other commands and functions. */ + switch(wParam) + { + case IDC_CLEAR: /* Total clear. */ + { + if (!m_bChangeOp) + { + // A special goody we are doing to preserve the history, if all was done was serious of unary operations last + CheckAndAddLastBinOpToHistory(false); + } + + m_lastVal = Rational{}; + + m_bChangeOp=false; + m_nPrecNum=m_nTempCom=m_nLastCom=m_nOpCode= m_openParenCount =0; + m_nPrevOpCode = 0; + m_bNoPrevEqu= true; + + + /* clear the parenthesis status box indicator, this will not be + cleared for CENTR */ + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetParenDisplayText(L""); + m_pCalcDisplay->SetExpressionDisplay(make_shared>>(), make_shared>>()); + } + + m_HistoryCollector.ClearHistoryLine(wstring()); + ClearTemporaryValues(); + } + break; + + case IDC_CENTR: /* Clear only temporary values. */ + { + // Clear the INV & leave (=xx indicator active + ClearTemporaryValues(); + } + + break; + + case IDC_BACK: + // Divide number by the current radix and truncate. + // Only allow backspace if we're recording. + if (m_bRecord) + { + m_input.Backspace(); + DisplayNum(); + } + else + { + HandleErrorCommand(wParam); + } + break; + + /* EQU enables the user to press it multiple times after and */ + /* operation to enable repeats of the last operation. */ + case IDC_EQU: + while (m_openParenCount > 0) + { + // when m_bError is set and m_ParNum is non-zero it goes into infinite loop + if (m_bError) + { + break; + } + // automatic closing of all the parenthesis to get a meaning ful result as well as ensure data integrity + m_nTempCom = m_nLastCom; // Put back this last saved command to the prev state so ) can be handled properly + ProcessCommand(IDC_CLOSEP); + m_nLastCom = m_nTempCom; // Actually this is IDC_CLOSEP + m_nTempCom = (INT)wParam; // put back in the state where last op seen was IDC_CLOSEP, and current op is IDC_EQU + } + + if (!m_bNoPrevEqu) + { + // It is possible now unary op changed the num in screen, but still m_lastVal hasnt changed. + m_lastVal = m_currentVal; + } + + /* Last thing keyed in was an operator. Lets do the op on*/ + /* a duplicate of the last entry. */ + if (IsBinOpCode(m_nLastCom)) + { + m_currentVal = m_lastVal; + } + + if (!m_HistoryCollector.FOpndAddedToHistory()) + { + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); + destroyrat(curRat); + } + + do { + + if (m_nOpCode) /* Is there a valid operation around? */ + { + /* If this is the first EQU in a string, set m_holdVal=m_currentVal */ + /* Otherwise let m_currentVal=m_holdVal. This keeps m_currentVal constant */ + /* through all EQUs in a row. */ + if (m_bNoPrevEqu) + { + m_holdVal = m_currentVal; + } + else + { + m_currentVal = m_holdVal; + DisplayNum(); // to update the m_numberString + m_HistoryCollector.AddBinOpToHistory(m_nOpCode, false); + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); // Adding the repeated last op to history + destroyrat(curRat); + } + + // Do the current or last operation. + m_currentVal = DoOperation(m_nOpCode, m_currentVal, m_lastVal); + m_nPrevOpCode = m_nOpCode; + m_lastVal = m_currentVal; + + /* Check for errors. If this wasn't done, DisplayNum */ + /* would immediately overwrite any error message. */ + if (!m_bError) + DisplayNum (); + + /* No longer the first EQU. */ + m_bNoPrevEqu= false; + } + else if (!m_bError) + DisplayNum(); + + if (m_nPrecNum==0 || !m_fPrecedence) + break; + + m_nOpCode = m_nPrecOp[--m_nPrecNum]; + m_lastVal = m_precedenceVals[m_nPrecNum]; + + // Precedence Invertion check + ni = NPrecedenceOfOp(m_nPrevOpCode); + nx = NPrecedenceOfOp(m_nOpCode); + if (ni <= nx) + { + m_HistoryCollector.EnclosePrecInvertionBrackets(); + } + m_HistoryCollector.PopLastOpndStart(); + + m_bNoPrevEqu= true; + } while (m_nPrecNum >= 0); + + if (!m_bError) + { + wstring groupedString = GroupDigitsPerRadix(m_numberString, m_radix); + m_HistoryCollector.CompleteHistoryLine(groupedString); + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetExpressionDisplay(make_shared>>(), make_shared>>()); + } + } + + m_bChangeOp=false; + m_nPrevOpCode = 0; + + break; + + case IDC_OPENP: + case IDC_CLOSEP: + nx=0; + if (wParam==IDC_OPENP) + { + nx=1; + } + + // -IF- the Paren holding array is full and we try to add a paren + // -OR- the paren holding array is empty and we try to remove a + // paren + // -OR- the the precidence holding array is full + if ((m_openParenCount >= MAXPRECDEPTH && nx) || (!m_openParenCount && !nx) + || ( (m_nPrecNum >= MAXPRECDEPTH && m_nPrecOp[m_nPrecNum-1]!=0) ) ) + { + HandleErrorCommand(wParam); + break; + } + + if (nx) + { + CheckAndAddLastBinOpToHistory(); + m_HistoryCollector.AddOpenBraceToHistory(); + + // Open level of parentheses, save number and operation. + m_parenVals[m_openParenCount] = m_lastVal; + + m_nOp[m_openParenCount++] = (m_bChangeOp ? m_nOpCode : 0); + + /* save a special marker on the precedence array */ + if (m_nPrecNum < m_nPrecOp.size()) + { + m_nPrecOp[m_nPrecNum++] = 0; + } + + m_lastVal = Rational{}; + if (IsBinOpCode(m_nLastCom)) + { + // We want 1 + ( to start as 1 + (0. Any number you type replaces 0. But if it is 1 + 3 (, it is + // treated as 1 + (3 + m_currentVal = Rational{}; + } + m_nTempCom=0; + m_nOpCode=0; + m_bChangeOp= false; // a ( is like starting a fresh sub equation + } + else + { + // Last thing keyed in was an operator. Lets do the op on a duplicate of the last entry. + if (IsBinOpCode(m_nLastCom)) + { + m_currentVal = m_lastVal; + } + + if (!m_HistoryCollector.FOpndAddedToHistory()) + { + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); + destroyrat(curRat); + } + + // Get the operation and number and return result. + m_currentVal = DoOperation(m_nOpCode, m_currentVal, m_lastVal); + m_nPrevOpCode = m_nOpCode; + + // Now process the precedence stack till we get to an opcode which is zero. + for (m_nOpCode = m_nPrecOp[--m_nPrecNum]; m_nOpCode; m_nOpCode = m_nPrecOp[--m_nPrecNum]) + { + // Precedence Inversion check + ni = NPrecedenceOfOp(m_nPrevOpCode); + nx = NPrecedenceOfOp(m_nOpCode); + if (ni <= nx) + { + m_HistoryCollector.EnclosePrecInvertionBrackets(); + } + m_HistoryCollector.PopLastOpndStart(); + + m_lastVal = m_precedenceVals[m_nPrecNum]; + + m_currentVal = DoOperation(m_nOpCode, m_currentVal, m_lastVal); + m_nPrevOpCode = m_nOpCode; + } + + m_HistoryCollector.AddCloseBraceToHistory(); + + // Now get back the operation and opcode at the begining of this parenthesis pair + + m_openParenCount -= 1; + m_lastVal = m_parenVals[m_openParenCount]; + m_nOpCode = m_nOp[m_openParenCount]; + + // m_bChangeOp should be true if m_nOpCode is valid + m_bChangeOp = (m_nOpCode != 0); + } + + // Set the "(=xx" indicator. + if (nullptr != m_pCalcDisplay) + { + m_pCalcDisplay->SetParenDisplayText(m_openParenCount ? to_wstring(m_openParenCount) : L""); + } + + if (!m_bError) + { + DisplayNum(); + } + + break; + + // BASE CHANGES: + case IDM_HEX: + case IDM_DEC: + case IDM_OCT: + case IDM_BIN: + { + SetRadixTypeAndNumWidth((RADIX_TYPE)(wParam - IDM_HEX), (NUM_WIDTH)-1); + m_HistoryCollector.UpdateHistoryExpression(m_radix, m_precision); + break; + } + + case IDM_QWORD: + case IDM_DWORD: + case IDM_WORD: + case IDM_BYTE: + if ( m_bRecord ) + { + m_currentVal = m_input.ToRational(m_radix, m_precision); + m_bRecord = false; + } + + // Compat. mode BaseX: Qword, Dword, Word, Byte + SetRadixTypeAndNumWidth((RADIX_TYPE)-1, (NUM_WIDTH)(wParam - IDM_QWORD)); + break; + + case IDM_DEG: + case IDM_RAD: + case IDM_GRAD: + m_angletype = static_cast(wParam - IDM_DEG); + break; + + case IDC_SIGN: + { + if (m_bRecord) + { + if (m_input.TryToggleSign(m_fIntegerMode, m_maxDecimalValueStrings[m_numwidth])) + { + DisplayNum(); + } + else + { + HandleErrorCommand(wParam); + } + break; + } + + // Doing +/- while in Record mode is not a unary operation + if (IsBinOpCode(m_nLastCom)) + { + m_currentVal = m_lastVal; + } + + if (!m_HistoryCollector.FOpndAddedToHistory()) + { + PRAT curRat = m_currentVal.ToPRAT(); + m_HistoryCollector.AddOpndToHistory(m_numberString, curRat); + destroyrat(curRat); + } + + PRAT curRat = m_currentVal.ToPRAT(); + NumObjNegate(&curRat); + m_currentVal = Rational{ curRat }; + destroyrat(curRat); + + DisplayNum(); + m_HistoryCollector.AddUnaryOpToHistory(IDC_SIGN, m_bInv, m_angletype); + } + break; + + case IDC_RECALL: + + if(m_bSetCalcState) + { + // Not a Memory recall. set the result + m_bSetCalcState = false; + } + else + { + // Recall immediate memory value. + m_currentVal = Rational{ *m_memoryValue }; + } + CheckAndAddLastBinOpToHistory(); + DisplayNum (); + break; + + case IDC_MPLUS: + { + /* MPLUS adds m_currentVal to immediate memory and kills the "mem" */ + /* indicator if the result is zero. */ + PRAT memRat = m_memoryValue->ToPRAT(); + PRAT curRat = m_currentVal.ToPRAT(); + addrat(&memRat, curRat, m_precision); + m_memoryValue = make_unique(TruncateNumForIntMath(Rational{ memRat })); // Memory should follow the current int mode + destroyrat(curRat); + destroyrat(memRat); + break; + } + case IDC_MMINUS: + { + /* MMINUS subtracts m_currentVal to immediate memory and kills the "mem" */ + /* indicator if the result is zero. */ + PRAT memRat = m_memoryValue->ToPRAT(); + PRAT curRat = m_currentVal.ToPRAT(); + subrat(&memRat, curRat, m_precision); + m_memoryValue = make_unique(TruncateNumForIntMath(Rational{ memRat })); + destroyrat(curRat); + destroyrat(memRat); + break; + } + case IDC_STORE: + case IDC_MCLEAR: + m_memoryValue = make_unique(wParam == IDC_STORE ? TruncateNumForIntMath(m_currentVal) : Rational{}); + break; + + case IDC_PI: + if (!m_fIntegerMode) + { + CheckAndAddLastBinOpToHistory(); // pi is like entering the number + m_currentVal = Rational{ (m_bInv ? two_pi : pi) }; + + DisplayNum(); + m_bInv = false; + break; + } + HandleErrorCommand(wParam); + break; + + case IDC_FE: + // Toggle exponential notation display. + m_nFE = NUMOBJ_FMT(!(int)m_nFE); + DisplayNum(); + break; + + case IDC_EXP: + if (m_bRecord && !m_fIntegerMode && m_input.TryBeginExponent()) + { + DisplayNum(); + break; + } + HandleErrorCommand(wParam); + break; + + case IDC_PNT: + if (m_bRecord && !m_fIntegerMode && m_input.TryAddDecimalPt()) + { + DisplayNum(); + break; + } + HandleErrorCommand(wParam); + break; + + case IDC_INV: + m_bInv=!m_bInv; + break; + } + +} + +// CheckAndAddLastBinOpToHistory +// +// This is a very confusing helper routine to add the last entered binary operator to the history. This is expected to +// leave the history with state. It can really add the last entered binary op, or it can actually remove +// the last operand from history. This happens because you can 'type' or 'compute' over last operand in some cases, thereby +// effectively removing only it from the equation but still keeping the previous portion of the equation. Eg. 1 + 4 sqrt 5. The last +// 5 will remove sqrt(4) as it is not used anymore to participate in 1 + 5 +// If you are messing with this, test cases like this CE, statistical functions, ( & MR buttons +void CCalcEngine::CheckAndAddLastBinOpToHistory(bool addToHistory) +{ + if (m_bChangeOp) + { + if (m_HistoryCollector.FOpndAddedToHistory()) + { + // if lasttime opnd was added but the last command was not a binary operator, then it must have come + // from commands which add the operand, like unary operator. So history at this is showing 1 + sqrt(4) + // but in reality the sqrt(4) is getting replaced by new number (may be unary op, or MR or SUM etc.) + // So erase the last operand + m_HistoryCollector.RemoveLastOpndFromHistory(); + } + + } + else if (m_HistoryCollector.FOpndAddedToHistory() && !m_bError) + { + // Corner case, where opnd is already in history but still a new opnd starting (1 + 4 sqrt 5). This is yet another + // special casing of previous case under if (m_bChangeOp), but this time we can do better than just removing it + // Let us make a current value =. So in case of 4 SQRT (or a equation under braces) and then a new equation is started, we can just form + // a useful equation of sqrt(4) = 2 and continue a new equation from now on. But no point in doing this for things like + // MR, SUM etc. All you will get is 5 = 5 kind of no useful equation. + if ((IsUnaryOpCode(m_nLastCom) || IDC_SIGN == m_nLastCom || IDC_CLOSEP == m_nLastCom) && + 0 == m_openParenCount) + { + if (addToHistory) + { + m_HistoryCollector.CompleteHistoryLine(GroupDigitsPerRadix(m_numberString, m_radix)); + } + } + else + { + m_HistoryCollector.RemoveLastOpndFromHistory(); + } + } +} + +// change the display area from a static text to an editbox, which has the focus can make +// Magnifer (Accessibility tool) work +void CCalcEngine::SetPrimaryDisplay(const wstring& szText, bool isError) +{ + if (m_pCalcDisplay != nullptr) + { + m_pCalcDisplay->SetPrimaryDisplay(szText, isError); + m_pCalcDisplay->SetIsInError(isError); + } +} + +void CCalcEngine::DisplayAnnounceBinaryOperator() +{ + // If m_pCalcDisplay is null, this is not a high priority function + // and should not be the reason we crash. + if (m_pCalcDisplay != nullptr) + { + m_pCalcDisplay->BinaryOperatorReceived(); + } +} + +// Unary operator Function Name table Element +// since unary operators button names are'nt exactly friendly for history purpose, +// we have this seperate table to get its localized name and for its Inv function if it exists. +typedef struct +{ + int idsFunc; // index of string for the unary op function. Can be NULL, in which case it same as button name + int idsFuncInv; // index of string for Inv of unary op. Can be NULL, in case it is same as idsFunc + bool fDontUseInExpEval; // true if this cant be used in reverse direction as well, ie. during expression evaluation +} UFNE; + +// Table for each unary operator +static const UFNE rgUfne[] = +{ + /* IDC_CHOP */{ 0, IDS_FRAC, false }, + /* IDC_ROL */{ 0, 0, true }, + /* IDC_ROR */{ 0, 0, true }, + + /* IDC_COM */{ 0, 0, true }, + /* IDC_SIN */{ IDS_SIND, IDS_ASIND, false }, // default in this table is degrees for sin,cos & tan + /* IDC_COS */{ IDS_COSD, IDS_ACOSD, false }, + /* IDC_TAN */{ IDS_TAND, IDS_ATAND, false }, + + /* IDC_SINH */{ 0, IDS_ASINH, false }, + /* IDC_COSH */{ 0, IDS_ACOSH, false }, + /* IDC_TANH */{ 0, IDS_ATANH, false }, + + /* IDC_LN */{ 0, IDS_POWE, false }, + /* IDC_LOG */{ 0, 0, false }, + /* IDC_SQRT */{ 0, 0, false }, + /* IDC_SQR */{ IDS_SQR, 0, false }, + /* IDC_CUB */{ IDS_CUBE, 0, false }, + /* IDC_FAC */{ IDS_FACT, 0, false }, + /* IDC_REC */{ IDS_REC, 0, false }, + /* IDC_DMS */{ 0, IDS_DEGREES, false }, + /* IDC_CUBEROOT */{ 0, 0, false }, + /* IDC_POW10 */{ 0, 0, false }, + /* IDC_PERCENT */{ 0, 0, false }, + + /* IDC_RADSIN */{ IDS_SINR, IDS_ASINR, false }, + /* IDC_RADCOS */{ IDS_COSR, IDS_ACOSR, false }, + /* IDC_RADTAN */{ IDS_TANR, IDS_ATANR, false }, + /* IDC_GRADCOS */{ IDS_SING, IDS_ASING, false }, + /* IDC_GRADCOS */{ IDS_COSG, IDS_ACOSG, false }, + /* IDC_GRADTAN */{ IDS_TANG, IDS_ATANG, false }, +}; + +wstring_view CCalcEngine::OpCodeToUnaryString(int nOpCode, bool fInv, ANGLE_TYPE angletype) +{ + // Special cases for Sign and Degrees + if (IDC_SIGN == nOpCode) + { + return GetString(IDS_NEGATE); + } + if (IDC_DEGREES == nOpCode) + { + return GetString(IDS_DEGREES); + } + + // Correct the trigometric functions with type of angle argument they take + if (ANGLE_RAD == angletype) + { + switch (nOpCode) + { + case IDC_SIN: + nOpCode = IDC_RADSIN; + break; + case IDC_COS: + nOpCode = IDC_RADCOS; + break; + case IDC_TAN: + nOpCode = IDC_RADTAN; + break; + } + } + else if (ANGLE_GRAD == angletype) + { + switch (nOpCode) + { + case IDC_SIN: + nOpCode = IDC_GRADSIN; + break; + case IDC_COS: + nOpCode = IDC_GRADCOS; + break; + case IDC_TAN: + nOpCode = IDC_GRADTAN; + break; + } + } + + // Try to lookup the ID in the UFNE table + int ids = 0; + int iufne = nOpCode - IDC_UNARYFIRST; + if (iufne >= 0 && iufne < ARRAYSIZE(rgUfne)) + { + if (fInv) + { + ids = rgUfne[iufne].idsFuncInv; + } + if (0 == ids) + { + ids = rgUfne[iufne].idsFunc; + } + } + + // If we didn't find an ID in the table, use the op code. + if (0 == ids) + { + ids = IdStrFromCmdId(nOpCode); + } + + return GetString(ids); +} + +// +// Sets the Angle Mode for special unary op IDC's which are used to index to the table rgUfne +// and returns the equivalent plain IDC for trignometric function. If it isnt a trignometric function +// returns the passed in idc itself. +int CCalcEngine::IdcSetAngleTypeDecMode(int idc) +{ + int idcAngleCmd = IDM_DEG; + + switch (idc) + { + case IDC_RADSIN: + idcAngleCmd = IDM_RAD; + idc = IDC_SIN; + break; + case IDC_RADCOS: + idcAngleCmd = IDM_RAD; + idc = IDC_COS; + break; + case IDC_RADTAN: + idcAngleCmd = IDM_RAD; + idc = IDC_TAN; + break; + case IDC_GRADSIN: + idcAngleCmd = IDM_GRAD; + idc = IDC_SIN; + break; + case IDC_GRADCOS: + idcAngleCmd = IDM_GRAD; + idc = IDC_COS; + break; + case IDC_GRADTAN: + idcAngleCmd = IDM_GRAD; + idc = IDC_TAN; + break; + } + ProcessCommand(idcAngleCmd); + return idc; + +} + +bool CCalcEngine::IsCurrentTooBigForTrig() +{ + bool result = false; + PRAT maxTrigRat = m_maxTrigonometricNum.ToPRAT(); + PRAT curRat = m_currentVal.ToPRAT(); + if (NumObjIsGreaterEq(curRat, maxTrigRat, m_precision)) + { + m_currentVal = Rational{}; + result = true; + } + destroyrat(curRat); + destroyrat(maxTrigRat); + + return result; +} + +int CCalcEngine::GetCurrentRadix() +{ + return m_radix; +} + +wstring CCalcEngine::GetCurrentResultForRadix(uint32_t radix, int32_t precision) +{ + Rational rat = (m_bRecord ? m_input.ToRational(m_radix, m_precision) : m_currentVal); + + ChangeConstants(m_radix, precision); + + wstring numberString = GetStringForDisplay(rat, radix); + if (!numberString.empty()) + { + //revert the precision to previously stored precision + ChangeConstants(m_radix, m_precision); + } + + return GroupDigitsPerRadix(numberString, radix); +} + +wstring CCalcEngine::GetStringForDisplay(Rational const& rat, uint32_t radix) +{ + wstring result{}; + // Check for standard\scientific mode + if (!m_fIntegerMode) + { + PRAT prat = rat.ToPRAT(); + result = NumObjToString(prat, radix, m_nFE, m_precision); + destroyrat(prat); + } + else + { + // Programmer mode + // Find most significant bit to determine if number is negative + PRAT hnoNumCopy = TruncateNumForIntMath(rat).ToPRAT(); + + try + { + ULONGLONG w64Bits = NumObjGetUlValue(hnoNumCopy, m_radix, m_precision); + bool fMsb = ((w64Bits >> (m_dwWordBitWidth - 1)) & 1); + if ((radix == 10) && fMsb) + { + // If high bit is set, then get the decimal number in -ve 2'scompl form. + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + NumObjNot(&hnoNumCopy, true, chopRat, m_radix, m_precision); + destroyrat(chopRat); + addrat(&hnoNumCopy, rat_one, m_precision); + NumObjNegate(&hnoNumCopy); + } + + result = NumObjToString(hnoNumCopy, radix, m_nFE, m_precision); + NumObjDestroy(&hnoNumCopy); + } + catch (DWORD) + { + if (hnoNumCopy != nullptr) + { + NumObjDestroy(&hnoNumCopy); + } + } + } + + return result; +} diff --git a/src/CalcManager/CEngine/scidisp.cpp b/src/CalcManager/CEngine/scidisp.cpp new file mode 100644 index 00000000..088ad75f --- /dev/null +++ b/src/CalcManager/CEngine/scidisp.cpp @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/****************************Module*Header***********************************\ +* Module Name: SCIDISP.C +* +* Module Descripton: +* +* Warnings: +* +* Created: +* +* Author: +\****************************************************************************/ +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace std; +using namespace CalcEngine; + +constexpr int MAX_EXPONENT = 4; +constexpr uint32_t MAX_GROUPING_SIZE = 16; +constexpr wstring_view c_decPreSepStr = L"[+-]?(\\d*)["; +constexpr wstring_view c_decPostSepStr = L"]?(\\d*)(?:e[+-]?(\\d*))?$"; + + +/****************************************************************************\ +* void DisplayNum(void) +* +* Convert m_currentVal to a string in the current radix. +* +* Updates the following variables: +* m_currentVal, m_numberString +\****************************************************************************/ +// +// State of calc last time DisplayNum was called +// +typedef struct { + PRAT hnoNum; + int32_t precision; + uint32_t radix; + INT nFE; + NUM_WIDTH numwidth; + bool fIntMath; + bool bRecord; + bool bUseSep; +} LASTDISP; + +LASTDISP gldPrevious = { nullptr, -1, 0, -1, (NUM_WIDTH)-1, false, false, false }; + +// Truncates if too big, makes it a non negative - the number in rat. Doesn't do anything if not in INT mode +CalcEngine::Rational CCalcEngine::TruncateNumForIntMath(CalcEngine::Rational const& rat) +{ + if (!m_fIntegerMode) + { + return rat; + } + + PRAT tempRat = rat.ToPRAT(); + + // Truncate to an integer. Do not round here. + intrat(&tempRat, m_radix, m_precision); + + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + // Can be converting a dec -ve number to Hex/Oct/Bin rep. Use 2's completement form + // Check the range. + if (NumObjIsLess(tempRat, rat_zero, m_precision)) + { + // if negative make positive by doing a twos complement + NumObjNegate(&tempRat); + subrat(&tempRat, rat_one, m_precision); + NumObjNot(&tempRat, true, chopRat, m_radix, m_precision); + } + + andrat(&tempRat, chopRat, m_radix, m_precision); + destroyrat(chopRat); + + Rational result{ tempRat }; + destroyrat(tempRat); + + return result; +} + +void CCalcEngine::DisplayNum(void) +{ + // + // Only change the display if + // we are in record mode -OR- + // this is the first time DisplayNum has been called, -OR- + // something important has changed since the last time DisplayNum was + // called. + // + PRAT curRat = m_currentVal.ToPRAT(); + bool hasValChanged = !gldPrevious.hnoNum || !NumObjIsEq(gldPrevious.hnoNum, curRat, m_precision); + destroyrat(curRat); + if ( m_bRecord || gldPrevious.hnoNum == nullptr || + hasValChanged || + gldPrevious.precision != m_precision || + gldPrevious.radix != m_radix || + gldPrevious.nFE != (int)m_nFE || + gldPrevious.bUseSep != true || + gldPrevious.numwidth != m_numwidth || + gldPrevious.fIntMath != m_fIntegerMode || + gldPrevious.bRecord != m_bRecord ) + { + gldPrevious.precision = m_precision; + gldPrevious.radix = m_radix; + gldPrevious.nFE = (int)m_nFE; + gldPrevious.numwidth = m_numwidth; + + gldPrevious.fIntMath = m_fIntegerMode; + gldPrevious.bRecord = m_bRecord; + gldPrevious.bUseSep = true; + + if (m_bRecord) + { + // Display the string and return. + m_numberString = m_input.ToString(m_radix, m_fIntegerMode); + } + else + { + // If we're in Programmer mode, perform integer truncation so e.g. 5 / 2 * 2 results in 4, not 5. + if (m_fIntegerMode) + { + m_currentVal = TruncateNumForIntMath(m_currentVal); + } + m_numberString = GetStringForDisplay(m_currentVal, m_radix); + } + + // Displayed number can go thru transformation. So copy it after transformation + destroyrat(gldPrevious.hnoNum); + gldPrevious.hnoNum = m_currentVal.ToPRAT(); + + if((m_radix == 10) && IsNumberInvalid(m_numberString, MAX_EXPONENT, m_precision, m_radix)) + { + DisplayError(CALC_E_OVERFLOW); + } + else + { + // Display the string and return. + SetPrimaryDisplay(GroupDigitsPerRadix(m_numberString, m_radix)); + } + } +} + +int CCalcEngine::IsNumberInvalid(const wstring& numberString, int iMaxExp, int iMaxMantissa, uint32_t radix) const +{ + int iError = 0; + + if (radix == 10) + { + // start with an optional + or - + // followed by zero or more digits + // followed by an optional decimal point + // followed by zero or more digits + // followed by an optional exponent + // in case there's an exponent: + // its optionally followed by a + or - + // which is followed by zero or more digits + wregex rx(wstring{ c_decPreSepStr } + m_decimalSeparator + wstring{ c_decPostSepStr }); + wsmatch matches; + if (regex_match(numberString, matches, rx)) + { + // Check that exponent isn't too long + if (matches.length(3) > iMaxExp) + { + iError = IDS_ERR_INPUT_OVERFLOW; + } + else + { + wstring exp = matches.str(1); + auto intItr = exp.begin(); + auto intEnd = exp.end(); + while (intItr != intEnd && *intItr == L'0') + { + intItr++; + } + + auto iMantissa = distance(intItr, intEnd) + matches.length(2); + if (iMantissa > iMaxMantissa) + { + iError = IDS_ERR_INPUT_OVERFLOW; + } + } + } + else + { + iError = IDS_ERR_UNK_CH; + } + } + else + { + for (const wchar_t& c : numberString) + { + if (radix == 16) + { + if (!(iswdigit(c) || (c >= L'A' && c <= L'F'))) + { + iError = IDS_ERR_UNK_CH; + } + } + else if (c < L'0' || c >= L'0' + radix) + { + iError = IDS_ERR_UNK_CH; + } + } + } + + return iError; +} + +/****************************************************************************\ +* +* DigitGroupingStringToGroupingVector +* +* Description: +* This will take the digit grouping string found in the regional applet and +* represent this string as a vector. +* +* groupingString +* 0;0 - no grouping +* 3;0 - group every 3 digits +* 3 - group 1st 3, then no grouping after +* 3;0;0 - group 1st 3, then no grouping after +* 3;2;0 - group 1st 3 and then every 2 digits +* 4;0 - group every 4 digits +* 5;3;2;0 - group 5, then 3, then every 2 +* 5;3;2 - group 5, then 3, then 2, then no grouping after +* +* Returns: the groupings as a vector +* +\****************************************************************************/ +vector CCalcEngine::DigitGroupingStringToGroupingVector(wstring_view groupingString) +{ + vector grouping{}; + uint32_t currentGroup = 0; + wchar_t* next = nullptr; + for (const wchar_t* itr = groupingString.data(); *itr != L'\0'; ++itr) + { + // Try to parse a grouping number from the string + currentGroup = wcstoul(itr, &next, 10); + + // If we successfully parsed a group, add it to the grouping. + if (currentGroup < MAX_GROUPING_SIZE) + { + grouping.emplace_back(currentGroup); + } + + // If we found a grouping and aren't at the end of the string yet, + // jump to the next position in the string (the ';'). + // The loop will then increment us to the next character, which should be a number. + if (next && (static_cast(next - groupingString.data()) < groupingString.length())) + { + itr = next; + } + } + + return grouping; +} + +wstring CCalcEngine::GroupDigitsPerRadix(wstring_view numberString, uint32_t radix) +{ + if (numberString.empty()) + { + return wstring{}; + } + + switch (radix) + { + case 10: + return GroupDigits(wstring{ m_groupSeparator }, m_decGrouping, numberString, (L'-' == numberString[0])); + case 8: + return GroupDigits(L" ", { 3, 0 }, numberString); + case 2: + case 16: + return GroupDigits(L" ", { 4, 0 }, numberString); + default: + return wstring{ numberString }; + } +} + +/****************************************************************************\ +* +* GroupDigits +* +* Description: +* This routine will take a grouping vector and the display string and +* add the separator according to the pattern indicated by the separator. +* +* Grouping +* 0,0 - no grouping +* 3,0 - group every 3 digits +* 3 - group 1st 3, then no grouping after +* 3,0,0 - group 1st 3, then no grouping after +* 3,2,0 - group 1st 3 and then every 2 digits +* 4,0 - group every 4 digits +* 5,3,2,0 - group 5, then 3, then every 2 +* 5,3,2 - group 5, then 3, then 2, then no grouping after +* +\***************************************************************************/ +wstring CCalcEngine::GroupDigits(wstring_view delimiter, vector const& grouping, wstring_view displayString, bool isNumNegative) +{ + // if there's nothing to do, bail + if (delimiter.empty() || grouping.empty()) + { + return wstring{ displayString }; + } + + // Find the position of exponential 'e' in the string + size_t exp = displayString.find(L'e'); + bool hasExponent = (exp != wstring_view::npos); + + // Find the position of decimal point in the string + size_t dec = displayString.find(m_decimalSeparator); + bool hasDecimal = (dec != wstring_view::npos); + + // Create an iterator that points to the end of the portion of the number subject to grouping (i.e. left of the decimal) + auto ritr = displayString.rend(); + if (hasDecimal) + { + ritr -= dec; + } + else if (hasExponent) + { + ritr -= exp; + } + else + { + ritr = displayString.rbegin(); + } + + wstringstream groupedStream{}; + uint32_t groupingSize = 0; + + auto groupItr = grouping.begin(); + auto currGrouping = *groupItr; + // Mark the 'end' of the string as either rend() or rend()-1 if there is a negative sign + // We exclude the sign here because we don't want to end up with e.g. "-,123,456" + // Then, iterate from back to front, adding group delimiters as needed. + auto reverse_end = displayString.rend() - (isNumNegative ? 1 : 0); + while (ritr != reverse_end) + { + groupedStream << *ritr++; + groupingSize++; + + // If a group is complete, add a separator + // Do not add a separator if: + // - grouping size is 0 + // - we are at the end of the digit string + if (currGrouping != 0 && (groupingSize % currGrouping) == 0 && ritr != reverse_end) + { + groupedStream << wstring{ delimiter }; + groupingSize = 0; // reset for a new group + + // Shift the grouping to next values if they exist + if (groupItr != grouping.end()) + { + ++groupItr; + + // Loop through grouping vector until we find a non-zero value. + // "0" values may appear in a form of either e.g. "3;0" or "3;0;0". + // A 0 in the last position means repeat the previous grouping. + // A 0 in another position is a group. So, "3;0;0" means "group 3, then group 0 repeatedly" + // This could be expressed as just "3" but GetLocaleInfo is returning 3;0;0 in some cases instead. + for (currGrouping = 0; groupItr != grouping.end(); ++groupItr) + { + // If it's a non-zero value, that's our new group + if (*groupItr != 0) + { + currGrouping = *groupItr; + break; + } + + // Otherwise, save the previous grouping in case we need to repeat it + currGrouping = *(groupItr - 1); + } + } + } + } + + // now copy the negative sign if it is there + if (isNumNegative) + { + groupedStream << displayString[0]; + } + + auto groupedString = groupedStream.str(); + wstring result(groupedString.rbegin(), groupedString.rend()); + // Add the right (fractional or exponential) part of the number to the final string. + if (hasDecimal) + { + result += displayString.substr(dec); + } + else if (hasExponent) + { + result += displayString.substr(exp); + } + + return result; +} diff --git a/src/CalcManager/CEngine/scifunc.cpp b/src/CalcManager/CEngine/scifunc.cpp new file mode 100644 index 00000000..f4597572 --- /dev/null +++ b/src/CalcManager/CEngine/scifunc.cpp @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/**************************************************************************/ +/*** SCICALC Scientific Calculator for Windows 3.00.12 ***/ +/*** (c)1989 Microsoft Corporation. All Rights Reserved. ***/ +/*** ***/ +/*** scifunc.c ***/ +/*** ***/ +/*** Functions contained: ***/ +/*** SciCalcFunctions--do sin, cos, tan, com, log, ln, rec, fac, etc.***/ +/*** DisplayError--Error display driver. ***/ +/*** ***/ +/*** Functions called: ***/ +/*** SciCalcFunctions call DisplayError. ***/ +/*** ***/ +/*** ***/ +/**************************************************************************/ +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace std; +using namespace CalcEngine; + +/* Routines for more complex mathematical functions/error checking. */ +CalcEngine::Rational CCalcEngine::SciCalcFunctions(CalcEngine::Rational const& rat, DWORD op) +{ + PRAT tempRat = rat.ToPRAT(); + try + { + switch (op) + { + case IDC_CHOP: + m_bInv ? fracrat(&tempRat , m_radix, m_precision) : intrat(&tempRat, m_radix, m_precision); + break; + + /* Return complement. */ + case IDC_COM: + { + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + NumObjNot(&tempRat, m_fIntegerMode, chopRat, m_radix, m_precision); + destroyrat(chopRat); + break; + } + + // Rotate Left with hi bit wrapped over to lo bit + case IDC_ROL: + if (m_fIntegerMode) + { + intrat(&tempRat, m_radix, m_precision); + + PRAT curRat = m_currentVal.ToPRAT(); + ULONGLONG w64Bits = NumObjGetUlValue(curRat, m_radix, m_precision); + ULONGLONG msb = (w64Bits >> (m_dwWordBitWidth - 1)) & 1; + + w64Bits <<= 1; // LShift by 1 + w64Bits |= msb; // Set the prev Msb as the current Lsb + NumObjSetUlonglongValue(&curRat, w64Bits, m_radix, m_precision); + m_currentVal = Rational{ curRat }; + destroyrat(curRat); + } + break; + + // Rotate right with lo bit wrapped over to hi bit + case IDC_ROR: + if (m_fIntegerMode) + { + intrat(&tempRat, m_radix, m_precision); + PRAT curRat = m_currentVal.ToPRAT(); + ULONGLONG w64Bits = NumObjGetUlValue(curRat, m_radix, m_precision); + + ULONGLONG lsb = ((w64Bits & 0x01) == 1) ? 1 : 0; + w64Bits >>= 1; //RShift by 1 + w64Bits |= (lsb << (m_dwWordBitWidth - 1)); + NumObjSetUlonglongValue(&curRat, w64Bits, m_radix, m_precision); + m_currentVal = Rational{ curRat }; + destroyrat(curRat); + } + break; + + case IDC_PERCENT: + { + PRAT hno = nullptr; + PRAT hno100 = nullptr; + + try + { + // If the operator is multiply/divide, we evaluate this as "X [op] (Y%)" + // Otherwise, we evaluate it as "X [op] (X * Y%)" + if (m_nOpCode == IDC_MUL || m_nOpCode == IDC_DIV) + { + NumObjSetIntValue(&hno100, 100); + + divrat(&tempRat, hno100, m_precision); + destroyrat(hno100); + } + else + { + hno = m_lastVal.ToPRAT(); + NumObjSetIntValue(&hno100, 100); + + divrat(&hno, hno100, m_precision); + destroyrat(hno100); + + mulrat(&tempRat, hno, m_precision); + destroyrat(hno); + } + } + catch ( DWORD nErrCode ) + { + destroyrat(hno); + destroyrat(hno100); + destroyrat(tempRat); + throw nErrCode; + } + break; + } + + case IDC_SIN: /* Sine; normal and arc */ + if (!m_fIntegerMode) + { + m_bInv ? asinanglerat(&tempRat, m_angletype, m_radix, m_precision) : NumObjSin(&tempRat, m_angletype, m_radix, m_precision); + } + break; + + case IDC_SINH: /* Sine- hyperbolic and archyperbolic */ + if (!m_fIntegerMode) + { + m_bInv ? asinhrat(&tempRat, m_radix, m_precision) : sinhrat(&tempRat, m_radix, m_precision); + } + break; + + case IDC_COS: /* Cosine, follows convention of sine function. */ + if (!m_fIntegerMode) + { + m_bInv ? acosanglerat(&tempRat, m_angletype, m_radix, m_precision) : NumObjCos(&tempRat, m_angletype, m_radix, m_precision); + } + break; + + case IDC_COSH: /* Cosine hyperbolic, follows convention of sine h function. */ + if (!m_fIntegerMode) + { + m_bInv ? acoshrat(&tempRat, m_radix, m_precision) : coshrat(&tempRat, m_radix, m_precision); + } + break; + + case IDC_TAN: /* Same as sine and cosine. */ + if (!m_fIntegerMode) + { + m_bInv ? atananglerat(&tempRat, m_angletype, m_radix, m_precision) : NumObjTan(&tempRat, m_angletype, m_radix, m_precision); + } + break; + + case IDC_TANH: /* Same as sine h and cosine h. */ + if (!m_fIntegerMode) + { + m_bInv ? atanhrat(&tempRat, m_precision) : tanhrat(&tempRat, m_radix, m_precision); + } + break; + + case IDC_REC: /* Reciprocal. */ + NumObjInvert(&tempRat, m_precision); + break; + + case IDC_SQR: /* Square */ + powrat(&tempRat, rat_two, m_radix, m_precision); + break; + + case IDC_SQRT: /* Sqrt only in Std mode */ + rootrat(&tempRat, rat_two, m_radix, m_precision); + break; + + case IDC_CUBEROOT: + case IDC_CUB: /* Cubing and cube root functions. */ + { + PRAT hno = nullptr; + + try + { + NumObjAssign(&hno, rat_one); + addrat(&hno, rat_two, m_precision); + + if (IDC_CUBEROOT == op) + { + rootrat(&tempRat, hno, m_radix, m_precision); + } + else + { + powrat(&tempRat, hno, m_radix, m_precision); + } + + destroyrat(hno); + } + catch (DWORD nErrCode) + { + destroyrat(hno); + destroyrat(tempRat); + throw nErrCode; + } + break; + } + + case IDC_LOG: /* Functions for common log. */ + log10rat(&tempRat, m_precision); + break; + case IDC_POW10: + NumObjAntiLog10(&tempRat, m_radix, m_precision); + break; + case IDC_LN: /* Functions for natural log. */ + if (m_bInv) + { + exprat(&tempRat, m_radix, m_precision); // e^x. + } + else + { + lograt(&tempRat, m_precision); + } + break; + + case IDC_FAC: /* Calculate factorial. Inverse is ineffective. */ + factrat(&tempRat, m_radix, m_precision); + break; + + case IDC_DEGREES: + ProcessCommand(IDC_INV); + // This case falls through to IDC_DMS case because in the old Win32 Calc, + // the degrees functionality was achieved as 'Inv' of 'dms' operation, + // so setting the IDC_INV command first and then performing 'dms' operation as global variables m_bInv, m_bRecord + // are set properly through ProcessCommand(IDC_INV) + case IDC_DMS: + { + if (!m_fIntegerMode) + { + PRAT hnoMin = nullptr; + PRAT hnoSec = nullptr; + PRAT hnoShft = nullptr; + + try + { + NumObjSetIntValue(&hnoShft, m_bInv ? 100 : 60); + + NumObjAssign(&hnoMin, tempRat); + intrat(&tempRat, m_radix, m_precision); + + subrat(&hnoMin, tempRat, m_precision); + mulrat(&hnoMin, hnoShft, m_precision); + NumObjAssign(&hnoSec, hnoMin ); + intrat(&hnoMin, m_radix, m_precision); + + subrat(&hnoSec, hnoMin, m_precision); + mulrat(&hnoSec, hnoShft, m_precision); + + // + // tempRat == degrees, hnoMin == minutes, hnoSec == seconds + // + + NumObjSetIntValue(&hnoShft, m_bInv ? 60 : 100); + divrat(&hnoSec, hnoShft, m_precision); + addrat(&hnoMin, hnoSec, m_precision); + + divrat(&hnoMin, hnoShft, m_precision); + addrat(&tempRat, hnoMin, m_precision); + + destroyrat(hnoShft); + destroyrat(hnoMin); + destroyrat(hnoSec); + } + catch (DWORD nErrCode) + { + destroyrat(hnoShft); + destroyrat(hnoMin); + destroyrat(hnoSec); + destroyrat(tempRat); + throw nErrCode; + } + } + break; + } + } // end switch( op ) + + Rational result{ tempRat }; + destroyrat(tempRat); + return result; + } + catch(DWORD nErrCode) + { + DisplayError(nErrCode); + destroyrat(tempRat); + return rat; + } +} + +/* Routine to display error messages and set m_bError flag. Errors are */ +/* called with DisplayError (n), where n is a DWORD between 0 and 5. */ + +void CCalcEngine::DisplayError(DWORD nError) +{ + wstring errorString{ GetString(IDS_ERRORS_FIRST + SCODE_CODE(nError)) }; + + SetPrimaryDisplay(errorString, true /*isError*/); + + m_bError = true; /* Set error flag. Only cleared with CLEAR or CENTR. */ + + m_HistoryCollector.ClearHistoryLine(errorString); +} + diff --git a/src/CalcManager/CEngine/scimath.cpp b/src/CalcManager/CEngine/scimath.cpp new file mode 100644 index 00000000..1a8b0ea3 --- /dev/null +++ b/src/CalcManager/CEngine/scimath.cpp @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace std; + +/**************************************************************************\ +* * +* * +* * +* # # ##### * +* # # # # # * +* # # # # # # # * +* # ### ### # # * +* # # ### # # # ### # # ### ##### # ### ### ### * +* # ## # # # ## # # # # # # ## # # # * +* # # # # # # # # # ##### # # ##### # * +* # # # # # # # # # # # # # # ## * +* # # # # # # # # # ### # # ### ### ## * +* * +* * +* Infinte Precision Production Version * +* * +\**************************************************************************/ +// +// RETAIL version of NUMOBJ math that uses Infinite Precision +// +// History +// +// 16-Nov-1996 Wrote it +// whenever-97 Rewrote it using improved ratpak model +// + +/*****************************************************************\ +* +* Generic Math Package support routines and variables +* +* History: +* 01-Dec-1996 Wrote them +* whenever-97 Rewrote them +* +\*****************************************************************/ + +/*****************************************************************\ +* +* Unary functions +* +* History: +* 01-Dec-1996 Wrote them +* whenever-97 Rewrote them +* +\*****************************************************************/ + +void NumObjInvert(PRAT * phno, int32_t precision) +{ + PRAT hno = nullptr; + + NumObjAssign( &hno, rat_one); + divrat( &hno, *phno, precision); + NumObjAssign( phno, hno ); + NumObjDestroy( &hno ); +} + +void NumObjNegate(PRAT *phno) +{ + (*phno)->pp->sign = -(*phno)->pp->sign; +} + +void NumObjAbs(PRAT *phno) +{ + (*phno)->pp->sign = 1; + (*phno)->pq->sign = 1; +} + +void NumObjAntiLog10(PRAT *phno, uint32_t radix, int32_t precision) +{ + PRAT hno = nullptr; + + NumObjSetIntValue( &hno, 10 ); + powrat( &hno, *phno, radix, precision); + NumObjAssign( phno, hno ); + NumObjDestroy( &hno ); +} + +void NumObjNot(PRAT *phno, bool fIntegerMode, PRAT chopNum, uint32_t radix, int32_t precision) +{ + if (radix == 10 && !fIntegerMode) + { + intrat( phno, radix, precision); + addrat( phno, rat_one, precision); + NumObjNegate( phno ); + } + else + { + + xorrat( phno, chopNum, radix, precision); + } +} + +void NumObjSin(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision ) +{ + sinanglerat(phno, angletype, radix, precision); +} + +void NumObjCos(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) +{ + cosanglerat(phno, angletype, radix, precision); +} + +void NumObjTan(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) +{ + tananglerat(phno, angletype, radix, precision); +} + +/******************************************************************\ +* +* Number format conversion routines +* +* History: +* 06-Dec-1996 wrote them +\******************************************************************/ +void NumObjSetIntValue(PRAT *phnol, LONG i ) +{ + PRAT pr = nullptr; + + pr = longtorat( i ); + NumObjAssign( phnol, pr ); + destroyrat(pr); +} + +void NumObjSetIUlongValue(PRAT *phnol, ULONG ul ) +{ + PRAT pr = nullptr; + + pr = Ulongtorat( ul ); + NumObjAssign( phnol, pr ); + destroyrat(pr); +} + +// A Set with 64 bit number +void NumObjSetUlonglongValue(PRAT *phnol, ULONGLONG ul, uint32_t radix, int32_t precision) +{ + ULONG hi, lo; + PRAT hno = nullptr; + + hi = HIDWORD(ul); + lo = LODWORD(ul); + NumObjSetIUlongValue(phnol, hi); + NumObjSetIntValue(&hno, 32); + lshrat(phnol, hno, radix, precision); + NumObjSetIUlongValue(&hno, lo); + orrat(phnol, hno, radix, precision); + NumObjDestroy(&hno); +} + +ULONGLONG NumObjGetUlValue( PRAT hnol, uint32_t radix, int32_t precision) +{ + return rattoUlonglong(hnol, radix, precision); +} + +wstring NumObjToString(PRAT hnoNum, uint32_t radix, NUMOBJ_FMT fmt, int32_t precision) +{ + return RatToString(hnoNum, fmt, radix, precision); +} + +bool NumObjIsZero(PRAT hno) +{ + return zerrat(hno); +} + +bool NumObjIsLess(PRAT hno1, PRAT hno2, int32_t precision) +{ + return rat_lt(hno1, hno2, precision); +} + +bool NumObjIsLessEq(PRAT hno1, PRAT hno2, int32_t precision) +{ + return rat_le(hno1, hno2, precision); +} + +bool NumObjIsGreaterEq(PRAT hno1, PRAT hno2, int32_t precision) +{ + return rat_ge(hno1, hno2, precision); +} + +bool NumObjIsEq(PRAT hno1, PRAT hno2, int32_t precision) +{ + return rat_equ(hno1, hno2, precision); +} + +void NumObjAssign(PRAT *phnol, PRAT hnor) +{ + DUPRAT(*phnol, hnor); +} + +int32_t NumObjGetExp(PRAT hno) +{ + return LOGRATRADIX(hno); +} + +void NumObjDestroy(PRAT *phno) +{ + destroyrat(*phno); +} diff --git a/src/CalcManager/CEngine/scioper.cpp b/src/CalcManager/CEngine/scioper.cpp new file mode 100644 index 00000000..5e78bd88 --- /dev/null +++ b/src/CalcManager/CEngine/scioper.cpp @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace CalcEngine; + +// Routines to perform standard operations &|^~<<>>+-/*% and pwr. +CalcEngine::Rational CCalcEngine::DoOperation(int operation, CalcEngine::Rational const& lhs, CalcEngine::Rational const& rhs) +{ + PRAT rhsRat = rhs.ToPRAT(); + PRAT hno = nullptr; + + // Remove any variance in how 0 could be represented in rat e.g. -0, 0/n, etc. + PRAT resultRat = nullptr; + if (lhs.IsZero()) + { + NumObjAssign(&resultRat, rat_zero); + } + else + { + resultRat = lhs.ToPRAT(); + } + + try + { + switch (operation) + { + case IDC_AND: + andrat(&resultRat, rhsRat, m_radix, m_precision); + break; + + case IDC_OR: + orrat(&resultRat, rhsRat, m_radix, m_precision); + break; + + case IDC_XOR: + xorrat(&resultRat, rhsRat, m_radix, m_precision); + break; + + case IDC_RSHF: + { + if (m_fIntegerMode) + { + NumObjSetIntValue(&hno, m_dwWordBitWidth); + if (NumObjIsGreaterEq(resultRat, hno, m_precision)) // Lsh/Rsh >= than current word size is always 0 + { + throw CALC_E_NORESULT; + } + } + + NumObjAssign(&hno, resultRat); + NumObjAssign(&resultRat, rhsRat); + + uint64_t w64Bits = NumObjGetUlValue(resultRat, m_radix, m_precision); + bool fMsb = (w64Bits >> (m_dwWordBitWidth - 1)) & 1; + + rshrat(&resultRat, hno, m_radix, m_precision); + + if (fMsb) + { + intrat(&resultRat, m_radix, m_precision); + PRAT temp = m_chopNumbers[m_numwidth].ToPRAT(); + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + rshrat(&temp, hno, m_radix, m_precision); + intrat(&temp, m_radix, m_precision); + xorrat(&temp, chopRat, m_radix, m_precision); + orrat(&resultRat, temp, m_radix, m_precision); + destroyrat(temp); + destroyrat(chopRat); + } + break; + } + + case IDC_LSHF: + if (m_fIntegerMode) + { + NumObjSetIntValue(&hno, m_dwWordBitWidth); + if (NumObjIsGreaterEq(resultRat, hno, m_precision)) + { + throw CALC_E_NORESULT; + } + } + + NumObjAssign(&hno, resultRat); + NumObjAssign(&resultRat, rhsRat); + + lshrat(&resultRat, hno, m_radix, m_precision); + break; + + case IDC_ADD: + addrat(&resultRat, rhsRat, m_precision); + break; + + case IDC_SUB: + // in order to do ( rhsRat - resultRat ) we actually do -(resultRat - rhsRat ) because it's quicker + subrat(&resultRat, rhsRat, m_precision); + NumObjNegate(&resultRat); + break; + + case IDC_MUL: + mulrat(&resultRat, rhsRat, m_precision); + break; + + case IDC_DIV: + case IDC_MOD: + { + int iNumeratorSign = 1, iDenominatorSign = 1, iFinalSign = 1; + // REVIEW: These lengthly number assignments can be replaced with some quick pointer swaps. + // the swaps cannot change the value of pratX unless we also modify the code that calls + // the DoOperation function. + NumObjAssign(&hno, resultRat); + NumObjAssign(&resultRat, rhsRat); + + if(m_fIntegerMode) + { + uint64_t w64Bits = NumObjGetUlValue(resultRat, m_radix, m_precision); + bool fMsb = (w64Bits >> (m_dwWordBitWidth - 1)) & 1; + + if (fMsb) + { + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + NumObjNot(&resultRat, true, chopRat, m_radix, m_precision); + destroyrat(chopRat); + addrat(&resultRat, rat_one, m_precision); + + iNumeratorSign = -1; + } + + w64Bits = NumObjGetUlValue(hno, m_radix, m_precision); + fMsb = (w64Bits >> (m_dwWordBitWidth - 1)) & 1; + + if (fMsb) + { + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + NumObjNot( &hno, true, chopRat, m_radix, m_precision); + destroyrat(chopRat); + addrat( &hno, rat_one, m_precision); + + iDenominatorSign = -1; + } + } + + if (operation == IDC_DIV) + { + iFinalSign = iNumeratorSign * iDenominatorSign; + divrat(&resultRat, hno, m_precision); + } + else + { + iFinalSign = iNumeratorSign; + modrat(&resultRat, hno ); + } + + if (m_fIntegerMode && iFinalSign == -1) + { + intrat(&resultRat, m_radix, m_precision); + NumObjNegate(&resultRat); + } + + break; + } + + case IDC_PWR: // Calculates rhsRat to the resultRat(th) power. + { + NumObjAssign(&hno, resultRat); + NumObjAssign(&resultRat, rhsRat); + + powrat(&resultRat, hno , m_radix, m_precision); + + break; + } + case IDC_ROOT: // Calculates rhsRat to the resultRat(th) root. + { + NumObjAssign(&hno, resultRat); + NumObjAssign(&resultRat, rhsRat); + + rootrat(&resultRat, hno, m_radix, m_precision); + + break; + } + } + } + catch (DWORD dwErrCode) + { + DisplayError(dwErrCode); + + // On error, return the original value + destroyrat(resultRat); + resultRat = lhs.ToPRAT(); + } + + destroyrat(hno); + destroyrat(rhsRat); + + Rational result{ resultRat }; + destroyrat(resultRat); + + return result; +} diff --git a/src/CalcManager/CEngine/sciset.cpp b/src/CalcManager/CEngine/sciset.cpp new file mode 100644 index 00000000..e303b004 --- /dev/null +++ b/src/CalcManager/CEngine/sciset.cpp @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/**************************************************************************/ +/*** SCICALC Scientific Calculator for Windows 3.00.12 ***/ +/*** (c)1989 Microsoft Corporation. All Rights Reserved. ***/ +/*** ***/ +/*** sciset.c ***/ +/*** ***/ +/*** Functions contained: ***/ +/*** ***/ +/*** Functions called: ***/ +/*** none ***/ +/*** ***/ +/*** History: ***/ +/*** 12-Dec-1996 Added SetMaxIntDigits ***/ +/*** Whenever-97 Removed SetMaxIntDigits ***/ +/*** ***/ +/**************************************************************************/ +#include "pch.h" +#include "Header Files/CalcEngine.h" + +using namespace CalcEngine; + +// +// To be called when either the radix or num width changes. You can use -1 in either of these values to mean +// dont change that. +void CCalcEngine::SetRadixTypeAndNumWidth(RADIX_TYPE radixtype, NUM_WIDTH numwidth) +{ + // When in integer mode, the number is represented in 2's complement form. When a bit width is changing, we can + // change the number representation back to sign, abs num form in ratpak. Soon when display sees this, it will + // convert to 2's complement form, but this time all high bits will be propogated. Eg. -127, in byte mode is + // represented as 1000,0001. This puts it back as sign=-1, 01111111 . But DisplayNum will see this and convert it + // back to 1111,1111,1000,0001 when in Word mode. + if (m_fIntegerMode) + { + PRAT curRat = m_currentVal.ToPRAT(); + ULONGLONG w64Bits = NumObjGetUlValue(curRat, m_radix, m_precision); + bool fMsb = (w64Bits >> (m_dwWordBitWidth - 1)) & 1; // make sure you use the old width + + if (fMsb) + { + // If high bit is set, then get the decimal number in -ve 2'scompl form. + PRAT chopRat = m_chopNumbers[m_numwidth].ToPRAT(); + NumObjNot(&curRat, true, chopRat, m_radix, m_precision); + destroyrat(chopRat); + addrat(&curRat, rat_one, m_precision); + NumObjNegate(&curRat); + m_currentVal = Rational{ curRat }; + } + + destroyrat(curRat); + } + + if (radixtype >= HEX_RADIX && radixtype <= BIN_RADIX) + { + m_radix = NRadixFromRadixType(radixtype); + // radixtype is not even saved + } + + if (numwidth >= QWORD_WIDTH && numwidth <= BYTE_WIDTH) + { + m_numwidth = numwidth; + m_dwWordBitWidth = DwWordBitWidthFromeNumWidth(numwidth); + } + + // inform ratpak that a change in base or precision has occured + BaseOrPrecisionChanged(); + + // display the correct number for the new state (ie convert displayed + // number to correct base) + DisplayNum(); +} + +LONG CCalcEngine::DwWordBitWidthFromeNumWidth(NUM_WIDTH /*numwidth*/) +{ + static constexpr int nBitMax[] = { 64, 32, 16, 8 }; + LONG wmax = nBitMax[0]; + + if (m_numwidth >= 0 && m_numwidth < ARRAYSIZE(nBitMax)) + { + wmax = nBitMax[m_numwidth]; + } + return wmax; +} + +uint32_t CCalcEngine::NRadixFromRadixType( RADIX_TYPE radixtype) +{ + static constexpr uint32_t rgnRadish[4]={16, 10, 8, 2}; /* Number bases in the same order as radixtype */ + uint32_t radix = 10; + + // convert special bases into symbolic values + if (radixtype >= 0 && radixtype < ARRAYSIZE(rgnRadish)) + { + radix = rgnRadish[radixtype]; + } + return radix; +} + +// Toggles a given bit into the number representation. returns true if it changed it actually. +bool CCalcEngine::TryToggleBit(CalcEngine::Rational& rat, DWORD wbitno) +{ + DWORD wmax = DwWordBitWidthFromeNumWidth(m_numwidth); + if (wbitno >= wmax) + { + return false; // ignore error cant happen + } + + PRAT hnumPow = nullptr; + NumObjAssign(&hnumPow, rat_two); + PRAT hnum = longtorat(wbitno); + powrat(&hnumPow, hnum, m_radix, m_precision); + + PRAT resultRat = rat.ToPRAT(); + intrat(&resultRat, m_radix, m_precision); + if (NumObjIsZero(resultRat)) + { + // This is the same work around happenning in SciCalcFunctions. Ought to move to intrat function itself. + // Basic bug is there which doesn treat 0/ n as 0, or -0 as 0 etc. + NumObjAssign(&resultRat, rat_zero); + } + + xorrat(&resultRat, hnumPow, m_radix, m_precision); + rat = Rational{ resultRat }; + destroyrat(resultRat); + NumObjDestroy(&hnumPow); + NumObjDestroy(&hnum); + + return true; +} + +// Returns the nearest power of two +int CCalcEngine::QuickLog2(int iNum) +{ + int iRes = 0; + + // while first digit is a zero + while (!(iNum & 1)) + { + iRes++; + iNum >>= 1; + } + + // if our number isn't a perfect square + iNum = iNum >> 1; + if (iNum) + { + // find the largest digit + for (iNum = iNum >> 1; iNum; iNum = iNum >> 1) + ++iRes; + + // and then add two + iRes += 2; + } + + return iRes; +} + +//////////////////////////////////////////////////////////////////////// +// +// UpdateMaxIntDigits +// +// determine the maximum number of digits needed for the current precision, +// word size, and base. This number is conservative towards the small side +// such that there may be some extra bits left over. For example, base 8 requires 3 bits per digit. +// A word size of 32 bits allows for 10 digits with a remainder of two bits. Bases +// that require variable numnber of bits (non-power-of-two bases) are approximated +// by the next highest power-of-two base (again, to be conservative and gaurentee +// there will be no over flow verse the current word size for numbers entered). +// Base 10 is a special case and always uses the base 10 precision (m_nPrecisionSav). +void CCalcEngine::UpdateMaxIntDigits() +{ + if (m_radix == 10) + { + // if in integer mode you still have to honor the max digits you can enter based on bit width + if (m_fIntegerMode) + { + m_cIntDigitsSav = static_cast(m_maxDecimalValueStrings[m_numwidth].length()) - 1; + // This is the max digits you can enter a decimal in fixed width mode aka integer mode -1. The last digit + // has to be checked separately + } + else + { + m_cIntDigitsSav = m_precision; + } + } + else + { + m_cIntDigitsSav = m_dwWordBitWidth / CCalcEngine::QuickLog2(m_radix); + } +} + +void CCalcEngine::ChangeBaseConstants(uint32_t radix, int maxIntDigits, int32_t precision) +{ + if (10 == radix) + { + ChangeConstants(radix, precision); // Base 10 precesion for internal computing still needs to be 32, to + // take care of decimals preceisly. For eg. to get the HI word of a qword, we do a rsh, which depends on getting + // 18446744073709551615 / 4294967296 = 4294967295.9999917... This is important it works this and doesnt reduce + // the precision to number of digits allowed to enter. In otherwords precision and # of allowed digits to be + // entered are different. + } + else + { + ChangeConstants(radix, maxIntDigits + 1); + } +} + +void CCalcEngine::BaseOrPrecisionChanged() +{ + UpdateMaxIntDigits(); + CCalcEngine::ChangeBaseConstants(m_radix, m_cIntDigitsSav, m_precision); +} diff --git a/src/CalcManager/CalcException.h b/src/CalcManager/CalcException.h new file mode 100644 index 00000000..60d82bbb --- /dev/null +++ b/src/CalcManager/CalcException.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +class CalcException : std::exception +{ +public: + CalcException(HRESULT hr) + { + m_hr = hr; + } + HRESULT GetException() + { + return m_hr; + } +private: + HRESULT m_hr; +}; + +void IFT(HRESULT hr) +{ + if (FAILED(hr)) + { + CalcException exception(hr); + throw(exception); + } +} diff --git a/src/CalcManager/CalcManager.vcxproj b/src/CalcManager/CalcManager.vcxproj new file mode 100644 index 00000000..02e5f8af --- /dev/null +++ b/src/CalcManager/CalcManager.vcxproj @@ -0,0 +1,335 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {311e866d-8b93-4609-a691-265941fee101} + Win32Proj + CalcManager + CalcManager + false + en-US + 15.0 + true + Windows Store + 10.0 + 10.0.17763.0 + 10.0.17134.0 + + + + + x64 + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + _UNICODE;UNICODE;%(PreprocessorDefinitions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + Use + false + true + /Zm250 /await /std:c++17 /permissive- /Zc:twoPhase- %(AdditionalOptions) + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + + + Console + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/src/CalcManager/CalcManager.vcxproj.filters b/src/CalcManager/CalcManager.vcxproj.filters new file mode 100644 index 00000000..fd61b03d --- /dev/null +++ b/src/CalcManager/CalcManager.vcxproj.filters @@ -0,0 +1,169 @@ + + + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {957a8e3c-00c7-48bc-b63c-83b2140a8251} + + + {a1bae6f0-0a01-447d-9a3a-5c65bcd384e6} + + + {5149465e-c5c9-48a2-b676-f11380b733a0} + + + + + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + CEngine + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + RatPack + + + + + + CEngine + + + CEngine + + + CEngine + + + + + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + RatPack + + + RatPack + + + RatPack + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/CalcManager/CalculatorHistory.Cpp b/src/CalcManager/CalculatorHistory.Cpp new file mode 100644 index 00000000..a34ca272 --- /dev/null +++ b/src/CalcManager/CalculatorHistory.Cpp @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CalculatorHistory.h" + +using namespace std; +using namespace CalculationManager; + +CalculatorHistory::CalculatorHistory(CALCULATOR_MODE eMode, size_t maxSize) : + m_mode(eMode), + m_maxHistorySize(maxSize) +{} + +unsigned int CalculatorHistory::AddToHistory(_In_ shared_ptr>> const &tokens, _In_ shared_ptr>> const &commands, _In_ wstring_view result) +{ + unsigned int addedIndex; + wstring generatedExpression; + shared_ptr spHistoryItem = make_shared(); + + spHistoryItem->historyItemVector.spTokens = tokens; + spHistoryItem->historyItemVector.spCommands = commands; + + // to be changed when pszexp is back + tokens->GetString(&generatedExpression); + // Prefixing and suffixing the special Unicode markers to ensure that the expression + // in the history doesn't get broken for RTL languages + spHistoryItem->historyItemVector.expression = L'\u202d' + generatedExpression + L'\u202c'; + spHistoryItem->historyItemVector.result = wstring(result); + addedIndex = AddItem(spHistoryItem); + + return addedIndex; +} + + +unsigned int CalculatorHistory::AddItem(_In_ shared_ptr const &spHistoryItem) +{ + int lastIndex; + + if (m_historyItems.size() >= m_maxHistorySize) + { + m_historyItems.erase(m_historyItems.begin()); + } + + m_historyItems.push_back(spHistoryItem); + lastIndex = static_cast(m_historyItems.size() - 1); + return lastIndex; +} + +bool CalculatorHistory::RemoveItem(_In_ unsigned int uIdx) +{ + if (uIdx > m_historyItems.size() - 1) + { + return false; + } + + m_historyItems.erase(m_historyItems.begin() + uIdx); + return true; +} + +vector> const& CalculatorHistory::GetHistory() +{ + return m_historyItems; +} + +shared_ptr const& CalculatorHistory::GetHistoryItem(_In_ unsigned int uIdx) +{ + assert(uIdx >= 0 && uIdx < m_historyItems.size()); + return m_historyItems.at(uIdx); +} + +CalculatorHistory::~CalculatorHistory(void) +{ + ClearHistory(); +} + +void CalculatorHistory::ClearHistory() +{ + m_historyItems.clear(); +} diff --git a/src/CalcManager/CalculatorHistory.h b/src/CalcManager/CalculatorHistory.h new file mode 100644 index 00000000..aececbda --- /dev/null +++ b/src/CalcManager/CalculatorHistory.h @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "ExpressionCommandInterface.h" +#include "Header Files/IHistoryDisplay.h" + +namespace CalculationManager +{ + enum CALCULATOR_MODE + { + CM_STD = 0, + CM_SCI, + }; + + struct HISTORYITEMVECTOR + { + std::shared_ptr>> spTokens; + std::shared_ptr>> spCommands; + std::wstring expression; + std::wstring result; + }; + + struct HISTORYITEM + { + HISTORYITEMVECTOR historyItemVector; + }; + + class CalculatorHistory : + public IHistoryDisplay + { + + public: + CalculatorHistory(CALCULATOR_MODE eMode, const size_t maxSize); + unsigned int AddToHistory(_In_ std::shared_ptr>> const &spTokens, _In_ std::shared_ptr>> const &spCommands, std::wstring_view result); + std::vector> const& GetHistory(); + std::shared_ptr const& GetHistoryItem(unsigned int uIdx); + void ClearHistory(); + unsigned int AddItem(_In_ std::shared_ptr const &spHistoryItem); + bool RemoveItem(unsigned int uIdx); + const size_t MaxHistorySize() const { return m_maxHistorySize; } + ~CalculatorHistory(void); + + private: + std::vector> m_historyItems; + CALCULATOR_MODE m_mode; + const size_t m_maxHistorySize; + }; +} diff --git a/src/CalcManager/CalculatorManager.cpp b/src/CalcManager/CalculatorManager.cpp new file mode 100644 index 00000000..d768e7af --- /dev/null +++ b/src/CalcManager/CalculatorManager.cpp @@ -0,0 +1,867 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files\CalcEngine.h" +#include "CalculatorManager.h" +#include "CalculatorResource.h" + +using namespace std; +using namespace CalcEngine; + +static constexpr size_t MAX_HISTORY_ITEMS = 20; +static constexpr size_t SERIALIZED_NUMBER_MINSIZE = 3; + +// Converts Memory Command enum value to unsigned char, +// while ignoring Warning C4309: 'conversion' : truncation of constant value +#define MEMORY_COMMAND_TO_UNSIGNED_CHAR(c)\ + __pragma(warning(push))\ + __pragma(warning(disable: 4309))\ + static_cast(c)\ + __pragma(warning(pop)) + +namespace CalculationManager +{ + CalculatorManager::CalculatorManager(_In_ ICalcDisplay* displayCallback, _In_ IResourceProvider* resourceProvider) : + m_displayCallback(displayCallback), + m_resourceProvider(resourceProvider), + m_currentDegreeMode(Command::CommandNULL), + m_savedDegreeMode(Command::CommandDEG), + m_isExponentialFormat(false), + m_persistedPrimaryValue(), + m_currentCalculatorEngine(nullptr), + m_pStdHistory(new CalculatorHistory(CM_STD, MAX_HISTORY_ITEMS)), + m_pSciHistory(new CalculatorHistory(CM_SCI, MAX_HISTORY_ITEMS)), + m_inHistoryItemLoadMode(false) + { + CCalcEngine::InitialOneTimeOnlySetup(*m_resourceProvider); + } + + /// + /// Destructor for CalculatorManager + /// Ends two CCalcEngine + /// + CalculatorManager::~CalculatorManager() + { + this->MemorizedNumberClearAll(); + } + + /// + /// Call the callback function using passed in IDisplayHelper. + /// Used to set the primary display value on ViewModel + /// + /// wstring representing text to be displayed + void CalculatorManager::SetPrimaryDisplay(_In_ const wstring& displayString, _In_ bool isError) + { + if (!m_inHistoryItemLoadMode) + { + m_displayCallback->SetPrimaryDisplay(displayString, isError); + } + } + + void CalculatorManager::SetIsInError(bool isError) + { + m_displayCallback->SetIsInError(isError); + } + + void CalculatorManager::DisplayPasteError() + { + m_currentCalculatorEngine->DisplayError(CALC_E_DOMAIN /*code for "Invalid input" error*/); + } + + void CalculatorManager::MaxDigitsReached() + { + m_displayCallback->MaxDigitsReached(); + } + + void CalculatorManager::BinaryOperatorReceived() + { + m_displayCallback->BinaryOperatorReceived(); + } + + void CalculatorManager::MemoryItemChanged(unsigned int indexOfMemory) + { + m_displayCallback->MemoryItemChanged(indexOfMemory); + } + + /// + /// Call the callback function using passed in IDisplayHelper. + /// Used to set the expression display value on ViewModel + /// + /// wstring representing expression to be displayed + void CalculatorManager::SetExpressionDisplay(_Inout_ shared_ptr>> const &tokens, _Inout_ shared_ptr>> const &commands) + { + if (!m_inHistoryItemLoadMode) + { + m_displayCallback->SetExpressionDisplay(tokens, commands); + } + } + + /// + /// Callback from the CalculatorControll + /// Passed in string representations of memorized numbers get passed to the client + /// + /// vector containing wstring values of memorized numbers + void CalculatorManager::SetMemorizedNumbers(_In_ const vector& memorizedNumbers) + { + m_displayCallback->SetMemorizedNumbers(memorizedNumbers); + } + + /// + /// Callback from the engine + /// Used to set the current unmatched open parenthesis count + /// + /// string containing the parenthesis count + void CalculatorManager::SetParenDisplayText(const wstring& parenthesisCount) + { + m_displayCallback->SetParenDisplayText(parenthesisCount); + } + + /// + /// Reset CalculatorManager. + /// Set the mode to the standard calculator + /// Set the degree mode as regular degree (as oppose to Rad or Grad) + /// Clear all the entries and memories + /// Clear Memory if clearMemory parameter is true.(Default value is true) + /// + void CalculatorManager::Reset(bool clearMemory /* = true*/) + { + m_savedCommands.clear(); + SetStandardMode(); + + if (m_scientificCalculatorEngine) + { + m_scientificCalculatorEngine->ProcessCommand(IDC_DEG); + m_scientificCalculatorEngine->ProcessCommand(IDC_CLEAR); + + if (m_isExponentialFormat) + { + m_isExponentialFormat = false; + m_scientificCalculatorEngine->ProcessCommand(IDC_FE); + } + } + if (m_programmerCalculatorEngine) + { + m_programmerCalculatorEngine->ProcessCommand(IDC_CLEAR); + } + + if (clearMemory) + { + this->MemorizedNumberClearAll(); + } + } + + /// + /// Change the current calculator engine to standard calculator engine. + /// + void CalculatorManager::SetStandardMode() + { + if (!m_standardCalculatorEngine) + { + m_standardCalculatorEngine = make_unique(false /* Respect Order of Operations */, false /* Set to Integer Mode */, m_resourceProvider, this, m_pStdHistory); + } + + m_currentCalculatorEngine = m_standardCalculatorEngine.get(); + m_currentCalculatorEngine->ProcessCommand(IDC_DEC); + m_currentCalculatorEngine->ProcessCommand(IDC_CLEAR); + m_currentCalculatorEngine->ChangePrecision(static_cast(CalculatorPrecision::StandardModePrecision)); + UpdateMaxIntDigits(); + m_pHistory = m_pStdHistory.get(); + } + + /// + /// Change the current calculator engine to scientific calculator engine. + /// + void CalculatorManager::SetScientificMode() + { + if (!m_scientificCalculatorEngine) + { + m_scientificCalculatorEngine = make_unique(true /* Respect Order of Operations */, false /* Set to Integer Mode */, m_resourceProvider, this, m_pSciHistory); + } + + m_currentCalculatorEngine = m_scientificCalculatorEngine.get(); + m_currentCalculatorEngine->ProcessCommand(IDC_DEC); + m_currentCalculatorEngine->ProcessCommand(IDC_CLEAR); + m_currentCalculatorEngine->ChangePrecision(static_cast(CalculatorPrecision::ScientificModePrecision)); + m_pHistory = m_pSciHistory.get(); + } + + /// + /// Change the current calculator engine to scientific calculator engine. + /// + void CalculatorManager::SetProgrammerMode() + { + if(!m_programmerCalculatorEngine) + { + m_programmerCalculatorEngine = make_unique(true /* Respect Order of Operations */, true /* Set to Integer Mode */, m_resourceProvider, this, nullptr); + } + + m_currentCalculatorEngine = m_programmerCalculatorEngine.get(); + m_currentCalculatorEngine->ProcessCommand(IDC_DEC); + m_currentCalculatorEngine->ProcessCommand(IDC_CLEAR); + m_currentCalculatorEngine->ChangePrecision(static_cast(CalculatorPrecision::ProgrammerModePrecision)); + } + + + /// + /// Send command to the Calc Engine + /// Cast Command Enum to WPARAM. + /// Handle special commands such as mode change and combination of two commands. + /// + /// Enum Command + void CalculatorManager::SendCommand(_In_ Command command) + { + // When the expression line is cleared, we save the current state, which includes, + // primary display, memory, and degree mode + if (command == Command::CommandCLEAR || command == Command::CommandEQU + || command == Command::ModeBasic || command == Command::ModeScientific || command == Command::ModeProgrammer) + { + switch (command) + { + case Command::ModeBasic: + this->SetStandardMode(); + break; + case Command::ModeScientific: + this->SetScientificMode(); + break; + case Command::ModeProgrammer: + this->SetProgrammerMode(); + break; + default: + m_currentCalculatorEngine->ProcessCommand(static_cast(command)); + } + + m_savedCommands.clear(); // Clear the previous command history + + if (command != Command::CommandEQU && command != Command::CommandCLEAR) + { + m_savedCommands.push_back(MapCommandForSerialize(command)); + } + this->SerializePrimaryDisplay(); + this->SerializeMemory(); + m_savedDegreeMode = m_currentDegreeMode; + return; + } + + if (command == Command::CommandDEG || command == Command::CommandRAD || command == Command::CommandGRAD) + { + m_currentDegreeMode = command; + } + + if (command != Command::CommandFE) + { + m_savedCommands.push_back(MapCommandForSerialize(command)); // Save the commands in the m_savedCommands + } + + switch (command) + { + case Command::CommandASIN: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandSIN)); + break; + case Command::CommandACOS: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandCOS)); + break; + case Command::CommandATAN: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandTAN)); + break; + case Command::CommandPOWE: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandLN)); + break; + case Command::CommandASINH: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandSINH)); + break; + case Command::CommandACOSH: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandCOSH)); + break; + case Command::CommandATANH: + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandINV)); + m_currentCalculatorEngine->ProcessCommand(static_cast(Command::CommandTANH)); + break; + case Command::CommandFE: + m_isExponentialFormat = !m_isExponentialFormat; + // fall through + default: + m_currentCalculatorEngine->ProcessCommand(static_cast(command)); + break; + } + } + + /// + /// Convert Command to unsigned char. + /// Since some Commands are higher than 255, they are saved after subtracting 255 + /// The smallest Command is CommandSIGN = 80, thus, subtracted value does not overlap with other values. + /// + /// Enum Command + unsigned char CalculatorManager::MapCommandForSerialize(Command command) + { + unsigned int commandToSave = static_cast(command); + commandToSave > UCHAR_MAX ? commandToSave -= UCHAR_MAX : commandToSave; + return static_cast(commandToSave); + } + + /// + /// Convert Command to unsigned int + /// The command that is smaller than 80, CommandSIGN, can be converted back to original value by adding 255. + /// + /// unsigned char value represent the saved command + unsigned int CalculatorManager::MapCommandForDeSerialize(unsigned char command) + { + unsigned int commandToLoad = command; + if (command < static_cast(Command::CommandSIGN)) + { + commandToLoad += UCHAR_MAX; + } + return commandToLoad; + } + + /// + /// Return saved degree mode which is saved when last time the expression was cleared. + /// + Command CalculatorManager::SerializeSavedDegreeMode() + { + return m_savedDegreeMode; + } + + void CalculatorManager::SerializePrimaryDisplay() + { + m_savedPrimaryValue.clear(); + m_currentCalculatorEngine->ProcessCommand(IDC_STORE); + auto memoryObject = m_currentCalculatorEngine->PersistedMemObject(); + if (memoryObject != nullptr) + { + m_savedPrimaryValue = SerializeRational(*memoryObject); + } + } + + /// + /// Return serialized primary display that is saved when the expression line was cleared. + /// + vector CalculatorManager::GetSerializedPrimaryDisplay() + { + return m_savedPrimaryValue; + } + + /// + /// DeSerialize the primary display from vector of long + /// + /// Serialized Rational of primary display + void CalculatorManager::DeSerializePrimaryDisplay(const vector &serializedPrimaryDisplay) + { + if (serializedPrimaryDisplay.size() == 0) + { + return; + } + m_persistedPrimaryValue = DeSerializeRational(serializedPrimaryDisplay.begin()); + this->LoadPersistedPrimaryValue(); + } + + /// + /// Load the persisted value that is saved in memory of CalcEngine + /// + void CalculatorManager::LoadPersistedPrimaryValue() + { + m_currentCalculatorEngine->PersistedMemObject(m_persistedPrimaryValue); + m_currentCalculatorEngine->ProcessCommand(IDC_RECALL); + } + + /// + /// Serialize the Memory to vector of long + /// + /// Serialized Rational of memory + void CalculatorManager::SerializeMemory() + { + m_serializedMemory.clear(); + + for (auto const& memoryItem : m_memorizedNumbers) + { + auto serialMem = SerializeRational(memoryItem); + m_serializedMemory.insert(m_serializedMemory.end(), serialMem.begin(), serialMem.end()); + } + } + + vector CalculatorManager::GetSerializedMemory() + { + return m_serializedMemory; + } + + /// + /// DeSerialize the Memory from vector of long + /// + /// Serialized Rational of memory + void CalculatorManager::DeSerializeMemory(const vector &serializedMemory) + { + vector::const_iterator itr = serializedMemory.begin(); + while (itr != serializedMemory.end()) + { + Rational memoryItem = DeSerializeRational(itr); + auto lengthMemoryItem = (2 * SERIALIZED_NUMBER_MINSIZE) + memoryItem.P().Mantissa().size() + memoryItem.Q().Mantissa().size(); + m_memorizedNumbers.push_back(memoryItem); + itr += lengthMemoryItem; + } + this->SetMemorizedNumbersString(); + } + + /// + /// Return the commands saved since the expression has been cleared. + /// + vector CalculatorManager::SerializeCommands() + { + return m_savedCommands; + } + + /// + /// Replay the serialized commands + /// + /// Serialized commands + void CalculatorManager::DeSerializeCommands(_In_ const vector& serializedData) + { + m_savedCommands.clear(); + + for (auto commandItr = serializedData.begin(); commandItr != serializedData.end(); ++commandItr) + { + if (*commandItr >= MEMORY_COMMAND_TO_UNSIGNED_CHAR(MemoryCommand::MemorizeNumber) && + *commandItr <= MEMORY_COMMAND_TO_UNSIGNED_CHAR(MemoryCommand::MemorizedNumberClearAll)) + { + //MemoryCommands(which have values above 255) are pushed on m_savedCommands upon casting to unsigned char. + //SerializeCommands uses m_savedCommands, which is then used in DeSerializeCommnds. + //Hence, a simple cast to MemoryCommand is not sufficient. + MemoryCommand memoryCommand = static_cast(*commandItr + UCHAR_MAX + 1); + unsigned int indexOfMemory = 0; + switch (memoryCommand) + { + case MemoryCommand::MemorizeNumber: + this->MemorizeNumber(); + break; + case MemoryCommand::MemorizedNumberLoad: + if (commandItr + 1 == serializedData.end()) + { + throw out_of_range("Expecting index of memory, data ended prematurely"); + } + indexOfMemory = *(++commandItr); + this->MemorizedNumberLoad(indexOfMemory); + break; + case MemoryCommand::MemorizedNumberAdd: + if (commandItr + 1 == serializedData.end()) + { + throw out_of_range("Expecting index of memory, data ended prematurely"); + } + indexOfMemory = *(++commandItr); + this->MemorizedNumberAdd(indexOfMemory); + break; + case MemoryCommand::MemorizedNumberSubtract: + if (commandItr + 1 == serializedData.end()) + { + throw out_of_range("Expecting index of memory, data ended prematurely"); + } + indexOfMemory = *(++commandItr); + this->MemorizedNumberSubtract(indexOfMemory); + break; + case MemoryCommand::MemorizedNumberClearAll: + this->MemorizedNumberClearAll(); + break; + default: + break; + } + } + else + { + this->SendCommand(static_cast(MapCommandForDeSerialize(*commandItr))); + } + } + } + + /// + /// Memorize the current displayed value + /// Notify the client with new the new memorize value vector + /// + void CalculatorManager::MemorizeNumber() + { + m_savedCommands.push_back(MEMORY_COMMAND_TO_UNSIGNED_CHAR(MemoryCommand::MemorizeNumber)); + if (!(m_currentCalculatorEngine->FInErrorState())) + { + m_currentCalculatorEngine->ProcessCommand(IDC_STORE); + + auto memoryObjectPtr = m_currentCalculatorEngine->PersistedMemObject(); + if (memoryObjectPtr != nullptr) + { + m_memorizedNumbers.insert(m_memorizedNumbers.begin(), *memoryObjectPtr); + } + + if (m_memorizedNumbers.size() > m_maximumMemorySize) + { + m_memorizedNumbers.resize(m_maximumMemorySize); + } + this->SetMemorizedNumbersString(); + } + } + + /// + /// Recall the memorized number. + /// The memorized number gets loaded to the primary display + /// + /// Index of the target memory + void CalculatorManager::MemorizedNumberLoad(_In_ unsigned int indexOfMemory) + { + SaveMemoryCommand(MemoryCommand::MemorizedNumberLoad, indexOfMemory); + if (!(m_currentCalculatorEngine->FInErrorState())) + { + this->MemorizedNumberSelect(indexOfMemory); + m_currentCalculatorEngine->ProcessCommand(IDC_RECALL); + } + } + + /// + /// Do the addition to the selected memory + /// It adds primary display value to the selected memory + /// Notify the client with new the new memorize value vector + /// + /// Index of the target memory + void CalculatorManager::MemorizedNumberAdd(_In_ unsigned int indexOfMemory) + { + SaveMemoryCommand(MemoryCommand::MemorizedNumberAdd, indexOfMemory); + if (!(m_currentCalculatorEngine->FInErrorState())) + { + if (m_memorizedNumbers.empty()) + { + this->MemorizeNumber(); + } + else + { + this->MemorizedNumberSelect(indexOfMemory); + m_currentCalculatorEngine->ProcessCommand(IDC_MPLUS); + + this->MemorizedNumberChanged(indexOfMemory); + + this->SetMemorizedNumbersString(); + } + + m_displayCallback->MemoryItemChanged(indexOfMemory); + } + } + + void CalculatorManager::MemorizedNumberClear(_In_ unsigned int indexOfMemory) + { + if (indexOfMemory < m_memorizedNumbers.size()) + { + SaveMemoryCommand(MemoryCommand::MemorizedNumberClear, indexOfMemory); + m_memorizedNumbers.erase(m_memorizedNumbers.begin() + indexOfMemory); + } + } + + /// + /// Do the subtraction to the selected memory + /// It adds primary display value to the selected memory + /// Notify the client with new the new memorize value vector + /// + /// Index of the target memory + void CalculatorManager::MemorizedNumberSubtract(_In_ unsigned int indexOfMemory) + { + SaveMemoryCommand(MemoryCommand::MemorizedNumberSubtract, indexOfMemory); + if (!(m_currentCalculatorEngine->FInErrorState())) + { + // To add negative of the number on display to the memory -x = x - 2x + if (m_memorizedNumbers.empty()) + { + this->MemorizeNumber(); + this->MemorizedNumberSubtract(0); + this->MemorizedNumberSubtract(0); + } + else + { + this->MemorizedNumberSelect(indexOfMemory); + m_currentCalculatorEngine->ProcessCommand(IDC_MMINUS); + + this->MemorizedNumberChanged(indexOfMemory); + + this->SetMemorizedNumbersString(); + } + + m_displayCallback->MemoryItemChanged(indexOfMemory); + } + } + + /// + /// Clear all the memorized values + /// Notify the client with new the new memorize value vector + /// + void CalculatorManager::MemorizedNumberClearAll() + { + m_savedCommands.push_back(MEMORY_COMMAND_TO_UNSIGNED_CHAR(MemoryCommand::MemorizedNumberClearAll)); + m_memorizedNumbers.clear(); + + m_currentCalculatorEngine->ProcessCommand(IDC_MCLEAR); + this->SetMemorizedNumbersString(); + } + + /// +/// Helper function that selects a memeory from the vector and set it to CCalcEngine +/// Saved RAT number needs to be copied and passed in, as CCalcEngine destoried the passed in RAT +/// +/// Index of the target memory + void CalculatorManager::MemorizedNumberSelect(_In_ unsigned int indexOfMemory) + { + if (!(m_currentCalculatorEngine->FInErrorState())) + { + auto memoryObject = m_memorizedNumbers.at(indexOfMemory); + m_currentCalculatorEngine->PersistedMemObject(memoryObject); + } + } + + /// + /// Helper function that needs to be executed when memory is modified + /// When memory is modified, destory the old RAT and put the new RAT in vector + /// + /// Index of the target memory + void CalculatorManager::MemorizedNumberChanged(_In_ unsigned int indexOfMemory) + { + if (!(m_currentCalculatorEngine->FInErrorState())) + { + auto memoryObject = m_currentCalculatorEngine->PersistedMemObject(); + if (memoryObject != nullptr) + { + m_memorizedNumbers.at(indexOfMemory) = *memoryObject; + } + } + } + + void CalculatorManager::SaveMemoryCommand(_In_ MemoryCommand command, _In_ unsigned int indexOfMemory) + { + m_savedCommands.push_back(MEMORY_COMMAND_TO_UNSIGNED_CHAR(command)); + if (indexOfMemory > UCHAR_MAX) + { + throw invalid_argument("Unexpected value. IndexOfMemory is bigger than the biggest unsigned char"); + } + m_savedCommands.push_back(static_cast(indexOfMemory)); + } + + vector> const& CalculatorManager::GetHistoryItems() + { + return m_pHistory->GetHistory(); + } + + vector> const& CalculatorManager::GetHistoryItems(_In_ CALCULATOR_MODE mode) + { + return (mode == CM_STD) ? + m_pStdHistory->GetHistory() : + m_pSciHistory->GetHistory(); + } + + shared_ptr const& CalculatorManager::GetHistoryItem(_In_ unsigned int uIdx) + { + return m_pHistory->GetHistoryItem(uIdx); + } + + void CalculatorManager::OnHistoryItemAdded(_In_ unsigned int addedItemIndex) + { + m_displayCallback->OnHistoryItemAdded(addedItemIndex); + } + + bool CalculatorManager::RemoveHistoryItem(_In_ unsigned int uIdx) + { + return m_pHistory->RemoveItem(uIdx); + } + + void CalculatorManager::ClearHistory() + { + m_pHistory->ClearHistory(); + } + + void CalculatorManager::SetRadix(RADIX_TYPE iRadixType) + { + switch (iRadixType) + { + case RADIX_TYPE::HEX_RADIX: + m_currentCalculatorEngine->ProcessCommand(IDC_HEX); + break; + case RADIX_TYPE::DEC_RADIX: + m_currentCalculatorEngine->ProcessCommand(IDC_DEC); + break; + case RADIX_TYPE::OCT_RADIX: + m_currentCalculatorEngine->ProcessCommand(IDC_OCT); + break; + case RADIX_TYPE::BIN_RADIX: + m_currentCalculatorEngine->ProcessCommand(IDC_BIN); + break; + default: + break; + } + SetMemorizedNumbersString(); + } + + void CalculatorManager::SetMemorizedNumbersString() + { + vector resultVector; + for (auto const& memoryItem : m_memorizedNumbers) + { + int radix = m_currentCalculatorEngine->GetCurrentRadix(); + wstring stringValue = m_currentCalculatorEngine->GetStringForDisplay(memoryItem, radix); + + if (!stringValue.empty()) + { + resultVector.push_back(m_currentCalculatorEngine->GroupDigitsPerRadix(stringValue, radix)); + } + } + m_displayCallback->SetMemorizedNumbers(resultVector); + } + + CalculationManager::Command CalculatorManager::GetCurrentDegreeMode() + { + if (m_currentDegreeMode == Command::CommandNULL) + { + m_currentDegreeMode = Command::CommandDEG; + } + return m_currentDegreeMode; + } + + void CalculatorManager::SetHistory(_In_ CALCULATOR_MODE eMode, _In_ vector> const& history) + { + CalculatorHistory* pHistory = nullptr; + + switch (eMode) + { + case CM_STD: + pHistory = m_pStdHistory.get(); + break; + case CM_SCI: + pHistory = m_pSciHistory.get(); + break; + } + + if (pHistory) + { + pHistory->ClearHistory(); + for (unsigned int i = 0; i < history.size(); ++i) + { + pHistory->AddItem(history[i]); + } + } + } + + wstring CalculatorManager::GetResultForRadix(uint32_t radix, int32_t precision) + { + return m_currentCalculatorEngine ? m_currentCalculatorEngine->GetCurrentResultForRadix(radix, precision) : L""; + } + + void CalculatorManager::SetPrecision(int32_t precision) + { + m_currentCalculatorEngine->ChangePrecision(precision); + } + + void CalculatorManager::UpdateMaxIntDigits() + { + m_currentCalculatorEngine->UpdateMaxIntDigits(); + } + + wchar_t CalculatorManager::DecimalSeparator() + { + return m_currentCalculatorEngine ? m_currentCalculatorEngine->DecimalSeparator() : m_resourceProvider->GetCEngineString(L"sDecimal")[0]; + } + + bool CalculatorManager::IsEngineRecording() + { + return m_currentCalculatorEngine->FInRecordingState() ? true : false; + } + + void CalculatorManager::SetInHistoryItemLoadMode(_In_ bool isHistoryItemLoadMode) + { + m_inHistoryItemLoadMode = isHistoryItemLoadMode; + } + + /// + /// Serialize Rational to vector of long + /// How Rational is serialized : + /// Serialized Rational.P(Number) + Serialized Rational.Q(Number) + /// How Number is saved : + /// [0] = Rational.P.Sign + /// [1] = Rational.P.Mantissa.size + /// [2] = Rational.P.Exp + /// [3] = Rational.P.Mantissa[0] + /// [4] = Rational.P.Mantissa[1] + /// ... + /// [2 + Rational.P.Mantissa.size] = Rational.P.Mantissa[size - 1] + /// + /// Rational number to be serialized + vector CalculatorManager::SerializeRational(Rational const& rat) + { + vector serializedRational{}; + + auto serialP = SerializeNumber(rat.P()); + serializedRational.insert(serializedRational.end(), serialP.begin(), serialP.end()); + + auto serialQ = SerializeNumber(rat.Q()); + serializedRational.insert(serializedRational.end(), serialQ.begin(), serialQ.end()); + + return serializedRational; + } + + /// + /// DeserializeRational vector and construct a Rational + /// How Rational is serialized : + /// Serialized Rational.P(Number) + Serialized Rational.Q(Number) + /// + Rational CalculatorManager::DeSerializeRational(vector::const_iterator itr) + { + auto p = DeSerializeNumber(itr); + auto q = DeSerializeNumber(itr + SERIALIZED_NUMBER_MINSIZE + p.Mantissa().size()); + + return Rational(p, q); + } + + /// + /// Serialize Number to vector of long + /// How Number is saved : + /// [0] = Number.Sign + /// [1] = Number.Mantissa.size + /// [2] = Number.Exp + /// [3] = Number.Mantissa[0] + /// [4] = Number.Mantissa[1] + /// ... + /// [2 + Number.Mantissa.size] = Number.Mantissa[size - 1] + /// + /// Number to be serialized + vector CalculatorManager::SerializeNumber(Number const& num) + { + vector serializedNumber{}; + + serializedNumber.push_back(num.Sign()); + serializedNumber.push_back(static_cast(num.Mantissa().size())); + serializedNumber.push_back(num.Exp()); + for (auto const& digit : num.Mantissa()) + { + serializedNumber.push_back(digit); + } + + return serializedNumber; + } + + /// + /// DeserializeNumber vector and construct a Number + /// How Number is saved : + /// [0] = Number.Sign + /// [1] = Number.Mantissa.size + /// [2] = Number.Exp + /// [3] = Number.Mantissa[0] + /// [4] = Number.Mantissa[1] + /// ... + /// [2 + Number.Mantissa.size] = Number.Mantissa[size - 1] + /// + Number CalculatorManager::DeSerializeNumber(vector::const_iterator itr) + { + int32_t sign = *itr; + uint32_t size = *(itr + 1); + int32_t exp = *(itr + 2); + vector mant{}; + for (size_t i = 0; i < size; ++i) + { + mant.emplace_back(*(itr + 3 + i)); + } + + return Number{ sign, exp, mant }; + } +} diff --git a/src/CalcManager/CalculatorManager.h b/src/CalcManager/CalculatorManager.h new file mode 100644 index 00000000..a0efffaf --- /dev/null +++ b/src/CalcManager/CalculatorManager.h @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "CalculatorHistory.h" +#include "Header Files\Rational.h" + +namespace CalculationManager +{ + enum class Command; + struct HISTORYITEM; + + enum class CalculatorMode + { + StandardMode, + ScientificMode, + ProgrammerMode, + }; + + enum class CalculatorPrecision + { + StandardModePrecision = 16, + ScientificModePrecision = 32, + ProgrammerModePrecision = 64 + }; + + // Numbering continues from the Enum Command from Command.h + // with some gap to ensure there is no overlap of these ids + // when static_cast is performed on these ids + // they shouldn't fall in any number range greater than 80. So never + // make the memory command ids go below 330 + enum class MemoryCommand + { + MemorizeNumber = 330, + MemorizedNumberLoad = 331, + MemorizedNumberAdd = 332, + MemorizedNumberSubtract = 333, + MemorizedNumberClearAll = 334, + MemorizedNumberClear = 335 + }; + + class CalculatorManager sealed : public virtual ICalcDisplay + { + private: + ICalcDisplay* const m_displayCallback; + CCalcEngine* m_currentCalculatorEngine; + std::unique_ptr m_scientificCalculatorEngine; + std::unique_ptr m_standardCalculatorEngine; + std::unique_ptr m_programmerCalculatorEngine; + IResourceProvider* const m_resourceProvider; + bool m_inHistoryItemLoadMode; + + std::vector m_memorizedNumbers; + CalcEngine::Rational m_persistedPrimaryValue; + + bool m_isExponentialFormat; + + static const unsigned int m_maximumMemorySize = 100; + + // For persistance + std::vector m_savedCommands; + std::vector m_savedPrimaryValue; + std::vector m_serializedMemory; + std::vector m_currentSerializedMemory; + Command m_currentDegreeMode; + Command m_savedDegreeMode; + unsigned char MapCommandForSerialize(Command command); + unsigned int MapCommandForDeSerialize(unsigned char command); + + void SaveMemoryCommand(_In_ MemoryCommand command, _In_ unsigned int indexOfMemory); + + void MemorizedNumberSelect(_In_ unsigned int); + void MemorizedNumberChanged(_In_ unsigned int); + + void LoadPersistedPrimaryValue(); + + static std::vector SerializeRational(CalcEngine::Rational const& rat); + static CalcEngine::Rational DeSerializeRational(std::vector::const_iterator itr); + + static std::vector SerializeNumber(CalcEngine::Number const& num); + static CalcEngine::Number DeSerializeNumber(std::vector::const_iterator itr); + + std::shared_ptr m_pStdHistory; + std::shared_ptr m_pSciHistory; + CalculatorHistory* m_pHistory; + + public: + // ICalcDisplay + void SetPrimaryDisplay(_In_ const std::wstring& displayString, _In_ bool isError) override; + void SetIsInError(bool isError) override; + void SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands) override; + void SetMemorizedNumbers(_In_ const std::vector& memorizedNumbers) override; + void OnHistoryItemAdded(_In_ unsigned int addedItemIndex) override; + void SetParenDisplayText(const std::wstring& parenthesisCount); + void DisplayPasteError(); + void MaxDigitsReached() override; + void BinaryOperatorReceived() override; + void MemoryItemChanged(unsigned int indexOfMemory) override; + + + CalculatorManager(ICalcDisplay* displayCallback, IResourceProvider* resourceProvider); + ~CalculatorManager(); + + void Reset(bool clearMemory = true); + void SetStandardMode(); + void SetScientificMode(); + void SetProgrammerMode(); + void SendCommand(_In_ Command command); + std::vector SerializeCommands(); + void DeSerializeCommands(_In_ const std::vector& serializedData); + void SerializeMemory(); + std::vector GetSerializedMemory(); + void DeSerializeMemory(const std::vector &serializedMemory); + void SerializePrimaryDisplay(); + std::vector GetSerializedPrimaryDisplay(); + void DeSerializePrimaryDisplay(const std::vector &serializedPrimaryDisplay); + Command SerializeSavedDegreeMode(); + + void MemorizeNumber(); + void MemorizedNumberLoad(_In_ unsigned int); + void MemorizedNumberAdd(_In_ unsigned int); + void MemorizedNumberSubtract(_In_ unsigned int); + void MemorizedNumberClear(_In_ unsigned int); + void MemorizedNumberClearAll(); + + bool IsEngineRecording(); + std::vector GetSavedCommands(){ return m_savedCommands; } + void SetRadix(RADIX_TYPE iRadixType); + void SetMemorizedNumbersString(); + std::wstring GetResultForRadix(uint32_t radix, int32_t precision); + void SetPrecision(int32_t precision); + void UpdateMaxIntDigits(); + wchar_t DecimalSeparator(); + + std::vector> const& GetHistoryItems(); + std::vector> const& GetHistoryItems(_In_ CalculationManager::CALCULATOR_MODE mode); + std::shared_ptr const& GetHistoryItem(_In_ unsigned int uIdx); + bool RemoveHistoryItem(_In_ unsigned int uIdx); + void ClearHistory(); + const size_t MaxHistorySize() const { return m_pHistory->MaxHistorySize(); } + CalculationManager::Command GetCurrentDegreeMode(); + void SetHistory(_In_ CALCULATOR_MODE eMode, _In_ std::vector> const& history); + void SetInHistoryItemLoadMode(_In_ bool isHistoryItemLoadMode); + }; +} diff --git a/src/CalcManager/CalculatorResource.h b/src/CalcManager/CalculatorResource.h new file mode 100644 index 00000000..51db56da --- /dev/null +++ b/src/CalcManager/CalculatorResource.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculationManager +{ + class IResourceProvider + { + public: + virtual ~IResourceProvider() { } + + // Should return a string from the resource table for strings used + // by the calculation engine. The strings that must be defined + // and the ids to define them with can be seen in EngineStrings.h + // with SIDS prefix. Additionally it must provide values for string + // ids "sDecimal", "sThousand" and "sGrouping". See + // http://technet.microsoft.com/en-us/library/cc782655(v=ws.10).aspx + // for what these values refer to. + virtual std::wstring GetCEngineString(const std::wstring& id) = 0; + }; +} diff --git a/src/CalcManager/CalculatorVector.h b/src/CalcManager/CalculatorVector.h new file mode 100644 index 00000000..5ddf5300 --- /dev/null +++ b/src/CalcManager/CalculatorVector.h @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +template +class CalculatorVector +{ +public: + HRESULT GetAt(_In_opt_ unsigned int index, _Out_ TType *item) + { + HRESULT hr = S_OK; + try + { + *item = m_vector.at(index); + } + catch (std::out_of_range /*ex*/) + { + hr = E_BOUNDS; + } + return hr; + } + + HRESULT GetSize(_Out_ unsigned int *size) + { + *size = static_cast(m_vector.size()); + return S_OK; + } + + HRESULT SetAt(_In_ unsigned int index, _In_opt_ TType item) + { + HRESULT hr = S_OK; + try + { + m_vector[index] = item; + } + catch (std::out_of_range /*ex*/) + { + hr = E_BOUNDS; + } + return hr; + } + + HRESULT RemoveAt(_In_ unsigned int index) + { + HRESULT hr = S_OK; + if (index < m_vector.size()) + { + m_vector.erase(m_vector.begin() + index); + } + else + { + hr = E_BOUNDS; + } + return hr; + } + + HRESULT InsertAt(_In_ unsigned int index, _In_ TType item) + { + HRESULT hr = S_OK; + try + { + auto iter = m_vector.begin() + index; + m_vector.insert(iter, item); + } + catch (std::bad_alloc /*ex*/) + { + hr = E_OUTOFMEMORY; + } + return hr; + } + + HRESULT Truncate(_In_ unsigned int index) + { + HRESULT hr = S_OK; + if (index < m_vector.size()) + { + auto startIter = m_vector.begin() + index; + m_vector.erase(startIter, m_vector.end()); + } + else + { + hr = E_BOUNDS; + } + return hr; + } + + HRESULT Append(_In_opt_ TType item) + { + HRESULT hr = S_OK; + try + { + m_vector.push_back(item); + } + catch (std::bad_alloc /*ex*/) + { + hr = E_OUTOFMEMORY; + } + return hr; + } + + HRESULT RemoveAtEnd() + { + m_vector.erase(--(m_vector.end())); + return S_OK; + } + + HRESULT Clear() + { + m_vector.clear(); + return S_OK; + } + + HRESULT GetString(_Out_ std::wstring* expression) + { + HRESULT hr = S_OK; + unsigned int nTokens = 0; + std::pair currentPair; + hr = this->GetSize(&nTokens); + if (SUCCEEDED(hr)) + { + for (unsigned int i = 0; i < nTokens; i++) + { + hr = this->GetAt(i, ¤tPair); + if (SUCCEEDED(hr)) + { + expression->append(currentPair.first); + + if (i != (nTokens - 1)) + { + expression->append(L" "); + } + } + } + + std::wstring expressionSuffix{}; + hr = GetExpressionSuffix(&expressionSuffix); + if (SUCCEEDED(hr)) + { + expression->append(expressionSuffix); + } + } + + return hr; + } + + HRESULT GetExpressionSuffix(_Out_ std::wstring* suffix) + { + *suffix = L" ="; + return S_OK; + } + +private: + std::vector m_vector; +}; diff --git a/src/CalcManager/Command.h b/src/CalcManager/Command.h new file mode 100644 index 00000000..25ad7cf0 --- /dev/null +++ b/src/CalcManager/Command.h @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace UnitConversionManager +{ + enum class Command + { + Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine, + Decimal, + Negate, Backspace, + Clear, + Reset, + None + }; +} + +namespace CurrencyConversionManager +{ + enum class Command + { + Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine, + Decimal, + Negate, Backspace, + Clear, + None + }; +} + +namespace CalculationManager +{ + enum class CommandType + { + UnaryCommand, + BinaryCommand, + OperandCommand, + Parentheses + }; + + enum class Command + { + // Commands for programmer calculators are omitted. + CommandDEG = 321, + CommandRAD = 322, + CommandGRAD = 323, + CommandDegrees = 324, + CommandHYP = 325, + + CommandNULL = 0, + + // No new command should not be added before CommandSign, 80 + // If it is needed, the following two functions need to be revised too. + // CalculatorManager::MapCommandForSerialize(Command command); + // CalculatorManager::MapCommandForDeSerialize(unsigned char command); + CommandSIGN = 80, + CommandCLEAR = 81, + CommandCENTR = 82, + CommandBACK = 83, + + CommandPNT = 84, + + // Hole 85 + // Unused commands defined in Command.h is omitted. + CommandXor = 88, + CommandLSHF = 89, + CommandRSHF = 90, + CommandDIV = 91, + CommandMUL = 92, + CommandADD = 93, + CommandSUB = 94, + CommandMOD = 95, + CommandROOT = 96, + CommandPWR = 97, + + CommandCHOP = 98, // Unary operators must be between CommandCHOP and CommandEQU + CommandROL = 99, + CommandROR = 100, + CommandCOM = 101, + CommandSIN = 102, + CommandCOS = 103, + CommandTAN = 104, + + CommandSINH = 105, + CommandCOSH = 106, + CommandTANH = 107, + + + CommandLN = 108, + CommandLOG = 109, + CommandSQRT = 110, + CommandSQR = 111, + CommandCUB = 112, + CommandFAC = 113, + CommandREC = 114, + CommandDMS = 115, + CommandCUBEROOT = 116, //x ^ 1/3 + CommandPOW10 = 117, // 10 ^ x + CommandPERCENT = 118, + + CommandFE = 119, + CommandPI = 120, + CommandEQU = 121, + + CommandMCLEAR = 122, + CommandRECALL = 123, + CommandSTORE = 124, + CommandMPLUS = 125, + CommandMMINUS = 126, + + CommandEXP = 127, + + CommandOPENP = 128, + CommandCLOSEP = 129, + + Command0 = 130, // The controls for 0 through F must be consecutive and in order + Command1 = 131, + Command2 = 132, + Command3 = 133, + Command4 = 134, + Command5 = 135, + Command6 = 136, + Command7 = 137, + Command8 = 138, + Command9 = 139, + CommandA = 140, + CommandB = 141, + CommandC = 142, + CommandD = 143, + CommandE = 144, + CommandF = 145, // this is last control ID which must match the string table + CommandINV = 146, + CommandSET_RESULT = 147, + + CommandAnd = 86, + CommandOR = 87, + CommandNot = 101, + + ModeBasic = 200, + ModeScientific = 201, + + CommandASIN = 202, + CommandACOS = 203, + CommandATAN = 204, + CommandPOWE = 205, + CommandASINH = 206, + CommandACOSH = 207, + CommandATANH = 208, + + ModeProgrammer = 209, + CommandHex = 313, + CommandDec = 314, + CommandOct = 315, + CommandBin = 316, + CommandQword = 317, + CommandDword = 318, + CommandWord = 319, + CommandByte = 320, + + CommandBINEDITSTART = 700, + CommandBINPOS0 = 700, + CommandBINPOS1 = 701, + CommandBINPOS2 = 702, + CommandBINPOS3 = 703, + CommandBINPOS4 = 704, + CommandBINPOS5 = 705, + CommandBINPOS6 = 706, + CommandBINPOS7 = 707, + CommandBINPOS8 = 708, + CommandBINPOS9 = 709, + CommandBINPOS10 = 710, + CommandBINPOS11 = 711, + CommandBINPOS12 = 712, + CommandBINPOS13 = 713, + CommandBINPOS14 = 714, + CommandBINPOS15 = 715, + CommandBINPOS16 = 716, + CommandBINPOS17 = 717, + CommandBINPOS18 = 718, + CommandBINPOS19 = 719, + CommandBINPOS20 = 720, + CommandBINPOS21 = 721, + CommandBINPOS22 = 722, + CommandBINPOS23 = 723, + CommandBINPOS24 = 724, + CommandBINPOS25 = 725, + CommandBINPOS26 = 726, + CommandBINPOS27 = 727, + CommandBINPOS28 = 728, + CommandBINPOS29 = 729, + CommandBINPOS30 = 730, + CommandBINPOS31 = 731, + CommandBINPOS32 = 732, + CommandBINPOS33 = 733, + CommandBINPOS34 = 734, + CommandBINPOS35 = 735, + CommandBINPOS36 = 736, + CommandBINPOS37 = 737, + CommandBINPOS38 = 738, + CommandBINPOS39 = 739, + CommandBINPOS40 = 740, + CommandBINPOS41 = 741, + CommandBINPOS42 = 742, + CommandBINPOS43 = 743, + CommandBINPOS44 = 744, + CommandBINPOS45 = 745, + CommandBINPOS46 = 746, + CommandBINPOS47 = 747, + CommandBINPOS48 = 748, + CommandBINPOS49 = 749, + CommandBINPOS50 = 750, + CommandBINPOS51 = 751, + CommandBINPOS52 = 752, + CommandBINPOS53 = 753, + CommandBINPOS54 = 754, + CommandBINPOS55 = 755, + CommandBINPOS56 = 756, + CommandBINPOS57 = 757, + CommandBINPOS58 = 758, + CommandBINPOS59 = 759, + CommandBINPOS60 = 760, + CommandBINPOS61 = 761, + CommandBINPOS62 = 762, + CommandBINPOS63 = 763, + CommandBINEDITEND = 763 + }; +} diff --git a/src/CalcManager/ExpressionCommand.cpp b/src/CalcManager/ExpressionCommand.cpp new file mode 100644 index 00000000..ec177a64 --- /dev/null +++ b/src/CalcManager/ExpressionCommand.cpp @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Header Files\CCommand.h" +#include "CalculatorVector.h" +#include "ExpressionCommand.h" + +using namespace std; + +constexpr wchar_t chNegate = L'-'; +constexpr wchar_t chExp = L'e'; +constexpr wchar_t chPlus = L'+'; + +CParentheses::CParentheses(_In_ int command) :m_command(command) +{} + +int CParentheses::GetCommand() const +{ + return m_command; +} + +CalculationManager::CommandType CParentheses::GetCommandType() const +{ + return CalculationManager::CommandType::Parentheses; +} + +void CParentheses::Accept(_In_ ISerializeCommandVisitor &commandVisitor) +{ + commandVisitor.Visit(*this); +} + +CUnaryCommand::CUnaryCommand(int command) +{ + m_command = make_shared>(); + m_command->Append(command); +} + +CUnaryCommand::CUnaryCommand(int command1, int command2) +{ + m_command = make_shared>(); + m_command->Append(command1); + m_command->Append(command2); +} + +const shared_ptr> & CUnaryCommand::GetCommands() const +{ + return m_command; +} + +CalculationManager::CommandType CUnaryCommand::GetCommandType() const +{ + return CalculationManager::CommandType::UnaryCommand; +} + +void CUnaryCommand::SetCommand(int command) +{ + m_command->Clear(); + m_command->Append(command); +} + +void CUnaryCommand::SetCommands(int command1, int command2) +{ + m_command->Clear(); + m_command->Append(command1); + m_command->Append(command2); +} + +void CUnaryCommand::Accept(_In_ ISerializeCommandVisitor &commandVisitor) +{ + commandVisitor.Visit(*this); +} + +CBinaryCommand::CBinaryCommand(int command) :m_command(command) +{} + +void CBinaryCommand::SetCommand(int command) +{ + m_command = command; +} + +int CBinaryCommand::GetCommand() const +{ + return m_command; +} + +CalculationManager::CommandType CBinaryCommand::GetCommandType() const +{ + return CalculationManager::CommandType::BinaryCommand; +} + +void CBinaryCommand::Accept(_In_ ISerializeCommandVisitor &commandVisitor) +{ + commandVisitor.Visit(*this); +} + +COpndCommand::COpndCommand(_In_ shared_ptr> const &commands, + _In_ bool fNegative, + _In_ bool fDecimal, + _In_ bool fSciFmt) : + m_commands(commands), m_fNegative(fNegative), m_fDecimal(fDecimal), m_fSciFmt(fSciFmt) +{ + m_hnoNum = nullptr; +} + + +void COpndCommand::Initialize(_In_ PRAT hNoNum) +{ + assert(&m_hnoNum != nullptr); + if (m_hnoNum != nullptr) + { + destroyrat(m_hnoNum); + m_hnoNum = nullptr; + } + DUPRAT(m_hnoNum, hNoNum); +} + +const shared_ptr> & COpndCommand::GetCommands() const +{ + return m_commands; +} + +void COpndCommand::SetCommands(shared_ptr> const& commands) +{ + m_commands = commands; +} + +void COpndCommand::AppendCommand(int command) +{ + unsigned int nCommands; + m_commands->GetSize(&nCommands); + if (m_fSciFmt) + { + ClearAllAndAppendCommand(static_cast(command)); + } + else + { + m_commands->Append(command); + } + if (command == IDC_PNT) + { + m_fDecimal = true; +} +} + +void COpndCommand::ToggleSign() +{ + unsigned int commandCount; + m_commands->GetSize(&commandCount); + + for (unsigned int i = 0; i < commandCount; i++) + { + int nOpCode; + m_commands->GetAt(i, &nOpCode); + + if (nOpCode != IDC_0) + { + m_fNegative = !m_fNegative; + break; + } + } +} + +void COpndCommand::RemoveFromEnd() +{ + if (m_fSciFmt) + { + ClearAllAndAppendCommand(CalculationManager::Command::Command0); + } + else + { + unsigned int nCommands; + m_commands->GetSize(&nCommands); + + if (nCommands == 1) + { + ClearAllAndAppendCommand(CalculationManager::Command::Command0); + } + else + { + int nOpCode; + m_commands->GetAt(nCommands - 1, &nOpCode); + if (nOpCode == IDC_PNT) + { + m_fDecimal = false; + } + m_commands->RemoveAt(nCommands - 1); + } +} +} + +bool COpndCommand::IsNegative() const +{ + return m_fNegative; +} + +bool COpndCommand::IsSciFmt() const +{ + return m_fSciFmt; +} + +bool COpndCommand::IsDecimalPresent() const +{ + return m_fDecimal; +} + +CalculationManager::CommandType COpndCommand::GetCommandType() const +{ + return CalculationManager::CommandType::OperandCommand; +} + +void COpndCommand::ClearAllAndAppendCommand(CalculationManager::Command command) +{ + m_commands->Clear(); + m_commands->Append(static_cast(command)); + m_fSciFmt = false; + m_fNegative = false; + m_fDecimal = false; +} + +const wstring & COpndCommand::GetToken(wchar_t decimalSymbol) +{ + static const wchar_t chZero = L'0'; + + unsigned int nCommands; + m_commands->GetSize(&nCommands); + m_token.clear(); + int nOpCode; + + for (unsigned int i = 0; i < nCommands; i++) + { + m_commands->GetAt(i, &nOpCode); + if (nOpCode == IDC_PNT) + { + m_token.append(wstring{ decimalSymbol }); + } + else if (nOpCode == IDC_EXP) + { + m_token.append(&chExp); + int nextOpCode; + m_commands->GetAt(i + 1, &nextOpCode); + if (nextOpCode != IDC_SIGN) + { + m_token.append(&chPlus); + } + } + else if (nOpCode == IDC_SIGN) + { + m_token.append(&chNegate); + } + else + { + wstring num = to_wstring(nOpCode - IDC_0); + m_token.append(num); + } + } + + // Remove zeros + bool fDigitsFound = false; + int trimIdx = 0; + for (unsigned int i = 0; i < m_token.size(); i++) + { + if (m_token.at(i) != chZero) + { + if (m_token.at(i) == decimalSymbol) + { + trimIdx = i - 1; + } + else + { + trimIdx = i; + } + fDigitsFound = true; + break; + } + } + + if (fDigitsFound) + { + m_token.erase(0, trimIdx); + if (m_fNegative) + { + m_token.insert(0, &chNegate); + } + } + else + { + m_token.clear(); + m_token.append(&chZero); + } + + return m_token; +} + +wstring COpndCommand::GetString(uint32_t radix, int32_t precision, wchar_t decimalSymbol) +{ + wstring numString{}; + if (m_hnoNum != nullptr) + { + numString = NumObjToString(m_hnoNum, radix, eNUMOBJ_FMT::FMT_FLOAT, precision); + } + + return numString; +} + +COpndCommand::~COpndCommand() +{ + destroyrat(m_hnoNum); +} + +void COpndCommand::Accept(_In_ ISerializeCommandVisitor &commandVisitor) +{ + commandVisitor.Visit(*this); +} + diff --git a/src/CalcManager/ExpressionCommand.h b/src/CalcManager/ExpressionCommand.h new file mode 100644 index 00000000..d36c51ab --- /dev/null +++ b/src/CalcManager/ExpressionCommand.h @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "ExpressionCommandInterface.h" +#include "Header Files\CalcEngine.h" + +class CParentheses : public IParenthesisCommand +{ +public: + CParentheses(_In_ int command); + int GetCommand() const; + CalculationManager::CommandType GetCommandType() const; + void Accept(_In_ ISerializeCommandVisitor &commandVisitor); + +private: + int m_command; +}; + +class CUnaryCommand : public IUnaryCommand +{ +public: + CUnaryCommand(int command); + CUnaryCommand(int command1, int command2); + const std::shared_ptr> & GetCommands() const; + CalculationManager::CommandType GetCommandType() const; + void SetCommand(int command); + void SetCommands(int command1, int command2); + void Accept(_In_ ISerializeCommandVisitor &commandVisitor); + +private: + std::shared_ptr> m_command; +}; + +class CBinaryCommand : public IBinaryCommand +{ +public: + CBinaryCommand(int command); + void SetCommand(int command); + int GetCommand() const; + CalculationManager::CommandType GetCommandType() const; + void Accept(_In_ ISerializeCommandVisitor &commandVisitor); + +private: + int m_command; +}; + +class COpndCommand : public IOpndCommand +{ +public: + COpndCommand(_In_ std::shared_ptr> const &commands, + _In_ bool fNegative, + _In_ bool fDecimal, + _In_ bool fSciFmt); + ~COpndCommand(); + void Initialize(_In_ PRAT hNoNum); + + const std::shared_ptr> & GetCommands() const; + void SetCommands(std::shared_ptr> const& commands); + void AppendCommand(int command); + void ToggleSign(); + void RemoveFromEnd(); + bool IsNegative() const; + bool IsSciFmt() const; + bool IsDecimalPresent() const; + const std::wstring & GetToken(wchar_t decimalSymbol); + CalculationManager::CommandType GetCommandType() const; + void Accept(_In_ ISerializeCommandVisitor &commandVisitor); + std::wstring GetString(uint32_t radix, int32_t precision, wchar_t decimalSymbol); + +private: + std::shared_ptr> m_commands; + bool m_fNegative; + bool m_fSciFmt; + bool m_fDecimal; + std::wstring m_token; + PRAT m_hnoNum; + void ClearAllAndAppendCommand(CalculationManager::Command command); +}; + +class ISerializeCommandVisitor +{ +public: + virtual void Visit(_In_ COpndCommand &opndCmd) = 0; + virtual void Visit(_In_ CUnaryCommand &unaryCmd) = 0; + virtual void Visit(_In_ CBinaryCommand &binaryCmd) = 0; + virtual void Visit(_In_ CParentheses ¶Cmd) = 0; +}; diff --git a/src/CalcManager/ExpressionCommandInterface.h b/src/CalcManager/ExpressionCommandInterface.h new file mode 100644 index 00000000..bf19bf2f --- /dev/null +++ b/src/CalcManager/ExpressionCommandInterface.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "CalculatorVector.h" +#include "Command.h" + +class ISerializeCommandVisitor; + +class IExpressionCommand +{ +public: + virtual CalculationManager::CommandType GetCommandType() const = 0; + virtual void Accept(_In_ ISerializeCommandVisitor &commandVisitor) = 0; +}; + +class IOperatorCommand : public IExpressionCommand +{ +public: + virtual void SetCommand(int command) = 0; +}; + +class IUnaryCommand : public IOperatorCommand +{ +public: + virtual const std::shared_ptr> & GetCommands() const = 0; + virtual void SetCommands(int command1, int command2) = 0; +}; + +class IBinaryCommand : public IOperatorCommand +{ +public: + virtual void SetCommand(int command) = 0; + virtual int GetCommand() const = 0; +}; + +class IOpndCommand : public IExpressionCommand +{ +public: + virtual const std::shared_ptr> & GetCommands() const= 0; + virtual void AppendCommand(int command) = 0; + virtual void ToggleSign() = 0; + virtual void RemoveFromEnd() = 0; + virtual bool IsNegative() const = 0; + virtual bool IsSciFmt() const = 0; + virtual bool IsDecimalPresent() const = 0; + virtual const std::wstring & GetToken(wchar_t decimalSymbol) = 0; + virtual void SetCommands(std::shared_ptr> const& commands) = 0; +}; + +class IParenthesisCommand : public IExpressionCommand +{ +public: + virtual int GetCommand() const = 0; +}; diff --git a/src/CalcManager/Header Files/CCommand.h b/src/CalcManager/Header Files/CCommand.h new file mode 100644 index 00000000..36e505ad --- /dev/null +++ b/src/CalcManager/Header Files/CCommand.h @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/****************************Module*Header*********************************** +* Module Name: CCommand.h +* +* Module Descripton: +* Resource ID's for the Engine Commands exposed. +* +* Warnings: +* +* Created: 13-Feb-2008 +* +\****************************************************************************/ + +// The following are the valid id's which can be passed to CCalcEngine::ProcessCommand + +#define IDM_HEX 313 +#define IDM_DEC 314 +#define IDM_OCT 315 +#define IDM_BIN 316 +#define IDM_QWORD 317 +#define IDM_DWORD 318 +#define IDM_WORD 319 +#define IDM_BYTE 320 +#define IDM_DEG 321 +#define IDM_RAD 322 +#define IDM_GRAD 323 +#define IDM_DEGREES 324 + +#define IDC_HEX IDM_HEX +#define IDC_DEC IDM_DEC +#define IDC_OCT IDM_OCT +#define IDC_BIN IDM_BIN + +#define IDC_DEG IDM_DEG +#define IDC_RAD IDM_RAD +#define IDC_GRAD IDM_GRAD +#define IDC_DEGREES IDM_DEGREES + +#define IDC_QWORD IDM_QWORD +#define IDC_DWORD IDM_DWORD +#define IDC_WORD IDM_WORD +#define IDC_BYTE IDM_BYTE + + +// Key IDs: +// These id's must be consecutive from IDC_FIRSTCONTROL to IDC_LASTCONTROL. +// The actual values don't matter but the order and sequence are very important. +// Also, the order of the controls must match the order of the control names +// in the string table. +// For example you want to declare the color for the control IDC_ST_AVE +// Find the string id for that control from the rc file +// Now define the control's id as IDC_FRISTCONTROL+stringID(IDC_ST_AVE) +#define IDC_FIRSTCONTROL IDC_SIGN +#define IDC_SIGN 80 +#define IDC_CLEAR 81 +#define IDC_CENTR 82 +#define IDC_BACK 83 + +#define IDC_PNT 84 + +// Hole 85 + +#define IDC_AND 86 // Binary operators must be between IDC_AND and IDC_PWR +#define IDC_OR 87 +#define IDC_XOR 88 +#define IDC_LSHF 89 +#define IDC_RSHF 90 +#define IDC_DIV 91 +#define IDC_MUL 92 +#define IDC_ADD 93 +#define IDC_SUB 94 +#define IDC_MOD 95 +#define IDC_ROOT 96 +#define IDC_PWR 97 + + +#define IDC_UNARYFIRST IDC_CHOP +#define IDC_CHOP 98 // Unary operators must be between IDC_CHOP and IDC_EQU +#define IDC_ROL 99 +#define IDC_ROR 100 +#define IDC_COM 101 +#define IDC_SIN 102 +#define IDC_COS 103 +#define IDC_TAN 104 + +#define IDC_SINH 105 +#define IDC_COSH 106 +#define IDC_TANH 107 + +#define IDC_LN 108 +#define IDC_LOG 109 +#define IDC_SQRT 110 +#define IDC_SQR 111 +#define IDC_CUB 112 +#define IDC_FAC 113 +#define IDC_REC 114 +#define IDC_DMS 115 +#define IDC_CUBEROOT 116 //x ^ 1/3 +#define IDC_POW10 117 // 10 ^ x +#define IDC_PERCENT 118 +#define IDC_UNARYLAST IDC_PERCENT + +#define IDC_FE 119 +#define IDC_PI 120 +#define IDC_EQU 121 + +#define IDC_MCLEAR 122 +#define IDC_RECALL 123 +#define IDC_STORE 124 +#define IDC_MPLUS 125 +#define IDC_MMINUS 126 + +#define IDC_EXP 127 + + +#define IDC_OPENP 128 +#define IDC_CLOSEP 129 + +#define IDC_0 130 // The controls for 0 through F must be consecutive and in order +#define IDC_1 131 +#define IDC_2 132 +#define IDC_3 133 +#define IDC_4 134 +#define IDC_5 135 +#define IDC_6 136 +#define IDC_7 137 +#define IDC_8 138 +#define IDC_9 139 +#define IDC_A 140 +#define IDC_B 141 +#define IDC_C 142 +#define IDC_D 143 +#define IDC_E 144 +#define IDC_F 145 // this is last control ID which must match the string table +#define IDC_INV 146 +#define IDC_SET_RESULT 147 + +#define IDC_LASTCONTROL IDC_SET_RESULT + +#define IDC_BINEDITSTART 700 +#define IDC_BINPOS0 700 +#define IDC_BINPOS1 701 +#define IDC_BINPOS2 702 +#define IDC_BINPOS3 703 +#define IDC_BINPOS4 704 +#define IDC_BINPOS5 705 +#define IDC_BINPOS6 706 +#define IDC_BINPOS7 707 +#define IDC_BINPOS8 708 +#define IDC_BINPOS9 709 +#define IDC_BINPOS10 710 +#define IDC_BINPOS11 711 +#define IDC_BINPOS12 712 +#define IDC_BINPOS13 713 +#define IDC_BINPOS14 714 +#define IDC_BINPOS15 715 +#define IDC_BINPOS16 716 +#define IDC_BINPOS17 717 +#define IDC_BINPOS18 718 +#define IDC_BINPOS19 719 +#define IDC_BINPOS20 720 +#define IDC_BINPOS21 721 +#define IDC_BINPOS22 722 +#define IDC_BINPOS23 723 +#define IDC_BINPOS24 724 +#define IDC_BINPOS25 725 +#define IDC_BINPOS26 726 +#define IDC_BINPOS27 727 +#define IDC_BINPOS28 728 +#define IDC_BINPOS29 729 +#define IDC_BINPOS30 730 +#define IDC_BINPOS31 731 +#define IDC_BINPOS32 732 +#define IDC_BINPOS33 733 +#define IDC_BINPOS34 734 +#define IDC_BINPOS35 735 +#define IDC_BINPOS36 736 +#define IDC_BINPOS37 737 +#define IDC_BINPOS38 738 +#define IDC_BINPOS39 739 +#define IDC_BINPOS40 740 +#define IDC_BINPOS41 741 +#define IDC_BINPOS42 742 +#define IDC_BINPOS43 743 +#define IDC_BINPOS44 744 +#define IDC_BINPOS45 745 +#define IDC_BINPOS46 746 +#define IDC_BINPOS47 747 +#define IDC_BINPOS48 748 +#define IDC_BINPOS49 749 +#define IDC_BINPOS50 750 +#define IDC_BINPOS51 751 +#define IDC_BINPOS52 752 +#define IDC_BINPOS53 753 +#define IDC_BINPOS54 754 +#define IDC_BINPOS55 755 +#define IDC_BINPOS56 756 +#define IDC_BINPOS57 757 +#define IDC_BINPOS58 758 +#define IDC_BINPOS59 759 +#define IDC_BINPOS60 760 +#define IDC_BINPOS61 761 +#define IDC_BINPOS62 762 +#define IDC_BINPOS63 763 +#define IDC_BINEDITEND 763 + + +// The strings in the following range IDS_ENGINESTR_FIRST ... IDS_ENGINESTR_MAX are strings allocated in the +// resource for the purpose internal to Engine and cant be used by the clients +#define IDS_ENGINESTR_FIRST 0 +#define IDS_ENGINESTR_MAX 200 + diff --git a/src/CalcManager/Header Files/CalcEngine.h b/src/CalcManager/Header Files/CalcEngine.h new file mode 100644 index 00000000..7c42b396 --- /dev/null +++ b/src/CalcManager/Header Files/CalcEngine.h @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +/****************************Module*Header***********************************\ +* Module Name: CalcEngine.h +* +* Module Descripton: +* The class definition for the Calculator's engine class CCalcEngine +* +* Warnings: +* +* Created: 17-Jan-2008 +* +\****************************************************************************/ + +#include "scimath.h" +#include "CCommand.h" +#include "EngineStrings.h" +#include "Command.h" +#include "CalculatorVector.h" +#include "ExpressionCommand.h" +#include "History.h" // for History Collector +#include "CalcInput.h" +#include "ICalcDisplay.h" +#include "Rational.h" + +// The following are NOT real exports of CalcEngine, but for forward declarations +// The real exports follows later + +// This is expected to be in same order as IDM_QWORD, IDM_DWORD etc. +enum eNUM_WIDTH { + QWORD_WIDTH, // Number width of 64 bits mode (default) + DWORD_WIDTH, // Number width of 32 bits mode + WORD_WIDTH, // Number width of 16 bits mode + BYTE_WIDTH // Number width of 16 bits mode +}; +typedef enum eNUM_WIDTH NUM_WIDTH; +static constexpr size_t NUM_WIDTH_LENGTH = 4; + +// This is expected to be in same order as IDM_HEX, IDM_DEC, IDM_OCT, IDM_BIN +enum eRADIX_TYPE { + HEX_RADIX, + DEC_RADIX, + OCT_RADIX, + BIN_RADIX +}; +typedef enum eRADIX_TYPE RADIX_TYPE; + +namespace CalculationManager +{ + class IResourceProvider; +} + +namespace CalculatorUnitTests +{ + class CalcEngineTests; +} + +class CCalcEngine { +public: + CCalcEngine(bool fPrecedence, bool fIntegerMode, CalculationManager::IResourceProvider* const pResourceProvider, __in_opt ICalcDisplay *pCalcDisplay, __in_opt std::shared_ptr pHistoryDisplay); + void ProcessCommand(WPARAM wID); + void DisplayError (DWORD nError); + std::unique_ptr PersistedMemObject(); + void PersistedMemObject(CalcEngine::Rational const& memObject); + bool FInErrorState() { return m_bError; } + bool FInRecordingState() { return m_bRecord; } + void SettingsChanged(); + bool IsCurrentTooBigForTrig(); + int GetCurrentRadix(); + std::wstring GetCurrentResultForRadix(uint32_t radix, int32_t precision); + void ChangePrecision(int32_t precision) { m_precision = precision; ChangeConstants(m_radix, precision); } + std::wstring GroupDigitsPerRadix(std::wstring_view numberString, uint32_t radix); + std::wstring GetStringForDisplay(CalcEngine::Rational const& rat, uint32_t radix); + void UpdateMaxIntDigits(); + wchar_t DecimalSeparator() const; + + // Static methods for the instance + static void InitialOneTimeOnlySetup(CalculationManager::IResourceProvider& resourceProvider); // Once per load time to call to intialize all shared global variables + // returns the ptr to string representing the operator. Mostly same as the button, but few special cases for x^y etc. + static std::wstring_view GetString(int ids) { return s_engineStrings[ids]; } + static std::wstring_view OpCodeToString(int nOpCode) { return GetString(IdStrFromCmdId(nOpCode)); } + static std::wstring_view OpCodeToUnaryString(int nOpCode, bool fInv, ANGLE_TYPE angletype); + +private: + bool m_fPrecedence; + bool m_fIntegerMode; /* This is true if engine is explicitly called to be in integer mode. All bases are restricted to be in integers only */ + ICalcDisplay *m_pCalcDisplay; + CalculationManager::IResourceProvider* const m_resourceProvider; + int m_nOpCode; /* ID value of operation. */ + int m_nPrevOpCode; // opcode which computed the number in m_currentVal. 0 if it is already bracketed or plain number or + // if it hasnt yet been computed + bool m_bChangeOp; /* Flag for changing operation. */ + bool m_bRecord; // Global mode: recording or displaying + bool m_bSetCalcState; //Falg for setting teh engine result state + CalcEngine::CalcInput m_input; // Global calc input object for decimal strings + eNUMOBJ_FMT m_nFE; /* Scientific notation conversion flag. */ + CalcEngine::Rational m_maxTrigonometricNum; + std::unique_ptr m_memoryValue; // Current memory value. + + CalcEngine::Rational m_holdVal; // For holding the second operand in repetitive calculations ( pressing "=" continuously) + + CalcEngine::Rational m_currentVal; // Currently displayed number used everywhere. + CalcEngine::Rational m_lastVal; // Number before operation (left operand). + std::array m_parenVals; // Holding array for parenthesis values. + std::array m_precedenceVals; // Holding array for precedence values. + bool m_bError; // Error flag. + bool m_bInv; // Inverse on/off flag. + bool m_bNoPrevEqu; /* Flag for previous equals. */ + + uint32_t m_radix; + int32_t m_precision; + int m_cIntDigitsSav; + std::vector m_decGrouping; // Holds the decimal digit grouping number + + std::wstring m_numberString; + + int m_nTempCom; /* Holding place for the last command. */ + int m_openParenCount; // Number of open parentheses. + std::array m_nOp; /* Holding array for parenthesis operations. */ + std::array m_nPrecOp; /* Holding array for precedence operations. */ + int m_nPrecNum; /* Current number of precedence ops in holding. */ + int m_nLastCom; // Last command entered. + ANGLE_TYPE m_angletype; // Current Angle type when in dec mode. one of deg, rad or grad + NUM_WIDTH m_numwidth; // one of qword, dword, word or byte mode. + LONG m_dwWordBitWidth; // # of bits in currently selected word size + + CHistoryCollector m_HistoryCollector; // Accumulator of each line of history as various commands are processed + + std::array m_chopNumbers; // word size enforcement + std::array m_maxDecimalValueStrings; // maximum values represented by a given word width based off m_chopNumbers + static std::array s_engineStrings; // the string table shared across all instances + wchar_t m_decimalSeparator; + wchar_t m_groupSeparator; + +private: + void ProcessCommandWorker(WPARAM wParam); + void HandleErrorCommand(WPARAM idc); + void HandleMaxDigitsReached(); + void DisplayNum(void); + int IsNumberInvalid(const std::wstring& numberString, int iMaxExp, int iMaxMantissa, uint32_t radix) const; + void DisplayAnnounceBinaryOperator(); + void SetPrimaryDisplay(const std::wstring& szText, bool isError = false); + void ClearTemporaryValues(); + CalcEngine::Rational TruncateNumForIntMath(CalcEngine::Rational const& rat); + CalcEngine::Rational SciCalcFunctions(CalcEngine::Rational const& rat, DWORD op); + CalcEngine::Rational DoOperation(int operation, CalcEngine::Rational const& lhs, CalcEngine::Rational const& rhs); + void SetRadixTypeAndNumWidth(RADIX_TYPE radixtype, NUM_WIDTH numwidth); + LONG DwWordBitWidthFromeNumWidth(NUM_WIDTH numwidth); + uint32_t NRadixFromRadixType( RADIX_TYPE radixtype); + + bool TryToggleBit(CalcEngine::Rational& rat, DWORD wbitno); + void CheckAndAddLastBinOpToHistory(bool addToHistory = true); + int IdcSetAngleTypeDecMode(int idc); + + void InitChopNumbers(); + + static void LoadEngineStrings(CalculationManager::IResourceProvider& resourceProvider); + static int IdStrFromCmdId(int id) { return id - IDC_FIRSTCONTROL + IDS_FIRSTENGSTR; } + + static std::vector DigitGroupingStringToGroupingVector(std::wstring_view groupingString); + std::wstring GroupDigits(std::wstring_view delimiter, std::vector const& grouping, std::wstring_view displayString, bool isNumNegative = false); + + static int QuickLog2(int iNum); + static void ChangeBaseConstants(uint32_t radix, int maxIntDigits, int32_t precision); + void BaseOrPrecisionChanged(); + + friend class CalculatorUnitTests::CalcEngineTests; +}; diff --git a/src/CalcManager/Header Files/CalcInput.h b/src/CalcManager/Header Files/CalcInput.h new file mode 100644 index 00000000..22b2b563 --- /dev/null +++ b/src/CalcManager/Header Files/CalcInput.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Rational.h" + +// Space to hold enough digits for a quadword binary number (64) plus digit separator strings for that number (20) +constexpr int MAX_STRLEN = 84; + +namespace CalcEngine +{ + class CalcNumSec + { + public: + CalcNumSec() : + m_isNegative(false), + value() + {} + + void Clear(); + bool IsEmpty() { return value.empty(); } + + bool IsNegative() { return m_isNegative; } + void IsNegative(bool value) { m_isNegative = value; } + + std::wstring value; + + private: + bool m_isNegative; + }; + + class CalcInput + { + public: + CalcInput() : CalcInput(L'.') + {} + + CalcInput(wchar_t decSymbol) : + m_base(), + m_exponent(), + m_hasExponent(false), + m_hasDecimal(false), + m_decPtIndex(0), + m_decSymbol(decSymbol) + {} + + void Clear(); + bool TryToggleSign(bool isIntegerMode, std::wstring_view maxNumStr); + bool TryAddDigit(unsigned int value, uint32_t radix, bool isIntegerMode, std::wstring_view maxNumStr, long wordBitWidth, int maxDigits); + bool TryAddDecimalPt(); + bool HasDecimalPt(); + bool TryBeginExponent(); + void Backspace(); + void SetDecimalSymbol(wchar_t decSymbol); + std::wstring ToString(uint32_t radix, bool isIntegerMode); + Rational ToRational(uint32_t radix, int32_t precision); + + private: + bool m_hasExponent; + bool m_hasDecimal; + size_t m_decPtIndex; + wchar_t m_decSymbol; + CalcNumSec m_base; + CalcNumSec m_exponent; + }; +} diff --git a/src/CalcManager/Header Files/CalcUtils.h b/src/CalcManager/Header Files/CalcUtils.h new file mode 100644 index 00000000..b6524a28 --- /dev/null +++ b/src/CalcManager/Header Files/CalcUtils.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +bool IsOpInRange(WPARAM op, uint32_t x, uint32_t y); +bool IsBinOpCode(WPARAM opCode); + +// WARNING: IDC_SIGN is a special unary op but still this doesnt catch this. Caller has to be aware +// of it and catch it themself or not needing this +bool IsUnaryOpCode(WPARAM opCode); +bool IsDigitOpCode(WPARAM opCode); +bool IsGuiSettingOpCode(WPARAM opCode); diff --git a/src/CalcManager/Header Files/EngineStrings.h b/src/CalcManager/Header Files/EngineStrings.h new file mode 100644 index 00000000..0f40533a --- /dev/null +++ b/src/CalcManager/Header Files/EngineStrings.h @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/****************************Module*Header*********************************** +* Module Name: EngineStrings.h +* +* Module Descripton: +* Resource String ID's for the private strings used by Engine. Internal to Engine related code +* not required by the clients +* +* Warnings: +* +* Created: 13-Feb-2008 +* +\****************************************************************************/ +#define IDS_FIRSTENGSTR IDS_ENGINESTR_FIRST + +#define IDS_DECIMAL 4 + +// All unary op function names for easy history reading +// This is where the first string after all the commands in order have been placed, should be placed +// keeping in consecutive helps us to allocate 1 string table and index them +#define IDS_FNSZFIRST (IDC_F -IDC_FIRSTCONTROL)+1 + +#define IDS_FRAC IDS_FNSZFIRST + +#define IDS_SIND IDS_FNSZFIRST+1 +#define IDS_COSD IDS_FNSZFIRST+2 +#define IDS_TAND IDS_FNSZFIRST+3 +#define IDS_ASIND IDS_FNSZFIRST+4 +#define IDS_ACOSD IDS_FNSZFIRST+5 +#define IDS_ATAND IDS_FNSZFIRST+6 + +#define IDS_SINR IDS_FNSZFIRST+7 +#define IDS_COSR IDS_FNSZFIRST+8 +#define IDS_TANR IDS_FNSZFIRST+9 +#define IDS_ASINR IDS_FNSZFIRST+10 +#define IDS_ACOSR IDS_FNSZFIRST+11 +#define IDS_ATANR IDS_FNSZFIRST+12 + +#define IDS_SING IDS_FNSZFIRST+13 +#define IDS_COSG IDS_FNSZFIRST+14 +#define IDS_TANG IDS_FNSZFIRST+15 +#define IDS_ASING IDS_FNSZFIRST+16 +#define IDS_ACOSG IDS_FNSZFIRST+17 +#define IDS_ATANG IDS_FNSZFIRST+18 + +#define IDS_ASINH IDS_FNSZFIRST+19 +#define IDS_ACOSH IDS_FNSZFIRST+20 +#define IDS_ATANH IDS_FNSZFIRST+21 +#define IDS_POWE IDS_FNSZFIRST+22 +#define IDS_POW10 IDS_FNSZFIRST+23 +#define IDS_SQRT IDS_FNSZFIRST+24 +#define IDS_SQR IDS_FNSZFIRST+25 +#define IDS_CUBE IDS_FNSZFIRST+26 +#define IDS_CUBERT IDS_FNSZFIRST+27 +#define IDS_FACT IDS_FNSZFIRST+28 +#define IDS_REC IDS_FNSZFIRST+29 +#define IDS_DEGREES IDS_FNSZFIRST+30 +#define IDS_NEGATE IDS_FNSZFIRST+31 +#define IDS_RSH IDS_FNSZFIRST+32 + +#define IDS_FNSZLAST IDS_RSH + +#define IDS_ERRORS_FIRST IDS_FNSZLAST+1 + +// This is the list of error strings corresponding to SCERR_DIVIDEZERO.. + +#define IDS_DIVBYZERO IDS_ERRORS_FIRST +#define IDS_DOMAIN IDS_ERRORS_FIRST+1 +#define IDS_UNDEFINED IDS_ERRORS_FIRST+2 +#define IDS_POS_INFINITY IDS_ERRORS_FIRST+3 +#define IDS_NEG_INFINITY IDS_ERRORS_FIRST+4 +#define IDS_NOMEM IDS_ERRORS_FIRST+6 +#define IDS_TOOMANY IDS_ERRORS_FIRST+7 +#define IDS_OVERFLOW IDS_ERRORS_FIRST+8 +#define IDS_NORESULT IDS_ERRORS_FIRST+9 +#define IDS_INSUFFICIENT_DATA IDS_ERRORS_FIRST+10 + +#define CSTRINGSENGMAX IDS_INSUFFICIENT_DATA+1 + +// Arithmetic expression evaluator error strings +#define IDS_ERR_UNK_CH CSTRINGSENGMAX+1 +#define IDS_ERR_UNK_FN CSTRINGSENGMAX+2 +#define IDS_ERR_UNEX_NUM CSTRINGSENGMAX+3 +#define IDS_ERR_UNEX_CH CSTRINGSENGMAX+4 +#define IDS_ERR_UNEX_SZ CSTRINGSENGMAX+5 +#define IDS_ERR_MISMATCH_CLOSE CSTRINGSENGMAX+6 +#define IDS_ERR_UNEX_END CSTRINGSENGMAX+7 +#define IDS_ERR_SG_INV_ERROR CSTRINGSENGMAX+8 +#define IDS_ERR_INPUT_OVERFLOW CSTRINGSENGMAX+9 +#define IDS_ERR_OUTPUT_OVERFLOW CSTRINGSENGMAX+10 + + +#define SIDS_PLUS_MINUS L"0" +#define SIDS_CLEAR L"1" +#define SIDS_CE L"2" +#define SIDS_BACKSPACE L"3" +#define SIDS_DECIMAL_SEPARATOR L"4" +#define SIDS_EMPTY_STRING L"5" +#define SIDS_AND L"6" +#define SIDS_OR L"7" +#define SIDS_XOR L"8" +#define SIDS_LSH L"9" +#define SIDS_RSH L"10" +#define SIDS_DIVIDE L"11" +#define SIDS_MULTIPLY L"12" +#define SIDS_PLUS L"13" +#define SIDS_MINUS L"14" +#define SIDS_MOD L"15" +#define SIDS_YROOT L"16" +#define SIDS_POW_HAT L"17" +#define SIDS_INT L"18" +#define SIDS_ROL L"19" +#define SIDS_ROR L"20" +#define SIDS_NOT L"21" +#define SIDS_SIN L"22" +#define SIDS_COS L"23" +#define SIDS_TAN L"24" +#define SIDS_SINH L"25" +#define SIDS_COSH L"26" +#define SIDS_TANH L"27" +#define SIDS_LN L"28" +#define SIDS_LOG L"29" +#define SIDS_SQRT L"30" +#define SIDS_XPOW2 L"31" +#define SIDS_XPOW3 L"32" +#define SIDS_NFACTORIAL L"33" +#define SIDS_RECIPROCAL L"34" +#define SIDS_DMS L"35" +#define SIDS_CUBEROOT L"36" +#define SIDS_POWTEN L"37" +#define SIDS_PERCENT L"38" +#define SIDS_SCIENTIFIC_NOTATION L"39" +#define SIDS_PI L"40" +#define SIDS_EQUAL L"41" +#define SIDS_MC L"42" +#define SIDS_MR L"43" +#define SIDS_MS L"44" +#define SIDS_MPLUS L"45" +#define SIDS_MMINUS L"46" +#define SIDS_EXP L"47" +#define SIDS_OPEN_PAREN L"48" +#define SIDS_CLOSE_PAREN L"49" +#define SIDS_0 L"50" +#define SIDS_1 L"51" +#define SIDS_2 L"52" +#define SIDS_3 L"53" +#define SIDS_4 L"54" +#define SIDS_5 L"55" +#define SIDS_6 L"56" +#define SIDS_7 L"57" +#define SIDS_8 L"58" +#define SIDS_9 L"59" +#define SIDS_A L"60" +#define SIDS_B L"61" +#define SIDS_C L"62" +#define SIDS_D L"63" +#define SIDS_E L"64" +#define SIDS_F L"65" +#define SIDS_FRAC L"66" +#define SIDS_SIND L"67" +#define SIDS_COSD L"68" +#define SIDS_TAND L"69" +#define SIDS_ASIND L"70" +#define SIDS_ACOSD L"71" +#define SIDS_ATAND L"72" +#define SIDS_SINR L"73" +#define SIDS_COSR L"74" +#define SIDS_TANR L"75" +#define SIDS_ASINR L"76" +#define SIDS_ACOSR L"77" +#define SIDS_ATANR L"78" +#define SIDS_SING L"79" +#define SIDS_COSG L"80" +#define SIDS_TANG L"81" +#define SIDS_ASING L"82" +#define SIDS_ACOSG L"83" +#define SIDS_ATANG L"84" +#define SIDS_ASINH L"85" +#define SIDS_ACOSH L"86" +#define SIDS_ATANH L"87" +#define SIDS_POWE L"88" +#define SIDS_POWTEN2 L"89" +#define SIDS_SQRT2 L"90" +#define SIDS_SQR L"91" +#define SIDS_CUBE L"92" +#define SIDS_CUBERT L"93" +#define SIDS_FACT L"94" +#define SIDS_RECIPROC L"95" +#define SIDS_DEGREES L"96" +#define SIDS_NEGATE L"97" +#define SIDS_RSH2 L"98" +#define SIDS_DIVIDEBYZERO L"99" +#define SIDS_DOMAIN L"100" +#define SIDS_UNDEFINED L"101" +#define SIDS_POS_INFINITY L"102" +#define SIDS_NEG_INFINITY L"103" +#define SIDS_ABORTED L"104" +#define SIDS_NOMEM L"105" +#define SIDS_TOOMANY L"106" +#define SIDS_OVERFLOW L"107" +#define SIDS_NORESULT L"108" +#define SIDS_INSUFFICIENT_DATA L"109" +// 110 is skipped by CSTRINGSENGMAX +#define SIDS_ERR_UNK_CH L"111" +#define SIDS_ERR_UNK_FN L"112" +#define SIDS_ERR_UNEX_NUM L"113" +#define SIDS_ERR_UNEX_CH L"114" +#define SIDS_ERR_UNEX_SZ L"115" +#define SIDS_ERR_MISMATCH_CLOSE L"116" +#define SIDS_ERR_UNEX_END L"117" +#define SIDS_ERR_SG_INV_ERROR L"118" +#define SIDS_ERR_INPUT_OVERFLOW L"119" +#define SIDS_ERR_OUTPUT_OVERFLOW L"120" + +__declspec(selectany) std::wstring g_sids[] = +{ + std::wstring(SIDS_PLUS_MINUS), + std::wstring(SIDS_C), + std::wstring(SIDS_CE), + std::wstring(SIDS_BACKSPACE), + std::wstring(SIDS_DECIMAL_SEPARATOR), + std::wstring(SIDS_EMPTY_STRING), + std::wstring(SIDS_AND), + std::wstring(SIDS_OR), + std::wstring(SIDS_XOR), + std::wstring(SIDS_LSH), + std::wstring(SIDS_RSH), + std::wstring(SIDS_DIVIDE), + std::wstring(SIDS_MULTIPLY), + std::wstring(SIDS_PLUS), + std::wstring(SIDS_MINUS), + std::wstring(SIDS_MOD), + std::wstring(SIDS_YROOT), + std::wstring(SIDS_POW_HAT), + std::wstring(SIDS_INT), + std::wstring(SIDS_ROL), + std::wstring(SIDS_ROR), + std::wstring(SIDS_NOT), + std::wstring(SIDS_SIN), + std::wstring(SIDS_COS), + std::wstring(SIDS_TAN), + std::wstring(SIDS_SINH), + std::wstring(SIDS_COSH), + std::wstring(SIDS_TANH), + std::wstring(SIDS_LN), + std::wstring(SIDS_LOG), + std::wstring(SIDS_SQRT), + std::wstring(SIDS_XPOW2), + std::wstring(SIDS_XPOW3), + std::wstring(SIDS_NFACTORIAL), + std::wstring(SIDS_RECIPROCAL), + std::wstring(SIDS_DMS), + std::wstring(SIDS_CUBEROOT), + std::wstring(SIDS_POWTEN), + std::wstring(SIDS_PERCENT), + std::wstring(SIDS_SCIENTIFIC_NOTATION), + std::wstring(SIDS_PI), + std::wstring(SIDS_EQUAL), + std::wstring(SIDS_MC), + std::wstring(SIDS_MR), + std::wstring(SIDS_MS), + std::wstring(SIDS_MPLUS), + std::wstring(SIDS_MMINUS), + std::wstring(SIDS_EXP), + std::wstring(SIDS_OPEN_PAREN), + std::wstring(SIDS_CLOSE_PAREN), + std::wstring(SIDS_0), + std::wstring(SIDS_1), + std::wstring(SIDS_2), + std::wstring(SIDS_3), + std::wstring(SIDS_4), + std::wstring(SIDS_5), + std::wstring(SIDS_6), + std::wstring(SIDS_7), + std::wstring(SIDS_8), + std::wstring(SIDS_9), + std::wstring(SIDS_A), + std::wstring(SIDS_B), + std::wstring(SIDS_C), + std::wstring(SIDS_D), + std::wstring(SIDS_E), + std::wstring(SIDS_F), + std::wstring(SIDS_FRAC), + std::wstring(SIDS_SIND), + std::wstring(SIDS_COSD), + std::wstring(SIDS_TAND), + std::wstring(SIDS_ASIND), + std::wstring(SIDS_ACOSD), + std::wstring(SIDS_ATAND), + std::wstring(SIDS_SINR), + std::wstring(SIDS_COSR), + std::wstring(SIDS_TANR), + std::wstring(SIDS_ASINR), + std::wstring(SIDS_ACOSR), + std::wstring(SIDS_ATANR), + std::wstring(SIDS_SING), + std::wstring(SIDS_COSG), + std::wstring(SIDS_TANG), + std::wstring(SIDS_ASING), + std::wstring(SIDS_ACOSG), + std::wstring(SIDS_ATANG), + std::wstring(SIDS_ASINH), + std::wstring(SIDS_ACOSH), + std::wstring(SIDS_ATANH), + std::wstring(SIDS_POWE), + std::wstring(SIDS_POWTEN2), + std::wstring(SIDS_SQRT2), + std::wstring(SIDS_SQR), + std::wstring(SIDS_CUBE), + std::wstring(SIDS_CUBERT), + std::wstring(SIDS_FACT), + std::wstring(SIDS_RECIPROC), + std::wstring(SIDS_DEGREES), + std::wstring(SIDS_NEGATE), + std::wstring(SIDS_RSH), + std::wstring(SIDS_DIVIDEBYZERO), + std::wstring(SIDS_DOMAIN), + std::wstring(SIDS_UNDEFINED), + std::wstring(SIDS_POS_INFINITY), + std::wstring(SIDS_NEG_INFINITY), + std::wstring(SIDS_ABORTED), + std::wstring(SIDS_NOMEM), + std::wstring(SIDS_TOOMANY), + std::wstring(SIDS_OVERFLOW), + std::wstring(SIDS_NORESULT), + std::wstring(SIDS_INSUFFICIENT_DATA), + std::wstring(SIDS_ERR_UNK_CH), + std::wstring(SIDS_ERR_UNK_FN), + std::wstring(SIDS_ERR_UNEX_NUM), + std::wstring(SIDS_ERR_UNEX_CH), + std::wstring(SIDS_ERR_UNEX_SZ), + std::wstring(SIDS_ERR_MISMATCH_CLOSE), + std::wstring(SIDS_ERR_UNEX_END), + std::wstring(SIDS_ERR_SG_INV_ERROR), + std::wstring(SIDS_ERR_INPUT_OVERFLOW), + std::wstring(SIDS_ERR_OUTPUT_OVERFLOW) +}; diff --git a/src/CalcManager/Header Files/History.h b/src/CalcManager/Header Files/History.h new file mode 100644 index 00000000..f885067f --- /dev/null +++ b/src/CalcManager/Header Files/History.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "ICalcDisplay.h" +#include "IHistoryDisplay.h" + +// maximum depth you can get by precedence. It is just an array's size limit. +static constexpr size_t MAXPRECDEPTH = 25; + +// Helper class really a internal class to CCalcEngine, to accumulate each history line of text by collecting the +// operands, operator, unary operator etc. Since it is a seperate entity, it can be unit tested on its own but does +// rely on CCalcEngine calling it in appropriate order. +class CHistoryCollector { +public: + CHistoryCollector(ICalcDisplay *pCalcDisplay, std::shared_ptr pHistoryDisplay, wchar_t decimalSymbol); // Can throw errors + ~CHistoryCollector(); + void AddOpndToHistory(std::wstring_view numStr, PRAT hNoNum, bool fRepetition = false); + void RemoveLastOpndFromHistory(); + void AddBinOpToHistory(int nOpCode, bool fNoRepetition = true); + void ChangeLastBinOp(int nOpCode, bool fPrecInvToHigher); + void AddUnaryOpToHistory(int nOpCode, bool fInv, ANGLE_TYPE angletype); + void AddOpenBraceToHistory(); + void AddCloseBraceToHistory(); + void PushLastOpndStart(int ichOpndStart = -1); + void PopLastOpndStart(); + void EnclosePrecInvertionBrackets(); + bool FOpndAddedToHistory(); + void CompleteHistoryLine(std::wstring_view numStr); + void ClearHistoryLine(std::wstring_view errStr); + int AddCommand(_In_ const std::shared_ptr & spCommand); + void UpdateHistoryExpression(uint32_t radix, int32_t precision); + void SetDecimalSymbol(wchar_t decimalSymbol); + +private: + std::shared_ptr m_pHistoryDisplay; + ICalcDisplay *m_pCalcDisplay; + + int m_iCurLineHistStart; // index of the begginning of the current equation + // a sort of state, set to the index before 2 after 2 in the expression 2 + 3 say. Useful for auto correct portion of history and for + // attaching the unary op around the last operand + int m_lastOpStartIndex; // index of the beginning of the last operand added to the history + int m_lastBinOpStartIndex; // index of the beginning of the last binary operator added to the history + std::array m_operandIndices; // Stack of index of opnd's beginning for each '('. A parallel array to m_hnoParNum, but abstracted independently of that + int m_curOperandIndex; // Stack index for the above stack + bool m_bLastOpndBrace; // iff the last opnd in history is already braced so we can avoid putting another one for unary operator + wchar_t m_decimalSymbol; + std::shared_ptr>> m_spTokens; + std::shared_ptr>> m_spCommands; + +private: + void ReinitHistory(); + int IchAddSzToEquationSz(std::wstring_view str, int icommandIndex); + void TruncateEquationSzFromIch(int ich); + void SetExpressionDisplay(); + void InsertSzInEquationSz(std::wstring_view str, int icommandIndex, int ich); + std::shared_ptr> GetOperandCommandsFromString(std::wstring_view numStr); +}; diff --git a/src/CalcManager/Header Files/ICalcDisplay.h b/src/CalcManager/Header Files/ICalcDisplay.h new file mode 100644 index 00000000..cc420a4f --- /dev/null +++ b/src/CalcManager/Header Files/ICalcDisplay.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Callback interface to be implemented by the clients of CCalcEngine +class ICalcDisplay { +public: + virtual void SetPrimaryDisplay(const std::wstring& pszText, bool isError) = 0; + virtual void SetIsInError(bool isInError) = 0; + virtual void SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands) = 0; + virtual void SetParenDisplayText(const std::wstring& pszText) = 0; + virtual void MaxDigitsReached() = 0; // not an error but still need to inform UI layer. + virtual void BinaryOperatorReceived() = 0; + virtual void OnHistoryItemAdded(_In_ unsigned int addedItemIndex) = 0; + virtual void SetMemorizedNumbers(const std::vector& memorizedNumbers) = 0; + virtual void MemoryItemChanged(unsigned int indexOfMemory) = 0; +}; diff --git a/src/CalcManager/Header Files/IHistoryDisplay.h b/src/CalcManager/Header Files/IHistoryDisplay.h new file mode 100644 index 00000000..bbadbe87 --- /dev/null +++ b/src/CalcManager/Header Files/IHistoryDisplay.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Callback interface to be implemented by the clients of CCalcEngine if they require equation history +class IHistoryDisplay { +public: + virtual ~IHistoryDisplay() {}; + virtual unsigned int AddToHistory(_In_ std::shared_ptr>> const &tokens, _In_ std::shared_ptr>> const &commands, _In_ std::wstring_view result) = 0; +}; diff --git a/src/CalcManager/Header Files/Number.h b/src/CalcManager/Header Files/Number.h new file mode 100644 index 00000000..061d6190 --- /dev/null +++ b/src/CalcManager/Header Files/Number.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#pragma once + +#include "RatPack/ratpak.h" + +namespace CalcEngine +{ + class Number + { + public: + Number() noexcept; + Number(int32_t sign, int32_t exp, std::vector const& mantissa) noexcept; + + explicit Number(PNUMBER p) noexcept; + PNUMBER ToPNUMBER() const; + + int32_t const& Sign() const; + int32_t const& Exp() const; + std::vector const& Mantissa() const; + + bool IsZero() const; + + private: + int32_t m_sign; + int32_t m_exp; + std::vector m_mantissa; + }; +} diff --git a/src/CalcManager/Header Files/Rational.h b/src/CalcManager/Header Files/Rational.h new file mode 100644 index 00000000..b447aaea --- /dev/null +++ b/src/CalcManager/Header Files/Rational.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#pragma once + +#include "Number.h" + +namespace CalcEngine +{ + class Rational + { + public: + Rational() noexcept; + Rational(Number const& n) noexcept; + Rational(Number const& p, Number const& q) noexcept; + + explicit Rational(PRAT prat) noexcept; + PRAT ToPRAT() const; + + Number const& P() const; + Number const& Q() const; + + bool IsZero() const; + + private: + Number m_p; + Number m_q; + }; +} diff --git a/src/CalcManager/Header Files/scimath.h b/src/CalcManager/Header Files/scimath.h new file mode 100644 index 00000000..300e36bd --- /dev/null +++ b/src/CalcManager/Header Files/scimath.h @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/**************************************************************************\ +* * +* * +* * +* # # ##### * +* # # # # # * +* # # # # # # # * +* # ### ### # # * +* # # ### # # # ### # # ### ##### # ### ### ### * +* # ## # # # ## # # # # # # ## # # # * +* # # # # # # # # # ##### # # ##### # * +* # # # # # # # # # # # # # # ## * +* # # # # # # # # # ### # # ### ### ## * +* * +* * +* Infinte Precision Production Version * +* * +\**************************************************************************/ +// +// RETAIL version of NUMOBJ math that uses Infinite Precision +// +#include "Ratpack/ratpak.h" + +// +// Unary functions +// +void NumObjInvert(PRAT *phno, int32_t precision); +void NumObjNegate(PRAT *phno); +void NumObjAbs(PRAT *phno); + +void NumObjSin(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); +void NumObjCos(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); +void NumObjTan(PRAT *phno, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); +void NumObjAntiLog10(PRAT *phno, uint32_t radix, int32_t precision); + +void NumObjNot(PRAT *phno, bool fIntegerMode, PRAT chopNum, uint32_t radix, int32_t precision); + +// +// Comparison functions +// +bool NumObjIsZero(PRAT hno); +bool NumObjIsLess(PRAT hno1, PRAT hno2, int32_t precision); +bool NumObjIsLessEq(PRAT hno1, PRAT hno2, int32_t precision); +bool NumObjIsGreaterEq(PRAT hno1, PRAT hno2, int32_t precision); +bool NumObjIsEq(PRAT hno1, PRAT hno2, int32_t precision); + +// +// Assignment operator. ('=' in C language) +// +void NumObjAssign(PRAT *phnol, PRAT hnor); + +// +// Data type conversion functions +// +void NumObjSetIntValue(PRAT *phnol, LONG i ); +void NumObjSetUlonglongValue(PRAT *phnol, ULONGLONG ul, uint32_t radix, int32_t precision); + +ULONGLONG NumObjGetUlValue( PRAT hnol, uint32_t radix, int32_t precision); + +// Returns a string representation of hnoNum +std::wstring NumObjToString(PRAT hnoNum, uint32_t radix, NUMOBJ_FMT fmt, int32_t precision); + +// +// NumObjGetExp +// +// returns an int that equals the exponent of the NumObj +// +int32_t NumObjGetExp(PRAT hno); + +// +// NumObjDestroy( hno ) +// +// call this when you nolonger need the NumObj. Failure to do so +// will result in memory leaks. +// +void NumObjDestroy(PRAT *phno); diff --git a/src/CalcManager/Ratpack/CalcErr.h b/src/CalcManager/Ratpack/CalcErr.h new file mode 100644 index 00000000..7e9025a4 --- /dev/null +++ b/src/CalcManager/Ratpack/CalcErr.h @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// CalcErr.h +// +// Defines the error codes thrown by ratpak and caught by Calculator +// +// +// Ratpak errors are 32 bit values layed out as follows: +// +// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 +// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 +// +-+-------+---------------------+-------------------------------+ +// |S| R | Facility | Code | +// +-+-------+---------------------+-------------------------------+ +// +// where +// +// S - Severity - indicates success/fail +// +// 0 - Success +// 1 - Fail +// +// R - Reserved - not currently used for anything +// +// r - reserved portion of the facility code. Reserved for internal +// use. Used to indicate HRESULT values that are not status +// values, but are instead message ids for display strings. +// +// Facility - is the facility code +// +// Code - is the actual error code +// +// This format is based losely on an OLE HRESULT and is compatible with the +// SUCCEEDED and FAILED marcos as well as the HRESULT_CODE macro + +// CALC_E_DIVIDEBYZERO +// +// The current operation would require a divide by zero to complete +#define CALC_E_DIVIDEBYZERO ((DWORD)0x80000000) + +// CALC_E_DOMAIN +// +// The given input is not within the domain of this function +#define CALC_E_DOMAIN ((DWORD)0x80000001) + +// CALC_E_INDEFINITE +// +// The result of this function is undefined +#define CALC_E_INDEFINITE ((DWORD)0x80000002) + +// CALC_E_POSINFINITY +// +// The result of this function is Positive Infinity. +#define CALC_E_POSINFINITY ((DWORD)0x80000003) + +// CALC_E_NEGINFINITY +// +// The result of this function is Negative Infinity +#define CALC_E_NEGINFINITY ((DWORD)0x80000004) + +// CALC_E_INVALIDRANGE +// +// The given input is within the domain of the function but is beyond +// the range for which calc can successfully compute the answer +#define CALC_E_INVALIDRANGE ((DWORD)0x80000006) + +// CALC_E_OUTOFMEMORY +// +// There is not enough free memory to complete the requested function +#define CALC_E_OUTOFMEMORY ((DWORD)0x80000007) + +// CALC_E_OVERFLOW +// +// The result of this operation is an overflow +#define CALC_E_OVERFLOW ((DWORD)0x80000008) + +// CALC_E_NORESULT +// +// The result of this operation is undefined +#define CALC_E_NORESULT ((DWORD)0x80000009) + diff --git a/src/CalcManager/Ratpack/basex.cpp b/src/CalcManager/Ratpack/basex.cpp new file mode 100644 index 00000000..4fc77212 --- /dev/null +++ b/src/CalcManager/Ratpack/basex.cpp @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File basex.c +// Copyright (C) 1995-97 Microsoft +// Date 03-14-97 +// +// +// Description +// +// Contains number routines for internal base computations, these assume +// internal base is a power of 2. +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + +void _mulnumx( PNUMBER *pa, PNUMBER b ); + +//---------------------------------------------------------------------------- +// +// FUNCTION: mulnumx +// +// ARGUMENTS: pointer to a number and a second number, the +// base is always BASEX. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa *= b. +// This is a stub which prevents multiplication by 1, this is a big speed +// improvement. +// +//---------------------------------------------------------------------------- + +void __inline mulnumx( PNUMBER *pa, PNUMBER b ) + +{ + if ( b->cdigit > 1 || b->mant[0] != 1 || b->exp != 0 ) + { + // If b is not one we multiply + if ( (*pa)->cdigit > 1 || (*pa)->mant[0] != 1 || (*pa)->exp != 0 ) + { + // pa and b are both nonone. + _mulnumx( pa, b ); + } + else + { + // if pa is one and b isn't just copy b. and adjust the sign. + long sign = (*pa)->sign; + DUPNUM(*pa,b); + (*pa)->sign *= sign; + } + } + else + { + // B is +/- 1, But we do have to set the sign. + (*pa)->sign *= b->sign; + } +} + +//---------------------------------------------------------------------------- +// +// FUNCTION: _mulnumx +// +// ARGUMENTS: pointer to a number and a second number, the +// base is always BASEX. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa *= b. +// Assumes the base is BASEX of both numbers. This algorithm is the +// same one you learned in gradeschool, except the base isn't 10 it's +// BASEX. +// +//---------------------------------------------------------------------------- + +void _mulnumx( PNUMBER *pa, PNUMBER b ) + +{ + PNUMBER c= nullptr; // c will contain the result. + PNUMBER a= nullptr; // a is the dereferenced number pointer from *pa + MANTTYPE *ptra; // ptra is a pointer to the mantissa of a. + MANTTYPE *ptrb; // ptrb is a pointer to the mantissa of b. + MANTTYPE *ptrc; // ptrc is a pointer to the mantissa of c. + MANTTYPE *ptrcoffset; // ptrcoffset, is the anchor location of the next + // single digit multiply partial result. + long iadigit=0; // Index of digit being used in the first number. + long ibdigit=0; // Index of digit being used in the second number. + MANTTYPE da=0; // da is the digit from the fist number. + TWO_MANTTYPE cy=0; // cy is the carry resulting from the addition of + // a multiplied row into the result. + TWO_MANTTYPE mcy=0; // mcy is the resultant from a single + // multiply, AND the carry of that multiply. + long icdigit=0; // Index of digit being calculated in final result. + + a=*pa; + + ibdigit = a->cdigit + b->cdigit - 1; + createnum( c, ibdigit + 1 ); + c->cdigit = ibdigit; + c->sign = a->sign * b->sign; + + c->exp = a->exp + b->exp; + ptra = a->mant; + ptrcoffset = c->mant; + + for ( iadigit = a->cdigit; iadigit > 0; iadigit-- ) + { + da = *ptra++; + ptrb = b->mant; + + // Shift ptrc, and ptrcoffset, one for each digit + ptrc = ptrcoffset++; + + for ( ibdigit = b->cdigit; ibdigit > 0; ibdigit-- ) + { + cy = 0; + mcy = (DWORDLONG)da * (*ptrb); + if ( mcy ) + { + icdigit = 0; + if ( ibdigit == 1 && iadigit == 1 ) + { + c->cdigit++; + } + } + + // If result is nonzero, or while result of carry is nonzero... + while ( mcy || cy ) + { + + // update carry from addition(s) and multiply. + cy += (TWO_MANTTYPE)ptrc[icdigit]+((DWORD)mcy&((DWORD)~BASEX)); + + // update result digit from + ptrc[icdigit++]=(MANTTYPE)((DWORD)cy&((DWORD)~BASEX)); + + // update carries from + mcy >>= BASEXPWR; + cy >>= BASEXPWR; + } + + ptrb++; + ptrc++; + + } + } + + // prevent different kinds of zeros, by stripping leading duplicate zeroes. + // digits are in order of increasing significance. + while ( c->cdigit > 1 && c->mant[c->cdigit-1] == 0 ) + { + c->cdigit--; + } + + destroynum( *pa ); + *pa=c; +} +//----------------------------------------------------------------------------- +// +// FUNCTION: numpowlongx +// +// ARGUMENTS: root as number power as long +// number. +// +// RETURN: None root is changed. +// +// DESCRIPTION: changes numeric representation of root to +// root ** power. Assumes base BASEX +// decomposes the exponent into it's sums of powers of 2, so on average +// it will take n+n/2 multiplies where n is the highest on bit. +// +//----------------------------------------------------------------------------- + +void numpowlongx( _Inout_ PNUMBER *proot, _In_ long power ) + +{ + PNUMBER lret = longtonum( 1, BASEX ); + + // Once the power remaining is zero we are done. + while ( power > 0 ) + { + // If this bit in the power decomposition is on, multiply the result + // by the root number. + if ( power & 1 ) + { + mulnumx( &lret, *proot ); + } + + // multiply the root number by itself to scale for the next bit (i.e. + // square it. + mulnumx( proot, *proot ); + + // move the next bit of the power into place. + power >>= 1; + } + destroynum( *proot ); + *proot=lret; + +} + +void _divnumx( PNUMBER *pa, PNUMBER b, int32_t precision); + +//---------------------------------------------------------------------------- +// +// FUNCTION: divnumx +// +// ARGUMENTS: pointer to a number, a second number and precision. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa /= b. +// Assumes radix is the internal radix representation. +// This is a stub which prevents division by 1, this is a big speed +// improvement. +// +//---------------------------------------------------------------------------- + +void __inline divnumx( PNUMBER *pa, PNUMBER b, int32_t precision) + +{ + if ( b->cdigit > 1 || b->mant[0] != 1 || b->exp != 0 ) + { + // b is not one. + if ( (*pa)->cdigit > 1 || (*pa)->mant[0] != 1 || (*pa)->exp != 0 ) + { + // pa and b are both not one. + _divnumx( pa, b, precision); + } + else + { + // if pa is one and b is not one, just copy b, and adjust the sign. + long sign = (*pa)->sign; + DUPNUM(*pa,b); + (*pa)->sign *= sign; + } + } + else + { + // b is one so don't divide, but set the sign. + (*pa)->sign *= b->sign; + } +} + +//---------------------------------------------------------------------------- +// +// FUNCTION: _divnumx +// +// ARGUMENTS: pointer to a number, a second number and precision. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa /= b. +// Assumes radix is the internal radix representation. +// +//---------------------------------------------------------------------------- + +void _divnumx( PNUMBER *pa, PNUMBER b, int32_t precision) + +{ + PNUMBER a= nullptr; // a is the dereferenced number pointer from *pa + PNUMBER c= nullptr; // c will contain the result. + PNUMBER lasttmp = nullptr; // lasttmp allows a backup when the algorithm + // guesses one bit too far. + PNUMBER tmp = nullptr; // current guess being worked on for divide. + PNUMBER rem = nullptr; // remainder after applying guess. + long cdigits; // count of digits for answer. + MANTTYPE *ptrc; // ptrc is a pointer to the mantissa of c. + + long thismax = precision + g_ratio; // set a maximum number of internal digits + // to shoot for in the divide. + + a=*pa; + if ( thismax < a->cdigit ) + { + // a has more digits than precision specified, bump up digits to shoot + // for. + thismax = a->cdigit; + } + + if ( thismax < b->cdigit ) + { + // b has more digits than precision specified, bump up digits to shoot + // for. + thismax = b->cdigit; + } + + // Create c (the divide answer) and set up exponent and sign. + createnum( c, thismax + 1 ); + c->exp = (a->cdigit+a->exp) - (b->cdigit+b->exp) + 1; + c->sign = a->sign * b->sign; + + ptrc = c->mant + thismax; + cdigits = 0; + + DUPNUM( rem, a ); + rem->sign = b->sign; + rem->exp = b->cdigit + b->exp - rem->cdigit; + + while ( cdigits++ < thismax && !zernum(rem) ) + { + long digit = 0; + *ptrc = 0; + while ( !lessnum( rem, b ) ) + { + digit = 1; + DUPNUM( tmp, b ); + destroynum( lasttmp ); + lasttmp=longtonum( 0, BASEX ); + while ( lessnum( tmp, rem ) ) + { + destroynum( lasttmp ); + DUPNUM(lasttmp,tmp); + addnum( &tmp, tmp, BASEX ); + digit *= 2; + } + if ( lessnum( rem, tmp ) ) + { + // too far, back up... + destroynum( tmp ); + digit /= 2; + tmp=lasttmp; + lasttmp= nullptr; + } + + tmp->sign *= -1; + addnum( &rem, tmp, BASEX ); + destroynum( tmp ); + destroynum( lasttmp ); + *ptrc |= digit; + } + rem->exp++; + ptrc--; + } + cdigits--; + if ( c->mant != ++ptrc ) + { + memmove( c->mant, ptrc, (int)(cdigits*sizeof(MANTTYPE)) ); + } + + if ( !cdigits ) + { + // A zero, make sure no wierd exponents creep in + c->exp = 0; + c->cdigit = 1; + } + else + { + c->cdigit = cdigits; + c->exp -= cdigits; + // prevent different kinds of zeros, by stripping leading duplicate + // zeroes. digits are in order of increasing significance. + while ( c->cdigit > 1 && c->mant[c->cdigit-1] == 0 ) + { + c->cdigit--; + } + } + + destroynum( rem ); + + destroynum( *pa ); + *pa=c; +} + + diff --git a/src/CalcManager/Ratpack/conv.cpp b/src/CalcManager/Ratpack/conv.cpp new file mode 100644 index 00000000..789e021a --- /dev/null +++ b/src/CalcManager/Ratpack/conv.cpp @@ -0,0 +1,1486 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//--------------------------------------------------------------------------- +// Package Title ratpak +// File conv.c +// Copyright (C) 1995-97 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains conversion, input and output routines for numbers rationals +// and longs. +// +// +// +//--------------------------------------------------------------------------- + +#include "pch.h" +#include "ratpak.h" + +using namespace std; + +static constexpr int MAX_ZEROS_AFTER_DECIMAL = 2; + +// digits 0..64 used by bases 2 .. 64 +static constexpr wstring_view DIGITS = L"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_@"; + +// ratio of internal 'digits' to output 'digits' +// Calculated elsewhere as part of initialization and when base is changed +long g_ratio; // int(log(2L^BASEXPWR)/log(radix)) +// Default decimal separator +wchar_t g_decimalSeparator = L'.'; + +// Used to strip trailing zeroes, and prevent combinatorial explosions +bool stripzeroesnum(_Inout_ PNUMBER pnum, long starting); + +void SetDecimalSeparator(wchar_t decimalSeparator) +{ + g_decimalSeparator = decimalSeparator; +} + +// +// Windows heap allocation +// +void* zmalloc(size_t a) +{ + return calloc(a, sizeof(unsigned char)); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: _dupnum +// +// ARGUMENTS: pointer to a number, pointer to a number +// +// RETURN: None +// +// DESCRIPTION: Copies the source to the destination +// +//----------------------------------------------------------------------------- + +void _dupnum(_In_ PNUMBER dest, _In_ PNUMBER src) +{ + memcpy(dest, src, (int)(sizeof(NUMBER) + ((src)->cdigit)*(sizeof(MANTTYPE)))); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: _destroynum +// +// ARGUMENTS: pointer to a number +// +// RETURN: None +// +// DESCRIPTION: Deletes the number and associated allocation +// +//----------------------------------------------------------------------------- + +void _destroynum( _In_ PNUMBER pnum ) + +{ + if ( pnum != nullptr) + { + free( pnum ); + } +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: _destroyrat +// +// ARGUMENTS: pointer to a rational +// +// RETURN: None +// +// DESCRIPTION: Deletes the rational and associated +// allocations. +// +//----------------------------------------------------------------------------- + +void _destroyrat( _In_ PRAT prat ) + +{ + if ( prat != nullptr) + { + destroynum( prat->pp ); + destroynum( prat->pq ); + free( prat ); + } +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: _createnum +// +// ARGUMENTS: size of number in 'digits' +// +// RETURN: pointer to a number +// +// DESCRIPTION: allocates and zeroes out number type. +// +//----------------------------------------------------------------------------- + +PNUMBER _createnum( _In_ ULONG size ) + +{ + PNUMBER pnumret= nullptr; + ULONG cbAlloc; + + // sizeof( MANTTYPE ) is the size of a 'digit' + if (SUCCEEDED(ULongAdd(size, 1, &cbAlloc)) && + SUCCEEDED(ULongMult(cbAlloc, sizeof(MANTTYPE), &cbAlloc)) && + SUCCEEDED(ULongAdd(cbAlloc, sizeof(NUMBER), &cbAlloc))) + { + pnumret = (PNUMBER)zmalloc( cbAlloc ); + if ( pnumret == nullptr) + { + throw( CALC_E_OUTOFMEMORY ); + } + } + else + { + throw( CALC_E_INVALIDRANGE ); + } + return( pnumret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: _createrat +// +// ARGUMENTS: none +// +// RETURN: pointer to a rational +// +// DESCRIPTION: allocates a rational structure but does not +// allocate the numbers that make up the rational p over q +// form. These number pointers are left pointing to null. +// +//----------------------------------------------------------------------------- + + +PRAT _createrat( void ) + +{ + PRAT prat= nullptr; + + prat = (PRAT)zmalloc( sizeof( RAT ) ); + + if ( prat == nullptr) + { + throw( CALC_E_OUTOFMEMORY ); + } + prat->pp = nullptr; + prat->pq = nullptr; + return( prat ); +} + + + +//----------------------------------------------------------------------------- +// +// FUNCTION: numtorat +// +// ARGUMENTS: pointer to a number, radix number is in. +// +// RETURN: Rational representation of number. +// +// DESCRIPTION: The rational representation of the number +// is guaranteed to be in the form p (number with internal +// base representation) over q (number with internal base +// representation) Where p and q are integers. +// +//----------------------------------------------------------------------------- + +PRAT numtorat( _In_ PNUMBER pin, uint32_t radix) + +{ + PNUMBER pnRadixn= nullptr; + DUPNUM( pnRadixn, pin ); + + PNUMBER qnRadixn=longtonum( 1, radix); + + // Ensure p and q start out as integers. + if ( pnRadixn->exp < 0 ) + { + qnRadixn->exp -= pnRadixn->exp; + pnRadixn->exp = 0; + } + + PRAT pout= nullptr; + createrat(pout); + + // There is probably a better way to do this. + pout->pp = numtonRadixx(pnRadixn, radix); + pout->pq = numtonRadixx(qnRadixn, radix); + + + destroynum( pnRadixn ); + destroynum( qnRadixn ); + + return( pout ); +} + + + +//---------------------------------------------------------------------------- +// +// FUNCTION: nRadixxtonum +// +// ARGUMENTS: pointer to a number, base requested. +// +// RETURN: number representation in radix requested. +// +// DESCRIPTION: Does a base conversion on a number from +// internal to requested base. Assumes number being passed +// in is really in internal base form. +// +//---------------------------------------------------------------------------- + +PNUMBER nRadixxtonum( _In_ PNUMBER a, uint32_t radix, int32_t precision) + +{ + unsigned long bitmask; + unsigned long cdigits; + MANTTYPE *ptr; + + PNUMBER sum = longtonum( 0, radix ); + PNUMBER powofnRadix = longtonum( BASEX, radix ); + + // A large penalty is paid for conversion of digits no one will see anyway. + // limit the digits to the minimum of the existing precision or the + // requested precision. + cdigits = precision + 1; + if ( cdigits > (unsigned long)a->cdigit ) + { + cdigits = (unsigned long)a->cdigit; + } + + // scale by the internal base to the internal exponent offset of the LSD + numpowlong( &powofnRadix, a->exp + (a->cdigit - cdigits), radix, precision); + + // Loop over all the relative digits from MSD to LSD + for ( ptr = &(a->mant[a->cdigit-1]); cdigits > 0; + ptr--, cdigits-- ) + { + // Loop over all the bits from MSB to LSB + for ( bitmask = BASEX/2; bitmask > 0; bitmask /= 2 ) + { + addnum( &sum, sum, radix ); + if ( *ptr & bitmask ) + { + sum->mant[0] |= 1; + } + } + } + + // Scale answer by power of internal exponent. + mulnum( &sum, powofnRadix, radix ); + + destroynum( powofnRadix ); + sum->sign = a->sign; + return( sum ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: numtonRadixx +// +// ARGUMENTS: pointer to a number, radix of that number. +// +// RETURN: number representation in internal radix. +// +// DESCRIPTION: Does a radix conversion on a number from +// specified radix to requested radix. Assumes the radix +// specified is the radix of the number passed in. +// +//----------------------------------------------------------------------------- + +PNUMBER numtonRadixx(_In_ PNUMBER a, uint32_t radix) +{ + PNUMBER pnumret = longtonum(0, BASEX); // pnumret is the number in internal form. + PNUMBER num_radix = longtonum(radix, BASEX); + MANTTYPE *ptrdigit = a->mant; // pointer to digit being worked on. + + // Digits are in reverse order, back over them LSD first. + ptrdigit += a->cdigit-1; + + PNUMBER thisdigit = nullptr; // thisdigit holds the current digit of a + // being summed into result. + long idigit; // idigit is the iterate of digits in a. + for ( idigit = 0; idigit < a->cdigit; idigit++ ) + { + mulnumx( &pnumret, num_radix); + // WARNING: + // This should just smack in each digit into a 'special' thisdigit. + // and not do the overhead of recreating the number type each time. + thisdigit = longtonum( *ptrdigit--, BASEX ); + addnum( &pnumret, thisdigit, BASEX ); + destroynum( thisdigit ); + } + + // Calculate the exponent of the external base for scaling. + numpowlongx( &num_radix, a->exp ); + + // ... and scale the result. + mulnumx( &pnumret, num_radix); + + destroynum(num_radix); + + // And propagate the sign. + pnumret->sign = a->sign; + + return( pnumret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: StringToRat +// +// ARGUMENTS: +// mantissaIsNegative true if mantissa is less than zero +// mantissa a string representation of a number +// exponentIsNegative true if exponent is less than zero +// exponent a string representation of a number +// +// RETURN: PRAT representation of string input. +// Or nullptr if no number scanned. +// +// EXPLANATION: This is for calc. +// +// +//----------------------------------------------------------------------------- + +PRAT StringToRat(bool mantissaIsNegative, wstring_view mantissa, bool exponentIsNegative, wstring_view exponent, uint32_t radix, int32_t precision) +{ + PRAT resultRat = nullptr; // holds exponent in rational form. + + // Deal with mantissa + if (mantissa.empty()) + { + // Preset value if no mantissa + if (exponent.empty()) + { + // Exponent not specified, preset value to zero + DUPRAT(resultRat, rat_zero); + } + else + { + // Exponent specified, preset value to one + DUPRAT(resultRat, rat_one); + } + } + else + { + // Mantissa specified, convert to number form. + PNUMBER pnummant = StringToNumber(mantissa, radix, precision); + if (pnummant == nullptr) + { + return nullptr; + } + + resultRat = numtorat(pnummant, radix); + // convert to rational form, and cleanup. + destroynum(pnummant); + } + + // Deal with exponent + long expt = 0; + if (!exponent.empty()) + { + // Exponent specified, convert to number form. + // Don't use native stuff, as it is restricted in the bases it can + // handle. + PNUMBER numExp = StringToNumber(exponent, radix, precision); + if (numExp == nullptr) + { + return nullptr; + } + + // Convert exponent number form to native integral form, and cleanup. + expt = numtolong(numExp, radix); + destroynum(numExp); + } + + // Convert native integral exponent form to rational multiplier form. + PNUMBER pnumexp = longtonum(radix, BASEX); + numpowlongx(&pnumexp, abs(expt)); + + PRAT pratexp = nullptr; + createrat(pratexp); + DUPNUM(pratexp->pp, pnumexp); + pratexp->pq = longtonum(1, BASEX); + destroynum(pnumexp); + + if (exponentIsNegative) + { + // multiplier is less than 1, this means divide. + divrat(&resultRat, pratexp, precision); + } + else if (expt > 0) + { + // multiplier is greater than 1, this means multiply. + mulrat(&resultRat, pratexp, precision); + } + // multiplier can be 1, in which case it'd be a waste of time to multiply. + + destroyrat(pratexp); + + if (mantissaIsNegative) + { + // A negative number was used, adjust the sign. + resultRat->pp->sign *= -1; + } + + return resultRat; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: StringToNumber +// +// ARGUMENTS: +// wstring_view numberString +// int radix +// int32_t precision +// +// RETURN: pnumber representation of string input. +// Or nullptr if no number scanned. +// +// EXPLANATION: This is a state machine, +// +// State Description Example, ^shows just read position. +// which caused the transition +// +// START Start state ^1.0 +// MANTS Mantissa sign -^1.0 +// LZ Leading Zero 0^1.0 +// LZDP Post LZ dec. pt. 000.^1 +// LD Leading digit 1^.0 +// DZ Post LZDP Zero 000.0^1 +// DD Post Decimal digit .01^2 +// DDP Leading Digit dec. pt. 1.^2 +// EXPB Exponent Begins 1.0e^2 +// EXPS Exponent sign 1.0e+^5 +// EXPD Exponent digit 1.0e1^2 or even 1.0e0^1 +// EXPBZ Exponent begin post 0 0.000e^+1 +// EXPSZ Exponent sign post 0 0.000e+^1 +// EXPDZ Exponent digit post 0 0.000e+1^2 +// ERR Error case 0.0.^ +// +// Terminal Description +// +// DP '.' +// ZR '0' +// NZ '1'..'9' 'A'..'Z' 'a'..'z' '@' '_' +// SG '+' '-' +// EX 'e' '^' e is used for radix 10, ^ for all other radixs. +// +//----------------------------------------------------------------------------- +static constexpr uint8_t DP = 0; +static constexpr uint8_t ZR = 1; +static constexpr uint8_t NZ = 2; +static constexpr uint8_t SG = 3; +static constexpr uint8_t EX = 4; + +static constexpr uint8_t START = 0; +static constexpr uint8_t MANTS = 1; +static constexpr uint8_t LZ = 2; +static constexpr uint8_t LZDP = 3; +static constexpr uint8_t LD = 4; +static constexpr uint8_t DZ = 5; +static constexpr uint8_t DD = 6; +static constexpr uint8_t DDP = 7; +static constexpr uint8_t EXPB = 8; +static constexpr uint8_t EXPS = 9; +static constexpr uint8_t EXPD = 10; +static constexpr uint8_t EXPBZ = 11; +static constexpr uint8_t EXPSZ = 12; +static constexpr uint8_t EXPDZ = 13; +static constexpr uint8_t ERR = 14; + +#if defined( DEBUG ) +char *statestr[] = { + "START", + "MANTS", + "LZ", + "LZDP", + "LD", + "DZ", + "DD", + "DDP", + "EXPB", + "EXPS", + "EXPD", + "EXPBZ", + "EXPSZ", + "EXPDZ", + "ERR", +}; +#endif + +// New state is machine[state][terminal] +static constexpr uint8_t machine[ERR+1][EX+1]= { + // DP, ZR, NZ, SG, EX + // START + { LZDP, LZ, LD, MANTS, ERR }, + // MANTS + { LZDP, LZ, LD, ERR, ERR }, + // LZ + { LZDP, LZ, LD, ERR, EXPBZ }, + // LZDP + { ERR, DZ, DD, ERR, EXPB }, + // LD + { DDP, LD, LD, ERR, EXPB }, + // DZ + { ERR, DZ, DD, ERR, EXPBZ }, + // DD + { ERR, DD, DD, ERR, EXPB }, + // DDP + { ERR, DD, DD, ERR, EXPB }, + // EXPB + { ERR, EXPD, EXPD, EXPS, ERR }, + // EXPS + { ERR, EXPD, EXPD, ERR, ERR }, + // EXPD + { ERR, EXPD, EXPD, ERR, ERR }, + // EXPBZ + { ERR, EXPDZ, EXPDZ, EXPSZ, ERR }, + // EXPSZ + { ERR, EXPDZ, EXPDZ, ERR, ERR }, + // EXPDZ + { ERR, EXPDZ, EXPDZ, ERR, ERR }, + // ERR + { ERR, ERR, ERR, ERR, ERR } +}; + +wchar_t NormalizeCharDigit(wchar_t c, uint32_t radix) +{ + // Allow upper and lower case letters as equivalent, base + // is in the range where this is not ambiguous. + if (size_t{ radix } >= DIGITS.find(L'A') && size_t { radix } <= DIGITS.find(L'Z')) + { + return toupper(c); + } + + return c; +} + +PNUMBER StringToNumber(wstring_view numberString, uint32_t radix, int32_t precision) +{ + long expSign = 1L; // expSign is exponent sign ( +/- 1 ) + long expValue = 0L; // expValue is exponent mantissa, should be unsigned + + PNUMBER pnumret = nullptr; + createnum(pnumret, numberString.length()); + pnumret->sign = 1L; + pnumret->cdigit = 0; + pnumret->exp = 0; + MANTTYPE *pmant = pnumret->mant + numberString.length() - 1; + + uint8_t state = START; // state is the state of the input state machine. + wchar_t curChar; + for (const auto& c : numberString) + { + // If the character is the decimal separator, use L'.' for the purposes of the state machine. + curChar = (c == g_decimalSeparator ? L'.' : c); + + // Switch states based on the character we encountered + switch (curChar) + { + case L'-': + case L'+': + state = machine[state][SG]; + break; + case L'.': + state = machine[state][DP]; + break; + case L'0': + state = machine[state][ZR]; + break; + case L'^': + case L'e': + if (curChar == L'^' || radix == 10) + { + state = machine[state][EX]; + break; + } + // Drop through in the 'e'-as-a-digit case + default: + state = machine[state][NZ]; + break; + } + + // Now update our result value based on the state we are in + switch (state) + { + case MANTS: + pnumret->sign = (curChar == L'-') ? -1 : 1; + break; + case EXPSZ: + case EXPS: + expSign = (curChar == L'-') ? -1 : 1; + break; + case EXPDZ: + case EXPD: + { + curChar = NormalizeCharDigit(curChar, radix); + + size_t pos = DIGITS.find(curChar); + if (pos != wstring_view::npos) + { + expValue *= radix; + expValue += static_cast(pos); + } + else + { + state = ERR; + } + } + break; + case LD: + pnumret->exp++; + // Fall through + case DD: + { + curChar = NormalizeCharDigit(curChar, radix); + + size_t pos = DIGITS.find(curChar); + if (pos != wstring_view::npos && pos < static_cast(radix)) + { + *pmant-- = static_cast(pos); + pnumret->exp--; + pnumret->cdigit++; + } + else + { + state = ERR; + } + } + break; + case DZ: + pnumret->exp--; + break; + case LZ: + case LZDP: + case DDP: + break; + } + } + + if (state == DZ || state == EXPDZ) + { + pnumret->cdigit = 1; + pnumret->exp = 0; + pnumret->sign = 1; + } + else + { + while (pnumret->cdigit < static_cast(numberString.length())) + { + pnumret->cdigit++; + pnumret->exp--; + } + + pnumret->exp += expSign * expValue; + } + + // If we don't have a number, clear our result. + if (pnumret->cdigit == 0) + { + destroynum(pnumret); + pnumret = nullptr; + } + + stripzeroesnum(pnumret, precision); + + return pnumret; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: longtorat +// +// ARGUMENTS: long +// +// RETURN: Rational representation of long input. +// +// DESCRIPTION: Converts long input to rational (p over q) +// form, where q is 1 and p is the long. +// +//----------------------------------------------------------------------------- + +PRAT longtorat( _In_ long inlong ) + +{ + PRAT pratret= nullptr; + createrat( pratret ); + pratret->pp = longtonum(inlong, BASEX ); + pratret->pq = longtonum(1L, BASEX ); + return( pratret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: Ulongtorat +// +// ARGUMENTS: ulong +// +// RETURN: Rational representation of unsigned long input. +// +// DESCRIPTION: Converts unsigned long input to rational (p over q) +// form, where q is 1 and p is the unsigned long. Being unsigned cant take negative +// numbers, but the full range of unsigned numbers +// +//----------------------------------------------------------------------------- + +PRAT Ulongtorat( _In_ unsigned long inulong ) + +{ + PRAT pratret= nullptr; + createrat( pratret ); + pratret->pp = Ulongtonum(inulong, BASEX ); + pratret->pq = longtonum(1L, BASEX ); + return( pratret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: longtonum +// +// ARGUMENTS: long input and radix requested. +// +// RETURN: number +// +// DESCRIPTION: Returns a number representation in the +// base requested of the long value passed in. +// +//----------------------------------------------------------------------------- + +PNUMBER longtonum( long inlong, uint32_t radix) + +{ + MANTTYPE *pmant; + PNUMBER pnumret= nullptr; + + createnum( pnumret, MAX_LONG_SIZE ); + pmant = pnumret->mant; + pnumret->cdigit = 0; + pnumret->exp = 0; + if ( inlong < 0 ) + { + pnumret->sign = -1; + inlong *= -1; + } + else + { + pnumret->sign = 1; + } + + do { + *pmant++ = (MANTTYPE)(inlong % radix); + inlong /= radix; + pnumret->cdigit++; + } while ( inlong ); + + return( pnumret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: Ulongtonum +// +// ARGUMENTS: ULONG input and radix requested. +// +// RETURN: number +// +// DESCRIPTION: Returns a number representation in the +// base requested of the unsigned long value passed in. Being unsigned number it has no +// negative number and takes the full range of unsigned number +// +//----------------------------------------------------------------------------- + + +PNUMBER Ulongtonum(unsigned long inlong, uint32_t radix) +{ + MANTTYPE *pmant; + PNUMBER pnumret= nullptr; + + createnum( pnumret, MAX_LONG_SIZE ); + pmant = pnumret->mant; + pnumret->cdigit = 0; + pnumret->exp = 0; + pnumret->sign = 1; + + do { + *pmant++ = (MANTTYPE)(inlong % radix); + inlong /= radix; + pnumret->cdigit++; + } while ( inlong ); + + return( pnumret ); +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: rattolong +// +// ARGUMENTS: rational number in internal base, integer radix and int32_t precision. +// +// RETURN: long +// +// DESCRIPTION: returns the long representation of the +// number input. Assumes that the number is in the internal +// base. +// +//----------------------------------------------------------------------------- + +long rattolong( _In_ PRAT prat , uint32_t radix, int32_t precision) +{ + if ( rat_gt( prat, rat_max_long, precision) || rat_lt( prat, rat_min_long, precision) ) + { + // Don't attempt rattolong of anything too big or small + throw( CALC_E_DOMAIN ); + } + + PRAT pint = nullptr; + DUPRAT(pint,prat); + + intrat( &pint, radix, precision); + divnumx( &(pint->pp), pint->pq, precision); + DUPNUM( pint->pq, num_one ); + + long lret = numtolong( pint->pp, BASEX ); + + destroyrat(pint); + + return( lret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: rattoUlong +// +// ARGUMENTS: rational number in internal base, integer radix and int32_t precision. +// +// RETURN: Ulong +// +// DESCRIPTION: returns the Ulong representation of the +// number input. Assumes that the number is in the internal +// base. +// +//----------------------------------------------------------------------------- +unsigned long rattoUlong( _In_ PRAT prat, uint32_t radix, int32_t precision) +{ + if ( rat_gt( prat, rat_dword, precision) || rat_lt( prat, rat_zero, precision) ) + { + // Don't attempt rattoulong of anything too big or small + throw( CALC_E_DOMAIN ); + } + + PRAT pint = nullptr; + DUPRAT(pint,prat); + + intrat( &pint, radix, precision); + divnumx( &(pint->pp), pint->pq, precision); + DUPNUM( pint->pq, num_one ); + + unsigned long lret = numtolong( pint->pp, BASEX ); // This happens to work even if it is only signed + + destroyrat(pint); + + return( lret ); +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: rattoUlonglong +// +// ARGUMENTS: rational number in internal base, integer radix and int32_t precision +// +// RETURN: Ulonglong +// +// DESCRIPTION: returns the 64 bit (irrespective of which processor this is running in) representation of the +// number input. Assumes that the number is in the internal +// base. Can throw exception if the number exceeds 2^64 +// Implementation by getting the HI & LO 32 bit words and concating them, as the +// internal base choosen happens to be 2^32, this is easier. +//----------------------------------------------------------------------------- + +ULONGLONG rattoUlonglong( _In_ PRAT prat, uint32_t radix, int32_t precision) +{ + PRAT pint = nullptr; + + // first get the LO 32 bit word + DUPRAT(pint, prat); + andrat(&pint, rat_dword, radix, precision); // & 0xFFFFFFFF (2 ^ 32 -1) + unsigned long lo = rattoUlong(pint, radix, precision); // wont throw exception because already hi-dword chopped off + + DUPRAT(pint, prat); // previous pint will get freed by this as well + PRAT prat32 = longtorat(32); + rshrat(&pint, prat32, radix, precision); + intrat( &pint, radix, precision); + andrat(&pint, rat_dword, radix, precision); // & 0xFFFFFFFF (2 ^ 32 -1) + unsigned long hi = rattoUlong(pint, radix, precision); + + destroyrat(prat32); + destroyrat(pint); + + return (((ULONGLONG)hi << 32) | lo); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: numtolong +// +// ARGUMENTS: number input and base of that number. +// +// RETURN: long +// +// DESCRIPTION: returns the long representation of the +// number input. Assumes that the number is really in the +// base claimed. +// +//----------------------------------------------------------------------------- +long numtolong( _In_ PNUMBER pnum, uint32_t radix ) +{ + long lret = 0; + + MANTTYPE *pmant = pnum->mant; + pmant += pnum->cdigit - 1; + + long expt = pnum->exp; + for (long length = pnum->cdigit; length > 0 && length + expt > 0; length--) + { + lret *= radix; + lret += *(pmant--); + } + + while (expt-- > 0) + { + lret *= radix; + } + lret *= pnum->sign; + + return lret; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: bool stripzeroesnum +// +// ARGUMENTS: a number representation +// +// RETURN: true if stripping done, modifies number in place. +// +// DESCRIPTION: Strips off trailing zeroes. +// +//----------------------------------------------------------------------------- + +bool stripzeroesnum(_Inout_ PNUMBER pnum, long starting) +{ + MANTTYPE *pmant; + long cdigits; + bool fstrip = false; + + // point pmant to the LeastCalculatedDigit + pmant=pnum->mant; + cdigits=pnum->cdigit; + // point pmant to the LSD + if ( cdigits > starting ) + { + pmant += cdigits - starting; + cdigits = starting; + } + + // Check we haven't gone too far, and we are still looking at zeroes. + while ( ( cdigits > 0 ) && !(*pmant) ) + { + // move to next significant digit and keep track of digits we can + // ignore later. + pmant++; + cdigits--; + fstrip = true; + } + + // If there are zeroes to remove. + if ( fstrip ) + { + // Remove them. + memmove( pnum->mant, pmant, (int)(cdigits*sizeof(MANTTYPE)) ); + // And adjust exponent and digit count accordingly. + pnum->exp += ( pnum->cdigit - cdigits ); + pnum->cdigit = cdigits; + } + return( fstrip ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: NumberToString +// +// ARGUMENTS: number representation +// fmt, one of FMT_FLOAT FMT_SCIENTIFIC or +// FMT_ENGINEERING +// integer radix and int32_t precision value +// +// RETURN: String representation of number. +// +// DESCRIPTION: Converts a number to it's string +// representation. +// +//----------------------------------------------------------------------------- +wstring NumberToString(_Inout_ PNUMBER& pnum, int format, uint32_t radix, int32_t precision) +{ + stripzeroesnum(pnum, precision + 2); + long length = pnum->cdigit; + long exponent = pnum->exp + length; // Actual number of digits to the left of decimal + + long oldFormat = format; + if (exponent > precision && format == FMT_FLOAT) + { + // Force scientific mode to prevent user from assuming 33rd digit is exact. + format = FMT_SCIENTIFIC; + } + + // Make length small enough to fit in pret. + if (length > precision) + { + length = precision; + } + + // 2 for signs, 1 for 'e'(or leading zero), 1 for dp, 1 for null and + // 10 for maximum exponent size. + int cchNum = (precision + 16); + + // If there is a chance a round has to occour, round. + // - if number is zero no rounding + // - if number of digits is less than the maximum output no rounding + PNUMBER round = nullptr; + if (!zernum(pnum) && (pnum->cdigit >= precision || (length - exponent > precision && exponent >= -MAX_ZEROS_AFTER_DECIMAL))) + { + // Otherwise round. + round = longtonum(radix, radix); + divnum(&round, num_two, radix, precision); + + // Make round number exponent one below the LSD for the number. + if (exponent > 0 || format == FMT_FLOAT) + { + round->exp = pnum->exp + pnum->cdigit - round->cdigit - precision; + } + else + { + round->exp = pnum->exp + pnum->cdigit - round->cdigit - precision - exponent; + length = precision + exponent; + } + + round->sign = pnum->sign; + } + + if (format == FMT_FLOAT) + { + // Figure out if the exponent will fill more space than the nonexponent field. + if ((length - exponent > precision) || (exponent > precision + 3)) + { + if (exponent >= -MAX_ZEROS_AFTER_DECIMAL) + { + round->exp -= exponent; + length = precision + exponent; + } + else + { + // Case where too many zeroes are to the right or left of the + // decimal pt. And we are forced to switch to scientific form. + format = FMT_SCIENTIFIC; + } + } + else if (length + abs(exponent) < precision && round) + { + // Minimum loss of precision occours with listing leading zeros + // if we need to make room for zeroes sacrifice some digits. + round->exp -= exponent; + } + } + + if (round != nullptr) + { + addnum(&pnum, round, radix); + long offset = (pnum->cdigit + pnum->exp) - (round->cdigit + round->exp); + destroynum(round); + if (stripzeroesnum(pnum, offset)) + { + // WARNING: nesting/recursion, too much has been changed, need to + // refigure format. + return NumberToString(pnum, oldFormat, radix, precision); + } + } + else + { + stripzeroesnum(pnum, precision); + } + + // Set up all the post rounding stuff. + bool useSciForm = false; + long eout = exponent - 1; // Displayed exponent. + MANTTYPE *pmant = pnum->mant + pnum->cdigit - 1; + // Case where too many digits are to the left of the decimal or + // FMT_SCIENTIFIC or FMT_ENGINEERING was specified. + if ((format == FMT_SCIENTIFIC) || (format == FMT_ENGINEERING)) + { + useSciForm = true; + if (eout != 0) + { + if (format == FMT_ENGINEERING) + { + exponent = (eout % 3); + eout -= exponent; + exponent++; + + // Fix the case where 0.02e-3 should really be 2.e-6 etc. + if (exponent < 0) + { + exponent += 3; + eout -= 3; + } + } + else + { + exponent = 1; + } + } + } + else + { + eout = 0; + } + + // Begin building the result string + wstringstream resultStream{}; + + // Make sure negative zeroes aren't allowed. + if ((pnum->sign == -1) && (length > 0)) + { + resultStream << L'-'; + } + + if (exponent <= 0 && !useSciForm) + { + resultStream << L'0'; + resultStream << g_decimalSeparator; + // Used up a digit unaccounted for. + } + + while (exponent < 0) + { + resultStream << L'0'; + exponent++; + } + + while (length > 0) + { + exponent--; + resultStream << DIGITS[*pmant--]; + length--; + + // Be more regular in using a decimal point. + if (exponent == 0) + { + resultStream << g_decimalSeparator; + } + } + + while (exponent > 0) + { + resultStream << L'0'; + exponent--; + // Be more regular in using a decimal point. + if (exponent == 0) + { + resultStream << g_decimalSeparator; + } + } + + if (useSciForm) + { + resultStream << (radix == 10 ? L'e' : L'^'); + resultStream << (eout < 0 ? L'-' : L'+'); + eout = abs(eout); + wstringstream exponentStream{}; + do + { + exponentStream << DIGITS[eout % radix]; + eout /= radix; + } while (eout > 0); + + auto expString = exponentStream.str(); + for (auto ritr = expString.rbegin(); ritr != expString.rend(); ritr++) + { + resultStream << *ritr; + } + } + + // Remove trailing decimal + auto resultString = resultStream.str(); + if (!resultString.empty() && resultString.back() == g_decimalSeparator) + { + resultString.pop_back(); + } + + return resultString; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: RatToString +// +// ARGUMENTS: +// PRAT *representation of a number. +// long representation of base to dump to screen. +// fmt, one of FMT_FLOAT FMT_SCIENTIFIC or FMT_ENGINEERING +// precision uint32_t +// +// RETURN: string +// +// DESCRIPTION: returns a string representation of rational number passed +// in, at least to the precision digits. +// +// NOTE: It may be that doing a GCD() could shorten the rational form +// And it may eventually be worthwhile to keep the result. That is +// why a pointer to the rational is passed in. +// +//----------------------------------------------------------------------------- +wstring RatToString(_Inout_ PRAT& prat, int format, uint32_t radix, int32_t precision) +{ + // Convert p and q of rational form from internal base to requested base. + // Scale by largest power of BASEX possible. + long scaleby = min(prat->pp->exp, prat->pq->exp); + scaleby = max(scaleby, 0); + + prat->pp->exp -= scaleby; + prat->pq->exp -= scaleby; + + PNUMBER p = nRadixxtonum(prat->pp, radix, precision); + PNUMBER q = nRadixxtonum(prat->pq, radix, precision); + + // finally take the time hit to actually divide. + divnum(&p, q, radix, precision); + destroynum(q); + + wstring result = NumberToString(p, format, radix, precision); + destroynum(p); + + return result; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: gcd +// +// ARGUMENTS: +// PNUMBER representation of a number. +// PNUMBER representation of a number. +// int for Radix +// +// RETURN: Greatest common divisor in internal BASEX PNUMBER form. +// +// DESCRIPTION: gcd uses remainders to find the greatest common divisor. +// +// ASSUMPTIONS: gcd assumes inputs are integers. +// +// NOTE: Before it was found that the TRIM macro actually kept the +// size down cheaper than GCD, this routine was used extensively. +// now it is not used but might be later. +// +//----------------------------------------------------------------------------- + +PNUMBER gcd( _In_ PNUMBER a, _In_ PNUMBER b) +{ + PNUMBER r= nullptr; + PNUMBER larger= nullptr; + PNUMBER smaller= nullptr; + + if (zernum(a)) + { + return b; + } + else if(zernum(b)) + { + return a; + } + + if (lessnum(a, b)) + { + DUPNUM(larger, b); + DUPNUM(smaller, a); + } + else + { + DUPNUM(larger, a); + DUPNUM(smaller, b); + } + + while (!zernum(smaller)) + { + remnum(&larger, smaller, BASEX); + // swap larger and smaller + r = larger; + larger = smaller; + smaller = r; + } + destroynum(smaller); + return larger; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: longfactnum +// +// ARGUMENTS: +// long integer to factorialize. +// long integer representing base of answer. +// unsigned long integer for radix +// +// RETURN: Factorial of input in radix PNUMBER form. +// +// NOTE: Not currently used. +// +//----------------------------------------------------------------------------- + +PNUMBER longfactnum(long inlong, uint32_t radix) + +{ + PNUMBER lret= nullptr; + PNUMBER tmp= nullptr; + + lret = longtonum( 1, radix); + + while ( inlong > 0 ) + { + tmp = longtonum( inlong--, radix); + mulnum( &lret, tmp, radix); + destroynum( tmp ); + } + return( lret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: longprodnum +// +// ARGUMENTS: +// long integer to factorialize. +// long integer representing base of answer. +// unsignd long integer for radix +// +// RETURN: Factorial of input in base PNUMBER form. +// +//----------------------------------------------------------------------------- + +PNUMBER longprodnum(long start, long stop, uint32_t radix) + +{ + PNUMBER lret= nullptr; + PNUMBER tmp= nullptr; + + lret = longtonum( 1, radix); + + while ( start <= stop ) + { + if ( start ) + { + tmp = longtonum( start, radix); + mulnum( &lret, tmp, radix); + destroynum( tmp ); + } + start++; + } + return( lret ); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: numpowlong +// +// ARGUMENTS: root as number power as long and radix of +// number along with the precision value in long. +// +// RETURN: None root is changed. +// +// DESCRIPTION: changes numeric representation of root to +// root ** power. Assumes radix is the radix of root. +// +//----------------------------------------------------------------------------- + +void numpowlong( _Inout_ PNUMBER *proot, long power, uint32_t radix, int32_t precision) +{ + PNUMBER lret = longtonum( 1, radix ); + + while ( power > 0 ) + { + if ( power & 1 ) + { + mulnum( &lret, *proot, radix ); + } + mulnum( proot, *proot, radix ); + TRIMNUM(*proot, precision); + power >>= 1; + } + destroynum( *proot ); + *proot=lret; + +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: ratpowlong +// +// ARGUMENTS: root as rational, power as long and precision as uint32_t. +// +// RETURN: None root is changed. +// +// DESCRIPTION: changes rational representation of root to +// root ** power. +// +//----------------------------------------------------------------------------- + +void ratpowlong( _Inout_ PRAT *proot, long power, int32_t precision) + +{ + if ( power < 0 ) + { + // Take the positive power and invert answer. + PNUMBER pnumtemp = nullptr; + ratpowlong( proot, -power, precision); + pnumtemp = (*proot)->pp; + (*proot)->pp = (*proot)->pq; + (*proot)->pq = pnumtemp; + } + else + { + PRAT lret= nullptr; + + lret = longtorat( 1 ); + + while ( power > 0 ) + { + if ( power & 1 ) + { + mulnumx( &(lret->pp), (*proot)->pp ); + mulnumx( &(lret->pq), (*proot)->pq ); + } + mulrat( proot, *proot, precision); + trimit(&lret, precision); + trimit(proot, precision); + power >>= 1; + } + destroyrat( *proot ); + *proot=lret; + } +} diff --git a/src/CalcManager/Ratpack/exp.cpp b/src/CalcManager/Ratpack/exp.cpp new file mode 100644 index 00000000..19545663 --- /dev/null +++ b/src/CalcManager/Ratpack/exp.cpp @@ -0,0 +1,540 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File exp.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains exp, and log functions for rationals +// +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + +//----------------------------------------------------------------------------- +// +// FUNCTION: exprat +// +// ARGUMENTS: x PRAT representation of number to exponentiate +// +// RETURN: exp of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ +// \ ] X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j j+1 +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +//----------------------------------------------------------------------------- + +void _exprat( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + + addnum(&(pret->pp),num_one, BASEX); + addnum(&(pret->pq),num_one, BASEX); + DUPRAT(thisterm,pret); + + n2=longtonum(0L, BASEX); + + do { + NEXTTERM(*px, INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + +void exprat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT pwr= nullptr; + PRAT pint= nullptr; + long intpwr; + + if ( rat_gt( *px, rat_max_exp, precision) || rat_lt( *px, rat_min_exp, precision) ) + { + // Don't attempt exp of anything large. + throw( CALC_E_DOMAIN ); + } + + DUPRAT(pwr,rat_exp); + DUPRAT(pint,*px); + + intrat(&pint, radix, precision); + + intpwr = rattolong(pint, radix, precision); + ratpowlong( &pwr, intpwr, precision); + + subrat(px, pint, precision); + + // It just so happens to be an integral power of e. + if ( rat_gt( *px, rat_negsmallest, precision) && rat_lt( *px, rat_smallest, precision) ) + { + DUPRAT(*px,pwr); + } + else + { + _exprat(px, precision); + mulrat(px, pwr, precision); + } + + destroyrat( pwr ); + destroyrat( pint ); +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: lograt, _lograt +// +// ARGUMENTS: x PRAT representation of number to logarithim +// +// RETURN: log of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ +// \ ] j*(1-X) +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j j+1 +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +// Number is scaled between one and e_to_one_half prior to taking the +// log. This is to keep execution time from exploding. +// +// +//----------------------------------------------------------------------------- + +void _lograt( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + + createrat(thisterm); + + // sub one from x + (*px)->pq->sign *= -1; + addnum(&((*px)->pp),(*px)->pq, BASEX); + (*px)->pq->sign *= -1; + + DUPRAT(pret,*px); + DUPRAT(thisterm,*px); + + n2=longtonum(1L, BASEX); + (*px)->pp->sign *= -1; + + do { + NEXTTERM(*px, MULNUM(n2) INC(n2) DIVNUM(n2), precision); + TRIMTOP(*px, precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + + +void lograt( PRAT *px, int32_t precision) + +{ + bool fneglog; + PRAT pwr = nullptr; // pwr is the large scaling factor. + PRAT offset = nullptr; // offset is the incremental scaling factor. + + + // Check for someone taking the log of zero or a negative number. + if ( rat_le( *px, rat_zero, precision) ) + { + throw( CALC_E_DOMAIN ); + } + + // Get number > 1, for scaling + fneglog = rat_lt( *px, rat_one, precision); + if ( fneglog ) + { + // WARNING: This is equivalent to doing *px = 1 / *px + PNUMBER pnumtemp= nullptr; + pnumtemp = (*px)->pp; + (*px)->pp = (*px)->pq; + (*px)->pq = pnumtemp; + } + + // Scale the number within BASEX factor of 1, for the large scale. + // log(x*2^(BASEXPWR*k)) = BASEXPWR*k*log(2)+log(x) + if ( LOGRAT2(*px) > 1 ) + { + // Take advantage of px's base BASEX to scale quickly down to + // a reasonable range. + long intpwr; + intpwr=LOGRAT2(*px)-1; + (*px)->pq->exp += intpwr; + pwr=longtorat(intpwr*BASEXPWR); + mulrat(&pwr, ln_two, precision); + // ln(x+e)-ln(x) looks close to e when x is close to one using some + // expansions. This means we can trim past precision digits+1. + TRIMTOP(*px, precision); + } + else + { + DUPRAT(pwr,rat_zero); + } + + DUPRAT(offset,rat_zero); + // Scale the number between 1 and e_to_one_half, for the small scale. + while ( rat_gt( *px, e_to_one_half, precision) ) + { + divrat( px, e_to_one_half, precision); + addrat( &offset, rat_one, precision); + } + + _lograt(px, precision); + + // Add the large and small scaling factors, take into account + // small scaling was done in e_to_one_half chunks. + divrat(&offset, rat_two, precision); + addrat(&pwr, offset, precision); + + // And add the resulting scaling factor to the answer. + addrat(px, pwr, precision); + + trimit(px, precision); + + // If number started out < 1 rescale answer to negative. + if ( fneglog ) + { + (*px)->pp->sign *= -1; + } + + destroyrat(offset); + destroyrat(pwr); +} + +void log10rat( PRAT *px, int32_t precision) + +{ + lograt(px, precision); + divrat(px, ln_ten, precision); +} + +// +// return if the given x is even number. The assumption here is its numberator is 1 and we are testing the numerator is +// even or not +bool IsEven(PRAT x, uint32_t radix, int32_t precision) +{ + PRAT tmp = nullptr; + bool bRet = false; + + DUPRAT(tmp, x); + divrat(&tmp, rat_two, precision); + fracrat(&tmp, radix, precision); + addrat(&tmp, tmp, precision); + subrat(&tmp, rat_one, precision); + if ( rat_lt( tmp, rat_zero, precision)) + { + bRet = true; + } + + destroyrat(tmp); + return bRet; +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: powrat +// +// ARGUMENTS: PRAT *px, PRAT y, uint32_t radix, int32_t precision +// +// RETURN: none, sets *px to *px to the y. +// +// EXPLANATION: Calculates the power of both px and +// handles special cases where px is a perfect root. +// Assumes, all checking has been done on validity of numbers. +// +// +//--------------------------------------------------------------------------- +void powrat(PRAT *px, PRAT y, uint32_t radix, int32_t precision) +{ + // Handle cases where px or y is 0 by calling powratcomp directly + if (zerrat(*px) || zerrat(y)) + { + powratcomp(px, y, radix, precision); + return; + } + // When y is 1, return px + if (rat_equ(y, rat_one, precision)) + { + return; + } + + try + { + powratNumeratorDenominator(px, y, radix, precision); + } + catch (...) + { + // If calculating the power using numerator/denominator + // failed, fallback to the less accurate method of + // passing in the original y + powratcomp(px, y, radix, precision); + } +} + +void powratNumeratorDenominator(PRAT *px, PRAT y, uint32_t radix, int32_t precision) +{ + // Prepare rationals + PRAT yNumerator = nullptr; + PRAT yDenominator = nullptr; + DUPRAT(yNumerator, rat_zero); // yNumerator->pq is 1 one + DUPRAT(yDenominator, rat_zero); // yDenominator->pq is 1 one + DUPNUM(yNumerator->pp, y->pp); + DUPNUM(yDenominator->pp, y->pq); + + // Calculate the following use the Powers of Powers rule: + // px ^ (yNum/yDenom) == px ^ yNum ^ (1/yDenom) + // 1. For px ^ yNum, we call powratcomp directly which will call ratpowlong + // and store the result in pxPowNum + // 2. For pxPowNum ^ (1/yDenom), we call powratcomp + // 3. Validate the result of 2 by adding/subtracting 0.5, flooring and call powratcomp with yDenom + // on the floored result. + + // 1. Initialize result. + PRAT pxPow = nullptr; + DUPRAT(pxPow, *px); + + // 2. Calculate pxPow = px ^ yNumerator + // if yNumerator is not 1 + if (!rat_equ(yNumerator, rat_one, precision)) + { + powratcomp(&pxPow, yNumerator, radix, precision); + } + + // 2. Calculate pxPowNumDenom = pxPowNum ^ (1/yDenominator), + // if yDenominator is not 1 + if (!rat_equ(yDenominator, rat_one, precision)) + { + // Calculate 1 over y + PRAT oneoveryDenom = nullptr; + DUPRAT(oneoveryDenom, rat_one); + divrat(&oneoveryDenom, yDenominator, precision); + + // ################################## + // Take the oneoveryDenom power + // ################################## + PRAT originalResult = nullptr; + DUPRAT(originalResult, pxPow); + powratcomp(&originalResult, oneoveryDenom, radix, precision); + + // ################################## + // Round the originalResult to roundedResult + // ################################## + PRAT roundedResult = nullptr; + DUPRAT(roundedResult, originalResult); + if (roundedResult->pp->sign == -1) + { + subrat(&roundedResult, rat_half, precision); + } + else + { + addrat(&roundedResult, rat_half, precision); + } + intrat(&roundedResult, radix, precision); + + // ################################## + // Take the yDenom power of the roundedResult. + // ################################## + PRAT roundedPower = nullptr; + DUPRAT(roundedPower, roundedResult); + powratcomp(&roundedPower, yDenominator, radix, precision); + + // ################################## + // if roundedPower == px, + // we found an exact power in roundedResult + // ################################## + if (rat_equ(roundedPower, pxPow, precision)) + { + DUPRAT(*px, roundedResult); + } + else + { + DUPRAT(*px, originalResult); + } + + destroyrat(oneoveryDenom); + destroyrat(originalResult); + destroyrat(roundedResult); + destroyrat(roundedPower); + } + else + { + DUPRAT(*px, pxPow); + } + + destroyrat(yNumerator); + destroyrat(yDenominator); + destroyrat(pxPow); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: powratcomp +// +// ARGUMENTS: PRAT *px, and PRAT y +// +// RETURN: none, sets *px to *px to the y. +// +// EXPLANATION: This uses x^y=e(y*ln(x)), or a more exact calculation where +// y is an integer. +// Assumes, all checking has been done on validity of numbers. +// +// +//--------------------------------------------------------------------------- +void powratcomp(PRAT *px, PRAT y, uint32_t radix, int32_t precision) +{ + long sign = ((*px)->pp->sign * (*px)->pq->sign); + + // Take the absolute value + (*px)->pp->sign = 1; + (*px)->pq->sign = 1; + + if ( zerrat( *px ) ) + { + // *px is zero. + if ( rat_lt( y, rat_zero, precision) ) + { + throw( CALC_E_DOMAIN ); + } + else if ( zerrat( y ) ) + { + // *px and y are both zero, special case a 1 return. + DUPRAT(*px,rat_one); + // Ensure sign is positive. + sign = 1; + } + } + else + { + PRAT pxint= nullptr; + DUPRAT(pxint,*px); + subrat(&pxint, rat_one, precision); + if ( rat_gt( pxint, rat_negsmallest, precision) && + rat_lt( pxint, rat_smallest, precision) && ( sign == 1 ) ) + { + // *px is one, special case a 1 return. + DUPRAT(*px,rat_one); + // Ensure sign is positive. + sign = 1; + } + else + { + + // Only do the exp if the number isn't zero or one + PRAT podd = nullptr; + DUPRAT(podd,y); + fracrat(&podd, radix, precision); + if ( rat_gt( podd, rat_negsmallest, precision) && rat_lt( podd, rat_smallest, precision) ) + { + // If power is an integer let ratpowlong deal with it. + PRAT iy = nullptr; + long inty; + DUPRAT(iy,y); + subrat(&iy, podd, precision); + inty = rattolong(iy, radix, precision); + + PRAT plnx = nullptr; + DUPRAT(plnx,*px); + lograt(&plnx, precision); + mulrat(&plnx, iy, precision); + if ( rat_gt( plnx, rat_max_exp, precision) || rat_lt( plnx, rat_min_exp, precision) ) + { + // Don't attempt exp of anything large or small.A + destroyrat(plnx); + destroyrat(iy); + destroyrat(pxint); + destroyrat(podd); + throw( CALC_E_DOMAIN ); + } + destroyrat(plnx); + ratpowlong(px, inty, precision); + if ( ( inty & 1 ) == 0 ) + { + sign=1; + } + destroyrat(iy); + } + else + { + // power is a fraction + if ( sign == -1 ) + { + // Need to throw an error if the exponent has an even denominator. + // As a first step, the numerator and denominator must be divided by 2 as many times as + // possible, so that 2/6 is allowed. + // If the final numerator is still even, the end result should be positive. + PRAT pNumerator = nullptr; + PRAT pDenominator = nullptr; + bool fBadExponent = false; + + // Get the numbers in arbitrary precision rational number format + DUPRAT(pNumerator, rat_zero); // pNumerator->pq is 1 one + DUPRAT(pDenominator, rat_zero); // pDenominator->pq is 1 one + + DUPNUM(pNumerator->pp, y->pp); + pNumerator->pp->sign = 1; + DUPNUM(pDenominator->pp, y->pq); + pDenominator->pp->sign = 1; + + while (IsEven(pNumerator, radix, precision) && IsEven(pDenominator, radix, precision)) // both Numerator & denominator is even + { + divrat(&pNumerator, rat_two, precision); + divrat(&pDenominator, rat_two, precision); + } + if (IsEven(pDenominator, radix, precision)) // denominator is still even + { + fBadExponent = true; + } + if (IsEven(pNumerator, radix, precision)) // numerator is still even + { + sign = 1; + } + destroyrat(pNumerator); + destroyrat(pDenominator); + + if (fBadExponent) + { + throw( CALC_E_DOMAIN ); + } + } + else + { + // If the exponent is not odd disregard the sign. + sign = 1; + } + + lograt(px, precision); + mulrat(px, y, precision); + exprat(px, radix, precision); + } + destroyrat(podd); + } + destroyrat(pxint); + } + (*px)->pp->sign *= sign; +} diff --git a/src/CalcManager/Ratpack/fact.cpp b/src/CalcManager/Ratpack/fact.cpp new file mode 100644 index 00000000..ab11451f --- /dev/null +++ b/src/CalcManager/Ratpack/fact.cpp @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File fact.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains fact(orial) and supporting _gamma functions. +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + +#define ABSRAT(x) (((x)->pp->sign=1),((x)->pq->sign=1)) +#define NEGATE(x) ((x)->pp->sign *= -1) + +//----------------------------------------------------------------------------- +// +// FUNCTION: factrat, _gamma, gamma +// +// ARGUMENTS: x PRAT representation of number to take the sine of +// +// RETURN: factorial of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2j +// n \ ] A 1 A +// A \ -----[ ---- - ---------------] +// / (2j)! n+2j (n+2j+1)(2j+1) +// /__] +// j=0 +// +// / oo +// | n-1 -x __ +// This was derived from | x e dx = | +// | | (n) { = (n-1)! for +integers} +// / 0 +// +// It can be shown that the above series is within precision if A is chosen +// big enough. +// A n precision +// Based on the relation ne = A 10 A was chosen as +// +// precision +// A = ln(Base /n)+1 +// A += n*ln(A) This is close enough for precision > base and n < 1.5 +// +// +//----------------------------------------------------------------------------- + + +void _gamma( PRAT *pn, uint32_t radix, int32_t precision) + +{ + PRAT factorial= nullptr; + PNUMBER count= nullptr; + PRAT tmp= nullptr; + PRAT one_pt_five= nullptr; + PRAT a= nullptr; + PRAT a2= nullptr; + PRAT term= nullptr; + PRAT sum= nullptr; + PRAT err= nullptr; + PRAT mpy= nullptr; + PRAT ratprec = nullptr; + PRAT ratRadix = nullptr; + long oldprec; + + // Set up constants and initial conditions + oldprec = precision; + ratprec = longtorat( oldprec ); + + // Find the best 'A' for convergence to the required precision. + a=longtorat( radix ); + lograt(&a, precision); + mulrat(&a, ratprec, precision); + + // Really is -ln(n)+1, but -ln(n) will be < 1 + // if we scale n between 0.5 and 1.5 + addrat(&a, rat_two, precision); + DUPRAT(tmp,a); + lograt(&tmp, precision); + mulrat(&tmp, *pn, precision); + addrat(&a, tmp, precision); + addrat(&a, rat_one, precision); + + // Calculate the necessary bump in precision and up the precision. + // The following code is equivalent to + // precision += ln(exp(a)*pow(a,n+1.5))-ln(radix)); + DUPRAT(tmp,*pn); + one_pt_five=longtorat( 3L ); + divrat( &one_pt_five, rat_two, precision); + addrat( &tmp, one_pt_five, precision); + DUPRAT(term,a); + powratcomp( &term, tmp, radix, precision); + DUPRAT( tmp, a ); + exprat( &tmp, radix, precision); + mulrat( &term, tmp, precision); + lograt( &term, precision); + ratRadix = longtorat(radix); + DUPRAT(tmp,ratRadix); + lograt( &tmp, precision); + subrat( &term, tmp, precision); + precision += rattolong( term, radix, precision); + + // Set up initial terms for series, refer to series in above comment block. + DUPRAT(factorial,rat_one); // Start factorial out with one + count = longtonum( 0L, BASEX ); + + DUPRAT(mpy,a); + powratcomp(&mpy,*pn, radix, precision); + // a2=a^2 + DUPRAT(a2,a); + mulrat(&a2, a, precision); + + // sum=(1/n)-(a/(n+1)) + DUPRAT(sum,rat_one); + divrat(&sum, *pn, precision); + DUPRAT(tmp,*pn); + addrat(&tmp, rat_one, precision); + DUPRAT(term,a); + divrat(&term, tmp, precision); + subrat(&sum, term, precision); + + DUPRAT(err,ratRadix); + NEGATE(ratprec); + powratcomp(&err,ratprec, radix, precision); + divrat(&err, ratRadix, precision); + + // Just get something not tiny in term + DUPRAT(term, rat_two ); + + // Loop until precision is reached, or asked to halt. + while ( !zerrat( term ) && rat_gt( term, err, precision) ) + { + addrat(pn, rat_two, precision); + + // WARNING: mixing numbers and rationals here. + // for speed and efficiency. + INC(count); + mulnumx(&(factorial->pp),count); + INC(count) + mulnumx(&(factorial->pp),count); + + divrat(&factorial, a2, precision); + + DUPRAT(tmp,*pn); + addrat( &tmp, rat_one, precision); + destroyrat(term); + createrat(term); + DUPNUM(term->pp,count); + DUPNUM(term->pq,num_one); + addrat( &term, rat_one, precision); + mulrat( &term, tmp, precision); + DUPRAT(tmp,a); + divrat( &tmp, term, precision); + + DUPRAT(term,rat_one); + divrat( &term, *pn, precision); + subrat( &term, tmp, precision); + + divrat (&term, factorial, precision); + addrat( &sum, term, precision); + ABSRAT(term); + } + + // Multiply by factor. + mulrat( &sum, mpy, precision); + + // And cleanup + precision = oldprec; + destroyrat(ratprec); + destroyrat(err); + destroyrat(term); + destroyrat(a); + destroyrat(a2); + destroyrat(tmp); + destroyrat(one_pt_five); + + destroynum(count); + + destroyrat(factorial); + destroyrat(*pn); + DUPRAT(*pn,sum); + destroyrat(sum); +} + +void factrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT fact = nullptr; + PRAT frac = nullptr; + PRAT neg_rat_one = nullptr; + + if ( rat_gt( *px, rat_max_fact, precision) || rat_lt( *px, rat_min_fact, precision) ) + { + // Don't attempt factorial of anything too large or small. + throw CALC_E_OVERFLOW; + } + + DUPRAT(fact,rat_one); + + DUPRAT(neg_rat_one,rat_one); + neg_rat_one->pp->sign *= -1; + + DUPRAT( frac, *px ); + fracrat( &frac, radix, precision); + + // Check for negative integers and throw an error. + if ( ( zerrat(frac) || ( LOGRATRADIX(frac) <= -precision) ) && + ( (*px)->pp->sign * (*px)->pq->sign == -1 ) ) + { + throw CALC_E_DOMAIN; + } + while ( rat_gt( *px, rat_zero, precision) && + ( LOGRATRADIX(*px) > -precision) ) + { + mulrat( &fact, *px, precision); + subrat( px, rat_one, precision); + } + + // Added to make numbers 'close enough' to integers use integer factorial. + if ( LOGRATRADIX(*px) <= -precision) + { + DUPRAT((*px),rat_zero); + intrat(&fact, radix, precision); + } + + while ( rat_lt( *px, neg_rat_one, precision) ) + { + addrat( px, rat_one, precision); + divrat( &fact, *px, precision); + } + + if ( rat_neq( *px, rat_zero, precision) ) + { + addrat( px, rat_one, precision); + _gamma( px, radix, precision); + mulrat( px, fact, precision); + } + else + { + DUPRAT(*px,fact); + } + + destroyrat(fact); + destroyrat(frac); + destroyrat(neg_rat_one); +} + diff --git a/src/CalcManager/Ratpack/itrans.cpp b/src/CalcManager/Ratpack/itrans.cpp new file mode 100644 index 00000000..66e6fc62 --- /dev/null +++ b/src/CalcManager/Ratpack/itrans.cpp @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File itrans.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains inverse sin, cos, tan functions for rationals +// +// Special Information +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + + +void ascalerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, int32_t precision) +{ + switch ( angletype ) + { + case ANGLE_RAD: + break; + case ANGLE_DEG: + divrat( pa, two_pi, precision); + mulrat( pa, rat_360, precision); + break; + case ANGLE_GRAD: + divrat( pa, two_pi, precision); + mulrat( pa, rat_400, precision); + break; + } +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: asinrat, _asinrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// sine of +// RETURN: asin of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2 2 +// \ ] (2j+1) X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j+2)*(2j+3) +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +// If abs(x) > 0.85 then an alternate form is used +// pi/2-sgn(x)*asin(sqrt(1-x^2) +// +// +//----------------------------------------------------------------------------- + +void _asinrat( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + DUPRAT(pret,*px); + DUPRAT(thisterm,*px); + DUPNUM(n2,num_one); + + do + { + NEXTTERM(xx,MULNUM(n2) MULNUM(n2) + INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } + while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + DESTROYTAYLOR(); +} + +void asinanglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + asinrat( pa, radix, precision); + ascalerat( pa, angletype, precision); +} + +void asinrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + long sgn; + PRAT pret= nullptr; + PRAT phack= nullptr; + + sgn = (*px)->pp->sign* (*px)->pq->sign; + + (*px)->pp->sign = 1; + (*px)->pq->sign = 1; + + // Avoid the really bad part of the asin curve near +/-1. + DUPRAT(phack,*px); + subrat(&phack, rat_one, precision); + // Since *px might be epsilon near zero we must set it to zero. + if ( rat_le(phack, rat_smallest, precision) && rat_ge(phack, rat_negsmallest, precision) ) + { + destroyrat(phack); + DUPRAT( *px, pi_over_two ); + } + else + { + destroyrat(phack); + if ( rat_gt( *px, pt_eight_five, precision) ) + { + if ( rat_gt( *px, rat_one, precision) ) + { + subrat( px, rat_one, precision); + if ( rat_gt( *px, rat_smallest, precision) ) + { + throw( CALC_E_DOMAIN ); + } + else + { + DUPRAT(*px,rat_one); + } + } + DUPRAT(pret,*px); + mulrat( px, pret, precision); + (*px)->pp->sign *= -1; + addrat( px, rat_one, precision); + rootrat( px, rat_two, radix, precision); + _asinrat( px, precision); + (*px)->pp->sign *= -1; + addrat( px, pi_over_two, precision); + destroyrat(pret); + } + else + { + _asinrat( px, precision); + } + } + (*px)->pp->sign = sgn; + (*px)->pq->sign = 1; +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: acosrat, _acosrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// cosine of +// RETURN: acos of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2 2 +// \ ] (2j+1) X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j+2)*(2j+3) +// /__] +// j=0 +// +// thisterm = 1 ; and stop when thisterm < precision used. +// 0 n +// +// In this case pi/2-asin(x) is used. At least for now _acosrat isn't +// called. +// +//----------------------------------------------------------------------------- + +void acosanglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + acosrat( pa, radix, precision); + ascalerat( pa, angletype, precision); +} + +void _acosrat( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + + createrat(thisterm); + thisterm->pp=longtonum( 1L, BASEX ); + thisterm->pq=longtonum( 1L, BASEX ); + + DUPNUM(n2,num_one); + + do + { + NEXTTERM(xx,MULNUM(n2) MULNUM(n2) + INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } + while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + +void acosrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + long sgn; + + sgn = (*px)->pp->sign*(*px)->pq->sign; + + (*px)->pp->sign = 1; + (*px)->pq->sign = 1; + + if ( rat_equ( *px, rat_one, precision) ) + { + if ( sgn == -1 ) + { + DUPRAT(*px,pi); + } + else + { + DUPRAT( *px, rat_zero ); + } + } + else + { + (*px)->pp->sign = sgn; + asinrat( px, radix, precision); + (*px)->pp->sign *= -1; + addrat(px, pi_over_two, precision); + } +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: atanrat, _atanrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// hyperbolic tangent of +// +// RETURN: atanh of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2 +// \ ] (2j)*X (-1^j) +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j+2) +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +// If abs(x) > 0.85 then an alternate form is used +// asin(x/sqrt(q+x^2)) +// +// And if abs(x) > 2.0 then this form is used. +// +// pi/2 - atan(1/x) +// +//----------------------------------------------------------------------------- + +void atananglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + atanrat( pa, radix, precision); + ascalerat( pa, angletype, precision); +} + +void _atanrat( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + + DUPRAT(pret,*px); + DUPRAT(thisterm,*px); + + DUPNUM(n2,num_one); + + xx->pp->sign *= -1; + + do { + NEXTTERM(xx,MULNUM(n2) INC(n2) INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + +void atanrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + long sgn; + PRAT tmpx= nullptr; + + sgn = (*px)->pp->sign * (*px)->pq->sign; + + (*px)->pp->sign = 1; + (*px)->pq->sign = 1; + + if ( rat_gt( (*px), pt_eight_five, precision) ) + { + if ( rat_gt( (*px), rat_two, precision) ) + { + (*px)->pp->sign = sgn; + (*px)->pq->sign = 1; + DUPRAT(tmpx,rat_one); + divrat(&tmpx, (*px), precision); + _atanrat(&tmpx, precision); + tmpx->pp->sign = sgn; + tmpx->pq->sign = 1; + DUPRAT(*px,pi_over_two); + subrat(px, tmpx, precision); + destroyrat( tmpx ); + } + else + { + (*px)->pp->sign = sgn; + DUPRAT(tmpx,*px); + mulrat( &tmpx, *px, precision); + addrat( &tmpx, rat_one, precision); + rootrat( &tmpx, rat_two, radix, precision); + divrat( px, tmpx, precision); + destroyrat( tmpx ); + asinrat( px, radix, precision); + (*px)->pp->sign = sgn; + (*px)->pq->sign = 1; + } + } + else + { + (*px)->pp->sign = sgn; + (*px)->pq->sign = 1; + _atanrat( px, precision); + } + if ( rat_gt( *px, pi_over_two, precision) ) + { + subrat( px, pi, precision); + } +} + diff --git a/src/CalcManager/Ratpack/itransh.cpp b/src/CalcManager/Ratpack/itransh.cpp new file mode 100644 index 00000000..7b75e703 --- /dev/null +++ b/src/CalcManager/Ratpack/itransh.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File itransh.c +// Copyright (C) 1995-97 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains inverse hyperbolic sin, cos, and tan functions. +// +// Special Information +// +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + + +//----------------------------------------------------------------------------- +// +// FUNCTION: asinhrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// hyperbolic sine of +// RETURN: asinh of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2 2 +// \ ] -(2j+1) X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j+2)*(2j+3) +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +// For abs(x) < .85, and +// +// asinh(x) = log(x+sqrt(x^2+1)) +// +// For abs(x) >= .85 +// +//----------------------------------------------------------------------------- + +void asinhrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT neg_pt_eight_five = nullptr; + + DUPRAT(neg_pt_eight_five,pt_eight_five); + neg_pt_eight_five->pp->sign *= -1; + if ( rat_gt( *px, pt_eight_five, precision) || rat_lt( *px, neg_pt_eight_five, precision) ) + { + PRAT ptmp = nullptr; + DUPRAT(ptmp,(*px)); + mulrat(&ptmp, *px, precision); + addrat(&ptmp, rat_one, precision); + rootrat(&ptmp, rat_two, radix, precision); + addrat(px, ptmp, precision); + lograt(px, precision); + destroyrat(ptmp); + } + else + { + CREATETAYLOR(); + xx->pp->sign *= -1; + + DUPRAT(pret,(*px)); + DUPRAT(thisterm,(*px)); + + DUPNUM(n2,num_one); + + do + { + NEXTTERM(xx,MULNUM(n2) MULNUM(n2) + INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } + while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); + } + destroyrat(neg_pt_eight_five); +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: acoshrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// hyperbolic cose of +// RETURN: acosh of x in PRAT form. +// +// EXPLANATION: This uses +// +// acosh(x)=ln(x+sqrt(x^2-1)) +// +// For x >= 1 +// +//----------------------------------------------------------------------------- + +void acoshrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + if ( rat_lt( *px, rat_one, precision) ) + { + throw CALC_E_DOMAIN; + } + else + { + PRAT ptmp = nullptr; + DUPRAT(ptmp,(*px)); + mulrat(&ptmp, *px, precision); + subrat(&ptmp, rat_one, precision); + rootrat(&ptmp,rat_two, radix, precision); + addrat(px, ptmp, precision); + lograt(px, precision); + destroyrat(ptmp); + } +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: atanhrat +// +// ARGUMENTS: x PRAT representation of number to take the inverse +// hyperbolic tangent of +// +// RETURN: atanh of x in PRAT form. +// +// EXPLANATION: This uses +// +// 1 x+1 +// atanh(x) = -*ln(----) +// 2 x-1 +// +//----------------------------------------------------------------------------- + +void atanhrat( PRAT *px, int32_t precision) + +{ + PRAT ptmp = nullptr; + DUPRAT(ptmp,(*px)); + subrat(&ptmp, rat_one, precision); + addrat(px, rat_one, precision); + divrat(px, ptmp, precision); + (*px)->pp->sign *= -1; + lograt(px, precision); + divrat(px, rat_two, precision); + destroyrat(ptmp); +} + diff --git a/src/CalcManager/Ratpack/logic.cpp b/src/CalcManager/Ratpack/logic.cpp new file mode 100644 index 00000000..ed598eca --- /dev/null +++ b/src/CalcManager/Ratpack/logic.cpp @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//--------------------------------------------------------------------------- +// Package Title ratpak +// File num.c +// Copyright (C) 1995-99 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains routines for and, or, xor, not and other support +// +//--------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + + +void lshrat( PRAT *pa, PRAT b, uint32_t radix, int32_t precision) + +{ + PRAT pwr= nullptr; + long intb; + + intrat(pa, radix, precision); + if ( !zernum( (*pa)->pp ) ) + { + // If input is zero we're done. + if ( rat_gt( b, rat_max_exp, precision) ) + { + // Don't attempt lsh of anything big + throw( CALC_E_DOMAIN ); + } + intb = rattolong(b, radix, precision); + DUPRAT(pwr,rat_two); + ratpowlong(&pwr, intb, precision); + mulrat(pa, pwr, precision); + destroyrat(pwr); + } +} + +void rshrat( PRAT *pa, PRAT b, uint32_t radix, int32_t precision) + +{ + PRAT pwr= nullptr; + long intb; + + intrat(pa, radix, precision); + if ( !zernum( (*pa)->pp ) ) + { + // If input is zero we're done. + if ( rat_lt( b, rat_min_exp, precision) ) + { + // Don't attempt rsh of anything big and negative. + throw( CALC_E_DOMAIN ); + } + intb = rattolong(b, radix, precision); + DUPRAT(pwr,rat_two); + ratpowlong(&pwr, intb, precision); + divrat(pa, pwr, precision); + destroyrat(pwr); + } +} + +void boolrat( PRAT *pa, PRAT b, int func, uint32_t radix, int32_t precision); +void boolnum( PNUMBER *pa, PNUMBER b, int func ); + + +enum { + FUNC_AND, + FUNC_OR, + FUNC_XOR +} BOOL_FUNCS; + +void andrat( PRAT *pa, PRAT b, uint32_t radix, int32_t precision) + +{ + boolrat( pa, b, FUNC_AND, radix, precision); +} + +void orrat( PRAT *pa, PRAT b, uint32_t radix, int32_t precision) + +{ + boolrat( pa, b, FUNC_OR, radix, precision); +} + +void xorrat( PRAT *pa, PRAT b, uint32_t radix, int32_t precision) + +{ + boolrat( pa, b, FUNC_XOR, radix, precision); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: boolrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes pointer. +// +// DESCRIPTION: Does the rational equivalent of *pa op= b; +// +//--------------------------------------------------------------------------- + +void boolrat( PRAT *pa, PRAT b, int func, uint32_t radix, int32_t precision) + +{ + PRAT tmp= nullptr; + intrat( pa, radix, precision); + DUPRAT(tmp,b); + intrat( &tmp, radix, precision); + + boolnum( &((*pa)->pp), tmp->pp, func ); + destroyrat(tmp); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: boolnum +// +// ARGUMENTS: pointer to a number a second number +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa &= b. +// radix doesn't matter for logicals. +// WARNING: Assumes numbers are unsigned. +// +//--------------------------------------------------------------------------- + +void boolnum( PNUMBER *pa, PNUMBER b, int func ) + +{ + PNUMBER c= nullptr; + PNUMBER a= nullptr; + MANTTYPE *pcha; + MANTTYPE *pchb; + MANTTYPE *pchc; + long cdigits; + long mexp; + MANTTYPE da; + MANTTYPE db; + + a=*pa; + cdigits = max( a->cdigit+a->exp, b->cdigit+b->exp ) - + min( a->exp, b->exp ); + createnum( c, cdigits ); + c->exp = min( a->exp, b->exp ); + mexp = c->exp; + c->cdigit = cdigits; + pcha = a->mant; + pchb = b->mant; + pchc = c->mant; + for ( ;cdigits > 0; cdigits--, mexp++ ) + { + da = ( ( ( mexp >= a->exp ) && ( cdigits + a->exp - c->exp > + (c->cdigit - a->cdigit) ) ) ? + *pcha++ : 0 ); + db = ( ( ( mexp >= b->exp ) && ( cdigits + b->exp - c->exp > + (c->cdigit - b->cdigit) ) ) ? + *pchb++ : 0 ); + switch ( func ) + { + case FUNC_AND: + *pchc++ = da & db; + break; + case FUNC_OR: + *pchc++ = da | db; + break; + case FUNC_XOR: + *pchc++ = da ^ db; + break; + } + } + c->sign = a->sign; + while ( c->cdigit > 1 && *(--pchc) == 0 ) + { + c->cdigit--; + } + destroynum( *pa ); + *pa=c; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: modrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes pointer. +// +// DESCRIPTION: Does the rational equivalent of frac(*pa); +// +//----------------------------------------------------------------------------- + +void modrat( PRAT *pa, PRAT b ) + +{ + PRAT tmp = nullptr; + + if ( zerrat( b ) ) + { + throw CALC_E_INDEFINITE; + } + DUPRAT(tmp,b); + + mulnumx( &((*pa)->pp), tmp->pq ); + mulnumx( &(tmp->pp), (*pa)->pq ); + remnum( &((*pa)->pp), tmp->pp, BASEX ); + mulnumx( &((*pa)->pq), tmp->pq ); + + //Get *pa back in the integer over integer form. + RENORMALIZE(*pa); + + destroyrat( tmp ); +} + diff --git a/src/CalcManager/Ratpack/num.cpp b/src/CalcManager/Ratpack/num.cpp new file mode 100644 index 00000000..07c93b05 --- /dev/null +++ b/src/CalcManager/Ratpack/num.cpp @@ -0,0 +1,655 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File num.c +// Copyright (C) 1995-97 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains number routines for add, mul, div, rem and other support +// and longs. +// +// Special Information +// +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + +using namespace std; + +//---------------------------------------------------------------------------- +// +// FUNCTION: addnum +// +// ARGUMENTS: pointer to a number a second number, and the +// radix. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa += b. +// Assumes radix is the base of both numbers. +// +// ALGORITHM: Adds each digit from least significant to most +// significant. +// +// +//---------------------------------------------------------------------------- + +void _addnum( PNUMBER *pa, PNUMBER b, uint32_t radix); + +void __inline addnum( PNUMBER *pa, PNUMBER b, uint32_t radix) + +{ + if ( b->cdigit > 1 || b->mant[0] != 0 ) + { // If b is zero we are done. + if ( (*pa)->cdigit > 1 || (*pa)->mant[0] != 0 ) + { // pa and b are both nonzero. + _addnum( pa, b, radix); + } + else + { // if pa is zero and b isn't just copy b. + DUPNUM(*pa,b); + } + } +} + +void _addnum( PNUMBER *pa, PNUMBER b, uint32_t radix) + +{ + PNUMBER c= nullptr; // c will contain the result. + PNUMBER a= nullptr; // a is the dereferenced number pointer from *pa + MANTTYPE *pcha; // pcha is a pointer to the mantissa of a. + MANTTYPE *pchb; // pchb is a pointer to the mantissa of b. + MANTTYPE *pchc; // pchc is a pointer to the mantissa of c. + long cdigits; // cdigits is the max count of the digits results + // used as a counter. + long mexp; // mexp is the exponent of the result. + MANTTYPE da; // da is a single 'digit' after possible padding. + MANTTYPE db; // db is a single 'digit' after possible padding. + MANTTYPE cy=0; // cy is the value of a carry after adding two 'digits' + long fcompla = 0; // fcompla is a flag to signal a is negative. + long fcomplb = 0; // fcomplb is a flag to signal b is negative. + + a=*pa; + + + // Calculate the overlap of the numbers after alignment, this includes + // necessary padding 0's + cdigits = max( a->cdigit+a->exp, b->cdigit+b->exp ) - + min( a->exp, b->exp ); + + createnum( c, cdigits + 1 ); + c->exp = min( a->exp, b->exp ); + mexp = c->exp; + c->cdigit = cdigits; + pcha = a->mant; + pchb = b->mant; + pchc = c->mant; + + // Figure out the sign of the numbers + if ( a->sign != b->sign ) + { + cy = 1; + fcompla = ( a->sign == -1 ); + fcomplb = ( b->sign == -1 ); + } + + // Loop over all the digits, real and 0 padded. Here we know a and b are + // aligned + for ( ;cdigits > 0; cdigits--, mexp++ ) + { + + // Get digit from a, taking padding into account. + da = ( ( ( mexp >= a->exp ) && ( cdigits + a->exp - c->exp > + (c->cdigit - a->cdigit) ) ) ? + *pcha++ : 0 ); + // Get digit from b, taking padding into account. + db = ( ( ( mexp >= b->exp ) && ( cdigits + b->exp - c->exp > + (c->cdigit - b->cdigit) ) ) ? + *pchb++ : 0 ); + + // Handle complementing for a and b digit. Might be a better way, but + // haven't found it yet. + if ( fcompla ) + { + da = (MANTTYPE)(radix) - 1 - da; + } + if ( fcomplb ) + { + db = (MANTTYPE)(radix) - 1 - db; + } + + // Update carry as necessary + cy = da + db + cy; + *pchc++ = (MANTTYPE)(cy % (MANTTYPE)radix); + cy /= (MANTTYPE)radix; + } + + // Handle carry from last sum as extra digit + if ( cy && !(fcompla || fcomplb) ) + { + *pchc++ = cy; + c->cdigit++; + } + + // Compute sign of result + if ( !(fcompla || fcomplb) ) + { + c->sign = a->sign; + } + else + { + if ( cy ) + { + c->sign = 1; + } + else + { + // In this particular case an overflow or underflow has occoured + // and all the digits need to be complemented, at one time an + // attempt to handle this above was made, it turned out to be much + // slower on average. + c->sign = -1; + cy = 1; + for ( ( cdigits = c->cdigit ), (pchc = c->mant); + cdigits > 0; + cdigits-- ) + { + cy = (MANTTYPE)radix - (MANTTYPE)1 - *pchc + cy; + *pchc++ = (MANTTYPE)( cy % (MANTTYPE)radix); + cy /= (MANTTYPE)radix; + } + } + } + + // Remove leading zeroes, remember digits are in order of + // increasing significance. i.e. 100 would be 0,0,1 + while ( c->cdigit > 1 && *(--pchc) == 0 ) + { + c->cdigit--; + } + destroynum( *pa ); + *pa=c; +} + +//---------------------------------------------------------------------------- +// +// FUNCTION: mulnum +// +// ARGUMENTS: pointer to a number a second number, and the +// radix. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa *= b. +// Assumes radix is the radix of both numbers. This algorithm is the +// same one you learned in gradeschool. +// +//---------------------------------------------------------------------------- + +void _mulnum( PNUMBER *pa, PNUMBER b, uint32_t radix); + +void __inline mulnum( PNUMBER *pa, PNUMBER b, uint32_t radix) + +{ + if ( b->cdigit > 1 || b->mant[0] != 1 || b->exp != 0 ) + { // If b is one we don't multiply exactly. + if ( (*pa)->cdigit > 1 || (*pa)->mant[0] != 1 || (*pa)->exp != 0 ) + { // pa and b are both nonone. + _mulnum( pa, b, radix); + } + else + { // if pa is one and b isn't just copy b, and adjust the sign. + long sign = (*pa)->sign; + DUPNUM(*pa,b); + (*pa)->sign *= sign; + } + } + else + { // But we do have to set the sign. + (*pa)->sign *= b->sign; + } +} + +void _mulnum( PNUMBER *pa, PNUMBER b, uint32_t radix) + +{ + PNUMBER c= nullptr; // c will contain the result. + PNUMBER a= nullptr; // a is the dereferenced number pointer from *pa + MANTTYPE *pcha; // pcha is a pointer to the mantissa of a. + MANTTYPE *pchb; // pchb is a pointer to the mantissa of b. + MANTTYPE *pchc; // pchc is a pointer to the mantissa of c. + MANTTYPE *pchcoffset; // pchcoffset, is the anchor location of the next + // single digit multiply partial result. + long iadigit = 0; // Index of digit being used in the first number. + long ibdigit = 0; // Index of digit being used in the second number. + MANTTYPE da = 0; // da is the digit from the fist number. + TWO_MANTTYPE cy = 0; // cy is the carry resulting from the addition of + // a multiplied row into the result. + TWO_MANTTYPE mcy = 0; // mcy is the resultant from a single + // multiply, AND the carry of that multiply. + long icdigit = 0; // Index of digit being calculated in final result. + + a=*pa; + ibdigit = a->cdigit + b->cdigit - 1; + createnum( c, ibdigit + 1 ); + c->cdigit = ibdigit; + c->sign = a->sign * b->sign; + + c->exp = a->exp + b->exp; + pcha = a->mant; + pchcoffset = c->mant; + + for ( iadigit = a->cdigit; iadigit > 0; iadigit-- ) + { + da = *pcha++; + pchb = b->mant; + + // Shift pchc, and pchcoffset, one for each digit + pchc = pchcoffset++; + + for ( ibdigit = b->cdigit; ibdigit > 0; ibdigit-- ) + { + cy = 0; + mcy = (TWO_MANTTYPE)da * *pchb; + if ( mcy ) + { + icdigit = 0; + if ( ibdigit == 1 && iadigit == 1 ) + { + c->cdigit++; + } + } + // If result is nonzero, or while result of carry is nonzero... + while ( mcy || cy ) + { + + // update carry from addition(s) and multiply. + cy += (TWO_MANTTYPE)pchc[icdigit]+(mcy%(TWO_MANTTYPE)radix); + + // update result digit from + pchc[icdigit++]=(MANTTYPE)(cy%(TWO_MANTTYPE)radix); + + // update carries from + mcy /= (TWO_MANTTYPE)radix; + cy /= (TWO_MANTTYPE)radix; + } + + pchb++; + pchc++; + } + } + + // prevent different kinds of zeros, by stripping leading duplicate zeroes. + // digits are in order of increasing significance. + while ( c->cdigit > 1 && c->mant[c->cdigit-1] == 0 ) + { + c->cdigit--; + } + + destroynum( *pa ); + *pa=c; +} + + +//---------------------------------------------------------------------------- +// +// FUNCTION: remnum +// +// ARGUMENTS: pointer to a number a second number, and the +// radix. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa %= b. +// Repeatedly subtracts off powers of 2 of b until *pa < b. +// +// +//---------------------------------------------------------------------------- + +void remnum( PNUMBER *pa, PNUMBER b, uint32_t radix) + +{ + PNUMBER tmp = nullptr; // tmp is the working remainder. + PNUMBER lasttmp = nullptr; // lasttmp is the last remainder which worked. + + // Once *pa is less than b, *pa is the remainder. + while ( !lessnum( *pa, b ) ) + { + DUPNUM( tmp, b ); + if ( lessnum( tmp, *pa ) ) + { + // Start off close to the right answer for subtraction. + tmp->exp = (*pa)->cdigit+(*pa)->exp - tmp->cdigit; + if ( MSD(*pa) <= MSD(tmp) ) + { + // Don't take the chance that the numbers are equal. + tmp->exp--; + } + } + + destroynum( lasttmp ); + lasttmp=longtonum( 0, radix); + + while ( lessnum( tmp, *pa ) ) + { + DUPNUM( lasttmp, tmp ); + addnum( &tmp, tmp, radix); + } + + if ( lessnum( *pa, tmp ) ) + { + // too far, back up... + destroynum( tmp ); + tmp=lasttmp; + lasttmp= nullptr; + } + + // Subtract the working remainder from the remainder holder. + tmp->sign = -1*(*pa)->sign; + addnum( pa, tmp, radix); + + destroynum( tmp ); + destroynum( lasttmp ); + + } +} + + +//--------------------------------------------------------------------------- +// +// FUNCTION: divnum +// +// ARGUMENTS: pointer to a number a second number, and the +// radix. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the number equivalent of *pa /= b. +// Assumes radix is the radix of both numbers. +// +//--------------------------------------------------------------------------- + +void _divnum( PNUMBER *pa, PNUMBER b, uint32_t radix, int32_t precision); + +void __inline divnum( PNUMBER *pa, PNUMBER b, uint32_t radix, int32_t precision) + +{ + if ( b->cdigit > 1 || b->mant[0] != 1 || b->exp != 0 ) + { + // b is not one + _divnum( pa, b, radix, precision); + } + else + { // But we do have to set the sign. + (*pa)->sign *= b->sign; + } +} + +void _divnum( PNUMBER *pa, PNUMBER b, uint32_t radix, int32_t precision) +{ + PNUMBER a = *pa; + long thismax = precision + 2; + if (thismax < a->cdigit) + { + thismax = a->cdigit; + } + + if (thismax < b->cdigit) + { + thismax = b->cdigit; + } + + PNUMBER c = nullptr; + createnum(c, thismax + 1); + c->exp = (a->cdigit + a->exp) - (b->cdigit + b->exp) + 1; + c->sign = a->sign * b->sign; + + MANTTYPE *ptrc = c->mant + thismax; + PNUMBER rem = nullptr; + PNUMBER tmp = nullptr; + DUPNUM(rem, a); + DUPNUM(tmp, b); + tmp->sign = a->sign; + rem->exp = b->cdigit + b->exp - rem->cdigit; + + // Build a table of multiplications of the divisor, this is quicker for + // more than radix 'digits' + list numberList{ longtonum(0L, radix) }; + for (unsigned long i = 1; i < radix; i++) + { + PNUMBER newValue = nullptr; + DUPNUM(newValue, numberList.front()); + addnum(&newValue, tmp, radix); + + numberList.emplace_front(newValue); + } + destroynum(tmp); + + long digit; + long cdigits = 0; + while (cdigits++ < thismax && !zernum(rem)) + { + digit = radix - 1; + PNUMBER multiple = nullptr; + for (const auto& num : numberList) + { + if (!lessnum(rem, num) || !--digit) + { + multiple = num; + break; + } + } + + if (digit) + { + multiple->sign *= -1; + addnum(&rem, multiple, radix); + multiple->sign *= -1; + } + rem->exp++; + *ptrc-- = (MANTTYPE)digit; + } + cdigits--; + + if (c->mant != ++ptrc) + { + memmove(c->mant, ptrc, (int)(cdigits * sizeof(MANTTYPE))); + } + + // Cleanup table structure + for (auto& num : numberList) + { + destroynum(num); + } + + if (!cdigits) + { + c->cdigit = 1; + c->exp = 0; + } + else + { + c->cdigit = cdigits; + c->exp -= cdigits; + while (c->cdigit > 1 && c->mant[c->cdigit - 1] == 0) + { + c->cdigit--; + } + } + destroynum(rem); + + destroynum(*pa); + *pa = c; +} + + +//--------------------------------------------------------------------------- +// +// FUNCTION: equnum +// +// ARGUMENTS: two numbers. +// +// RETURN: Boolean +// +// DESCRIPTION: Does the number equivalent of ( a == b ) +// Only assumes that a and b are the same radix. +// +//--------------------------------------------------------------------------- + +bool equnum( PNUMBER a, PNUMBER b ) + +{ + long diff; + MANTTYPE *pa; + MANTTYPE *pb; + long cdigits; + long ccdigits; + MANTTYPE da; + MANTTYPE db; + + diff = ( a->cdigit + a->exp ) - ( b->cdigit + b->exp ); + if ( diff < 0 ) + { + // If the exponents are different, these are different numbers. + return false; + } + else + { + if ( diff > 0 ) + { + // If the exponents are different, these are different numbers. + return false; + } + else + { + // OK the exponents match. + pa = a->mant; + pb = b->mant; + pa += a->cdigit - 1; + pb += b->cdigit - 1; + cdigits = max( a->cdigit, b->cdigit ); + ccdigits = cdigits; + + // Loop over all digits until we run out of digits or there is a + // difference in the digits. + for ( ;cdigits > 0; cdigits-- ) + { + da = ( (cdigits > (ccdigits - a->cdigit) ) ? + *pa-- : 0 ); + db = ( (cdigits > (ccdigits - b->cdigit) ) ? + *pb-- : 0 ); + if ( da != db ) + { + return false; + } + } + + // In this case, they are equal. + return true; + } + } +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: lessnum +// +// ARGUMENTS: two numbers. +// +// RETURN: Boolean +// +// DESCRIPTION: Does the number equivalent of ( abs(a) < abs(b) ) +// Only assumes that a and b are the same radix, WARNING THIS IS AN. +// UNSIGNED COMPARE! +// +//--------------------------------------------------------------------------- + +bool lessnum( PNUMBER a, PNUMBER b ) + +{ + long diff; + MANTTYPE *pa; + MANTTYPE *pb; + long cdigits; + long ccdigits; + MANTTYPE da; + MANTTYPE db; + + + diff = ( a->cdigit + a->exp ) - ( b->cdigit + b->exp ); + if ( diff < 0 ) + { + // The exponent of a is less than b + return true; + } + else + { + if ( diff > 0 ) + { + return false; + } + else + { + pa = a->mant; + pb = b->mant; + pa += a->cdigit - 1; + pb += b->cdigit - 1; + cdigits = max( a->cdigit, b->cdigit ); + ccdigits = cdigits; + for ( ;cdigits > 0; cdigits-- ) + { + da = ( (cdigits > (ccdigits - a->cdigit) ) ? + *pa-- : 0 ); + db = ( (cdigits > (ccdigits - b->cdigit) ) ? + *pb-- : 0 ); + diff = da-db; + if ( diff ) + { + return( diff < 0 ); + } + } + // In this case, they are equal. + return false; + } + } +} + +//---------------------------------------------------------------------------- +// +// FUNCTION: zernum +// +// ARGUMENTS: number +// +// RETURN: Boolean +// +// DESCRIPTION: Does the number equivalent of ( !a ) +// +//---------------------------------------------------------------------------- + +bool zernum( PNUMBER a ) + +{ + long length; + MANTTYPE *pcha; + length = a->cdigit; + pcha = a->mant; + + // loop over all the digits until you find a nonzero or until you run + // out of digits + while ( length-- > 0 ) + { + if ( *pcha++ ) + { + // One of the digits isn't zero, therefore the number isn't zero + return false; + } + } + // All of the digits are zero, therefore the number is zero + return true; +} diff --git a/src/CalcManager/Ratpack/rat.cpp b/src/CalcManager/Ratpack/rat.cpp new file mode 100644 index 00000000..3d1023d8 --- /dev/null +++ b/src/CalcManager/Ratpack/rat.cpp @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File rat.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains mul, div, add, and other support functions for rationals. +// +// +// +//----------------------------------------------------------------------------- + +#include "pch.h" +#include "ratpak.h" + +using namespace std; + +//----------------------------------------------------------------------------- +// +// FUNCTION: gcdrat +// +// ARGUMENTS: pointer to a rational. +// +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Divides p and q in rational by the G.C.D. +// of both. It was hoped this would speed up some +// calculations, and until the above trimming was done it +// did, but after trimming gcdratting, only slows things +// down. +// +//----------------------------------------------------------------------------- + +void gcdrat( PRAT *pa, uint32_t radix, int32_t precision) + +{ + PNUMBER pgcd= nullptr; + PRAT a= nullptr; + + a=*pa; + pgcd = gcd( a->pp, a->pq ); + + if ( !zernum( pgcd ) ) + { + divnumx( &(a->pp), pgcd, precision); + divnumx( &(a->pq), pgcd, precision); + } + + destroynum( pgcd ); + *pa=a; + + RENORMALIZE(*pa); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: fracrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes pointer. +// +// DESCRIPTION: Does the rational equivalent of frac(*pa); +// +//----------------------------------------------------------------------------- + +void fracrat( PRAT *pa , uint32_t radix, int32_t precision) +{ + // Only do the intrat operation if number is nonzero. + // and only if the bottom part is not one. + if ( !zernum( (*pa)->pp ) && !equnum( (*pa)->pq, num_one ) ) + { + wstring ratStr = RatToString(*pa, FMT_FLOAT, radix, precision); + PNUMBER pnum = StringToNumber(ratStr, radix, precision); + + destroyrat( *pa ); + *pa = numtorat( pnum, radix); + destroynum( pnum ); + } + + remnum( &((*pa)->pp), (*pa)->pq, BASEX ); + + //Get *pa back in the integer over integer form. + RENORMALIZE(*pa); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: mulrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the rational equivalent of *pa *= b. +// Assumes radix is the radix of both numbers. +// +//----------------------------------------------------------------------------- + +void mulrat( PRAT *pa, PRAT b, int32_t precision) + + { + // Only do the multiply if it isn't zero. + if ( !zernum( (*pa)->pp ) ) + { + mulnumx( &((*pa)->pp), b->pp ); + mulnumx( &((*pa)->pq), b->pq ); + trimit(pa, precision); + } + else + { + // If it is zero, blast a one in the denominator. + DUPNUM( ((*pa)->pq), num_one ); + } + +#ifdef MULGCD + gcdrat( pa ); +#endif + +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: divrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the rational equivalent of *pa /= b. +// Assumes radix is the radix of both numbers. +// +//----------------------------------------------------------------------------- + + +void divrat( PRAT *pa, PRAT b, int32_t precision) + +{ + + if ( !zernum( (*pa)->pp ) ) + { + // Only do the divide if the top isn't zero. + mulnumx( &((*pa)->pp), b->pq ); + mulnumx( &((*pa)->pq), b->pp ); + + if ( zernum( (*pa)->pq ) ) + { + // raise an exception if the bottom is 0. + throw( CALC_E_DIVIDEBYZERO ); + } + trimit(pa, precision); + } + else + { + // Top is zero. + if ( zerrat( b ) ) + { + // If bottom is zero + // 0 / 0 is indefinite, raise an exception. + throw( CALC_E_INDEFINITE ); + } + else + { + // 0/x make a unique 0. + DUPNUM( ((*pa)->pq), num_one ); + } + } + +#ifdef DIVGCD + gcdrat( pa ); +#endif + +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: subrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the rational equivalent of *pa += b. +// Assumes base is internal througought. +// +//----------------------------------------------------------------------------- + +void subrat( PRAT *pa, PRAT b, int32_t precision) + +{ + b->pp->sign *= -1; + addrat( pa, b, precision); + b->pp->sign *= -1; +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: addrat +// +// ARGUMENTS: pointer to a rational a second rational. +// +// RETURN: None, changes first pointer. +// +// DESCRIPTION: Does the rational equivalent of *pa += b. +// Assumes base is internal througought. +// +//----------------------------------------------------------------------------- + +void addrat( PRAT *pa, PRAT b, int32_t precision) + +{ + PNUMBER bot= nullptr; + + if ( equnum( (*pa)->pq, b->pq ) ) + { + // Very special case, q's match., + // make sure signs are involved in the calculation + // we have to do this since the optimization here is only + // working with the top half of the rationals. + (*pa)->pp->sign *= (*pa)->pq->sign; + (*pa)->pq->sign = 1; + b->pp->sign *= b->pq->sign; + b->pq->sign = 1; + addnum( &((*pa)->pp), b->pp, BASEX ); + } + else + { + // Usual case q's aren't the same. + DUPNUM( bot, (*pa)->pq ); + mulnumx( &bot, b->pq ); + mulnumx( &((*pa)->pp), b->pq ); + mulnumx( &((*pa)->pq), b->pp ); + addnum( &((*pa)->pp), (*pa)->pq, BASEX ); + destroynum( (*pa)->pq ); + (*pa)->pq = bot; + trimit(pa, precision); + + // Get rid of negative zeroes here. + (*pa)->pp->sign *= (*pa)->pq->sign; + (*pa)->pq->sign = 1; + } + +#ifdef ADDGCD + gcdrat( pa ); +#endif + +} + + + +//----------------------------------------------------------------------------- +// +// FUNCTION: rootrat +// +// PARAMETERS: y prat representation of number to take the root of +// n prat representation of the root to take. +// +// RETURN: bth root of a in rat form. +// +// EXPLANATION: This is now a stub function to powrat(). +// +//----------------------------------------------------------------------------- + +void rootrat( PRAT *py, PRAT n, uint32_t radix, int32_t precision) +{ + // Initialize 1/n + PRAT oneovern= nullptr; + DUPRAT(oneovern,rat_one); + divrat(&oneovern, n, precision); + + powrat(py, oneovern, radix, precision); + + destroyrat(oneovern); +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: zerrat +// +// ARGUMENTS: Rational number. +// +// RETURN: Boolean +// +// DESCRIPTION: Returns true if input is zero. +// False otherwise. +// +//----------------------------------------------------------------------------- + +bool zerrat( PRAT a ) + +{ + return( zernum(a->pp) ); +} + + diff --git a/src/CalcManager/Ratpack/ratconst.h b/src/CalcManager/Ratpack/ratconst.h new file mode 100644 index 00000000..d208b33c --- /dev/null +++ b/src/CalcManager/Ratpack/ratconst.h @@ -0,0 +1,482 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Autogenerated by _dumprawrat in support.c +NUMBER init_num_one= { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_num_two= { + 1, + 1, + 0, + { 2,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_num_five= { + 1, + 1, + 0, + { 5,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_num_six= { + 1, + 1, + 0, + { 6,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_num_ten= { + 1, + 1, + 0, + { 10,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_smallest = { + 1, + 1, + 0, + { 1,} +}; +NUMBER init_q_rat_smallest = { + 1, + 4, + 0, + { 0, 190439170, 901055854, 10097,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_negsmallest = { + -1, + 1, + 0, + { 1,} +}; +NUMBER init_q_rat_negsmallest = { + 1, + 4, + 0, + { 0, 190439170, 901055854, 10097,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_pt_eight_five = { + 1, + 1, + 0, + { 85,} +}; +NUMBER init_q_pt_eight_five = { + 1, + 1, + 0, + { 100,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_six = { + 1, + 1, + 0, + { 6,} +}; +NUMBER init_q_rat_six = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_two = { + 1, + 1, + 0, + { 2,} +}; +NUMBER init_q_rat_two = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_zero = { + 1, + 1, + 0, + { 0,} +}; +NUMBER init_q_rat_zero = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_one = { + 1, + 1, + 0, + { 1,} +}; +NUMBER init_q_rat_one = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_neg_one = { + -1, + 1, + 0, + { 1,} +}; +NUMBER init_q_rat_neg_one = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_half = { + 1, + 1, + 0, + { 1,} +}; +NUMBER init_q_rat_half = { + 1, + 1, + 0, + { 2,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_ten = { + 1, + 1, + 0, + { 10,} +}; +NUMBER init_q_rat_ten = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_pi = { + 1, + 6, + 0, + { 125527896, 283898350, 1960493936, 1672850762, 1288168272, 8,} +}; +NUMBER init_q_pi = { + 1, + 6, + 0, + { 1288380402, 1120116153, 1860424692, 1944118326, 1583591604, 2,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_two_pi = { + 1, + 6, + 0, + { 251055792, 567796700, 1773504224, 1198217877, 428852897, 17,} +}; +NUMBER init_q_two_pi = { + 1, + 6, + 0, + { 1288380402, 1120116153, 1860424692, 1944118326, 1583591604, 2,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_pi_over_two = { + 1, + 6, + 0, + { 125527896, 283898350, 1960493936, 1672850762, 1288168272, 8,} +}; +NUMBER init_q_pi_over_two = { + 1, + 6, + 0, + { 429277156, 92748659, 1573365737, 1740753005, 1019699561, 5,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_one_pt_five_pi = { + 1, + 6, + 0, + { 1241201312, 270061909, 1051574664, 1924965045, 1340320627, 70,} +}; +NUMBER init_q_one_pt_five_pi = { + 1, + 6, + 0, + { 1579671539, 1837970263, 1067644340, 523549916, 2119366659, 14,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_e_to_one_half = { + 1, + 6, + 0, + { 256945612, 216219427, 223516738, 477442596, 581063757, 23,} +}; +NUMBER init_q_e_to_one_half = { + 1, + 6, + 0, + { 1536828363, 698484484, 1127331835, 224219346, 245499408, 14,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_exp = { + 1, + 6, + 0, + { 943665199, 1606559160, 1094967530, 1759391384, 1671799163, 1123581,} +}; +NUMBER init_q_rat_exp = { + 1, + 6, + 0, + { 879242208, 2022880100, 617392930, 1374929092, 1367479163, 413342,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_ln_ten = { + 1, + 6, + 0, + { 2086268922, 165794492, 1416063951, 1851428830, 1893239400, 65366841,} +}; +NUMBER init_q_ln_ten = { + 1, + 6, + 0, + { 26790652, 564532679, 783998273, 216030448, 1564709968, 28388458,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_ln_two = { + 1, + 6, + 0, + { 1789230241, 1057927868, 715399197, 908801241, 1411265331, 3,} +}; +NUMBER init_q_ln_two = { + 1, + 6, + 0, + { 1559869847, 1930657510, 1228561531, 219003871, 593099283, 5,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rad_to_deg = { + 1, + 6, + 0, + { 2127722024, 1904928383, 2016479213, 2048947859, 1578647346, 492,} +}; +NUMBER init_q_rad_to_deg = { + 1, + 6, + 0, + { 125527896, 283898350, 1960493936, 1672850762, 1288168272, 8,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rad_to_grad = { + 1, + 6, + 0, + { 2125526288, 684931327, 570267400, 129125085, 1038224725, 547,} +}; +NUMBER init_q_rad_to_grad = { + 1, + 6, + 0, + { 125527896, 283898350, 1960493936, 1672850762, 1288168272, 8,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_qword = { + 1, + 3, + 0, + { 2147483647, 2147483647, 3,} +}; +NUMBER init_q_rat_qword = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_dword = { + 1, + 2, + 0, + { 2147483647, 1,} +}; +NUMBER init_q_rat_dword = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_max_long = { + 1, + 1, + 0, + { 2147483647,} +}; +NUMBER init_q_rat_max_long = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_min_long = { + -1, + 2, + 0, + { 0, 1,} +}; +NUMBER init_q_rat_min_long = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_word = { + 1, + 1, + 0, + { 65535,} +}; +NUMBER init_q_rat_word = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_byte = { + 1, + 1, + 0, + { 255,} +}; +NUMBER init_q_rat_byte = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_400 = { + 1, + 1, + 0, + { 400,} +}; +NUMBER init_q_rat_400 = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_360 = { + 1, + 1, + 0, + { 360,} +}; +NUMBER init_q_rat_360 = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_200 = { + 1, + 1, + 0, + { 200,} +}; +NUMBER init_q_rat_200 = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_180 = { + 1, + 1, + 0, + { 180,} +}; +NUMBER init_q_rat_180 = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_max_exp = { + 1, + 1, + 0, + { 100000,} +}; +NUMBER init_q_rat_max_exp = { + 1, + 1, + 0, + { 1,} +}; +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_min_exp = { + -1, + 1, + 0, + { 100000,} +}; +NUMBER init_q_rat_min_exp = { + 1, + 1, + 0, + { 1,} +}; + +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_max_fact = { + 1, + 1, + 0, + { 3249, } +}; +NUMBER init_q_rat_max_fact = { + 1, + 1, + 0, + { 1, } +}; + +// Autogenerated by _dumprawrat in support.c +NUMBER init_p_rat_min_fact = { + -1, + 1, + 0, + { 1000, } +}; +NUMBER init_q_rat_min_fact = { + 1, + 1, + 0, + { 1, } +}; diff --git a/src/CalcManager/Ratpack/ratpak.h b/src/CalcManager/Ratpack/ratpak.h new file mode 100644 index 00000000..ee376b86 --- /dev/null +++ b/src/CalcManager/Ratpack/ratpak.h @@ -0,0 +1,452 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File ratpak.h +// Copyright (C) 1995-99 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Infinite precision math package header file, if you use ratpak.lib you +// need to include this header. +// +//----------------------------------------------------------------------------- + +#include "CalcErr.h" + +static constexpr uint32_t BASEXPWR = 31L;// Internal log2(BASEX) +static constexpr uint32_t BASEX = 0x80000000; // Internal radix used in calculations, hope to raise + // this to 2^32 after solving scaling problems with + // overflow detection esp. in mul + +typedef unsigned long MANTTYPE; +typedef unsigned __int64 TWO_MANTTYPE; + +enum eNUMOBJ_FMT { + FMT_FLOAT, // returns floating point, or exponential if number is too big + FMT_SCIENTIFIC, // always returns scientific notation + FMT_ENGINEERING // always returns engineering notation such that exponent is a multiple of 3 + +}; + +enum eANGLE_TYPE { + ANGLE_DEG, // Calculate trig using 360 degrees per revolution + ANGLE_RAD, // Calculate trig using 2 pi radians per revolution + ANGLE_GRAD // Calculate trig using 400 gradients per revolution + +}; + +typedef enum eNUMOBJ_FMT NUMOBJ_FMT; +typedef enum eANGLE_TYPE ANGLE_TYPE; + +//----------------------------------------------------------------------------- +// +// NUMBER type is a representation of a generic sized generic radix number +// +//----------------------------------------------------------------------------- + +#pragma warning(push) +#pragma warning(disable:4200) // nonstandard extension used : zero-sized array in struct/union +typedef struct _number +{ + long sign; // The sign of the mantissa, +1, or -1 + long cdigit; // The number of digits, or what passes for digits in the + // radix being used. + long exp; // The offset of digits from the radix point + // (decimal point in radix 10) + MANTTYPE mant[]; + // This is actually allocated as a continuation of the + // NUMBER structure. +} NUMBER, *PNUMBER, **PPNUMBER; +#pragma warning(pop) + + +//----------------------------------------------------------------------------- +// +// RAT type is a representation radix on 2 NUMBER types. +// pp/pq, where pp and pq are pointers to integral NUMBER types. +// +//----------------------------------------------------------------------------- + +typedef struct _rat + { + PNUMBER pp; + PNUMBER pq; + } RAT, *PRAT; + +static constexpr uint32_t MAX_LONG_SIZE = 33; // Base 2 requires 32 'digits' + +//----------------------------------------------------------------------------- +// +// List of useful constants for evaluation, note this list needs to be +// initialized. +// +//----------------------------------------------------------------------------- + +extern PNUMBER num_one; +extern PNUMBER num_two; +extern PNUMBER num_five; +extern PNUMBER num_six; +extern PNUMBER num_ten; + +extern PRAT ln_ten; +extern PRAT ln_two; +extern PRAT rat_zero; +extern PRAT rat_neg_one; +extern PRAT rat_one; +extern PRAT rat_two; +extern PRAT rat_six; +extern PRAT rat_half; +extern PRAT rat_ten; +extern PRAT pt_eight_five; +extern PRAT pi; +extern PRAT pi_over_two; +extern PRAT two_pi; +extern PRAT one_pt_five_pi; +extern PRAT e_to_one_half; +extern PRAT rat_exp; +extern PRAT rad_to_deg; +extern PRAT rad_to_grad; +extern PRAT rat_qword; +extern PRAT rat_dword; +extern PRAT rat_word; +extern PRAT rat_byte; +extern PRAT rat_360; +extern PRAT rat_400; +extern PRAT rat_180; +extern PRAT rat_200; +extern PRAT rat_nRadix; +extern PRAT rat_smallest; +extern PRAT rat_negsmallest; +extern PRAT rat_max_exp; +extern PRAT rat_min_exp; +extern PRAT rat_max_fact; +extern PRAT rat_min_fact; +extern PRAT rat_max_long; +extern PRAT rat_min_long; + +// DUPNUM Duplicates a number taking care of allocation and internals +#define DUPNUM(a,b) destroynum(a);createnum( a, (b)->cdigit );_dupnum(a, b); + +// DUPRAT Duplicates a rational taking care of allocation and internals +#define DUPRAT(a,b) destroyrat(a);createrat(a);DUPNUM((a)->pp,(b)->pp);DUPNUM((a)->pq,(b)->pq); + +// LOG*RADIX calculates the integral portion of the log of a number in +// the base currently being used, only accurate to within g_ratio + +#define LOGNUMRADIX(pnum) (((pnum)->cdigit+(pnum)->exp)*g_ratio) +#define LOGRATRADIX(prat) (LOGNUMRADIX((prat)->pp)-LOGNUMRADIX((prat)->pq)) + +// LOG*2 calculates the integral portion of the log of a number in +// the internal base being used, only accurate to within g_ratio + +#define LOGNUM2(pnum) ((pnum)->cdigit+(pnum)->exp) +#define LOGRAT2(prat) (LOGNUM2((prat)->pp)-LOGNUM2((prat)->pq)) + +#if defined( DEBUG_RATPAK ) +//----------------------------------------------------------------------------- +// +// Debug versions of rational number creation and destruction routines. +// used for debugging allocation errors. +// +//----------------------------------------------------------------------------- + +#define createrat(y) (y)=_createrat(); \ +{ \ + std::wstringstream outputString; \ + outputString << "createrat " << y << " " << # y << " file= " << __FILE__ << ", line= " << __LINE__ << "\n"; \ + OutputDebugString(outputString.str().c_str()); \ +} +#define destroyrat(x) \ +{ \ + std::wstringstream outputString; \ + outputString << "destroyrat " << x << " file= " << __FILE__ << ", line= " << __LINE__ << "\n"; \ + OutputDebugString(outputString.str().c_str()); \ +} \ +_destroyrat(x),(x)=nullptr +#define createnum(y,x) (y)=_createnum(x); \ +{ \ + std::wstringstream outputString; \ + outputString << "createnum " << y << " " << # y << " file= " << __FILE__ << ", line= " << __LINE__ << "\n"; \ + OutputDebugString(outputString.str().c_str()); \ +} +#define destroynum(x) \ +{ \ + std::wstringstream outputString; \ + outputString << "destroynum " << x << " file= " << __FILE__ << ", line= " << __LINE__ << "\n"; \ + OutputDebugString(outputString.str().c_str()); \ +} \ +_destroynum(x),(x)=nullptr +#else +#define createrat(y) (y)=_createrat() +#define destroyrat(x) _destroyrat(x),(x)=nullptr +#define createnum(y,x) (y)=_createnum(x) +#define destroynum(x) _destroynum(x),(x)=nullptr +#endif + +//----------------------------------------------------------------------------- +// +// Defines for checking when to stop taylor series expansions due to +// precision satisfaction. +// +//----------------------------------------------------------------------------- + +// RENORMALIZE, gets the exponents non-negative. +#define RENORMALIZE(x) if ( (x)->pp->exp < 0 ) { \ + (x)->pq->exp -= (x)->pp->exp; \ + (x)->pp->exp = 0; \ + } \ + if ( (x)->pq->exp < 0 ) { \ + (x)->pp->exp -= (x)->pq->exp; \ + (x)->pq->exp = 0; \ + } + +// TRIMNUM ASSUMES the number is in radix form NOT INTERNAL BASEX!!! +#define TRIMNUM(x, precision) if ( !g_ftrueinfinite ) { \ + long trim = (x)->cdigit - precision-g_ratio;\ + if ( trim > 1 ) \ + { \ +memmove( (x)->mant, &((x)->mant[trim]), sizeof(MANTTYPE)*((x)->cdigit-trim) ); \ + (x)->cdigit -= trim; \ + (x)->exp += trim; \ + } \ + } +// TRIMTOP ASSUMES the number is in INTERNAL BASEX!!! +#define TRIMTOP(x, precision) if ( !g_ftrueinfinite ) { \ + long trim = (x)->pp->cdigit - (precision/g_ratio) - 2;\ + if ( trim > 1 ) \ + { \ +memmove( (x)->pp->mant, &((x)->pp->mant[trim]), sizeof(MANTTYPE)*((x)->pp->cdigit-trim) ); \ + (x)->pp->cdigit -= trim; \ + (x)->pp->exp += trim; \ + } \ + trim = min((x)->pp->exp,(x)->pq->exp);\ + (x)->pp->exp -= trim;\ + (x)->pq->exp -= trim;\ + } + +#define SMALL_ENOUGH_RAT(a, precision) (zernum((a)->pp) || ( ( ( (a)->pq->cdigit + (a)->pq->exp ) - ( (a)->pp->cdigit + (a)->pp->exp ) - 1 ) * g_ratio > precision ) ) + +//----------------------------------------------------------------------------- +// +// Defines for setting up taylor series expansions for infinite precision +// functions. +// +//----------------------------------------------------------------------------- + +#define CREATETAYLOR() PRAT xx=nullptr;\ + PNUMBER n2=nullptr; \ + PRAT pret=nullptr; \ + PRAT thisterm=nullptr; \ + DUPRAT(xx,*px); \ + mulrat(&xx,*px, precision); \ + createrat(pret); \ + pret->pp=longtonum( 0L, BASEX ); \ + pret->pq=longtonum( 0L, BASEX ); + +#define DESTROYTAYLOR() destroynum( n2 ); \ + destroyrat( xx );\ + destroyrat( thisterm );\ + destroyrat( *px );\ + trimit(&pret, precision);\ + *px=pret; + +// INC(a) is the rational equivalent of a++ +// Check to see if we can avoid doing this the hard way. +#define INC(a) if ( (a)->mant[0] < BASEX - 1 ) \ + { \ + (a)->mant[0]++; \ + } \ + else \ + { \ + addnum( &(a), num_one, BASEX); \ + } + +#define MSD(x) ((x)->mant[(x)->cdigit-1]) +// MULNUM(b) is the rational equivalent of thisterm *= b where thisterm is +// a rational and b is a number, NOTE this is a mixed type operation for +// efficiency reasons. +#define MULNUM(b) mulnumx( &(thisterm->pp), b); + +// DIVNUM(b) is the rational equivalent of thisterm /= b where thisterm is +// a rational and b is a number, NOTE this is a mixed type operation for +// efficiency reasons. +#define DIVNUM(b) mulnumx( &(thisterm->pq), b); + +// NEXTTERM(p,d) is the rational equivalent of +// thisterm *= p +// d +// pret += thisterm +#define NEXTTERM(p,d,precision) mulrat(&thisterm,p,precision);d addrat( &pret, thisterm, precision ) + +//----------------------------------------------------------------------------- +// +// External variables used in the math package. +// +//----------------------------------------------------------------------------- + +extern bool g_ftrueinfinite; // set to true to allow infinite precision + // don't use unless you know what you are doing + // used to help decide when to stop calculating. + +extern long g_ratio; // Internally calculated ratio of internal radix + +//----------------------------------------------------------------------------- +// +// External functions defined in the math package. +// +//----------------------------------------------------------------------------- + +// Call whenever decimal separator character changes. +extern void SetDecimalSeparator(wchar_t decimalSeparator); + +// Call whenever either radix or precision changes, is smarter about recalculating constants. +extern void ChangeConstants(uint32_t radix, int32_t precision); + +extern bool equnum(_In_ PNUMBER a, _In_ PNUMBER b ); // returns true of a == b +extern bool lessnum(_In_ PNUMBER a, _In_ PNUMBER b ); // returns true of a < b +extern bool zernum(_In_ PNUMBER a ); // returns true of a == 0 +extern bool zerrat(_In_ PRAT a ); // returns true if a == 0/q +extern std::wstring NumberToString(_Inout_ PNUMBER& pnum, int format, uint32_t radix, int32_t precision); + +// returns a text representation of a PRAT +extern std::wstring RatToString(_Inout_ PRAT& prat, int format, uint32_t radix, int32_t precision); + +extern long numtolong(_In_ PNUMBER pnum, uint32_t radix ); +extern long rattolong(_In_ PRAT prat, uint32_t radix, int32_t precision); +ULONGLONG rattoUlonglong(_In_ PRAT prat, uint32_t radix, int32_t precision); +extern PNUMBER _createnum(_In_ ULONG size ); // returns an empty number structure with size digits +extern PNUMBER nRadixxtonum(_In_ PNUMBER a, uint32_t radix, int32_t precision); +extern PNUMBER gcd(_In_ PNUMBER a, _In_ PNUMBER b ); +extern PNUMBER StringToNumber(std::wstring_view numberString, uint32_t radix, int32_t precision); // takes a text representation of a number and returns a number. + +// takes a text representation of a number as a mantissa with sign and an exponent with sign. +extern PRAT StringToRat(bool mantissaIsNegative, std::wstring_view mantissa, bool exponentIsNegative, std::wstring_view exponent, uint32_t radix, int32_t precision); + +extern PNUMBER longfactnum(long inlong, uint32_t radix); +extern PNUMBER longprodnum(long start, long stop, uint32_t radix); +extern PNUMBER longtonum(long inlong, uint32_t radix); +extern PNUMBER Ulongtonum(unsigned long inlong, uint32_t radix); +extern PNUMBER numtonRadixx(PNUMBER a, uint32_t radix); + +// creates a empty/undefined rational representation (p/q) +extern PRAT _createrat( void ); + +// returns a new rat structure with the acos of x->p/x->q taking into account +// angle type +extern void acosanglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +// returns a new rat structure with the acosh of x->p/x->q +extern void acoshrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the acos of x->p/x->q +extern void acosrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the asin of x->p/x->q taking into account +// angle type +extern void asinanglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +extern void asinhrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); +// returns a new rat structure with the asinh of x->p/x->q + +// returns a new rat structure with the asin of x->p/x->q +extern void asinrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the atan of x->p/x->q taking into account +// angle type +extern void atananglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +// returns a new rat structure with the atanh of x->p/x->q +extern void atanhrat( _Inout_ PRAT *px, int32_t precision); + +// returns a new rat structure with the atan of x->p/x->q +extern void atanrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the cosh of x->p/x->q +extern void coshrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the cos of x->p/x->q +extern void cosrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the cos of x->p/x->q taking into account +// angle type +extern void cosanglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +// returns a new rat structure with the exp of x->p/x->q this should not be called explicitly. +extern void _exprat( _Inout_ PRAT *px, int32_t precision); + +// returns a new rat structure with the exp of x->p/x->q +extern void exprat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the log base 10 of x->p/x->q +extern void log10rat( _Inout_ PRAT *px, int32_t precision); + +// returns a new rat structure with the natural log of x->p/x->q +extern void lograt( _Inout_ PRAT *px, int32_t precision); + +extern PRAT longtorat( long inlong ); +extern PRAT Ulongtorat( unsigned long inulong ); +extern PRAT numtorat( _In_ PNUMBER pin, uint32_t radix); + +extern void sinhrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); +extern void sinrat( _Inout_ PRAT *px ); + +// returns a new rat structure with the sin of x->p/x->q taking into account +// angle type +extern void sinanglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +extern void tanhrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); +extern void tanrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); + +// returns a new rat structure with the tan of x->p/x->q taking into account +// angle type +extern void tananglerat( _Inout_ PRAT *px, ANGLE_TYPE angletype, uint32_t radix, int32_t precision); + +extern void _dupnum(_In_ PNUMBER dest, _In_ PNUMBER src); + +extern void _destroynum( _In_ PNUMBER pnum ); +extern void _destroyrat( _In_ PRAT prat ); +extern void addnum( _Inout_ PNUMBER *pa, _In_ PNUMBER b, uint32_t radix); +extern void addrat( _Inout_ PRAT *pa, _In_ PRAT b, int32_t precision); +extern void andrat( _Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern void divnum( _Inout_ PNUMBER *pa, _In_ PNUMBER b, uint32_t radix, int32_t precision); +extern void divnumx( _Inout_ PNUMBER *pa, _In_ PNUMBER b, int32_t precision); +extern void divrat( _Inout_ PRAT *pa, _In_ PRAT b, int32_t precision); +extern void fracrat( _Inout_ PRAT *pa , uint32_t radix, int32_t precision); +extern void factrat( _Inout_ PRAT *pa, uint32_t radix, int32_t precision); +extern void modrat( _Inout_ PRAT *pa, _In_ PRAT b ); +extern void gcdrat( _Inout_ PRAT *pa, uint32_t radix, int32_t precision); +extern void intrat( _Inout_ PRAT *px, uint32_t radix, int32_t precision); +extern void mulnum( _Inout_ PNUMBER *pa, _In_ PNUMBER b, uint32_t radix); +extern void mulnumx( _Inout_ PNUMBER *pa, _In_ PNUMBER b ); +extern void mulrat( _Inout_ PRAT *pa, _In_ PRAT b, int32_t precision); +extern void numpowlong( _Inout_ PNUMBER *proot, long power, uint32_t radix, int32_t precision); +extern void numpowlongx( _Inout_ PNUMBER *proot, long power ); +extern void orrat( _Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern void powrat( _Inout_ PRAT *pa, _In_ PRAT b , uint32_t radix, int32_t precision); +extern void powratNumeratorDenominator(_Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern void powratcomp(_Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern void ratpowlong( _Inout_ PRAT *proot, long power, int32_t precision); +extern void remnum( _Inout_ PNUMBER *pa, _In_ PNUMBER b, uint32_t radix); +extern void rootrat( _Inout_ PRAT *pa, _In_ PRAT b , uint32_t radix, int32_t precision); +extern void scale2pi( _Inout_ PRAT *px, uint32_t radix, int32_t precision); +extern void scale( _Inout_ PRAT *px, _In_ PRAT scalefact, uint32_t radix, int32_t precision); +extern void subrat( _Inout_ PRAT *pa, _In_ PRAT b, int32_t precision); +extern void xorrat( _Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern void lshrat( _Inout_ PRAT *pa, _In_ PRAT b , uint32_t radix, int32_t precision); +extern void rshrat( _Inout_ PRAT *pa, _In_ PRAT b, uint32_t radix, int32_t precision); +extern bool rat_equ( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern bool rat_neq( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern bool rat_gt( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern bool rat_ge( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern bool rat_lt( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern bool rat_le( _In_ PRAT a, _In_ PRAT b, int32_t precision); +extern void inbetween( _In_ PRAT *px, _In_ PRAT range, int32_t precision); +extern void trimit( _Inout_ PRAT *px, int32_t precision); +extern void _dumprawrat(_In_ const wchar_t *varname, _In_ PRAT rat, std::wostream& out); +extern void _dumprawnum(_In_ const wchar_t *varname, _In_ PNUMBER num, std::wostream& out); diff --git a/src/CalcManager/Ratpack/support.cpp b/src/CalcManager/Ratpack/support.cpp new file mode 100644 index 00000000..9ea60e0d --- /dev/null +++ b/src/CalcManager/Ratpack/support.cpp @@ -0,0 +1,719 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//---------------------------------------------------------------------------- +// Package Title ratpak +// File support.c +// Copyright (C) 1995-96 Microsoft +// Date 10-21-96 +// +// +// Description +// +// Contains support functions for rationals and numbers. +// +// Special Information +// +// +// +//---------------------------------------------------------------------------- + +#include "pch.h" +#include "ratpak.h" + +using namespace std; + +void _readconstants( void ); + +#if defined( GEN_CONST ) +static int cbitsofprecision = 0; +#define READRAWRAT(v) +#define READRAWNUM(v) +#define DUMPRAWRAT(v) _dumprawrat(#v,v, wcout) +#define DUMPRAWNUM(v) fprintf( stderr, \ +"// Autogenerated by _dumprawrat in support.c\n" ); \ + fprintf( stderr, "NUMBER init_" #v "= {\n" ); \ + _dumprawnum(v, wcout); \ + fprintf( stderr, "};\n" ) + + +#else + +#define DUMPRAWRAT(v) +#define DUMPRAWNUM(v) +#define READRAWRAT(v) createrat(v); DUPNUM((v)->pp,(&(init_p_##v))); \ +DUPNUM((v)->pq,(&(init_q_##v))); +#define READRAWNUM(v) DUPNUM(v,(&(init_##v))) + +#define INIT_AND_DUMP_RAW_NUM_IF_NULL(r, v) if (r == nullptr) { r = longtonum(v, BASEX); DUMPRAWNUM(v); } +#define INIT_AND_DUMP_RAW_RAT_IF_NULL(r, v) if (r == nullptr) { r = longtorat(v); DUMPRAWRAT(v); } + +static constexpr int RATIO_FOR_DECIMAL = 9; +static constexpr int DECIMAL = 10; +static constexpr int CALC_DECIMAL_DIGITS_DEFAULT = 32; + +static int cbitsofprecision = RATIO_FOR_DECIMAL * DECIMAL * CALC_DECIMAL_DIGITS_DEFAULT; + +#include "ratconst.h" + +#endif + +bool g_ftrueinfinite = false; // Set to true if you don't want + // chopping internally + // precision used internally + +PNUMBER num_one= nullptr; +PNUMBER num_two= nullptr; +PNUMBER num_five= nullptr; +PNUMBER num_six= nullptr; +PNUMBER num_ten= nullptr; + +PRAT ln_ten= nullptr; +PRAT ln_two= nullptr; +PRAT rat_zero= nullptr; +PRAT rat_one= nullptr; +PRAT rat_neg_one= nullptr; +PRAT rat_two= nullptr; +PRAT rat_six= nullptr; +PRAT rat_half= nullptr; +PRAT rat_ten= nullptr; +PRAT pt_eight_five= nullptr; +PRAT pi= nullptr; +PRAT pi_over_two= nullptr; +PRAT two_pi= nullptr; +PRAT one_pt_five_pi= nullptr; +PRAT e_to_one_half= nullptr; +PRAT rat_exp= nullptr; +PRAT rad_to_deg= nullptr; +PRAT rad_to_grad= nullptr; +PRAT rat_qword= nullptr; +PRAT rat_dword= nullptr; // unsigned max ulong +PRAT rat_word= nullptr; +PRAT rat_byte= nullptr; +PRAT rat_360= nullptr; +PRAT rat_400= nullptr; +PRAT rat_180= nullptr; +PRAT rat_200= nullptr; +PRAT rat_nRadix= nullptr; +PRAT rat_smallest= nullptr; +PRAT rat_negsmallest= nullptr; +PRAT rat_max_exp= nullptr; +PRAT rat_min_exp= nullptr; +PRAT rat_max_fact = nullptr; +PRAT rat_min_fact = nullptr; +PRAT rat_min_long= nullptr; // min signed long +PRAT rat_max_long= nullptr; // max signed long + +//---------------------------------------------------------------------------- +// +// FUNCTION: ChangeConstants +// +// ARGUMENTS: base changing to, and precision to use. +// +// RETURN: None +// +// SIDE EFFECTS: sets a mess of constants. +// +// +//---------------------------------------------------------------------------- + +void ChangeConstants(uint32_t radix, int32_t precision) +{ + // ratio is set to the number of digits in the current radix, you can get + // in the internal BASEX radix, this is important for length calculations + // in translating from radix to BASEX and back. + + uint64_t limit = static_cast(BASEX) / static_cast(radix); + g_ratio = 0; + for (uint32_t digit = 1; digit < limit; digit *= radix ) + { + g_ratio++; + } + g_ratio += !g_ratio; + + destroyrat(rat_nRadix); + rat_nRadix=longtorat( radix ); + + // Check to see what we have to recalculate and what we don't + if (cbitsofprecision < (g_ratio * static_cast(radix) * precision)) + { + g_ftrueinfinite = false; + + INIT_AND_DUMP_RAW_NUM_IF_NULL(num_one, 1L); + INIT_AND_DUMP_RAW_NUM_IF_NULL(num_two, 2L); + INIT_AND_DUMP_RAW_NUM_IF_NULL(num_five, 5L); + INIT_AND_DUMP_RAW_NUM_IF_NULL(num_six, 6L); + INIT_AND_DUMP_RAW_NUM_IF_NULL(num_ten, 10L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_six, 6L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_two, 2L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_zero, 0L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_one, 1L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_neg_one, -1L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_ten, 10L); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_word, 0xffff); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_word, 0xff); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_400, 400); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_360, 360); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_200, 200); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_180, 180); + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_max_exp, 100000); + + // 3248, is the max number for which calc is able to compute factorial, after that it is unable to compute due to overflow. + // Hence restricted factorial range as at most 3248.Beyond that calc will throw overflow error immediately. + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_max_fact, 3249); + + // -1000, is the min number for which calc is able to compute factorial, after that it takes too long to compute. + INIT_AND_DUMP_RAW_RAT_IF_NULL(rat_min_fact, -1000); + + DUPRAT(rat_smallest, rat_nRadix); + ratpowlong(&rat_smallest, -precision, precision); + DUPRAT(rat_negsmallest, rat_smallest); + rat_negsmallest->pp->sign = -1; + DUMPRAWRAT(rat_smallest); + DUMPRAWRAT(rat_negsmallest); + + if (rat_half == nullptr) + { + createrat(rat_half); + DUPNUM(rat_half->pp, num_one); + DUPNUM(rat_half->pq, num_two); + DUMPRAWRAT(rat_half); + } + + if (pt_eight_five == nullptr) + { + createrat(pt_eight_five); + pt_eight_five->pp = longtonum(85L, BASEX); + pt_eight_five->pq = longtonum(100L, BASEX); + DUMPRAWRAT(pt_eight_five); + } + + DUPRAT(rat_qword, rat_two); + numpowlong(&(rat_qword->pp), 64, BASEX, precision); + subrat(&rat_qword, rat_one, precision); + DUMPRAWRAT(rat_qword); + + DUPRAT(rat_dword, rat_two); + numpowlong(&(rat_dword->pp), 32, BASEX, precision); + subrat(&rat_dword, rat_one, precision); + DUMPRAWRAT(rat_dword); + + DUPRAT(rat_max_long, rat_two); + numpowlong(&(rat_max_long->pp), 31, BASEX, precision); + DUPRAT(rat_min_long, rat_max_long); + subrat(&rat_max_long, rat_one, precision); // rat_max_long = 2^31 -1 + DUMPRAWRAT(rat_max_long); + + rat_min_long->pp->sign *= -1; // rat_min_long = -2^31 + DUMPRAWRAT(rat_min_long); + + DUPRAT(rat_min_exp, rat_max_exp); + rat_min_exp->pp->sign *= -1; + DUMPRAWRAT(rat_min_exp); + + cbitsofprecision = g_ratio * radix * precision; + + // Apparently when dividing 180 by pi, another (internal) digit of + // precision is needed. + long extraPrecision = precision + g_ratio; + DUPRAT(pi, rat_half); + asinrat(&pi, radix, extraPrecision); + mulrat(&pi, rat_six, extraPrecision); + DUMPRAWRAT(pi); + + DUPRAT(two_pi, pi); + DUPRAT(pi_over_two, pi); + DUPRAT(one_pt_five_pi, pi); + addrat(&two_pi, pi, extraPrecision); + DUMPRAWRAT(two_pi); + + divrat(&pi_over_two, rat_two, extraPrecision); + DUMPRAWRAT(pi_over_two); + + addrat(&one_pt_five_pi, pi_over_two, extraPrecision); + DUMPRAWRAT(one_pt_five_pi); + + DUPRAT(e_to_one_half, rat_half); + _exprat(&e_to_one_half, extraPrecision); + DUMPRAWRAT(e_to_one_half); + + DUPRAT(rat_exp, rat_one); + _exprat(&rat_exp, extraPrecision); + DUMPRAWRAT(rat_exp); + + // WARNING: remember lograt uses exponent constants calculated above... + + DUPRAT(ln_ten, rat_ten); + lograt(&ln_ten, extraPrecision); + DUMPRAWRAT(ln_ten); + + DUPRAT(ln_two, rat_two); + lograt(&ln_two, extraPrecision); + DUMPRAWRAT(ln_two); + + + destroyrat(rad_to_deg); + rad_to_deg = longtorat(180L); + divrat(&rad_to_deg, pi, extraPrecision); + DUMPRAWRAT(rad_to_deg); + + destroyrat(rad_to_grad); + rad_to_grad = longtorat(200L); + divrat(&rad_to_grad, pi, extraPrecision); + DUMPRAWRAT(rad_to_grad); + } + else + { + _readconstants(); + + DUPRAT(rat_smallest, rat_nRadix); + ratpowlong(&rat_smallest, -precision, precision); + DUPRAT(rat_negsmallest, rat_smallest); + rat_negsmallest->pp->sign = -1; + } + +} + +//---------------------------------------------------------------------------- +// +// FUNCTION: intrat +// +// ARGUMENTS: pointer to x PRAT representation of number +// +// RETURN: no return value x PRAT is smashed with integral number +// +// +//---------------------------------------------------------------------------- + +void intrat( PRAT *px, uint32_t radix, int32_t precision) +{ + // Only do the intrat operation if number is nonzero. + // and only if the bottom part is not one. + if ( !zernum( (*px)->pp ) && !equnum( (*px)->pq, num_one ) ) + { + wstring ratStr = RatToString(*px, FMT_FLOAT, radix, precision); + PNUMBER pnum = StringToNumber(ratStr, radix, precision); + + destroyrat( *px ); + *px = numtorat( pnum, radix); + destroynum( pnum ); + + PRAT pret = nullptr; + DUPRAT(pret,*px); + modrat( &pret, rat_one ); + + subrat( px, pret, precision); + destroyrat( pret ); + } +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_equ +// +// ARGUMENTS: PRAT a and PRAT b +// +// RETURN: true if equal false otherwise. +// +// +//--------------------------------------------------------------------------- + +bool rat_equ( PRAT a, PRAT b, int32_t precision) + +{ + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + rattmp->pp->sign *= -1; + addrat( &rattmp, b, precision); + bool bret = zernum( rattmp->pp ); + destroyrat( rattmp ); + return( bret ); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_ge +// +// ARGUMENTS: PRAT a, PRAT b and long precision +// +// RETURN: true if a is greater than or equal to b +// +// +//--------------------------------------------------------------------------- + +bool rat_ge( PRAT a, PRAT b, int32_t precision) + +{ + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + b->pp->sign *= -1; + addrat( &rattmp, b, precision); + b->pp->sign *= -1; + bool bret = ( zernum( rattmp->pp ) || + rattmp->pp->sign * rattmp->pq->sign == 1 ); + destroyrat( rattmp ); + return( bret ); +} + + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_gt +// +// ARGUMENTS: PRAT a and PRAT b +// +// RETURN: true if a is greater than b +// +// +//--------------------------------------------------------------------------- + +bool rat_gt( PRAT a, PRAT b, int32_t precision) + +{ + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + b->pp->sign *= -1; + addrat( &rattmp, b, precision); + b->pp->sign *= -1; + bool bret = ( !zernum( rattmp->pp ) && + rattmp->pp->sign * rattmp->pq->sign == 1 ); + destroyrat( rattmp ); + return( bret ); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_le +// +// ARGUMENTS: PRAT a, PRAT b and long precision +// +// RETURN: true if a is less than or equal to b +// +// +//--------------------------------------------------------------------------- + +bool rat_le( PRAT a, PRAT b, int32_t precision) + +{ + + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + b->pp->sign *= -1; + addrat( &rattmp, b, precision); + b->pp->sign *= -1; + bool bret = ( zernum( rattmp->pp ) || + rattmp->pp->sign * rattmp->pq->sign == -1 ); + destroyrat( rattmp ); + return( bret ); +} + + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_lt +// +// ARGUMENTS: PRAT a, PRAT b and long precision +// +// RETURN: true if a is less than b +// +// +//--------------------------------------------------------------------------- + +bool rat_lt( PRAT a, PRAT b, int32_t precision) + +{ + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + b->pp->sign *= -1; + addrat( &rattmp, b, precision); + b->pp->sign *= -1; + bool bret = ( !zernum( rattmp->pp ) && + rattmp->pp->sign * rattmp->pq->sign == -1 ); + destroyrat( rattmp ); + return( bret ); +} + + +//--------------------------------------------------------------------------- +// +// FUNCTION: rat_neq +// +// ARGUMENTS: PRAT a and PRAT b +// +// RETURN: true if a is not equal to b +// +// +//--------------------------------------------------------------------------- + + +bool rat_neq( PRAT a, PRAT b, int32_t precision) + +{ + PRAT rattmp= nullptr; + DUPRAT(rattmp,a); + rattmp->pp->sign *= -1; + addrat( &rattmp, b, precision); + bool bret = !( zernum( rattmp->pp ) ); + destroyrat( rattmp ); + return( bret ); +} + +//--------------------------------------------------------------------------- +// +// function: scale +// +// ARGUMENTS: pointer to x PRAT representation of number, and scaling factor +// +// RETURN: no return, value x PRAT is smashed with a scaled number in the +// range of the scalefact. +// +//--------------------------------------------------------------------------- + +void scale( PRAT *px, PRAT scalefact, uint32_t radix, int32_t precision ) +{ + PRAT pret = nullptr; + DUPRAT(pret,*px); + + // Logscale is a quick way to tell how much extra precision is needed for + // scaleing by scalefact. + long logscale = g_ratio * ( (pret->pp->cdigit+pret->pp->exp) - + (pret->pq->cdigit+pret->pq->exp) ); + if ( logscale > 0 ) + { + precision += logscale; + } + + divrat( &pret, scalefact, precision); + intrat(&pret, radix, precision); + mulrat( &pret, scalefact, precision); + pret->pp->sign *= -1; + addrat( px, pret, precision); + + destroyrat( pret ); +} + +//--------------------------------------------------------------------------- +// +// function: scale2pi +// +// ARGUMENTS: pointer to x PRAT representation of number +// +// RETURN: no return, value x PRAT is smashed with a scaled number in the +// range of 0..2pi +// +//--------------------------------------------------------------------------- + +void scale2pi( PRAT *px, uint32_t radix, int32_t precision ) +{ + PRAT pret = nullptr; + PRAT my_two_pi = nullptr; + DUPRAT(pret,*px); + + // Logscale is a quick way to tell how much extra precision is needed for + // scaleing by 2 pi. + long logscale = g_ratio * ( (pret->pp->cdigit+pret->pp->exp) - + (pret->pq->cdigit+pret->pq->exp) ); + if ( logscale > 0 ) + { + precision += logscale; + DUPRAT(my_two_pi,rat_half); + asinrat( &my_two_pi, radix, precision); + mulrat( &my_two_pi, rat_six, precision); + mulrat( &my_two_pi, rat_two, precision); + } + else + { + DUPRAT(my_two_pi,two_pi); + logscale = 0; + } + + divrat( &pret, my_two_pi, precision); + intrat(&pret, radix, precision); + mulrat( &pret, my_two_pi, precision); + pret->pp->sign *= -1; + addrat( px, pret, precision); + + destroyrat( my_two_pi ); + destroyrat( pret ); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: inbetween +// +// ARGUMENTS: PRAT *px, and PRAT range. +// +// RETURN: none, changes *px to -/+range, if px is outside -range..+range +// +//--------------------------------------------------------------------------- + +void inbetween( PRAT *px, PRAT range, int32_t precision) + +{ + if ( rat_gt(*px,range, precision) ) + { + DUPRAT(*px,range); + } + else + { + range->pp->sign *= -1; + if ( rat_lt(*px, range, precision) ) + { + DUPRAT(*px,range); + } + range->pp->sign *= -1; + } +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: _dumprawrat +// +// ARGUMENTS: const wchar *name of variable, PRAT x, output stream out +// +// RETURN: none, prints the results of a dump of the internal structures +// of a PRAT, suitable for READRAWRAT to stderr. +// +//--------------------------------------------------------------------------- + +void _dumprawrat( const wchar_t *varname, PRAT rat, wostream& out) + +{ + _dumprawnum(varname, rat->pp, out ); + _dumprawnum(varname, rat->pq, out ); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: _dumprawnum +// +// ARGUMENTS: const wchar *name of variable, PNUMBER num, output stream out +// +// RETURN: none, prints the results of a dump of the internal structures +// of a PNUMBER, suitable for READRAWNUM to stderr. +// +//--------------------------------------------------------------------------- + +void _dumprawnum(const wchar_t *varname, PNUMBER num, wostream& out) + +{ + int i; + + out << L"NUMBER " << varname << L" = {\n"; + out << L"\t"<< num->sign << L",\n"; + out << L"\t" << num->cdigit << L",\n"; + out << L"\t" << num->exp << L",\n"; + out << L"\t{ "; + + for ( i = 0; i < num->cdigit; i++ ) + { + out << L" "<< num->mant[i] << L","; + } + out << L"}\n"; + out << L"};\n"; +} + +void _readconstants( void ) + +{ + READRAWNUM(num_one); + READRAWNUM(num_two); + READRAWNUM(num_five); + READRAWNUM(num_six); + READRAWNUM(num_ten); + READRAWRAT(pt_eight_five); + READRAWRAT(rat_six); + READRAWRAT(rat_two); + READRAWRAT(rat_zero); + READRAWRAT(rat_one); + READRAWRAT(rat_neg_one); + READRAWRAT(rat_half); + READRAWRAT(rat_ten); + READRAWRAT(pi); + READRAWRAT(two_pi); + READRAWRAT(pi_over_two); + READRAWRAT(one_pt_five_pi); + READRAWRAT(e_to_one_half); + READRAWRAT(rat_exp); + READRAWRAT(ln_ten); + READRAWRAT(ln_two); + READRAWRAT(rad_to_deg); + READRAWRAT(rad_to_grad); + READRAWRAT(rat_qword); + READRAWRAT(rat_dword); + READRAWRAT(rat_word); + READRAWRAT(rat_byte); + READRAWRAT(rat_360); + READRAWRAT(rat_400); + READRAWRAT(rat_180); + READRAWRAT(rat_200); + READRAWRAT(rat_smallest); + READRAWRAT(rat_negsmallest); + READRAWRAT(rat_max_exp); + READRAWRAT(rat_min_exp); + READRAWRAT(rat_max_fact); + READRAWRAT(rat_min_fact); + READRAWRAT(rat_min_long); + READRAWRAT(rat_max_long); +} + +//--------------------------------------------------------------------------- +// +// FUNCTION: trimit +// +// ARGUMENTS: PRAT *px, long precision +// +// +// DESCRIPTION: Chops off digits from rational numbers to avoid time +// explosions in calculations of functions using series. +// It can be shown that it is enough to only keep the first n digits +// of the largest of p or q in the rational p over q form, and of course +// scale the smaller by the same number of digits. This will give you +// n-1 digits of accuracy. This dramatically speeds up calculations +// involving hundreds of digits or more. +// The last part of this trim dealing with exponents never affects accuracy +// +// RETURN: none, modifies the pointed to PRAT +// +//--------------------------------------------------------------------------- + +void trimit( PRAT *px, int32_t precision) + +{ + if ( !g_ftrueinfinite ) + { + long trim; + PNUMBER pp=(*px)->pp; + PNUMBER pq=(*px)->pq; + trim = g_ratio * (min((pp->cdigit+pp->exp),(pq->cdigit+pq->exp))-1) - precision; + if ( trim > g_ratio ) + { + trim /= g_ratio; + + if ( trim <= pp->exp ) + { + pp->exp -= trim; + } + else + { + memmove( pp->mant, &(pp->mant[trim-pp->exp]), sizeof(MANTTYPE)*(pp->cdigit-trim+pp->exp) ); + pp->cdigit -= trim-pp->exp; + pp->exp = 0; + } + + if ( trim <= pq->exp ) + { + pq->exp -= trim; + } + else + { + memmove( pq->mant, &(pq->mant[trim-pq->exp]), sizeof(MANTTYPE)*(pq->cdigit-trim+pq->exp) ); + pq->cdigit -= trim-pq->exp; + pq->exp = 0; + } + } + trim = min(pp->exp,pq->exp); + pp->exp -= trim; + pq->exp -= trim; + } +} diff --git a/src/CalcManager/Ratpack/trans.cpp b/src/CalcManager/Ratpack/trans.cpp new file mode 100644 index 00000000..1bbfee6d --- /dev/null +++ b/src/CalcManager/Ratpack/trans.cpp @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//---------------------------------------------------------------------------- +// File trans.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains sin, cos and tan for rationals +// +// +//---------------------------------------------------------------------------- + +#include "pch.h" +#include "ratpak.h" + + + +void scalerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision ) +{ + switch ( angletype ) + { + case ANGLE_RAD: + scale2pi( pa, radix, precision); + break; + case ANGLE_DEG: + scale( pa, rat_360, radix, precision); + break; + case ANGLE_GRAD: + scale( pa, rat_400, radix, precision); + break; + } +} + + +//----------------------------------------------------------------------------- +// +// FUNCTION: sinrat, _sinrat +// +// ARGUMENTS: x PRAT representation of number to take the sine of +// +// RETURN: sin of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2j+1 +// \ ] j X +// \ -1 * --------- +// / (2j+1)! +// /__] +// j=0 +// or, +// n +// ___ 2 +// \ ] -X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j)*(2j+1) +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +//----------------------------------------------------------------------------- + + +void _sinrat( PRAT *px, int32_t precision) + +{ + CREATETAYLOR(); + + DUPRAT(pret,*px); + DUPRAT(thisterm,*px); + + DUPNUM(n2,num_one); + xx->pp->sign *= -1; + + do { + NEXTTERM(xx,INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); + + // Since *px might be epsilon above 1 or below -1, due to TRIMIT we need + // this trick here. + inbetween(px, rat_one, precision); + + // Since *px might be epsilon near zero we must set it to zero. + if ( rat_le(*px, rat_smallest, precision) && rat_ge(*px, rat_negsmallest, precision) ) + { + DUPRAT(*px,rat_zero); + } +} + +void sinrat( PRAT *px, uint32_t radix, int32_t precision) +{ + scale2pi(px, radix, precision); + _sinrat(px, precision); +} + +void sinanglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + scalerat( pa, angletype, radix, precision); + switch ( angletype ) + { + case ANGLE_DEG: + if ( rat_gt( *pa, rat_180, precision) ) + { + subrat(pa, rat_360, precision); + } + divrat( pa, rat_180, precision); + mulrat( pa, pi, precision); + break; + case ANGLE_GRAD: + if ( rat_gt( *pa, rat_200, precision) ) + { + subrat(pa,rat_400, precision); + } + divrat( pa, rat_200, precision); + mulrat( pa, pi, precision); + break; + } + _sinrat( pa, precision); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: cosrat, _cosrat +// +// ARGUMENTS: x PRAT representation of number to take the cosine of +// +// RETURN: cosin of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2j j +// \ ] X -1 +// \ --------- +// / (2j)! +// /__] +// j=0 +// or, +// n +// ___ 2 +// \ ] -X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j)*(2j+1) +// /__] +// j=0 +// +// thisterm = 1 ; and stop when thisterm < precision used. +// 0 n +// +//----------------------------------------------------------------------------- + + +void _cosrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + CREATETAYLOR(); + + destroynum(pret->pp); + destroynum(pret->pq); + + pret->pp=longtonum( 1L, radix); + pret->pq=longtonum( 1L, radix); + + DUPRAT(thisterm,pret) + + n2=longtonum(0L, radix); + xx->pp->sign *= -1; + + do { + NEXTTERM(xx,INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); + // Since *px might be epsilon above 1 or below -1, due to TRIMIT we need + // this trick here. + inbetween(px, rat_one, precision); + // Since *px might be epsilon near zero we must set it to zero. + if ( rat_le(*px, rat_smallest, precision) && rat_ge(*px, rat_negsmallest, precision) ) + { + DUPRAT(*px,rat_zero); + } +} + +void cosrat( PRAT *px, uint32_t radix, int32_t precision) +{ + scale2pi(px, radix, precision); + _cosrat(px, radix, precision); +} + +void cosanglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + scalerat( pa, angletype, radix, precision); + switch ( angletype ) + { + case ANGLE_DEG: + if ( rat_gt( *pa, rat_180, precision) ) + { + PRAT ptmp= nullptr; + DUPRAT(ptmp,rat_360); + subrat(&ptmp, *pa, precision); + destroyrat(*pa); + *pa=ptmp; + } + divrat( pa, rat_180, precision); + mulrat( pa, pi, precision); + break; + case ANGLE_GRAD: + if ( rat_gt( *pa, rat_200, precision) ) + { + PRAT ptmp= nullptr; + DUPRAT(ptmp,rat_400); + subrat(&ptmp, *pa, precision); + destroyrat(*pa); + *pa=ptmp; + } + divrat( pa, rat_200, precision); + mulrat( pa, pi, precision); + break; + } + _cosrat( pa, radix, precision); +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: tanrat, _tanrat +// +// ARGUMENTS: x PRAT representation of number to take the tangent of +// +// RETURN: tan of x in PRAT form. +// +// EXPLANATION: This uses sinrat and cosrat +// +//----------------------------------------------------------------------------- + + +void _tanrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT ptmp= nullptr; + + DUPRAT(ptmp,*px); + _sinrat(px, precision); + _cosrat(&ptmp, radix, precision); + if ( zerrat( ptmp ) ) + { + destroyrat(ptmp); + throw( CALC_E_DOMAIN ); + } + divrat(px, ptmp, precision); + + destroyrat(ptmp); + +} + +void tanrat( PRAT *px, uint32_t radix, int32_t precision) +{ + scale2pi(px, radix, precision); + _tanrat(px, radix, precision); +} + +void tananglerat( _Inout_ PRAT *pa, ANGLE_TYPE angletype, uint32_t radix, int32_t precision) + +{ + scalerat( pa, angletype, radix, precision); + switch ( angletype ) + { + case ANGLE_DEG: + if ( rat_gt( *pa, rat_180, precision) ) + { + subrat(pa, rat_180, precision); + } + divrat( pa, rat_180, precision); + mulrat( pa, pi, precision); + break; + case ANGLE_GRAD: + if ( rat_gt( *pa, rat_200, precision) ) + { + subrat(pa, rat_200, precision); + } + divrat( pa, rat_200, precision); + mulrat( pa, pi, precision); + break; + } + _tanrat( pa, radix, precision); +} + diff --git a/src/CalcManager/Ratpack/transh.cpp b/src/CalcManager/Ratpack/transh.cpp new file mode 100644 index 00000000..fb756516 --- /dev/null +++ b/src/CalcManager/Ratpack/transh.cpp @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//----------------------------------------------------------------------------- +// Package Title ratpak +// File transh.c +// Copyright (C) 1995-96 Microsoft +// Date 01-16-95 +// +// +// Description +// +// Contains hyperbolic sin, cos, and tan for rationals. +// +// +//----------------------------------------------------------------------------- +#include "pch.h" +#include "ratpak.h" + + + +bool IsValidForHypFunc(PRAT px, int32_t precision) +{ + PRAT ptmp = nullptr; + bool bRet = true; + + DUPRAT(ptmp,rat_min_exp); + divrat(&ptmp, rat_ten, precision); + if ( rat_lt( px, ptmp, precision) ) + { + bRet = false; + } + destroyrat( ptmp ); + return bRet; + +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: sinhrat, _sinhrat +// +// ARGUMENTS: x PRAT representation of number to take the sine hyperbolic +// of +// RETURN: sinh of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2j+1 +// \ ] X +// \ --------- +// / (2j+1)! +// /__] +// j=0 +// or, +// n +// ___ 2 +// \ ] X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j)*(2j+1) +// /__] +// j=0 +// +// thisterm = X ; and stop when thisterm < precision used. +// 0 n +// +// if x is bigger than 1.0 (e^x-e^-x)/2 is used. +// +//----------------------------------------------------------------------------- + + +void _sinhrat( PRAT *px, int32_t precision) + +{ + if ( !IsValidForHypFunc(*px, precision)) + { + // Don't attempt exp of anything large or small + throw( CALC_E_DOMAIN ); + } + + CREATETAYLOR(); + + DUPRAT(pret,*px); + DUPRAT(thisterm,pret); + + DUPNUM(n2,num_one); + + do { + NEXTTERM(xx,INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + +void sinhrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT tmpx= nullptr; + + if ( rat_ge( *px, rat_one, precision) ) + { + DUPRAT(tmpx,*px); + exprat(px, radix, precision); + tmpx->pp->sign *= -1; + exprat(&tmpx, radix, precision); + subrat( px, tmpx, precision); + divrat( px, rat_two, precision); + destroyrat( tmpx ); + } + else + { + _sinhrat( px, precision); + } +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: coshrat +// +// ARGUMENTS: x PRAT representation of number to take the cosine +// hyperbolic of +// +// RETURN: cosh of x in PRAT form. +// +// EXPLANATION: This uses Taylor series +// +// n +// ___ 2j +// \ ] X +// \ --------- +// / (2j)! +// /__] +// j=0 +// or, +// n +// ___ 2 +// \ ] X +// \ thisterm ; where thisterm = thisterm * --------- +// / j j+1 j (2j)*(2j+1) +// /__] +// j=0 +// +// thisterm = 1 ; and stop when thisterm < precision used. +// 0 n +// +// if x is bigger than 1.0 (e^x+e^-x)/2 is used. +// +//----------------------------------------------------------------------------- + + +void _coshrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + if ( !IsValidForHypFunc(*px, precision)) + { + // Don't attempt exp of anything large or small + throw( CALC_E_DOMAIN ); + } + + CREATETAYLOR(); + + pret->pp=longtonum( 1L, radix); + pret->pq=longtonum( 1L, radix); + + DUPRAT(thisterm,pret) + + n2=longtonum(0L, radix); + + do { + NEXTTERM(xx,INC(n2) DIVNUM(n2) INC(n2) DIVNUM(n2), precision); + } while ( !SMALL_ENOUGH_RAT( thisterm, precision) ); + + DESTROYTAYLOR(); +} + +void coshrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT tmpx= nullptr; + + (*px)->pp->sign = 1; + (*px)->pq->sign = 1; + if ( rat_ge( *px, rat_one, precision) ) + { + DUPRAT(tmpx,*px); + exprat(px, radix, precision); + tmpx->pp->sign *= -1; + exprat(&tmpx, radix, precision); + addrat( px, tmpx, precision); + divrat( px, rat_two, precision); + destroyrat( tmpx ); + } + else + { + _coshrat( px, radix, precision); + } + // Since *px might be epsilon below 1 due to TRIMIT + // we need this trick here. + if ( rat_lt(*px, rat_one, precision) ) + { + DUPRAT(*px,rat_one); + } +} + +//----------------------------------------------------------------------------- +// +// FUNCTION: tanhrat +// +// ARGUMENTS: x PRAT representation of number to take the tangent +// hyperbolic of +// +// RETURN: tanh of x in PRAT form. +// +// EXPLANATION: This uses sinhrat and coshrat +// +//----------------------------------------------------------------------------- + +void tanhrat( PRAT *px, uint32_t radix, int32_t precision) + +{ + PRAT ptmp= nullptr; + + DUPRAT(ptmp,*px); + sinhrat(px, radix, precision); + coshrat(&ptmp, radix, precision); + mulnumx(&((*px)->pp),ptmp->pq); + mulnumx(&((*px)->pq),ptmp->pp); + + destroyrat(ptmp); + +} diff --git a/src/CalcManager/UnitConverter.cpp b/src/CalcManager/UnitConverter.cpp new file mode 100644 index 00000000..a3109386 --- /dev/null +++ b/src/CalcManager/UnitConverter.cpp @@ -0,0 +1,1081 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Command.h" +#include "UnitConverter.h" + +using namespace concurrency; +using namespace std; +using namespace UnitConversionManager; + +static constexpr uint32_t EXPECTEDSERIALIZEDTOKENCOUNT = 7; +static constexpr uint32_t EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT = 3; +static constexpr uint32_t EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT = 3; +static constexpr uint32_t EXPECTEDSERIALIZEDUNITTOKENCOUNT = 6; +static constexpr uint32_t EXPECTEDSTATEDATATOKENCOUNT = 5; +static constexpr uint32_t EXPECTEDMAPCOMPONENTTOKENCOUNT = 2; + +static constexpr int32_t MAXIMUMDIGITSALLOWED = 15; +static constexpr int32_t OPTIMALDIGITSALLOWED = 7; + +static constexpr wchar_t LEFTESCAPECHAR = L'{'; +static constexpr wchar_t RIGHTESCAPECHAR = L'}'; + +static const double OPTIMALDECIMALALLOWED = pow(10, -1 * (OPTIMALDIGITSALLOWED - 1)); +static const double MINIMUMDECIMALALLOWED = pow(10, -1 * (MAXIMUMDIGITSALLOWED - 1)); + +unordered_map quoteConversions; +unordered_map unquoteConversions; + +/// +/// Constructor, sets up all the variables and requires a configLoader +/// +/// An instance of the IConverterDataLoader interface which we use to read in category/unit names and conversion data +UnitConverter::UnitConverter(_In_ const shared_ptr& dataLoader) : + UnitConverter::UnitConverter(dataLoader, nullptr) +{ +} + +/// +/// Constructor, sets up all the variables and requires two configLoaders +/// +/// An instance of the IConverterDataLoader interface which we use to read in category/unit names and conversion data +/// An instance of the IConverterDataLoader interface, specialized for loading currency data from an internet service +UnitConverter::UnitConverter(_In_ const shared_ptr& dataLoader, _In_ const shared_ptr& currencyDataLoader) +{ + m_dataLoader = dataLoader; + m_currencyDataLoader = currencyDataLoader; + //declaring the delimiter character conversion map + quoteConversions[L'|'] = L"{p}"; + quoteConversions[L'['] = L"{lc}"; + quoteConversions[L']'] = L"{rc}"; + quoteConversions[L':'] = L"{co}"; + quoteConversions[L','] = L"{cm}"; + quoteConversions[L';'] = L"{sc}"; + quoteConversions[LEFTESCAPECHAR] = L"{lb}"; + quoteConversions[RIGHTESCAPECHAR] = L"{rb}"; + unquoteConversions[L"{p}"] = L'|'; + unquoteConversions[L"{lc}"] = L'['; + unquoteConversions[L"{rc}"] = L']'; + unquoteConversions[L"{co}"] = L':'; + unquoteConversions[L"{cm}"] = L','; + unquoteConversions[L"{sc}"] = L';'; + unquoteConversions[L"{lb}"] = LEFTESCAPECHAR; + unquoteConversions[L"{rb}"] = RIGHTESCAPECHAR; + Reset(); +} + +void UnitConverter::Initialize() +{ + m_dataLoader->LoadData(); +} + +bool UnitConverter::CheckLoad() +{ + if (m_categories.empty()) + { + Reset(); + } + return !m_categories.empty(); +} + +/// +/// Returns a list of the categories in use by this converter +/// +vector UnitConverter::GetCategories() +{ + CheckLoad(); + return m_categories; +} + +/// +/// Sets the current category in use by this converter, +/// and returns a list of unit types that exist under the given category. +/// +/// Category struct which we are setting +CategorySelectionInitializer UnitConverter::SetCurrentCategory(const Category& input) +{ + if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(input)) + { + m_currencyDataLoader->LoadData(); + } + + vector newUnitList{}; + if (CheckLoad()) + { + if (m_currentCategory.id != input.id) + { + vector& unitVector = m_categoryToUnits[m_currentCategory]; + for (unsigned int i = 0; i < unitVector.size(); i++) + { + if (unitVector[i].id == m_fromType.id) + { + unitVector[i].isConversionSource = true; + } + else + { + unitVector[i].isConversionSource = false; + } + if (unitVector[i].id == m_toType.id) + { + unitVector[i].isConversionTarget = true; + } + else + { + unitVector[i].isConversionTarget = false; + } + } + m_currentCategory = input; + if (!m_currentCategory.supportsNegative && m_currentDisplay.front() == L'-') + { + m_currentDisplay.erase(0, 1); + } + } + + newUnitList = m_categoryToUnits[input]; + } + + InitializeSelectedUnits(); + return make_tuple(newUnitList, m_fromType, m_toType); +} + +/// +/// Gets the category currently being used +/// +Category UnitConverter::GetCurrentCategory() +{ + return m_currentCategory; +} + +/// +/// Sets the current unit types to be used, indicates a likely change in the +/// display values, so we re-calculate and callback the updated values +/// +/// Unit struct which the user is modifying +/// Unit struct we are converting to +void UnitConverter::SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) +{ + if (CheckLoad()) + { + m_fromType = fromType; + m_toType = toType; + Calculate(); + + UpdateCurrencySymbols(); + UpdateViewModel(); + } +} + +/// +/// Switches the active field, indicating that we are now entering data into +/// what was originally the return field, and storing results into what was +/// originally the current field. We swap appropriate values, +/// but do not callback, as values have not changed. +/// +/// +/// wstring representing the value user had in the field they've just activated. +/// We use this to handle cases where the front-end may choose to trim more digits +/// than we have been storing internally, in which case appending will not function +/// as expected without the use of this parameter. +/// +void UnitConverter::SwitchActive(const wstring& newValue) +{ + if (CheckLoad()) + { + swap(m_fromType, m_toType); + swap(m_currentHasDecimal, m_returnHasDecimal); + m_returnDisplay = m_currentDisplay; + m_currentDisplay = newValue; + m_currentHasDecimal = (m_currentDisplay.find(L'.') != m_currentDisplay.npos); + m_switchedActive = true; + + if (m_currencyDataLoader != nullptr && m_vmCurrencyCallback != nullptr) + { + shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); + const pair currencyRatios = currencyDataLoader->GetCurrencyRatioEquality(m_fromType, m_toType); + + m_vmCurrencyCallback->CurrencyRatiosCallback(currencyRatios.first, currencyRatios.second); + } + } +} + +wstring UnitConverter::CategoryToString(const Category& c, const wchar_t * delimiter) +{ + wstringstream out(wstringstream::out); + out << Quote(std::to_wstring(c.id)) << delimiter << Quote(std::to_wstring(c.supportsNegative)) << delimiter << Quote(c.name) << delimiter; + return out.str(); +} + +vector UnitConverter::StringToVector(const wstring& w, const wchar_t * delimiter, bool addRemainder) +{ + size_t delimiterIndex = w.find(delimiter); + size_t startIndex = 0; + vector serializedTokens = vector(); + while (delimiterIndex != w.npos) + { + serializedTokens.push_back(w.substr(startIndex, delimiterIndex - startIndex)); + startIndex = delimiterIndex + (int)wcslen(delimiter); + delimiterIndex = w.find(delimiter, startIndex); + } + if (addRemainder) + { + delimiterIndex = w.size(); + serializedTokens.push_back(w.substr(startIndex, delimiterIndex - startIndex)); + } + return serializedTokens; +} + +Category UnitConverter::StringToCategory(const wstring& w) +{ + vector tokenList = StringToVector(w, L";"); + assert(tokenList.size() == EXPECTEDSERIALIZEDCATEGORYTOKENCOUNT); + Category serializedCategory; + serializedCategory.id = _wtoi(Unquote(tokenList[0]).c_str()); + serializedCategory.supportsNegative = (tokenList[1].compare(L"1") == 0); + serializedCategory.name = Unquote(tokenList[2]); + return serializedCategory; +} + +wstring UnitConverter::UnitToString(const Unit& u, const wchar_t * delimiter) +{ + wstringstream out(wstringstream::out); + out << Quote(std::to_wstring(u.id)) << delimiter << Quote(u.name) << delimiter << Quote(u.abbreviation) << delimiter << std::to_wstring(u.isConversionSource) << delimiter << std::to_wstring(u.isConversionTarget) << delimiter << std::to_wstring(u.isWhimsical) << delimiter; + return out.str(); +} + +Unit UnitConverter::StringToUnit(const wstring& w) +{ + vector tokenList = StringToVector(w, L";"); + assert(tokenList.size() == EXPECTEDSERIALIZEDUNITTOKENCOUNT); + Unit serializedUnit; + serializedUnit.id = _wtoi(Unquote(tokenList[0]).c_str()); + serializedUnit.name = Unquote(tokenList[1]); + serializedUnit.accessibleName = serializedUnit.name; + serializedUnit.abbreviation = Unquote(tokenList[2]); + serializedUnit.isConversionSource = (tokenList[3].compare(L"1") == 0); + serializedUnit.isConversionTarget = (tokenList[4].compare(L"1") == 0); + serializedUnit.isWhimsical = (tokenList[5].compare(L"1") == 0); + return serializedUnit; +} + +ConversionData UnitConverter::StringToConversionData(const wstring& w) +{ + vector tokenList = StringToVector(w, L";"); + assert(tokenList.size() == EXPECTEDSERIALIZEDCONVERSIONDATATOKENCOUNT); + ConversionData serializedConversionData; + serializedConversionData.ratio = stod(Unquote(tokenList[0]).c_str()); + serializedConversionData.offset = stod(Unquote(tokenList[1]).c_str()); + serializedConversionData.offsetFirst = (tokenList[2].compare(L"1") == 0); + return serializedConversionData; +} + +wstring UnitConverter::ConversionDataToString(ConversionData d, const wchar_t * delimiter) +{ + wstringstream out(wstringstream::out); + out.precision(32); + out << fixed << d.ratio; + wstring ratio = out.str(); + out.str(L""); + out << fixed << d.offset; + wstring offset = out.str(); + out.str(L""); + TrimString(ratio); + TrimString(offset); + out << Quote(ratio) << delimiter << Quote(offset) << delimiter << std::to_wstring(d.offsetFirst) << delimiter; + return out.str(); +} + +/// +/// Serializes the data in the converter and returns it as a string +/// +wstring UnitConverter::Serialize() +{ + if (CheckLoad()) + { + wstringstream out(wstringstream::out); + const wchar_t * delimiter = L";"; + + out << UnitToString(m_fromType, delimiter) << "|"; + out << UnitToString(m_toType, delimiter) << "|"; + out << CategoryToString(m_currentCategory, delimiter) << "|"; + out << std::to_wstring(m_currentHasDecimal) << delimiter << std::to_wstring(m_returnHasDecimal) << delimiter << std::to_wstring(m_switchedActive) << delimiter; + out << m_currentDisplay << delimiter << m_returnDisplay << delimiter << "|"; + wstringstream categoryString(wstringstream::out); + wstringstream categoryToUnitString(wstringstream::out);; + wstringstream unitToUnitToDoubleString(wstringstream::out);; + for (const Category& c : m_categories) + { + categoryString << CategoryToString(c, delimiter) << ","; + } + + for (const auto& cur : m_categoryToUnits) + { + categoryToUnitString << CategoryToString(cur.first, delimiter) << "["; + for (const Unit& u : cur.second) + { + categoryToUnitString << UnitToString(u, delimiter) << ","; + } + categoryToUnitString << "[" << "]"; + } + + for (const auto& cur : m_ratioMap) + { + unitToUnitToDoubleString << UnitToString(cur.first, delimiter) << "["; + for (const auto& curConversion : cur.second) + { + unitToUnitToDoubleString << UnitToString(curConversion.first, delimiter) << ":"; + unitToUnitToDoubleString << ConversionDataToString(curConversion.second, delimiter) << ":,"; + } + unitToUnitToDoubleString << "[" << "]"; + } + + out << categoryString.str() << "|"; + out << categoryToUnitString.str() << "|"; + out << unitToUnitToDoubleString.str() << "|"; + wstring test = out.str(); + return test; + } + else + { + return wstring(); + } +} + +/// +/// De-Serializes the data in the converter from a string +/// +/// wstring holding the serialized data. If it does not have expected number of parameters, we will ignore it +void UnitConverter::DeSerialize(const wstring& serializedData) +{ + Reset(); + if (!serializedData.empty()) + { + vector outerTokens = StringToVector(serializedData, L"|"); + assert(outerTokens.size() == EXPECTEDSERIALIZEDTOKENCOUNT); + m_fromType = StringToUnit(outerTokens[0]); + m_toType = StringToUnit(outerTokens[1]); + m_currentCategory = StringToCategory(outerTokens[2]); + vector stateDataTokens = StringToVector(outerTokens[3], L";"); + assert(stateDataTokens.size() == EXPECTEDSTATEDATATOKENCOUNT); + m_currentHasDecimal = (stateDataTokens[0].compare(L"1") == 0); + m_returnHasDecimal = (stateDataTokens[1].compare(L"1") == 0); + m_switchedActive = (stateDataTokens[2].compare(L"1") == 0); + m_currentDisplay = stateDataTokens[3]; + m_returnDisplay = stateDataTokens[4]; + vector categoryListTokens = StringToVector(outerTokens[4], L","); + for (wstring token : categoryListTokens) + { + m_categories.push_back(StringToCategory(token)); + } + vector unitVectorTokens = StringToVector(outerTokens[5], L"]"); + for (wstring unitVector : unitVectorTokens) + { + vector mapcomponents = StringToVector(unitVector, L"["); + assert(mapcomponents.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); + Category key = StringToCategory(mapcomponents[0]); + vector units = StringToVector(mapcomponents[1], L","); + for (wstring unit : units) + { + m_categoryToUnits[key].push_back(StringToUnit(unit)); + } + } + vector ratioMapTokens = StringToVector(outerTokens[6], L"]"); + for (wstring token : ratioMapTokens) + { + vector ratioMapComponentTokens = StringToVector(token, L"["); + assert(ratioMapComponentTokens.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); + Unit key = StringToUnit(ratioMapComponentTokens[0]); + vector ratioMapList = StringToVector(ratioMapComponentTokens[1], L","); + for (wstring subtoken : ratioMapList) + { + vector ratioMapSubComponentTokens = StringToVector(subtoken, L":"); + assert(ratioMapSubComponentTokens.size() == EXPECTEDMAPCOMPONENTTOKENCOUNT); + Unit subkey = StringToUnit(ratioMapSubComponentTokens[0]); + ConversionData conversion = StringToConversionData(ratioMapSubComponentTokens[1]); + m_ratioMap[key][subkey] = conversion; + } + } + UpdateViewModel(); + } +} + +/// +/// De-Serializes the data in the converter from a string +/// +/// wstring holding the serialized data. If it does not have expected number of parameters, we will ignore it +void UnitConverter::RestoreUserPreferences(const wstring& userPreferences) +{ + if (!userPreferences.empty()) + { + vector outerTokens = StringToVector(userPreferences, L"|"); + if (outerTokens.size() == 3) + { + m_fromType = StringToUnit(outerTokens[0]); + m_toType = StringToUnit(outerTokens[1]); + m_currentCategory = StringToCategory(outerTokens[2]); + } + } +} + +/// +/// Serializes the Category and Associated Units in the converter and returns it as a string +/// +wstring UnitConverter::SaveUserPreferences() +{ + wstringstream out(wstringstream::out); + const wchar_t * delimiter = L";"; + + out << UnitToString(m_fromType, delimiter) << "|"; + out << UnitToString(m_toType, delimiter) << "|"; + out << CategoryToString(m_currentCategory, delimiter) << "|"; + wstring test = out.str(); + return test; +} + +/// +/// Sanitizes the input string, escape quoting any symbols we rely on for our delimiters, and returns the sanitized string. +/// +/// wstring to be sanitized +wstring UnitConverter::Quote(const wstring& s) +{ + wstringstream quotedString(wstringstream::out); + + //Iterate over the delimiter characters we need to quote + wstring::const_iterator cursor = s.begin(); + while(cursor != s.end()) + { + if (quoteConversions.find(*cursor) != quoteConversions.end()) + { + quotedString << quoteConversions[*cursor]; + } + else + { + quotedString << *cursor; + } + ++cursor; + } + return quotedString.str(); +} + +/// +/// Unsanitizes the sanitized input string, returning it to its original contents before we had quoted it. +/// +/// wstring to be unsanitized +wstring UnitConverter::Unquote(const wstring& s) +{ + wstringstream quotedSubString(wstringstream::out); + wstringstream unquotedString(wstringstream::out); + wstring::const_iterator cursor = s.begin(); + while(cursor != s.end()) + { + if(*cursor == LEFTESCAPECHAR) + { + quotedSubString.str(L""); + while (cursor != s.end() && *cursor != RIGHTESCAPECHAR) + { + quotedSubString << *cursor; + ++cursor; + } + if (cursor == s.end()) + { + //badly formatted + break; + } + else + { + quotedSubString << *cursor; + unquotedString << unquoteConversions[quotedSubString.str()]; + } + } + else + { + unquotedString << *cursor; + } + ++cursor; + } + return unquotedString.str(); +} + +/// +/// Handles inputs to the converter from the view-model, corresponding to a given button or keyboard press +/// +/// Command enum representing the command that was entered +void UnitConverter::SendCommand(Command command) +{ + if (CheckLoad()) + { + //TODO: Localization of characters + bool clearFront = false; + if (m_currentDisplay == L"0") + { + clearFront = true; + } + bool clearBack = false; + if ((m_currentHasDecimal && m_currentDisplay.size() - 1 >= MAXIMUMDIGITSALLOWED) || (!m_currentHasDecimal && m_currentDisplay.size() >= MAXIMUMDIGITSALLOWED)) + { + clearBack = true; + } + if (command != Command::Negate && m_switchedActive) + { + ClearValues(); + m_switchedActive = false; + clearFront = true; + clearBack = false; + } + switch (command) + { + case Command::Zero: + m_currentDisplay += L"0"; + break; + + case Command::One: + m_currentDisplay += L"1"; + break; + + case Command::Two: + m_currentDisplay += L"2"; + break; + + case Command::Three: + m_currentDisplay += L"3"; + break; + + case Command::Four: + m_currentDisplay += L"4"; + break; + + case Command::Five: + m_currentDisplay += L"5"; + break; + + case Command::Six: + m_currentDisplay += L"6"; + break; + + case Command::Seven: + m_currentDisplay += L"7"; + break; + + case Command::Eight: + m_currentDisplay += L"8"; + break; + + case Command::Nine: + m_currentDisplay += L"9"; + break; + + case Command::Decimal: + clearFront = false; + clearBack = false; + if (!m_currentHasDecimal) + { + m_currentDisplay += L"."; + m_currentHasDecimal = true; + } + break; + + case Command::Backspace: + clearFront = false; + clearBack = false; + if ((m_currentDisplay.front() != '-' && m_currentDisplay.size() > 1) || m_currentDisplay.size() > 2) + { + if (m_currentDisplay.back() == '.') + { + m_currentHasDecimal = false; + } + m_currentDisplay.pop_back(); + } + else + { + m_currentDisplay = L"0"; + m_currentHasDecimal = false; + } + break; + + case Command::Negate: + clearFront = false; + clearBack = false; + if (m_currentCategory.supportsNegative) + { + if (m_currentDisplay.front() == '-') + { + m_currentDisplay.erase(0, 1); + } + else + { + m_currentDisplay.insert(0, 1, '-'); + } + } + break; + + case Command::Clear: + clearFront = false; + clearBack = false; + ClearValues(); + break; + + case Command::Reset: + clearFront = false; + clearBack = false; + ClearValues(); + Reset(); + break; + + default: + break; + } + + + if (clearFront) + { + m_currentDisplay.erase(0, 1); + } + if (clearBack) + { + m_currentDisplay.erase(m_currentDisplay.size() - 1, 1); + m_vmCallback->MaxDigitsReached(); + } + + Calculate(); + + UpdateViewModel(); + } +} + +/// +/// Sets the callback interface to send display update calls to +/// +/// instance of IDisplayCallback interface that receives our update calls +void UnitConverter::SetViewModelCallback(_In_ const shared_ptr& newCallback) +{ + m_vmCallback = newCallback; + if (CheckLoad()) + { + UpdateViewModel(); + } +} + +void UnitConverter::SetViewModelCurrencyCallback(_In_ const shared_ptr& newCallback) +{ + m_vmCurrencyCallback = newCallback; + + shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); + if (currencyDataLoader != nullptr) + { + currencyDataLoader->SetViewModelCallback(newCallback); + } +} + +task> UnitConverter::RefreshCurrencyRatios() +{ + shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); + return create_task([this, currencyDataLoader]() + { + if (currencyDataLoader != nullptr) + { + return currencyDataLoader->TryLoadDataFromWebOverrideAsync(); + } + else + { + return task_from_result(false); + } + }).then([this, currencyDataLoader](bool didLoad) + { + wstring timestamp = L""; + if (currencyDataLoader != nullptr) + { + timestamp = currencyDataLoader->GetCurrencyTimestamp(); + } + + return make_pair(didLoad, timestamp); + }, task_continuation_context::use_default()); +} + +shared_ptr UnitConverter::GetCurrencyConverterDataLoader() +{ + return dynamic_pointer_cast(m_currencyDataLoader); +} + +/// +/// Converts a double value into another unit type, currently by multiplying by the given double ratio +/// +/// double input value to convert +/// double conversion ratio to use +double UnitConverter::Convert(double value, ConversionData conversionData) +{ + if (conversionData.offsetFirst) + { + return (value + conversionData.offset) * conversionData.ratio; + } + else + { + return (value * conversionData.ratio) + conversionData.offset; + } +} + +/// +/// Calculates the suggested values for the current display value and returns them as a vector +/// +vector> UnitConverter::CalculateSuggested() +{ + if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(m_currentCategory)) + { + return vector>(); + } + + vector> returnVector; + vector intermediateVector; + vector intermediateWhimsicalVector; + unordered_map ratios = m_ratioMap[m_fromType]; + //Calculate converted values for every other unit type in this category, along with their magnitude + for (const auto& cur : ratios) + { + if (cur.first != m_fromType && cur.first != m_toType) + { + double convertedValue = Convert(stod(m_currentDisplay), cur.second); + SuggestedValueIntermediate newEntry; + newEntry.magnitude = log10(convertedValue); + newEntry.value = convertedValue; + newEntry.type = cur.first; + if(newEntry.type.isWhimsical == false) + intermediateVector.push_back(newEntry); + else + intermediateWhimsicalVector.push_back(newEntry); + } + } + + //Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value + sort(intermediateVector.begin(), intermediateVector.end(), [] + (SuggestedValueIntermediate first, SuggestedValueIntermediate second) + { + if (abs(first.magnitude) == abs(second.magnitude)) + { + return first.magnitude > second.magnitude; + } + else + { + return abs(first.magnitude) < abs(second.magnitude); + } + }); + + //Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings + for (const auto& entry : intermediateVector) + { + wstring roundedString; + if (abs(entry.value) < 100) + { + roundedString = RoundSignificant(entry.value, 2); + } + else if (abs(entry.value) < 1000) + { + roundedString = RoundSignificant(entry.value, 1); + } + else + { + roundedString = RoundSignificant(entry.value, 0); + } + if (stod(roundedString) != 0.0 || m_currentCategory.supportsNegative) + { + TrimString(roundedString); + returnVector.push_back(make_tuple(roundedString, entry.type)); + } + } + + // The Whimsicals are determined differently + //Sort the resulting list by absolute magnitude, breaking ties by choosing the positive value + sort(intermediateWhimsicalVector.begin(), intermediateWhimsicalVector.end(), [] + (SuggestedValueIntermediate first, SuggestedValueIntermediate second) + { + if (abs(first.magnitude) == abs(second.magnitude)) + { + return first.magnitude > second.magnitude; + } + else + { + return abs(first.magnitude) < abs(second.magnitude); + } + }); + + //Now that the list is sorted, iterate over it and populate the return vector with properly rounded and formatted return strings + vector> whimsicalReturnVector; + + for (const auto& entry : intermediateWhimsicalVector) + { + wstring roundedString; + if (abs(entry.value) < 100) + { + roundedString = RoundSignificant(entry.value, 2); + } + else if (abs(entry.value) < 1000) + { + roundedString = RoundSignificant(entry.value, 1); + } + else + { + roundedString = RoundSignificant(entry.value, 0); + } + + // How to work out which is the best whimsical value to add to the vector? + if (stod(roundedString) != 0.0) + { + TrimString(roundedString); + whimsicalReturnVector.push_back(make_tuple(roundedString, entry.type)); + } + } + // Pickup the 'best' whimsical value - currently the first one + if (whimsicalReturnVector.size() != 0) + { + returnVector.push_back(whimsicalReturnVector.at(0)); + } + + // + + return returnVector; +} + +/// +/// Resets the converter to its initial state +/// +void UnitConverter::Reset() +{ + m_categories = m_dataLoader->LoadOrderedCategories(); + + ClearValues(); + m_switchedActive = false; + + if (!m_categories.empty()) + { + m_currentCategory = m_categories[0]; + + m_categoryToUnits.clear(); + m_ratioMap.clear(); + bool readyCategoryFound = false; + for (const Category& category : m_categories) + { + shared_ptr activeDataLoader = GetDataLoaderForCategory(category); + if (activeDataLoader == nullptr) + { + // The data loader is different depending on the category, e.g. currency data loader + // is different from the static data loader. + // If there is no data loader for this category, continue. + continue; + } + + vector units = activeDataLoader->LoadOrderedUnits(category); + m_categoryToUnits[category] = units; + + // Just because the units are empty, doesn't mean the user can't select this category, + // we just want to make sure we don't let an unready category be the default. + if (!units.empty()) + { + for (Unit u : units) + { + m_ratioMap[u] = activeDataLoader->LoadOrderedRatios(u); + } + + if (!readyCategoryFound) + { + m_currentCategory = category; + readyCategoryFound = true; + } + } + } + + InitializeSelectedUnits(); + Calculate(); + } +} + +/// +/// Sets the active data loader based on the input category. +/// +shared_ptr UnitConverter::GetDataLoaderForCategory(const Category& category) +{ + if (m_currencyDataLoader != nullptr && m_currencyDataLoader->SupportsCategory(category)) + { + return m_currencyDataLoader; + } + else + { + return m_dataLoader; + } +} + +/// +/// Sets the intial values for m_fromType and m_toType. +/// This is an internal helper method as opposed to SetCurrentUnits +/// which is for external use by clients. +/// If we fail to set units, we will fallback to the EMPTY_UNIT. +/// +void UnitConverter::InitializeSelectedUnits() +{ + + if (m_categoryToUnits.empty()) + { + return; + } + + auto itr = m_categoryToUnits.find(m_currentCategory); + if (itr == m_categoryToUnits.end()) + { + return; + } + + vector curUnits = itr->second; + if (!curUnits.empty()) + { + bool conversionSourceSet = false; + bool conversionTargetSet = false; + for (const Unit& cur : curUnits) + { + if (!conversionSourceSet && cur.isConversionSource) + { + m_fromType = cur; + conversionSourceSet = true; + } + + if (!conversionTargetSet && cur.isConversionTarget) + { + m_toType = cur; + conversionTargetSet = true; + } + + if (conversionSourceSet && conversionTargetSet) + { + return; + } + } + } + + m_fromType = EMPTY_UNIT; + m_toType = EMPTY_UNIT; +} + +/// +/// Resets the value fields to 0 +/// +void UnitConverter::ClearValues() +{ + m_currentHasDecimal = false; + m_returnHasDecimal = false; + m_currentDisplay = L"0"; +} + +/// +/// Checks if either unit is EMPTY_UNIT. +/// +bool UnitConverter::AnyUnitIsEmpty() +{ + return m_fromType == EMPTY_UNIT || m_toType == EMPTY_UNIT; +} + +/// +/// Calculates a new return value based on the current display value +/// +void UnitConverter::Calculate() +{ + unordered_map conversionTable = m_ratioMap[m_fromType]; + double returnValue = stod(m_currentDisplay); + if (AnyUnitIsEmpty() || (conversionTable[m_toType].ratio == 1.0 && conversionTable[m_toType].offset == 0.0)) + { + m_returnDisplay = m_currentDisplay; + TrimString(m_returnDisplay); + } + else + { + returnValue = Convert(returnValue, conversionTable[m_toType]); + m_returnDisplay = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED); + TrimString(m_returnDisplay); + int numPreDecimal = (int)m_returnDisplay.size(); + if (m_returnDisplay.find(L'.') != m_returnDisplay.npos) + { + numPreDecimal = (int)m_returnDisplay.find(L'.'); + } + if (returnValue < 0) + { + numPreDecimal--; + } + + if (numPreDecimal > MAXIMUMDIGITSALLOWED || (returnValue != 0 && abs(returnValue) < MINIMUMDECIMALALLOWED)) + { + wstringstream out(wstringstream::out); + out << scientific << returnValue; + m_returnDisplay = out.str(); + } + else + { + returnValue = stod(m_returnDisplay); + wstring returnString; + if (m_currentDisplay.size() <= OPTIMALDIGITSALLOWED && abs(returnValue) >= OPTIMALDECIMALALLOWED) + { + returnString = RoundSignificant(returnValue, OPTIMALDIGITSALLOWED - min(numPreDecimal, OPTIMALDIGITSALLOWED)); + } + else + { + returnString = RoundSignificant(returnValue, MAXIMUMDIGITSALLOWED - min(numPreDecimal, MAXIMUMDIGITSALLOWED)); + } + m_returnDisplay = returnString; + TrimString(m_returnDisplay); + } + } + + m_returnHasDecimal = (m_returnDisplay.find(L'.') != m_returnDisplay.npos); +} + +/// +/// Trims out any trailing zeroes or decimals in the given input string +/// +/// wstring to trim +void UnitConverter::TrimString(wstring& returnString) +{ + if (returnString.find(L'.') != m_returnDisplay.npos) + { + wstring::iterator iter; + for (iter = returnString.end() - 1; ;iter--) + { + if (*iter != L'0') + { + returnString.erase(iter + 1, returnString.end()); + break; + } + } + if (*(returnString.end()-1) == L'.') + { + returnString.erase(returnString.end()-1, returnString.end()); + } + } +} + +/// +/// Rounds the given double to the given number of significant digits +/// +/// input double +/// int number of significant digits to round to +wstring UnitConverter::RoundSignificant(double num, int numSignificant) +{ + wstringstream out(wstringstream::out); + out << fixed; + out.precision(numSignificant); + out << num; + return out.str(); +} + +void UnitConverter::UpdateCurrencySymbols() +{ + if (m_currencyDataLoader != nullptr && m_vmCurrencyCallback != nullptr) + { + shared_ptr currencyDataLoader = GetCurrencyConverterDataLoader(); + const pair currencySymbols = currencyDataLoader->GetCurrencySymbols(m_fromType, m_toType); + const pair currencyRatios = currencyDataLoader->GetCurrencyRatioEquality(m_fromType, m_toType); + + m_vmCurrencyCallback->CurrencySymbolsCallback(currencySymbols.first, currencySymbols.second); + m_vmCurrencyCallback->CurrencyRatiosCallback(currencyRatios.first, currencyRatios.second); + } +} + +void UnitConverter::UpdateViewModel() +{ + m_vmCallback->DisplayCallback(m_currentDisplay, m_returnDisplay); + m_vmCallback->SuggestedValueCallback(CalculateSuggested()); +} diff --git a/src/CalcManager/UnitConverter.h b/src/CalcManager/UnitConverter.h new file mode 100644 index 00000000..3f323c35 --- /dev/null +++ b/src/CalcManager/UnitConverter.h @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace UnitConversionManager +{ + enum class Command; + + struct Unit + { + Unit(){} + Unit(int id, std::wstring name, std::wstring abbreviation, bool isConversionSource, bool isConversionTarget, bool isWhimsical) + : id(id), name(name), accessibleName(name), abbreviation(abbreviation), isConversionSource(isConversionSource), isConversionTarget(isConversionTarget), isWhimsical(isWhimsical) + { + } + + Unit(int id, std::wstring currencyName, std::wstring countryName, std::wstring abbreviation, bool isRtlLanguage, bool isConversionSource, bool isConversionTarget) + : id(id), abbreviation(abbreviation), isConversionSource(isConversionSource), isConversionTarget(isConversionTarget), isWhimsical(false) + { + std::wstring nameValue1 = isRtlLanguage ? currencyName : countryName; + std::wstring nameValue2 = isRtlLanguage ? countryName : currencyName; + + name = nameValue1 + L" - " + nameValue2; + accessibleName = nameValue1 + L" " + nameValue2; + } + + virtual ~Unit() {} + + int id; + std::wstring name; + std::wstring accessibleName; + std::wstring abbreviation; + bool isConversionSource; + bool isConversionTarget; + bool isWhimsical; + + bool operator!= (const Unit& that) const + { + return that.id != id; + } + + bool operator== (const Unit& that) const + { + return that.id == id; + } + }; + + // The EMPTY_UNIT acts as a 'null-struct' so that + // Unit pointers can safely be dereferenced without + // null checks. + // + // unitId, name, abbreviation, isConversionSource, isConversionTarget, isWhimsical + const Unit EMPTY_UNIT = Unit{ -1, L"", L"", true, true, false }; + + struct Category + { + Category(){} + + Category(int id, std::wstring name, bool supportsNegative) : id(id), name(name), supportsNegative(supportsNegative) + { + } + + int id; + std::wstring name; + bool supportsNegative; + + bool operator!= (const Category& that) const + { + return that.id != id; + } + + bool operator== (const Category& that) const + { + return that.id == id; + } + }; + + class UnitHash + { + public: + size_t operator() (const Unit & x) const { + return x.id; + } + }; + + class CategoryHash + { + public: + size_t operator() (const Category & x) const { + return x.id; + } + }; + + struct SuggestedValueIntermediate + { + double magnitude; + double value; + Unit type; + }; + + struct ConversionData + { + ConversionData(){} + ConversionData(double ratio, double offset, bool offsetFirst) : ratio(ratio), offset(offset), offsetFirst(offsetFirst) + { + } + + virtual ~ConversionData() {} + + double ratio; + double offset; + bool offsetFirst; + }; + + struct CurrencyStaticData + { + std::wstring countryCode; + std::wstring countryName; + std::wstring currencyCode; + std::wstring currencyName; + std::wstring currencySymbol; + }; + + struct CurrencyRatio + { + double ratio; + std::wstring sourceCurrencyCode; + std::wstring targetCurrencyCode; + }; + + typedef std::tuple, UnitConversionManager::Unit, UnitConversionManager::Unit> CategorySelectionInitializer; + typedef std::unordered_map, UnitConversionManager::UnitHash> UnitToUnitToConversionDataMap; + typedef std::unordered_map, UnitConversionManager::CategoryHash> CategoryToUnitVectorMap; + + class IViewModelCurrencyCallback + { + public: + virtual ~IViewModelCurrencyCallback() { }; + virtual void CurrencyDataLoadFinished(bool didLoad) = 0; + virtual void CurrencySymbolsCallback(_In_ const std::wstring& fromSymbol, _In_ const std::wstring& toSymbol) = 0; + virtual void CurrencyRatiosCallback(_In_ const std::wstring& ratioEquality, _In_ const std::wstring& accRatioEquality) = 0; + virtual void CurrencyTimestampCallback(_In_ const std::wstring& timestamp, bool isWeekOldData) = 0; + virtual void NetworkBehaviorChanged(_In_ int newBehavior) = 0; + }; + + class IConverterDataLoader + { + public: + virtual ~IConverterDataLoader() { }; + virtual void LoadData() = 0; // prepare data if necessary before calling other functions + virtual std::vector LoadOrderedCategories() = 0; + virtual std::vector LoadOrderedUnits(const Category& c) = 0; + virtual std::unordered_map LoadOrderedRatios(const Unit& u) = 0; + virtual bool SupportsCategory(const Category& target) = 0; + }; + + class ICurrencyConverterDataLoader + { + public: + virtual void SetViewModelCallback(const std::shared_ptr& callback) = 0; + virtual std::pair GetCurrencySymbols(_In_ const UnitConversionManager::Unit& unit1, _In_ const UnitConversionManager::Unit& unit2) = 0; + virtual std::pair GetCurrencyRatioEquality(_In_ const UnitConversionManager::Unit& unit1, _In_ const UnitConversionManager::Unit& unit2) = 0; + virtual std::wstring GetCurrencyTimestamp() = 0; + + virtual concurrency::task TryLoadDataFromCacheAsync() = 0; + virtual concurrency::task TryLoadDataFromWebAsync() = 0; + virtual concurrency::task TryLoadDataFromWebOverrideAsync() = 0; + }; + + class IUnitConverterVMCallback + { + public: + virtual ~IUnitConverterVMCallback() { }; + virtual void DisplayCallback(const std::wstring& from, const std::wstring& to) = 0; + virtual void SuggestedValueCallback(const std::vector>& suggestedValues) = 0; + virtual void MaxDigitsReached() = 0; + }; + + class IUnitConverter + { + public: + virtual ~IUnitConverter() { } + virtual void Initialize() = 0; // Use to initialize first time, use deserialize instead to rehydrate + virtual std::vector GetCategories() = 0; + virtual CategorySelectionInitializer SetCurrentCategory(const Category& input) = 0; + virtual Category GetCurrentCategory() = 0; + virtual void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) = 0; + virtual void SwitchActive(const std::wstring& newValue) = 0; + virtual std::wstring Serialize() = 0; + virtual void DeSerialize(const std::wstring& serializedData) = 0; + virtual std::wstring SaveUserPreferences() = 0; + virtual void RestoreUserPreferences(_In_ const std::wstring& userPreferences) = 0; + virtual void SendCommand(Command command) = 0; + virtual void SetViewModelCallback(_In_ const std::shared_ptr& newCallback) = 0; + virtual void SetViewModelCurrencyCallback(_In_ const std::shared_ptr& newCallback) = 0; + virtual concurrency::task> RefreshCurrencyRatios() = 0; + }; + + class UnitConverter : public IUnitConverter, public std::enable_shared_from_this + { + public: + UnitConverter(_In_ const std::shared_ptr& dataLoader); + UnitConverter(_In_ const std::shared_ptr& dataLoader, _In_ const std::shared_ptr& currencyDataLoader); + + // IUnitConverter + void Initialize() override; + std::vector GetCategories() override; + CategorySelectionInitializer SetCurrentCategory(const Category& input) override; + Category GetCurrentCategory() override; + void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) override; + void SwitchActive(const std::wstring& newValue) override; + std::wstring Serialize() override; + void DeSerialize(const std::wstring& serializedData) override; + std::wstring SaveUserPreferences() override; + void RestoreUserPreferences(const std::wstring& userPreference) override; + void SendCommand(Command command) override; + void SetViewModelCallback(_In_ const std::shared_ptr& newCallback) override; + void SetViewModelCurrencyCallback(_In_ const std::shared_ptr& newCallback) override; + concurrency::task> RefreshCurrencyRatios() override; + // IUnitConverter + + static std::vector StringToVector(const std::wstring& w, const wchar_t * delimiter, bool addRemainder = false); + static std::wstring Quote(const std::wstring& s); + static std::wstring Unquote(const std::wstring& s); + + private: + bool CheckLoad(); + double Convert(double value, ConversionData conversionData); + std::vector> CalculateSuggested(); + void Reset(); + void ClearValues(); + void Calculate(); + void TrimString(std::wstring& input); + void InitializeSelectedUnits(); + std::wstring RoundSignificant(double num, int numSignificant); + Category StringToCategory(const std::wstring& w); + std::wstring CategoryToString(const Category& c, const wchar_t * delimiter); + std::wstring UnitToString(const Unit& u, const wchar_t * delimiter); + Unit StringToUnit(const std::wstring& w); + ConversionData StringToConversionData(const std::wstring& w); + std::wstring ConversionDataToString(ConversionData d, const wchar_t * delimiter); + void UpdateCurrencySymbols(); + void UpdateViewModel(); + bool AnyUnitIsEmpty(); + std::shared_ptr GetDataLoaderForCategory(const Category& category); + std::shared_ptr GetCurrencyConverterDataLoader(); + + private: + std::shared_ptr m_dataLoader; + std::shared_ptr m_currencyDataLoader; + std::shared_ptr m_vmCallback; + std::shared_ptr m_vmCurrencyCallback; + std::vector m_categories; + CategoryToUnitVectorMap m_categoryToUnits; + UnitToUnitToConversionDataMap m_ratioMap; + Category m_currentCategory; + Unit m_fromType; + Unit m_toType; + std::wstring m_currentDisplay; + std::wstring m_returnDisplay; + bool m_currentHasDecimal; + bool m_returnHasDecimal; + bool m_switchedActive; + }; +} diff --git a/src/CalcManager/pch.cpp b/src/CalcManager/pch.cpp new file mode 100644 index 00000000..1da170eb --- /dev/null +++ b/src/CalcManager/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" diff --git a/src/CalcManager/pch.h b/src/CalcManager/pch.h new file mode 100644 index 00000000..27e01ff3 --- /dev/null +++ b/src/CalcManager/pch.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/CalcManager/targetver.h b/src/CalcManager/targetver.h new file mode 100644 index 00000000..221efabb --- /dev/null +++ b/src/CalcManager/targetver.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/src/CalcViewModel/ApplicationViewModel.cpp b/src/CalcViewModel/ApplicationViewModel.cpp new file mode 100644 index 00000000..9d11419d --- /dev/null +++ b/src/CalcViewModel/ApplicationViewModel.cpp @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "ApplicationViewModel.h" +#include "StandardCalculatorViewModel.h" +#include "DateCalculatorViewModel.h" +#include "Common\AppResourceProvider.h" +#include "DataLoaders\CurrencyHttpClient.h" +#include "DataLoaders\CurrencyDataLoader.h" +#include "DataLoaders\UnitConverterDataLoader.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::DataLoaders; +using namespace CalculatorApp::ViewModel; +using namespace CalculationManager; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::System; +using namespace Windows::Storage; +using namespace Utils; +using namespace Windows::Foundation::Collections; +using namespace Windows::Globalization; +using namespace Windows::UI::ViewManagement; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; + +namespace CalculatorApp::ViewModel::ApplicationViewModelProperties +{ + StringReference Mode(L"Mode"); + StringReference PreviousMode(L"PreviousMode"); + StringReference ClearMemoryVisibility(L"ClearMemoryVisibility"); + StringReference AppBarVisibility(L"AppBarVisibility"); + StringReference CategoryName(L"CategoryName"); + StringReference Categories(L"Categories"); +} + +ApplicationViewModel::ApplicationViewModel() : + m_CalculatorViewModel(nullptr), + m_DateCalcViewModel(nullptr), + m_ConverterViewModel(nullptr), + m_PreviousMode(ViewMode::None), + m_mode(ViewMode::None), + m_categories(nullptr) +{ + SetMenuCategories(); +} + +void ApplicationViewModel::Mode::set(ViewMode value) +{ + if (m_mode != value) + { + PreviousMode = m_mode; + m_mode = value; + OnModeChanged(); + RaisePropertyChanged(ApplicationViewModelProperties::Mode); + } +} + +void ApplicationViewModel::Categories::set(IObservableVector^ value) +{ + if (m_categories != value) + { + m_categories = value; + RaisePropertyChanged(ApplicationViewModelProperties::Categories); + } +} + +void ApplicationViewModel::Initialize(ViewMode mode) +{ + if (!NavCategory::IsValidViewMode(mode)) + { + mode = ViewMode::Standard; + } + + try + { + Mode = mode; + } + catch (const std::exception& e) + { + TraceLogger::GetInstance().LogStandardException(__FUNCTIONW__, e); + if (!TryRecoverFromNavigationModeFailure()) + { + // Could not navigate to standard mode either. + // Throw the original exception so we have a good stack to debug. + throw; + } + } + catch (Exception^ e) + { + TraceLogger::GetInstance().LogPlatformException(__FUNCTIONW__, e); + if (!TryRecoverFromNavigationModeFailure()) + { + // Could not navigate to standard mode either. + // Throw the original exception so we have a good stack to debug. + throw; + } + } +} + +bool ApplicationViewModel::TryRecoverFromNavigationModeFailure() +{ + // Here we are simply trying to recover from being unable to navigate to a mode. + // Try falling back to standard mode and if there are *any* exceptions, we should + // fail because something is seriously wrong. + try + { + Mode = ViewMode::Standard; + return true; + } + catch (...) + { + return false; + } +} + +void ApplicationViewModel::OnModeChanged() +{ + assert(NavCategory::IsValidViewMode(m_mode)); + TraceLogger::GetInstance().LogModeChangeBegin(m_PreviousMode, m_mode, ApplicationView::GetApplicationViewIdForWindow(CoreWindow::GetForCurrentThread())); + if (NavCategory::IsCalculatorViewMode(m_mode)) + { + TraceLogger::GetInstance().LogCalculatorModeViewed(m_mode, ApplicationView::GetApplicationViewIdForWindow(CoreWindow::GetForCurrentThread())); + if (!m_CalculatorViewModel) + { + m_CalculatorViewModel = ref new StandardCalculatorViewModel(); + } + m_CalculatorViewModel->SetCalculatorType(m_mode); + } + else if (NavCategory::IsDateCalculatorViewMode(m_mode)) + { + TraceLogger::GetInstance().LogDateCalculatorModeViewed(m_mode, ApplicationView::GetApplicationViewIdForWindow(CoreWindow::GetForCurrentThread())); + if (!m_DateCalcViewModel) + { + m_DateCalcViewModel = ref new DateCalculatorViewModel(); + } + } + else if (NavCategory::IsConverterViewMode(m_mode)) + { + TraceLogger::GetInstance().LogConverterModeViewed(m_mode, ApplicationView::GetApplicationViewIdForWindow(CoreWindow::GetForCurrentThread())); + if (!m_ConverterViewModel) + { + auto dataLoader = make_shared(ref new GeographicRegion()); + auto currencyDataLoader = make_shared(make_unique()); + m_ConverterViewModel = ref new UnitConverterViewModel(make_shared(dataLoader, currencyDataLoader)); + } + + m_ConverterViewModel->Mode = m_mode; + } + + auto resProvider = AppResourceProvider::GetInstance(); + CategoryName = resProvider.GetResourceString(NavCategory::GetNameResourceKey(m_mode)); + + // This is the only place where a ViewMode enum should be cast to an int. + // + // Save the changed mode, so that the new window launches in this mode. + // Don't save until after we have adjusted to the new mode, so we don't save a mode that fails to load. + ApplicationData::Current->LocalSettings->Values->Insert(ApplicationViewModelProperties::Mode, NavCategory::Serialize(m_mode)); + + TraceLogger::GetInstance().LogModeChangeEnd(m_mode, ApplicationView::GetApplicationViewIdForWindow(CoreWindow::GetForCurrentThread())); + RaisePropertyChanged(ApplicationViewModelProperties::ClearMemoryVisibility); + RaisePropertyChanged(ApplicationViewModelProperties::AppBarVisibility); +} + +void ApplicationViewModel::OnCopyCommand(Object^ parameter) +{ + if (NavCategory::IsConverterViewMode(m_mode)) + { + ConverterViewModel->OnCopyCommand(parameter); + } + else if (NavCategory::IsDateCalculatorViewMode(m_mode)) + { + DateCalcViewModel->OnCopyCommand(parameter); + } + else + { + CalculatorViewModel->OnCopyCommand(parameter); + } +} + +void ApplicationViewModel::OnPasteCommand(Object^ parameter) +{ + if (NavCategory::IsConverterViewMode(m_mode)) + { + ConverterViewModel->OnPasteCommand(parameter); + } + else + { + CalculatorViewModel->OnPasteCommand(parameter); + } +} + +void ApplicationViewModel::SetMenuCategories() +{ + // Use the Categories property instead of the backing variable + // because we want to take advantage of binding updates and + // property setter logic. + Categories = NavCategoryGroup::CreateMenuOptions(); +} diff --git a/src/CalcViewModel/ApplicationViewModel.h b/src/CalcViewModel/ApplicationViewModel.h new file mode 100644 index 00000000..a8d459f7 --- /dev/null +++ b/src/CalcViewModel/ApplicationViewModel.h @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "StandardCalculatorViewModel.h" +#include "DateCalculatorViewModel.h" +#include "UnitConverterViewModel.h" + +namespace CalculatorApp +{ + namespace ViewModel + { + namespace ApplicationViewModelProperties + { + extern Platform::StringReference Mode; + extern Platform::StringReference PreviousMode; + extern Platform::StringReference ClearMemoryVisibility; + extern Platform::StringReference AppBarVisibility; + extern Platform::StringReference CategoryName; + extern Platform::StringReference Categories; + } + + [Windows::UI::Xaml::Data::Bindable] + public ref class ApplicationViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + ApplicationViewModel(); + + void Initialize(CalculatorApp::Common::ViewMode mode); // Use for first init, use deserialize for rehydration + + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(StandardCalculatorViewModel^, CalculatorViewModel); + OBSERVABLE_PROPERTY_RW(DateCalculatorViewModel^, DateCalcViewModel); + OBSERVABLE_PROPERTY_RW(CalculatorApp::ViewModel::UnitConverterViewModel^, ConverterViewModel); + OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::ViewMode, PreviousMode); + OBSERVABLE_PROPERTY_RW(Platform::String^, CategoryName); + + COMMAND_FOR_METHOD(CopyCommand, ApplicationViewModel::OnCopyCommand); + COMMAND_FOR_METHOD(PasteCommand, ApplicationViewModel::OnPasteCommand); + + property CalculatorApp::Common::ViewMode Mode + { + CalculatorApp::Common::ViewMode get() + { + return m_mode; + } + + void set(CalculatorApp::Common::ViewMode value); + } + + property Windows::Foundation::Collections::IObservableVector^ Categories + { + Windows::Foundation::Collections::IObservableVector^ get() + { + return m_categories; + } + + void set(Windows::Foundation::Collections::IObservableVector^ value); + } + + property Windows::UI::Xaml::Visibility ClearMemoryVisibility + { + Windows::UI::Xaml::Visibility get() + { + return CalculatorApp::Common::NavCategory::IsCalculatorViewMode(Mode) + ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + property Windows::UI::Xaml::Visibility AppBarVisibility + { + Windows::UI::Xaml::Visibility get() + { + return CalculatorApp::Common::NavCategory::IsCalculatorViewMode(Mode) + ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + private: + bool TryRecoverFromNavigationModeFailure(); + + void OnModeChanged(); + + void OnCopyCommand(Platform::Object^ parameter); + void OnPasteCommand(Platform::Object^ parameter); + + void SetMenuCategories(); + + CalculatorApp::Common::ViewMode m_mode; + Windows::Foundation::Collections::IObservableVector^ m_categories; + }; + } +} diff --git a/src/CalcViewModel/CalcViewModel.vcxproj b/src/CalcViewModel/CalcViewModel.vcxproj new file mode 100644 index 00000000..8ace7400 --- /dev/null +++ b/src/CalcViewModel/CalcViewModel.vcxproj @@ -0,0 +1,409 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {90e9761d-9262-4773-942d-caeae75d7140} + StaticLibrary + CalcViewModel + en-US + 14.0 + true + Windows Store + 10.0.17763.0 + 10.0.17134.0 + 10.0 + + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + StaticLibrary + false + true + v141 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + Use + true + true + $(SolutionDir)..\src\;%(AdditionalIncludeDirectories) + 4453 + /bigobj /await /std:c++17 %(AdditionalOptions) + + + Console + false + false + + + /ignore:4264 %(AdditionalOptions) + + + + + /DSEND_TELEMETRY %(AdditionalOptions) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + Create + Create + Create + Create + + + + + + + + {311e866d-8b93-4609-a691-265941fee101} + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/CalcViewModel/CalcViewModel.vcxproj.filters b/src/CalcViewModel/CalcViewModel.vcxproj.filters new file mode 100644 index 00000000..5154242f --- /dev/null +++ b/src/CalcViewModel/CalcViewModel.vcxproj.filters @@ -0,0 +1,227 @@ + + + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {1daab7c4-63f6-4266-a259-f34acad66d09} + + + {8d4edf06-c312-4312-978a-b6c2beb8295a} + + + {0184f727-b8aa-4af8-a699-63f1b56e7853} + + + + + + + + + + + + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + DataLoaders + + + DataLoaders + + + DataLoaders + + + + + + + + + + + + + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + Common\Automation + + + DataLoaders + + + DataLoaders + + + DataLoaders + + + DataLoaders + + + DataLoaders + + + Common + + + + + DataLoaders + + + + \ No newline at end of file diff --git a/src/CalcViewModel/Common/AlwaysSelectedCollectionView.h b/src/CalcViewModel/Common/AlwaysSelectedCollectionView.h new file mode 100644 index 00000000..e04f1360 --- /dev/null +++ b/src/CalcViewModel/Common/AlwaysSelectedCollectionView.h @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp { namespace Common +{ + ref class AlwaysSelectedCollectionView sealed: + public Windows::UI::Xaml::DependencyObject, + public Windows::UI::Xaml::Data::ICollectionView + { + internal: + AlwaysSelectedCollectionView(Windows::UI::Xaml::Interop::IBindableVector^ source): + m_currentPosition(-1) + { + m_source = source; + + Windows::UI::Xaml::Interop::IBindableObservableVector^ observable = dynamic_cast(source); + if (observable) + { + observable->VectorChanged += + ref new Windows::UI::Xaml::Interop::BindableVectorChangedEventHandler(this, &AlwaysSelectedCollectionView::OnSourceBindableVectorChanged); + } + } + + private: + // ICollectionView + // Not implemented methods + virtual WF::IAsyncOperation^ LoadMoreItemsAsync(unsigned int) = Windows::UI::Xaml::Data::ICollectionView::LoadMoreItemsAsync + { + throw ref new Platform::NotImplementedException(); + } + virtual bool MoveCurrentToFirst() = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentToFirst + { + throw ref new Platform::NotImplementedException(); + } + virtual bool MoveCurrentToLast() = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentToLast + { + throw ref new Platform::NotImplementedException(); + } + virtual bool MoveCurrentToNext() = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentToNext + { + throw ref new Platform::NotImplementedException(); + } + virtual bool MoveCurrentToPrevious() = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentToPrevious + { + throw ref new Platform::NotImplementedException(); + } + property Windows::Foundation::Collections::IObservableVector^ CollectionGroups + { + virtual Windows::Foundation::Collections::IObservableVector^ get() = Windows::UI::Xaml::Data::ICollectionView::CollectionGroups::get + { + return ref new Platform::Collections::Vector(); + } + } + property bool HasMoreItems + { + virtual bool get() = Windows::UI::Xaml::Data::ICollectionView::HasMoreItems::get + { + return false; + } + } + + // Implementented methods + virtual bool MoveCurrentTo(Platform::Object^ item) = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentTo + { + if (item) + { + unsigned int newCurrentPosition = 0; + bool result = m_source->IndexOf(item, &newCurrentPosition); + if (result) + { + m_currentPosition = newCurrentPosition; + m_currentChanged(this, nullptr); + return true; + } + } + + // The item is not in the collection + // We're going to schedule a call back later so we + // restore the selection to the way we wanted it to begin with + if (m_currentPosition >= 0 && m_currentPosition < static_cast(m_source->Size)) + { + this->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, + ref new Windows::UI::Core::DispatchedHandler( + [this]() + { + m_currentChanged(this, nullptr); + })); + } + return false; + } + + virtual bool MoveCurrentToPosition(int index) = Windows::UI::Xaml::Data::ICollectionView::MoveCurrentToPosition + { + if (index < 0 || index >= static_cast(m_source->Size)) + { + return false; + } + + m_currentPosition = index; + m_currentChanged(this, nullptr); + return true; + } + + property Platform::Object^ CurrentItem + { + virtual Platform::Object^ get() = Windows::UI::Xaml::Data::ICollectionView::CurrentItem::get + { + if (m_currentPosition >= 0 && m_currentPosition < static_cast(m_source->Size)) + { + return m_source->GetAt(m_currentPosition); + } + return nullptr; + } + } + + property int CurrentPosition + { + virtual int get() = Windows::UI::Xaml::Data::ICollectionView::CurrentPosition::get + { + return m_currentPosition; + } + } + + property bool IsCurrentAfterLast + { + virtual bool get() = Windows::UI::Xaml::Data::ICollectionView::IsCurrentAfterLast::get + { + return m_currentPosition >= static_cast(m_source->Size); + } + } + + property bool IsCurrentBeforeFirst + { + virtual bool get() = Windows::UI::Xaml::Data::ICollectionView::IsCurrentBeforeFirst::get + { + return m_currentPosition < 0; + } + } + + event WF::EventHandler^ CurrentChanged + { + virtual WF::EventRegistrationToken add(WF::EventHandler^ handler) = Windows::UI::Xaml::Data::ICollectionView::CurrentChanged::add + { + return m_currentChanged += handler; + } + virtual void remove(WF::EventRegistrationToken token) = Windows::UI::Xaml::Data::ICollectionView::CurrentChanged::remove + { + m_currentChanged -= token; + } + } + event Windows::UI::Xaml::Data::CurrentChangingEventHandler^ CurrentChanging + { + virtual WF::EventRegistrationToken add(Windows::UI::Xaml::Data::CurrentChangingEventHandler^ handler) = Windows::UI::Xaml::Data::ICollectionView::CurrentChanging::add + { + return m_currentChanging += handler; + } + virtual void remove(WF::EventRegistrationToken token) = Windows::UI::Xaml::Data::ICollectionView::CurrentChanging::remove + { + m_currentChanging -= token; + } + } + + // IVector + // Not implemented methods + virtual void Append(Platform::Object^ /*item*/) = Windows::Foundation::Collections::IVector::Append + { + throw ref new Platform::NotImplementedException(); + } + virtual void Clear() = Windows::Foundation::Collections::IVector::Clear + { + throw ref new Platform::NotImplementedException(); + } + virtual unsigned int GetMany(unsigned int /*startIndex*/, Platform::WriteOnlyArray^ /*items*/) = Windows::Foundation::Collections::IVector::GetMany + { + throw ref new Platform::NotImplementedException(); + } + virtual Windows::Foundation::Collections::IVectorView^ GetView() = Windows::Foundation::Collections::IVector::GetView + { + throw ref new Platform::NotImplementedException(); + } + virtual void InsertAt(unsigned int /*index*/, Platform::Object^ /*item*/) = Windows::Foundation::Collections::IVector::InsertAt + { + throw ref new Platform::NotImplementedException(); + } + virtual void RemoveAt(unsigned int /*index*/) = Windows::Foundation::Collections::IVector::RemoveAt + { + throw ref new Platform::NotImplementedException(); + } + virtual void RemoveAtEnd() = Windows::Foundation::Collections::IVector::RemoveAtEnd + { + throw ref new Platform::NotImplementedException(); + } + virtual void ReplaceAll(const Platform::Array^ /*items*/) = Windows::Foundation::Collections::IVector::ReplaceAll + { + throw ref new Platform::NotImplementedException(); + } + virtual void SetAt(unsigned int /*index*/, Platform::Object^ /*item*/) = Windows::Foundation::Collections::IVector::SetAt + { + throw ref new Platform::NotImplementedException(); + } + + // Implemented methods + virtual Platform::Object^ GetAt(unsigned int index) = Windows::Foundation::Collections::IVector::GetAt + { + return m_source->GetAt(index); + } + + virtual bool IndexOf(Platform::Object^ item, unsigned int* index) = Windows::Foundation::Collections::IVector::IndexOf + { + return m_source->IndexOf(item, index); + } + + property unsigned int Size + { + virtual unsigned int get() = Windows::Foundation::Collections::IVector::Size::get + { + return m_source->Size; + } + } + + // IObservableVector + event Windows::Foundation::Collections::VectorChangedEventHandler^ VectorChanged + { + virtual WF::EventRegistrationToken add(Windows::Foundation::Collections::VectorChangedEventHandler^ handler) = Windows::Foundation::Collections::IObservableVector::VectorChanged::add + { + return m_vectorChanged += handler; + } + virtual void remove(WF::EventRegistrationToken token) = Windows::Foundation::Collections::IObservableVector::VectorChanged::remove + { + m_vectorChanged -= token; + } + } + + // IIterable + // Not implemented + virtual Windows::Foundation::Collections::IIterator^ First() = Windows::Foundation::Collections::IIterable::First + { + throw ref new Platform::NotImplementedException(); + } + + // Event handlers + void OnSourceBindableVectorChanged(Windows::UI::Xaml::Interop::IBindableObservableVector^ source, Platform::Object^ e) + { + Windows::Foundation::Collections::IVectorChangedEventArgs^ args = safe_cast(e); + m_vectorChanged(this, args); + } + + Windows::UI::Xaml::Interop::IBindableVector^ m_source; + int m_currentPosition; + event WF::EventHandler^ m_currentChanged; + event Windows::UI::Xaml::Data::CurrentChangingEventHandler^ m_currentChanging; + event Windows::Foundation::Collections::VectorChangedEventHandler^ m_vectorChanged; + }; + + public ref class AlwaysSelectedCollectionViewConverter sealed: public Windows::UI::Xaml::Data::IValueConverter + { + public: + AlwaysSelectedCollectionViewConverter() + { } + + private: + virtual Platform::Object^ Convert( + Platform::Object^ value, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::Convert + { + auto result = dynamic_cast(value); + if (result) + { + return ref new AlwaysSelectedCollectionView(result); + } + return Windows::UI::Xaml::DependencyProperty::UnsetValue; // Can't convert + } + + virtual Platform::Object^ ConverBack( + Platform::Object^ /*value*/, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::ConvertBack + { + return Windows::UI::Xaml::DependencyProperty::UnsetValue; + } + }; +}} diff --git a/src/CalcViewModel/Common/AppResourceProvider.cpp b/src/CalcViewModel/Common/AppResourceProvider.cpp new file mode 100644 index 00000000..d7982827 --- /dev/null +++ b/src/CalcViewModel/Common/AppResourceProvider.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "pch.h" +#include "AppResourceProvider.h" + +using namespace Platform; +using namespace Windows::ApplicationModel::Resources; +using namespace CalculatorApp; + +AppResourceProvider::AppResourceProvider() +{ + m_stringResLoader = ResourceLoader::GetForViewIndependentUse(); + m_cEngineStringResLoader = ResourceLoader::GetForViewIndependentUse(L"CEngineStrings"); +} + +AppResourceProvider & AppResourceProvider::GetInstance() +{ + static AppResourceProvider s_instance; + return s_instance; +} + +String^ AppResourceProvider::GetResourceString(_In_ String^ key) +{ + return m_stringResLoader->GetString(key); +} + +String^ AppResourceProvider::GetCEngineString(_In_ String^ key) +{ + return m_cEngineStringResLoader->GetString(key); +} diff --git a/src/CalcViewModel/Common/AppResourceProvider.h b/src/CalcViewModel/Common/AppResourceProvider.h new file mode 100644 index 00000000..5807e82f --- /dev/null +++ b/src/CalcViewModel/Common/AppResourceProvider.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + class AppResourceProvider + { + public: + static AppResourceProvider & GetInstance(); + Platform::String^ GetResourceString(_In_ Platform::String^ key); + Platform::String^ GetCEngineString(_In_ Platform::String^ key); + + private: + AppResourceProvider(); + Windows::ApplicationModel::Resources::ResourceLoader^ m_stringResLoader; + Windows::ApplicationModel::Resources::ResourceLoader^ m_cEngineStringResLoader; + }; +} diff --git a/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h b/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h new file mode 100644 index 00000000..40069a7c --- /dev/null +++ b/src/CalcViewModel/Common/Automation/INarratorAnnouncementHost.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "NarratorAnnouncement.h" + +// Declaration of the INarratorAnnouncementHost interface. +// This interface exists to hide the concrete announcement host +// being used. Depending on the version of the OS the app is running on, +// the app may need a host that uses LiveRegionChanged or RaiseNotification. + +namespace CalculatorApp::Common::Automation +{ + public interface class INarratorAnnouncementHost + { + public: + // Is the host available on this OS. + bool IsHostAvailable(); + + // Make a new instance of a concrete host. + INarratorAnnouncementHost^ MakeHost(); + + // Make an announcement using the concrete host's preferred method. + void Announce(NarratorAnnouncement^ announcement); + }; +} diff --git a/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp b/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp new file mode 100644 index 00000000..614f4595 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/LiveRegionHost.cpp @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "LiveRegionHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::UI::Xaml::Controls; + +LiveRegionHost::LiveRegionHost() : + m_host(nullptr) +{} + +bool LiveRegionHost::IsHostAvailable() +{ + // LiveRegion is always available. + return true; +} + +INarratorAnnouncementHost^ LiveRegionHost::MakeHost() +{ + return ref new LiveRegionHost(); +} + +void LiveRegionHost::Announce(NarratorAnnouncement^ announcement) +{ + if (m_host == nullptr) + { + m_host = ref new TextBlock(); + AutomationProperties::SetLiveSetting(m_host, AutomationLiveSetting::Assertive); + } + + AutomationProperties::SetName(m_host, announcement->Announcement); + AutomationPeer^ peer = FrameworkElementAutomationPeer::FromElement(m_host); + if (peer != nullptr) + { + peer->RaiseAutomationEvent(AutomationEvents::LiveRegionChanged); + } +} diff --git a/src/CalcViewModel/Common/Automation/LiveRegionHost.h b/src/CalcViewModel/Common/Automation/LiveRegionHost.h new file mode 100644 index 00000000..212b8534 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/LiveRegionHost.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the LiveRegionHost class. +// This class announces NarratorAnnouncements using the LiveRegionChanged event. +// This event is unreliable and should be deprecated in favor of the new +// RaiseNotification API in RS3. + +namespace CalculatorApp::Common::Automation +{ + // This class exists so that the app can run on RS2 and use LiveRegions + // to host notifiactions on those builds. + // When the app switches to min version RS3, this class can be removed + // and the app will switch to using the Notification API. + // TODO - MSFT 12735088 + public ref class LiveRegionHost sealed : public INarratorAnnouncementHost + { + public: + LiveRegionHost(); + + virtual bool IsHostAvailable(); + virtual INarratorAnnouncementHost^ MakeHost(); + + virtual void Announce(NarratorAnnouncement^ announcement); + + private: + Windows::UI::Xaml::UIElement^ m_host; + }; +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp new file mode 100644 index 00000000..72b4a712 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.cpp @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NarratorAnnouncement.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Platform; + +namespace CalculatorApp::Common::Automation +{ + namespace CalculatorActivityIds + { + StringReference DisplayUpdated(L"DisplayUpdated"); + StringReference MaxDigitsReached(L"MaxDigitsReached"); + StringReference MemoryCleared(L"MemoryCleared"); + StringReference MemoryItemChanged(L"MemorySlotChanged"); + StringReference MemoryItemAdded(L"MemorySlotAdded"); + StringReference HistoryCleared(L"HistoryCleared"); + StringReference CategoryNameChanged(L"CategoryNameChanged"); + StringReference UpdateCurrencyRates(L"UpdateCurrencyRates"); + StringReference DisplayCopied(L"DisplayCopied"); + } +} + +NarratorAnnouncement::NarratorAnnouncement( + String^ announcement, + String^ activityId, + AutomationNotificationKind kind, + AutomationNotificationProcessing processing) + : + m_announcement(announcement), + m_activityId(activityId), + m_kind(kind), + m_processing(processing) +{} + +String^ NarratorAnnouncement::Announcement::get() +{ + return m_announcement; +} + +String^ NarratorAnnouncement::ActivityId::get() +{ + return m_activityId; +} + +AutomationNotificationKind NarratorAnnouncement::Kind::get() +{ + return m_kind; +} + +AutomationNotificationProcessing NarratorAnnouncement::Processing::get() +{ + return m_processing; +} + +bool NarratorAnnouncement::IsValid(NarratorAnnouncement^ announcement) +{ + return announcement != nullptr + && announcement->Announcement != nullptr + && !announcement->Announcement->IsEmpty(); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetDisplayUpdatedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::DisplayUpdated, + AutomationNotificationKind::Other, + AutomationNotificationProcessing::ImportantMostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetMaxDigitsReachedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::MaxDigitsReached, + AutomationNotificationKind::Other, + AutomationNotificationProcessing::ImportantMostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetMemoryClearedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::MemoryCleared, + AutomationNotificationKind::ItemRemoved, + AutomationNotificationProcessing::ImportantMostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetMemoryItemChangedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::MemoryItemChanged, + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::MostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetMemoryItemAddedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::MemoryItemAdded, + AutomationNotificationKind::ItemAdded, + AutomationNotificationProcessing::MostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetHistoryClearedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::HistoryCleared, + AutomationNotificationKind::ItemRemoved, + AutomationNotificationProcessing::MostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetCategoryNameChangedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::CategoryNameChanged, + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::ImportantMostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetUpdateCurrencyRatesAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::UpdateCurrencyRates, + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::ImportantMostRecent); +} + +NarratorAnnouncement^ CalculatorAnnouncement::GetDisplayCopiedAnnouncement(String^ announcement) +{ + return ref new NarratorAnnouncement( + announcement, + CalculatorActivityIds::DisplayCopied, + AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::ImportantMostRecent); +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h new file mode 100644 index 00000000..16635b4a --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncement.h @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp::Common::Automation +{ + // These enum types are copied from the types available in + // Windows::UI::Xaml::Automation::Peers in the RS3 SDK. + // When this app switches to min version RS3, these custom + // enums should be removed and the Windows types should be used + // instead. + // TODO - MSFT 12735088 + public enum class AutomationNotificationKind + { + ItemAdded = 0, + ItemRemoved = 1, + ActionCompleted = 2, + ActionAborted = 3, + Other = 4 + }; + + public enum class AutomationNotificationProcessing + { + ImportantAll = 0, + ImportantMostRecent = 1, + All = 2, + MostRecent = 3, + CurrentThenMostRecent = 4 + }; + + public ref class NarratorAnnouncement sealed + { + public: + property Platform::String^ Announcement + { + Platform::String^ get(); + } + + property Platform::String^ ActivityId + { + Platform::String^ get(); + } + + property AutomationNotificationKind Kind + { + AutomationNotificationKind get(); + } + + property AutomationNotificationProcessing Processing + { + AutomationNotificationProcessing get(); + } + + static bool IsValid(NarratorAnnouncement^ announcement); + + private: + // Make CalculatorAnnouncement a friend class so it is the only + // class that can access the private constructor. + friend class CalculatorAnnouncement; + + NarratorAnnouncement( + Platform::String^ announcement, + Platform::String^ activityId, + AutomationNotificationKind kind, + AutomationNotificationProcessing processing); + + Platform::String^ m_announcement; + Platform::String^ m_activityId; + AutomationNotificationKind m_kind; + AutomationNotificationProcessing m_processing; + }; + + // CalculatorAnnouncement is intended to contain only static methods + // that return announcements made for the Calculator app. + class CalculatorAnnouncement + { + public: + static NarratorAnnouncement^ GetDisplayUpdatedAnnouncement(Platform::String^ announcement); + static NarratorAnnouncement^ GetMaxDigitsReachedAnnouncement(Platform::String^ announcement); + + static NarratorAnnouncement^ GetMemoryClearedAnnouncement(Platform::String^ announcement); + static NarratorAnnouncement^ GetMemoryItemChangedAnnouncement(Platform::String^ announcement); + static NarratorAnnouncement^ GetMemoryItemAddedAnnouncement(Platform::String^ announcement); + + static NarratorAnnouncement^ GetHistoryClearedAnnouncement(Platform::String^ announcement); + + static NarratorAnnouncement^ GetCategoryNameChangedAnnouncement(Platform::String^ announcement); + + static NarratorAnnouncement^ GetUpdateCurrencyRatesAnnouncement(Platform::String^ announcement); + + static NarratorAnnouncement^ GetDisplayCopiedAnnouncement(Platform::String^ announcement); + }; +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp new file mode 100644 index 00000000..5c62a3bd --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NarratorAnnouncementHostFactory.h" +#include "NotificationHost.h" +#include "LiveRegionHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace std; + +INarratorAnnouncementHost^ NarratorAnnouncementHostFactory::s_hostProducer; +vector NarratorAnnouncementHostFactory::s_hosts; + +// This static variable is used only to call the initialization function, to initialize the other static variables. +int NarratorAnnouncementHostFactory::s_init = NarratorAnnouncementHostFactory::Initialize(); +int NarratorAnnouncementHostFactory::Initialize() +{ + RegisterHosts(); + NarratorAnnouncementHostFactory::s_hostProducer = GetHostProducer(); + + return 0; +} + +// For now, there are two type of announcement hosts. +// We'd prefer to use Notification if it's available and fallback to LiveRegion +// if not. The availabilty of the host depends on the version of the OS the app is running on. +// When the app switches to min version RS3, the LiveRegionHost can be removed and we will always +// use NotificationHost. +// TODO - MSFT 12735088 +void NarratorAnnouncementHostFactory::RegisterHosts() +{ + // The host that will be used is the first available host, + // therefore, order of hosts is important here. + NarratorAnnouncementHostFactory::s_hosts = { + ref new NotificationHost(), + ref new LiveRegionHost() + }; +} + +INarratorAnnouncementHost^ NarratorAnnouncementHostFactory::GetHostProducer() +{ + for (INarratorAnnouncementHost^ host : NarratorAnnouncementHostFactory::s_hosts) + { + if (host->IsHostAvailable()) + { + return host; + } + } + + assert(false && L"No suitable AnnouncementHost was found."); + return nullptr; +} + +INarratorAnnouncementHost^ NarratorAnnouncementHostFactory::MakeHost() +{ + if (NarratorAnnouncementHostFactory::s_hostProducer == nullptr) + { + assert(false && L"No host producer has been assigned."); + return nullptr; + } + + return NarratorAnnouncementHostFactory::s_hostProducer->MakeHost(); +} diff --git a/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h new file mode 100644 index 00000000..c11a894e --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorAnnouncementHostFactory.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the NarratorAnnouncementHostFactory class. +// This class exists to hide the construction of a concrete INarratorAnnouncementHost. +// Depending on the version of the OS the app is running on, the factory will return +// an announcement host appropriate for that version. + +namespace CalculatorApp::Common::Automation +{ + class NarratorAnnouncementHostFactory + { + public: + static INarratorAnnouncementHost^ MakeHost(); + + private: + NarratorAnnouncementHostFactory() {} + + static int Initialize(); + static void RegisterHosts(); + static INarratorAnnouncementHost^ GetHostProducer(); + + private: + static int s_init; + static INarratorAnnouncementHost^ s_hostProducer; + static std::vector s_hosts; + }; +} diff --git a/src/CalcViewModel/Common/Automation/NarratorNotifier.cpp b/src/CalcViewModel/Common/Automation/NarratorNotifier.cpp new file mode 100644 index 00000000..4dc48074 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorNotifier.cpp @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Implementation of the NarratorNotifier class. + +#include "pch.h" +#include "NarratorNotifier.h" +#include "NarratorAnnouncementHostFactory.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; + +DependencyProperty^ NarratorNotifier::s_announcementProperty; + +NarratorNotifier::NarratorNotifier() +{ + m_announcementHost = NarratorAnnouncementHostFactory::MakeHost(); +} + +void NarratorNotifier::Announce(NarratorAnnouncement^ announcement) +{ + if (NarratorAnnouncement::IsValid(announcement) + && m_announcementHost != nullptr) + { + m_announcementHost->Announce(announcement); + } +} + +void NarratorNotifier::RegisterDependencyProperties() +{ + s_announcementProperty = DependencyProperty::Register( + L"Announcement", // The name of the dependency property. + NarratorAnnouncement::typeid, // The type of the dependency property. + NarratorNotifier::typeid, // The owner of the dependency property. + ref new PropertyMetadata( + nullptr, // Default value of the dependency property. + ref new PropertyChangedCallback(OnAnnouncementChanged))); +} + +void NarratorNotifier::OnAnnouncementChanged(_In_ DependencyObject^ dependencyObject, _In_ DependencyPropertyChangedEventArgs^ e) +{ + auto instance = safe_cast(dependencyObject); + if (instance != nullptr) + { + instance->Announce(safe_cast(e->NewValue)); + } +} diff --git a/src/CalcViewModel/Common/Automation/NarratorNotifier.h b/src/CalcViewModel/Common/Automation/NarratorNotifier.h new file mode 100644 index 00000000..c12e2277 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NarratorNotifier.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Declaration of the NarratorNotifier class. + +#pragma once +#include "INarratorAnnouncementHost.h" + +namespace CalculatorApp::Common::Automation +{ + public ref class NarratorNotifier sealed : public Windows::UI::Xaml::DependencyObject + { + public: + NarratorNotifier(); + + void Announce(NarratorAnnouncement^ announcement); + + property NarratorAnnouncement^ Announcement + { + NarratorAnnouncement^ get() { return GetAnnouncement(this); } + void set(NarratorAnnouncement^ value) + { + SetAnnouncement(this, value); + } + } + + static void RegisterDependencyProperties(); + + static property Windows::UI::Xaml::DependencyProperty^ AnnouncementProperty + { + Windows::UI::Xaml::DependencyProperty^ get() + { + return s_announcementProperty; + } + } + + static NarratorAnnouncement^ GetAnnouncement(Windows::UI::Xaml::DependencyObject^ element) + { + return safe_cast(element->GetValue(s_announcementProperty)); + } + + static void SetAnnouncement(Windows::UI::Xaml::DependencyObject^ element, NarratorAnnouncement^ value) + { + element->SetValue(s_announcementProperty, value); + } + + private: + static void OnAnnouncementChanged( + _In_ Windows::UI::Xaml::DependencyObject^ dependencyObject, + _In_ Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ eventArgs); + + static Windows::UI::Xaml::DependencyProperty^ s_announcementProperty; + + private: + INarratorAnnouncementHost^ m_announcementHost; + }; +} diff --git a/src/CalcViewModel/Common/Automation/NotificationHost.cpp b/src/CalcViewModel/Common/Automation/NotificationHost.cpp new file mode 100644 index 00000000..77ddddb1 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NotificationHost.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NotificationHost.h" + +using namespace CalculatorApp::Common::Automation; +using namespace Windows::Foundation::Metadata; +using namespace Windows::UI::Xaml::Automation; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::UI::Xaml::Controls; + +NotificationHost::NotificationHost() : + m_host(nullptr) +{} + +bool NotificationHost::IsHostAvailable() +{ + return ApiInformation::IsMethodPresent( + L"Windows.UI.Xaml.Automation.Peers.AutomationPeer", + L"RaiseNotificationEvent"); +} + +INarratorAnnouncementHost^ NotificationHost::MakeHost() +{ + return ref new NotificationHost(); +} + +void NotificationHost::Announce(NarratorAnnouncement^ announcement) +{ + if (m_host == nullptr) + { + m_host = ref new TextBlock(); + } + + auto peer = FrameworkElementAutomationPeer::FromElement(m_host); + if (peer != nullptr) + { + peer->RaiseNotificationEvent( + GetWindowsNotificationKind(announcement->Kind), + GetWindowsNotificationProcessing(announcement->Processing), + announcement->Announcement, + announcement->ActivityId); + } +} + +StandardPeers::AutomationNotificationKind NotificationHost::GetWindowsNotificationKind( + CustomPeers::AutomationNotificationKind customKindType) +{ + switch (customKindType) + { + case CustomPeers::AutomationNotificationKind::ItemAdded: + return StandardPeers::AutomationNotificationKind::ItemAdded; + + case CustomPeers::AutomationNotificationKind::ItemRemoved: + return StandardPeers::AutomationNotificationKind::ItemRemoved; + + case CustomPeers::AutomationNotificationKind::ActionCompleted: + return StandardPeers::AutomationNotificationKind::ActionCompleted; + + case CustomPeers::AutomationNotificationKind::ActionAborted: + return StandardPeers::AutomationNotificationKind::ActionAborted; + + case CustomPeers::AutomationNotificationKind::Other: + return StandardPeers::AutomationNotificationKind::Other; + + default: + assert(false && L"Unexpected AutomationNotificationKind"); + } + + return StandardPeers::AutomationNotificationKind::Other; +} + +StandardPeers::AutomationNotificationProcessing NotificationHost::GetWindowsNotificationProcessing( + CustomPeers::AutomationNotificationProcessing customProcessingType) +{ + switch (customProcessingType) + { + case CustomPeers::AutomationNotificationProcessing::ImportantAll: + return StandardPeers::AutomationNotificationProcessing::ImportantAll; + + case CustomPeers::AutomationNotificationProcessing::ImportantMostRecent: + return StandardPeers::AutomationNotificationProcessing::ImportantMostRecent; + + case CustomPeers::AutomationNotificationProcessing::All: + return StandardPeers::AutomationNotificationProcessing::All; + + case CustomPeers::AutomationNotificationProcessing::MostRecent: + return StandardPeers::AutomationNotificationProcessing::MostRecent; + + case CustomPeers::AutomationNotificationProcessing::CurrentThenMostRecent: + return StandardPeers::AutomationNotificationProcessing::CurrentThenMostRecent; + + default: + assert(false && L"Unexpected AutomationNotificationProcessing"); + } + + return StandardPeers::AutomationNotificationProcessing::ImportantMostRecent; +} diff --git a/src/CalcViewModel/Common/Automation/NotificationHost.h b/src/CalcViewModel/Common/Automation/NotificationHost.h new file mode 100644 index 00000000..7bcd1e24 --- /dev/null +++ b/src/CalcViewModel/Common/Automation/NotificationHost.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "INarratorAnnouncementHost.h" + +// Declaration of the NotificationHost class. +// This class announces NarratorAnnouncements using the RaiseNotification API +// available in RS3. + +namespace CalculatorApp::Common::Automation +{ + public ref class NotificationHost sealed : public INarratorAnnouncementHost + { + public: + NotificationHost(); + + virtual bool IsHostAvailable(); + virtual INarratorAnnouncementHost^ MakeHost(); + + virtual void Announce(NarratorAnnouncement^ announcement); + + private: + static Windows::UI::Xaml::Automation::Peers::AutomationNotificationKind GetWindowsNotificationKind( + CalculatorApp::Common::Automation::AutomationNotificationKind customKindType); + + static Windows::UI::Xaml::Automation::Peers::AutomationNotificationProcessing GetWindowsNotificationProcessing( + CalculatorApp::Common::Automation::AutomationNotificationProcessing customProcessingType); + + private: + Windows::UI::Xaml::UIElement^ m_host; + }; +} + diff --git a/src/CalcViewModel/Common/BindableBase.cpp b/src/CalcViewModel/Common/BindableBase.cpp new file mode 100644 index 00000000..270e68ac --- /dev/null +++ b/src/CalcViewModel/Common/BindableBase.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "BindableBase.h" + +using namespace CalculatorApp::Common; + +using namespace Platform; +using namespace Windows::UI::Xaml::Data; + +/// +/// Notifies listeners that a property value has changed. +/// +/// Name of the property used to notify listeners. +void BindableBase::OnPropertyChanged(String^ propertyName) +{ + PropertyChanged(this, ref new PropertyChangedEventArgs(propertyName)); +} + +Windows::UI::Xaml::Data::ICustomProperty^ BindableBase::GetCustomProperty(Platform::String^ name) +{ + return nullptr; +} + +Windows::UI::Xaml::Data::ICustomProperty^ BindableBase::GetIndexedProperty(Platform::String^ name, Windows::UI::Xaml::Interop::TypeName type) +{ + return nullptr; +} + +Platform::String^ BindableBase::GetStringRepresentation() +{ + return this->ToString(); +} diff --git a/src/CalcViewModel/Common/BindableBase.h b/src/CalcViewModel/Common/BindableBase.h new file mode 100644 index 00000000..b6a9f32e --- /dev/null +++ b/src/CalcViewModel/Common/BindableBase.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + /// + /// Implementation of to simplify models. + /// + [Windows::Foundation::Metadata::WebHostHidden] + public ref class BindableBase : Windows::UI::Xaml::DependencyObject, Windows::UI::Xaml::Data::INotifyPropertyChanged, Windows::UI::Xaml::Data::ICustomPropertyProvider + { + public: + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; + + public: + // ICustomPropertyProvider + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetCustomProperty(Platform::String^ name); + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetIndexedProperty(Platform::String^ name, Windows::UI::Xaml::Interop::TypeName type); + virtual Platform::String^ GetStringRepresentation(); + + property Windows::UI::Xaml::Interop::TypeName Type + { + virtual Windows::UI::Xaml::Interop::TypeName get() { return this->GetType(); } + } + + + protected: + virtual void OnPropertyChanged(Platform::String^ propertyName); + }; + } +} diff --git a/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.cpp b/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.cpp new file mode 100644 index 00000000..4dfd4bce --- /dev/null +++ b/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.cpp @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CalculatorButtonPressedEventArgs.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace Platform; + +NumbersAndOperatorsEnum CalculatorButtonPressedEventArgs::GetOperationFromCommandParameter(_In_ Object^ commandParameter) +{ + auto eventArgs = dynamic_cast(commandParameter); + if (eventArgs != nullptr) + { + return eventArgs->Operation; + } + else + { + return safe_cast(commandParameter); + } +} + +String^ CalculatorButtonPressedEventArgs::GetAuditoryFeedbackFromCommandParameter(_In_ Object^ commandParameter) +{ + auto eventArgs = dynamic_cast(commandParameter); + if (eventArgs != nullptr) + { + return eventArgs->AuditoryFeedback; + } + else + { + return nullptr; + } +} + diff --git a/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.h b/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.h new file mode 100644 index 00000000..b197479e --- /dev/null +++ b/src/CalcViewModel/Common/CalculatorButtonPressedEventArgs.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + public ref class CalculatorButtonPressedEventArgs sealed + { + public: + PROPERTY_R(Platform::String^, AuditoryFeedback); + PROPERTY_R(CalculatorApp::NumbersAndOperatorsEnum, Operation); + + CalculatorButtonPressedEventArgs( + Platform::String^ feedback, CalculatorApp::NumbersAndOperatorsEnum operation) : + m_AuditoryFeedback(feedback), m_Operation(operation) {} + + static CalculatorApp::NumbersAndOperatorsEnum GetOperationFromCommandParameter(_In_ Platform::Object^ commandParameter); + static Platform::String^ GetAuditoryFeedbackFromCommandParameter(_In_ Platform::Object^ commandParameter); + }; + } +} diff --git a/src/CalcViewModel/Common/CalculatorButtonUser.h b/src/CalcViewModel/Common/CalculatorButtonUser.h new file mode 100644 index 00000000..75b2c3f6 --- /dev/null +++ b/src/CalcViewModel/Common/CalculatorButtonUser.h @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace CM = CalculationManager; + + public enum class NumbersAndOperatorsEnum + { + Zero = (int) CM::Command::Command0, + One = (int) CM::Command::Command1, + Two = (int) CM::Command::Command2, + Three = (int) CM::Command::Command3, + Four = (int) CM::Command::Command4, + Five = (int) CM::Command::Command5, + Six = (int) CM::Command::Command6, + Seven = (int) CM::Command::Command7, + Eight = (int) CM::Command::Command8, + Nine = (int) CM::Command::Command9, + Add = (int) CM::Command::CommandADD, + Subtract = (int) CM::Command::CommandSUB, + Multiply = (int) CM::Command::CommandMUL, + Divide = (int) CM::Command::CommandDIV, + Invert = (int) CM::Command::CommandREC, + Equals = (int) CM::Command::CommandEQU, + Decimal = (int) CM::Command::CommandPNT, + Sqrt = (int) CM::Command::CommandSQRT, + Percent = (int) CM::Command::CommandPERCENT, + Negate = (int) CM::Command::CommandSIGN, + Backspace = (int) CM::Command::CommandBACK, + ClearEntry = (int) CM::Command::CommandCENTR, + Clear = (int) CM::Command::CommandCLEAR, + Degree = (int) CM::Command::CommandDEG, + Radians = (int) CM::Command::CommandRAD, + Grads = (int) CM::Command::CommandGRAD, + Degrees = (int) CM::Command::CommandDegrees, + OpenParenthesis = (int) CM::Command::CommandOPENP, + CloseParenthesis = (int) CM::Command::CommandCLOSEP, + Pi = (int) CM::Command::CommandPI, + Sin = (int) CM::Command::CommandSIN, + Cos = (int) CM::Command::CommandCOS, + Tan = (int) CM::Command::CommandTAN, + Factorial = (int) CM::Command::CommandFAC, + XPower2 = (int) CM::Command::CommandSQR, + Mod = (int) CM::Command::CommandMOD, + FToE = (int) CM::Command::CommandFE, + LogBaseE = (int) CM::Command::CommandLN, + InvSin = (int) CM::Command::CommandASIN, + InvCos = (int) CM::Command::CommandACOS, + InvTan = (int) CM::Command::CommandATAN, + LogBase10 = (int) CM::Command::CommandLOG, + XPowerY = (int) CM::Command::CommandPWR, + YRootX = (int) CM::Command::CommandROOT, + TenPowerX = (int) CM::Command::CommandPOW10, + EPowerX = (int) CM::Command::CommandPOWE, + Exp = (int) CM::Command::CommandEXP, + IsScientificMode = (int) CM::Command::ModeScientific, + IsStandardMode = (int) CM::Command::ModeBasic, + None = (int) CM::Command::CommandNULL, + IsProgrammerMode = (int) CM::Command::ModeProgrammer, + DecButton = (int) CM::Command::CommandDec, + OctButton = (int) CM::Command::CommandOct, + HexButton = (int) CM::Command::CommandHex, + BinButton = (int) CM::Command::CommandBin, + And = (int) CM::Command::CommandAnd, + Ror = (int) CM::Command::CommandROR, + Rol = (int) CM::Command::CommandROL, + Or = (int) CM::Command::CommandOR, + Lsh = (int) CM::Command::CommandLSHF, + Rsh = (int) CM::Command::CommandRSHF, + Xor = (int) CM::Command::CommandXor, + Not = (int) CM::Command::CommandNot, + A = (int) CM::Command::CommandA, + B = (int) CM::Command::CommandB, + C = (int) CM::Command::CommandC, + D = (int) CM::Command::CommandD, + E = (int) CM::Command::CommandE, + F = (int) CM::Command::CommandF, + Memory, // This is the memory button. Doesn't have a direct mapping to the CalcEngine. + Sinh = (int) CM::Command::CommandSINH, + Cosh = (int) CM::Command::CommandCOSH, + Tanh = (int) CM::Command::CommandTANH, + InvSinh = (int) CM::Command::CommandASINH, + InvCosh = (int) CM::Command::CommandACOSH, + InvTanh = (int) CM::Command::CommandATANH, + Qword = (int) CM::Command::CommandQword, + Dword = (int) CM::Command::CommandDword, + Word = (int) CM::Command::CommandWord, + Byte = (int) CM::Command::CommandByte, + Cube = (int) CM::Command::CommandCUB, + DMS = (int) CM::Command::CommandDMS, + + BINSTART = (int) CM::Command::CommandBINEDITSTART, + BINPOS0 = (int) CM::Command::CommandBINPOS0, + BINPOS1 = (int) CM::Command::CommandBINPOS1, + BINPOS2 = (int) CM::Command::CommandBINPOS2, + BINPOS3 = (int) CM::Command::CommandBINPOS3, + BINPOS4 = (int) CM::Command::CommandBINPOS4, + BINPOS5 = (int) CM::Command::CommandBINPOS5, + BINPOS6 = (int) CM::Command::CommandBINPOS6, + BINPOS7 = (int) CM::Command::CommandBINPOS7, + BINPOS8 = (int) CM::Command::CommandBINPOS8, + BINPOS9 = (int) CM::Command::CommandBINPOS9, + BINPOS10 = (int) CM::Command::CommandBINPOS10, + BINPOS11 = (int) CM::Command::CommandBINPOS11, + BINPOS12 = (int) CM::Command::CommandBINPOS12, + BINPOS13 = (int) CM::Command::CommandBINPOS13, + BINPOS14 = (int) CM::Command::CommandBINPOS14, + BINPOS15 = (int) CM::Command::CommandBINPOS15, + BINPOS16 = (int) CM::Command::CommandBINPOS16, + BINPOS17 = (int) CM::Command::CommandBINPOS17, + BINPOS18 = (int) CM::Command::CommandBINPOS18, + BINPOS19 = (int) CM::Command::CommandBINPOS19, + BINPOS20 = (int) CM::Command::CommandBINPOS20, + BINPOS21 = (int) CM::Command::CommandBINPOS21, + BINPOS22 = (int) CM::Command::CommandBINPOS22, + BINPOS23 = (int) CM::Command::CommandBINPOS23, + BINPOS24 = (int) CM::Command::CommandBINPOS24, + BINPOS25 = (int) CM::Command::CommandBINPOS25, + BINPOS26 = (int) CM::Command::CommandBINPOS26, + BINPOS27 = (int) CM::Command::CommandBINPOS27, + BINPOS28 = (int) CM::Command::CommandBINPOS28, + BINPOS29 = (int) CM::Command::CommandBINPOS29, + BINPOS30 = (int) CM::Command::CommandBINPOS30, + BINPOS31 = (int) CM::Command::CommandBINPOS31, + BINPOS32 = (int) CM::Command::CommandBINPOS32, + BINPOS33 = (int) CM::Command::CommandBINPOS33, + BINPOS34 = (int) CM::Command::CommandBINPOS34, + BINPOS35 = (int) CM::Command::CommandBINPOS35, + BINPOS36 = (int) CM::Command::CommandBINPOS36, + BINPOS37 = (int) CM::Command::CommandBINPOS37, + BINPOS38 = (int) CM::Command::CommandBINPOS38, + BINPOS39 = (int) CM::Command::CommandBINPOS39, + BINPOS40 = (int) CM::Command::CommandBINPOS40, + BINPOS41 = (int) CM::Command::CommandBINPOS41, + BINPOS42 = (int) CM::Command::CommandBINPOS42, + BINPOS43 = (int) CM::Command::CommandBINPOS43, + BINPOS44 = (int) CM::Command::CommandBINPOS44, + BINPOS45 = (int) CM::Command::CommandBINPOS45, + BINPOS46 = (int) CM::Command::CommandBINPOS46, + BINPOS47 = (int) CM::Command::CommandBINPOS47, + BINPOS48 = (int) CM::Command::CommandBINPOS48, + BINPOS49 = (int) CM::Command::CommandBINPOS49, + BINPOS50 = (int) CM::Command::CommandBINPOS50, + BINPOS51 = (int) CM::Command::CommandBINPOS51, + BINPOS52 = (int) CM::Command::CommandBINPOS52, + BINPOS53 = (int) CM::Command::CommandBINPOS53, + BINPOS54 = (int) CM::Command::CommandBINPOS54, + BINPOS55 = (int) CM::Command::CommandBINPOS55, + BINPOS56 = (int) CM::Command::CommandBINPOS56, + BINPOS57 = (int) CM::Command::CommandBINPOS57, + BINPOS58 = (int) CM::Command::CommandBINPOS58, + BINPOS59 = (int) CM::Command::CommandBINPOS59, + BINPOS60 = (int) CM::Command::CommandBINPOS60, + BINPOS61 = (int) CM::Command::CommandBINPOS61, + BINPOS62 = (int) CM::Command::CommandBINPOS62, + BINPOS63 = (int) CM::Command::CommandBINPOS63, + BINEND = (int) CM::Command::CommandBINEDITEND, + Hyp = (int) CM::Command::CommandHYP + }; + + // This contains list of functions whose usage we are tracelogging + public enum class FunctionLogEnum + { + Invert = (int) CM::Command::CommandREC, + Sqrt = (int) CM::Command::CommandSQRT, + Percent = (int) CM::Command::CommandPERCENT, + Negate = (int) CM::Command::CommandSIGN, + Degrees = (int) CM::Command::CommandDegrees, + Pi = (int) CM::Command::CommandPI, + Sin = (int) CM::Command::CommandSIN, + Cos = (int) CM::Command::CommandCOS, + Tan = (int) CM::Command::CommandTAN, + Factorial = (int) CM::Command::CommandFAC, + XPower2 = (int) CM::Command::CommandSQR, + Mod = (int) CM::Command::CommandMOD, + FToE = (int) CM::Command::CommandFE, + LogBaseE = (int) CM::Command::CommandLN, + InvSin = (int) CM::Command::CommandASIN, + InvCos = (int) CM::Command::CommandACOS, + InvTan = (int) CM::Command::CommandATAN, + LogBase10 = (int) CM::Command::CommandLOG, + XPowerY = (int) CM::Command::CommandPWR, + YRootX = (int) CM::Command::CommandROOT, + TenPowerX = (int) CM::Command::CommandPOW10, + EPowerX = (int) CM::Command::CommandPOWE, + Exp = (int) CM::Command::CommandEXP, + DecButton = (int) CM::Command::CommandDec, + OctButton = (int) CM::Command::CommandOct, + HexButton = (int) CM::Command::CommandHex, + BinButton = (int) CM::Command::CommandBin, + And = (int) CM::Command::CommandAnd, + Ror = (int) CM::Command::CommandROR, + Rol = (int) CM::Command::CommandROL, + Or = (int) CM::Command::CommandOR, + Lsh = (int) CM::Command::CommandLSHF, + Rsh = (int) CM::Command::CommandRSHF, + Xor = (int) CM::Command::CommandXor, + Not = (int) CM::Command::CommandNot, + Sinh = (int) CM::Command::CommandSINH, + Cosh = (int) CM::Command::CommandCOSH, + Tanh = (int) CM::Command::CommandTANH, + InvSinh = (int) CM::Command::CommandASINH, + InvCosh = (int) CM::Command::CommandACOSH, + InvTanh = (int) CM::Command::CommandATANH, + Cube = (int) CM::Command::CommandCUB, + DMS = (int) CM::Command::CommandDMS, + }; +} diff --git a/src/CalcViewModel/Common/CalculatorDisplay.cpp b/src/CalcViewModel/Common/CalculatorDisplay.cpp new file mode 100644 index 00000000..6001b647 --- /dev/null +++ b/src/CalcViewModel/Common/CalculatorDisplay.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This class provides the concrete implemenation for the ICalcDisplay interface +// that is declared in the Calculation Manager Library. +#include "pch.h" +#include "CalculatorDisplay.h" +#include "StandardCalculatorViewModel.h" + +using namespace CalculatorApp; +using namespace CalculationManager; +using namespace std; + +CalculatorDisplay::CalculatorDisplay() +{ +} + +void CalculatorDisplay::SetCallback(Platform::WeakReference callbackReference) +{ + m_callbackReference = callbackReference; +} + +void CalculatorDisplay::SetHistoryCallback(Platform::WeakReference callbackReference) +{ + m_historyCallbackReference = callbackReference; +} + +void CalculatorDisplay::SetPrimaryDisplay(_In_ const wstring& displayStringValue, _In_ bool isError) +{ + if (m_callbackReference) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->SetPrimaryDisplay(displayStringValue, isError); + } + } +} + +void CalculatorDisplay::SetParenDisplayText(_In_ const std::wstring& parenthesisCount) +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->SetParenthesisCount(parenthesisCount); + } + } +} + +void CalculatorDisplay::SetIsInError(bool isError) +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->IsInError = isError; + } + } +} + +void CalculatorDisplay::SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands) +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->SetExpressionDisplay(tokens, commands); + } + } +} + +void CalculatorDisplay::SetMemorizedNumbers(_In_ const vector& newMemorizedNumbers) +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->SetMemorizedNumbers(newMemorizedNumbers); + } + } +} + +void CalculatorDisplay::OnHistoryItemAdded(_In_ unsigned int addedItemIndex) +{ + if (m_historyCallbackReference != nullptr) + { + auto historyVM = m_historyCallbackReference.Resolve(); + if (historyVM) + { + historyVM->OnHistoryItemAdded(addedItemIndex); + } + } +} + +void CalculatorDisplay::MaxDigitsReached() +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->OnMaxDigitsReached(); + } + } +} + +void CalculatorDisplay::BinaryOperatorReceived() +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->OnBinaryOperatorReceived(); + } + } +} + +void CalculatorDisplay::MemoryItemChanged(unsigned int indexOfMemory) +{ + if (m_callbackReference != nullptr) + { + auto calcVM = m_callbackReference.Resolve(); + if (calcVM) + { + calcVM->OnMemoryItemChanged(indexOfMemory); + } + } +} diff --git a/src/CalcViewModel/Common/CalculatorDisplay.h b/src/CalcViewModel/Common/CalculatorDisplay.h new file mode 100644 index 00000000..39b5a9c7 --- /dev/null +++ b/src/CalcViewModel/Common/CalculatorDisplay.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + // Callback interface to be implemented by the CalculatorManager + class CalculatorDisplay: public ICalcDisplay + { + public: + CalculatorDisplay(); + void SetCallback(Platform::WeakReference callbackReference); + void SetHistoryCallback(Platform::WeakReference callbackReference); + + private: + void SetPrimaryDisplay(_In_ const std::wstring& displayString, _In_ bool isError) override; + void SetIsInError(bool isError) override; + void SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands) override; + void SetMemorizedNumbers(_In_ const std::vector& memorizedNumbers) override; + void OnHistoryItemAdded(_In_ unsigned int addedItemIndex) override; + void SetParenDisplayText(_In_ const std::wstring& parenthesisCount) override; + void MaxDigitsReached() override; + void BinaryOperatorReceived() override; + void MemoryItemChanged(unsigned int indexOfMemory) override; + + private: + Platform::WeakReference m_callbackReference; + Platform::WeakReference m_historyCallbackReference; + }; +} diff --git a/src/CalcViewModel/Common/ConversionResultTaskHelper.cpp b/src/CalcViewModel/Common/ConversionResultTaskHelper.cpp new file mode 100644 index 00000000..db943c5c --- /dev/null +++ b/src/CalcViewModel/Common/ConversionResultTaskHelper.cpp @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "ConversionResultTaskHelper.h" + +using namespace CalculatorApp::Common; +using namespace concurrency; +using namespace std; + +ConversionResultTaskHelper::ConversionResultTaskHelper(unsigned int delay, const function functionToRun) : + m_delay{ delay }, + m_storedFunction{ functionToRun } +{ + auto token = m_cts.get_token(); + auto delayTask = CompleteAfter(delay); + delayTask.then([this, token]() + { + if (!token.is_canceled()) + { + m_storedFunction(); + } + }, task_continuation_context::use_current()); +} + +ConversionResultTaskHelper::~ConversionResultTaskHelper() +{ + m_cts.cancel(); +} + +#pragma optimize("", off) +// 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 +task ConversionResultTaskHelper::CompleteAfter(unsigned int timeout) +{ + co_await winrt::resume_after(winrt::Windows::Foundation::TimeSpan{ std::chrono::duration(timeout) }); +}; +#pragma optimize("", on) diff --git a/src/CalcViewModel/Common/ConversionResultTaskHelper.h b/src/CalcViewModel/Common/ConversionResultTaskHelper.h new file mode 100644 index 00000000..a89f3f14 --- /dev/null +++ b/src/CalcViewModel/Common/ConversionResultTaskHelper.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + class ConversionResultTaskHelper + { + public: + ConversionResultTaskHelper(unsigned int delay, const std::function functionToRun); + ~ConversionResultTaskHelper(); + + private: + concurrency::task CompleteAfter(unsigned int timeout); + + unsigned int m_delay; + concurrency::cancellation_token_source m_cts; + const std::function m_storedFunction; + }; + } +} diff --git a/src/CalcViewModel/Common/CopyPasteManager.cpp b/src/CalcViewModel/Common/CopyPasteManager.cpp new file mode 100644 index 00000000..0695ec5d --- /dev/null +++ b/src/CalcViewModel/Common/CopyPasteManager.cpp @@ -0,0 +1,569 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CopyPasteManager.h" +#include "Common\LocalizationSettings.h" + +using namespace std; +using namespace concurrency; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::System; +using namespace Windows::ApplicationModel::DataTransfer; + +size_t maxOperandLength; +unsigned long long maxOperandNumber; + +String^ CopyPasteManager::supportedFormats[] = +{ + StandardDataFormats::Text +}; + +constexpr wstring_view c_validCharacterSet{ L"0123456789()+-*/.abcdefABCDEF" }; +// [\s\x85] means white-space characters +static const wstring c_wspc = L"[\\s\\x85]*"; +static const wstring c_wspcLParens = c_wspc + L"[(]*" + c_wspc; +static const wstring c_wspcRParens = c_wspc + L"[)]*" + c_wspc; +static const wstring c_signedDecFloat = L"[-+]?\\d*(\\d|[.])\\d*"; + +// Programmer Mode Integer patterns +// Support digit separators ` (WinDbg/MASM), ' (C++), and _ (C# and other languages) +static const wstring c_hexProgrammerChars = L"([a-f]|[A-F]|\\d)+((_|'|`)([a-f]|[A-F]|\\d)+)*"; +static const wstring c_decProgrammerChars = L"\\d+((_|'|`)\\d+)*"; +static const wstring c_octProgrammerChars = L"[0-7]+((_|'|`)[0-7]+)*"; +static const wstring c_binProgrammerChars = L"[0-1]+((_|'|`)[0-1]+)*"; +static const wstring c_uIntSuffixes = L"[uU]?[lL]{0,2}"; + +// RegEx Patterns used by various modes +static const array standardModePatterns = +{ + wregex(c_wspc + c_signedDecFloat + c_wspc) +}; +static const array scientificModePatterns = +{ + wregex(c_wspcLParens + c_signedDecFloat + c_wspcRParens), + wregex(c_wspcLParens + c_signedDecFloat + L"[e]([+]|[-])+\\d+" + c_wspcRParens) +}; +static const array, 4> programmerModePatterns = +{ { + // Hex numbers like 5F, 4A0C, 0xa9, 0xFFull, 47CDh + { + wregex(c_wspcLParens + L"(0[xX])?" + c_hexProgrammerChars + c_uIntSuffixes + c_wspcRParens), + wregex(c_wspcLParens + c_hexProgrammerChars + L"[hH]?" + c_wspcRParens) + }, + // Decimal numbers like -145, 145, 0n145, 123ull etc + { + wregex(c_wspcLParens + L"[-+]?" + c_decProgrammerChars + L"[lL]{0,2}" +c_wspcRParens), + wregex(c_wspcLParens + L"(0[nN])?" + c_decProgrammerChars + c_uIntSuffixes + c_wspcRParens) + }, + // Octal numbers like 06, 010, 0t77, 0o77, 077ull etc + { + wregex(c_wspcLParens + L"(0[otOT])?" + c_octProgrammerChars + c_uIntSuffixes + c_wspcRParens) + }, + // Binary numbers like 011010110, 0010110, 10101001, 1001b, 0b1001, 0y1001, 0b1001ull + { + wregex(c_wspcLParens + L"(0[byBY])?" + c_binProgrammerChars + c_uIntSuffixes + c_wspcRParens), + wregex(c_wspcLParens + c_binProgrammerChars + L"[bB]?" + c_wspcRParens) + } + } }; +static const array unitConverterPatterns = +{ + wregex(c_wspc + L"[-+]?\\d*[.]?\\d*" + c_wspc) +}; + +void CopyPasteManager::CopyToClipboard(String^ stringToCopy) +{ + // Copy the string to the clipboard + auto dataPackage = ref new DataPackage(); + dataPackage->SetText(stringToCopy); + Clipboard::SetContent(dataPackage); +} + +task CopyPasteManager::GetStringToPaste(ViewMode mode, CategoryGroupType modeType, int programmerNumberBase, int bitLengthType) +{ + // Retrieve the text in the clipboard + auto dataPackageView = Clipboard::GetContent(); + + // TODO: Suport all formats supported by ClipboardHasText + //-- add support to avoid pasting of expressions like 12 34(as of now we allow 1234) + //-- add support to allow pasting for expressions like .2 , -.2 + //-- add support to allow pasting for expressions like 1.3e12(as of now we allow 1.3e+12) + + return create_task((dataPackageView->GetTextAsync(::StandardDataFormats::Text))) + .then([mode, modeType, programmerNumberBase, bitLengthType](String^ pastedText) + { + return ValidatePasteExpression(pastedText, mode, modeType, programmerNumberBase, bitLengthType); + } + , task_continuation_context::use_arbitrary()); +} + +int CopyPasteManager::ClipboardTextFormat() +{ + int result = -1; + + auto dataPackageView = Clipboard::GetContent(); + + for (int i = 0; i < RTL_NUMBER_OF(supportedFormats); i++) + { + if (dataPackageView->Contains(supportedFormats[i])) + { + result = i; + break; + } + } + return result; +} + +String^ CopyPasteManager::ValidatePasteExpression(String^ pastedText, ViewMode mode, int programmerNumberBase, int bitLengthType) +{ + return CopyPasteManager::ValidatePasteExpression(pastedText, mode, NavCategory::GetGroupType(mode), programmerNumberBase, bitLengthType); +} + +// return "NoOp" if pastedText is invalid else return pastedText + +String^ CopyPasteManager::ValidatePasteExpression(String^ pastedText, ViewMode mode, CategoryGroupType modeType, int programmerNumberBase, int bitLengthType) +{ + if (pastedText->Length() > MaxPasteableLength) + { + // return NoOp to indicate don't paste anything. + TraceLogger::GetInstance().LogInvalidInputPasted(L"PastedExpressionSizeGreaterThanMaxAllowed", L"MoreThanMaxInput", mode, programmerNumberBase, bitLengthType); + return StringReference(PasteErrorString); + } + + wstring pasteExpression = pastedText->Data(); + + // Get english translated expression + String^ englishString = LocalizationSettings::GetInstance().GetEnglishValueFromLocalizedDigits(pasteExpression); + + // Removing the spaces, comma separator from the pasteExpression to allow pasting of expressions like 1 + 2+1,333 + pasteExpression = Utils::RemoveUnwantedCharsFromWstring(englishString->Data()); + + // If the last character is an = sign, remove it from the pasteExpression to allow evaluating the result on paste. + if (!pasteExpression.empty() && pasteExpression.back() == L'=') + { + pasteExpression = pasteExpression.substr(0, pasteExpression.length() - 1); + } + + // Extract operands from the expression to make regex comparison easy and quick. For whole expression it was taking too much of time. + // Operands vector will have the list of operands in the pasteExpression + vector operands = ExtractOperands(pasteExpression, mode, programmerNumberBase, bitLengthType); + if (operands.empty()) + { + // return NoOp to indicate don't paste anything. + return StringReference(PasteErrorString); + } + + if (modeType == CategoryGroupType::Converter) + { + operands = { pasteExpression }; + } + + // validate each operand with patterns for different modes + if (!ExpressionRegExMatch(operands, mode, modeType, programmerNumberBase, bitLengthType)) + { + TraceLogger::GetInstance().LogInvalidInputPasted(L"InvalidExpressionForPresentMode", pastedText->Data(), mode, programmerNumberBase, bitLengthType); + return StringReference(PasteErrorString); + } + + return ref new String(pastedText->Data()); +} + +vector CopyPasteManager::ExtractOperands(const wstring& pasteExpression, ViewMode mode, int programmerNumberBase, int bitLengthType) +{ + vector operands{}; + size_t lastIndex = 0; + bool haveOperator = false; + bool startExpCounting = false; + bool startOfExpression = true; + bool isPreviousOpenParen = false; + bool isPreviousOperator = false; + + // This will have the exponent length + size_t expLength = 0; + for (size_t i = 0; i < pasteExpression.length(); i++) + { + // if the current character is not a valid one don't process it + if (c_validCharacterSet.find(pasteExpression.at(i)) == wstring_view::npos) + { + continue; + } + + if (operands.size() >= MaxOperandCount) + { + TraceLogger::GetInstance().LogInvalidInputPasted(L"OperandCountGreaterThanMaxCount", pasteExpression.c_str(), mode, programmerNumberBase, bitLengthType); + operands.clear(); + return operands; + } + + if (startExpCounting) + { + if ((pasteExpression.at(i) >= L'0') && (pasteExpression.at(i) <= L'9')) + { + expLength++; + + // to disallow pasting of 1e+12345 as 1e+1234, max exponent that can be pasted is 9999. + if (expLength > MaxExponentLength) + { + TraceLogger::GetInstance().LogInvalidInputPasted(L"ExponentLengthGreaterThanMaxLength", pasteExpression.c_str(), mode, programmerNumberBase, bitLengthType); + operands.clear(); + return operands; + } + } + } + + if ((mode != ViewMode::Programmer) && (pasteExpression.at(i) == L'e')) + { + startExpCounting = true; + } + + if (((pasteExpression.at(i) == L'+') || (pasteExpression.at(i) == L'-') || (pasteExpression.at(i) == L'*') || (pasteExpression.at(i) == L'/'))) + { + if ((pasteExpression.at(i) == L'+') || (pasteExpression.at(i) == L'-')) + { + // don't break the expression into operands if the encountered character corresponds to sign command(+-) + if (isPreviousOpenParen || startOfExpression || isPreviousOperator || ((mode != ViewMode::Programmer) && !((i != 0) && (pasteExpression.at(i - 1) != L'e')))) + { + isPreviousOperator = false; + continue; + } + } + + startExpCounting = false; + expLength = 0; + haveOperator = true; + isPreviousOperator = true; + operands.push_back(pasteExpression.substr(lastIndex, i - lastIndex)); + lastIndex = i + 1; + } + else + { + isPreviousOperator = false; + } + + isPreviousOpenParen = (pasteExpression.at(i) == L'('); + startOfExpression = false; + } + + if (!haveOperator) + { + operands.clear(); + operands.push_back(pasteExpression); + } + else + { + operands.push_back(pasteExpression.substr(lastIndex, pasteExpression.length() - 1)); + } + + return operands; +} + +bool CopyPasteManager::ExpressionRegExMatch(vector operands, ViewMode mode, CategoryGroupType modeType, int programmerNumberBase, int bitLengthType) +{ + if (operands.empty()) + { + return false; + } + + bool expMatched = true; + vector patterns{}; + + pair operandLimits = GetMaxOperandLengthAndValue(mode, modeType, programmerNumberBase, bitLengthType); + size_t maxOperandLength = operandLimits.first; + uint64_t maxOperandValue = operandLimits.second; + + if (mode == ViewMode::Standard) + { + patterns.assign(standardModePatterns.begin(), standardModePatterns.end()); + } + else if (mode == ViewMode::Scientific) + { + patterns.assign(scientificModePatterns.begin(), scientificModePatterns.end()); + } + else if (mode == ViewMode::Programmer) + { + patterns.assign(programmerModePatterns[programmerNumberBase - HexBase].begin(), programmerModePatterns[programmerNumberBase - HexBase].end()); + } + else if (modeType == CategoryGroupType::Converter) + { + patterns.assign(unitConverterPatterns.begin(), unitConverterPatterns.end()); + } + + for (const wstring& operand : operands) + { + // Each operand only needs to match one of the available patterns. + bool operandMatched = false; + for (const wregex& pattern : patterns) + { + operandMatched = operandMatched || regex_match(operand, pattern); + } + + if (operandMatched) + { + // Remove characters that are valid in the expression but we do not want to include in length calculations + // or which will break conversion from string-to-ULL. + wstring operandValue = SanitizeOperand(operand); + + // If an operand exceeds the maximum length allowed, break and return. + if (OperandLength(operandValue, mode, modeType, programmerNumberBase) > maxOperandLength) + { + expMatched = false; + break; + } + + // If maxOperandValue is set and the operandValue exceeds it, break and return. + if (maxOperandValue != 0) + { + unsigned long long int operandAsULL = 0; + if (!TryOperandToULL(operandValue, programmerNumberBase, operandAsULL)) + { + // Operand was empty, received invalid_argument, or received out_of_range. Input is invalid. + expMatched = false; + break; + } + + if (operandAsULL > maxOperandValue) + { + expMatched = false; + break; + } + } + } + + expMatched = expMatched && operandMatched; + } + + return expMatched; +} + +pair CopyPasteManager::GetMaxOperandLengthAndValue(ViewMode mode, CategoryGroupType modeType, int programmerNumberBase, int bitLengthType) +{ + size_t maxLength = 0; + uint64_t maxValue = 0; + + if (mode == ViewMode::Standard) + { + maxLength = MaxStandardOperandLength; + } + else if (mode == ViewMode::Scientific) + { + maxLength = MaxScientificOperandLength; + } + else if (mode == ViewMode::Programmer) + { + unsigned int bitLength = 0; + switch (bitLengthType) + { + case QwordType: + bitLength = 64; + break; + case DwordType: + bitLength = 32; + break; + case WordType: + bitLength = 16; + break; + case ByteType: + bitLength = 8; + break; + } + + double bitsPerDigit = 0; + switch (programmerNumberBase) + { + case BinBase: + bitsPerDigit = log2(2); + break; + case OctBase: + bitsPerDigit = log2(8); + break; + case DecBase: + bitsPerDigit = log2(10); + break; + case HexBase: + bitsPerDigit = log2(16); + break; + } + + unsigned int signBit = (programmerNumberBase == DecBase) ? 1 : 0; + + maxLength = (size_t)ceil((bitLength - signBit) / bitsPerDigit); + maxValue = UINT64_MAX >> (MaxProgrammerBitLength - (bitLength - signBit)); + } + else if (modeType == CategoryGroupType::Converter) + { + maxLength = MaxConverterInputLength; + } + + return make_pair(maxLength, maxValue); +} + +wstring CopyPasteManager::SanitizeOperand(const wstring& operand) +{ + wchar_t unWantedChars[] = { L'\'', L'_', L'`', L'(', L')', L'-' }; + + return Utils::RemoveUnwantedCharsFromWstring(operand, unWantedChars, ARRAYSIZE(unWantedChars)); +} + +bool CopyPasteManager::TryOperandToULL(const wstring& operand, int numberBase, unsigned long long int& result) +{ + result = 0; + + if (operand.length() == 0 || operand.front() == L'-') + { + return false; + } + + // Default to base10 + int intBase = 10; + switch (numberBase) + { + case HexBase: + intBase = 16; + break; + case OctBase: + intBase = 8; + break; + case BinBase: + intBase = 2; + break; + case DecBase: + intBase = 10; + break; + } + + wstring::size_type size = 0; + try + { + result = stoull(operand, &size, intBase); + return true; + } + catch (invalid_argument) + { + // Do nothing + } + catch (out_of_range) + { + // Do nothing + } + + return false; +} + +size_t CopyPasteManager::OperandLength(wstring operand, ViewMode mode, CategoryGroupType modeType, int programmerNumberBase) +{ + size_t len = 0; + if (mode == ViewMode::Standard || mode == ViewMode::Scientific) + { + len = StandardScientificOperandLength(operand); + } + else if (mode == ViewMode::Programmer) + { + len = ProgrammerOperandLength(operand, programmerNumberBase); + } + else if (modeType == CategoryGroupType::Converter) + { + len = operand.length(); + } + + return len; +} + +size_t CopyPasteManager::StandardScientificOperandLength(wstring operand) +{ + bool hasDecimal = false; + for (size_t i = 0; i < operand.length(); i++) + { + if (operand[i] == L'.') + { + hasDecimal = true; + } + } + + if (hasDecimal) + { + if (operand.length() >= 2) + { + if ((operand[0] == L'0') && (operand[1] == L'.')) + { + return operand.length() - 2; + } + else + { + return operand.length() - 1; + } + } + } + + return operand.length(); +} + +size_t CopyPasteManager::ProgrammerOperandLength(const wstring& operand, int numberBase) +{ + size_t len = operand.length(); + + vector prefixes{}; + vector suffixes{}; + switch (numberBase) + { + case BinBase: + prefixes = { L"0B", L"0Y" }; + suffixes = { L"B" }; + break; + case DecBase: + prefixes = { L"-", L"0N" }; + break; + case OctBase: + prefixes = { L"0T", L"0O" }; + break; + case HexBase: + prefixes = { L"0X" }; + suffixes = { L"H" }; + break; + default: + // No defined prefixes/suffixes + break; + } + + // UInt suffixes are common across all modes + const array uintSuffixes = { L"ULL", L"UL", L"LL", L"U", L"L" }; + suffixes.insert(suffixes.end(), uintSuffixes.begin(), uintSuffixes.end()); + + wstring operandUpper = operand; + transform(operandUpper.begin(), operandUpper.end(), operandUpper.begin(), toupper); + + // Detect if there is a suffix and subtract its length + // Check suffixes first to allow e.g. "0b" to result in length 1 (value 0), rather than length 0 (no value). + for (const wstring& suffix : suffixes) + { + if (len < suffix.length()) + { + continue; + } + + if (operandUpper.compare(operandUpper.length() - suffix.length(), suffix.length(), suffix) == 0) + { + len -= suffix.length(); + break; + } + } + + // Detect if there is a prefix and subtract its length + for (const wstring& prefix : prefixes) + { + if (len < prefix.length()) + { + continue; + } + + if (operandUpper.compare(0, prefix.length(), prefix) == 0) + { + len -= prefix.length(); + break; + } + } + + return len; +} diff --git a/src/CalcViewModel/Common/CopyPasteManager.h b/src/CalcViewModel/Common/CopyPasteManager.h new file mode 100644 index 00000000..24caa812 --- /dev/null +++ b/src/CalcViewModel/Common/CopyPasteManager.h @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "AppResourceProvider.h" + +namespace CalculatorUnitTests +{ + class CopyPasteManagerTest; +} + +namespace CalculatorApp +{ + +#define QwordType 1 +#define DwordType 2 +#define WordType 3 +#define ByteType 4 +#define HexBase 5 +#define DecBase 6 +#define OctBase 7 +#define BinBase 8 + + class CopyPasteManager + { + public: + static void CopyToClipboard(Platform::String^ stringToCopy); + static concurrency::task GetStringToPaste(CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1, int bitLengthType = -1); + static bool HasStringToPaste() + { + return ClipboardTextFormat() >= 0; + } + + static constexpr auto PasteErrorString = L"NoOp"; + + private: + static int ClipboardTextFormat(); + static Platform::String^ ValidatePasteExpression( + Platform::String^ pastedText, + CalculatorApp::Common::ViewMode mode, + int programmerNumberBase, + int bitLengthType); + static Platform::String^ ValidatePasteExpression( + Platform::String^ pastedText, + CalculatorApp::Common::ViewMode mode, + CalculatorApp::Common::CategoryGroupType modeType, + int programmerNumberBase, + int bitLengthType); + + static std::vector ExtractOperands(const std::wstring& pasteExpression, CalculatorApp::Common::ViewMode mode, int programmerNumberBase = -1, int bitLengthType = -1); + static bool ExpressionRegExMatch(std::vector operands, CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1, int bitLengthType = -1); + + static std::pair GetMaxOperandLengthAndValue(CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1, int bitLengthType = -1); + static std::wstring SanitizeOperand(const std::wstring& operand); + static bool TryOperandToULL(const std::wstring& operand, int numberBase, unsigned long long int& result); + static size_t OperandLength(std::wstring operand, CalculatorApp::Common::ViewMode mode, CalculatorApp::Common::CategoryGroupType modeType, int programmerNumberBase = -1); + static size_t StandardScientificOperandLength(std::wstring operand); + static size_t ProgrammerOperandLength(const std::wstring& operand, int numberBase); + + static constexpr size_t MaxStandardOperandLength = 16; + static constexpr size_t MaxScientificOperandLength = 32; + static constexpr size_t MaxConverterInputLength = 16; + static constexpr size_t MaxOperandCount = 100; + static constexpr size_t MaxPasteableLength = 512; + static constexpr size_t MaxExponentLength = 4; + static constexpr size_t MaxProgrammerBitLength = 64; + + static Platform::String^ supportedFormats[]; + + friend class CalculatorUnitTests::CopyPasteManagerTest; + }; +} diff --git a/src/CalcViewModel/Common/DateCalculator.cpp b/src/CalcViewModel/Common/DateCalculator.cpp new file mode 100644 index 00000000..6b7eba88 --- /dev/null +++ b/src/CalcViewModel/Common/DateCalculator.cpp @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "DateCalculator.h" + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Globalization; +using namespace CalculatorApp::Common::DateCalculation; + +DateCalculationEngine::DateCalculationEngine(_In_ String^ calendarIdentifier) +{ + m_calendar = ref new Calendar(); + m_calendar->ChangeCalendarSystem(calendarIdentifier); +} + +// Adding Duration to a Date +// Returns: True if function succeeds to calculate the date else returns False +bool DateCalculationEngine::AddDuration(_In_ DateTime startDate, _In_ const DateDifference& duration, _Out_ DateTime *endDate) +{ + try + { + m_calendar->SetDateTime(startDate); + + if (duration.year != 0) + { + m_calendar->AddYears(duration.year); + } + if (duration.month != 0) + { + m_calendar->AddMonths(duration.month); + } + if (duration.day != 0) + { + m_calendar->AddDays(duration.day); + } + + *endDate = m_calendar->GetDateTime(); + } + catch (Platform::InvalidArgumentException^ ex) + { + // Do nothing + return false; + } + + return true; +} + +// Subtracting Duration from a Date +// Returns: True if function succeeds to calculate the date else returns False +bool DateCalculationEngine::SubtractDuration(_In_ DateTime startDate, _In_ const DateDifference& duration, _Out_ DateTime *endDate) +{ + // For Subtract the Algorithm is different than Add. Here the smaller units are subtracted first + // and then the larger units. + try + { + m_calendar->SetDateTime(startDate); + + if (duration.day != 0) + { + m_calendar->AddDays(-duration.day); + } + if (duration.month != 0) + { + m_calendar->AddMonths(-duration.month); + } + if (duration.year != 0) + { + m_calendar->AddYears(-duration.year); + } + + *endDate = m_calendar->GetDateTime(); + } + catch (Platform::InvalidArgumentException^ ex) + { + // Do nothing + return false; + } + + // Check that the UniversalTime value is not negative + return (endDate->UniversalTime >= 0); +} + +// Calculate the difference between two dates +void DateCalculationEngine::GetDateDifference(_In_ DateTime date1, _In_ DateTime date2, _In_ DateUnit outputFormat, _Out_ DateDifference *difference) +{ + DateTime startDate; + DateTime endDate; + DateTime pivotDate; + DateTime tempPivotDate; + UINT daysDiff = 0; + UINT differenceInDates[c_unitsOfDate] = { 0 }; + + if (date1.UniversalTime < date2.UniversalTime) + { + startDate = date1; + endDate = date2; + } + else + { + startDate = date2; + endDate = date1; + } + + pivotDate = startDate; + + daysDiff = GetDifferenceInDays(startDate, endDate); + + // If output has units other than days + // 0th bit: Year, 1st bit: Month, 2nd bit: Week, 3rd bit: Day + if (static_cast(outputFormat) & 7) + { + UINT daysInMonth; + UINT approximateDaysInYear; + + // If we're unable to calculate the days-in-month or days-in-year, we'll leave the values at 0. + if (TryGetCalendarDaysInMonth(startDate, daysInMonth) + && TryGetCalendarDaysInYear(endDate, approximateDaysInYear)) + { + UINT daysIn[c_unitsOfDate] = { approximateDaysInYear, daysInMonth, c_daysInWeek, 1 }; + + for (int unitIndex = 0; unitIndex < c_unitsGreaterThanDays; unitIndex++) + { + tempPivotDate = pivotDate; + + // Check if the bit flag is set for the date unit + DateUnit dateUnit = static_cast(1 << unitIndex); + + if (static_cast(outputFormat & dateUnit)) + { + bool isEndDateHit = false; + differenceInDates[unitIndex] = (daysDiff / daysIn[unitIndex]); + + if (differenceInDates[unitIndex] != 0) + { + try + { + pivotDate = AdjustCalendarDate(pivotDate, dateUnit, static_cast(differenceInDates[unitIndex])); + } + catch (Platform::InvalidArgumentException^) + { + // Operation failed due to out of bound result + // Do nothing + differenceInDates[unitIndex] = 0; + } + } + + int tempDaysDiff; + + do + { + tempDaysDiff = GetDifferenceInDays(pivotDate, endDate); + + if (tempDaysDiff < 0) + { + // pivotDate has gone over the end date; start from the begining of this unit + differenceInDates[unitIndex] -= 1; + pivotDate = tempPivotDate; + pivotDate = AdjustCalendarDate(pivotDate, dateUnit, static_cast(differenceInDates[unitIndex])); + isEndDateHit = true; + } + else if (tempDaysDiff > 0) + { + if (isEndDateHit) + { + // This is the closest the pivot can get to the end date for this unit + break; + } + + // pivotDate is still below the end date + try + { + pivotDate = AdjustCalendarDate(pivotDate, dateUnit, 1); + differenceInDates[unitIndex] += 1; + } + catch (Platform::InvalidArgumentException^) + { + // handling for 31st Dec, 9999 last valid date + // Do nothing - break out + break; + } + } + } while (tempDaysDiff != 0); // dates are the same - exit the loop + + tempPivotDate = AdjustCalendarDate(tempPivotDate, dateUnit, static_cast(differenceInDates[unitIndex])); + pivotDate = tempPivotDate; + daysDiff = GetDifferenceInDays(pivotDate, endDate); + } + } + } + } + + differenceInDates[3] = daysDiff; + + difference->year = differenceInDates[0]; + difference->month = differenceInDates[1]; + difference->week = differenceInDates[2]; + difference->day = differenceInDates[3]; +} + + +// Private Methods + +// Gets number of days between the two date time values +int DateCalculationEngine::GetDifferenceInDays(DateTime date1, DateTime date2) +{ + // A tick is defined as the number of 100 nanoseconds + long long ticksDifference = date2.UniversalTime - date1.UniversalTime; + return static_cast(ticksDifference / static_cast(c_day)); +} + +// Gets number of Calendar days in the month in which this date falls. +// Returns true if successful, false otherwise. +bool DateCalculationEngine::TryGetCalendarDaysInMonth(_In_ DateTime date, _Out_ UINT& daysInMonth) +{ + bool result = false; + m_calendar->SetDateTime(date); + + // NumberOfDaysInThisMonth returns -1 if unknown. + int daysInThisMonth = m_calendar->NumberOfDaysInThisMonth; + if (daysInThisMonth != -1) + { + daysInMonth = static_cast(daysInThisMonth); + result = true; + } + + return result; +} + +// Gets number of Calendar days in the year in which this date falls. +// Returns true if successful, false otherwise. +bool DateCalculationEngine::TryGetCalendarDaysInYear(_In_ DateTime date, _Out_ UINT& daysInYear) +{ + bool result = false; + UINT days = 0; + + m_calendar->SetDateTime(date); + + // NumberOfMonthsInThisYear returns -1 if unknown. + int monthsInYear = m_calendar->NumberOfMonthsInThisYear; + if (monthsInYear != -1) + { + bool monthResult = true; + + // Not all years begin with Month 1. + int firstMonthThisYear = m_calendar->FirstMonthInThisYear; + for (int month = 0; month < monthsInYear; month++) + { + m_calendar->Month = firstMonthThisYear + month; + + // NumberOfDaysInThisMonth returns -1 if unknown. + int daysInMonth = m_calendar->NumberOfDaysInThisMonth; + if (daysInMonth == -1) + { + monthResult = false; + break; + } + + days += daysInMonth; + } + + if (monthResult) + { + daysInYear = days; + result = true; + } + } + + return result; +} + +// Adds/Subtracts certain value for a particular date unit +DateTime DateCalculationEngine::AdjustCalendarDate(Windows::Foundation::DateTime date, DateUnit dateUnit, int difference) +{ + m_calendar->SetDateTime(date); + + switch (dateUnit) + { + case DateUnit::Year: + m_calendar->AddYears(difference); + break; + case DateUnit::Month: + m_calendar->AddMonths(difference); + break; + case DateUnit::Week: + m_calendar->AddWeeks(difference); + break; + } + + return m_calendar->GetDateTime(); +} diff --git a/src/CalcViewModel/Common/DateCalculator.h b/src/CalcViewModel/Common/DateCalculator.h new file mode 100644 index 00000000..b755e2ee --- /dev/null +++ b/src/CalcViewModel/Common/DateCalculator.h @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +const ULONGLONG c_millisecond = 10000; +const ULONGLONG c_second = 1000 * c_millisecond; +const ULONGLONG c_minute = 60 * c_second; +const ULONGLONG c_hour = 60 * c_minute; +const ULONGLONG c_day = 24 * c_hour; + +const int c_unitsOfDate = 4; // Units Year,Month,Week,Day +const int c_unitsGreaterThanDays = 3; // Units Greater than Days (Year/Month/Week) 3 +const int c_daysInWeek = 7; + +namespace CalculatorApp +{ + namespace Common + { + namespace DateCalculation + { + public enum class _Enum_is_bitflag_ DateUnit + { + Year = 0x01, + Month = 0x02, + Week = 0x04, + Day = 0x08 + }; + + // Struct to store the difference between two Dates in the form of Years, Months , Weeks + struct DateDifference + { + int year = 0; + int month = 0; + int week = 0; + int day = 0; + }; + + class DateCalculationEngine + { + public: + // Constructor + DateCalculationEngine(_In_ Platform::String^ calendarIdentifier); + + // Public Methods + bool __nothrow AddDuration(_In_ Windows::Foundation::DateTime startDate, _In_ const DateDifference& duration, _Out_ Windows::Foundation::DateTime *endDate); + bool __nothrow SubtractDuration(_In_ Windows::Foundation::DateTime startDate, _In_ const DateDifference& duration, _Out_ Windows::Foundation::DateTime *endDate); + void __nothrow GetDateDifference(_In_ Windows::Foundation::DateTime date1, _In_ Windows::Foundation::DateTime date2, _In_ DateUnit outputFormat, _Out_ DateDifference *difference); + + private: + // Private Variables + Windows::Globalization::Calendar^ m_calendar; + + // Private Methods + int GetDifferenceInDays(Windows::Foundation::DateTime date1, Windows::Foundation::DateTime date2); + bool TryGetCalendarDaysInMonth(_In_ Windows::Foundation::DateTime date, _Out_ UINT& daysInMonth); + bool TryGetCalendarDaysInYear(_In_ Windows::Foundation::DateTime date, _Out_ UINT& daysInYear); + Windows::Foundation::DateTime AdjustCalendarDate(Windows::Foundation::DateTime date, DateUnit dateUnit, int difference); + }; + } + } +} diff --git a/src/CalcViewModel/Common/DelegateCommand.h b/src/CalcViewModel/Common/DelegateCommand.h new file mode 100644 index 00000000..0cd4fd79 --- /dev/null +++ b/src/CalcViewModel/Common/DelegateCommand.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + template + ref class DelegateCommand: public Windows::UI::Xaml::Input::ICommand + { + internal: + + typedef void (TTarget::*CommandHandlerFunc)(Platform::Object^); + + DelegateCommand(TTarget^ target, CommandHandlerFunc func): + m_weakTarget(target), + m_function(func) + { } + + private: + + // Explicit, and private, implementation of ICommand, this way of programming makes it so + // the ICommand methods will only be available if the ICommand interface is requested via a dynamic_cast + // The ICommand interface is meant to be consumed by Xaml and not by the app, this is a defensive measure against + // code in the app calling Execute. + virtual void ExecuteImpl(Platform::Object^ parameter) sealed = Windows::UI::Xaml::Input::ICommand::Execute + { + TTarget^ target = m_weakTarget.Resolve(); + if (target) + { + (target->*m_function)(parameter); + } + } + + virtual bool CanExecuteImpl(Platform::Object^ parameter) sealed = Windows::UI::Xaml::Input::ICommand::CanExecute + { + return true; + } + + virtual event Windows::Foundation::EventHandler^ CanExecuteChangedImpl + { + virtual Windows::Foundation::EventRegistrationToken add(Windows::Foundation::EventHandler^ handler) sealed = Windows::UI::Xaml::Input::ICommand::CanExecuteChanged::add + { + return m_canExecuteChanged += handler; + } + virtual void remove(Windows::Foundation::EventRegistrationToken token) sealed = Windows::UI::Xaml::Input::ICommand::CanExecuteChanged::remove + { + m_canExecuteChanged -= token; + } + } + + private: + + event Windows::Foundation::EventHandler^ m_canExecuteChanged; + + CommandHandlerFunc m_function; + Platform::WeakReference m_weakTarget; + + }; + + template + DelegateCommand^ MakeDelegate(TTarget^ target, TFuncPtr&& function) + { + return ref new DelegateCommand(target, std::forward(function)); + } + + } +} diff --git a/src/CalcViewModel/Common/DisplayExpressionToken.h b/src/CalcViewModel/Common/DisplayExpressionToken.h new file mode 100644 index 00000000..ea107626 --- /dev/null +++ b/src/CalcViewModel/Common/DisplayExpressionToken.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp::Common +{ + public enum class TokenType + { + Operator, + Operand, + Separator + }; + + [Windows::UI::Xaml::Data::Bindable] + public ref class DisplayExpressionToken sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + internal: + DisplayExpressionToken(Platform::String^ token, int tokenPosition, bool fEditable, TokenType type) : + m_Token(token), m_TokenPosition(tokenPosition), m_IsTokenEditable(fEditable), m_Type(type), m_OriginalToken(token), m_InEditMode(false) + {} + public: + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(Platform::String^, Token); + OBSERVABLE_PROPERTY_RW(int, TokenPosition); + OBSERVABLE_PROPERTY_RW(bool, IsTokenEditable); + OBSERVABLE_PROPERTY_RW(int, CommandIndex); + OBSERVABLE_PROPERTY_R(Platform::String^, OriginalToken); + + property bool IsTokenInEditMode { + bool get() { return m_InEditMode; } + void set(bool val) + { + if (!val) + { + m_OriginalToken = ref new Platform::String(m_Token->Data()); + } + m_InEditMode = val; + } + } + internal: + OBSERVABLE_PROPERTY_RW(TokenType, Type); + private: + bool m_InEditMode; + }; +} diff --git a/src/CalcViewModel/Common/EngineResourceProvider.cpp b/src/CalcViewModel/Common/EngineResourceProvider.cpp new file mode 100644 index 00000000..657a2637 --- /dev/null +++ b/src/CalcViewModel/Common/EngineResourceProvider.cpp @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "EngineResourceProvider.h" +#include "Common/LocalizationSettings.h" + +using namespace CalculatorApp::Common; +using namespace Platform; +using namespace Windows::ApplicationModel::Resources; +using namespace std; + +namespace CalculatorApp +{ + EngineResourceProvider::EngineResourceProvider() + { + m_resLoader = ResourceLoader::GetForViewIndependentUse("CEngineStrings"); + } + + wstring EngineResourceProvider::GetCEngineString(const wstring& id) + { + const auto& localizationSettings = LocalizationSettings::GetInstance(); + + if (id.compare(L"sDecimal") == 0) + { + return localizationSettings.GetDecimalSeparatorStr(); + } + + if (id.compare(L"sThousand") == 0) + { + return localizationSettings.GetNumberGroupingSeparatorStr(); + } + + if (id.compare(L"sGrouping") == 0) + { + // The following groupings are the onces that CalcEngine supports. + // 0;0 0x000 - no grouping + // 3;0 0x003 - group every 3 digits + // 3;2;0 0x023 - group 1st 3 and then every 2 digits + // 4;0 0x004 - group every 4 digits + // 5;3;2;0 0x235 - group 5, then 3, then every 2 + wstring numberGroupingString = localizationSettings.GetNumberGroupingStr(); + return numberGroupingString; + } + + StringReference idRef(id.c_str()); + String^ str = m_resLoader->GetString(idRef); + return str->Begin(); + } +} diff --git a/src/CalcViewModel/Common/EngineResourceProvider.h b/src/CalcViewModel/Common/EngineResourceProvider.h new file mode 100644 index 00000000..b7d735dd --- /dev/null +++ b/src/CalcViewModel/Common/EngineResourceProvider.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + class EngineResourceProvider : public CalculationManager::IResourceProvider + { + public: + EngineResourceProvider(); + virtual std::wstring GetCEngineString(const std::wstring& id) override; + + private: + Windows::ApplicationModel::Resources::ResourceLoader^ m_resLoader; + }; +} diff --git a/src/CalcViewModel/Common/ExpressionCommandDeserializer.cpp b/src/CalcViewModel/Common/ExpressionCommandDeserializer.cpp new file mode 100644 index 00000000..09bdcc31 --- /dev/null +++ b/src/CalcViewModel/Common/ExpressionCommandDeserializer.cpp @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "ExpressionCommandDeserializer.h" + +using namespace CalculatorApp::Common; +using namespace Windows::Storage::Streams; + +CommandDeserializer::CommandDeserializer(_In_ DataReader^ dataReader) :m_dataReader(dataReader){} + +std::shared_ptr CommandDeserializer::Deserialize(_In_ CalculationManager::CommandType cmdType) +{ + switch (cmdType) + { + case CalculationManager::CommandType::OperandCommand: + + return std::make_shared(DeserializeOperand()); + break; + + case CalculationManager::CommandType::Parentheses: + + return std::make_shared(DeserializeParentheses()); + break; + + case CalculationManager::CommandType::UnaryCommand: + + return std::make_shared(DeserializeUnary()); + break; + + case CalculationManager::CommandType::BinaryCommand: + + return std::make_shared(DeserializeBinary()); + break; + + default: + throw ref new Platform::Exception(E_INVALIDARG, ref new Platform::String(L"Unknown command type")); + } +} + +COpndCommand CommandDeserializer::DeserializeOperand() +{ + bool fNegative = m_dataReader->ReadBoolean(); + bool fDecimal = m_dataReader->ReadBoolean(); + bool fSciFmt = m_dataReader->ReadBoolean(); + + std::shared_ptr> cmdVector = std::make_shared>(); + auto cmdVectorSize = m_dataReader->ReadUInt32(); + + for (unsigned int j = 0; j < cmdVectorSize; ++j) + { + int eachOpndcmd = m_dataReader->ReadInt32(); + cmdVector->Append(eachOpndcmd); + } + + return COpndCommand(cmdVector, fNegative, fDecimal, fSciFmt); +} + +CParentheses CommandDeserializer::DeserializeParentheses() +{ + int parenthesisCmd = m_dataReader->ReadInt32(); + return CParentheses(parenthesisCmd); +} + +CUnaryCommand CommandDeserializer::DeserializeUnary() +{ + auto cmdSize = m_dataReader->ReadUInt32(); + std::shared_ptr> cmdVector = std::make_shared>(); + + if (cmdSize == 1) + { + int eachOpndcmd = m_dataReader->ReadInt32(); + return CUnaryCommand(eachOpndcmd); + } + else + { + int eachOpndcmd1 = m_dataReader->ReadInt32(); + int eachOpndcmd2 = m_dataReader->ReadInt32(); + return CUnaryCommand(eachOpndcmd1, eachOpndcmd2); + } +} + +CBinaryCommand CommandDeserializer::DeserializeBinary() +{ + int cmd = m_dataReader->ReadInt32(); + return CBinaryCommand(cmd); +} diff --git a/src/CalcViewModel/Common/ExpressionCommandDeserializer.h b/src/CalcViewModel/Common/ExpressionCommandDeserializer.h new file mode 100644 index 00000000..988017bf --- /dev/null +++ b/src/CalcViewModel/Common/ExpressionCommandDeserializer.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + class CommandDeserializer + { + public: + CommandDeserializer(_In_ Windows::Storage::Streams::DataReader^ dataReader); + std::shared_ptr Deserialize(_In_ CalculationManager::CommandType cmdType); + + private: + Windows::Storage::Streams::DataReader^ m_dataReader; + COpndCommand DeserializeOperand(); + CParentheses DeserializeParentheses(); + CUnaryCommand DeserializeUnary(); + CBinaryCommand DeserializeBinary(); + }; + } +} diff --git a/src/CalcViewModel/Common/ExpressionCommandSerializer.cpp b/src/CalcViewModel/Common/ExpressionCommandSerializer.cpp new file mode 100644 index 00000000..51c3eae9 --- /dev/null +++ b/src/CalcViewModel/Common/ExpressionCommandSerializer.cpp @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Common\ExpressionCommandSerializer.h" + +using namespace CalculatorApp::Common; +using namespace Windows::Storage::Streams; + +SerializeCommandVisitor::SerializeCommandVisitor(_In_ DataWriter^ dataWriter) :m_dataWriter(dataWriter) +{ +} + +void SerializeCommandVisitor::Visit(_In_ COpndCommand &opndCmd) +{ + m_dataWriter->WriteBoolean(opndCmd.IsNegative()); + m_dataWriter->WriteBoolean(opndCmd.IsDecimalPresent()); + m_dataWriter->WriteBoolean(opndCmd.IsSciFmt()); + + auto opndCmds = opndCmd.GetCommands(); + unsigned int opndCmdSize; + opndCmds->GetSize(&opndCmdSize); + m_dataWriter->WriteUInt32(opndCmdSize); + for (unsigned int j = 0; j < opndCmdSize; ++j) + { + int eachOpndcmd; + opndCmds->GetAt(j, &eachOpndcmd); + m_dataWriter->WriteInt32(eachOpndcmd); + } +} + +void SerializeCommandVisitor::Visit(_In_ CUnaryCommand &unaryCmd) +{ + auto cmds = unaryCmd.GetCommands(); + unsigned int cmdSize; + cmds->GetSize(&cmdSize); + m_dataWriter->WriteUInt32(cmdSize); + for (unsigned int j = 0; j < cmdSize; ++j) + { + int eachOpndcmd; + cmds->GetAt(j, &eachOpndcmd); + m_dataWriter->WriteInt32(eachOpndcmd); + } +} + +void SerializeCommandVisitor::Visit(_In_ CBinaryCommand &binaryCmd) +{ + int cmd = binaryCmd.GetCommand(); + m_dataWriter->WriteInt32(cmd); +} + +void SerializeCommandVisitor::Visit(_In_ CParentheses ¶Cmd) +{ + int parenthesisCmd = paraCmd.GetCommand(); + m_dataWriter->WriteInt32(parenthesisCmd); +} diff --git a/src/CalcViewModel/Common/ExpressionCommandSerializer.h b/src/CalcViewModel/Common/ExpressionCommandSerializer.h new file mode 100644 index 00000000..344a4f65 --- /dev/null +++ b/src/CalcViewModel/Common/ExpressionCommandSerializer.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + class SerializeCommandVisitor : public ISerializeCommandVisitor + { + public: + SerializeCommandVisitor(_In_ Windows::Storage::Streams::DataWriter^ dataWriter); + + void Visit(_In_ COpndCommand &opndCmd); + void Visit(_In_ CUnaryCommand &unaryCmd); + void Visit(_In_ CBinaryCommand &binaryCmd); + void Visit(_In_ CParentheses ¶Cmd); + + private: + Windows::Storage::Streams::DataWriter^ m_dataWriter; + }; + } +} diff --git a/src/CalcViewModel/Common/KeyboardShortcutManager.cpp b/src/CalcViewModel/Common/KeyboardShortcutManager.cpp new file mode 100644 index 00000000..d8c40f49 --- /dev/null +++ b/src/CalcViewModel/Common/KeyboardShortcutManager.cpp @@ -0,0 +1,876 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "KeyboardShortcutManager.h" +#include "AppResourceProvider.h" +#include "ApplicationViewModel.h" +#include "LocalizationSettings.h" + +using namespace Concurrency; +using namespace Platform; +using namespace std; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::System; +using namespace Utils; +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; + +namespace MUXC = Microsoft::UI::Xaml::Controls; + +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, Character); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKey); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyControlChord); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyShiftChord); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyAltChord); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyControlShiftChord); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyInverseChord); +DEPENDENCY_PROPERTY_INITIALIZATION(KeyboardShortcutManager, VirtualKeyControlInverseChord); + +static multimap> s_CharacterForButtons; +static multimap> s_VirtualKeysForButtons; +static multimap> s_VirtualKeyControlChordsForButtons; +static multimap> s_VirtualKeyShiftChordsForButtons; +static multimap> s_VirtualKeyAltChordsForButtons; +static multimap> s_VirtualKeyControlShiftChordsForButtons; +static multimap> s_VirtualKeyInverseChordsForButtons; +static multimap> s_VirtualKeyControlInverseChordsForButtons; + +static const TimeSpan c_lightUpTime = { 500000 }; // Quarter of a second +static multimap s_ShiftKeyPressed; +static multimap s_ControlKeyPressed; +static multimap s_ShiftButtonChecked; +static multimap s_IsDropDownOpen; + +static reader_writer_lock s_keyboardShortcutMapLock; + +namespace CalculatorApp +{ + namespace Common + { + // Lights up all of the buttons in the given range + // The range is defined by a pair of iterators + template + void LightUpButtons(const T& buttons) + { + auto iterator = buttons.first; + for (; iterator != buttons.second; ++iterator) + { + auto button = iterator->second.Resolve(); + if (button && button->IsEnabled) + { + LightUpButton(button); + } + } + } + + void LightUpButton(ButtonBase^ button) + { + // If the button is a toggle button then we don't need + // to change the UI of the button + if (dynamic_cast(button)) + { + return; + } + + // The button will go into the visual Pressed state with this call + VisualStateManager::GoToState(button, "Pressed", true); + + // This timer will fire after c_lightUpTime and make the button + // go back to the normal state. + // This timer will only fire once after which it will be destroyed + auto timer = ref new DispatcherTimer(); + timer->Interval = c_lightUpTime; + + WeakReference timerWeakReference(timer); + WeakReference buttonWeakReference(button); + timer->Tick += ref new EventHandler( + [buttonWeakReference, timerWeakReference](Object^, Object^) + { + auto button = buttonWeakReference.Resolve(); + if (button) + { + VisualStateManager::GoToState(button, "Normal", true); + } + + // Cancel the timer after we're done so it only fires once + auto timer = timerWeakReference.Resolve(); + if (timer) + { + timer->Stop(); + } + }); + timer->Start(); + } + + // Looks for the first button reference that it can resolve + // and execute its command. + // NOTE: It is assumed that all buttons associated with a particular + // key have the same command + template + void RunFirstEnabledButtonCommand(const T& buttons) + { + auto buttonIterator = buttons.first; + for (; buttonIterator != buttons.second; ++buttonIterator) + { + auto button = buttonIterator->second.Resolve(); + if (button && button->IsEnabled) + { + RunButtonCommand(button); + break; + } + } + } + + void RunButtonCommand(ButtonBase^ button) + { + if (button->IsEnabled) + { + auto command = button->Command; + auto parameter = button->CommandParameter; + if (command && command->CanExecute(parameter)) + { + command->Execute(parameter); + } + + auto radio = dynamic_cast(button); + if (radio) + { + radio->IsChecked = true; + return; + } + + auto toggle = dynamic_cast(button); + if (toggle) + { + toggle->IsChecked = !toggle->IsChecked->Value; + return; + } + } + } + } +} + +static multimap s_ignoreNextEscape; +static multimap s_keepIgnoringEscape; +static multimap s_fHonorShortcuts; +static multimap s_fHandledEnter; +static multimap s_AboutFlyout; + +void KeyboardShortcutManager::IgnoreEscape(bool onlyOnce) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + int viewId = Utils::GetWindowId(); + + if (s_ignoreNextEscape.find(viewId) != s_ignoreNextEscape.end()) + { + s_ignoreNextEscape.erase(viewId); + s_ignoreNextEscape.insert(std::make_pair(viewId, true)); + } + + if (s_keepIgnoringEscape.find(viewId) != s_keepIgnoringEscape.end()) + { + s_keepIgnoringEscape.erase(viewId); + s_keepIgnoringEscape.insert(std::make_pair(viewId, !onlyOnce)); + } +} + +void KeyboardShortcutManager::HonorEscape() +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + int viewId = Utils::GetWindowId(); + + if (s_ignoreNextEscape.find(viewId) != s_ignoreNextEscape.end()) + { + s_ignoreNextEscape.erase(viewId); + s_ignoreNextEscape.insert(std::make_pair(viewId, false)); + } + + if (s_keepIgnoringEscape.find(viewId) != s_keepIgnoringEscape.end()) + { + s_keepIgnoringEscape.erase(viewId); + s_keepIgnoringEscape.insert(std::make_pair(viewId, false)); + } +} + +void KeyboardShortcutManager::OnCharacterPropertyChanged( + DependencyObject^ target, + String^ oldValue, + String^ newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_CharacterForButtons.find(viewId); + + if (iterViewMap != s_CharacterForButtons.end()) + { + if (oldValue) + { + iterViewMap->second.erase(oldValue->Data()[0]); + } + + if (newValue) + { + if (newValue == L".") + { + wchar_t decSep = LocalizationSettings::GetInstance().GetDecimalSeparator(); + iterViewMap->second.insert(std::make_pair(decSep, WeakReference(button))); + } + else + { + iterViewMap->second.insert(std::make_pair(newValue->Data()[0], WeakReference(button))); + } + } + } + else + { + s_CharacterForButtons.insert(std::make_pair(viewId, std::multimap())); + + if (newValue == L".") + { + wchar_t decSep = LocalizationSettings::GetInstance().GetDecimalSeparator(); + s_CharacterForButtons.find(viewId)->second.insert(std::make_pair(decSep, WeakReference(button))); + } + else + { + s_CharacterForButtons.find(viewId)->second.insert(std::make_pair(newValue->Data()[0], WeakReference(button))); + } + } +} + +void KeyboardShortcutManager::OnVirtualKeyPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = static_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeysForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeysForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(button))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeysForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeysForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(button))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyControlChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + Control^ control = dynamic_cast(target); + + if (control == nullptr) + { + // Handling Ctrl+E shortcut for Date Calc, target would be NavigationView^ in that case + control = safe_cast(target); + } + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyControlChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyControlChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(control))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyControlChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyControlChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(control))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyShiftChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyShiftChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyShiftChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(button))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyShiftChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyShiftChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(button))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyAltChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + MUXC::NavigationView^ navView = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyAltChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyAltChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(navView))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyAltChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyAltChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(navView))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyControlShiftChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyControlShiftChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyControlShiftChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(button))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyControlShiftChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyControlShiftChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(button))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyInverseChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyInverseChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyInverseChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(button))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyInverseChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyInverseChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(button))); + } +} + +void KeyboardShortcutManager::OnVirtualKeyControlInverseChordPropertyChanged( + DependencyObject^ target, + MyVirtualKey /*oldValue*/, + MyVirtualKey newValue) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto button = safe_cast(target); + + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_VirtualKeyControlInverseChordsForButtons.find(viewId); + + // Check if the View Id has already been registered + if (iterViewMap != s_VirtualKeyControlInverseChordsForButtons.end()) + { + iterViewMap->second.insert(std::make_pair(newValue, WeakReference(button))); + } + else + { + // If the View Id is not already registered, then register it and make the entry + s_VirtualKeyControlInverseChordsForButtons.insert(std::make_pair(viewId, std::multimap())); + s_VirtualKeyControlInverseChordsForButtons.find(viewId)->second.insert(std::make_pair(newValue, WeakReference(button))); + } +} + +// In the three event handlers bellow we will not mark the event as handled +// because this is a sumplemental operation and we don't want to interfere with +// the normal keyboard handling. +void KeyboardShortcutManager::OnCharacterReceivedHandler(CoreWindow^ sender, CharacterReceivedEventArgs^ args) +{ + int viewId = Utils::GetWindowId(); + auto currentHonorShortcuts = s_fHonorShortcuts.find(viewId); + + if (currentHonorShortcuts != s_fHonorShortcuts.end()) + { + if (currentHonorShortcuts->second) + { + wchar_t character = static_cast(args->KeyCode); + auto buttons = s_CharacterForButtons.find(viewId)->second.equal_range(character); + + RunFirstEnabledButtonCommand(buttons); + + LightUpButtons(buttons); + } + } +} + +const std::multimap& GetCurrentKeyDictionary(MyVirtualKey key, bool altPressed = false) +{ + int viewId = Utils::GetWindowId(); + + if (altPressed) + { + return s_VirtualKeyAltChordsForButtons.find(viewId)->second; + } + else if ((s_ShiftKeyPressed.find(viewId)->second) && ((Window::Current->CoreWindow->GetKeyState(VirtualKey::Control) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down)) + { + return s_VirtualKeyControlShiftChordsForButtons.find(viewId)->second; + } + else if (s_ShiftKeyPressed.find(viewId)->second) + { + return s_VirtualKeyShiftChordsForButtons.find(viewId)->second; + } + else if (s_ShiftButtonChecked.find(viewId)->second) + { + if ((Window::Current->CoreWindow->GetKeyState(VirtualKey::Control) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) + { + auto iterViewMap = s_VirtualKeyControlInverseChordsForButtons.find(viewId); + if (iterViewMap != s_VirtualKeyControlInverseChordsForButtons.end()) + { + for (auto iterator = iterViewMap->second.begin(); iterator != iterViewMap->second.end(); ++iterator) + { + if (key == iterator->first) + { + return s_VirtualKeyControlInverseChordsForButtons.find(viewId)->second; + } + } + } + } + else + { + auto iterViewMap = s_VirtualKeyControlInverseChordsForButtons.find(viewId); + if (iterViewMap != s_VirtualKeyControlInverseChordsForButtons.end()) + { + for (auto iterator = iterViewMap->second.begin(); iterator != iterViewMap->second.end(); ++iterator) + { + if (key == iterator->first) + { + return s_VirtualKeyInverseChordsForButtons.find(viewId)->second; + } + } + } + } + } + if ((Window::Current->CoreWindow->GetKeyState(VirtualKey::Control) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) + { + return s_VirtualKeyControlChordsForButtons.find(viewId)->second; + } + else + { + return s_VirtualKeysForButtons.find(viewId)->second; + } +} + +void KeyboardShortcutManager::OnKeyDownHandler(CoreWindow^ sender, KeyEventArgs^ args) +{ + // If keyboard shortcuts like Ctrl+C or Ctrl+V are not handled + if (!args->Handled) + { + auto key = args->VirtualKey; + int viewId = Utils::GetWindowId(); + + auto currentControlKeyPressed = s_ControlKeyPressed.find(viewId); + auto currentShiftKeyPressed = s_ShiftKeyPressed.find(viewId); + + bool isControlKeyPressed = (currentControlKeyPressed != s_ControlKeyPressed.end()) && (currentControlKeyPressed->second); + bool isShiftKeyPressed = (currentShiftKeyPressed != s_ShiftKeyPressed.end()) && (currentShiftKeyPressed->second); + + // Handle Ctrl + E for DateCalculator + if ((key == VirtualKey::E) && + isControlKeyPressed && + !isShiftKeyPressed) + { + const auto& lookupMap = GetCurrentKeyDictionary(static_cast(key)); + auto buttons = lookupMap.equal_range(static_cast(key)); + auto navView = buttons.first->second.Resolve(); + auto appViewModel = safe_cast(navView->DataContext); + appViewModel->Mode = ViewMode::Date; + auto categoryName = AppResourceProvider::GetInstance().GetResourceString(L"DateCalculationModeText"); + appViewModel->CategoryName = categoryName; + + auto menuItems = static_cast^>(navView->MenuItemsSource); + auto flatIndex = NavCategory::GetFlatIndex(ViewMode::Date); + navView->SelectedItem = menuItems->GetAt(flatIndex); + return; + } + + auto currentHonorShortcuts = s_fHonorShortcuts.find(viewId); + + auto currentIgnoreNextEscape = s_ignoreNextEscape.find(viewId); + + if (currentIgnoreNextEscape != s_ignoreNextEscape.end()) + { + if (currentIgnoreNextEscape->second && key == VirtualKey::Escape) + { + auto currentKeepIgnoringEscape = s_keepIgnoringEscape.find(viewId); + + if (currentKeepIgnoringEscape != s_keepIgnoringEscape.end()) + { + if (!currentKeepIgnoringEscape->second) + { + HonorEscape(); + } + return; + } + } + } + + if (key == VirtualKey::Control) + { + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto currentControlKeyPressed = s_ControlKeyPressed.find(viewId); + + if (currentControlKeyPressed != s_ControlKeyPressed.end()) + { + s_ControlKeyPressed.erase(viewId); + s_ControlKeyPressed.insert(std::make_pair(viewId, true)); + } + return; + } + else if (key == VirtualKey::Shift) + { + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto currentShiftKeyPressed = s_ShiftKeyPressed.find(viewId); + + if (currentShiftKeyPressed != s_ShiftKeyPressed.end()) + { + s_ShiftKeyPressed.erase(viewId); + s_ShiftKeyPressed.insert(std::make_pair(viewId, true)); + } + return; + } + + const auto& lookupMap = GetCurrentKeyDictionary(static_cast(key)); + auto buttons = lookupMap.equal_range(static_cast(key)); + + auto currentIsDropDownOpen = s_IsDropDownOpen.find(viewId); + + if (currentHonorShortcuts != s_fHonorShortcuts.end()) + { + if (currentHonorShortcuts->second) + { + RunFirstEnabledButtonCommand(buttons); + + // Ctrl+C and Ctrl+V shifts focus to some button because of which enter doesn't work after copy/paste. So don't shift focus if Ctrl+C or Ctrl+V is pressed. + // When drop down is open, pressing escape shifts focus to clear button. So dont's shift focus if drop down is open. + // Ctrl+Insert is equivalent to Ctrl+C and Shift+Insert is equivalent to Ctrl+V + if (currentIsDropDownOpen != s_IsDropDownOpen.end() && !currentIsDropDownOpen->second) + { + // Do not Light Up Buttons when Ctrl+C, Ctrl+V, Ctrl+Insert or Shift+Insert is pressed + if (!(isControlKeyPressed && (key == VirtualKey::C || key == VirtualKey::V || key == VirtualKey::Insert)) + && !(isShiftKeyPressed && (key == VirtualKey::Insert))) + { + LightUpButtons(buttons); + } + } + } + } + } +} + +void KeyboardShortcutManager::OnKeyUpHandler(CoreWindow^ sender, KeyEventArgs^ args) +{ + int viewId = Utils::GetWindowId(); + auto key = args->VirtualKey; + + if (args->VirtualKey == VirtualKey::Shift) + { + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto currentShiftKeyPressed = s_ShiftKeyPressed.find(viewId); + + if (currentShiftKeyPressed != s_ShiftKeyPressed.end()) + { + s_ShiftKeyPressed.erase(viewId); + s_ShiftKeyPressed.insert(std::make_pair(viewId, false)); + } + } + else if (args->VirtualKey == VirtualKey::Control) + { + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + auto currentControlKeyPressed = s_ControlKeyPressed.find(viewId); + + if (currentControlKeyPressed != s_ControlKeyPressed.end()) + { + s_ControlKeyPressed.erase(viewId); + s_ControlKeyPressed.insert(std::make_pair(viewId, false)); + } + } +} + +void KeyboardShortcutManager::OnAcceleratorKeyActivated(CoreDispatcher^, AcceleratorKeyEventArgs^ args) +{ + if (args->KeyStatus.IsKeyReleased) + { + auto key = args->VirtualKey; + bool altPressed = args->KeyStatus.IsMenuKeyDown; + + // If the Alt/Menu key is not pressed then we don't care about the key anymore + if (!altPressed) + { + return; + } + + const auto& lookupMap = GetCurrentKeyDictionary(static_cast(key), altPressed); + auto listItems = lookupMap.equal_range(static_cast(key)); + for (auto listIterator = listItems.first; listIterator != listItems.second; ++listIterator) + { + auto item = listIterator->second.Resolve(); + if (item != nullptr) + { + auto navView = safe_cast (item); + + auto menuItems = static_cast^>(navView->MenuItemsSource); + if (menuItems != nullptr) + { + auto vm = safe_cast(navView->DataContext); + if (nullptr != vm) + { + ViewMode toMode = NavCategory::GetViewModeForVirtualKey(static_cast(key)); + if (NavCategory::IsValidViewMode(toMode)) + { + vm->Mode = toMode; + navView->SelectedItem = menuItems->GetAt(NavCategory::GetFlatIndex(toMode)); + } + } + } + break; + } + } + } + + if (args->VirtualKey == VirtualKey::Escape) + { + int viewId = Utils::GetWindowId(); + auto iterViewMap = s_AboutFlyout.find(viewId); + + if ((iterViewMap != s_AboutFlyout.end()) && (iterViewMap->second != nullptr)) + { + iterViewMap->second->Hide(); + } + } +} + +void KeyboardShortcutManager::Initialize() +{ + auto coreWindow = Window::Current->CoreWindow; + coreWindow->CharacterReceived += + ref new TypedEventHandler(&KeyboardShortcutManager::OnCharacterReceivedHandler); + coreWindow->KeyDown += + ref new TypedEventHandler(&KeyboardShortcutManager::OnKeyDownHandler); + coreWindow->KeyUp += + ref new TypedEventHandler(&KeyboardShortcutManager::OnKeyUpHandler); + coreWindow->Dispatcher->AcceleratorKeyActivated += + ref new TypedEventHandler(&KeyboardShortcutManager::OnAcceleratorKeyActivated); + + KeyboardShortcutManager::RegisterNewAppViewId(); +} + +void KeyboardShortcutManager::ShiftButtonChecked(bool checked) +{ + int viewId = Utils::GetWindowId(); + + if (s_ShiftButtonChecked.find(viewId) != s_ShiftButtonChecked.end()) + { + s_ShiftButtonChecked.erase(viewId); + s_ShiftButtonChecked.insert(std::make_pair(viewId, checked)); + } +} + +void KeyboardShortcutManager::UpdateDropDownState(bool isOpen) +{ + int viewId = Utils::GetWindowId(); + + if (s_IsDropDownOpen.find(viewId) != s_IsDropDownOpen.end()) + { + s_IsDropDownOpen.erase(viewId); + s_IsDropDownOpen.insert(std::make_pair(viewId, isOpen)); + } +} + +void KeyboardShortcutManager::UpdateDropDownState(Flyout^ aboutPageFlyout) +{ + int viewId = Utils::GetWindowId(); + + if (s_AboutFlyout.find(viewId) != s_AboutFlyout.end()) + { + s_AboutFlyout.erase(viewId); + s_AboutFlyout.insert(std::make_pair(viewId, aboutPageFlyout)); + } +} + +void KeyboardShortcutManager::HonorShortcuts(bool allow) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + int viewId = Utils::GetWindowId(); + + if (s_fHonorShortcuts.find(viewId) != s_fHonorShortcuts.end()) + { + s_fHonorShortcuts.erase(viewId); + s_fHonorShortcuts.insert(std::make_pair(viewId, allow)); + } +} + +void KeyboardShortcutManager::HandledEnter(bool ishandled) +{ + int viewId = Utils::GetWindowId(); + + if (s_fHandledEnter.find(viewId) != s_fHandledEnter.end()) + { + s_fHandledEnter.erase(viewId); + s_fHandledEnter.insert(std::make_pair(viewId, ishandled)); + } +} + +void KeyboardShortcutManager::RegisterNewAppViewId() +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + int appViewId = Utils::GetWindowId(); + + // Check if the View Id has already been registered + if (s_CharacterForButtons.find(appViewId) == s_CharacterForButtons.end()) + { + s_CharacterForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeysForButtons.find(appViewId) == s_VirtualKeysForButtons.end()) + { + s_VirtualKeysForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyControlChordsForButtons.find(appViewId) == s_VirtualKeyControlChordsForButtons.end()) + { + s_VirtualKeyControlChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyShiftChordsForButtons.find(appViewId) == s_VirtualKeyShiftChordsForButtons.end()) + { + s_VirtualKeyShiftChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyAltChordsForButtons.find(appViewId) == s_VirtualKeyAltChordsForButtons.end()) + { + s_VirtualKeyAltChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyControlShiftChordsForButtons.find(appViewId) == s_VirtualKeyControlShiftChordsForButtons.end()) + { + s_VirtualKeyControlShiftChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyInverseChordsForButtons.find(appViewId) == s_VirtualKeyInverseChordsForButtons.end()) + { + s_VirtualKeyInverseChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + if (s_VirtualKeyControlInverseChordsForButtons.find(appViewId) == s_VirtualKeyControlInverseChordsForButtons.end()) + { + s_VirtualKeyControlInverseChordsForButtons.insert(std::make_pair(appViewId, std::multimap())); + } + + s_ShiftKeyPressed.insert(std::make_pair(appViewId, false)); + s_ControlKeyPressed.insert(std::make_pair(appViewId, false)); + s_ShiftButtonChecked.insert(std::make_pair(appViewId, false)); + s_IsDropDownOpen.insert(std::make_pair(appViewId, false)); + s_ignoreNextEscape.insert(std::make_pair(appViewId, false)); + s_keepIgnoringEscape.insert(std::make_pair(appViewId, false)); + s_fHonorShortcuts.insert(std::make_pair(appViewId, true)); + s_fHandledEnter.insert(std::make_pair(appViewId, true)); + s_AboutFlyout.insert(std::make_pair(appViewId, nullptr)); +} + +void KeyboardShortcutManager::OnWindowClosed(int viewId) +{ + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_keyboardShortcutMapLock); + + s_CharacterForButtons.erase(viewId); + + s_VirtualKeysForButtons.erase(viewId); + s_VirtualKeyControlChordsForButtons.erase(viewId); + s_VirtualKeyShiftChordsForButtons.erase(viewId); + s_VirtualKeyAltChordsForButtons.erase(viewId); + s_VirtualKeyControlShiftChordsForButtons.erase(viewId); + s_VirtualKeyInverseChordsForButtons.erase(viewId); + s_VirtualKeyControlInverseChordsForButtons.erase(viewId); + + s_ShiftKeyPressed.erase(viewId); + s_ControlKeyPressed.erase(viewId); + s_ShiftButtonChecked.erase(viewId); + s_IsDropDownOpen.erase(viewId); + s_ignoreNextEscape.erase(viewId); + s_keepIgnoringEscape.erase(viewId); + s_fHonorShortcuts.erase(viewId); + s_fHandledEnter.erase(viewId); + s_AboutFlyout.erase(viewId); +} diff --git a/src/CalcViewModel/Common/KeyboardShortcutManager.h b/src/CalcViewModel/Common/KeyboardShortcutManager.h new file mode 100644 index 00000000..28c3b617 --- /dev/null +++ b/src/CalcViewModel/Common/KeyboardShortcutManager.h @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + public ref class KeyboardShortcutManager sealed + { + public: + KeyboardShortcutManager() {} + + DEPENDENCY_PROPERTY_OWNER(KeyboardShortcutManager); + + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(Platform::String^, Character); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKey); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyControlChord); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyShiftChord); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyAltChord); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyControlShiftChord); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyInverseChord); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(MyVirtualKey, VirtualKeyControlInverseChord); + + internal: + + static void Initialize(); + + // Sometimes, like with popups, escape is treated as special and even + // though it is handled we get it passed through to us. In those cases + // we need to be able to ignore it (looking at e->Hanlded isn't sufficient + // because that always returns true). + // The onlyOnce flag is used to indicate whether we should only ignore the + // next escape, or keep ignoring until you explicitly HonorEscape. + static void IgnoreEscape(bool onlyOnce); + static void HonorEscape(); + static void HonorShortcuts(bool allow); + static void HandledEnter(bool ishandled); + static void UpdateDropDownState(bool); + static void ShiftButtonChecked(bool checked); + static void UpdateDropDownState(Windows::UI::Xaml::Controls::Flyout^ aboutPageFlyout); + + static void RegisterNewAppViewId(); + static void OnWindowClosed(int viewId); + + private: + + static void OnCharacterPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + Platform::String^ oldValue, + Platform::String^ newValue); + + static void OnVirtualKeyPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyControlChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyShiftChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyInverseChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyControlInverseChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyAltChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnVirtualKeyControlShiftChordPropertyChanged( + Windows::UI::Xaml::DependencyObject^ target, + MyVirtualKey oldValue, + MyVirtualKey newValue); + + static void OnCharacterReceivedHandler(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::CharacterReceivedEventArgs^ args); + static void OnKeyDownHandler(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ args); + static void OnKeyUpHandler(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ args); + static void OnAcceleratorKeyActivated(Windows::UI::Core::CoreDispatcher^, Windows::UI::Core::AcceleratorKeyEventArgs^ args); + }; + } +} diff --git a/src/CalcViewModel/Common/LocalizationService.cpp b/src/CalcViewModel/Common/LocalizationService.cpp new file mode 100644 index 00000000..df1d85b3 --- /dev/null +++ b/src/CalcViewModel/Common/LocalizationService.cpp @@ -0,0 +1,554 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "LocalizationService.h" +#include "LocalizationSettings.h" +#include "AppResourceProvider.h" + +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::LocalizationServiceProperties; +using namespace Concurrency; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::ApplicationModel::Resources::Core; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Globalization; +using namespace Windows::Globalization::DateTimeFormatting; +using namespace Windows::Globalization::Fonts; +using namespace Windows::Globalization::NumberFormatting; +using namespace Windows::System::UserProfile; +using namespace Windows::UI::Text; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Documents; +using namespace Windows::UI::Xaml::Media; + +DEPENDENCY_PROPERTY_INITIALIZATION(LocalizationService, FontType); +DEPENDENCY_PROPERTY_INITIALIZATION(LocalizationService, FontSize); + +static reader_writer_lock s_locServiceInstanceLock; + +LocalizationService^ LocalizationService::s_singletonInstance = nullptr; + +// Resources for the engine use numbers as keys. It's inconvenient, but also difficult to +// change given that the engine heavily relies on perfect ordering of certain elements. +// The key for open parenthesis, '(', is "48". +static constexpr auto s_openParenResourceKey = L"48"; + +LocalizationService^ LocalizationService::GetInstance() +{ + + if (s_singletonInstance == nullptr) + { + // Writer lock for the static maps + reader_writer_lock::scoped_lock lock(s_locServiceInstanceLock); + + if (s_singletonInstance == nullptr) + { + s_singletonInstance = ref new LocalizationService(); + } + } + return s_singletonInstance; +} + +LocalizationService::LocalizationService() +{ + m_language = ApplicationLanguages::Languages->GetAt(0); + m_flowDirection = ResourceContext::GetForCurrentView()->QualifierValues->Lookup(L"LayoutDirection") + != L"LTR" ? FlowDirection::RightToLeft : FlowDirection::LeftToRight; + + auto resourceLoader = AppResourceProvider::GetInstance(); + m_fontFamilyOverride = resourceLoader.GetResourceString(L"LocalizedFontFamilyOverride"); + + String^ reserved = L"RESERVED_FOR_FONTLOC"; + + m_overrideFontApiValues = ((m_fontFamilyOverride != nullptr) && (m_fontFamilyOverride != reserved)); + if (m_overrideFontApiValues) + { + String^ localizedUICaptionFontSizeFactorOverride = resourceLoader.GetResourceString(L"LocalizedUICaptionFontSizeFactorOverride"); + String^ localizedUITextFontSizeFactorOverride = resourceLoader.GetResourceString(L"LocalizedUITextFontSizeFactorOverride"); + String^ localizedFontWeightOverride = resourceLoader.GetResourceString(L"LocalizedFontWeightOverride"); + + // If any of the font overrides are modified then all of them need to be modified + assert(localizedFontWeightOverride != reserved); + assert(localizedUITextFontSizeFactorOverride != reserved); + assert(localizedUICaptionFontSizeFactorOverride != reserved); + + m_fontWeightOverride = ParseFontWeight(localizedFontWeightOverride); + m_uiTextFontScaleFactorOverride = _wtof(localizedUITextFontSizeFactorOverride->Data()); + m_uiCaptionFontScaleFactorOverride = _wtof(localizedUICaptionFontSizeFactorOverride->Data()); + } + + m_fontGroup = ref new LanguageFontGroup(m_language); +} + +FontWeight LocalizationService::ParseFontWeight(String^ fontWeight) +{ + wstring weight = fontWeight->Data(); + transform(weight.begin(), weight.end(), weight.begin(), towlower); + fontWeight = ref new String(weight.c_str()); + + if (fontWeight == "black") + { + return FontWeights::Black; + } + else if (fontWeight == "bold") + { + return FontWeights::Bold; + } + else if (fontWeight == "extrablack") + { + return FontWeights::ExtraBlack; + } + else if (fontWeight == "extrabold") + { + return FontWeights::ExtraBold; + } + else if (fontWeight == "extralight") + { + return FontWeights::ExtraLight; + } + else if (fontWeight == "light") + { + return FontWeights::Light; + } + else if (fontWeight == "medium") + { + return FontWeights::Medium; + } + else if (fontWeight == "normal") + { + return FontWeights::Normal; + } + else if (fontWeight == "semibold") + { + return FontWeights::SemiBold; + } + else if (fontWeight == "semilight") + { + return FontWeights::SemiLight; + } + else if (fontWeight == "thin") + { + return FontWeights::Thin; + } + else + { + throw invalid_argument("Invalid argument: fontWeight"); + } +} + +FlowDirection LocalizationService::GetFlowDirection() +{ + return m_flowDirection; +} + +bool LocalizationService::IsRtlLayout() +{ + return m_flowDirection == FlowDirection::RightToLeft; +} + +String^ LocalizationService::GetLanguage() +{ + return m_language; +} + +bool LocalizationService::GetOverrideFontApiValues() +{ + return m_overrideFontApiValues; +} + +FontFamily^ LocalizationService::GetLanguageFontFamilyForType(LanguageFontType fontType) +{ + if (m_overrideFontApiValues) + { + return ref new FontFamily(m_fontFamilyOverride); + } + else + { + return ref new FontFamily(GetLanguageFont(fontType)->FontFamily); + } +} + +LanguageFont^ LocalizationService::GetLanguageFont(LanguageFontType fontType) +{ + assert(!m_overrideFontApiValues); + assert(m_fontGroup); + + switch (fontType) + { + case LanguageFontType::UIText: + return m_fontGroup->UITextFont; + case LanguageFontType::UICaption: + return m_fontGroup->UICaptionFont; + default: + throw std::invalid_argument("fontType"); + } +} + +String^ LocalizationService::GetFontFamilyOverride() +{ + assert(m_overrideFontApiValues); + return m_fontFamilyOverride; +} + +FontWeight LocalizationService::GetFontWeightOverride() +{ + assert(m_overrideFontApiValues); + return m_fontWeightOverride; +} + +double LocalizationService::GetFontScaleFactorOverride(LanguageFontType fontType) +{ + assert(m_overrideFontApiValues); + + switch (fontType) + { + case LanguageFontType::UIText: + return m_uiTextFontScaleFactorOverride; + case LanguageFontType::UICaption: + return m_uiCaptionFontScaleFactorOverride; + default: + throw invalid_argument("Invalid argument: fontType"); + } +} + +void LocalizationService::OnFontTypePropertyChanged(DependencyObject^ target, LanguageFontType /*oldValue*/, LanguageFontType /*newValue*/) +{ + UpdateFontFamilyAndSize(target); +} + +void LocalizationService::OnFontWeightPropertyChanged(DependencyObject^ target, FontWeight /*oldValue*/, FontWeight /*newValue*/) +{ + UpdateFontFamilyAndSize(target); +} + +void LocalizationService::OnFontSizePropertyChanged(DependencyObject^ target, double /*oldValue*/, double /*newValue*/) +{ + UpdateFontFamilyAndSize(target); +} + +void LocalizationService::UpdateFontFamilyAndSize(DependencyObject^ target) +{ + FontFamily^ fontFamily; + FontWeight fontWeight; + bool fOverrideFontWeight = false; + double scaleFactor; + + auto service = LocalizationService::GetInstance(); + auto fontType = LocalizationService::GetFontType(target); + + if (service->GetOverrideFontApiValues()) + { + fontFamily = ref new FontFamily(service->GetFontFamilyOverride()); + scaleFactor = service->GetFontScaleFactorOverride(fontType) / 100.0; + fontWeight = service->GetFontWeightOverride(); + fOverrideFontWeight = true; + } + else + { + auto languageFont = service->GetLanguageFont(fontType); + fontFamily = ref new FontFamily(languageFont->FontFamily); + scaleFactor = languageFont->ScaleFactor / 100.0; + } + + double sizeToUse = LocalizationService::GetFontSize(target) * scaleFactor; + + auto control = dynamic_cast(target); + if (control) + { + control->FontFamily = fontFamily; + if (fOverrideFontWeight) + { + control->FontWeight = fontWeight; + } + if (sizeToUse != 0.0) + { + control->FontSize = sizeToUse; + } + else + { + control->ClearValue(Control::FontSizeProperty); + } + } + else + { + auto textBlock = dynamic_cast(target); + if (textBlock) + { + textBlock->FontFamily = fontFamily; + if (fOverrideFontWeight) + { + textBlock->FontWeight = fontWeight; + } + if (sizeToUse != 0.0) + { + textBlock->FontSize = sizeToUse; + } + else + { + textBlock->ClearValue(TextBlock::FontSizeProperty); + } + } + else + { + RichTextBlock^ richTextBlock = dynamic_cast(target); + if (richTextBlock) + { + richTextBlock->FontFamily = fontFamily; + if (fOverrideFontWeight) + { + richTextBlock->FontWeight = fontWeight; + } + if (sizeToUse != 0.0) + { + richTextBlock->FontSize = sizeToUse; + } + else + { + richTextBlock->ClearValue(RichTextBlock::FontSizeProperty); + } + } + else + { + TextElement^ textElement = dynamic_cast(target); + if (textElement) + { + textElement->FontFamily = fontFamily; + if (fOverrideFontWeight) + { + textElement->FontWeight = fontWeight; + } + if (sizeToUse != 0.0) + { + textElement->FontSize = sizeToUse; + } + else + { + textElement->ClearValue(TextElement::FontSizeProperty); + } + } + } + } + } +} + +// If successful, returns a formatter that respects the user's regional format settings, +// as configured by running intl.cpl. +DecimalFormatter^ LocalizationService::GetRegionalSettingsAwareDecimalFormatter() +{ + IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); + if (languageIdentifiers != nullptr) + { + return ref new DecimalFormatter(languageIdentifiers, GlobalizationPreferences::HomeGeographicRegion); + } + + return ref new DecimalFormatter(); +} + +// If successful, returns a formatter that respects the user's regional format settings, +// as configured by running intl.cpl. +// +// This helper function creates a DateTimeFormatter with a TwentyFour hour clock +DateTimeFormatter^ LocalizationService::GetRegionalSettingsAwareDateTimeFormatter(_In_ String^ format) +{ + IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); + if (languageIdentifiers == nullptr) + { + languageIdentifiers = ApplicationLanguages::Languages; + } + + return ref new DateTimeFormatter(format, languageIdentifiers); +} + +// If successful, returns a formatter that respects the user's regional format settings, +// as configured by running intl.cpl. +DateTimeFormatter^ LocalizationService::GetRegionalSettingsAwareDateTimeFormatter( + _In_ String^ format, + _In_ String^ calendarIdentifier, + _In_ String^ clockIdentifier) +{ + IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); + if (languageIdentifiers == nullptr) + { + languageIdentifiers = ApplicationLanguages::Languages; + } + + return ref new DateTimeFormatter( + format, + languageIdentifiers, + GlobalizationPreferences::HomeGeographicRegion, + calendarIdentifier, + clockIdentifier); +} + +CurrencyFormatter^ LocalizationService::GetRegionalSettingsAwareCurrencyFormatter() +{ + String^ userCurrency = (GlobalizationPreferences::Currencies->Size > 0) + ? GlobalizationPreferences::Currencies->GetAt(0) + : StringReference(DefaultCurrencyCode.data()); + + IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); + if (languageIdentifiers == nullptr) + { + languageIdentifiers = ApplicationLanguages::Languages; + } + + auto currencyFormatter = ref new CurrencyFormatter( + userCurrency, + languageIdentifiers, + GlobalizationPreferences::HomeGeographicRegion); + + int fractionDigits = LocalizationSettings::GetInstance().GetCurrencyTrailingDigits(); + currencyFormatter->FractionDigits = fractionDigits; + + return currencyFormatter; +} + +IIterable^ LocalizationService::GetLanguageIdentifiers() +{ + WCHAR currentLocale[LOCALE_NAME_MAX_LENGTH] = {}; + int result = GetUserDefaultLocaleName(currentLocale, LOCALE_NAME_MAX_LENGTH); + if (result != 0) + { + // GetUserDefaultLocaleName may return an invalid bcp47 language tag with trailing non-BCP47 friendly characters, + // which if present would start with an underscore, for example sort order + // (see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373814(v=vs.85).aspx). + // Therefore, if there is an underscore in the locale name, trim all characters from the underscore onwards. + WCHAR* underscore = wcschr(currentLocale, L'_'); + if (underscore != nullptr) + { + *underscore = L'\0'; + } + + String^ localeString = ref new String(currentLocale); + // validate if the locale we have is valid + // otherwise we fallback to the default. + if (Language::IsWellFormed(localeString)) + { + auto languageList = ref new Vector(); + languageList->Append(localeString); + return languageList; + } + } + + return nullptr; +} + +unordered_map LocalizationService::GetTokenToReadableNameMap() +{ + // Resources for the engine use numbers as keys. It's inconvenient, but also difficult to + // change given that the engine heavily relies on perfect ordering of certain elements. + // To compromise, we'll declare a map from engine resource key to automation name from the + // standard project resources. + static vector> s_parenEngineKeyResourceMap = { + // Sine permutations + make_pair(L"67", L"SineDegrees"), + make_pair(L"73", L"SineRadians"), + make_pair(L"79", L"SineGradians"), + make_pair(L"70", L"InverseSineDegrees"), + make_pair(L"76", L"InverseSineRadians"), + make_pair(L"82", L"InverseSineGradians"), + make_pair(L"25", L"HyperbolicSine"), + make_pair(L"85", L"InverseHyperbolicSine"), + + // Cosine permutations + make_pair(L"68", L"CosineDegrees"), + make_pair(L"74", L"CosineRadians"), + make_pair(L"80", L"CosineGradians"), + make_pair(L"71", L"InverseCosineDegrees"), + make_pair(L"77", L"InverseCosineRadians"), + make_pair(L"83", L"InverseCosineGradians"), + make_pair(L"26", L"HyperbolicCosine"), + make_pair(L"86", L"InverseHyperbolicCosine"), + + // Tangent permutations + make_pair(L"69", L"TangentDegrees"), + make_pair(L"75", L"TangentRadians"), + make_pair(L"81", L"TangentGradians"), + make_pair(L"72", L"InverseTangentDegrees"), + make_pair(L"78", L"InverseTangentRadians"), + make_pair(L"84", L"InverseTangentGradians"), + make_pair(L"27", L"HyperbolicTangent"), + make_pair(L"87", L"InverseHyperbolicTangent"), + + // Miscellaneous Scientific functions + make_pair(L"94", L"Factorial"), + make_pair(L"35", L"DegreeMinuteSecond"), + make_pair(L"28", L"NaturalLog"), + make_pair(L"91", L"Square") + }; + + static vector> s_noParenEngineKeyResourceMap = { + // Programmer mode functions + make_pair(L"9", L"LeftShift"), + make_pair(L"10", L"RightShift"), + + // Y Root scientific function + make_pair(L"16", L"YRoot") + }; + + unordered_map tokenToReadableNameMap{}; + auto resProvider = AppResourceProvider::GetInstance(); + + static const wstring openParen = resProvider.GetCEngineString(StringReference(s_openParenResourceKey))->Data(); + + for (const auto& keyPair : s_parenEngineKeyResourceMap) + { + wstring engineStr = resProvider.GetCEngineString(StringReference(keyPair.first.c_str()))->Data(); + wstring automationName = resProvider.GetResourceString(StringReference(keyPair.second.c_str()))->Data(); + + tokenToReadableNameMap.emplace(engineStr + openParen, automationName); + } + s_parenEngineKeyResourceMap.clear(); + + for (const auto& keyPair : s_noParenEngineKeyResourceMap) + { + wstring engineStr = resProvider.GetCEngineString(StringReference(keyPair.first.c_str()))->Data(); + wstring automationName = resProvider.GetResourceString(StringReference(keyPair.second.c_str()))->Data(); + + tokenToReadableNameMap.emplace(engineStr, automationName); + } + s_noParenEngineKeyResourceMap.clear(); + + // Also replace hyphens with "minus" + wstring minusText = resProvider.GetResourceString(L"minus")->Data(); + tokenToReadableNameMap.emplace(L"-", minusText); + + return tokenToReadableNameMap; +} + +String^ LocalizationService::GetNarratorReadableToken(String^ rawToken) +{ + static unordered_map s_tokenToReadableNameMap = GetTokenToReadableNameMap(); + + auto itr = s_tokenToReadableNameMap.find(rawToken->Data()); + if (itr == s_tokenToReadableNameMap.end()) + { + return rawToken; + } + else + { + static const String^ openParen = AppResourceProvider::GetInstance().GetCEngineString(StringReference(s_openParenResourceKey)); + return ref new String(itr->second.c_str()) + L" " + openParen; + } +} + +String^ LocalizationService::GetNarratorReadableString(String^ rawString) +{ + wstringstream readableString{}; + readableString << L""; + + wstring asWstring = rawString->Data(); + for (const auto& c : asWstring) + { + readableString << LocalizationService::GetNarratorReadableToken(L"" + c)->Data(); + } + + return ref new String(readableString.str().c_str()); +} diff --git a/src/CalcViewModel/Common/LocalizationService.h b/src/CalcViewModel/Common/LocalizationService.h new file mode 100644 index 00000000..1afdbb9b --- /dev/null +++ b/src/CalcViewModel/Common/LocalizationService.h @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp { namespace Common +{ + namespace LocalizationServiceProperties + { + static constexpr std::wstring_view DefaultCurrencyCode{ L"USD" }; + } + + public enum class LanguageFontType + { + UIText, + UICaption, + }; + + public ref class LocalizationService sealed + { + public: + + DEPENDENCY_PROPERTY_OWNER(LocalizationService); + + DEPENDENCY_PROPERTY_ATTACHED_WITH_DEFAULT_AND_CALLBACK(LanguageFontType, FontType, LanguageFontType::UIText); + DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(double, FontSize); + + internal: + static LocalizationService^ GetInstance(); + + Windows::UI::Xaml::FlowDirection GetFlowDirection(); + bool IsRtlLayout(); + bool GetOverrideFontApiValues(); + Platform::String^ GetLanguage(); + Windows::UI::Xaml::Media::FontFamily^ GetLanguageFontFamilyForType(LanguageFontType fontType); + Platform::String^ GetFontFamilyOverride(); + Windows::UI::Text::FontWeight GetFontWeightOverride(); + double GetFontScaleFactorOverride(LanguageFontType fontType); + + static Windows::Globalization::NumberFormatting::DecimalFormatter^ GetRegionalSettingsAwareDecimalFormatter(); + static Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ GetRegionalSettingsAwareDateTimeFormatter(_In_ Platform::String^ format); + static Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ GetRegionalSettingsAwareDateTimeFormatter( + _In_ Platform::String^ format, + _In_ Platform::String^ calendarIdentifier, + _In_ Platform::String^ clockIdentifier); + + static Windows::Globalization::NumberFormatting::CurrencyFormatter^ GetRegionalSettingsAwareCurrencyFormatter(); + + static Platform::String^ GetNarratorReadableToken(Platform::String^ rawToken); + static Platform::String^ GetNarratorReadableString(Platform::String^ rawString); + + private: + Windows::Globalization::Fonts::LanguageFont^ GetLanguageFont(LanguageFontType fontType); + Windows::UI::Text::FontWeight ParseFontWeight(Platform::String^ fontWeight); + + static Windows::Foundation::Collections::IIterable^ GetLanguageIdentifiers(); + + // Attached property callbacks + static void OnFontTypePropertyChanged(Windows::UI::Xaml::DependencyObject^ target, LanguageFontType oldValue, LanguageFontType newValue); + static void OnFontWeightPropertyChanged(Windows::UI::Xaml::DependencyObject^ target, Windows::UI::Text::FontWeight oldValue, Windows::UI::Text::FontWeight newValue); + static void OnFontSizePropertyChanged(Windows::UI::Xaml::DependencyObject^ target, double oldValue, double newValue); + + static void UpdateFontFamilyAndSize(Windows::UI::Xaml::DependencyObject^ target); + + static std::unordered_map GetTokenToReadableNameMap(); + + private: + LocalizationService(); + + static LocalizationService^ s_singletonInstance; + + Windows::Globalization::Fonts::LanguageFontGroup^ m_fontGroup; + Platform::String^ m_language; + Windows::UI::Xaml::FlowDirection m_flowDirection; + bool m_overrideFontApiValues; + Platform::String^ m_fontFamilyOverride; + Windows::UI::Text::FontWeight m_fontWeightOverride; + double m_uiTextFontScaleFactorOverride; + double m_uiCaptionFontScaleFactorOverride; + }; + +}} + diff --git a/src/CalcViewModel/Common/LocalizationSettings.h b/src/CalcViewModel/Common/LocalizationSettings.h new file mode 100644 index 00000000..fe49a529 --- /dev/null +++ b/src/CalcViewModel/Common/LocalizationSettings.h @@ -0,0 +1,385 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "LocalizationService.h" + +namespace CalculatorApp +{ + namespace Common + { + class LocalizationSettings + { + private: + LocalizationSettings() + { + int result = 0; + + // Use DecimalFormatter as it respects the locale and the user setting + Windows::Globalization::NumberFormatting::DecimalFormatter^ formatter; + formatter = CalculatorApp::Common::LocalizationService::GetRegionalSettingsAwareDecimalFormatter(); + formatter->FractionDigits = 0; + formatter->IsDecimalPointAlwaysDisplayed = false; + + for (unsigned int i = 0; i < 10; i++) + { + m_digitSymbols.at(i) = formatter->FormatUInt(i)->Data()[0]; + } + + result = ResolveLocaleName(formatter->ResolvedLanguage->Data(), + m_resolvedName, + LOCALE_NAME_MAX_LENGTH); + if (result == 0) + { + throw std::runtime_error("Unexpected error resolving locale name"); + } + else + { + wchar_t decimalString[LocaleSettingBufferSize] = L""; + result = GetLocaleInfoEx(m_resolvedName, + LOCALE_SDECIMAL, + decimalString, + ARRAYSIZE(decimalString)); + if (result == 0) + { + throw std::runtime_error("Unexpected error while getting locale info"); + } + + wchar_t groupingSymbolString[LocaleSettingBufferSize] = L""; + result = GetLocaleInfoEx(m_resolvedName, + LOCALE_STHOUSAND, + groupingSymbolString, + ARRAYSIZE(groupingSymbolString)); + if (result == 0) + { + throw std::runtime_error("Unexpected error while getting locale info"); + } + + wchar_t numberGroupingString[LocaleSettingBufferSize] = L""; + result = GetLocaleInfoEx(m_resolvedName, + LOCALE_SGROUPING, + numberGroupingString, + ARRAYSIZE(numberGroupingString)); + if (result == 0) + { + throw std::runtime_error("Unexpected error while getting locale info"); + } + + // Get locale info for List Separator, eg. comma is used in many locales + wchar_t listSeparatorString[4] = L""; + result = ::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, + LOCALE_SLIST, + listSeparatorString, + ARRAYSIZE(listSeparatorString)); // Max length of the expected return value is 4 + if (result == 0) + { + throw std::runtime_error("Unexpected error while getting locale info"); + } + + int currencyTrailingDigits = 0; + result = GetLocaleInfoEx(m_resolvedName, + LOCALE_ICURRDIGITS | LOCALE_RETURN_NUMBER, + (LPWSTR)¤cyTrailingDigits, + sizeof(currencyTrailingDigits) / sizeof(WCHAR)); + if (result == 0) + { + throw std::runtime_error("Unexpected error while getting locale info"); + } + + // Currency symbol precedence is either 0 or 1. + // A value of 0 indicates the symbol follows the currency value. + int currencySymbolPrecedence = 1; + result = GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, + LOCALE_IPOSSYMPRECEDES | LOCALE_RETURN_NUMBER, + (LPWSTR)¤cySymbolPrecedence, + sizeof(currencySymbolPrecedence) / sizeof(WCHAR)); + + // As CalcEngine only supports the first character of the decimal separator, + // Only first character of the decimal separator string is supported. + m_decimalSeparator = decimalString[0]; + m_numberGroupSeparator = groupingSymbolString[0]; + m_numberGrouping = numberGroupingString; + m_listSeparator = listSeparatorString; + m_currencyTrailingDigits = currencyTrailingDigits; + m_currencySymbolPrecedence = currencySymbolPrecedence; + } + + // Get the system calendar type + // Note: This function returns 0 on failure. + // We'll ignore the failure in that case and the CalendarIdentifier would get set to GregorianCalendar. + CALID calId; + ::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, + LOCALE_ICALENDARTYPE | LOCALE_RETURN_NUMBER, + reinterpret_cast(&calId), + sizeof(calId)); + + m_calendarIdentifier = GetCalendarIdentifierFromCalid(calId); + + // Get FirstDayOfWeek Date and Time setting + wchar_t day[80] = L""; + ::GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, + LOCALE_IFIRSTDAYOFWEEK, // The first day in a week + reinterpret_cast(day), // Argument is of type PWSTR + ARRAYSIZE(day)); // Max return size are 80 characters + + // The LOCALE_IFIRSTDAYOFWEEK integer value varies from 0, 1, .. 6 for Monday, Tuesday, ... Sunday + // DayOfWeek enum value varies from 0, 1, .. 6 for Sunday, Monday, ... Saturday + // Hence, DayOfWeek = (valueof(LOCALE_IFIRSTDAYOFWEEK) + 1) % 7 + m_firstDayOfWeek = static_cast((_wtoi(day) + 1) % 7); // static cast int to DayOfWeek enum + } + + public: + // A LocalizationSettings object is not copyable. + LocalizationSettings(const LocalizationSettings&) = delete; + LocalizationSettings& operator=(const LocalizationSettings&) = delete; + + // A LocalizationSettings object is not moveable. + LocalizationSettings(LocalizationSettings&&) = delete; + LocalizationSettings& operator=(LocalizationSettings&&) = delete; + + // Provider of the singleton LocalizationSettings instance. + static const LocalizationSettings& GetInstance() + { + static const LocalizationSettings localizationSettings; + + return localizationSettings; + } + + Platform::String^ GetLocaleName() const + { + return ref new Platform::String(m_resolvedName); + } + + bool IsDigitEnUsSetting() const + { + if (this->GetDigitSymbolFromEnUsDigit('0') == L'0') + { + return true; + } + return false; + } + + void LocalizeDisplayValue(_Inout_ std::wstring* stringToLocalize) const + { + if (IsDigitEnUsSetting()) + { + return; + } + + for (wchar_t& ch : *stringToLocalize) + { + if (IsEnUsDigit(ch)) + { + ch = GetDigitSymbolFromEnUsDigit(ch); + } + } + } + + Platform::String^ GetEnglishValueFromLocalizedDigits(const std::wstring& localizedString) const + { + if (m_resolvedName == L"en-US") + { + return ref new Platform::String(localizedString.c_str()); + } + + size_t i = 0; + size_t length = localizedString.size(); + std::unique_ptr englishString(new wchar_t[length + 1]); // +1 for the null termination + + for (; i < length; ++i) + { + wchar_t ch = localizedString[i]; + if (!IsEnUsDigit(ch)) + { + for (int j = 0; j < 10; ++j) + { + if (ch == m_digitSymbols[j]) + { + ch = j.ToString()->Data()[0]; + break; + //ch = val - L'0'; + } + } + } + if (ch == m_decimalSeparator) + { + ch = L'.'; + } + englishString[i] = ch; + } + englishString[i] = '\0'; + + return ref new Platform::String(englishString.get()); + } + + bool IsEnUsDigit(const wchar_t digit) const + { + if (digit >= L'0' && digit <= L'9') + { + return true; + } + return false; + } + + bool IsLocalizedDigit(const wchar_t digit) const + { + for (auto dig : m_digitSymbols) + { + if (digit == dig) + { + return true; + } + } + return false; + } + + bool IsLocalizedHexDigit(const wchar_t digit) const + { + if (IsLocalizedDigit(digit)) + { + return true; + } + + for (auto dig : s_hexSymbols) + { + if (digit == dig) + { + return true; + } + } + + return false; + } + + wchar_t GetDigitSymbolFromEnUsDigit(wchar_t digitSymbol) const + { + assert(digitSymbol >= L'0' && digitSymbol <= L'9'); + int digit = digitSymbol - L'0'; + return m_digitSymbols.at(digit); // throws on out of range + } + + wchar_t GetDecimalSeparator() const + { + return m_decimalSeparator; + } + + wchar_t GetNumberGroupSeparator() const + { + return m_numberGroupSeparator; + } + + std::wstring GetDecimalSeparatorStr() const + { + std::wstring result; + result.push_back(m_decimalSeparator); + return result; + } + + std::wstring GetNumberGroupingSeparatorStr() const + { + std::wstring result; + result.push_back(m_numberGroupSeparator); + return result; + } + + std::wstring GetNumberGroupingStr() const + { + return m_numberGrouping; + } + + void RemoveGroupSeparators(const wchar_t* value, const size_t length, std::wstring* rawValue) const + { + rawValue->clear(); + rawValue->reserve(length); + + for (size_t i = 0; i < length; i++) + { + if (value[i] != L' ' && value[i] != m_numberGroupSeparator) + { + rawValue->append(1, value[i]); + } + } + } + + Platform::String^ GetCalendarIdentifier() const + { + return m_calendarIdentifier; + } + + std::wstring GetListSeparator() const + { + return m_listSeparator; + } + + Windows::Globalization::DayOfWeek GetFirstDayOfWeek() const + { + return m_firstDayOfWeek; + } + + int GetCurrencyTrailingDigits() const + { + return m_currencyTrailingDigits; + } + + int GetCurrencySymbolPrecedence() const + { + return m_currencySymbolPrecedence; + } + + private: + static Platform::String^ GetCalendarIdentifierFromCalid(CALID calId) + { + switch (calId) + { + case CAL_GREGORIAN: + case CAL_GREGORIAN_ARABIC: + case CAL_GREGORIAN_ME_FRENCH: + case CAL_GREGORIAN_US: + case CAL_GREGORIAN_XLIT_ENGLISH: + case CAL_GREGORIAN_XLIT_FRENCH: + return Windows::Globalization::CalendarIdentifiers::Gregorian; + + case CAL_HEBREW: + return Windows::Globalization::CalendarIdentifiers::Hebrew; + + case CAL_HIJRI: + case CAL_PERSIAN: + return Windows::Globalization::CalendarIdentifiers::Hijri; + + case CAL_JAPAN: + return Windows::Globalization::CalendarIdentifiers::Japanese; + + case CAL_KOREA: + return Windows::Globalization::CalendarIdentifiers::Korean; + + case CAL_TAIWAN: + return Windows::Globalization::CalendarIdentifiers::Taiwan; + + case CAL_THAI: + return Windows::Globalization::CalendarIdentifiers::Thai; + + case CAL_UMALQURA: + return Windows::Globalization::CalendarIdentifiers::UmAlQura; + + // Gregorian will be the default Calendar Type + default: + return Windows::Globalization::CalendarIdentifiers::Gregorian; + } + } + + wchar_t m_decimalSeparator; + wchar_t m_numberGroupSeparator; + std::wstring m_numberGrouping; + std::array m_digitSymbols; + // Hexadecimal characters are not currently localized + static constexpr std::array s_hexSymbols{ L'A', L'B', L'C', L'D', L'E', L'F' }; + std::wstring m_listSeparator; + Platform::String^ m_calendarIdentifier; + Windows::Globalization::DayOfWeek m_firstDayOfWeek; + int m_currencySymbolPrecedence; + wchar_t m_resolvedName[LOCALE_NAME_MAX_LENGTH]; + int m_currencyTrailingDigits; + static const unsigned int LocaleSettingBufferSize = 16; + }; + } +} diff --git a/src/CalcViewModel/Common/LocalizationStringUtil.h b/src/CalcViewModel/Common/LocalizationStringUtil.h new file mode 100644 index 00000000..f3b69413 --- /dev/null +++ b/src/CalcViewModel/Common/LocalizationStringUtil.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "AppResourceProvider.h" + +namespace CalculatorApp +{ + namespace Common + { + class LocalizationStringUtil + { + public: + static std::wstring GetLocalizedString(const wchar_t* pMessage, ...) + { + std::wstring returnString = L""; + const UINT32 length = 1024; + std::unique_ptr spBuffer = std::unique_ptr(new wchar_t[length]); + va_list args = NULL; + va_start(args, pMessage); + DWORD fmtReturnVal = FormatMessage(FORMAT_MESSAGE_FROM_STRING, + pMessage, + 0, + 0, + spBuffer.get(), + length, + &args); + va_end(args); + + if (fmtReturnVal != 0) + { + returnString = spBuffer.get(); + } + + return returnString; + } + + template + static Platform::String^ GetLocalizedNarratorAnnouncement(Platform::String^ resourceKey, Platform::String^& formatVariable, T*... params) + { + EnsureInitialization(resourceKey, formatVariable); + return StringReference(GetLocalizedString(formatVariable->Data(), params...).c_str()); + } + + private: + static void EnsureInitialization(Platform::String^ resourceKey, Platform::String^& formatVariable) + { + if (resourceKey == nullptr || resourceKey->IsEmpty()) + { + return; + } + + // If the formatVariable already has a value, we don't need to set it again. Simply return. + if (formatVariable != nullptr && !formatVariable->IsEmpty()) + { + return; + } + + formatVariable = AppResourceProvider::GetInstance().GetResourceString(resourceKey); + } + }; + } +} diff --git a/src/CalcViewModel/Common/MyVirtualKey.h b/src/CalcViewModel/Common/MyVirtualKey.h new file mode 100644 index 00000000..4a69b95c --- /dev/null +++ b/src/CalcViewModel/Common/MyVirtualKey.h @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + public enum class MyVirtualKey + { + None = 0, + LeftButton = 1, + RightButton = 2, + Cancel = 3, + MiddleButton = 4, + XButton1 = 5, + XButton2 = 6, + Back = 8, + Tab = 9, + Clear = 12, + Enter = 13, + Shift = 16, + Control = 17, + Menu = 18, + Pause = 19, + CapitalLock = 20, + Kana = 21, + Hangul = 21, + Junja = 23, + Final = 24, + Hanja = 25, + Kanji = 25, + Escape = 27, + Convert = 28, + NonConvert = 29, + Accept = 30, + ModeChange = 31, + Space = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + Left = 37, + Up = 38, + Right = 39, + Down = 40, + Select = 41, + Print = 42, + Execute = 43, + Snapshot = 44, + Insert = 45, + Delete = 46, + Help = 47, + Number0 = 48, + Number1 = 49, + Number2 = 50, + Number3 = 51, + Number4 = 52, + Number5 = 53, + Number6 = 54, + Number7 = 55, + Number8 = 56, + Number9 = 57, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftWindows = 91, + RightWindows = 92, + Application = 93, + Sleep = 95, + NumberPad0 = 96, + NumberPad1 = 97, + NumberPad2 = 98, + NumberPad3 = 99, + NumberPad4 = 100, + NumberPad5 = 101, + NumberPad6 = 102, + NumberPad7 = 103, + NumberPad8 = 104, + NumberPad9 = 105, + Multiply = 106, + Add = 107, + Separator = 108, + Subtract = 109, + Decimal = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + F13 = 124, + F14 = 125, + F15 = 126, + F16 = 127, + F17 = 128, + F18 = 129, + F19 = 130, + F20 = 131, + F21 = 132, + F22 = 133, + F23 = 134, + F24 = 135, + NumberKeyLock = 144, + Scroll = 145, + LeftShift = 160, + RightShift = 161, + LeftControl = 162, + RightControl = 163, + LeftMenu = 164, + RightMenu = 165 + }; + } +} diff --git a/src/CalcViewModel/Common/NavCategory.cpp b/src/CalcViewModel/Common/NavCategory.cpp new file mode 100644 index 00000000..83980a1b --- /dev/null +++ b/src/CalcViewModel/Common/NavCategory.cpp @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NavCategory.h" +#include "AppResourceProvider.h" +#include "Common\LocalizationStringUtil.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Foundation::Collections; + +namespace UCM = UnitConversionManager; + +// Calculator categories always support negative and positive. +static constexpr bool SUPPORTS_ALL = true; + +// Converter categories usually only support positive. +static constexpr bool SUPPORTS_NEGATIVE = true; +static constexpr bool POSITIVE_ONLY = false; + +// The order of items in this list determines the order of groups in the menu. +static constexpr array s_categoryGroupManifest = { + NavCategoryGroupInitializer { CategoryGroupType::Calculator, L"CalculatorModeTextCaps", L"CalculatorModeText", L"CalculatorModePluralText"}, + NavCategoryGroupInitializer { CategoryGroupType::Converter, L"ConverterModeTextCaps", L"ConverterModeText", L"ConverterModePluralText" } +}; + +// vvv THESE CONSTANTS SHOULD NEVER CHANGE vvv +static constexpr int STANDARD_ID = 0; +static constexpr int SCIENTIFIC_ID = 1; +static constexpr int PROGRAMMER_ID = 2; +static constexpr int DATE_ID = 3; +static constexpr int VOLUME_ID = 4; +static constexpr int LENGTH_ID = 5; +static constexpr int WEIGHT_ID = 6; +static constexpr int TEMPERATURE_ID = 7; +static constexpr int ENERGY_ID = 8; +static constexpr int AREA_ID = 9; +static constexpr int SPEED_ID = 10; +static constexpr int TIME_ID = 11; +static constexpr int POWER_ID = 12; +static constexpr int DATA_ID = 13; +static constexpr int PRESSURE_ID = 14; +static constexpr int ANGLE_ID = 15; +static constexpr int CURRENCY_ID = 16; +// ^^^ THESE CONSTANTS SHOULD NEVER CHANGE ^^^ + +// The order of items in this list determines the order of items in the menu. +static constexpr array s_categoryManifest = { + NavCategoryInitializer { ViewMode::Standard, STANDARD_ID, L"Standard", L"StandardMode", L"\uE8EF", CategoryGroupType::Calculator, MyVirtualKey::Number1, SUPPORTS_ALL }, + NavCategoryInitializer { ViewMode::Scientific, SCIENTIFIC_ID, L"Scientific", L"ScientificMode", L"\uF196", CategoryGroupType::Calculator, MyVirtualKey::Number2, SUPPORTS_ALL }, + NavCategoryInitializer { ViewMode::Programmer, PROGRAMMER_ID, L"Programmer", L"ProgrammerMode", L"\uECCE", CategoryGroupType::Calculator, MyVirtualKey::Number3, SUPPORTS_ALL }, + NavCategoryInitializer { ViewMode::Date, DATE_ID, L"Date", L"DateCalculationMode", L"\uE787", CategoryGroupType::Calculator, MyVirtualKey::Number4, SUPPORTS_ALL }, + NavCategoryInitializer { ViewMode::Currency, CURRENCY_ID, L"Currency", L"CategoryName_Currency", L"\uEB0D", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Volume, VOLUME_ID, L"Volume", L"CategoryName_Volume", L"\uF1AA", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Length, LENGTH_ID, L"Length", L"CategoryName_Length", L"\uECC6", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Weight, WEIGHT_ID, L"Weight and Mass", L"CategoryName_Weight", L"\uF4C1", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Temperature, TEMPERATURE_ID, L"Temperature", L"CategoryName_Temperature", L"\uE7A3", CategoryGroupType::Converter, MyVirtualKey::None, SUPPORTS_NEGATIVE }, + NavCategoryInitializer { ViewMode::Energy, ENERGY_ID, L"Energy", L"CategoryName_Energy", L"\uECAD", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Area, AREA_ID, L"Area", L"CategoryName_Area", L"\uE809", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Speed, SPEED_ID, L"Speed", L"CategoryName_Speed", L"\uEADA", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Time, TIME_ID, L"Time", L"CategoryName_Time", L"\uE917", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Power, POWER_ID, L"Power", L"CategoryName_Power", L"\uE945", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Data, DATA_ID, L"Data", L"CategoryName_Data", L"\uF20F", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Pressure, PRESSURE_ID, L"Pressure", L"CategoryName_Pressure", L"\uEC4A", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY }, + NavCategoryInitializer { ViewMode::Angle, ANGLE_ID, L"Angle", L"CategoryName_Angle", L"\uF515", CategoryGroupType::Converter, MyVirtualKey::None, POSITIVE_ONLY } +}; + +// This function should only be used when storing the mode to app data. +int NavCategory::Serialize(ViewMode mode) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? iter->serializationId + : -1; +} + +// This function should only be used when restoring the mode from app data. +ViewMode NavCategory::Deserialize(Platform::Object^ obj) +{ + // If we cast directly to ViewMode we will fail + // because we technically store an int. + // Need to cast to int, then ViewMode. + auto boxed = dynamic_cast^>(obj); + if (boxed != nullptr) + { + int serializationId = boxed->Value; + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [serializationId](const NavCategoryInitializer& initializer) + { + return initializer.serializationId == serializationId; + }); + + if (iter != s_categoryManifest.end()) + { + return iter->viewMode; + } + } + + return ViewMode::None; +} + +bool NavCategory::IsValidViewMode(ViewMode mode) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode; + }); + + return iter != s_categoryManifest.end(); +} + +bool NavCategory::IsCalculatorViewMode(ViewMode mode) +{ + // Historically, Date Calculator is not a Calculator mode + // even though it is in the Calculator category. + return !IsDateCalculatorViewMode(mode) && IsModeInCategoryGroup(mode, CategoryGroupType::Calculator); +} + +bool NavCategory::IsDateCalculatorViewMode(ViewMode mode) +{ + return mode == ViewMode::Date; +} + +bool NavCategory::IsConverterViewMode(ViewMode mode) +{ + return IsModeInCategoryGroup(mode, CategoryGroupType::Converter); +} + +bool NavCategory::IsModeInCategoryGroup(ViewMode mode, CategoryGroupType type) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode, type](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode && initializer.groupType == type; + }); + + return iter != s_categoryManifest.end(); +} + +String^ NavCategory::GetFriendlyName(ViewMode mode) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? StringReference(iter->friendlyName) + : L"None"; +} + +ViewMode NavCategory::GetViewModeForFriendlyName(String^ name) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [name](const NavCategoryInitializer& initializer) + { + return wcscmp(initializer.friendlyName, name->Data()) == 0; + }); + + return (iter != s_categoryManifest.end()) + ? iter->viewMode + : ViewMode::None; +} + +String^ NavCategory::GetNameResourceKey(ViewMode mode) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? StringReference(iter->nameResourceKey) + "Text" + : nullptr; +} + +CategoryGroupType NavCategory::GetGroupType(ViewMode mode) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode](const NavCategoryInitializer& initializer) + { + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? iter->groupType + : CategoryGroupType::None; +} + +// GetIndex is 0-based, GetPostion is 1-based +int NavCategory::GetIndex(ViewMode mode) +{ + int position = NavCategory::GetPosition(mode); + return max(-1, position - 1); +} + +int NavCategory::GetFlatIndex(ViewMode mode) +{ + int index = -1; + CategoryGroupType type = CategoryGroupType::None; + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode, &type, &index](const NavCategoryInitializer& initializer) + { + index++; + if (initializer.groupType != type) + { + type = initializer.groupType; + index++; + } + + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? index + : -1; +} + +// GetIndex is 0-based, GetPostion is 1-based +int NavCategory::GetIndexInGroup(ViewMode mode, CategoryGroupType type) +{ + int index = -1; + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode, type, &index](const NavCategoryInitializer& initializer) + { + if (initializer.groupType == type) + { + index++; + return initializer.viewMode == mode; + } + + return false; + }); + + return (iter != s_categoryManifest.end()) + ? index + : -1; +} + +// GetIndex is 0-based, GetPostion is 1-based +int NavCategory::GetPosition(ViewMode mode) +{ + int position = 0; + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [mode, &position](const NavCategoryInitializer& initializer) + { + position++; + return initializer.viewMode == mode; + }); + + return (iter != s_categoryManifest.end()) + ? position + : -1; +} + +ViewMode NavCategory::GetViewModeForVirtualKey(MyVirtualKey virtualKey) +{ + auto iter = find_if(begin(s_categoryManifest), end(s_categoryManifest), + [virtualKey](const NavCategoryInitializer& initializer) + { + return initializer.virtualKey == virtualKey; + }); + + return (iter != s_categoryManifest.end()) + ? iter->viewMode + : ViewMode::None; +} + +vector NavCategory::GetCategoryAcceleratorKeys() +{ + vector accelerators{}; + for (auto category : s_categoryManifest) + { + if (category.virtualKey != MyVirtualKey::None) + { + accelerators.push_back(category.virtualKey); + } + } + + return accelerators; +} + +NavCategoryGroup::NavCategoryGroup(const NavCategoryGroupInitializer& groupInitializer) : + m_Categories(ref new Vector()) +{ + m_GroupType = groupInitializer.type; + + auto resProvider = AppResourceProvider::GetInstance(); + String^ headerResourceKey = StringReference(groupInitializer.headerResourceKey); + String^ modeResourceKey = StringReference(groupInitializer.modeResourceKey); + String^ automationResourceKey = StringReference(groupInitializer.automationResourceKey); + m_Name = resProvider.GetResourceString(headerResourceKey); + String^ groupMode = resProvider.GetResourceString(modeResourceKey); + String^ automationName = resProvider.GetResourceString(automationResourceKey); + + String^ navCategoryHeaderAutomationNameFormat = resProvider.GetResourceString(L"NavCategoryHeader_AutomationNameFormat"); + m_AutomationName = ref new String(LocalizationStringUtil::GetLocalizedString( + navCategoryHeaderAutomationNameFormat->Data(), + automationName->Data()).c_str()); + + String^ navCategoryItemAutomationNameFormat = resProvider.GetResourceString(L"NavCategoryItem_AutomationNameFormat"); + + for (const NavCategoryInitializer& categoryInitializer : s_categoryManifest) + { + if (categoryInitializer.groupType == groupInitializer.type) + { + String^ nameResourceKey = StringReference(categoryInitializer.nameResourceKey); + String^ categoryName = resProvider.GetResourceString(nameResourceKey + "Text"); + String^ categoryAutomationName = ref new String(LocalizationStringUtil::GetLocalizedString( + navCategoryItemAutomationNameFormat->Data(), + categoryName->Data(), + m_Name->Data()).c_str()); + + m_Categories->Append(ref new NavCategory( + categoryName, + categoryAutomationName, + StringReference(categoryInitializer.glyph), + resProvider.GetResourceString(nameResourceKey + "AccessKey"), + groupMode, + categoryInitializer.viewMode, + categoryInitializer.supportsNegative)); + } + } +} + +IObservableVector^ NavCategoryGroup::CreateMenuOptions() +{ + auto menuOptions = ref new Vector(); + menuOptions->Append(CreateCalculatorCategory()); + menuOptions->Append(CreateConverterCategory()); + return menuOptions; +} + +NavCategoryGroup^ NavCategoryGroup::CreateCalculatorCategory() +{ + return ref new NavCategoryGroup(s_categoryGroupManifest.at(0)); +} + +NavCategoryGroup^ NavCategoryGroup::CreateConverterCategory() +{ + return ref new NavCategoryGroup(s_categoryGroupManifest.at(1)); +} + +vector NavCategoryGroup::GetInitializerCategoryGroup(CategoryGroupType groupType) +{ + vector initializers{}; + copy_if(begin(s_categoryManifest), end(s_categoryManifest), back_inserter(initializers), + [groupType](const NavCategoryInitializer& initializer) + { + return initializer.groupType == groupType; + }); + + return initializers; +} + +String^ NavCategoryGroup::GetHeaderResourceKey(CategoryGroupType type) +{ + auto iter = find_if(begin(s_categoryGroupManifest), end(s_categoryGroupManifest), + [type](const NavCategoryGroupInitializer& initializer) + { + return initializer.type == type; + }); + + return (iter != s_categoryGroupManifest.end()) + ? StringReference(iter->headerResourceKey) + : nullptr; +} diff --git a/src/CalcViewModel/Common/NavCategory.h b/src/CalcViewModel/Common/NavCategory.h new file mode 100644 index 00000000..b8f544fb --- /dev/null +++ b/src/CalcViewModel/Common/NavCategory.h @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* The NavCategory group of classes and enumerations is intended to serve + * as a single location for storing metadata about a navigation mode. + * + * These .h and .cpp files: + * - Define the ViewMode enumeration which is used for setting the mode of the app. + * - Define a list of metadata associated with each ViewMode. + * - Define the order of groups and items in the navigation menu. + * - Provide a static helper function for creating the navigation menu options. + * - Provide static helper functions for querying information about a given ViewMode. + */ + +#pragma once + +namespace CalculatorApp +{ + namespace Common + { + // Don't change the order of these enums + // and definitely don't use int arithmetic + // to change modes. + public enum class ViewMode + { + None = -1, + Standard = 0, + Scientific = 1, + Programmer = 2, + Date = 3, + Volume = 4, + Length = 5, + Weight = 6, + Temperature = 7, + Energy = 8, + Area = 9, + Speed = 10, + Time = 11, + Power = 12, + Data = 13, + Pressure = 14, + Angle = 15, + Currency = 16 + }; + + public enum class CategoryGroupType + { + None = -1, + Calculator = 0, + Converter = 1 + }; + + private struct NavCategoryInitializer + { + constexpr NavCategoryInitializer( + ViewMode mode, + int id, + wchar_t const * name, + wchar_t const * nameKey, + wchar_t const * glyph, + CategoryGroupType group, + CalculatorApp::Common::MyVirtualKey vKey, + bool categorySupportsNegative) + : + viewMode(mode), + serializationId(id), + friendlyName(name), + nameResourceKey(nameKey), + glyph(glyph), + groupType(group), + virtualKey(vKey), + supportsNegative(categorySupportsNegative) + {} + + const ViewMode viewMode; + const int serializationId; + const wchar_t * const friendlyName; + const wchar_t * const nameResourceKey; + const wchar_t * const glyph; + const CategoryGroupType groupType; + const CalculatorApp::Common::MyVirtualKey virtualKey; + const bool supportsNegative; + }; + + private struct NavCategoryGroupInitializer + { + constexpr NavCategoryGroupInitializer(CategoryGroupType t, wchar_t const * h, wchar_t const * n, wchar_t const * a) : + type(t), headerResourceKey(h), modeResourceKey(n), automationResourceKey(a) + {} + + const CategoryGroupType type; + const wchar_t *headerResourceKey; + const wchar_t *modeResourceKey; + const wchar_t *automationResourceKey; + }; + + [Windows::UI::Xaml::Data::Bindable] + public ref class NavCategory sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + OBSERVABLE_OBJECT(); + + property Platform::String^ Name + { + Platform::String^ get() { return m_name; } + } + + property Platform::String^ AutomationName + { + Platform::String^ get() { return m_automationName; } + } + + property Platform::String^ Glyph + { + Platform::String^ get() { return m_glyph; } + } + + property int Position + { + int get() { return m_position; } + } + + property ViewMode Mode + { + ViewMode get() + { + return m_viewMode; + } + } + + property Platform::String^ AutomationId + { + Platform::String^ get() + { + return m_viewMode.ToString(); + } + } + + property Platform::String^ AccessKey + { + Platform::String^ get() + { + return m_accessKey; + } + } + + + property bool SupportsNegative + { + bool get() + { + return m_supportsNegative; + } + } + + // For saving/restoring last mode used. + static int Serialize(ViewMode mode); + static ViewMode Deserialize(Platform::Object^ obj); + static ViewMode GetViewModeForFriendlyName(Platform::String^ name); + + static bool IsValidViewMode(ViewMode mode); + static bool IsCalculatorViewMode(ViewMode mode); + static bool IsDateCalculatorViewMode(ViewMode mode); + static bool IsConverterViewMode(ViewMode mode); + + static Platform::String^ GetFriendlyName(ViewMode mode); + static Platform::String^ GetNameResourceKey(ViewMode mode); + static CategoryGroupType GetGroupType(ViewMode mode); + + // GetIndex is 0-based, GetPostion is 1-based + static int GetIndex(ViewMode mode); + static int GetFlatIndex(ViewMode mode); + static int GetIndexInGroup(ViewMode mode, CategoryGroupType type); + static int GetPosition(ViewMode mode); + + static ViewMode GetViewModeForVirtualKey(CalculatorApp::Common::MyVirtualKey virtualKey); + + internal: + NavCategory(Platform::String^ name, Platform::String^ automationName, Platform::String^ glyph, Platform::String^ accessKey, Platform::String^ mode, ViewMode viewMode, bool supportsNegative) : + m_name(name), + m_automationName(automationName), + m_glyph(glyph), + m_accessKey(accessKey), + m_mode(mode), + m_viewMode(viewMode), + m_supportsNegative(supportsNegative) + { + m_position = NavCategory::GetPosition(m_viewMode); + } + + static std::vector GetCategoryAcceleratorKeys(); + + private: + static bool IsModeInCategoryGroup(ViewMode mode, CategoryGroupType groupType); + + ViewMode m_viewMode; + Platform::String^ m_name; + Platform::String^ m_automationName; + Platform::String^ m_glyph; + Platform::String^ m_accessKey; + Platform::String^ m_mode; + int m_position; + bool m_supportsNegative; + }; + + [Windows::UI::Xaml::Data::Bindable] + public ref class NavCategoryGroup sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + + public: + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_R(Platform::String^, Name); + OBSERVABLE_PROPERTY_R(Platform::String^, AutomationName); + OBSERVABLE_PROPERTY_R(CategoryGroupType, GroupType); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector^, Categories); + + static Windows::Foundation::Collections::IObservableVector^ CreateMenuOptions(); + + static Platform::String^ GetHeaderResourceKey(CategoryGroupType type); + + internal: + static NavCategoryGroup^ CreateCalculatorCategory(); + static NavCategoryGroup^ CreateConverterCategory(); + + private: + NavCategoryGroup(const NavCategoryGroupInitializer& groupInitializer); + + static std::vector GetInitializerCategoryGroup(CategoryGroupType groupType); + }; + } +} diff --git a/src/CalcViewModel/Common/NetworkManager.cpp b/src/CalcViewModel/Common/NetworkManager.cpp new file mode 100644 index 00000000..5e37a05d --- /dev/null +++ b/src/CalcViewModel/Common/NetworkManager.cpp @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "NetworkManager.h" + +using namespace CalculatorApp; +using namespace Platform; +using namespace Windows::Networking::Connectivity; + +NetworkManager::NetworkManager() +{ + m_NetworkStatusChangedToken = + NetworkInformation::NetworkStatusChanged += ref new NetworkStatusChangedEventHandler( + this, &NetworkManager::OnNetworkStatusChange, CallbackContext::Same); +} + +NetworkManager::~NetworkManager() +{ + NetworkInformation::NetworkStatusChanged -= m_NetworkStatusChangedToken; +} + +NetworkAccessBehavior NetworkManager::GetNetworkAccessBehavior() +{ + NetworkAccessBehavior behavior = NetworkAccessBehavior::Offline; + ConnectionProfile^ connectionProfile = NetworkInformation::GetInternetConnectionProfile(); + if (connectionProfile != nullptr) + { + NetworkConnectivityLevel connectivityLevel = connectionProfile->GetNetworkConnectivityLevel(); + if (connectivityLevel == NetworkConnectivityLevel::InternetAccess + || connectivityLevel == NetworkConnectivityLevel::ConstrainedInternetAccess) + { + ConnectionCost^ connectionCost = connectionProfile->GetConnectionCost(); + behavior = ConvertCostInfoToBehavior(connectionCost); + } + } + + return behavior; +} + +void NetworkManager::OnNetworkStatusChange(_In_ Object^ /*sender*/) +{ + NetworkBehaviorChanged(GetNetworkAccessBehavior()); +} + +// See app behavior guidelines at https://msdn.microsoft.com/en-us/library/windows/apps/xaml/jj835821(v=win.10).aspx +NetworkAccessBehavior NetworkManager::ConvertCostInfoToBehavior(_In_ 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/CalcViewModel/Common/NetworkManager.h b/src/CalcViewModel/Common/NetworkManager.h new file mode 100644 index 00000000..cb34ab9a --- /dev/null +++ b/src/CalcViewModel/Common/NetworkManager.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + public enum class NetworkAccessBehavior + { + Normal = 0, + OptIn = 1, + Offline = 2 + }; + + public delegate void NetworkBehaviorChangedHandler(NetworkAccessBehavior behavior); + + public ref class NetworkManager sealed + { + public: + NetworkManager(); + + static NetworkAccessBehavior GetNetworkAccessBehavior(); + + event NetworkBehaviorChangedHandler^ NetworkBehaviorChanged; + + private: + ~NetworkManager(); + + void OnNetworkStatusChange(_In_ Platform::Object^ sender); + static NetworkAccessBehavior ConvertCostInfoToBehavior(_In_ Windows::Networking::Connectivity::ConnectionCost^ connectionCost); + + private: + Windows::Foundation::EventRegistrationToken m_NetworkStatusChangedToken; + }; +} diff --git a/src/CalcViewModel/Common/TraceActivity.h b/src/CalcViewModel/Common/TraceActivity.h new file mode 100644 index 00000000..120dc25c --- /dev/null +++ b/src/CalcViewModel/Common/TraceActivity.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + constexpr int64_t WINEVENT_KEYWORD_RESPONSE_TIME = 0x1000000000000; + + // RAII wrapper that automatically sends the Stop event when the class gets destructed. + class TraceActivity + { + public: + TraceActivity() : + m_channel(nullptr), + m_activity(nullptr), + m_fields(nullptr) + { } + + TraceActivity( + winrt::Windows::Foundation::Diagnostics::LoggingChannel channel, + std::wstring_view activityName, + winrt::Windows::Foundation::Diagnostics::LoggingFields fields) : + m_channel(channel), + m_activityName(activityName), + m_fields(fields), + m_activity(nullptr) + { + // Write the activity's START event. Note that you must not specify keyword + // or level for START and STOP events because they always use the activity's + // keyword and level. + m_activity = m_channel.StartActivity( + m_activityName, + m_fields, + winrt::Windows::Foundation::Diagnostics::LoggingLevel::Verbose, + winrt::Windows::Foundation::Diagnostics::LoggingOptions(WINEVENT_KEYWORD_RESPONSE_TIME) + ); + } + + ~TraceActivity() + { + if (m_activity != nullptr) + { + // Write the activity's STOP event. + m_activity.StopActivity(m_activityName, m_fields); + m_activity = nullptr; + } + } + + private: + std::wstring m_activityName; + winrt::Windows::Foundation::Diagnostics::LoggingChannel m_channel; + winrt::Windows::Foundation::Diagnostics::LoggingFields m_fields; + winrt::Windows::Foundation::Diagnostics::LoggingActivity m_activity; + }; +} diff --git a/src/CalcViewModel/Common/TraceLogger.cpp b/src/CalcViewModel/Common/TraceLogger.cpp new file mode 100644 index 00000000..7f1e5a2f --- /dev/null +++ b/src/CalcViewModel/Common/TraceLogger.cpp @@ -0,0 +1,985 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "pch.h" +#include "TraceLogger.h" +#include "NetworkManager.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace Concurrency; +using namespace std; +using namespace winrt; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Diagnostics; +using namespace winrt::Windows::Globalization; +using namespace winrt::Windows::Globalization::DateTimeFormatting; +using namespace winrt::Windows::System::UserProfile; + +namespace CalculatorApp +{ + static multimap> s_memoryMap; + + static constexpr array s_programmerType{ + L"N/A", L"QwordType", L"DwordType", + L"WordType", L"ByteType", L"HexBase", + L"DecBase", L"OctBase", L"BinBase" }; + static reader_writer_lock s_traceLoggerLock; + + // Telemetry events. Uploaded to asimov. + constexpr auto EVENT_NAME_DEBUG = L"Debug"; + constexpr auto EVENT_NAME_ERROR = L"ErrorMessage"; + constexpr auto EVENT_NAME_APP_PRELAUNCHED_BY_SYSTEM = L"AppPrelaunchedBySystem"; + constexpr auto EVENT_NAME_PRELAUNCHED_APP_ACTIVATED_BY_USER = L"PrelaunchedAppActivatedByUser"; + constexpr auto EVENT_NAME_APP_LAUNCH_BEGIN = L"AppLaunchBegin"; + constexpr auto EVENT_NAME_APP_LAUNCH_END = L"AppLaunchEnd"; + constexpr auto EVENT_NAME_APP_RESUME_END = L"AppResumeEnd"; + constexpr auto EVENT_NAME_PREVIOUS_STATE_WINDOW_ON_CREATION = L"PreviousStateOnWindowCreation"; + constexpr auto EVENT_NAME_SIZE_ON_SUSPENSION = L"CalculatorSizeOnSuspension"; + constexpr auto EVENT_NAME_CALCULATOR_VIEWED_IN_SESSION = L"CalculatorViewedInSession"; + constexpr auto EVENT_NAME_DATE_CALCULATOR_VIEWED_IN_SESSION = L"DateCalculatorViewedInSession"; + constexpr auto EVENT_NAME_CONVERTER_VIEWED_IN_SESSION = L"ConverterViewedInSession"; + constexpr auto EVENT_NAME_MODE_CHANGE_BEGIN = L"ModeChangeBegin"; + constexpr auto EVENT_NAME_MODE_CHANGE_END = L"ModeChangeEnd"; + constexpr auto EVENT_NAME_HISTORY_BODY_OPENED = L"HistoryBodyOpened"; + constexpr auto EVENT_NAME_HISTORY_ITEM_LOAD_BEGIN = L"HistoryItemLoadBegin"; + constexpr auto EVENT_NAME_HISTORY_ITEM_LOAD_END = L"HistoryItemLoadEnd"; + constexpr auto EVENT_NAME_HISTORY_FLYOUT_OPEN_BEGIN = L"HistoryFlyoutOpenBegin"; + constexpr auto EVENT_NAME_HISTORY_FLYOUT_OPEN_END = L"HistoryFlyoutOpenEnd"; + constexpr auto EVENT_NAME_NEW_WINDOW_CREATION_BEGIN = L"NewWindowCreationBegin"; + constexpr auto EVENT_NAME_NEW_WINDOW_CREATION_END = L"NewWindowCreationEnd"; + constexpr auto EVENT_NAME_HISTORY_CLEAR = L"HistoryClearBegin"; + constexpr auto EVENT_NAME_MULTIPLE_MEMORY_USED = L"MultipleMemoryUsed"; + constexpr auto EVENT_NAME_SINGLE_MEMORY_USED = L"SingleMemoryUsed"; + constexpr auto EVENT_NAME_SHARED_MEMORY_USED = L"SharedMemoryUsed"; + constexpr auto EVENT_NAME_MEMORY_BODY_OPENED = L"MemoryBodyOpened"; + constexpr auto EVENT_NAME_MEMORY_FLYOUT_OPEN_BEGIN = L"MemoryFlyoutOpenBegin"; + constexpr auto EVENT_NAME_MEMORY_FLYOUT_OPEN_END = L"MemoryFlyoutOpenEnd"; + constexpr auto EVENT_NAME_MEMORY_CLEAR_ALL = L"MemoryClearAll"; + constexpr auto EVENT_NAME_INVALID_INPUT_PASTED = L"InvalidInputPasted"; + constexpr auto EVENT_NAME_VALID_INPUT_PASTED = L"ValidInputPasted"; + constexpr auto EVENT_NAME_BITFLIP_PANE_CLICKED = L"BitFlipPaneClicked"; + constexpr auto EVENT_NAME_BITFLIP_BUTTONS_USED = L"BitFlipToggleButtonUsed"; + constexpr auto EVENT_NAME_ANGLE_BUTTONS_USED = L"AngleButtonUsedInSession"; + constexpr auto EVENT_NAME_HYP_BUTTON_USED = L"HypButtonUsedInSession"; + constexpr auto EVENT_NAME_FUNCTION_USAGE = L"FunctionUsageInSession"; + constexpr auto EVENT_NAME_BITLENGTH_BUTTON_USED = L"BitLengthButtonUsed"; + constexpr auto EVENT_NAME_RADIX_BUTTON_USED = L"RadixButtonUsed"; + constexpr auto EVENT_NAME_MAX_WINDOW_COUNT = L"MaxWindowCountInSession"; + constexpr auto EVENT_NAME_WINDOW_LAUNCHED_PROTOCOL = L"WindowActivatedThroughProtocol"; + constexpr auto EVENT_NAME_WINDOW_LAUNCHED_TILESEARCH = L"WindowLaunchedThroughTile"; + constexpr auto EVENT_NAME_DATE_DIFFERENCE_USED = L"DateDifferenceModeUsed"; + constexpr auto EVENT_NAME_DATE_ADD_SUBTRACT_USED = L"DateAddSubtractModeUsed"; + constexpr auto EVENT_NAME_DATE_DIFFERENCE_FOUND = L"DateDifferenceFound"; + constexpr auto EVENT_NAME_HIDE_IF_SHOWN = L"HideIfShown"; + constexpr auto EVENT_NAME_ABOUT_FLYOUT_OPENED = L"AboutFlyoutOpened"; + constexpr auto EVENT_NAME_NAV_BAR_OPENED = L"NavBarOpened"; + constexpr auto EVENT_NAME_CORE_WINDOW_WAS_NULL = L"CoreWindowWasNull"; + + constexpr auto EVENT_NAME_EXCEPTION = L"Exception"; + +#ifdef SEND_TELEMETRY + // c.f. WINEVENT_KEYWORD_RESERVED_63-56 0xFF00000000000000 // Bits 63-56 - channel keywords + // c.f. WINEVENT_KEYWORD_* 0x00FF000000000000 // Bits 55-48 - system-reserved keywords + constexpr int64_t MICROSOFT_KEYWORD_CRITICAL_DATA = 0x0000800000000000; // Bit 47 + constexpr int64_t MICROSOFT_KEYWORD_MEASURES = 0x0000400000000000; // Bit 46 + constexpr int64_t MICROSOFT_KEYWORD_TELEMETRY = 0x0000200000000000; // Bit 45 + constexpr int64_t MICROSOFT_KEYWORD_RESERVED_44 = 0x0000100000000000; // Bit 44 (reserved for future assignment) +#else + // define all Keyword options as 0 when we do not want to upload app telemetry + constexpr int64_t MICROSOFT_KEYWORD_CRITICAL_DATA = 0; + constexpr int64_t MICROSOFT_KEYWORD_MEASURES = 0; + constexpr int64_t MICROSOFT_KEYWORD_TELEMETRY = 0; + constexpr int64_t MICROSOFT_KEYWORD_RESERVED_44 = 0; +#endif + +#pragma region TraceLogger setup and cleanup + + TraceLogger::TraceLogger() : + g_calculatorProvider( + L"MicrosoftCalculator", + LoggingChannelOptions(GUID{ 0x4f50731a, 0x89cf, 0x4782, 0xb3, 0xe0, 0xdc, 0xe8, 0xc9, 0x4, 0x76, 0xba }), // Microsoft Telemetry group + GUID{ 0x905ca09, 0x610e, 0x401e, 0xb6, 0x50, 0x2f, 0x21, 0x29, 0x80, 0xb9, 0xe0 }), //Unique providerID {0905CA09-610E-401E-B650-2F212980B9E0} + m_appLaunchActivity{ nullptr } + { + // initialize the function array + InitFunctionLogArray(); + } + + TraceLogger::~TraceLogger() + { + } + + TraceLogger& TraceLogger::GetInstance() + { + static TraceLogger s_selfInstance; + return s_selfInstance; + } + + bool TraceLogger::GetTraceLoggingProviderEnabled() const + { + return g_calculatorProvider.Enabled(); + } + +#pragma region Tracing methods + void TraceLogger::LogTelemetryEvent(wstring_view eventName, LoggingFields fields) const + { + g_calculatorProvider.LogEvent(eventName, fields, LoggingLevel::Verbose, LoggingOptions(MICROSOFT_KEYWORD_TELEMETRY)); + } + + void TraceLogger::LogMeasureEvent(wstring_view eventName, LoggingFields fields) const + { + g_calculatorProvider.LogEvent(eventName, fields, LoggingLevel::Verbose, LoggingOptions(MICROSOFT_KEYWORD_MEASURES)); + } + + void TraceLogger::LogCriticalDataEvent(wstring_view eventName, LoggingFields fields) const + { + g_calculatorProvider.LogEvent(eventName, fields, LoggingLevel::Verbose, LoggingOptions(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + void TraceLogger::LogPerformanceEvent(wstring_view eventName, LoggingFields fields) const + { + g_calculatorProvider.LogEvent(eventName, fields, LoggingLevel::Verbose, LoggingOptions(WINEVENT_KEYWORD_RESPONSE_TIME)); + } + + void TraceLogger::LogInfoEvent(wstring_view eventName, LoggingFields fields) const + { + g_calculatorProvider.LogEvent(eventName, fields, LoggingLevel::Information); + } + + unique_ptr TraceLogger::CreateTraceActivity(wstring_view eventName, LoggingFields fields) const + { + return make_unique(g_calculatorProvider, eventName, fields); + } +#pragma endregion + + void TraceLogger::InsertIntoMemoryMap(int windowId, bool isStandard, bool isScientific, bool isProgrammer) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + auto iterMap = s_memoryMap.find(windowId); + if (iterMap == s_memoryMap.end()) + { + s_memoryMap.insert(std::make_pair(windowId, vector())); + iterMap = s_memoryMap.find(windowId); + } + + if (isScientific) + { + iterMap->second.insert(iterMap->second.begin(), L"Scientific"); + } + else if (isProgrammer) + { + iterMap->second.insert(iterMap->second.begin(), L"Programmer"); + } + else + { + iterMap->second.insert(iterMap->second.begin(), L"Standard"); + } + } + + void TraceLogger::UpdateMemoryMap(int windowId, int memoryPosition, bool isStandard, bool isScientific, bool isProgrammer) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + auto iterMap = s_memoryMap.find(windowId); + assert(iterMap != s_memoryMap.end()); + assert(iterMap->second.size() >= (unsigned int)memoryPosition); + + if (isScientific) + { + iterMap->second[memoryPosition] = L"Scientific"; + } + else if (isProgrammer) + { + iterMap->second[memoryPosition] = L"Programmer"; + } + else + { + iterMap->second[memoryPosition] = L"Standard"; + } + } + + void TraceLogger::DeleteFromMemoryMap(int windowId, int memoryPosition) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + auto iterMap = s_memoryMap.find(windowId); + assert(iterMap != s_memoryMap.end()); + + iterMap->second.erase(iterMap->second.begin() + memoryPosition); + } + + // return true if windowId is logged once else return false + bool TraceLogger::UpdateWindowIdLog(int windowId) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + if (windowIdLog.find(windowId) == windowIdLog.end()) + { + return false; + } + if (windowIdLog[windowId] == false) + { + windowIdLog[windowId] = true; + return true; + } + else + { + return false; + } + } + + // call comes here at the time of ApplicationViewModel initialisation + void TraceLogger::LogCalculatorModeViewed(ViewMode mode, int windowId) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + // store windowId in windowIdLog which says we have logged mode for the present windowId. + if (windowIdLog.find(windowId) == windowIdLog.end()) + { + windowIdLog.insert(pair(windowId, false)); + } + // if the event is not logged already for the present mode + if (currentMode != mode) + { + currentMode = mode; + + LoggingFields fields{}; + fields.AddString(L"CalculatorMode", NavCategory::GetFriendlyName(mode)->Data()); + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_CALCULATOR_VIEWED_IN_SESSION, fields); + } + } + + // call comes here at the time of ApplicationViewModel initialization + void TraceLogger::LogDateCalculatorModeViewed(ViewMode mode, int windowId) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + // store windowId in windowIdLog which says we have logged mode for the present windowId. + if (windowIdLog.find(windowId) == windowIdLog.end()) + { + windowIdLog.insert(pair(windowId, false)); + } + // if the event is not logged already for the present mode + if (currentMode != mode) + { + currentMode = mode; + + LoggingFields fields{}; + fields.AddString(L"DateCalculatorMode", NavCategory::GetFriendlyName(mode)->Data()); + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_DATE_CALCULATOR_VIEWED_IN_SESSION, fields); + } + } + + // call comes here at the time of ApplicationViewModel initialization + void TraceLogger::LogConverterModeViewed(ViewMode mode, int windowId) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + if (windowIdLog.find(windowId) == windowIdLog.end()) + { + windowIdLog.insert(pair(windowId, false)); + } + // if the event is not logged already for the present mode + if (currentMode != mode) + { + currentMode = mode; + + LoggingFields fields{}; + fields.AddString(L"ConverterMode", NavCategory::GetFriendlyName(mode)->Data()); + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_CONVERTER_VIEWED_IN_SESSION, fields); + } + } + + void TraceLogger::LogSharedMemoryUsed(wstring_view fromMode, wstring_view toMode, unsigned int memorySize) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"FromMode", fromMode); + fields.AddString(L"ToMode", toMode); + fields.AddUInt32(L"MemoryListSize", memorySize); + LogTelemetryEvent(EVENT_NAME_SHARED_MEMORY_USED, fields); + } + + void TraceLogger::LogBitFlipPaneClicked() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_BITFLIP_PANE_CLICKED, fields); + } + + void TraceLogger::LogBitFlipUsed() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_BITFLIP_BUTTONS_USED, fields); + } + + void TraceLogger::LogAppLaunchStart() + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + if (!isAppLaunchBeginLogged) + { + m_appLaunchActivity = g_calculatorProvider.StartActivity( + EVENT_NAME_APP_LAUNCH_BEGIN, + nullptr, + LoggingLevel::Verbose, + LoggingOptions(MICROSOFT_KEYWORD_TELEMETRY)); + + isAppLaunchBeginLogged = true; + } + } + + void TraceLogger::LogAppLaunchComplete(/*Windows::ApplicationModel::Activation::ActivationKind activationKind, Windows::ApplicationModel::Activation::ApplicationExecutionState executionState*/) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + if ((m_appLaunchActivity != nullptr) && (!isAppLaunchEndLogged)) + { + m_appLaunchActivity.StopActivity(EVENT_NAME_APP_LAUNCH_END); + + isAppLaunchEndLogged = true; + } + + m_appLaunchActivity = nullptr; + } + + void TraceLogger::LogAppResumeComplete() + { + if (m_appLaunchActivity != nullptr) + { + m_appLaunchActivity.StopActivity(EVENT_NAME_APP_RESUME_END); + } + + m_appLaunchActivity = nullptr; + } + + void TraceLogger::LogDebug(wstring_view debugData) + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"DebugData", debugData); + LogTelemetryEvent(EVENT_NAME_DEBUG, fields); + } + + void TraceLogger::LogOnAppLaunch(wstring_view previousExecutionState) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"PreviousExecutionState", previousExecutionState); + LogTelemetryEvent(EVENT_NAME_PREVIOUS_STATE_WINDOW_ON_CREATION, fields); + } + + void TraceLogger::LogAboutFlyoutOpened() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_ABOUT_FLYOUT_OPENED, fields); + } + + void TraceLogger::LogNavBarOpened() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_NAV_BAR_OPENED, fields); + } + + void TraceLogger::LogClearHistory() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_HISTORY_CLEAR, fields); + } + + void TraceLogger::LogHistoryFlyoutOpenBegin(unsigned int historyItemCount) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + // we want to record the event only when history item count is atleast 20 + if (historyItemCount >= 20) + { + LoggingFields fields{}; + fields.AddUInt32(L"HistoryItemCount", historyItemCount); + LogTelemetryEvent(EVENT_NAME_HISTORY_FLYOUT_OPEN_BEGIN, fields); + } + } + + void TraceLogger::LogHistoryFlyoutOpenEnd(int historyItemCount) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + if (historyItemCount >= 20) + { + LoggingFields fields{}; + fields.AddUInt32(L"HistoryItemCount", historyItemCount); + LogTelemetryEvent(EVENT_NAME_HISTORY_FLYOUT_OPEN_END, fields); + } + } + + void TraceLogger::LogHistoryBodyOpened() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_HISTORY_BODY_OPENED, fields); + } + + void TraceLogger::LogMemoryFlyoutOpenBegin(unsigned int memoryItemCount) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + // we want to record the event only when memory item count is atleast 4 + if (memoryItemCount >= 4) + { + LoggingFields fields{}; + fields.AddUInt32(L"MemoryItemCount", memoryItemCount); + LogTelemetryEvent(EVENT_NAME_MEMORY_FLYOUT_OPEN_BEGIN, fields); + } + } + + void TraceLogger::LogMemoryFlyoutOpenEnd(unsigned int memoryItemCount) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + if (memoryItemCount >= 4) + { + LoggingFields fields{}; + fields.AddUInt32(L"MemoryItemCount", memoryItemCount); + LogTelemetryEvent(EVENT_NAME_MEMORY_FLYOUT_OPEN_END, fields); + } + } + + void TraceLogger::LogMemoryBodyOpened() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_MEMORY_BODY_OPENED, fields); + } + + //If calculator is launched in any mode other than standard then this call will come which is not intended. But there is no way to avoid it. + //So don't use this function to analyze the count of mode change in session instead use CalculatorViewedInSession and ConverterViewedInSession to do that. + //Use of this function is to analyze perf of mode change. + void TraceLogger::LogModeChangeBegin(ViewMode fromMode, ViewMode toMode, int windowId) + { + if (!GetTraceLoggingProviderEnabled()) return; + + if (NavCategory::IsValidViewMode(fromMode) && NavCategory::IsValidViewMode(toMode)) + { + LoggingFields fields{}; + fields.AddString(L"FromMode", NavCategory::GetFriendlyName(fromMode)->Data()); + fields.AddString(L"ToMode", NavCategory::GetFriendlyName(toMode)->Data()); + fields.AddInt32(L"WindowId", windowId); + LogMeasureEvent(EVENT_NAME_MODE_CHANGE_BEGIN, fields); + } + } + + //comment: same as LogModeChangeBegin + void TraceLogger::LogModeChangeEnd(ViewMode toMode, int windowId) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + if (NavCategory::IsValidViewMode(toMode)) + { + LoggingFields fields{}; + fields.AddString(L"ToMode", NavCategory::GetFriendlyName(toMode)->Data()); + fields.AddInt32(L"WindowId", windowId); + LogMeasureEvent(EVENT_NAME_MODE_CHANGE_END, fields); + } + } + + void TraceLogger::LogHistoryItemLoadBegin() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_HISTORY_ITEM_LOAD_BEGIN, fields); + } + + void TraceLogger::LogHistoryItemLoadEnd(unsigned int historyTokenCount) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"HistoryTokenCount", historyTokenCount); + LogTelemetryEvent(EVENT_NAME_HISTORY_ITEM_LOAD_END, fields); + } + + void TraceLogger::LogNewWindowCreationBegin(int windowId) + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_NEW_WINDOW_CREATION_BEGIN, fields); + } + + void TraceLogger::LogNewWindowCreationEnd(int windowId) + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_NEW_WINDOW_CREATION_END, fields); + } + + void TraceLogger::LogError(wstring_view errorString) + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"ErrorString", errorString); + LogTelemetryEvent(EVENT_NAME_ERROR, fields); + } + + void TraceLogger::LogPrelaunchedAppActivatedByUser() + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_PRELAUNCHED_APP_ACTIVATED_BY_USER, fields); + } + + void TraceLogger::LogAppPrelaunchedBySystem() + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_APP_PRELAUNCHED_BY_SYSTEM, fields); + } + + void TraceLogger::LogMemoryClearAll(int windowId) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + auto iterMap = s_memoryMap.find(windowId); + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_MEMORY_CLEAR_ALL, fields); + + if (iterMap != s_memoryMap.end()) + { + iterMap->second.clear(); + } + } + + void TraceLogger::LogMemoryUsed(int windowId, unsigned int slotPosition, bool isStandard, bool isScientific, bool isProgrammer, unsigned int memorySize) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + // Reader lock for the static resources + reader_writer_lock::scoped_lock_read lock(s_traceLoggerLock); + auto iterMap = s_memoryMap.find(windowId); + + if (slotPosition == 0) + { + LogSingleMemoryUsed(memorySize); + } + else + { + LogMultipleMemoryUsed(slotPosition, memorySize); + } + + if (iterMap != s_memoryMap.end()) + { + // if current mode is not equal to mode of memoryMap[slotPosition] then LogSharedMemoryUsed + if (isStandard && (iterMap->second[slotPosition] != L"Standard")) + { + LogSharedMemoryUsed(iterMap->second[slotPosition], L"Standard", memorySize); + } + else if (isScientific && (iterMap->second[slotPosition] != L"Scientific")) + { + LogSharedMemoryUsed(iterMap->second[slotPosition], L"Scientific", memorySize); + } + else if (isProgrammer && (iterMap->second[slotPosition] != L"Programmer")) + { + LogSharedMemoryUsed(iterMap->second[slotPosition], L"Programmer", memorySize); + } + } + } + + void TraceLogger::LogMultipleMemoryUsed(unsigned int slotPosition, unsigned int memorySize) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"MemoryIndex", slotPosition); + fields.AddUInt32(L"MemoryListSize", memorySize); + LogTelemetryEvent(EVENT_NAME_MULTIPLE_MEMORY_USED, fields); + } + + void TraceLogger::LogSingleMemoryUsed(unsigned int memorySize) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"MemoryListSize", memorySize); + LogTelemetryEvent(EVENT_NAME_SINGLE_MEMORY_USED, fields); + } + + void TraceLogger::LogInvalidInputPasted(wstring_view reason, wstring_view pastedExpression, ViewMode mode, int programmerNumberBase, int bitLengthType) + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"Mode", NavCategory::GetFriendlyName(mode)->Data()); + fields.AddString(L"Reason", reason); + fields.AddString(L"PastedExpression", pastedExpression); + fields.AddString(L"ProgrammerNumberBase", GetProgrammerType(programmerNumberBase).c_str()); + fields.AddString(L"BitLengthType", GetProgrammerType(bitLengthType).c_str()); + LogTelemetryEvent(EVENT_NAME_INVALID_INPUT_PASTED, fields); + } + + void TraceLogger::LogValidInputPasted(ViewMode mode) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"Mode", NavCategory::GetFriendlyName(mode)->Data()); + LogTelemetryEvent(EVENT_NAME_VALID_INPUT_PASTED, fields); + } + + void TraceLogger::LogStandardException(wstring_view functionName, const exception& e) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"FunctionName", functionName); + wstringstream exceptionMessage; + exceptionMessage << e.what(); + fields.AddString(L"ExceptionMessage", exceptionMessage.str()); + LogMeasureEvent(EVENT_NAME_EXCEPTION, fields); + } + + void TraceLogger::LogWinRTException(wstring_view functionName, hresult_error const& e) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"FunctionName", functionName); + fields.AddInt32(L"HRESULT", e.code()); + fields.AddString(L"ExceptionMessage", e.message()); + LogMeasureEvent(EVENT_NAME_EXCEPTION, fields); + } + + void TraceLogger::LogPlatformException(wstring_view functionName, Platform::Exception^ e) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddString(L"FunctionName", functionName); + fields.AddInt32(L"HRESULT", e->HResult); + fields.AddString(L"ExceptionMessage", e->Message->Data()); + LogMeasureEvent(EVENT_NAME_EXCEPTION, fields); + } + + void TraceLogger::UpdateFunctionUsage(int funcIndex) + { + // Writer lock for the static resources + reader_writer_lock::scoped_lock lock(s_traceLoggerLock); + + if (GetIndex(funcIndex)) + { + // funcIndex is passed by reference and will be having the returned index + funcLog[funcIndex].count++; + } + } + + void TraceLogger::InitFunctionLogArray() + { + int i = -1; + for (int funcIndex = 0; funcIndex != maxFunctionSize; funcIndex++) + { + FunctionLogEnum func = safe_cast(funcIndex); + wstring functionName = func.ToString()->Data(); + if (functionName.compare(L"CalculatorApp.FunctionLogEnum") != 0) + { + findIndex[funcIndex] = ++i; + funcLog.push_back(FuncLog(functionName)); + } + } + // update the functionCount with total function count which we are tracking through tracelog. + functionCount = i; + } + + wstring TraceLogger::GetProgrammerType(int index) + { + if (index >= 0) + { + return s_programmerType[index]; + } + //return "N/A" + return s_programmerType[0]; + } + + bool TraceLogger::GetIndex(int &index) + { + if (findIndex[index] > 0) + { + index = findIndex[index]; + return true; + } + return false; + } + + void TraceLogger::UpdateWindowCount(size_t windowCount) + { + maxWindowCount = (maxWindowCount > windowCount) ? maxWindowCount : windowCount; + windowLaunchCount++; + } + + void TraceLogger::LogMaxWindowCount() + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + fields.AddUInt32(L"WindowCount", (unsigned int)maxWindowCount); + LogTelemetryEvent(EVENT_NAME_MAX_WINDOW_COUNT, fields); + } + + void TraceLogger::LogWindowActivated() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_WINDOW_LAUNCHED_PROTOCOL, fields); + } + + void TraceLogger::LogWindowLaunched() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_WINDOW_LAUNCHED_TILESEARCH, fields); + } + + void TraceLogger::LogBitLengthButtonUsed(int windowId) + { + if (bitLengthButtonUsage.find(windowId) == bitLengthButtonUsage.end()) + { + bitLengthButtonUsage.insert(pair(windowId, 1)); + } + else + { + bitLengthButtonUsage[windowId]++; + } + if ((bitLengthButtonUsage[windowId] == 5) && !bitLengthButtonLoggedInSession) + { + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_BITLENGTH_BUTTON_USED, fields); + + bitLengthButtonLoggedInSession = true; + } + } + + void TraceLogger::LogRadixButtonUsed(int windowId) + { + if (radixButtonUsage.find(windowId) == radixButtonUsage.end()) + { + radixButtonUsage.insert(pair(windowId, 1)); + } + else + { + radixButtonUsage[windowId]++; + } + if ((radixButtonUsage[windowId] == 2) && !radixButtonLoggedInSession) + { + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_RADIX_BUTTON_USED, fields); + + radixButtonLoggedInSession = true; + } + } + + void TraceLogger::LogAngleButtonUsed(int windowId) + { + if (angleButtonUsage.find(windowId) == angleButtonUsage.end()) + { + angleButtonUsage.insert(pair(windowId, 1)); + } + else + { + angleButtonUsage[windowId]++; + } + if ((angleButtonUsage[windowId] == 2) && !angleButtonLoggedInSession) + { + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_ANGLE_BUTTONS_USED, fields); + + angleButtonLoggedInSession = true; + } + } + + void TraceLogger::LogFunctionUsage(int windowId) + { + if (!GetTraceLoggingProviderEnabled()) return; + + for (int i = 0; i < functionCount; i++) + { + // log only those functions which are used + if (funcLog[i].count > 0) + { + LoggingFields fields{}; + fields.AddString(L"FunctionName", funcLog[i].funcName.data()); + fields.AddUInt32(L"UsageCount", funcLog[i].count); + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_FUNCTION_USAGE, fields); + } + } + } + + void TraceLogger::LogHypButtonUsed(int windowId) + { + if (!isHypButtonLogged) + { + LoggingFields fields{}; + fields.AddUInt32(L"WindowId", windowId); + LogTelemetryEvent(EVENT_NAME_HYP_BUTTON_USED, fields); + + isHypButtonLogged = true; + } + } + + void TraceLogger::LogDateDifferenceModeUsed(int windowId) + { + if (!m_dateDiffUsageLoggedInSession) + { + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_DATE_DIFFERENCE_USED, fields); + + m_dateDiffUsageLoggedInSession = true; + } + } + + void TraceLogger::LogDateAddSubtractModeUsed(int windowId, bool isAddMode) + { + auto usageMap = isAddMode ? &m_dateAddModeUsage : &m_dateSubtractModeUsage; + auto isLoggedInSession = isAddMode ? &m_dateAddUsageLoggedInSession : &m_dateSubtractUsageLoggedInSession; + + if (usageMap->find(windowId) == usageMap->end()) + { + usageMap->insert(pair(windowId, 1)); + } + else + { + (*usageMap)[windowId]++; + } + + // Ignore first 3 calls during the initialization of the combo box selected items for Add mode + int firstChangeEventCount = isAddMode ? 4 : 1; + + if (((*usageMap)[windowId] == firstChangeEventCount) + && (!(*isLoggedInSession))) + { + LoggingFields fields{}; + fields.AddString(L"AddSubtractMode", isAddMode ? L"Add" : L"Subtract"); + LogTelemetryEvent(EVENT_NAME_DATE_ADD_SUBTRACT_USED, fields); + + *isLoggedInSession = true; + } + } + + void TraceLogger::LogDateClippedTimeDifferenceFound(Calendar const& today, DateTime const& clippedTime) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + auto calendarSystem = today.GetCalendarSystem(); + auto clock = today.GetClock(); + DateTimeFormatter dtFormatter{ + L"longdate shorttime", + { L"en-US" }, + GlobalizationPreferences::HomeGeographicRegion(), + calendarSystem, + clock }; + + LoggingFields fields{}; + fields.AddString(L"ResolvedCalendarLanguage", today.ResolvedLanguage()); + fields.AddString(L"Timezone", today.GetTimeZone()); + fields.AddString(L"CalendarSystem", calendarSystem); + fields.AddString(L"Clock", clock); + fields.AddBoolean(L"IsDaylightSavingTime", today.IsDaylightSavingTime()); + fields.AddString(L"TodayDate", dtFormatter.Format(today.GetDateTime())); + fields.AddString(L"ClippedDate", dtFormatter.Format(clippedTime)); + LogTelemetryEvent(EVENT_NAME_DATE_DIFFERENCE_FOUND, fields); + } + + void TraceLogger::LogUserRequestedRefreshFailed() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(L"UserRequestedRefreshFailed", fields); + } + + void TraceLogger::LogConversionResult(wstring_view fromValue, wstring_view fromUnit, wstring_view toValue, wstring_view toUnit) const + { + if (!GetTraceLoggingProviderEnabled()) return; + + wstring behaviorString{}; + NetworkAccessBehavior behavior = NetworkManager::GetNetworkAccessBehavior(); + switch (behavior) + { + case NetworkAccessBehavior::Offline: + behaviorString = L"Offline"; + break; + + case NetworkAccessBehavior::OptIn: + behaviorString = L"Metered"; + break; + + case NetworkAccessBehavior::Normal: + default: + behaviorString = L"Online"; + break; + } + + LoggingFields fields{}; + fields.AddString(L"NetworkAccess", behaviorString); + fields.AddString(L"FromValue", fromValue); + fields.AddString(L"FromUnit", fromUnit); + fields.AddString(L"ToValue", toValue); + fields.AddString(L"ToUnit", toUnit); + LogTelemetryEvent(L"CurrencyConverterInputReceived", fields); + } + + void TraceLogger::LogViewClosingTelemetry(int windowId) + { + LogFunctionUsage(windowId); + LogMaxWindowCount(); + } + + void TraceLogger::LogCoreWindowWasNull() const + { + if (!GetTraceLoggingProviderEnabled()) return; + + LoggingFields fields{}; + LogTelemetryEvent(EVENT_NAME_CORE_WINDOW_WAS_NULL, fields); + } +} + diff --git a/src/CalcViewModel/Common/TraceLogger.h b/src/CalcViewModel/Common/TraceLogger.h new file mode 100644 index 00000000..c6bfc3dd --- /dev/null +++ b/src/CalcViewModel/Common/TraceLogger.h @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "TraceActivity.h" + +static const int maxFunctionSize = (int)CalculationManager::Command::CommandBINEDITEND; + +// A trace logging provider can only be instantiated and registered once per module. +// This class implements a singleton model ensure that only one instance is created. +namespace CalculatorApp +{ + struct FuncLog + { + public: + int count; + std::wstring funcName; + FuncLog() { count = 0; } + FuncLog(std::wstring fName) + { + funcName = fName; + count = 0; + } + }; + + class TraceLogger + { + public: + TraceLogger(_In_ TraceLogger const&) = delete; + TraceLogger const & operator= (_In_ TraceLogger const&) = delete; + ~TraceLogger(); + static TraceLogger& GetInstance(); + bool GetTraceLoggingProviderEnabled() const; + + void LogAppLaunchStart(); + void LogAppLaunchComplete(); + void LogAppResumeComplete(); + void LogOnAppLaunch(std::wstring_view previousExecutionState) const; + void LogMemoryClearAll(int); + void LogBitFlipPaneClicked() const; + void LogBitFlipUsed() const; + void LogHistoryBodyOpened() const; + void LogHistoryItemLoadBegin() const; + void LogHistoryItemLoadEnd(unsigned int) const; + void LogHistoryFlyoutOpenBegin(unsigned int) const; + void LogHistoryFlyoutOpenEnd(int) const; + void LogCalculatorModeViewed(CalculatorApp::Common::ViewMode, int); + void LogDateCalculatorModeViewed(CalculatorApp::Common::ViewMode, int); + void LogConverterModeViewed(CalculatorApp::Common::ViewMode, int); + void LogModeChangeBegin(CalculatorApp::Common::ViewMode, CalculatorApp::Common::ViewMode, int); + void LogModeChangeEnd(CalculatorApp::Common::ViewMode, int) const; + void LogClearHistory() const; + void InsertIntoMemoryMap(int, bool, bool, bool); + void UpdateMemoryMap(int, int, bool, bool, bool); + void DeleteFromMemoryMap(int, int); + void LogMemoryUsed(int, unsigned int, bool, bool, bool, unsigned int) const; + void LogMultipleMemoryUsed(unsigned int, unsigned int) const; + void LogSingleMemoryUsed(unsigned int) const; + void LogSharedMemoryUsed(std::wstring_view, std::wstring_view, unsigned int) const; + void LogMemoryBodyOpened() const; + void LogMemoryFlyoutOpenBegin(unsigned int) const; + void LogDebug(std::wstring_view debugData); + void LogMemoryFlyoutOpenEnd(unsigned int) const; + void LogInvalidInputPasted(std::wstring_view reason, std::wstring_view pastedExpression, CalculatorApp::Common::ViewMode mode, int ProgrammerNumberBase, int bitLengthType); + void LogValidInputPasted(CalculatorApp::Common::ViewMode mode) const; + void UpdateFunctionUsage(int func); + void LogFunctionUsage(int); + void InitFunctionLogArray(); + void LogBitLengthButtonUsed(int windowId); + void LogRadixButtonUsed(int windowId); + void LogAngleButtonUsed(int windowId); + void LogHypButtonUsed(int windowId); + void LogNewWindowCreationBegin(int windowId); + void LogNewWindowCreationEnd(int windowId); + void LogError(std::wstring_view errorString); + void LogPrelaunchedAppActivatedByUser(); + void LogAppPrelaunchedBySystem(); + void UpdateWindowCount(size_t windowCount); + bool UpdateWindowIdLog(int windowId); + void LogMaxWindowCount(); + void LogWindowActivated() const; + void LogWindowLaunched() const; + void LogUserRequestedRefreshFailed() const; + void LogConversionResult(std::wstring_view fromValue, std::wstring_view fromUnit, std::wstring_view toValue, std::wstring_view toUnit) const; + void LogAboutFlyoutOpened() const; + void LogNavBarOpened() const; + void LogViewClosingTelemetry(int); + void LogCoreWindowWasNull() const; + + // Trace methods for Date Calculator usage + void LogDateDifferenceModeUsed(int windowId); + void LogDateAddSubtractModeUsed(int windowId, bool isAddMode); + void LogDateClippedTimeDifferenceFound(winrt::Windows::Globalization::Calendar const& today, winrt::Windows::Foundation::DateTime const& clippedTime) const; + + void LogStandardException(std::wstring_view functionName, _In_ const std::exception& e) const; + void LogWinRTException(std::wstring_view functionName, _In_ winrt::hresult_error const& e) const; + void LogPlatformException(std::wstring_view functionName, _In_ Platform::Exception^ e) const; + + private: + // Create an instance of TraceLogger + TraceLogger(); + + // Any new Log method should + // a) decide the level of logging. This will help us in limiting recording of events only upto a certain level. See this link for guidance https://msdn.microsoft.com/en-us/library/windows/desktop/aa363742(v=vs.85).aspx + // We're using Verbose level for events that are called frequently and needed only for debugging or capturing perf for specific scenarios + // b) should decide whether or not to log to telemetry and pass TraceLoggingKeyword(MICROSOFT_KEYWORD_TELEMETRY) accordingly + // c) Should accept a variable number of additional data arguments if needed + void LogTelemetryEvent(std::wstring_view eventName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + void LogMeasureEvent(std::wstring_view eventName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + void LogCriticalDataEvent(std::wstring_view eventName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + void LogPerformanceEvent(std::wstring_view eventName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + void LogInfoEvent(std::wstring_view eventName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + + std::unique_ptr CreateTraceActivity(std::wstring_view activityName, winrt::Windows::Foundation::Diagnostics::LoggingFields fields) const; + + winrt::Windows::Foundation::Diagnostics::LoggingChannel g_calculatorProvider; + + bool isSizeChangeLogged = false; + bool isHideIfShownLogged = false; + bool isSizeChangedFirstTime = true; // to track the first size changed event which is fired on the launch of app + bool isAutoConversionBeginLoggedInSession = false; + bool isAutoConversionEndLoggedInSession = false; + bool angleButtonLoggedInSession = false; + bool radixButtonLoggedInSession = false; + bool bitLengthButtonLoggedInSession = false; + GUID sessionGuid; + CalculatorApp::Common::ViewMode currentMode = CalculatorApp::Common::ViewMode::None; + std::vector funcLog; + int functionCount = 0; + bool isHypButtonLogged = false; + bool isAngleButtonInitialized = false; + unsigned int findIndex[maxFunctionSize] = { 0 }; + bool GetIndex(int &index); + std::wstring GetProgrammerType(int index); + size_t maxWindowCount = 0; + bool isAppLaunchBeginLogged = false; + bool isAppLaunchEndLogged = false; + std::map bitLengthButtonUsage; + std::map angleButtonUsage; + std::map radixButtonUsage; + std::map windowIdLog; + + // Private variables for Date Calculator usage + bool m_dateDiffUsageLoggedInSession = false; + bool m_dateAddUsageLoggedInSession = false; + bool m_dateSubtractUsageLoggedInSession = false; + std::map m_dateAddModeUsage; + std::map m_dateSubtractModeUsage; + + size_t windowLaunchCount = 0; + + winrt::Windows::Foundation::Diagnostics::LoggingActivity m_appLaunchActivity; + }; +} diff --git a/src/CalcViewModel/Common/Utils.cpp b/src/CalcViewModel/Common/Utils.cpp new file mode 100644 index 00000000..c0ddf69a --- /dev/null +++ b/src/CalcViewModel/Common/Utils.cpp @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// Utils.cpp +// + +#include "pch.h" +#include "Utils.h" +#include "Common\AppResourceProvider.h" +#include "Common\ExpressionCommandSerializer.h" +#include "Common\ExpressionCommandDeserializer.h" +#include "ViewState.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace concurrency; +using namespace Platform; +using namespace std; +using namespace Utils; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Storage::Streams; +using namespace Windows::UI::Core; +using namespace Windows::UI::ViewManagement; +using namespace Windows::UI::Xaml; +using namespace Windows::Foundation; +using namespace Windows::Storage; + +void Utils::IFTPlatformException(HRESULT hr) +{ + if (FAILED(hr)) + { + Platform::Exception^ exception = ref new Platform::Exception(hr); + throw(exception); + } +} + +String^ Utils::GetStringValue(String^ input) +{ + // Remove first and last " characters + if (input->Length() >= 3) + { + wstring out(input->Begin() + 1, input->End() - 1); + return ref new String(out.c_str()); + } + return input; +} + +double Utils::GetDoubleFromWstring(wstring input) +{ + wchar_t unWantedChars[] = { L' ', L',', 8234, 8235, 8236, 8237 }; + wstring ws = RemoveUnwantedCharsFromWstring(input, unWantedChars, 6); + string inputString(ws.begin(), ws.end()); + return ::atof(inputString.c_str()); +} + +//returns windowId for the current view +int Utils::GetWindowId() +{ + int windowId = -1; + + auto window = CoreWindow::GetForCurrentThread(); + if (window != nullptr) + { + windowId = ApplicationView::GetApplicationViewIdForWindow(window); + } + + return windowId; +} + +void Utils::RunOnUIThreadNonblocking(std::function&& function, _In_ CoreDispatcher^ currentDispatcher) +{ + if (currentDispatcher != nullptr) + { + auto task = create_task(currentDispatcher->RunAsync(CoreDispatcherPriority::Normal, + ref new DispatchedHandler([function]() + { + function(); + }))); + } +} + +// returns if the last character of a wstring is the target wchar_t +bool Utils::IsLastCharacterTarget(_In_ wstring const &input, _In_ wchar_t target) +{ + return !input.empty() && input.back() == target; +} + +//return wstring after removing characters like space, comma, and double quotes +wstring Utils::RemoveUnwantedCharsFromWstring(wstring input) +{ + wchar_t unWantedChars[] = { L' ', L',', L'"', 8234, 8235, 8236, 8237 }; + return RemoveUnwantedCharsFromWstring(input, unWantedChars, 6); +} + +//return wstring after removing characters specified by unwantedChars array +wstring Utils::RemoveUnwantedCharsFromWstring(wstring input, wchar_t* unwantedChars, unsigned int size) +{ + for (unsigned int i = 0; i < size; ++i) + { + input.erase(std::remove(input.begin(), input.end(), unwantedChars[i]), input.end()); + } + return input; +} + +void Utils::SerializeCommandsAndTokens(_In_ shared_ptr>> const &tokens, + _In_ shared_ptr>> const &commands, + DataWriter^ writer) +{ + unsigned int commandsSize; + IFTPlatformException(commands->GetSize(&commandsSize)); + + // save the size of the commands vector + writer->WriteUInt32(commandsSize); + + SerializeCommandVisitor cmdVisitor(writer); + for (unsigned int i = 0; i < commandsSize; ++i) + { + shared_ptr exprCmd; + IFTPlatformException(commands->GetAt(i, &exprCmd)); + + CalculationManager::CommandType commandType = exprCmd->GetCommandType(); + writer->WriteInt32(static_cast(commandType)); + exprCmd->Accept(cmdVisitor); + } + + unsigned int tokensSize; + IFTPlatformException(tokens->GetSize(&tokensSize)); + writer->WriteUInt32(tokensSize); + + for (unsigned int i = 0; i < tokensSize; ++i) + { + pair eachToken; + IFTPlatformException(tokens->GetAt(i, &eachToken)); + + auto stringData = ref new Platform::String(eachToken.first.c_str()); + auto intData = eachToken.second; + writer->WriteUInt32(writer->MeasureString(stringData)); + writer->WriteString(stringData); + writer->WriteInt32(intData); + } +} + +const shared_ptr>> Utils::DeserializeCommands(DataReader^ reader) +{ + shared_ptr>> commandVector = make_shared>>(); + auto commandVectorSize = reader->ReadUInt32(); + + CommandDeserializer cmdDeserializer(reader); + for (unsigned int i = 0; i < commandVectorSize; ++i) + { + auto commandTypeInt = reader->ReadInt32(); + CalculationManager::CommandType commandType = static_cast(commandTypeInt); + shared_ptr exprCmd = cmdDeserializer.Deserialize(commandType); + commandVector->Append(exprCmd); + } + + return commandVector; +} + +const shared_ptr>> Utils::DeserializeTokens(DataReader^ reader) +{ + shared_ptr>> tokenVector = make_shared>>(); + auto tokensSize = reader->ReadUInt32(); + + for (unsigned int i = 0; i < tokensSize; ++i) + { + pair eachToken; + auto stringDataLen = reader->ReadUInt32(); + auto stringData = reader->ReadString(stringDataLen); + auto intData = reader->ReadInt32(); + eachToken.first = stringData->Data(); + eachToken.second = intData; + tokenVector->Append(eachToken); + } + + return tokenVector; +} + +DateTime Utils::GetUniversalSystemTime() +{ + SYSTEMTIME sysTime = {}; + GetSystemTime(&sysTime); + + FILETIME sysTimeAsFileTime = {}; + SystemTimeToFileTime(&sysTime, &sysTimeAsFileTime); + + ULARGE_INTEGER ularge; + ularge.HighPart = sysTimeAsFileTime.dwHighDateTime; + ularge.LowPart = sysTimeAsFileTime.dwLowDateTime; + + DateTime result; + result.UniversalTime = ularge.QuadPart; + return result; +} + +bool Utils::IsDateTimeOlderThan(DateTime dateTime, const long long duration) +{ + DateTime now = Utils::GetUniversalSystemTime(); + return dateTime.UniversalTime + duration < now.UniversalTime; +} + +task Utils::WriteFileToFolder(IStorageFolder^ folder, String^ fileName, String^ contents, CreationCollisionOption collisionOption) +{ + if (folder == nullptr) + { + co_return; + } + + StorageFile^ file = co_await folder->CreateFileAsync(fileName, collisionOption); + if (file == nullptr) + { + co_return; + } + + co_await FileIO::WriteTextAsync(file, contents); +} + +task Utils::ReadFileFromFolder(IStorageFolder^ folder, String^ fileName) +{ + if (folder == nullptr) + { + co_return nullptr; + } + + StorageFile^ file = co_await folder->GetFileAsync(fileName); + if (file == nullptr) + { + co_return nullptr; + } + + String^ contents = co_await FileIO::ReadTextAsync(file); + co_return contents; +} diff --git a/src/CalcViewModel/Common/Utils.h b/src/CalcViewModel/Common/Utils.h new file mode 100644 index 00000000..b666b6bb --- /dev/null +++ b/src/CalcViewModel/Common/Utils.h @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Utility macros to make Models easier to write +// generates a member variable called m_ +#define PROPERTY_R(t, n)\ + property t n {\ + t get() { return m_##n; }\ + private: void set(t value) { m_##n = value; }\ + } private: t m_##n; public: + +#define PROPERTY_RW(t, n)\ + property t n {\ + t get() { return m_##n; }\ + void set(t value) { m_##n = value; }\ + } private: t m_##n; public: + +#define OBSERVABLE_PROPERTY_R(t, n)\ + property t n {\ + t get() { return m_##n; }\ + private: void set(t value) {\ + if (m_##n != value) {\ + m_##n = value;\ + RaisePropertyChanged(L#n);\ + }}\ + } private: t m_##n; public: + +#define OBSERVABLE_PROPERTY_RW(t, n)\ + property t n {\ + t get() { return m_##n; }\ + void set(t value) {\ + if (m_##n != value) {\ + m_##n = value;\ + RaisePropertyChanged(L#n);\ + }\ + }\ + } private: t m_##n; public: + +#define NAMED_OBSERVABLE_PROPERTY_RW(t, n)\ + OBSERVABLE_PROPERTY_RW(t, n)\ + private: property Platform::StringReference n##_PropertyName {\ + Platform::StringReference get() { return Platform::StringReference(L#n); }\ + } public: + +#define OBSERVABLE_PROPERTY_FIELD(n) m_##n + +// This variant of the observable object is for objects that don't want to react to property changes +#ifndef UNIT_TESTS +#define OBSERVABLE_OBJECT() virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged;\ + internal: void RaisePropertyChanged(Platform::String^ p) {\ + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(p)); } public: +#else +#define OBSERVABLE_OBJECT() virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged;\ + internal: void RaisePropertyChanged(Platform::String^ p) {\ + } public: +#endif + +// The callback specified in the macro is a method in the class that will be called every time the object changes +// the callback is supposed to be have a single parameter of type Platform::String^ +#ifndef UNIT_TESTS +#define OBSERVABLE_OBJECT_CALLBACK(c) virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged;\ + internal: void RaisePropertyChanged(Platform::String^ p) {\ + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(p));\ + c(p);\ + } public: +#else +#define OBSERVABLE_OBJECT_CALLBACK(c) virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged;\ + internal: void RaisePropertyChanged(Platform::String^ p) {\ + c(p);\ + } public: +#endif + +// The variable member generated by this macro should not be used in the class code, use the +// property getter instead. +#define COMMAND_FOR_METHOD(p, m) property Windows::UI::Xaml::Input::ICommand^ p {\ + Windows::UI::Xaml::Input::ICommand^ get() {\ + if (!donotuse_##p) {\ + donotuse_##p = CalculatorApp::Common::MakeDelegate(this, &m);\ + } return donotuse_##p; }} private: Windows::UI::Xaml::Input::ICommand^ donotuse_##p; public: + +#define DEPENDENCY_PROPERTY_DECLARATION(t, n)\ + property t n {\ + t get() { return safe_cast(GetValue(s_##n##Property)); }\ + void set(t value) { SetValue(s_##n##Property, value); } }\ + private: static Windows::UI::Xaml::DependencyProperty^ s_##n##Property; public: + +// Utilities for DependencyProperties +namespace Utils +{ + namespace Details + { + template + struct IsRefClass + { + static const bool value = __is_ref_class(T); + }; + + template + struct RemoveHat + { + typedef T type; + }; + + template + struct RemoveHat + { + typedef T type; + }; + + template + typename std::enable_if::value, T^>::type MakeDefault() + { + return nullptr; + } + + template + typename std::enable_if::value, T>::type MakeDefault() + { + return T(); + } + + // There's a bug in Xaml in which custom enums are not recognized by the property system/binding + // therefore this template will determine that for enums the type to use to register the + // DependencyProperty is to be Object, for everything else it will use the type + // NOTE: If we are to find more types in which this is broken this template + // will be specialized for those types to return Object + template + struct TypeToUseForDependencyProperty + { + typedef typename std::conditional::value, Platform::Object, T>::type type; + }; + } + + const wchar_t LRE = 0x202a; // Left-to-Right Embedding + const wchar_t PDF = 0x202c; // Pop Directional Formatting + const wchar_t LRO = 0x202d; // Left-to-Right Override + + // Regular DependencyProperty + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty( + _In_ const wchar_t* const name, + _In_ Windows::UI::Xaml::PropertyMetadata^ metadata) + { + typedef typename Details::RemoveHat::type OwnerType; + typedef typename Details::RemoveHat::type ThisPropertyType; + typedef typename Details::TypeToUseForDependencyProperty::type ThisDependencyPropertyType; + + static_assert(Details::IsRefClass::value, "The owner of a DependencyProperty must be a ref class"); + + return Windows::UI::Xaml::DependencyProperty::Register( + Platform::StringReference(name), + ThisDependencyPropertyType::typeid, // Work around bugs in Xaml by using the filtered type + OwnerType::typeid, + metadata); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty(_In_ const wchar_t* const name) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata(Details::MakeDefault())); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyProperty(_In_ const wchar_t* const name, TType defaultValue) + { + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata(defaultValue)); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyWithCallback( + _In_ wchar_t const * const name, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + Details::MakeDefault(), + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyWithCallback( + _In_ wchar_t const * const name, + TType defaultValue, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyProperty( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + defaultValue, + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + // Attached DependencyProperty + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttached( + _In_ const wchar_t* const name, + _In_ Windows::UI::Xaml::PropertyMetadata^ metadata) + { + typedef typename Details::RemoveHat::type OwnerType; + typedef typename Details::RemoveHat::type ThisPropertyType; + typedef typename Details::TypeToUseForDependencyProperty::type ThisDependencyPropertyType; + + static_assert(Details::IsRefClass::value, "The owner of a DependencyProperty must be a ref class"); + + return Windows::UI::Xaml::DependencyProperty::RegisterAttached( + Platform::StringReference(name), + ThisDependencyPropertyType::typeid, // Work around bugs in Xaml by using the filtered type + OwnerType::typeid, + metadata); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttached(_In_ const wchar_t* const name) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyPropertyAttached( + name, + ref new Windows::UI::Xaml::PropertyMetadata(Details::MakeDefault())); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttached(_In_ const wchar_t* const name, TType defaultValue) + { + return RegisterDependencyPropertyAttached( + name, + ref new Windows::UI::Xaml::PropertyMetadata(defaultValue)); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttachedWithCallback( + _In_ wchar_t const * const name, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyPropertyAttached( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + Details::MakeDefault(), + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + template + Windows::UI::Xaml::DependencyProperty^ RegisterDependencyPropertyAttachedWithCallback( + _In_ wchar_t const * const name, + TType defaultValue, + TCallback callback) + { + typedef typename Details::RemoveHat::type ThisPropertyType; + return RegisterDependencyPropertyAttached( + name, + ref new Windows::UI::Xaml::PropertyMetadata( + defaultValue, + ref new Windows::UI::Xaml::PropertyChangedCallback(callback))); + } + + template + void Swap(T *ref1, T *ref2) + { + T temp = *ref1; + *ref1 = *ref2; + *ref2 = temp; + } + + void IFTPlatformException(HRESULT hr); + Platform::String^ GetStringValue(Platform::String^ input); + bool IsLastCharacterTarget(std::wstring const &input, wchar_t target); + std::wstring RemoveUnwantedCharsFromWstring(std::wstring inputString, wchar_t* unwantedChars, unsigned int size); + std::wstring RemoveUnwantedCharsFromWstring(std::wstring input); + double GetDoubleFromWstring(std::wstring input); + int GetWindowId(); + void RunOnUIThreadNonblocking(std::function&& function, _In_ Windows::UI::Core::CoreDispatcher^ currentDispatcher); + void SerializeCommandsAndTokens(_In_ std::shared_ptr>> const &tokens, + _In_ std::shared_ptr>> const &commands, + Windows::Storage::Streams::DataWriter^ writer); + + const std::shared_ptr>> DeserializeCommands(Windows::Storage::Streams::DataReader^ reader); + const std::shared_ptr>> DeserializeTokens(Windows::Storage::Streams::DataReader^ reader); + + Windows::Foundation::DateTime GetUniversalSystemTime(); + bool IsDateTimeOlderThan(Windows::Foundation::DateTime dateTime, const long long duration); + + concurrency::task WriteFileToFolder(Windows::Storage::IStorageFolder^ folder, Platform::String^ fileName, Platform::String^ contents, Windows::Storage::CreationCollisionOption collisionOption); + concurrency::task ReadFileFromFolder(Windows::Storage::IStorageFolder^ folder, Platform::String^ fileName); +} + +// This goes into the header to define the property, in the public: section of the class +#define DEPENDENCY_PROPERTY_OWNER(owner)\ + private: typedef owner DependencyPropertiesOwner; public: + +// Normal DependencyProperty +#define DEPENDENCY_PROPERTY(type, name)\ + property type name {\ + type get() { return safe_cast(GetValue(s_##name##Property)); }\ + void set(type value) { SetValue(s_##name##Property, value); }\ + } private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyProperty(L#name); } public: + +#define DEPENDENCY_PROPERTY_WITH_DEFAULT(type, name, defaultValue)\ + property type name {\ + type get() { return safe_cast(GetValue(s_##name##Property)); }\ + void set(type value) { SetValue(s_##name##Property, value); }\ + } private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyProperty(L#name, defaultValue); } public: + +#define DEPENDENCY_PROPERTY_WITH_CALLBACK(type, name)\ + property type name {\ + type get() { return safe_cast(GetValue(s_##name##Property)); }\ + void set(type value) { SetValue(s_##name##Property, value); }\ + } private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyWithCallback(L#name, &On##name##PropertyChangedImpl); }\ + static void On##name##PropertyChangedImpl(Windows::UI::Xaml::DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ args) {\ + auto self = safe_cast(sender);\ + self->On##name##PropertyChanged(safe_cast(args->OldValue), safe_cast(args->NewValue)); } public: + +#define DEPENDENCY_PROPERTY_WITH_DEFAULT_AND_CALLBACK(type, name, defaultValue)\ + property type name {\ + type get() { return safe_cast(GetValue(s_##name##Property)); }\ + void set(type value) { SetValue(s_##name##Property, value); }\ + } private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyWithCallback(L#name, defaultValue, &On##name##PropertyChangedImpl); }\ + static void On##name##PropertyChangedImpl(Windows::UI::Xaml::DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ args) {\ + auto self = safe_cast(sender);\ + self->On##name##PropertyChanged(safe_cast(args->OldValue), safe_cast(args->NewValue)); } public: + +// Attached DependencyProperty +#define DEPENDENCY_PROPERTY_ATTACHED(type, name)\ + static type Get##name(Windows::UI::Xaml::DependencyObject^ target) { return safe_cast(target->GetValue(s_##name##Property)); }\ + static void Set##name(Windows::UI::Xaml::DependencyObject^ target, type value) { target->SetValue(s_##name##Property, value); }\ + private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyAttached(L#name); } public: + +#define DEPENDENCY_PROPERTY_ATTACHED_WITH_DEFAULT(type, name, defaultValue)\ + static type Get##name(Windows::UI::Xaml::DependencyObject^ target) { return safe_cast(target->GetValue(s_##name##Property)); }\ + static void Set##name(Windows::UI::Xaml::DependencyObject^ target, type value) { target->SetValue(s_##name##Property, value); }\ + private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyAttached(L#name, defaultValue); } public: + +#define DEPENDENCY_PROPERTY_ATTACHED_WITH_CALLBACK(type, name)\ + static type Get##name(Windows::UI::Xaml::DependencyObject^ target) { return safe_cast(target->GetValue(s_##name##Property)); }\ + static void Set##name(Windows::UI::Xaml::DependencyObject^ target, type value) { target->SetValue(s_##name##Property, value); }\ + private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyAttachedWithCallback(L#name, &On##name##PropertyChangedImpl); }\ + static void On##name##PropertyChangedImpl(Windows::UI::Xaml::DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ args) {\ + On##name##PropertyChanged(sender, safe_cast(args->OldValue), safe_cast(args->NewValue)); } public: + +#define DEPENDENCY_PROPERTY_ATTACHED_WITH_DEFAULT_AND_CALLBACK(type, name, defaultValue)\ + static type Get##name(Windows::UI::Xaml::DependencyObject^ target) { return safe_cast(target->GetValue(s_##name##Property)); }\ + static void Set##name(Windows::UI::Xaml::DependencyObject^ target, type value) { target->SetValue(s_##name##Property, value); }\ + private: static Windows::UI::Xaml::DependencyProperty^ s_##name##Property;\ + public: static property Windows::UI::Xaml::DependencyProperty^ name##Property {\ + Windows::UI::Xaml::DependencyProperty^ get() { assert(s_##name##Property); return s_##name##Property; }\ + }\ + private: static Windows::UI::Xaml::DependencyProperty^ Initialize##name##Property() {\ + return Utils::RegisterDependencyPropertyAttachedWithCallback(L#name, defaultValue, &On##name##PropertyChangedImpl); }\ + static void On##name##PropertyChangedImpl(Windows::UI::Xaml::DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ args) {\ + On##name##PropertyChanged(sender, safe_cast(args->OldValue), safe_cast(args->NewValue)); } public: + +// This goes into the cpp to initalize the static variable +#define DEPENDENCY_PROPERTY_INITIALIZATION(owner, name)\ + Windows::UI::Xaml::DependencyProperty^ owner::s_##name##Property =\ + owner::Initialize##name##Property(); + +namespace CalculatorApp +{ + template + T from_cx(Platform::Object^ from) + { + T to{ nullptr }; + + winrt::check_hresult(reinterpret_cast<::IUnknown*>(from)->QueryInterface(winrt::guid_of(), + reinterpret_cast(winrt::put_abi(to)))); + + return to; + } +} diff --git a/src/CalcViewModel/Common/ValidatingConverters.h b/src/CalcViewModel/Common/ValidatingConverters.h new file mode 100644 index 00000000..9da3fc1f --- /dev/null +++ b/src/CalcViewModel/Common/ValidatingConverters.h @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp { namespace Common +{ + public ref class ValidSelectedItemConverter sealed: public Windows::UI::Xaml::Data::IValueConverter + { + public: + ValidSelectedItemConverter() + { } + + private: + + virtual Platform::Object^ Convert( + Platform::Object^ value, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::Convert + { + // Pass through as we don't want to change the value from the source + return value; + } + + virtual Platform::Object^ ConverBack( + Platform::Object^ value, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::ConvertBack + { + if (value) + { + return value; + } + // Stop the binding if the object is nullptr + return Windows::UI::Xaml::DependencyProperty::UnsetValue; + } + }; + + public ref class ValidSelectedIndexConverter sealed: public Windows::UI::Xaml::Data::IValueConverter + { + public: + ValidSelectedIndexConverter() + { } + + private: + + virtual Platform::Object^ Convert( + Platform::Object^ value, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::Convert + { + // Pass through as we don't want to change the value from the source + return value; + } + + virtual Platform::Object^ ConverBack( + Platform::Object^ value, + Windows::UI::Xaml::Interop::TypeName /*targetType*/, + Platform::Object^ /*parameter*/, + Platform::String^ /*language*/) = Windows::UI::Xaml::Data::IValueConverter::ConvertBack + { + // The value to be valid has to be a boxed int32 value + // extract that value and ensure it is valid, ie >= 0 + if (value) + { + auto box = dynamic_cast(value); + if (box && box->Type == Windows::Foundation::PropertyType::Int32) + { + int index = box->GetInt32(); + if (index >= 0) + { + return value; + } + } + } + // The value is not valid therefore stop the binding right here + return Windows::UI::Xaml::DependencyProperty::UnsetValue; + } + }; +}} diff --git a/src/CalcViewModel/DataLoaders/CurrencyDataLoader.cpp b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.cpp new file mode 100644 index 00000000..aea37617 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.cpp @@ -0,0 +1,814 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CurrencyDataLoader.h" +#include "UnitConverterDataConstants.h" +#include "Common\AppResourceProvider.h" +#include "Common\LocalizationStringUtil.h" +#include "Common\LocalizationService.h" +#include "Common\LocalizationSettings.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::LocalizationServiceProperties; +using namespace CalculatorApp::DataLoaders; +using namespace CalculatorApp::ViewModel; +using namespace CalculatorApp::ViewModel::CurrencyDataLoaderConstants; +using namespace concurrency; +using namespace Platform; +using namespace std; +using namespace UnitConversionManager; +using namespace Windows::ApplicationModel::Resources::Core; +using namespace Windows::Data::Json; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Globalization::DateTimeFormatting; +using namespace Windows::Globalization::NumberFormatting; +using namespace Windows::Storage; +using namespace Windows::System::UserProfile; +using namespace Windows::UI::Core; +using namespace Windows::Web::Http; + +static constexpr auto CURRENCY_UNIT_FROM_KEY = L"CURRENCY_UNIT_FROM_KEY"; +static constexpr auto CURRENCY_UNIT_TO_KEY = L"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) +static constexpr long long DAY_DURATION = 1LL * 60 * 60 * 24 * 10000000; +static constexpr long long WEEK_DURATION = DAY_DURATION * 7; + +static constexpr int FORMATTER_DIGIT_COUNT = 4; + +static constexpr auto CACHE_TIMESTAMP_KEY = L"CURRENCY_CONVERTER_TIMESTAMP"; +static constexpr auto CACHE_LANGCODE_KEY = L"CURRENCY_CONVERTER_LANGCODE"; +static constexpr auto CACHE_DELIMITER = L"%"; + +static constexpr auto STATIC_DATA_FILENAME = L"CURRENCY_CONVERTER_STATIC_DATA.txt"; +static constexpr array STATIC_DATA_PROPERTIES = { + wstring_view{ L"CountryCode", 11 }, + wstring_view{ L"CountryName", 11 }, + wstring_view{ L"CurrencyCode", 12 }, + wstring_view{ L"CurrencyName", 12 }, + wstring_view{ L"CurrencySymbol", 14 } +}; + +static constexpr auto ALL_RATIOS_DATA_FILENAME = L"CURRENCY_CONVERTER_ALL_RATIOS_DATA.txt"; +static constexpr auto RATIO_KEY = L"Rt"; +static constexpr auto CURRENCY_CODE_KEY = L"An"; +static constexpr array ALL_RATIOS_DATA_PROPERTIES = { + wstring_view{ RATIO_KEY, 2 }, + wstring_view{ CURRENCY_CODE_KEY, 2 } +}; + +static constexpr auto DEFAULT_FROM_TO_CURRENCY_FILE_URI = L"ms-appx:///DataLoaders/DefaultFromToCurrency.json"; +static constexpr auto FROM_KEY = L"from"; +static constexpr auto TO_KEY = L"to"; + +// Fallback default values. +static constexpr auto DEFAULT_FROM_CURRENCY = DefaultCurrencyCode.data(); +static constexpr auto DEFAULT_TO_CURRENCY = L"EUR"; + +namespace CalculatorApp +{ + namespace ViewModel + { + namespace UnitConverterResourceKeys + { + StringReference CurrencyUnitFromKey(CURRENCY_UNIT_FROM_KEY); + StringReference CurrencyUnitToKey(CURRENCY_UNIT_TO_KEY); + } + + namespace CurrencyDataLoaderConstants + { + StringReference CacheTimestampKey(CACHE_TIMESTAMP_KEY); + StringReference CacheLangcodeKey(CACHE_LANGCODE_KEY); + StringReference CacheDelimiter(CACHE_DELIMITER); + StringReference StaticDataFilename(STATIC_DATA_FILENAME); + StringReference AllRatiosDataFilename(ALL_RATIOS_DATA_FILENAME); + long long DayDuration = DAY_DURATION; + } + } +} + +CurrencyDataLoader::CurrencyDataLoader(_In_ unique_ptr client) : + m_client(move(client)), + m_loadStatus(CurrencyLoadStatus::NotLoaded), + m_responseLanguage(L"en-US"), + m_ratioFormat(L""), + m_timestampFormat(L""), + m_networkManager(ref new NetworkManager()), + m_meteredOverrideSet(false) +{ + if (GlobalizationPreferences::Languages->Size > 0) + { + m_responseLanguage = GlobalizationPreferences::Languages->GetAt(0); + } + + if (m_client != nullptr) + { + m_client->SetSourceCurrencyCode(StringReference(DefaultCurrencyCode.data())); + m_client->SetResponseLanguage(m_responseLanguage); + } + + if (CoreWindow::GetForCurrentThread() != nullptr) + { + // 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(L"CurrencyFromToRatioFormat")->Data(); + m_timestampFormat = AppResourceProvider::GetInstance().GetResourceString(L"CurrencyTimestampFormat")->Data(); +} + +CurrencyDataLoader::~CurrencyDataLoader() +{ + UnregisterForNetworkBehaviorChanges(); +} + +void CurrencyDataLoader::UnregisterForNetworkBehaviorChanges() +{ + m_networkManager->NetworkBehaviorChanged -= m_networkBehaviorToken; +} + +void CurrencyDataLoader::RegisterForNetworkBehaviorChanges() +{ + UnregisterForNetworkBehaviorChanges(); + + m_networkBehaviorToken = + m_networkManager->NetworkBehaviorChanged += ref new NetworkBehaviorChangedHandler([this](NetworkAccessBehavior newBehavior) + { + this->OnNetworkBehaviorChanged(newBehavior); + }); + + OnNetworkBehaviorChanged(NetworkManager::GetNetworkAccessBehavior()); +} + +void CurrencyDataLoader::OnNetworkBehaviorChanged(NetworkAccessBehavior newBehavior) +{ + m_networkAccessBehavior = newBehavior; + if (m_vmCallback != nullptr) + { + m_vmCallback->NetworkBehaviorChanged(static_cast(m_networkAccessBehavior)); + } +} + +bool CurrencyDataLoader::LoadFinished() +{ + return m_loadStatus != CurrencyLoadStatus::NotLoaded; +} + +bool CurrencyDataLoader::LoadedFromCache() +{ + return m_loadStatus == CurrencyLoadStatus::LoadedFromCache; +} + +bool CurrencyDataLoader::LoadedFromWeb() +{ + return m_loadStatus == CurrencyLoadStatus::LoadedFromWeb; +} + +void CurrencyDataLoader::ResetLoadStatus() +{ + m_loadStatus = CurrencyLoadStatus::NotLoaded; +} + +#pragma optimize("", off) // Turn off optimizations to work around DevDiv 393321 +void CurrencyDataLoader::LoadData() +{ + RegisterForNetworkBehaviorChanges(); + + if (!LoadFinished()) + { + create_task([this]() -> task + { + vector()>> loadFunctions = { + [this]() { return TryLoadDataFromCacheAsync(); }, + [this]() { return TryLoadDataFromWebAsync(); }, + }; + + bool didLoad = false; + for (auto& f : loadFunctions) + { + didLoad = co_await f(); + if (didLoad) + { + break; + } + } + + co_return didLoad; + }).then([this](bool didLoad) + { + UpdateDisplayedTimestamp(); + NotifyDataLoadFinished(didLoad); + }, task_continuation_context::use_current()); + } +}; +#pragma optimize("", on) + +vector CurrencyDataLoader::LoadOrderedCategories() +{ + // This function should not be called + // The model will use the categories from UnitConverterDataLoader + return vector(); +} + +vector CurrencyDataLoader::LoadOrderedUnits(const UCM::Category& /*category*/) +{ + lock_guard lock(m_currencyUnitsMutex); + return m_currencyUnits; +} + +unordered_map CurrencyDataLoader::LoadOrderedRatios(const UCM::Unit& unit) +{ + lock_guard lock(m_currencyUnitsMutex); + return m_currencyRatioMap.at(unit); +} + +bool CurrencyDataLoader::SupportsCategory(const UCM::Category& target) +{ + static int currencyId = NavCategory::Serialize(ViewMode::Currency); + return target.id == currencyId; +} + +void CurrencyDataLoader::SetViewModelCallback(const shared_ptr& callback) +{ + m_vmCallback = callback; + OnNetworkBehaviorChanged(m_networkAccessBehavior); +} + +pair CurrencyDataLoader::GetCurrencySymbols(const UCM::Unit& unit1, const UCM::Unit& unit2) +{ + lock_guard lock(m_currencyUnitsMutex); + + wstring symbol1 = L""; + wstring symbol2 = L""; + + auto itr1 = m_currencyMetadata.find(unit1); + auto itr2 = m_currencyMetadata.find(unit2); + if (itr1 != m_currencyMetadata.end() && itr2 != m_currencyMetadata.end()) + { + symbol1 = (itr1->second).symbol; + symbol2 = (itr2->second).symbol; + } + + return make_pair(symbol1, symbol2); +} + +pair CurrencyDataLoader::GetCurrencyRatioEquality(_In_ const UCM::Unit& unit1, _In_ const UCM::Unit& unit2) +{ + try + { + auto iter = m_currencyRatioMap.find(unit1); + if (iter != m_currencyRatioMap.end()) + { + unordered_map ratioMap = iter->second; + auto iter2 = ratioMap.find(unit2); + if (iter2 != ratioMap.end()) + { + double ratio = (iter2->second).ratio; + + // Round the raio 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 = pow(10, FORMATTER_DIGIT_COUNT); + double rounded = static_cast(ratio * static_cast(scale)) / scale; + + wstring digitSymbol = wstring{ LocalizationSettings::GetInstance().GetDigitSymbolFromEnUsDigit(L'1') }; + wstring roundedFormat = m_ratioFormatter->Format(rounded)->Data(); + + wstring ratioString = LocalizationStringUtil::GetLocalizedString( + m_ratioFormat.c_str(), + digitSymbol.c_str(), + unit1.abbreviation.c_str(), + roundedFormat.c_str(), + unit2.abbreviation.c_str() + ); + + wstring accessibleRatioString = LocalizationStringUtil::GetLocalizedString( + m_ratioFormat.c_str(), + digitSymbol.c_str(), + unit1.accessibleName.c_str(), + roundedFormat.c_str(), + unit2.accessibleName.c_str() + ); + + return make_pair(ratioString, accessibleRatioString); + } + } + } + catch (...) {} + + return make_pair(L"", L""); +} + +#pragma optimize("", off) // Turn off optimizations to work around DevDiv 393321 +task CurrencyDataLoader::TryLoadDataFromCacheAsync() +{ + try + { + ResetLoadStatus(); + + auto localSettings = ApplicationData::Current->LocalSettings; + if (localSettings == nullptr || !localSettings->Values->HasKey(CacheTimestampKey)) + { + co_return false; + } + + bool loadComplete = false; + m_cacheTimestamp = static_cast(localSettings->Values->Lookup(CacheTimestampKey)); + if (Utils::IsDateTimeOlderThan(m_cacheTimestamp, DAY_DURATION) + && m_networkAccessBehavior == NetworkAccessBehavior::Normal) + { + loadComplete = co_await TryLoadDataFromWebAsync(); + } + + if (!loadComplete) + { + loadComplete = co_await TryFinishLoadFromCacheAsync(); + } + + co_return loadComplete; + } + catch (Exception^ ex) + { + TraceLogger::GetInstance().LogPlatformException(__FUNCTIONW__, ex); + co_return false; + } + catch (const exception& e) + { + TraceLogger::GetInstance().LogStandardException(__FUNCTIONW__, e); + co_return false; + } + catch (...) + { + co_return false; + } +} + +task CurrencyDataLoader::TryFinishLoadFromCacheAsync() +{ + auto localSettings = ApplicationData::Current->LocalSettings; + if (localSettings == nullptr) + { + co_return false; + } + + if (!localSettings->Values->HasKey(CacheLangcodeKey) + || !static_cast(localSettings->Values->Lookup(CacheLangcodeKey))->Equals(m_responseLanguage)) + { + co_return false; + } + + StorageFolder^ localCacheFolder = ApplicationData::Current->LocalCacheFolder; + if (localCacheFolder == nullptr) + { + co_return false; + } + + String^ staticDataResponse = co_await Utils::ReadFileFromFolder(localCacheFolder, StaticDataFilename); + String^ allRatiosResponse = co_await Utils::ReadFileFromFolder(localCacheFolder, AllRatiosDataFilename); + + vector staticData{}; + CurrencyRatioMap ratioMap{}; + + bool didParse = TryParseWebResponses( + staticDataResponse, + allRatiosResponse, + staticData, + ratioMap); + if (!didParse) + { + co_return false; + } + + m_loadStatus = CurrencyLoadStatus::LoadedFromCache; + co_await FinalizeUnits(staticData, ratioMap); + + co_return true; +} + +task CurrencyDataLoader::TryLoadDataFromWebAsync() +{ + try + { + ResetLoadStatus(); + + if (m_client == nullptr) + { + co_return false; + } + + if (m_networkAccessBehavior == NetworkAccessBehavior::Offline || + (m_networkAccessBehavior == NetworkAccessBehavior::OptIn && !m_meteredOverrideSet)) + { + co_return false; + } + + String^ staticDataResponse = co_await m_client->GetCurrencyMetadata(); + String^ allRatiosResponse = co_await m_client->GetCurrencyRatios(); + if (staticDataResponse == nullptr || allRatiosResponse == nullptr) + { + co_return false; + } + + vector staticData{}; + CurrencyRatioMap ratioMap{}; + + bool didParse = TryParseWebResponses( + staticDataResponse, + allRatiosResponse, + staticData, + ratioMap); + if (!didParse) + { + co_return false; + } + + // Set the timestamp before saving it below. + m_cacheTimestamp = Utils::GetUniversalSystemTime(); + + try + { + const vector> cachedFiles = { + { StaticDataFilename, staticDataResponse }, + { AllRatiosDataFilename, allRatiosResponse } + }; + + StorageFolder^ localCacheFolder = ApplicationData::Current->LocalCacheFolder; + for (const auto& fileInfo : cachedFiles) + { + co_await Utils::WriteFileToFolder( + localCacheFolder, + fileInfo.first, + fileInfo.second, + CreationCollisionOption::ReplaceExisting); + } + + SaveLangCodeAndTimestamp(); + } + catch (...) + { + // If we fail to save to cache it's okay, we should still continue. + } + + m_loadStatus = CurrencyLoadStatus::LoadedFromWeb; + co_await FinalizeUnits(staticData, ratioMap); + + co_return true; + } + catch (Exception^ ex) + { + TraceLogger::GetInstance().LogPlatformException(__FUNCTIONW__, ex); + co_return false; + } + catch (const exception& e) + { + TraceLogger::GetInstance().LogStandardException(__FUNCTIONW__, e); + co_return false; + } + catch (...) + { + co_return false; + } +} + +task CurrencyDataLoader::TryLoadDataFromWebOverrideAsync() +{ + m_meteredOverrideSet = true; + bool didLoad = co_await TryLoadDataFromWebAsync(); + if (!didLoad) + { + m_loadStatus = CurrencyLoadStatus::FailedToLoad; + TraceLogger::GetInstance().LogUserRequestedRefreshFailed(); + } + + co_return didLoad; +}; +#pragma optimize("", on) + +bool CurrencyDataLoader::TryParseWebResponses( + _In_ String^ staticDataJson, + _In_ String^ allRatiosJson, + _Inout_ vector& staticData, + _Inout_ CurrencyRatioMap& allRatiosData) +{ + return TryParseStaticData(staticDataJson, staticData) + && TryParseAllRatiosData(allRatiosJson, allRatiosData); +} + +bool CurrencyDataLoader::TryParseStaticData(_In_ String^ rawJson, _Inout_ vector& staticData) +{ + JsonArray^ data = nullptr; + if (!JsonArray::TryParse(rawJson, &data)) + { + return false; + } + + wstring countryCode{ L"" }; + wstring countryName{ L"" }; + wstring currencyCode{ L"" }; + wstring currencyName{ L"" }; + wstring currencySymbol{ L"" }; + + vector values = { + &countryCode, + &countryName, + ¤cyCode, + ¤cyName, + ¤cySymbol + }; + + assert(values.size() == STATIC_DATA_PROPERTIES.size()); + staticData.resize(size_t{ data->Size }); + for (unsigned int i = 0; i < data->Size; i++) + { + JsonObject^ obj = data->GetAt(i)->GetObject(); + + for (size_t j = 0; j < values.size(); j++) + { + (*values[j]) = obj->GetNamedString(StringReference(STATIC_DATA_PROPERTIES[j].data()))->Data(); + } + + staticData[i] = CurrencyStaticData{ + countryCode, + countryName, + currencyCode, + currencyName, + currencySymbol + }; + } + + // TODO - MSFT 8533667: this sort will be replaced by a WinRT call to sort localized strings + sort(begin(staticData), end(staticData), [](CurrencyStaticData unit1, CurrencyStaticData unit2) + { + return unit1.countryName < unit2.countryName; + }); + + return true; +} + +bool CurrencyDataLoader::TryParseAllRatiosData(_In_ String^ rawJson, _Inout_ CurrencyRatioMap& allRatios) +{ + JsonArray^ data = nullptr; + if (!JsonArray::TryParse(rawJson, &data)) + { + return false; + } + + wstring sourceCurrencyCode{ DefaultCurrencyCode }; + + allRatios.clear(); + for (unsigned int i = 0; i < data->Size; i++) + { + JsonObject^ obj = data->GetAt(i)->GetObject(); + + // Rt is ratio, An is target currency ISO code. + double relativeRatio = obj->GetNamedNumber(StringReference(RATIO_KEY)); + wstring targetCurrencyCode = obj->GetNamedString(StringReference(CURRENCY_CODE_KEY))->Data(); + + allRatios.emplace(targetCurrencyCode, 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 curreny ratios. +#pragma optimize("", off) // Turn off optimizations to work around DevDiv 393321 +task CurrencyDataLoader::FinalizeUnits(_In_ const vector& staticData, _In_ const CurrencyRatioMap& ratioMap) +{ + unordered_map> idToUnit{}; + + SelectedUnits defaultCurrencies = co_await GetDefaultFromToCurrency(); + wstring fromCurrency = defaultCurrencies.first; + wstring toCurrency = defaultCurrencies.second; + + { + lock_guard lock(m_currencyUnitsMutex); + + int i = 1; + m_currencyUnits.clear(); + m_currencyMetadata.clear(); + bool isConversionSourceSet = false; + bool isConversionTargetSet = false; + for (const UCM::CurrencyStaticData& currencyUnit : staticData) + { + auto itr = ratioMap.find(currencyUnit.currencyCode); + if (itr != ratioMap.end() && (itr->second).ratio > 0) + { + int id = static_cast(UnitConverterUnits::UnitEnd + i); + + bool isConversionSource = (fromCurrency == currencyUnit.currencyCode); + isConversionSourceSet = isConversionSourceSet || isConversionSource; + + bool isConversionTarget = (toCurrency == currencyUnit.currencyCode); + isConversionTargetSet = isConversionTargetSet || isConversionTarget; + + UCM::Unit unit = UCM::Unit{ + id, // id + currencyUnit.currencyName, // currencyName + currencyUnit.countryName, // countryName + currencyUnit.currencyCode, // abbreviation + m_isRtlLanguage, // isRtlLanguage + isConversionSource, // isConversionSource + isConversionTarget // isConversionTarget + }; + + m_currencyUnits.push_back(unit); + m_currencyMetadata.emplace(unit, CurrencyUnitMetadata{ currencyUnit.currencySymbol }); + idToUnit.insert(pair>(unit.id, pair(unit, (itr->second).ratio))); + i++; + } + } + + if (!isConversionSourceSet || !isConversionTargetSet) + { + GuaranteeSelectedUnits(); + defaultCurrencies = { DEFAULT_FROM_CURRENCY, DEFAULT_TO_CURRENCY }; + } + + m_currencyRatioMap.clear(); + for (const auto& unit : m_currencyUnits) + { + unordered_map conversions; + double unitFactor = idToUnit[unit.id].second; + for (auto itr = idToUnit.begin(); itr != idToUnit.end(); itr++) + { + UCM::Unit targetUnit = (itr->second).first; + double conversionRatio = (itr->second).second; + UCM::ConversionData parsedData = { 1.0, 0.0, false }; + assert(unitFactor > 0); // divide by zero assert + parsedData.ratio = conversionRatio / unitFactor; + conversions.insert(make_pair(targetUnit, parsedData)); + } + + m_currencyRatioMap.insert(make_pair(unit, conversions)); + } + } // unlocked m_currencyUnitsMutex + + SaveSelectedUnitsToLocalSettings(defaultCurrencies); +}; +#pragma optimize("", on) + +void CurrencyDataLoader::GuaranteeSelectedUnits() +{ + bool isConversionSourceSet = false; + bool isConversionTargetSet = false; + for (UCM::Unit& unit : 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 CurrencyDataLoader::NotifyDataLoadFinished(bool didLoad) +{ + if (!didLoad) + { + m_loadStatus = CurrencyLoadStatus::FailedToLoad; + } + + if (m_vmCallback != nullptr) + { + m_vmCallback->CurrencyDataLoadFinished(didLoad); + } +} + +void CurrencyDataLoader::SaveLangCodeAndTimestamp() +{ + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + if (localSettings == nullptr) + { + return; + } + + localSettings->Values->Insert(CacheTimestampKey, m_cacheTimestamp); + localSettings->Values->Insert(CacheLangcodeKey, m_responseLanguage); +} + +void CurrencyDataLoader::UpdateDisplayedTimestamp() +{ + if (m_vmCallback != nullptr) + { + wstring timestamp = GetCurrencyTimestamp(); + bool isWeekOld = Utils::IsDateTimeOlderThan(m_cacheTimestamp, WEEK_DURATION); + + m_vmCallback->CurrencyTimestampCallback(timestamp, isWeekOld); + } +} +wstring CurrencyDataLoader::GetCurrencyTimestamp() +{ + wstring timestamp = L""; + + DateTime epoch{}; + if (m_cacheTimestamp.UniversalTime != epoch.UniversalTime) + { + DateTimeFormatter^ dateFormatter = ref new DateTimeFormatter(L"{month.abbreviated} {day.integer}, {year.full}"); + wstring date = dateFormatter->Format(m_cacheTimestamp)->Data(); + + DateTimeFormatter^ timeFormatter = ref new DateTimeFormatter(L"shorttime"); + wstring time = timeFormatter->Format(m_cacheTimestamp)->Data(); + + timestamp = LocalizationStringUtil::GetLocalizedString( + m_timestampFormat.c_str(), + date.c_str(), + time.c_str() + ); + } + + return timestamp; +} + +#pragma optimize("", off) // Turn off optimizations to work around DevDiv 393321 +task CurrencyDataLoader::GetDefaultFromToCurrency() +{ + wstring fromCurrency{ DEFAULT_FROM_CURRENCY }; + wstring toCurrency{ DEFAULT_TO_CURRENCY }; + + // First, check if we previously stored the last used currencies. + bool foundInLocalSettings = TryGetLastUsedCurrenciesFromLocalSettings(&fromCurrency, &toCurrency); + if (!foundInLocalSettings) + { + try + { + // Second, see if the current locale has preset defaults in DefaultFromToCurrency.json. + Uri^ fileUri = ref new Uri(StringReference(DEFAULT_FROM_TO_CURRENCY_FILE_URI)); + StorageFile^ defaultFromToCurrencyFile = co_await StorageFile::GetFileFromApplicationUriAsync(fileUri); + if (defaultFromToCurrencyFile != nullptr) + { + String^ fileContents = co_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(StringReference(FROM_KEY)); + String^ selectedTo = regionalDefaults->GetNamedString(StringReference(TO_KEY)); + + fromCurrency = selectedFrom->Data(); + toCurrency = selectedTo->Data(); + } + } + catch (...) {} + } + + co_return make_pair(fromCurrency, toCurrency); +}; +#pragma optimize("", on) + +bool CurrencyDataLoader::TryGetLastUsedCurrenciesFromLocalSettings(_Out_ wstring* const fromCurrency, _Out_ wstring* const toCurrency) +{ + String^ fromKey = UnitConverterResourceKeys::CurrencyUnitFromKey; + String^ toKey = UnitConverterResourceKeys::CurrencyUnitToKey; + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + if (localSettings != nullptr && localSettings->Values != nullptr) + { + IPropertySet^ values = localSettings->Values; + if (values->HasKey(fromKey) && values->HasKey(toKey)) + { + *fromCurrency = static_cast(values->Lookup(fromKey))->Data(); + *toCurrency = static_cast(values->Lookup(toKey))->Data(); + + return true; + } + } + + return false; +} + +void CurrencyDataLoader::SaveSelectedUnitsToLocalSettings(_In_ const SelectedUnits& selectedUnits) +{ + String^ fromKey = UnitConverterResourceKeys::CurrencyUnitFromKey; + String^ toKey = UnitConverterResourceKeys::CurrencyUnitToKey; + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + if (localSettings != nullptr && localSettings->Values != nullptr) + { + IPropertySet^ values = localSettings->Values; + values->Insert(fromKey, StringReference(selectedUnits.first.c_str())); + values->Insert(toKey, StringReference(selectedUnits.second.c_str())); + } +} diff --git a/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h new file mode 100644 index 00000000..e7e97382 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/CurrencyDataLoader.h @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Common\NetworkManager.h" +#include "ICurrencyHttpClient.h" + +namespace CalculatorApp +{ + namespace ViewModel + { + public enum class CurrencyLoadStatus + { + NotLoaded = 0, + FailedToLoad = 1, + LoadedFromCache = 2, + LoadedFromWeb = 3 + }; + + namespace UnitConverterResourceKeys + { + extern Platform::StringReference CurrencyUnitFromKey; + extern Platform::StringReference CurrencyUnitToKey; + } + + namespace CurrencyDataLoaderConstants + { + extern Platform::StringReference CacheTimestampKey; + extern Platform::StringReference CacheLangcodeKey; + extern Platform::StringReference CacheDelimiter; + extern Platform::StringReference StaticDataFilename; + extern Platform::StringReference AllRatiosDataFilename; + extern long long DayDuration; + } + + namespace UCM = UnitConversionManager; + + typedef std::unordered_map CurrencyRatioMap; + typedef std::pair SelectedUnits; + + struct CurrencyUnitMetadata + { + CurrencyUnitMetadata(const std::wstring& s) : symbol(s) {} + + const std::wstring symbol; + }; + + class CurrencyDataLoader : public UCM::IConverterDataLoader, + public UCM::ICurrencyConverterDataLoader + { + public: + CurrencyDataLoader(_In_ std::unique_ptr client); + ~CurrencyDataLoader(); + + bool LoadFinished(); + bool LoadedFromCache(); + bool LoadedFromWeb(); + + // IConverterDataLoader + void LoadData() override; + std::vector LoadOrderedCategories() override; + std::vector LoadOrderedUnits(const UCM::Category& category) override; + std::unordered_map LoadOrderedRatios(const UCM::Unit& unit) override; + bool SupportsCategory(const UnitConversionManager::Category& target) override; + // IConverterDataLoader + + // ICurrencyConverterDataLoader + void SetViewModelCallback(const std::shared_ptr& callback) override; + std::pair GetCurrencySymbols(const UCM::Unit& unit1, const UCM::Unit& unit2) override; + std::pair GetCurrencyRatioEquality(_In_ const UnitConversionManager::Unit& unit1, _In_ const UnitConversionManager::Unit& unit2) override; + std::wstring GetCurrencyTimestamp() override; + + concurrency::task TryLoadDataFromCacheAsync() override; + concurrency::task TryLoadDataFromWebAsync() override; + concurrency::task TryLoadDataFromWebOverrideAsync() override; + // ICurrencyConverterDataLoader + + void OnNetworkBehaviorChanged(CalculatorApp::NetworkAccessBehavior newBehavior); + + private: + void ResetLoadStatus(); + void NotifyDataLoadFinished(bool didLoad); + + concurrency::task TryFinishLoadFromCacheAsync(); + + bool TryParseWebResponses( + _In_ Platform::String^ staticDataJson, + _In_ Platform::String^ allRatiosJson, + _Inout_ std::vector& staticData, + _Inout_ CurrencyRatioMap& allRatiosData); + bool TryParseStaticData(_In_ Platform::String^ rawJson, _Inout_ std::vector& staticData); + bool TryParseAllRatiosData(_In_ Platform::String^ rawJson, _Inout_ CurrencyRatioMap& allRatiosData); + concurrency::task FinalizeUnits(_In_ const std::vector& staticData, _In_ const CurrencyRatioMap& ratioMap); + void GuaranteeSelectedUnits(); + + void SaveLangCodeAndTimestamp(); + void UpdateDisplayedTimestamp(); + + void RegisterForNetworkBehaviorChanges(); + void UnregisterForNetworkBehaviorChanges(); + + concurrency::task GetDefaultFromToCurrency(); + bool TryGetLastUsedCurrenciesFromLocalSettings(_Out_ std::wstring* const fromCurrency, _Out_ std::wstring* const toCurrency); + void SaveSelectedUnitsToLocalSettings(_In_ const SelectedUnits& selectedUnits); + + private: + Platform::String^ m_responseLanguage; + std::unique_ptr m_client; + + bool m_isRtlLanguage; + + std::mutex m_currencyUnitsMutex; + std::vector m_currencyUnits; + UCM::UnitToUnitToConversionDataMap m_currencyRatioMap; + std::unordered_map m_currencyMetadata; + + std::shared_ptr m_vmCallback; + + Windows::Globalization::NumberFormatting::DecimalFormatter^ m_ratioFormatter; + std::wstring m_ratioFormat; + Windows::Foundation::DateTime m_cacheTimestamp; + std::wstring m_timestampFormat; + + CurrencyLoadStatus m_loadStatus; + + CalculatorApp::NetworkManager^ m_networkManager; + CalculatorApp::NetworkAccessBehavior m_networkAccessBehavior; + Windows::Foundation::EventRegistrationToken m_networkBehaviorToken; + bool m_meteredOverrideSet; + }; + } +} diff --git a/src/CalcViewModel/DataLoaders/CurrencyHttpClient.cpp b/src/CalcViewModel/DataLoaders/CurrencyHttpClient.cpp new file mode 100644 index 00000000..7a2658a3 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/CurrencyHttpClient.cpp @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "CurrencyHttpClient.h" + +using namespace CalculatorApp::DataLoaders; +using namespace Platform; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Web::Http; + +static constexpr auto sc_MetadataUriLocalizeFor = L"https://go.microsoft.com/fwlink/?linkid=2041093&localizeFor="; +static constexpr auto sc_RatiosUriRelativeTo = L"https://go.microsoft.com/fwlink/?linkid=2041339&localCurrency="; + +CurrencyHttpClient::CurrencyHttpClient() : + m_client(ref new HttpClient()), + m_responseLanguage(L"en-US") +{ +} + +void CurrencyHttpClient::SetSourceCurrencyCode(String^ sourceCurrencyCode) +{ + m_sourceCurrencyCode = sourceCurrencyCode; +} + +void CurrencyHttpClient::SetResponseLanguage(String^ responseLanguage) +{ + m_responseLanguage = responseLanguage; +} + +IAsyncOperationWithProgress^ CurrencyHttpClient::GetCurrencyMetadata() +{ + wstring uri = wstring{ sc_MetadataUriLocalizeFor } + m_responseLanguage->Data(); + auto metadataUri = ref new Uri(StringReference(uri.c_str())); + + return m_client->GetStringAsync(metadataUri); +} + +IAsyncOperationWithProgress^ CurrencyHttpClient::GetCurrencyRatios() +{ + wstring uri = wstring{ sc_RatiosUriRelativeTo } + m_sourceCurrencyCode->Data(); + auto ratiosUri = ref new Uri(StringReference(uri.c_str())); + + return m_client->GetStringAsync(ratiosUri); +} diff --git a/src/CalcViewModel/DataLoaders/CurrencyHttpClient.h b/src/CalcViewModel/DataLoaders/CurrencyHttpClient.h new file mode 100644 index 00000000..8187c74a --- /dev/null +++ b/src/CalcViewModel/DataLoaders/CurrencyHttpClient.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "ICurrencyHttpClient.h" + +namespace CalculatorApp +{ + namespace DataLoaders + { + class CurrencyHttpClient : public ICurrencyHttpClient + { + public: + CurrencyHttpClient(); + + void SetSourceCurrencyCode(Platform::String^ sourceCurrencyCode) override; + void SetResponseLanguage(Platform::String^ responseLanguage) override; + + Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyMetadata() override; + Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyRatios() override; + + private: + Windows::Web::Http::HttpClient^ m_client; + Platform::String^ m_responseLanguage; + Platform::String^ m_sourceCurrencyCode; + }; + } +} diff --git a/src/CalcViewModel/DataLoaders/DefaultFromToCurrency.json b/src/CalcViewModel/DataLoaders/DefaultFromToCurrency.json new file mode 100644 index 00000000..cc664135 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/DefaultFromToCurrency.json @@ -0,0 +1,134 @@ +{ + "default": { + "from": "USD", + "to": "EUR" + }, + "ar-AE": { + "from": "USD", + "to": "AED" + }, + "ar-EG": { + "from": "USD", + "to": "EGP" + }, + "ar-SA": { + "from": "USD", + "to": "SAR" + }, + "da-DK": { + "from": "DKK", + "to": "USD" + }, + "de-CH": { + "from": "EUR", + "to": "CHF" + }, + "de-DE": { + "from": "EUR", + "to": "USD" + }, + "en-AU": { + "from": "AUD", + "to": "USD" + }, + "en-CA": { + "from": "CAD", + "to": "USD" + }, + "en-ES": { + "from": "EUR", + "to": "USD" + }, + "en-GB": { + "from": "GBP", + "to": "USD" + }, + "en-IN": { + "from": "USD", + "to": "INR" + }, + "en-US": { + "from": "USD", + "to": "EUR" + }, + "es-AR": { + "from": "USD", + "to": "ARS" + }, + "es-CL": { + "from": "USD", + "to": "CLP" + }, + "es-CO": { + "from": "USD", + "to": "COP" + }, + "es-ES": { + "from": "EUR", + "to": "USD" + }, + "es-MX": { + "from": "USD", + "to": "MXN" + }, + "es-PE": { + "from": "USD", + "to": "PEN" + }, + "es-VE": { + "from": "USD", + "to": "VEF" + }, + "es-XL": { + "from": "USD", + "to": "EUR" + }, + "es-US": { + "from": "USD", + "to": "EUR" + }, + "fr-CH": { + "from": "EUR", + "to": "CHF" + }, + "fr-FR": { + "from": "EUR", + "to": "USD" + }, + "it-IT": { + "from": "EUR", + "to": "USD" + }, + "it-SM": { + "from": "EUR", + "to": "USD" + }, + "ja-JP": { + "from": "USD", + "to": "JPY" + }, + "nb-NO": { + "from": "NOK", + "to": "USD" + }, + "pt-BR": { + "from": "USD", + "to": "BRL" + }, + "sv-SE": { + "from": "SEK", + "to": "USD" + }, + "th-TH": { + "from": "USD", + "to": "THB" + }, + "zh-CN": { + "from": "USD", + "to": "CNY" + }, + "zh-HK": { + "from": "USD", + "to": "HKD" + } +} \ No newline at end of file diff --git a/src/CalcViewModel/DataLoaders/ICurrencyHttpClient.h b/src/CalcViewModel/DataLoaders/ICurrencyHttpClient.h new file mode 100644 index 00000000..50ec8571 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/ICurrencyHttpClient.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace DataLoaders + { + class ICurrencyHttpClient + { + public: + virtual ~ICurrencyHttpClient() {} + + virtual void SetSourceCurrencyCode(Platform::String^ sourceCurrencyCode) = 0; + virtual void SetResponseLanguage(Platform::String^ responseLanguage) = 0; + + virtual Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyMetadata() = 0; + virtual Windows::Foundation::IAsyncOperationWithProgress^ GetCurrencyRatios() = 0; + }; + } +} diff --git a/src/CalcViewModel/DataLoaders/UnitConverterDataConstants.h b/src/CalcViewModel/DataLoaders/UnitConverterDataConstants.h new file mode 100644 index 00000000..bd8cb169 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/UnitConverterDataConstants.h @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace CalculatorApp +{ + namespace ViewModel + { + private 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, + UnitEnd = Data_Zetabytes + }; + } +} diff --git a/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.cpp b/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.cpp new file mode 100644 index 00000000..754b3ee8 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.cpp @@ -0,0 +1,598 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "Common\AppResourceProvider.h" +#include "UnitConverterDataLoader.h" +#include "UnitConverterDataConstants.h" +#include "CurrencyDataLoader.h" + +using namespace CalculatorApp::Common; +using namespace CalculatorApp::DataLoaders; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace std; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::ApplicationModel::Resources::Core; +using namespace Windows::Globalization; + +static constexpr bool CONVERT_WITH_OFFSET_FIRST = true; + +UnitConverterDataLoader::UnitConverterDataLoader(GeographicRegion^ region) : + m_currentRegionCode(region->CodeTwoLetter) +{ + m_categoryList = make_shared>(); + m_categoryToUnits = make_shared(); + m_ratioMap = make_shared(); +} + +vector UnitConverterDataLoader::LoadOrderedCategories() +{ + return *m_categoryList; +} + +vector UnitConverterDataLoader::LoadOrderedUnits(const UCM::Category& category) +{ + return m_categoryToUnits->at(category); +} + +unordered_map UnitConverterDataLoader::LoadOrderedRatios(const UCM::Unit& unit) +{ + return m_ratioMap->at(unit); +} + +bool UnitConverterDataLoader::SupportsCategory(const UCM::Category& target) +{ + shared_ptr> supportedCategories = nullptr; + if (!m_categoryList->empty()) + { + supportedCategories = m_categoryList; + } + else + { + GetCategories(supportedCategories); + } + + static int currencyId = NavCategory::Serialize(ViewMode::Currency); + auto itr = find_if(supportedCategories->begin(), supportedCategories->end(), + [&](const UCM::Category& category) + { + return currencyId != category.id && target.id == category.id; + }); + + return itr != supportedCategories->end(); +} + +void UnitConverterDataLoader::LoadData() +{ + unordered_map idToUnit; + + unordered_map> orderedUnitMap{}; + unordered_map> categoryToUnitConversionDataMap{}; + unordered_map> explicitConversionData{}; + + // 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(); + for (UCM::Category objectCategory : *m_categoryList) + { + ViewMode categoryViewMode = NavCategory::Deserialize(objectCategory.id); + 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->insert(pair>(objectCategory, {})); + continue; + } + + vector orderedUnits = orderedUnitMap[categoryViewMode]; + vector unitList; + + // Sort the units by order + sort(orderedUnits.begin(), orderedUnits.end(), [](const OrderedUnit& first, const OrderedUnit& second){ return first.order < second.order; }); + + for (OrderedUnit u : orderedUnits) + { + unitList.push_back(static_cast(u)); + idToUnit.insert(pair(u.id, u)); + } + + // Save units per category + m_categoryToUnits->insert(pair>(objectCategory, unitList)); + + // For each unit, populate the conversion data + for (UCM::Unit unit : unitList) + { + unordered_map conversions; + + if (explicitConversionData.find(unit.id) == explicitConversionData.end()) + { + // Get the associated units for a category id + unordered_map unitConversions = categoryToUnitConversionDataMap.at(categoryViewMode); + double unitFactor = unitConversions[unit.id]; + + for (auto itr = unitConversions.begin(); itr != unitConversions.end(); ++itr) + { + UCM::ConversionData parsedData = { 1.0, 0.0, false }; + assert(itr->second > 0); // divide by zero assert + parsedData.ratio = unitFactor / itr->second; + conversions.insert(pair(idToUnit.at(itr->first), parsedData)); + } + } + else + { + unordered_map unitConversions = explicitConversionData.at(unit.id); + for (auto itr = unitConversions.begin(); itr != unitConversions.end(); ++itr) + { + conversions.insert(pair(idToUnit.at(itr->first), itr->second)); + } + } + + m_ratioMap->insert(pair>(unit, conversions)); + } + } +} + +void UnitConverterDataLoader::GetCategories(_In_ shared_ptr> categoriesList) +{ + categoriesList->clear(); + auto converterCategory = NavCategoryGroup::CreateConverterCategory(); + for (auto const& category : converterCategory->Categories) + { + /* Id, CategoryName, SupportsNegative */ + categoriesList->emplace_back( + NavCategory::Serialize(category->Mode), + category->Name->Data(), + category->SupportsNegative); + } +} + +void UnitConverterDataLoader::GetUnits(_In_ unordered_map>& unitMap) +{ + bool USSource, USTarget; + bool UKSource, UKTarget; + bool Source, Target; + + USSource = (GetRegion() == L"US") ? true : false; + USTarget = USSource; + + UKSource = (GetRegion() == L"UK") ? true : false; + UKTarget = UKSource; + + Source = (GetRegion() == L"Others") ? true : false; + Target = Source; + + vector areaUnits; + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_Acre, GetLocalizedStringName(L"UnitName_Acre"), GetLocalizedStringName(L"UnitAbbreviation_Acre"), 9 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_Hectare, GetLocalizedStringName(L"UnitName_Hectare"), GetLocalizedStringName(L"UnitAbbreviation_Hectare"), 4 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareCentimeter, GetLocalizedStringName(L"UnitName_SquareCentimeter"), GetLocalizedStringName(L"UnitAbbreviation_SquareCentimeter"), 2 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareFoot, GetLocalizedStringName(L"UnitName_SquareFoot"), GetLocalizedStringName(L"UnitAbbreviation_SquareFoot"), 7, (UKSource || Source), USTarget, false }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareInch, GetLocalizedStringName(L"UnitName_SquareInch"), GetLocalizedStringName(L"UnitAbbreviation_SquareInch"), 6 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareKilometer, GetLocalizedStringName(L"UnitName_SquareKilometer"), GetLocalizedStringName(L"UnitAbbreviation_SquareKilometer"), 5 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareMeter, GetLocalizedStringName(L"UnitName_SquareMeter"), GetLocalizedStringName(L"UnitAbbreviation_SquareMeter"), 3, USSource, (UKTarget || Target), false}); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareMile, GetLocalizedStringName(L"UnitName_SquareMile"), GetLocalizedStringName(L"UnitAbbreviation_SquareMile"), 10 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareMillimeter, GetLocalizedStringName(L"UnitName_SquareMillimeter"), GetLocalizedStringName(L"UnitAbbreviation_SquareMillimeter"), 1 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SquareYard, GetLocalizedStringName(L"UnitName_SquareYard"), GetLocalizedStringName(L"UnitAbbreviation_SquareYard"), 8 }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_Hand, GetLocalizedStringName(L"UnitName_Hand"), GetLocalizedStringName(L"UnitAbbreviation_Hand"), 11, false, false, true}); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_Paper, GetLocalizedStringName(L"UnitName_Paper"), GetLocalizedStringName(L"UnitAbbreviation_Paper"), 12, false, false, true }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_SoccerField, GetLocalizedStringName(L"UnitName_SoccerField"), GetLocalizedStringName(L"UnitAbbreviation_SoccerField"),13, false, false, true }); + areaUnits.push_back(OrderedUnit{ UnitConverterUnits::Area_Castle, GetLocalizedStringName(L"UnitName_Castle"), GetLocalizedStringName(L"UnitAbbreviation_Castle"), 14, false, false, true }); + unitMap.emplace(ViewMode::Area, areaUnits); + + vector dataUnits; + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Bit, GetLocalizedStringName(L"UnitName_Bit"), GetLocalizedStringName(L"UnitAbbreviation_Bit"), 1 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Byte, GetLocalizedStringName(L"UnitName_Byte"), GetLocalizedStringName(L"UnitAbbreviation_Byte"), 2 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Exabits, GetLocalizedStringName(L"UnitName_Exabits"), GetLocalizedStringName(L"UnitAbbreviation_Exabits"), 23 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Exabytes, GetLocalizedStringName(L"UnitName_Exabytes"), GetLocalizedStringName(L"UnitAbbreviation_Exabytes"), 25 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Exbibits, GetLocalizedStringName(L"UnitName_Exbibits"), GetLocalizedStringName(L"UnitAbbreviation_Exbibits"), 24 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Exbibytes, GetLocalizedStringName(L"UnitName_Exbibytes"), GetLocalizedStringName(L"UnitAbbreviation_Exbibytes"), 26 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Gibibits, GetLocalizedStringName(L"UnitName_Gibibits"), GetLocalizedStringName(L"UnitAbbreviation_Gibibits"), 12 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Gibibytes, GetLocalizedStringName(L"UnitName_Gibibytes"), GetLocalizedStringName(L"UnitAbbreviation_Gibibytes"), 14 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Gigabit, GetLocalizedStringName(L"UnitName_Gigabit"), GetLocalizedStringName(L"UnitAbbreviation_Gigabit"), 11 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Gigabyte, GetLocalizedStringName(L"UnitName_Gigabyte"), GetLocalizedStringName(L"UnitAbbreviation_Gigabyte"),13, true, false, false}); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Kibibits, GetLocalizedStringName(L"UnitName_Kibibits"), GetLocalizedStringName(L"UnitAbbreviation_Kibibits"), 4 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Kibibytes, GetLocalizedStringName(L"UnitName_Kibibytes"), GetLocalizedStringName(L"UnitAbbreviation_Kibibytes"),6 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Kilobit, GetLocalizedStringName(L"UnitName_Kilobit"), GetLocalizedStringName(L"UnitAbbreviation_Kilobit"), 3 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Kilobyte, GetLocalizedStringName(L"UnitName_Kilobyte"), GetLocalizedStringName(L"UnitAbbreviation_Kilobyte"), 5 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Mebibits, GetLocalizedStringName(L"UnitName_Mebibits"), GetLocalizedStringName(L"UnitAbbreviation_Mebibits"), 8 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Mebibytes, GetLocalizedStringName(L"UnitName_Mebibytes"), GetLocalizedStringName(L"UnitAbbreviation_Mebibytes"), 10 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Megabit, GetLocalizedStringName(L"UnitName_Megabit"), GetLocalizedStringName(L"UnitAbbreviation_Megabit"), 7 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Megabyte, GetLocalizedStringName(L"UnitName_Megabyte"), GetLocalizedStringName(L"UnitAbbreviation_Megabyte"), 9, false, true, false}); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Pebibits, GetLocalizedStringName(L"UnitName_Pebibits"), GetLocalizedStringName(L"UnitAbbreviation_Pebibits"), 20 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Pebibytes, GetLocalizedStringName(L"UnitName_Pebibytes"), GetLocalizedStringName(L"UnitAbbreviation_Pebibytes"), 22 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Petabit, GetLocalizedStringName(L"UnitName_Petabit"), GetLocalizedStringName(L"UnitAbbreviation_Petabit"), 19 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Petabyte, GetLocalizedStringName(L"UnitName_Petabyte"), GetLocalizedStringName(L"UnitAbbreviation_Petabyte"), 21 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Tebibits, GetLocalizedStringName(L"UnitName_Tebibits"), GetLocalizedStringName(L"UnitAbbreviation_Tebibits"), 16 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Tebibytes, GetLocalizedStringName(L"UnitName_Tebibytes"), GetLocalizedStringName(L"UnitAbbreviation_Tebibytes"), 18 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Terabit, GetLocalizedStringName(L"UnitName_Terabit"), GetLocalizedStringName(L"UnitAbbreviation_Terabit"), 15 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Terabyte, GetLocalizedStringName(L"UnitName_Terabyte"), GetLocalizedStringName(L"UnitAbbreviation_Terabyte"), 17 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Yobibits, GetLocalizedStringName(L"UnitName_Yobibits"), GetLocalizedStringName(L"UnitAbbreviation_Yobibits"), 32 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Yobibytes, GetLocalizedStringName(L"UnitName_Yobibytes"), GetLocalizedStringName(L"UnitAbbreviation_Yobibytes"), 34 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Yottabit, GetLocalizedStringName(L"UnitName_Yottabit"), GetLocalizedStringName(L"UnitAbbreviation_Yottabit"), 31 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Yottabyte, GetLocalizedStringName(L"UnitName_Yottabyte"), GetLocalizedStringName(L"UnitAbbreviation_Yottabyte"), 33 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Zebibits, GetLocalizedStringName(L"UnitName_Zebibits"), GetLocalizedStringName(L"UnitAbbreviation_Zebibits"), 28 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Zebibytes, GetLocalizedStringName(L"UnitName_Zebibytes"), GetLocalizedStringName(L"UnitAbbreviation_Zebibytes"), 30 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Zetabits, GetLocalizedStringName(L"UnitName_Zetabits"), GetLocalizedStringName(L"UnitAbbreviation_Zetabits"), 27 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_Zetabytes, GetLocalizedStringName(L"UnitName_Zetabytes"), GetLocalizedStringName(L"UnitAbbreviation_Zetabytes"),29 }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_FloppyDisk, GetLocalizedStringName(L"UnitName_FloppyDisk"), GetLocalizedStringName(L"UnitAbbreviation_FloppyDisk"), 13, false, false, true }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_CD, GetLocalizedStringName(L"UnitName_CD"), GetLocalizedStringName(L"UnitAbbreviation_CD"), 14, false, false, true }); + dataUnits.push_back(OrderedUnit{ UnitConverterUnits::Data_DVD, GetLocalizedStringName(L"UnitName_DVD"), GetLocalizedStringName(L"UnitAbbreviation_DVD"), 15, false, false, true }); + unitMap.emplace(ViewMode::Data, dataUnits); + + vector energyUnits; + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_BritishThermalUnit, GetLocalizedStringName(L"UnitName_BritishThermalUnit"), GetLocalizedStringName(L"UnitAbbreviation_BritishThermalUnit"), 7 }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Calorie, GetLocalizedStringName(L"UnitName_Calorie"), GetLocalizedStringName(L"UnitAbbreviation_Calorie"), 4 }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_ElectronVolt, GetLocalizedStringName(L"UnitName_Electron-Volt"), GetLocalizedStringName(L"UnitAbbreviation_Electron-Volt"), 1 }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_FootPound, GetLocalizedStringName(L"UnitName_Foot-Pound"), GetLocalizedStringName(L"UnitAbbreviation_Foot-Pound"), 6 }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Joule, GetLocalizedStringName(L"UnitName_Joule"), GetLocalizedStringName(L"UnitAbbreviation_Joule"), 2, true, false, false}); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Kilocalorie, GetLocalizedStringName(L"UnitName_Kilocalorie"), GetLocalizedStringName(L"UnitAbbreviation_Kilocalorie"), 5, false, true, false }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Kilojoule, GetLocalizedStringName(L"UnitName_Kilojoule"), GetLocalizedStringName(L"UnitAbbreviation_Kilojoule"), 3 }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Battery, GetLocalizedStringName(L"UnitName_Battery"), GetLocalizedStringName(L"UnitAbbreviation_Battery"), 8, false, false, true }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_Banana, GetLocalizedStringName(L"UnitName_Banana"), GetLocalizedStringName(L"UnitAbbreviation_Banana"), 9, false, false, true }); + energyUnits.push_back(OrderedUnit{ UnitConverterUnits::Energy_SliceOfCake, GetLocalizedStringName(L"UnitName_SliceOfCake"), GetLocalizedStringName(L"UnitAbbreviation_SliceOfCake"),10, false, false, true }); + unitMap.emplace(ViewMode::Energy, energyUnits); + + vector lengthUnits; + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Centimeter, GetLocalizedStringName(L"UnitName_Centimeter"), GetLocalizedStringName(L"UnitAbbreviation_Centimeter"), 4 , USSource, (Target || UKTarget), false}); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Foot, GetLocalizedStringName(L"UnitName_Foot"), GetLocalizedStringName(L"UnitAbbreviation_Foot"), 8 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Inch, GetLocalizedStringName(L"UnitName_Inch"), GetLocalizedStringName(L"UnitAbbreviation_Inch"), 7 , (Source|| UKSource), USTarget, false }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Kilometer, GetLocalizedStringName(L"UnitName_Kilometer"), GetLocalizedStringName(L"UnitAbbreviation_Kilometer"), 6 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Meter, GetLocalizedStringName(L"UnitName_Meter"), GetLocalizedStringName(L"UnitAbbreviation_Meter"), 5 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Micron, GetLocalizedStringName(L"UnitName_Micron"), GetLocalizedStringName(L"UnitAbbreviation_Micron"), 2 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Mile, GetLocalizedStringName(L"UnitName_Mile"), GetLocalizedStringName(L"UnitAbbreviation_Mile"), 10 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Millimeter, GetLocalizedStringName(L"UnitName_Millimeter"), GetLocalizedStringName(L"UnitAbbreviation_Millimeter"), 3 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Nanometer, GetLocalizedStringName(L"UnitName_Nanometer"), GetLocalizedStringName(L"UnitAbbreviation_Nanometer"), 1 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_NauticalMile, GetLocalizedStringName(L"UnitName_NauticalMile"), GetLocalizedStringName(L"UnitAbbreviation_NauticalMile"), 11 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Yard, GetLocalizedStringName(L"UnitName_Yard"), GetLocalizedStringName(L"UnitAbbreviation_Yard"), 9 }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Paperclip, GetLocalizedStringName(L"UnitName_Paperclip"), GetLocalizedStringName(L"UnitAbbreviation_Paperclip"), 12 ,false, false, true }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_Hand, GetLocalizedStringName(L"UnitName_Hand"), GetLocalizedStringName(L"UnitAbbreviation_Hand"), 13 ,false, false, true }); + lengthUnits.push_back(OrderedUnit{ UnitConverterUnits::Length_JumboJet, GetLocalizedStringName(L"UnitName_JumboJet"), GetLocalizedStringName(L"UnitAbbreviation_JumboJet"), 14 , false, false, true }); + unitMap.emplace(ViewMode::Length, lengthUnits); + + vector powerUnits; + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_BritishThermalUnitPerMinute, GetLocalizedStringName(L"UnitName_BTUPerMinute"), GetLocalizedStringName(L"UnitAbbreviation_BTUPerMinute"), 5 }); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_FootPoundPerMinute, GetLocalizedStringName(L"UnitName_Foot-PoundPerMinute"), GetLocalizedStringName(L"UnitAbbreviation_Foot-PoundPerMinute"), 4 }); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_Horsepower, GetLocalizedStringName(L"UnitName_Horsepower"), GetLocalizedStringName(L"UnitAbbreviation_Horsepower") , 3 , false, true, false }); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_Kilowatt, GetLocalizedStringName(L"UnitName_Kilowatt"), GetLocalizedStringName(L"UnitAbbreviation_Kilowatt"), 2 , (Source|| USSource), false, false}); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_Watt, GetLocalizedStringName(L"UnitName_Watt"), GetLocalizedStringName(L"UnitAbbreviation_Watt"), 1, UKSource }); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_LightBulb, GetLocalizedStringName(L"UnitName_LightBulb"), GetLocalizedStringName(L"UnitAbbreviation_LightBulb"), 6 ,false, false, true}); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_Horse, GetLocalizedStringName(L"UnitName_Horse"), GetLocalizedStringName(L"UnitAbbreviation_Horse"), 7 ,false, false, true}); + powerUnits.push_back(OrderedUnit{ UnitConverterUnits::Power_TrainEngine, GetLocalizedStringName(L"UnitName_TrainEngine"), GetLocalizedStringName(L"UnitAbbreviation_TrainEngine"), 8 ,false, false, true }); + unitMap.emplace(ViewMode::Power, powerUnits); + + vector tempUnits; + tempUnits.push_back(OrderedUnit{ UnitConverterUnits::Temperature_DegreesCelsius, GetLocalizedStringName(L"UnitName_DegreesCelsius"), GetLocalizedStringName(L"UnitAbbreviation_DegreesCelsius"), 1, USSource, (Target || UKTarget), false }); + tempUnits.push_back(OrderedUnit{ UnitConverterUnits::Temperature_DegreesFahrenheit, GetLocalizedStringName(L"UnitName_DegreesFahrenheit"), GetLocalizedStringName(L"UnitAbbreviation_DegreesFahrenheit"), 2 , (Source || UKSource), USTarget, false }); + tempUnits.push_back(OrderedUnit{ UnitConverterUnits::Temperature_Kelvin, GetLocalizedStringName(L"UnitName_Kelvin"), GetLocalizedStringName(L"UnitAbbreviation_Kelvin"), 3 }); + unitMap.emplace(ViewMode::Temperature, tempUnits); + + vector timeUnits; + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Day, GetLocalizedStringName(L"UnitName_Day"), GetLocalizedStringName(L"UnitAbbreviation_Day"), 6 }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Hour, GetLocalizedStringName(L"UnitName_Hour"), GetLocalizedStringName(L"UnitAbbreviation_Hour"), 5 ,true, false, false }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Microsecond, GetLocalizedStringName(L"UnitName_Microsecond"), GetLocalizedStringName(L"UnitAbbreviation_Microsecond"), 1 }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Millisecond, GetLocalizedStringName(L"UnitName_Millisecond"), GetLocalizedStringName(L"UnitAbbreviation_Millisecond"), 2 }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Minute, GetLocalizedStringName(L"UnitName_Minute"), GetLocalizedStringName(L"UnitAbbreviation_Minute"), 4 ,false, true, false }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Second, GetLocalizedStringName(L"UnitName_Second"), GetLocalizedStringName(L"UnitAbbreviation_Second"), 3 }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Week, GetLocalizedStringName(L"UnitName_Week"), GetLocalizedStringName(L"UnitAbbreviation_Week"), 7 }); + timeUnits.push_back(OrderedUnit{ UnitConverterUnits::Time_Year, GetLocalizedStringName(L"UnitName_Year"), GetLocalizedStringName(L"UnitAbbreviation_Year"), 8 }); + unitMap.emplace(ViewMode::Time, timeUnits); + + vector speedUnits; + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_CentimetersPerSecond, GetLocalizedStringName(L"UnitName_CentimetersPerSecond"), GetLocalizedStringName(L"UnitAbbreviation_CentimetersPerSecond"), 1 }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_FeetPerSecond, GetLocalizedStringName(L"UnitName_FeetPerSecond"), GetLocalizedStringName(L"UnitAbbreviation_FeetPerSecond"), 4 }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_KilometersPerHour, GetLocalizedStringName(L"UnitName_KilometersPerHour"), GetLocalizedStringName(L"UnitAbbreviation_KilometersPerHour"), 3 ,(USSource || UKSource), Target, false }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_Knot, GetLocalizedStringName(L"UnitName_Knot"), GetLocalizedStringName(L"UnitAbbreviation_Knot"), 6 }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_Mach, GetLocalizedStringName(L"UnitName_Mach"), GetLocalizedStringName(L"UnitAbbreviation_Mach"), 7 }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_MetersPerSecond, GetLocalizedStringName(L"UnitName_MetersPerSecond"), GetLocalizedStringName(L"UnitAbbreviation_MetersPerSecond"), 2 }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_MilesPerHour, GetLocalizedStringName(L"UnitName_MilesPerHour"), GetLocalizedStringName(L"UnitAbbreviation_MilesPerHour"), 5, Source, (UKTarget || USTarget), false }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_Turtle, GetLocalizedStringName(L"UnitName_Turtle"), GetLocalizedStringName(L"UnitAbbreviation_Turtle"), 8 ,false, false, true }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_Horse, GetLocalizedStringName(L"UnitName_Horse"), GetLocalizedStringName(L"UnitAbbreviation_Horse"),9 , false, false, true }); + speedUnits.push_back(OrderedUnit{ UnitConverterUnits::Speed_Jet, GetLocalizedStringName(L"UnitName_Jet"), GetLocalizedStringName(L"UnitAbbreviation_Jet"), 10, false, false, true }); + unitMap.emplace(ViewMode::Speed, speedUnits); + + vector volumeUnits; + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CubicCentimeter, GetLocalizedStringName(L"UnitName_CubicCentimeter"), GetLocalizedStringName(L"UnitAbbreviation_CubicCentimeter"), 2 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CubicFoot, GetLocalizedStringName(L"UnitName_CubicFoot"), GetLocalizedStringName(L"UnitAbbreviation_CubicFoot"), 13 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CubicInch, GetLocalizedStringName(L"UnitName_CubicInch"), GetLocalizedStringName(L"UnitAbbreviation_CubicInch"), 12 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CubicMeter, GetLocalizedStringName(L"UnitName_CubicMeter"), GetLocalizedStringName(L"UnitAbbreviation_CubicMeter"), 4 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CubicYard, GetLocalizedStringName(L"UnitName_CubicYard"), GetLocalizedStringName(L"UnitAbbreviation_CubicYard"), 14 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CupUS, GetLocalizedStringName(L"UnitName_CupUS"), GetLocalizedStringName(L"UnitAbbreviation_CupUS"), 8 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_FluidOunceUK, GetLocalizedStringName(L"UnitName_FluidOunceUK"), GetLocalizedStringName(L"UnitAbbreviation_FluidOunceUK"), 17 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_FluidOunceUS, GetLocalizedStringName(L"UnitName_FluidOunceUS"), GetLocalizedStringName(L"UnitAbbreviation_FluidOunceUS"), 7 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_GallonUK, GetLocalizedStringName(L"UnitName_GallonUK"), GetLocalizedStringName(L"UnitAbbreviation_GallonUK"), 20 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_GallonUS, GetLocalizedStringName(L"UnitName_GallonUS"), GetLocalizedStringName(L"UnitAbbreviation_GallonUS"), 11 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_Liter, GetLocalizedStringName(L"UnitName_Liter"), GetLocalizedStringName(L"UnitAbbreviation_Liter"), 3 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_Milliliter, GetLocalizedStringName(L"UnitName_Milliliter"), GetLocalizedStringName(L"UnitAbbreviation_Milliliter"), 1 , USSource, (Target || UKTarget), false}); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_PintUK, GetLocalizedStringName(L"UnitName_PintUK"), GetLocalizedStringName(L"UnitAbbreviation_PintUK"), 18 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_PintUS, GetLocalizedStringName(L"UnitName_PintUS"), GetLocalizedStringName(L"UnitAbbreviation_PintUS"), 9 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_TablespoonUS, GetLocalizedStringName(L"UnitName_TablespoonUS"), GetLocalizedStringName(L"UnitAbbreviation_TablespoonUS"), 6 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_TeaspoonUS, GetLocalizedStringName(L"UnitName_TeaspoonUS"), GetLocalizedStringName(L"UnitAbbreviation_TeaspoonUS"), 5 ,Source, USTarget, false }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_QuartUK, GetLocalizedStringName(L"UnitName_QuartUK"), GetLocalizedStringName(L"UnitAbbreviation_QuartUK"), 19 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_QuartUS, GetLocalizedStringName(L"UnitName_QuartUS"), GetLocalizedStringName(L"UnitAbbreviation_QuartUS"), 10 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_TeaspoonUK, GetLocalizedStringName(L"UnitName_TeaspoonUK"), GetLocalizedStringName(L"UnitAbbreviation_TeaspoonUK"), 15, UKSource }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_TablespoonUK, GetLocalizedStringName(L"UnitName_TablespoonUK"), GetLocalizedStringName(L"UnitAbbreviation_TablespoonUK"), 16 }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_CoffeeCup, GetLocalizedStringName(L"UnitName_CoffeeCup"), GetLocalizedStringName(L"UnitAbbreviation_CoffeeCup"), 22 ,false, false, true }); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_Bathtub, GetLocalizedStringName(L"UnitName_Bathtub"), GetLocalizedStringName(L"UnitAbbreviation_Bathtub"), 23 ,false, false, true}); + volumeUnits.push_back(OrderedUnit{ UnitConverterUnits::Volume_SwimmingPool, GetLocalizedStringName(L"UnitName_SwimmingPool"), GetLocalizedStringName(L"UnitAbbreviation_SwimmingPool"), 24 ,false, false, true }); + unitMap.emplace(ViewMode::Volume, volumeUnits); + + vector weightUnits; + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Carat, GetLocalizedStringName(L"UnitName_Carat"), GetLocalizedStringName(L"UnitAbbreviation_Carat"), 1 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Centigram, GetLocalizedStringName(L"UnitName_Centigram"), GetLocalizedStringName(L"UnitAbbreviation_Centigram"), 3 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Decigram, GetLocalizedStringName(L"UnitName_Decigram"), GetLocalizedStringName(L"UnitAbbreviation_Decigram"), 4 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Decagram, GetLocalizedStringName(L"UnitName_Decagram"), GetLocalizedStringName(L"UnitAbbreviation_Decagram"), 6 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Gram, GetLocalizedStringName(L"UnitName_Gram"), GetLocalizedStringName(L"UnitAbbreviation_Gram"), 5 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Hectogram, GetLocalizedStringName(L"UnitName_Hectogram"), GetLocalizedStringName(L"UnitAbbreviation_Hectogram"), 7 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Kilogram, GetLocalizedStringName(L"UnitName_Kilogram"), GetLocalizedStringName(L"UnitAbbreviation_Kilogram"), 8 ,(USSource || UKSource), Target, false}); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_LongTon, GetLocalizedStringName(L"UnitName_LongTon"), GetLocalizedStringName(L"UnitAbbreviation_LongTon"), 14 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Milligram, GetLocalizedStringName(L"UnitName_Milligram"), GetLocalizedStringName(L"UnitAbbreviation_Milligram"), 2 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Ounce, GetLocalizedStringName(L"UnitName_Ounce"), GetLocalizedStringName(L"UnitAbbreviation_Ounce"), 10 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Pound, GetLocalizedStringName(L"UnitName_Pound"), GetLocalizedStringName(L"UnitAbbreviation_Pound"), 11 , Source, (USTarget ||UKTarget), false }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_ShortTon, GetLocalizedStringName(L"UnitName_ShortTon"), GetLocalizedStringName(L"UnitAbbreviation_ShortTon"), 13 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Stone, GetLocalizedStringName(L"UnitName_Stone"), GetLocalizedStringName(L"UnitAbbreviation_Stone"), 12 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Tonne, GetLocalizedStringName(L"UnitName_Tonne"), GetLocalizedStringName(L"UnitAbbreviation_Tonne"), 9 }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Snowflake, GetLocalizedStringName(L"UnitName_Snowflake"), GetLocalizedStringName(L"UnitAbbreviation_Snowflake"), 15 ,false, false, true }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_SoccerBall, GetLocalizedStringName(L"UnitName_SoccerBall"), GetLocalizedStringName(L"UnitAbbreviation_SoccerBall"), 16 , false, false, true }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Elephant, GetLocalizedStringName(L"UnitName_Elephant"), GetLocalizedStringName(L"UnitAbbreviation_Elephant"), 17 ,false, false, true }); + weightUnits.push_back(OrderedUnit{ UnitConverterUnits::Weight_Whale, GetLocalizedStringName(L"UnitName_Whale"), GetLocalizedStringName(L"UnitAbbreviation_Whale"), 18 ,false, false, true }); + unitMap.emplace(ViewMode::Weight, weightUnits); + + vector pressureUnits; + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_Atmosphere, GetLocalizedStringName(L"UnitName_Atmosphere"), GetLocalizedStringName(L"UnitAbbreviation_Atmosphere"), 1 , true, false, false }); + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_Bar, GetLocalizedStringName(L"UnitName_Bar"), GetLocalizedStringName(L"UnitAbbreviation_Bar"), 2, false, true, false}); + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_KiloPascal, GetLocalizedStringName(L"UnitName_KiloPascal"), GetLocalizedStringName(L"UnitAbbreviation_KiloPascal"), 3 }); + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_MillimeterOfMercury, GetLocalizedStringName(L"UnitName_MillimeterOfMercury "), GetLocalizedStringName(L"UnitAbbreviation_MillimeterOfMercury "), 4 }); + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_Pascal, GetLocalizedStringName(L"UnitName_Pascal"), GetLocalizedStringName(L"UnitAbbreviation_Pascal"), 5 }); + pressureUnits.push_back(OrderedUnit{ UnitConverterUnits::Pressure_PSI, GetLocalizedStringName(L"UnitName_PSI"), GetLocalizedStringName(L"UnitAbbreviation_PSI"), 6, false, false, false }); + unitMap.emplace(ViewMode::Pressure, pressureUnits); + + vector angleUnits; + angleUnits.push_back(OrderedUnit{ UnitConverterUnits::Angle_Degree, GetLocalizedStringName(L"UnitName_Degree"), GetLocalizedStringName(L"UnitAbbreviation_Degree"), 1, true, false, false }); + angleUnits.push_back(OrderedUnit{ UnitConverterUnits::Angle_Radian, GetLocalizedStringName(L"UnitName_Radian"), GetLocalizedStringName(L"UnitAbbreviation_Radian"), 2, false, true, false }); + angleUnits.push_back(OrderedUnit{ UnitConverterUnits::Angle_Gradian, GetLocalizedStringName(L"UnitName_Gradian"), GetLocalizedStringName(L"UnitAbbreviation_Gradian"), 3}); + unitMap.emplace(ViewMode::Angle, angleUnits); +} + +void UnitConverterDataLoader::GetConversionData(_In_ unordered_map>& categoryToUnitConversionMap) +{ + /*categoryId, UnitId, factor*/ + static const vector unitDataList = { + { ViewMode::Area, UnitConverterUnits::Area_Acre, 4046.8564224 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareMeter, 1 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareFoot, 0.09290304 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareYard, 0.83612736 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareMillimeter, 0.000001 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareCentimeter, 0.0001 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareInch, 0.00064516 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareMile, 2589988.110336 }, + { ViewMode::Area, UnitConverterUnits::Area_SquareKilometer, 1000000 }, + { ViewMode::Area, UnitConverterUnits::Area_Hectare, 10000 }, + { ViewMode::Area, UnitConverterUnits::Area_Hand, 0.012516104 }, + { ViewMode::Area, UnitConverterUnits::Area_Paper, 0.06032246 }, + { ViewMode::Area, UnitConverterUnits::Area_SoccerField, 10869.66 }, + { ViewMode::Area, UnitConverterUnits::Area_Castle, 100000 }, + + { ViewMode::Data, UnitConverterUnits::Data_Bit, 0.000000125 }, + { ViewMode::Data, UnitConverterUnits::Data_Byte, 0.000001 }, + { ViewMode::Data, UnitConverterUnits::Data_Kilobyte, 0.001 }, + { ViewMode::Data, UnitConverterUnits::Data_Megabyte, 1 }, + { ViewMode::Data, UnitConverterUnits::Data_Gigabyte, 1000 }, + { ViewMode::Data, UnitConverterUnits::Data_Terabyte, 1000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Petabyte, 1000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Exabytes, 1000000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Zetabytes, 1000000000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Yottabyte, 1000000000000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Kilobit, 0.000125 }, + { ViewMode::Data, UnitConverterUnits::Data_Megabit, 0.125 }, + { ViewMode::Data, UnitConverterUnits::Data_Gigabit, 125 }, + { ViewMode::Data, UnitConverterUnits::Data_Terabit, 125000 }, + { ViewMode::Data, UnitConverterUnits::Data_Petabit, 125000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Exabits, 125000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Zetabits, 125000000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Yottabit, 125000000000000000 }, + { ViewMode::Data, UnitConverterUnits::Data_Gibibits, 134.217728 }, + { ViewMode::Data, UnitConverterUnits::Data_Gibibytes, 1073.741824 }, + { ViewMode::Data, UnitConverterUnits::Data_Kibibits, 0.000128 }, + { ViewMode::Data, UnitConverterUnits::Data_Kibibytes, 0.001024 }, + { ViewMode::Data, UnitConverterUnits::Data_Mebibits, 0.131072 }, + { ViewMode::Data, UnitConverterUnits::Data_Mebibytes, 1.048576 }, + { ViewMode::Data, UnitConverterUnits::Data_Pebibits, 140737488.355328 }, + { ViewMode::Data, UnitConverterUnits::Data_Pebibytes, 1125899906.842624 }, + { ViewMode::Data, UnitConverterUnits::Data_Tebibits, 137438.953472 }, + { ViewMode::Data, UnitConverterUnits::Data_Tebibytes, 1099511.627776 }, + { ViewMode::Data, UnitConverterUnits::Data_Exbibits, 144115188075.855872 }, + { ViewMode::Data, UnitConverterUnits::Data_Exbibytes, 1152921504606.846976 }, + { ViewMode::Data, UnitConverterUnits::Data_Zebibits, 147573952589676.412928 }, + { ViewMode::Data, UnitConverterUnits::Data_Zebibytes, 1180591620717411.303424 }, + { ViewMode::Data, UnitConverterUnits::Data_Yobibits, 151115727451828646.838272 }, + { ViewMode::Data, UnitConverterUnits::Data_Yobibytes, 1208925819614629174.706176 }, + { ViewMode::Data, UnitConverterUnits::Data_FloppyDisk, 1.509949 }, + { ViewMode::Data, UnitConverterUnits::Data_CD, 734.003200 }, + { ViewMode::Data, UnitConverterUnits::Data_DVD, 5046.586573 }, + + { ViewMode::Energy, UnitConverterUnits::Energy_Calorie, 4.184 }, + { ViewMode::Energy, UnitConverterUnits::Energy_Kilocalorie, 4184}, + { ViewMode::Energy, UnitConverterUnits::Energy_BritishThermalUnit, 1055.056 }, + { ViewMode::Energy, UnitConverterUnits::Energy_Kilojoule, 1000 }, + { ViewMode::Energy, UnitConverterUnits::Energy_ElectronVolt, 0.0000000000000000001602176565 }, + { ViewMode::Energy, UnitConverterUnits::Energy_Joule, 1 }, + { ViewMode::Energy, UnitConverterUnits::Energy_FootPound, 1.3558179483314 }, + { ViewMode::Energy, UnitConverterUnits::Energy_Battery, 9000 }, + { ViewMode::Energy, UnitConverterUnits::Energy_Banana, 439614 }, + { ViewMode::Energy, UnitConverterUnits::Energy_SliceOfCake, 1046700 }, + + { ViewMode::Length, UnitConverterUnits::Length_Inch, 0.0254 }, + { ViewMode::Length, UnitConverterUnits::Length_Foot, 0.3048 }, + { ViewMode::Length, UnitConverterUnits::Length_Yard, 0.9144 }, + { ViewMode::Length, UnitConverterUnits::Length_Mile, 1609.344 }, + { ViewMode::Length, UnitConverterUnits::Length_Micron, 0.000001 }, + { ViewMode::Length, UnitConverterUnits::Length_Millimeter, 0.001 }, + { ViewMode::Length, UnitConverterUnits::Length_Nanometer, 0.000000001 }, + { ViewMode::Length, UnitConverterUnits::Length_Centimeter, 0.01 }, + { ViewMode::Length, UnitConverterUnits::Length_Meter, 1 }, + { ViewMode::Length, UnitConverterUnits::Length_Kilometer, 1000 }, + { ViewMode::Length, UnitConverterUnits::Length_NauticalMile, 1852 }, + { ViewMode::Length, UnitConverterUnits::Length_Paperclip, 0.035052 }, + { ViewMode::Length, UnitConverterUnits::Length_Hand, 0.18669 }, + { ViewMode::Length, UnitConverterUnits::Length_JumboJet, 76 }, + + { ViewMode::Power, UnitConverterUnits::Power_BritishThermalUnitPerMinute, 17.58426666666667 }, + { ViewMode::Power, UnitConverterUnits::Power_FootPoundPerMinute, 0.0225969658055233 }, + { ViewMode::Power, UnitConverterUnits::Power_Watt, 1 }, + { ViewMode::Power, UnitConverterUnits::Power_Kilowatt, 1000 }, + { ViewMode::Power, UnitConverterUnits::Power_Horsepower, 745.69987158227022 }, + { ViewMode::Power, UnitConverterUnits::Power_LightBulb, 60 }, + { ViewMode::Power, UnitConverterUnits::Power_Horse, 745.7 }, + { ViewMode::Power, UnitConverterUnits::Power_TrainEngine, 2982799.486329081 }, + + { ViewMode::Time, UnitConverterUnits::Time_Day, 86400 }, + { ViewMode::Time, UnitConverterUnits::Time_Second, 1 }, + { ViewMode::Time, UnitConverterUnits::Time_Week, 604800 }, + { ViewMode::Time, UnitConverterUnits::Time_Year, 31557600 }, + { ViewMode::Time, UnitConverterUnits::Time_Millisecond, 0.001 }, + { ViewMode::Time, UnitConverterUnits::Time_Microsecond, 0.000001 }, + { ViewMode::Time, UnitConverterUnits::Time_Minute, 60 }, + { ViewMode::Time, UnitConverterUnits::Time_Hour, 3600 }, + + { ViewMode::Volume, UnitConverterUnits::Volume_CupUS, 236.588237 }, + { ViewMode::Volume, UnitConverterUnits::Volume_PintUS, 473.176473 }, + { ViewMode::Volume, UnitConverterUnits::Volume_PintUK, 568.26125 }, + { ViewMode::Volume, UnitConverterUnits::Volume_QuartUS, 946.352946 }, + { ViewMode::Volume, UnitConverterUnits::Volume_QuartUK, 1136.5225 }, + { ViewMode::Volume, UnitConverterUnits::Volume_GallonUS, 3785.411784 }, + { ViewMode::Volume, UnitConverterUnits::Volume_GallonUK, 4546.09 }, + { ViewMode::Volume, UnitConverterUnits::Volume_Liter, 1000 }, + { ViewMode::Volume, UnitConverterUnits::Volume_TeaspoonUS, 4.928922 }, + { ViewMode::Volume, UnitConverterUnits::Volume_TablespoonUS, 14.786765 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CubicCentimeter, 1 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CubicYard, 764554.857984 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CubicMeter, 1000000 }, + { ViewMode::Volume, UnitConverterUnits::Volume_Milliliter, 1 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CubicInch, 16.387064 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CubicFoot, 28316.846592 }, + { ViewMode::Volume, UnitConverterUnits::Volume_FluidOunceUS, 29.5735295625 }, + { ViewMode::Volume, UnitConverterUnits::Volume_FluidOunceUK, 28.4130625 }, + { ViewMode::Volume, UnitConverterUnits::Volume_TeaspoonUK, 5.91938802083333333333 }, + { ViewMode::Volume, UnitConverterUnits::Volume_TablespoonUK, 17.7581640625 }, + { ViewMode::Volume, UnitConverterUnits::Volume_CoffeeCup, 236.5882 }, + { ViewMode::Volume, UnitConverterUnits::Volume_Bathtub, 378541.2 }, + { ViewMode::Volume, UnitConverterUnits::Volume_SwimmingPool, 3750000000 }, + + { ViewMode::Weight, UnitConverterUnits::Weight_Kilogram, 1 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Hectogram, 0.1 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Decagram, 0.01 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Gram, 0.001 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Pound, 0.45359237 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Ounce, 0.028349523125 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Milligram, 0.000001 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Centigram, 0.00001 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Decigram, 0.0001 }, + { ViewMode::Weight, UnitConverterUnits::Weight_LongTon, 1016.0469088 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Tonne, 1000 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Stone, 6.35029318 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Carat, 0.0002 }, + { ViewMode::Weight, UnitConverterUnits::Weight_ShortTon, 907.18474 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Snowflake, 0.000002 }, + { ViewMode::Weight, UnitConverterUnits::Weight_SoccerBall, 0.4325 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Elephant, 4000 }, + { ViewMode::Weight, UnitConverterUnits::Weight_Whale, 90000 }, + + { ViewMode::Speed, UnitConverterUnits::Speed_CentimetersPerSecond, 1 }, + { ViewMode::Speed, UnitConverterUnits::Speed_FeetPerSecond, 30.48 }, + { ViewMode::Speed, UnitConverterUnits::Speed_KilometersPerHour, 27.777777777777777777778 }, + { ViewMode::Speed, UnitConverterUnits::Speed_Knot, 51.44 }, + { ViewMode::Speed, UnitConverterUnits::Speed_Mach, 34030 }, + { ViewMode::Speed, UnitConverterUnits::Speed_MetersPerSecond, 100 }, + { ViewMode::Speed, UnitConverterUnits::Speed_MilesPerHour, 44.7 }, + { ViewMode::Speed, UnitConverterUnits::Speed_Turtle, 8.94 }, + { ViewMode::Speed, UnitConverterUnits::Speed_Horse, 2011.5 }, + { ViewMode::Speed, UnitConverterUnits::Speed_Jet, 24585 }, + + { ViewMode::Angle, UnitConverterUnits::Angle_Degree, 1 }, + { ViewMode::Angle, UnitConverterUnits::Angle_Radian, 57.29577951308233 }, + { ViewMode::Angle, UnitConverterUnits::Angle_Gradian, 0.9 }, + + { ViewMode::Pressure, UnitConverterUnits::Pressure_Atmosphere, 1 }, + { ViewMode::Pressure, UnitConverterUnits::Pressure_Bar, 0.9869232667160128 }, + { ViewMode::Pressure, UnitConverterUnits::Pressure_KiloPascal, 0.0098692326671601 }, + { ViewMode::Pressure, UnitConverterUnits::Pressure_MillimeterOfMercury, 0.0013155687145324 }, + { ViewMode::Pressure, UnitConverterUnits::Pressure_Pascal, 9.869232667160128e-6 }, + { ViewMode::Pressure, UnitConverterUnits::Pressure_PSI, 0.068045961016531 } + }; + + // Populate the hash map and return; + for (UnitData unitdata : unitDataList) + { + if (categoryToUnitConversionMap.find(unitdata.categoryId) == categoryToUnitConversionMap.end()) + { + unordered_map conversionData; + conversionData.insert(pair(unitdata.unitId, unitdata.factor)); + categoryToUnitConversionMap.insert(pair>(unitdata.categoryId, conversionData)); + } + else + { + categoryToUnitConversionMap.at(unitdata.categoryId).insert(pair(unitdata.unitId, unitdata.factor)); + } + } + } + + wstring UnitConverterDataLoader::GetLocalizedStringName(String^ stringId) + { + return AppResourceProvider::GetInstance().GetResourceString(stringId)->Data(); + } + + wstring UnitConverterDataLoader::GetRegion() + { + if ((m_currentRegionCode == L"US") || + (m_currentRegionCode == L"LR") || + (m_currentRegionCode == L"MM")) + { + return L"US"; + } + else if (m_currentRegionCode == L"GB") + { + return L"UK"; + } + else + { + return L"Others"; + } + } + + void UnitConverterDataLoader::GetExplicitConversionData(_In_ unordered_map>& unitToUnitConversionList) + { + /* categoryId, ParentUnitId, UnitId, ratio, offset, offsetfirst*/ + ExplicitUnitConversionData conversionDataList[] = { + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesCelsius, UnitConverterUnits::Temperature_DegreesCelsius , 1, 0 }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesCelsius, UnitConverterUnits::Temperature_DegreesFahrenheit, 1.8, 32 }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesCelsius, UnitConverterUnits::Temperature_Kelvin, 1, 273.15 }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesFahrenheit, UnitConverterUnits::Temperature_DegreesCelsius, 0.55555555555555555555555555555556, -32, CONVERT_WITH_OFFSET_FIRST }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesFahrenheit, UnitConverterUnits::Temperature_DegreesFahrenheit, 1, 0 }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_DegreesFahrenheit, UnitConverterUnits::Temperature_Kelvin, 0.55555555555555555555555555555556, 459.67, CONVERT_WITH_OFFSET_FIRST }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_Kelvin, UnitConverterUnits::Temperature_DegreesCelsius, 1, -273.15, CONVERT_WITH_OFFSET_FIRST }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_Kelvin, UnitConverterUnits::Temperature_DegreesFahrenheit, 1.8, -459.67 }, + { ViewMode::Temperature, UnitConverterUnits::Temperature_Kelvin, UnitConverterUnits::Temperature_Kelvin, 1, 0 } + }; + + // Populate the hash map and return; + for (ExplicitUnitConversionData data : conversionDataList) + { + if (unitToUnitConversionList.find(data.parentUnitId) == unitToUnitConversionList.end()) + { + unordered_map conversionData; + conversionData.insert(pair(data.unitId, static_cast(data))); + unitToUnitConversionList.insert(pair>(data.parentUnitId, conversionData)); + } + else + { + unitToUnitConversionList.at(data.parentUnitId).insert(pair(data.unitId, static_cast(data))); + } + } + } diff --git a/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.h b/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.h new file mode 100644 index 00000000..07f2beb8 --- /dev/null +++ b/src/CalcViewModel/DataLoaders/UnitConverterDataLoader.h @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace ViewModel + { + struct OrderedUnit : UnitConversionManager::Unit + { + OrderedUnit(){} + + OrderedUnit(int id, std::wstring name, std::wstring abbreviation, int order, bool isConversionSource = false, bool isConversionTarget = false, bool isWhimsical = false) + : UnitConversionManager::Unit(id, name, abbreviation, isConversionSource, isConversionTarget, isWhimsical), order(order) + { + } + + int order; + }; + + struct UnitData + { + CalculatorApp::Common::ViewMode categoryId; + int unitId; + double factor; + }; + + struct ExplicitUnitConversionData : UnitConversionManager::ConversionData + { + ExplicitUnitConversionData(){} + ExplicitUnitConversionData(CalculatorApp::Common::ViewMode categoryId, int parentUnitId, int unitId, double ratio, double offset, bool offsetFirst = false) : + categoryId(categoryId), parentUnitId(parentUnitId), unitId(unitId), UnitConversionManager::ConversionData(ratio, offset, offsetFirst) + { + + } + + CalculatorApp::Common::ViewMode categoryId; + int parentUnitId; + int unitId; + }; + + class UnitConverterDataLoader : public UnitConversionManager::IConverterDataLoader, + public std::enable_shared_from_this + { + public: + UnitConverterDataLoader(Windows::Globalization::GeographicRegion^ region); + + private: + // IConverterDataLoader + void LoadData() override; + std::vector LoadOrderedCategories() override; + std::vector LoadOrderedUnits(const UnitConversionManager::Category& c) override; + std::unordered_map LoadOrderedRatios(const UnitConversionManager::Unit& unit) override; + bool SupportsCategory(const UnitConversionManager::Category& target) override; + // IConverterDataLoader + + void GetCategories(_In_ std::shared_ptr> categoriesList); + void GetUnits(_In_ std::unordered_map>& unitMap); + void GetConversionData(_In_ std::unordered_map>& categoryToUnitConversionMap); + void GetExplicitConversionData(_In_ std::unordered_map>& unitToUnitConversionList); + + std::wstring GetLocalizedStringName(_In_ Platform::String^ stringId); + std::wstring GetRegion(); + + std::shared_ptr> m_categoryList; + std::shared_ptr m_categoryToUnits; + std::shared_ptr m_ratioMap; + Platform::String^ m_currentRegionCode; + }; + } +} diff --git a/src/CalcViewModel/DateCalculatorViewModel.cpp b/src/CalcViewModel/DateCalculatorViewModel.cpp new file mode 100644 index 00000000..764db235 --- /dev/null +++ b/src/CalcViewModel/DateCalculatorViewModel.cpp @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "DateCalculatorViewModel.h" +#include "Common\LocalizationStringUtil.h" +#include "Common\LocalizationService.h" +#include "Common\LocalizationSettings.h" +#include "Common\CopyPasteManager.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::DateCalculation; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Foundation; +using namespace Windows::Globalization; +using namespace Windows::Globalization::DateTimeFormatting; +using namespace Windows::System::UserProfile; + +namespace CalculatorApp::ViewModel::DateCalculatorViewModelProperties +{ + StringReference StrDateDiffResult(L"StrDateDiffResult"); + StringReference StrDateDiffResultAutomationName(L"StrDateDiffResultAutomationName"); + StringReference StrDateDiffResultInDays(L"StrDateDiffResultInDays"); + StringReference StrDateResult(L"StrDateResult"); + StringReference StrDateResultAutomationName(L"StrDateResultAutomationName"); + StringReference IsDiffInDays(L"IsDiffInDays"); +} + +DateCalculatorViewModel::DateCalculatorViewModel() : + m_IsDateDiffMode(true), + m_IsAddMode(true), + m_isOutOfBound(false), + m_DaysOffset(0), + m_MonthsOffset(0), + m_YearsOffset(0), + m_StrDateDiffResult(L""), + m_StrDateDiffResultAutomationName(L""), + m_StrDateDiffResultInDays(L""), + m_StrDateResult(L""), + m_StrDateResultAutomationName(L""), + m_fromDate({ 0 }), + m_toDate({ 0 }), + m_startDate({ 0 }), + m_dateResult({ 0 }) +{ + const auto& localizationSettings = LocalizationSettings::GetInstance(); + + // Initialize Date Output format instances + InitializeDateOutputFormats(localizationSettings.GetCalendarIdentifier()); + + // Initialize Date Calc engine + m_dateCalcEngine = make_shared(localizationSettings.GetCalendarIdentifier()); + + // Initialize dates of DatePicker controls to today's date + auto calendar = ref new Calendar(); + auto today = calendar->GetDateTime(); + + // FromDate and ToDate should be clipped (adjusted to a consistent hour in UTC) + m_fromDate = today; + m_toDate = today; + FromDate = ClipTime(today); + ToDate = ClipTime(today); + + // StartDate should not be clipped + StartDate = today; + m_dateResult = today; + + // Initialize the list separator delimiter appended with a space at the end, e.g. ", " + // This will be used for date difference formatting: Y years, M months, W weeks, D days + m_listSeparator = ref new String((localizationSettings.GetListSeparator() + L" ").c_str()); + + // Initialize the output results + UpdateDisplayResult(); + + m_offsetValues = ref new Vector(); + for (int i = 0; i <= c_maxOffsetValue; i++) + { + wstring numberStr(to_wstring(i)); + localizationSettings.LocalizeDisplayValue(&numberStr); + m_offsetValues->Append(ref new String(numberStr.c_str())); + } + + /* In the ClipTime function, we used to change timezone to UTC before clipping the time. + The comment from the previous delopers said this was done to eliminate the effects of + Daylight Savings Time. We can't think of a good reason why this change in timezone is + necessary and did find bugs related to the change, therefore, we have removed the + change. Just in case, we will see if the clipped time is ever a different day from the + original day, which would hopefully indicate the change in timezone was actually + necessary. We will collect telemetry if we find this case. If we don't see any + telemetry events after the application has been used for some time, we will feel safe + and can remove this function. */ + DayOfWeek trueDayOfWeek = calendar->DayOfWeek; + + DateTime clippedTime = ClipTime(today); + calendar->SetDateTime(clippedTime); + if (calendar->DayOfWeek != trueDayOfWeek) + { + calendar->SetDateTime(today); + TraceLogger::GetInstance().LogDateClippedTimeDifferenceFound( + from_cx(calendar), + winrt::Windows::Foundation::DateTime{ winrt::Windows::Foundation::TimeSpan{ clippedTime.UniversalTime } }); + } +} + +void DateCalculatorViewModel::OnPropertyChanged(_In_ String^ prop) +{ + if (prop == DateCalculatorViewModelProperties::StrDateDiffResult) + { + UpdateStrDateDiffResultAutomationName(); + } + else if (prop == DateCalculatorViewModelProperties::StrDateResult) + { + UpdateStrDateResultAutomationName(); + } + else if (prop != DateCalculatorViewModelProperties::StrDateDiffResultAutomationName + && prop != DateCalculatorViewModelProperties::StrDateDiffResultInDays + && prop != DateCalculatorViewModelProperties::StrDateResultAutomationName + && prop != DateCalculatorViewModelProperties::IsDiffInDays) + { + OnInputsChanged(); + } +} + +void DateCalculatorViewModel::OnInputsChanged() +{ + DateDifference dateDiff; + + if (m_IsDateDiffMode) + { + DateTime clippedFromDate = ClipTime(FromDate); + DateTime clippedToDate = ClipTime(ToDate); + + // Calculate difference between two dates + m_dateCalcEngine->GetDateDifference(clippedFromDate, clippedToDate, m_allDateUnitsOutputFormat, &dateDiff); + DateDiffResult = dateDiff; + + m_dateCalcEngine->GetDateDifference(clippedFromDate, clippedToDate, m_daysOutputFormat, &dateDiff); + DateDiffResultInDays = dateDiff; + } + else + { + dateDiff.day = DaysOffset; + dateDiff.month = MonthsOffset; + dateDiff.year = YearsOffset; + + DateTime dateTimeResult; + + if (m_IsAddMode) + { + // Add number of Days, Months and Years to a Date + IsOutOfBound = !m_dateCalcEngine->AddDuration(StartDate, dateDiff, &dateTimeResult); + } + else + { + // Subtract number of Days, Months and Years from a Date + IsOutOfBound = !m_dateCalcEngine->SubtractDuration(StartDate, dateDiff, &dateTimeResult); + } + + if (!m_isOutOfBound) + { + DateResult = dateTimeResult; + } + } +} + +void DateCalculatorViewModel::UpdateDisplayResult() +{ + if (m_IsDateDiffMode) + { + // Are to and from dates the same + if (m_dateDiffResultInDays.day == 0) + { + IsDiffInDays = true; + StrDateDiffResultInDays = L""; + StrDateDiffResult = AppResourceProvider::GetInstance().GetResourceString(L"Date_SameDates"); + } + else if ((m_dateDiffResult.year == 0) && + (m_dateDiffResult.month == 0) && + (m_dateDiffResult.week == 0)) + { + IsDiffInDays = true; + StrDateDiffResultInDays = L""; + + // Display result in number of days + StrDateDiffResult = GetDateDiffStringInDays(); + } + else + { + IsDiffInDays = false; + + // Display result in days, weeks, months and years + StrDateDiffResult = GetDateDiffString(); + + // Display result in number of days + StrDateDiffResultInDays = GetDateDiffStringInDays(); + } + } + else + { + if (m_isOutOfBound) + { + // Display Date out of bound message + StrDateResult = AppResourceProvider::GetInstance().GetResourceString(L"Date_OutOfBoundMessage"); + } + else + { + // Display the resulting date in long format + StrDateResult = m_dateTimeFormatter->Format(DateResult); + } + } +} + +void DateCalculatorViewModel::UpdateStrDateDiffResultAutomationName() +{ + String^ automationFormat = AppResourceProvider::GetInstance().GetResourceString(L"Date_DifferenceResultAutomationName"); + wstring localizedAutomationName = LocalizationStringUtil::GetLocalizedString(automationFormat->Data(), StrDateDiffResult->Data()); + StrDateDiffResultAutomationName = ref new String(localizedAutomationName.c_str()); +} + +void DateCalculatorViewModel::UpdateStrDateResultAutomationName() +{ + String^ automationFormat = AppResourceProvider::GetInstance().GetResourceString(L"Date_ResultingDateAutomationName"); + wstring localizedAutomationName = LocalizationStringUtil::GetLocalizedString(automationFormat->Data(), StrDateResult->Data()); + StrDateResultAutomationName = ref new String(localizedAutomationName.c_str()); +} + +void DateCalculatorViewModel::InitializeDateOutputFormats(_In_ String^ calendarIdentifier) +{ + // Format for Add/Subtract days + m_dateTimeFormatter = LocalizationService::GetRegionalSettingsAwareDateTimeFormatter( + L"longdate", + calendarIdentifier, + ClockIdentifiers::TwentyFourHour); // Clock Identifier is not used + + // Format for Date Difference + m_allDateUnitsOutputFormat = DateUnit::Year | DateUnit::Month | DateUnit::Week | DateUnit::Day; + m_daysOutputFormat = DateUnit::Day; +} + +String^ DateCalculatorViewModel::GetDateDiffString() const +{ + String^ result = L""; + bool addDelimiter = false; + AppResourceProvider resourceLoader = AppResourceProvider::GetInstance(); + + auto yearCount = m_dateDiffResult.year; + if (yearCount > 0) + { + result = String::Concat(GetLocalizedNumberString(yearCount), L" "); + + if (yearCount > 1) + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Years")); + } + else + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Year")); + } + + // set the flags to add a delimiter whenever the next unit is added + addDelimiter = true; + } + + auto monthCount = m_dateDiffResult.month; + if (monthCount > 0) + { + if (addDelimiter) + { + result = String::Concat(result, m_listSeparator); + } + else + { + addDelimiter = true; + } + + result = String::Concat(result, String::Concat(GetLocalizedNumberString(monthCount), L" ")); + + if (monthCount > 1) + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Months")); + } + else + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Month")); + } + } + + auto weekCount = m_dateDiffResult.week; + if (weekCount > 0) + { + if (addDelimiter) + { + result = String::Concat(result, m_listSeparator); + } + else + { + addDelimiter = true; + } + + result = String::Concat(result, String::Concat(GetLocalizedNumberString(weekCount), L" ")); + + if (weekCount > 1) + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Weeks")); + } + else + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Week")); + } + } + + auto dayCount = m_dateDiffResult.day; + if (dayCount > 0) + { + if (addDelimiter) + { + result = String::Concat(result, m_listSeparator); + } + else + { + addDelimiter = true; + } + + result = String::Concat(result, String::Concat(GetLocalizedNumberString(dayCount), L" ")); + + if (dayCount > 1) + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Days")); + } + else + { + result = String::Concat(result, resourceLoader.GetResourceString(L"Date_Day")); + } + } + + return result; +} + +String^ DateCalculatorViewModel::GetDateDiffStringInDays() const +{ + String^ strDateUnit; + + // Display the result as '1 day' or 'N days' + if (m_dateDiffResultInDays.day > 1) + { + strDateUnit = AppResourceProvider::GetInstance().GetResourceString(L"Date_Days"); + } + else + { + strDateUnit = AppResourceProvider::GetInstance().GetResourceString(L"Date_Day"); + } + + return String::Concat(GetLocalizedNumberString(m_dateDiffResultInDays.day), String::Concat(L" ", strDateUnit)); +} + +void DateCalculatorViewModel::OnCopyCommand(Platform::Object^ parameter) +{ + if (m_IsDateDiffMode) + { + CopyPasteManager::CopyToClipboard(m_StrDateDiffResult); + } + else + { + CopyPasteManager::CopyToClipboard(m_StrDateResult); + } +} + +String^ DateCalculatorViewModel::GetLocalizedNumberString(int value) const +{ + wstring numberStr(to_wstring(value)); + LocalizationSettings::GetInstance().LocalizeDisplayValue(&numberStr); + return ref new String(numberStr.c_str()); +} + +// Adjusts the given DateTime to 12AM of the same day +DateTime DateCalculatorViewModel::ClipTime(DateTime dateTime) +{ + auto calendar = ref new Calendar(); + calendar->SetDateTime(dateTime); + calendar->Period = 1; + calendar->Hour = 12; + calendar->Minute = 0; + calendar->Second = 0; + calendar->Nanosecond = 0; + + return calendar->GetDateTime(); +} diff --git a/src/CalcViewModel/DateCalculatorViewModel.h b/src/CalcViewModel/DateCalculatorViewModel.h new file mode 100644 index 00000000..de1dc7e3 --- /dev/null +++ b/src/CalcViewModel/DateCalculatorViewModel.h @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Common\DateCalculator.h" + +const int c_maxOffsetValue = 999; + +namespace CalculatorApp +{ + namespace ViewModel + { + [Windows::UI::Xaml::Data::Bindable] + public ref class DateCalculatorViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + DateCalculatorViewModel(); + + OBSERVABLE_OBJECT_CALLBACK(OnPropertyChanged); + + // Input Properties + OBSERVABLE_PROPERTY_RW(bool, IsDateDiffMode); + OBSERVABLE_PROPERTY_RW(bool, IsAddMode); + OBSERVABLE_PROPERTY_RW(bool, IsDiffInDays); // If diff is only in days or the dates are the same, + // then show only one result and avoid redundancy + + OBSERVABLE_PROPERTY_RW(int, DaysOffset); + OBSERVABLE_PROPERTY_RW(int, MonthsOffset); + OBSERVABLE_PROPERTY_RW(int, YearsOffset); + + // Read only property for offset values + property Windows::Foundation::Collections::IVector^ OffsetValues + { + Windows::Foundation::Collections::IVector^ get() { return m_offsetValues; } + } + + // From date for Date Diff + property Windows::Foundation::DateTime FromDate + { + Windows::Foundation::DateTime get() { return m_fromDate; } + + void set(Windows::Foundation::DateTime value) + { + if (m_fromDate.UniversalTime != value.UniversalTime) + { + m_fromDate = value; + RaisePropertyChanged("FromDate"); + } + } + } + + // To date for Date Diff + property Windows::Foundation::DateTime ToDate + { + Windows::Foundation::DateTime get() { return m_toDate; } + + void set(Windows::Foundation::DateTime value) + { + if (m_toDate.UniversalTime != value.UniversalTime) + { + m_toDate = value; + RaisePropertyChanged("ToDate"); + } + } + } + + // Start date for Add/Subtract date + property Windows::Foundation::DateTime StartDate + { + Windows::Foundation::DateTime get() { return m_startDate; } + + void set(Windows::Foundation::DateTime value) + { + if (m_startDate.UniversalTime != value.UniversalTime) + { + m_startDate = value; + RaisePropertyChanged("StartDate"); + } + } + } + + // Output Properties + OBSERVABLE_PROPERTY_RW(Platform::String^, StrDateDiffResult); + OBSERVABLE_PROPERTY_RW(Platform::String^, StrDateDiffResultAutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, StrDateDiffResultInDays); + OBSERVABLE_PROPERTY_RW(Platform::String^, StrDateResult); + OBSERVABLE_PROPERTY_RW(Platform::String^, StrDateResultAutomationName); + + COMMAND_FOR_METHOD(CopyCommand, DateCalculatorViewModel::OnCopyCommand); + + void OnCopyCommand(Platform::Object^ parameter); + + private: + void OnPropertyChanged(_In_ Platform::String^ prop); + void OnInputsChanged(); + void UpdateDisplayResult(); + void UpdateStrDateDiffResultAutomationName(); + void UpdateStrDateResultAutomationName(); + void InitializeDateOutputFormats(Platform::String^ calendarIdentifer); + Platform::String^ GetDateDiffString() const; + Platform::String^ GetDateDiffStringInDays() const; + Platform::String^ GetLocalizedNumberString(int value) const; + static Windows::Foundation::DateTime ClipTime(Windows::Foundation::DateTime dateTime); + + static void CheckClipTimeSameDay(Windows::Globalization::Calendar^ reference); + + property bool IsOutOfBound + { + bool get() { return m_isOutOfBound; } + void set(bool value) { m_isOutOfBound = value; UpdateDisplayResult(); } + } + + property CalculatorApp::Common::DateCalculation::DateDifference DateDiffResult + { + CalculatorApp::Common::DateCalculation::DateDifference get() { return m_dateDiffResult; } + void set(CalculatorApp::Common::DateCalculation::DateDifference value) { m_dateDiffResult = value; UpdateDisplayResult(); } + } + + property CalculatorApp::Common::DateCalculation::DateDifference DateDiffResultInDays + { + CalculatorApp::Common::DateCalculation::DateDifference get() { return m_dateDiffResultInDays; } + void set(CalculatorApp::Common::DateCalculation::DateDifference value) { m_dateDiffResultInDays = value; UpdateDisplayResult(); } + } + + property Windows::Foundation::DateTime DateResult + { + Windows::Foundation::DateTime get() { return m_dateResult; } + void set(Windows::Foundation::DateTime value) { m_dateResult = value; UpdateDisplayResult();} + } + + private: + // Property variables + bool m_isOutOfBound; + Platform::Collections::Vector^ m_offsetValues; + Windows::Foundation::DateTime m_fromDate; + Windows::Foundation::DateTime m_toDate; + Windows::Foundation::DateTime m_startDate; + Windows::Foundation::DateTime m_dateResult; + CalculatorApp::Common::DateCalculation::DateDifference m_dateDiffResult; + CalculatorApp::Common::DateCalculation::DateDifference m_dateDiffResultInDays; + + // Private members + std::shared_ptr m_dateCalcEngine; + CalculatorApp::Common::DateCalculation::DateUnit m_daysOutputFormat; + CalculatorApp::Common::DateCalculation::DateUnit m_allDateUnitsOutputFormat; + Windows::Globalization::DateTimeFormatting::DateTimeFormatter^ m_dateTimeFormatter; + Platform::String^ m_listSeparator; + }; + } +} diff --git a/src/CalcViewModel/HistoryItemViewModel.cpp b/src/CalcViewModel/HistoryItemViewModel.cpp new file mode 100644 index 00000000..f3726fc7 --- /dev/null +++ b/src/CalcViewModel/HistoryItemViewModel.cpp @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "HistoryItemViewModel.h" +#include "Common\LocalizationService.h" + +using namespace CalculatorApp::Common; +using namespace CalculatorApp::ViewModel; +using namespace std; +using namespace Platform; + + +HistoryItemViewModel::HistoryItemViewModel(String^ expression, String^ result, + _In_ const shared_ptr>> &spTokens, + _In_ const shared_ptr>> &spCommands) :m_expression(expression), m_result(result), m_spTokens(spTokens), m_spCommands(spCommands) +{ + // updating accessibility names for expression and result + m_accExpression = HistoryItemViewModel::GetAccessibleExpressionFromTokens(spTokens, m_expression); + m_accResult = LocalizationService::GetNarratorReadableString(m_result); +} + +String^ HistoryItemViewModel::GetAccessibleExpressionFromTokens(_In_ shared_ptr< CalculatorVector< pair< wstring, int > > > const &spTokens, _In_ String^ fallbackExpression) +{ + // updating accessibility names for expression and result + wstringstream accExpression{}; + accExpression << L""; + + unsigned int nTokens; + HRESULT hr = spTokens->GetSize(&nTokens); + if (SUCCEEDED(hr)) + { + pair tokenItem; + for (unsigned int i = 0; i < nTokens; i++) + { + hr = spTokens->GetAt(i, &tokenItem); + if (FAILED(hr)) + { + break; + } + + wstring token = tokenItem.first; + accExpression << LocalizationService::GetNarratorReadableToken(StringReference(token.c_str()))->Data(); + } + } + + if (SUCCEEDED(hr)) + { + wstring expressionSuffix{}; + hr = spTokens->GetExpressionSuffix(&expressionSuffix); + if (SUCCEEDED(hr)) + { + accExpression << expressionSuffix; + } + } + + if (FAILED(hr)) + { + return LocalizationService::GetNarratorReadableString(fallbackExpression); + } + else + { + return ref new String(accExpression.str().c_str()); + } +} diff --git a/src/CalcViewModel/HistoryItemViewModel.h b/src/CalcViewModel/HistoryItemViewModel.h new file mode 100644 index 00000000..25cb4abb --- /dev/null +++ b/src/CalcViewModel/HistoryItemViewModel.h @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace ViewModel + { + [Windows::UI::Xaml::Data::Bindable] + public ref class HistoryItemViewModel sealed : Windows::UI::Xaml::Data::ICustomPropertyProvider + { + + internal: + + HistoryItemViewModel(Platform::String^ expression, + Platform::String^ result, + _In_ std::shared_ptr>> const &spTokens, + _In_ std::shared_ptr>> const &spCommands); + + std::shared_ptr>> const& GetTokens() + { + return m_spTokens; + } + + std::shared_ptr>> const& GetCommands() + { + return m_spCommands; + } + + public: + + property Platform::String^ Expression + { + Platform::String^ get() { return m_expression; } + } + + property Platform::String^ AccExpression + { + Platform::String^ get() { return m_accExpression; } + } + + property Platform::String^ Result + { + Platform::String^ get() { return m_result; } + } + + property Platform::String^ AccResult + { + Platform::String^ get() { return m_accResult; } + } + + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetCustomProperty(Platform::String^ name) + { + return nullptr; + } + + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetIndexedProperty(Platform::String^ name, Windows::UI::Xaml::Interop::TypeName type) + { + return nullptr; + } + + virtual property Windows::UI::Xaml::Interop::TypeName Type + { + Windows::UI::Xaml::Interop::TypeName get() + { + return this->GetType(); + } + } + + virtual Platform::String^ GetStringRepresentation() + { + return m_accExpression + " " + m_accResult; + } + + private: + static Platform::String^ GetAccessibleExpressionFromTokens( + _In_ std::shared_ptr>> const &spTokens, + _In_ Platform::String^ fallbackExpression); + + private: + Platform::String^ m_expression; + Platform::String^ m_accExpression; + Platform::String^ m_accResult; + Platform::String^ m_result; + std::shared_ptr>> m_spTokens; + std::shared_ptr>> m_spCommands; + }; + } +} diff --git a/src/CalcViewModel/HistoryViewModel.cpp b/src/CalcViewModel/HistoryViewModel.cpp new file mode 100644 index 00000000..a0b5d90d --- /dev/null +++ b/src/CalcViewModel/HistoryViewModel.cpp @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "HistoryViewModel.h" +#include "Common\LocalizationStringUtil.h" +#include "Common\LocalizationSettings.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::Automation; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace Windows::Security::Cryptography; +using namespace Windows::Foundation::Collections; +using CalculatorApp::TraceLogger; + +static StringReference HistoryVectorLengthKey{ L"HistoryVectorLength" }; + +namespace CalculatorApp::ViewModel::HistoryResourceKeys +{ + StringReference HistoryCleared(L"HistoryList_Cleared"); +} + +HistoryViewModel::HistoryViewModel(_In_ CalculationManager::CalculatorManager* calculatorManager) : + m_calculatorManager(calculatorManager), + m_localizedHistoryCleared(nullptr) +{ + AreHistoryShortcutsEnabled = true; + + Items = ref new Platform::Collections::Vector(); + ItemSize = 0; +} + +void HistoryViewModel::RestoreCompleteHistory() +{ + RestoreHistory(CalculationManager::CALCULATOR_MODE::CM_STD); + RestoreHistory(CalculationManager::CALCULATOR_MODE::CM_SCI); +} + +// this will reload Items with the history list based on current mode +void HistoryViewModel::ReloadHistory(_In_ ViewMode currentMode) +{ + if (currentMode == ViewMode::Standard) + { + m_currentMode = CalculationManager::CALCULATOR_MODE::CM_STD; + } + else if (currentMode == ViewMode::Scientific) + { + m_currentMode = CalculationManager::CALCULATOR_MODE::CM_SCI; + } + else + { + return; + } + + auto historyListModel = m_calculatorManager->GetHistoryItems(m_currentMode); + auto historyListVM = ref new Platform::Collections::Vector(); + const auto& localizer = LocalizationSettings::GetInstance(); + if (historyListModel.size() > 0) + { + for (auto ritr = historyListModel.rbegin(); ritr != historyListModel.rend(); ++ritr) + { + wstring expression = (*ritr)->historyItemVector.expression; + wstring result = (*ritr)->historyItemVector.result; + localizer.LocalizeDisplayValue(&expression); + localizer.LocalizeDisplayValue(&result); + + auto item = ref new HistoryItemViewModel(ref new Platform::String( expression.c_str()), + ref new Platform::String(result.c_str()), + (*ritr)->historyItemVector.spTokens, (*ritr)->historyItemVector.spCommands); + historyListVM->Append(item); + } + } + + Items = historyListVM; + UpdateItemSize(); +} + +void HistoryViewModel::OnHistoryItemAdded(_In_ unsigned int addedItemIndex) +{ + auto newItem = m_calculatorManager->GetHistoryItem(addedItemIndex); + const auto& localizer = LocalizationSettings::GetInstance(); + wstring expression = newItem->historyItemVector.expression; + wstring result = newItem->historyItemVector.result; + localizer.LocalizeDisplayValue(&expression); + localizer.LocalizeDisplayValue(&result); + auto item = ref new HistoryItemViewModel(ref new Platform::String(expression.c_str()), + ref new Platform::String(result.c_str()), + newItem->historyItemVector.spTokens, newItem->historyItemVector.spCommands ); + + // check if we have not hit the max items + if (Items->Size >= m_calculatorManager->MaxHistorySize()) + { + // this means the item already exists + Items->RemoveAt(Items->Size -1); + } + + assert(addedItemIndex <= m_calculatorManager->MaxHistorySize() && addedItemIndex >= 0); + Items->InsertAt(0, item); + UpdateItemSize(); + SaveHistory(); +} + +void HistoryViewModel::SetCalculatorDisplay(CalculatorDisplay &calculatorDisplay) +{ + WeakReference historyViewModel(this); + calculatorDisplay.SetHistoryCallback(historyViewModel); +} + +void HistoryViewModel::ShowItem(_In_ HistoryItemViewModel^ e) +{ + HistoryItemClicked(e); +} + +void HistoryViewModel::DeleteItem(_In_ HistoryItemViewModel^ e) +{ + uint32_t itemIndex; + if (Items->IndexOf(e, &itemIndex)) + { + if (m_calculatorManager->RemoveHistoryItem(itemIndex)) + { + // Keys for the history container are index based. + // SaveHistory() re-inserts the items anyway, so it's faster to just clear out the container. + CalculationManager::CALCULATOR_MODE currentMode = m_currentMode; + ApplicationDataContainer^ historyContainer = GetHistoryContainer(currentMode); + historyContainer->Values->Clear(); + + Items->RemoveAt(itemIndex); + UpdateItemSize(); + SaveHistory(); + } + } +} + +void HistoryViewModel::OnHideCommand(_In_ Platform::Object^ e) +{ + // added at VM layer so that the views do not have to individually raise events + HideHistoryClicked(); +} + +void HistoryViewModel::OnClearCommand(_In_ Platform::Object^ e) +{ + TraceLogger::GetInstance().LogClearHistory(); + if (AreHistoryShortcutsEnabled == true) + { + m_calculatorManager->ClearHistory(); + + if (Items->Size > 0) + { + CalculationManager::CALCULATOR_MODE currentMode = m_currentMode; + ClearHistoryContainer(currentMode); + Items->Clear(); + UpdateItemSize(); + } + + MakeHistoryClearedNarratorAnnouncement(HistoryResourceKeys::HistoryCleared, m_localizedHistoryCleared); + } +} + +// this method restores history vector per mode +void HistoryViewModel::RestoreHistory(_In_ CalculationManager::CALCULATOR_MODE cMode) +{ + ApplicationDataContainer^ historyContainer = GetHistoryContainer(cMode); + std::shared_ptr>> historyVector = std::make_shared>>(); + auto historyVectorLength = static_cast(historyContainer->Values->Lookup(HistoryVectorLengthKey)); + bool failure = false; + + if (historyVectorLength > 0) + { + for (int i = 0; i < historyVectorLength; ++i) + { + try + { + // deserialize each item + auto item = DeserializeHistoryItem(i.ToString(), historyContainer); + std::shared_ptr Item = std::make_shared(item); + historyVector->push_back(Item); + } + catch (Platform::Exception^ e) + { + failure = true; + break; + } + } + + if (!failure) + { + // if task has been cancelled set history to 0 + m_calculatorManager->SetHistory(cMode, *historyVector); + + // update length once again for consistency between stored number of items and length + UpdateHistoryVectorLength(static_cast(historyVector->size()), cMode); + } + else + { + // in case of failure do not show any item + UpdateHistoryVectorLength(0, cMode); + } + } +} + +Platform::String^ HistoryViewModel::GetHistoryContainerKey(_In_ CalculationManager::CALCULATOR_MODE cMode) +{ + Platform::ValueType^ modeValue = static_cast(cMode); + return Platform::String::Concat(modeValue->ToString(), L"_History"); +} + +ApplicationDataContainer^ HistoryViewModel::GetHistoryContainer(_In_ CalculationManager::CALCULATOR_MODE cMode) +{ + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + ApplicationDataContainer^ historyContainer; + + // naming container based on mode + Platform::String^ historyContainerKey = GetHistoryContainerKey(cMode); + + if (localSettings->Containers->HasKey(historyContainerKey)) + { + historyContainer = localSettings->Containers->Lookup(historyContainerKey); + } + else + { + // create container for adding data + historyContainer = localSettings->CreateContainer(historyContainerKey, ApplicationDataCreateDisposition::Always); + int initialHistoryVectorLength = 0; + historyContainer->Values->Insert(HistoryVectorLengthKey, initialHistoryVectorLength); + } + + return historyContainer; +} + +void HistoryViewModel::ClearHistoryContainer(_In_ CalculationManager::CALCULATOR_MODE cMode) +{ + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + localSettings->DeleteContainer(GetHistoryContainerKey(cMode)); +} + +// this method will be used to update the history item length +void HistoryViewModel::UpdateHistoryVectorLength(_In_ int newValue, _In_ CalculationManager::CALCULATOR_MODE cMode) +{ + ApplicationDataContainer^ historyContainer = GetHistoryContainer(cMode); + historyContainer->Values->Remove(HistoryVectorLengthKey); + historyContainer->Values->Insert(HistoryVectorLengthKey, newValue); +} + +void HistoryViewModel::ClearHistory() +{ + ClearHistoryContainer(CalculationManager::CALCULATOR_MODE::CM_STD); + ClearHistoryContainer(CalculationManager::CALCULATOR_MODE::CM_SCI); +} + +void HistoryViewModel::SaveHistory() +{ + ApplicationDataContainer^ historyContainer = GetHistoryContainer(m_currentMode); + auto currentHistoryVector = m_calculatorManager->GetHistoryItems(m_currentMode); + bool failure = false; + int index = 0; + Platform::String^ serializedHistoryItem; + + for (auto iter = currentHistoryVector.begin(); iter != currentHistoryVector.end(); ++iter) + { + try + { + serializedHistoryItem = SerializeHistoryItem(*iter); + historyContainer->Values->Insert(index.ToString(), serializedHistoryItem); + } + catch (Platform::Exception^) + { + failure = true; + break; + } + + ++index; + } + + if (!failure) + { + // insertion is successful + UpdateHistoryVectorLength(static_cast(currentHistoryVector.size()), m_currentMode); + } + else + { + UpdateHistoryVectorLength(0, m_currentMode); + } +} + +// this serializes a history item into a base64 encoded string +Platform::String^ HistoryViewModel::SerializeHistoryItem(_In_ std::shared_ptr const &item) +{ + HRESULT hr = S_OK; + DataWriter^ writer = ref new DataWriter(); + auto expr = item->historyItemVector.expression; + auto result = item->historyItemVector.result; + auto platformExpr = ref new Platform::String(expr.c_str()); + writer->WriteUInt32(writer->MeasureString(platformExpr)); + writer->WriteString(platformExpr); + auto platformResult = ref new Platform::String(result.c_str()); + writer->WriteUInt32(writer->MeasureString(platformResult)); + writer->WriteString(platformResult); + + Utils::SerializeCommandsAndTokens(item->historyItemVector.spTokens, item->historyItemVector.spCommands, writer); + + IBuffer^ buffer = writer->DetachBuffer(); + if (buffer == nullptr) + { + throw ref new Platform::Exception(E_POINTER, ref new Platform::String(L"History Item is NULL")); + } + + return CryptographicBuffer::EncodeToBase64String(buffer); +} + +CalculationManager::HISTORYITEM HistoryViewModel::DeserializeHistoryItem(_In_ Platform::String^ historyItemKey, _In_ ApplicationDataContainer^ historyContainer) +{ + CalculationManager::HISTORYITEM historyItem; + if (historyContainer->Values->HasKey(historyItemKey)) + { + Object^ historyItemValues = historyContainer->Values->Lookup(historyItemKey); + + if (historyItemValues == nullptr) + { + throw ref new Platform::Exception(E_POINTER, ref new Platform::String(L"History Item is NULL")); + } + + String^ historyData = safe_cast(historyItemValues); + IBuffer^ buffer = CryptographicBuffer::DecodeFromBase64String(historyData); + + if (buffer == nullptr) + { + throw ref new Platform::Exception(E_POINTER, ref new Platform::String(L"History Item is NULL")); + } + + DataReader^ reader = DataReader::FromBuffer(buffer); + auto exprLen = reader->ReadUInt32(); + auto expression = reader->ReadString(exprLen); + historyItem.historyItemVector.expression = expression->Data(); + + auto resultLen = reader->ReadUInt32(); + auto result = reader->ReadString(resultLen); + historyItem.historyItemVector.result = result->Data(); + + historyItem.historyItemVector.spCommands = Utils::DeserializeCommands(reader); + historyItem.historyItemVector.spTokens = Utils::DeserializeTokens(reader); + } + else + { + throw ref new Platform::Exception(E_ACCESSDENIED, ref new Platform::String(L"History Item not found")); + } + return historyItem; +} + +bool HistoryViewModel::IsValid(_In_ CalculationManager::HISTORYITEM item) +{ + return (!item.historyItemVector.expression.empty() && + !item.historyItemVector.result.empty() && + (bool)item.historyItemVector.spCommands && + (bool)item.historyItemVector.spTokens); +} + +void HistoryViewModel::UpdateItemSize() +{ + ItemSize = Items->Size; +} + +void HistoryViewModel::MakeHistoryClearedNarratorAnnouncement(String^ resourceKey, String^& formatVariable) +{ + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement(resourceKey, formatVariable); + + HistoryAnnouncement = CalculatorAnnouncement::GetHistoryClearedAnnouncement(announcement); +} diff --git a/src/CalcViewModel/HistoryViewModel.h b/src/CalcViewModel/HistoryViewModel.h new file mode 100644 index 00000000..eb5d6544 --- /dev/null +++ b/src/CalcViewModel/HistoryViewModel.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "Common\Automation\NarratorAnnouncement.h" +#include "Common\CalculatorDisplay.h" +#include "HistoryItemViewModel.h" + +namespace CalculatorApp +{ + namespace CM = CalculationManager; + + namespace ViewModel + { + public delegate void HideHistoryClickedHandler(); + public delegate void HistoryItemClickedHandler(CalculatorApp::ViewModel::HistoryItemViewModel^ e); + + [Windows::UI::Xaml::Data::Bindable] + public ref class HistoryViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + + public: + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(int, ItemSize); + OBSERVABLE_PROPERTY_RW(Windows::UI::Xaml::Interop::IBindableObservableVector^, Items); + OBSERVABLE_PROPERTY_RW(bool, AreHistoryShortcutsEnabled); + OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::Automation::NarratorAnnouncement^, HistoryAnnouncement); + + void OnHistoryItemAdded(_In_ unsigned int addedItemIndex); + + COMMAND_FOR_METHOD(HideCommand, HistoryViewModel::OnHideCommand); + void OnHideCommand(_In_ Platform::Object^ e); + COMMAND_FOR_METHOD(ClearCommand, HistoryViewModel::OnClearCommand); + void OnClearCommand(_In_ Platform::Object^ e); + + // events that are created + event HideHistoryClickedHandler^ HideHistoryClicked; + event HistoryItemClickedHandler^ HistoryItemClicked; + void ShowItem(_In_ CalculatorApp::ViewModel::HistoryItemViewModel^ e); + void ClearHistory(); + void RestoreCompleteHistory(); + + internal: + HistoryViewModel(_In_ CalculationManager::CalculatorManager* calculatorManager); + void SetCalculatorDisplay(CalculatorDisplay &calculatorDisplay); + void ReloadHistory(_In_ CalculatorApp::Common::ViewMode currentMode); + + void DeleteItem(_In_ CalculatorApp::ViewModel::HistoryItemViewModel^ e); + + // store history in app data functions + Platform::String^ SerializeHistoryItem(_In_ std::shared_ptr const &item); + void SaveHistory(); + + private: + CalculationManager::CalculatorManager* const m_calculatorManager; + CalculatorDisplay m_calculatorDisplay; + CalculationManager::CALCULATOR_MODE m_currentMode; + Platform::String^ m_localizedHistoryCleared; + + void RestoreHistory(_In_ CalculationManager::CALCULATOR_MODE cMode); + CalculationManager::HISTORYITEM DeserializeHistoryItem(_In_ Platform::String^ historyItemKey, _In_ Windows::Storage::ApplicationDataContainer^ historyContainer); + Windows::Storage::ApplicationDataContainer^ GetHistoryContainer(_In_ CalculationManager::CALCULATOR_MODE cMode); + Platform::String^ GetHistoryContainerKey(_In_ CalculationManager::CALCULATOR_MODE cMode); + void ClearHistoryContainer(_In_ CalculationManager::CALCULATOR_MODE cMode); + void UpdateHistoryVectorLength(_In_ int newValue, _In_ CalculationManager::CALCULATOR_MODE cMode); + bool IsValid(_In_ CalculationManager::HISTORYITEM item); + + void MakeHistoryClearedNarratorAnnouncement(Platform::String^ resourceKey, Platform::String^& formatVariable); + + friend class CalculatorDisplay; + void UpdateItemSize(); + }; + } +} diff --git a/src/CalcViewModel/MemoryItemViewModel.cpp b/src/CalcViewModel/MemoryItemViewModel.cpp new file mode 100644 index 00000000..b7d21649 --- /dev/null +++ b/src/CalcViewModel/MemoryItemViewModel.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "MemoryItemViewModel.h" +#include "StandardCalculatorViewModel.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::Automation; +using namespace CalculatorApp::ViewModel; +using namespace Platform; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace Windows::Security::Cryptography; +using namespace Windows::Foundation::Collections; + +void MemoryItemViewModel::Clear() +{ + m_calcVM->OnMemoryClear(Position); +}; + +void MemoryItemViewModel::MemoryAdd() +{ + m_calcVM->OnMemoryAdd(Position); +}; + +void MemoryItemViewModel::MemorySubtract() +{ + m_calcVM->OnMemorySubtract(Position); +}; diff --git a/src/CalcViewModel/MemoryItemViewModel.h b/src/CalcViewModel/MemoryItemViewModel.h new file mode 100644 index 00000000..1b7b6332 --- /dev/null +++ b/src/CalcViewModel/MemoryItemViewModel.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace ViewModel + { + ref class StandardCalculatorViewModel; + + /// + /// Model representation of a single item in the Memory list + /// + [Windows::UI::Xaml::Data::Bindable] + public ref class MemoryItemViewModel sealed : + public Windows::UI::Xaml::Data::INotifyPropertyChanged, + Windows::UI::Xaml::Data::ICustomPropertyProvider + { + public: + MemoryItemViewModel(StandardCalculatorViewModel^ calcVM) : m_Position(-1), m_calcVM(calcVM) {} + OBSERVABLE_OBJECT(); + OBSERVABLE_PROPERTY_RW(int, Position); + OBSERVABLE_PROPERTY_RW(Platform::String^, Value); + + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetCustomProperty(Platform::String^ name) + { + return nullptr; + } + + virtual Windows::UI::Xaml::Data::ICustomProperty^ GetIndexedProperty(Platform::String^ name, Windows::UI::Xaml::Interop::TypeName type) + { + return nullptr; + } + + virtual property Windows::UI::Xaml::Interop::TypeName Type + { + Windows::UI::Xaml::Interop::TypeName get() + { + return this->GetType(); + } + } + + virtual Platform::String^ GetStringRepresentation() + { + return Value; + } + + void Clear(); + void MemoryAdd(); + void MemorySubtract(); + + private: + StandardCalculatorViewModel^ m_calcVM; + }; + } +} diff --git a/src/CalcViewModel/StandardCalculatorViewModel.cpp b/src/CalcViewModel/StandardCalculatorViewModel.cpp new file mode 100644 index 00000000..19409f39 --- /dev/null +++ b/src/CalcViewModel/StandardCalculatorViewModel.cpp @@ -0,0 +1,2010 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "StandardCalculatorViewModel.h" +#include "Common\CalculatorButtonPressedEventArgs.h" +#include "Common\LocalizationStringUtil.h" +#include "Common\LocalizationSettings.h" +#include "Common\CopyPasteManager.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::Automation; +using namespace CalculatorApp::ViewModel; +using namespace CalculationManager; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Foundation; +using namespace Windows::System; +using namespace Windows::UI::Core; +using namespace Windows::UI::Popups; +using namespace Windows::Storage::Streams; +using namespace Windows::Foundation::Collections; +using namespace Utils; + +constexpr int StandardModePrecision = 16; +constexpr int ScientificModePrecision = 32; +constexpr int ProgrammerModePrecision = 64; + +namespace CalculatorApp::ViewModel +{ + namespace CalculatorViewModelProperties + { + StringReference IsMemoryEmpty(L"IsMemoryEmpty"); + StringReference IsScientific(L"IsScientific"); + StringReference IsStandard(L"IsStandard"); + StringReference IsProgrammer(L"IsProgrammer"); + StringReference DisplayValue(L"DisplayValue"); + StringReference IsInError(L"IsInError"); + StringReference BinaryDisplayValue(L"BinaryDisplayValue"); + } + + namespace CalculatorResourceKeys + { + StringReference CalculatorExpression(L"Format_CalculatorExpression"); + StringReference CalculatorResults(L"Format_CalculatorResults"); + StringReference CalculatorResults_DecimalSeparator_Announced(L"Format_CalculatorResults_Decimal"); + StringReference HexButton(L"Format_HexButtonValue"); + StringReference DecButton(L"Format_DecButtonValue"); + StringReference OctButton(L"Format_OctButtonValue"); + StringReference BinButton(L"Format_BinButtonValue"); + StringReference LeftParenthesisAutomationFormat(L"Format_OpenParenthesisAutomationNamePrefix"); + StringReference MaxDigitsReachedFormat(L"Format_MaxDigitsReached"); + StringReference ButtonPressFeedbackFormat(L"Format_ButtonPressAuditoryFeedback"); + StringReference MemorySave(L"Format_MemorySave"); + StringReference MemoryItemChanged(L"Format_MemorySlotChanged"); + StringReference MemoryItemCleared(L"Format_MemorySlotCleared"); + StringReference MemoryCleared(L"Memory_Cleared"); + StringReference DisplayCopied(L"Display_Copied"); + } +} + +StandardCalculatorViewModel::StandardCalculatorViewModel() : + m_DisplayValue(L"0"), + m_DecimalDisplayValue(L"0"), + m_HexDisplayValue(L"0"), + m_BinaryDisplayValue(L"0"), + m_OctalDisplayValue(L"0"), + m_standardCalculatorManager(make_unique(&m_calculatorDisplay, &m_resourceProvider)), + m_MemorizedNumbers(ref new Vector()), + m_IsMemoryEmpty(true), + m_IsFToEChecked(false), + m_isShiftChecked(false), + m_IsShiftProgrammerChecked(false), + m_IsQwordEnabled(true), + m_IsDwordEnabled(true), + m_IsWordEnabled(true), + m_IsByteEnabled(true), + m_isBitFlipChecked(false), + m_isBinaryBitFlippingEnabled(false), + m_CurrentRadixType(RADIX_TYPE::DEC_RADIX), + m_CurrentAngleType(NumbersAndOperatorsEnum::Degree), + m_OpenParenthesisCount(L""), + m_Announcement(nullptr), + m_feedbackForButtonPress(nullptr), + m_isRtlLanguage(false), + m_localizedMaxDigitsReachedAutomationFormat(nullptr), + m_localizedButtonPressFeedbackAutomationFormat(nullptr), + m_localizedMemorySavedAutomationFormat(nullptr), + m_localizedMemoryItemChangedAutomationFormat(nullptr), + m_localizedMemoryItemClearedAutomationFormat(nullptr), + m_localizedMemoryCleared(nullptr) +{ + WeakReference calculatorViewModel(this); + m_calculatorDisplay.SetCallback(calculatorViewModel); + m_expressionAutomationNameFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::CalculatorExpression); + m_localizedCalculationResultAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::CalculatorResults); + m_localizedCalculationResultDecimalAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::CalculatorResults_DecimalSeparator_Announced); + m_localizedHexaDecimalAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::HexButton); + m_localizedDecimalAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::DecButton); + m_localizedOctalAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::OctButton); + m_localizedBinaryAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::BinButton); + m_leftParenthesisAutomationFormat = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::LeftParenthesisAutomationFormat); + + // Initialize the Automation Name + CalculationResultAutomationName = GetLocalizedStringFormat(m_localizedCalculationResultAutomationFormat, m_DisplayValue); + CalculationExpressionAutomationName = GetLocalizedStringFormat(m_expressionAutomationNameFormat, L""); + + // Initialize history view model + m_HistoryVM = ref new HistoryViewModel(m_standardCalculatorManager.get()); + m_HistoryVM->SetCalculatorDisplay(m_calculatorDisplay); + + m_decimalSeparator = LocalizationSettings::GetInstance().GetDecimalSeparator(); + + if (CoreWindow::GetForCurrentThread() != nullptr) + { + // Must have a CoreWindow to access the resource context. + m_isRtlLanguage = LocalizationService::GetInstance()->IsRtlLayout(); + } + + IsEditingEnabled = false; + IsUnaryOperatorEnabled = true; + IsBinaryOperatorEnabled = true; + IsOperandEnabled = true; + IsNegateEnabled = true; + IsDecimalEnabled = true; + AreHistoryShortcutsEnabled = true; + AreProgrammerRadixOperatorsEnabled = false; + + m_tokenPosition = -1; + m_isLastOperationHistoryLoad = false; +} + +String^ StandardCalculatorViewModel::LocalizeDisplayValue(_In_ wstring const &displayValue, _In_ bool isError) +{ + wstring result(displayValue); + + LocalizationSettings::GetInstance().LocalizeDisplayValue(&result); + + // WINBLUE: 440747 - In BiDi languages, error messages need to be wrapped in LRE/PDF + if (isError && m_isRtlLanguage) + { + result.insert(result.begin(), Utils::LRE); + result.push_back(Utils::PDF); + } + + return ref new Platform::String(result.c_str()); +} + +String^ StandardCalculatorViewModel::CalculateNarratorDisplayValue(_In_ wstring const &displayValue, _In_ String^ localizedDisplayValue, _In_ bool isError) +{ + String^ localizedValue = localizedDisplayValue; + String^ automationFormat = m_localizedCalculationResultAutomationFormat; + + // The narrator doesn't read the decimalSeparator if it's the last character + if (Utils::IsLastCharacterTarget(displayValue, m_decimalSeparator)) + { + // remove the decimal separator, to avoid a long pause between words + 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" + automationFormat = m_localizedCalculationResultDecimalAutomationFormat; + } + + // In Programmer modes using non-base10, we want the strings to be read as literal digits. + if (IsProgrammer && CurrentRadixType != RADIX_TYPE::DEC_RADIX) + { + localizedValue = GetNarratorStringReadRawNumbers(localizedValue); + } + + return GetLocalizedStringFormat(automationFormat, localizedValue); +} + +String^ StandardCalculatorViewModel::GetNarratorStringReadRawNumbers(_In_ String^ localizedDisplayValue) +{ + wstringstream wss; + RADIX_TYPE radix = static_cast(CurrentRadixType); + auto& locSettings = LocalizationSettings::GetInstance(); + + // Insert a space after each digit in the string, to force Narrator to read them as separate numbers. + wstring wstrValue(localizedDisplayValue->Data()); + for (wchar_t& c : wstrValue) + { + wss << c; + if (locSettings.IsLocalizedHexDigit(c)) + { + wss << L' '; + } + } + + return ref new String(wss.str().c_str()); +} + +void StandardCalculatorViewModel::SetPrimaryDisplay(_In_ wstring const &displayStringValue, _In_ bool isError) +{ + String^ localizedDisplayStringValue = LocalizeDisplayValue(displayStringValue, isError); + + // Set this variable before the DisplayValue is modified, Otherwise the DisplayValue will + // not match what the narrator is saying + m_CalculationResultAutomationName = CalculateNarratorDisplayValue(displayStringValue, localizedDisplayStringValue, isError); + + DisplayValue = localizedDisplayStringValue; + + IsInError = isError; + + if (IsProgrammer) + { + UpdateProgrammerPanelDisplay(); + } +} + +void StandardCalculatorViewModel::DisplayPasteError() +{ + m_standardCalculatorManager->DisplayPasteError(); +} + +void StandardCalculatorViewModel::SetParenthesisCount(_In_ const wstring& parenthesisCount) +{ + if (IsProgrammer || IsScientific) + { + OpenParenthesisCount = ref new String(parenthesisCount.c_str()); + RaisePropertyChanged("LeftParenthesisAutomationName"); + } +} + +void StandardCalculatorViewModel::DisableButtons(CommandType selectedExpressionCommandType) +{ + if (selectedExpressionCommandType == CommandType::OperandCommand) + { + IsBinaryOperatorEnabled = false; + IsUnaryOperatorEnabled = false; + IsOperandEnabled = true; + IsNegateEnabled = true; + IsDecimalEnabled = true; + } + if (selectedExpressionCommandType == CommandType::BinaryCommand) + { + IsBinaryOperatorEnabled = true; + IsUnaryOperatorEnabled = false; + IsOperandEnabled = false; + IsNegateEnabled = false; + IsDecimalEnabled = false; + } + if (selectedExpressionCommandType == CommandType::UnaryCommand) + { + IsBinaryOperatorEnabled = false; + IsUnaryOperatorEnabled = true; + IsOperandEnabled = false; + IsNegateEnabled = true; + IsDecimalEnabled = false; + } +} + +String ^ StandardCalculatorViewModel::GetLeftParenthesisAutomationName() +{ + String^ parenthesisCount = ((m_OpenParenthesisCount == nullptr) ? "0" : m_OpenParenthesisCount); + wstring localizedParenthesisCount = std::wstring(parenthesisCount->Data()); + LocalizationSettings::GetInstance().LocalizeDisplayValue(&localizedParenthesisCount); + + return GetLocalizedStringFormat(m_leftParenthesisAutomationFormat, ref new String(localizedParenthesisCount.c_str())); +} + +void StandardCalculatorViewModel::SetExpressionDisplay(_Inout_ shared_ptr>> const &tokens, _Inout_ shared_ptr>> const &commands) +{ + m_tokens = tokens; + m_commands = commands; + if (!IsEditingEnabled) + { + SetTokens(tokens); + } + + CalculationExpressionAutomationName = GetCalculatorExpressionAutomationName(); + + AreTokensUpdated = true; +} + +void StandardCalculatorViewModel::SetHistoryExpressionDisplay(_Inout_ shared_ptr>> const &tokens, _Inout_ shared_ptr>> const &commands) +{ + m_tokens = make_shared>>(*tokens); + m_commands = make_shared>>(*commands); + IsEditingEnabled = false; + + // Setting the History Item Load Mode so that UI does not get updated with recalculation of every token + m_standardCalculatorManager->SetInHistoryItemLoadMode(true); + Recalculate(true); + m_standardCalculatorManager->SetInHistoryItemLoadMode(false); + m_isLastOperationHistoryLoad = true; +} + +void StandardCalculatorViewModel::SetTokens(_Inout_ shared_ptr>> const &tokens) +{ + AreTokensUpdated = false; + + if (m_ExpressionTokens == nullptr) + { + m_ExpressionTokens = ref new Vector(); + } + else + { + m_ExpressionTokens->Clear(); + } + + unsigned int nTokens = 0; + tokens->GetSize(&nTokens); + pair currentToken; + const auto& localizer = LocalizationSettings::GetInstance(); + + for (unsigned int i = 0; i < nTokens; ++i) + { + if (SUCCEEDED(tokens->GetAt(i, ¤tToken))) + { + Common::TokenType type; + const wstring separator = L" "; + bool isEditable = (currentToken.second == -1) ? false : true; + localizer.LocalizeDisplayValue(&(currentToken.first)); + + if (!isEditable) + { + if (currentToken.first == separator) + { + type = TokenType::Separator; + } + else + { + type = TokenType::Operator; + } + } + + else + { + shared_ptr command; + IFTPlatformException(m_commands->GetAt(static_cast(currentToken.second), &command)); + + if (command->GetCommandType() == CommandType::OperandCommand) + { + type = TokenType::Operand; + } + else + { + type = TokenType::Operator; + } + } + DisplayExpressionToken^ expressionToken = ref new DisplayExpressionToken(ref new String(currentToken.first.c_str()), i, isEditable, type); + m_ExpressionTokens->Append(expressionToken); + } + } +} + +String^ StandardCalculatorViewModel::GetCalculatorExpressionAutomationName() +{ + String^ expression = L""; + for (auto&& token : m_ExpressionTokens) + { + expression += LocalizationService::GetNarratorReadableToken(token->Token); + } + + return GetLocalizedStringFormat(m_expressionAutomationNameFormat, expression); +} + +void StandardCalculatorViewModel::SetMemorizedNumbers(const vector& newMemorizedNumbers) +{ + const auto& localizer = LocalizationSettings::GetInstance(); + if (newMemorizedNumbers.size() == 0) // Memory has been cleared + { + MemorizedNumbers->Clear(); + IsMemoryEmpty = true; + } + // A new value is added to the memory + else if (newMemorizedNumbers.size() > MemorizedNumbers->Size) + { + while (newMemorizedNumbers.size() > MemorizedNumbers->Size) + { + size_t newValuePosition = newMemorizedNumbers.size() - MemorizedNumbers->Size - 1; + auto stringValue = newMemorizedNumbers.at(newValuePosition); + + MemoryItemViewModel^ memorySlot = ref new MemoryItemViewModel(this); + memorySlot->Position = 0; + localizer.LocalizeDisplayValue(&stringValue); + memorySlot->Value = Utils::LRO + ref new String(stringValue.c_str()) + Utils::PDF; + + MemorizedNumbers->InsertAt(0, memorySlot); + IsMemoryEmpty = false; + + // Update the slot position for the rest of the slots + for (unsigned int i = 1; i < MemorizedNumbers->Size; i++) + { + MemorizedNumbers->GetAt(i)->Position++; + } + } + } + else if (newMemorizedNumbers.size() == MemorizedNumbers->Size) // Either M+ or M- + { + for (unsigned int i = 0; i < MemorizedNumbers->Size; i++) + { + auto newStringValue = newMemorizedNumbers.at(i); + localizer.LocalizeDisplayValue(&newStringValue); + + // If the value is different, update the value + if (MemorizedNumbers->GetAt(i)->Value != StringReference(newStringValue.c_str())) + { + MemorizedNumbers->GetAt(i)->Value = Utils::LRO + ref new String(newStringValue.c_str()) + Utils::PDF; + } + + } + } +} + +void StandardCalculatorViewModel::FtoEButtonToggled() +{ + OnButtonPressed(NumbersAndOperatorsEnum::FToE); +} + +void StandardCalculatorViewModel::HandleUpdatedOperandData(Command cmdenum) +{ + DisplayExpressionToken^ displayExpressionToken = ExpressionTokens->GetAt(m_tokenPosition); + if (displayExpressionToken == nullptr) + { + return; + } + if ((displayExpressionToken->Token == nullptr) || (displayExpressionToken->Token->Length() == 0)) + { + displayExpressionToken->CommandIndex = 0; + } + + wchar_t ch; + if ((cmdenum >= Command::Command0) && (cmdenum <= Command::Command9)) + { + switch (cmdenum) + { + case Command::Command0: + ch = '0'; + break; + case Command::Command1: + ch = '1'; + break; + case Command::Command2: + ch = '2'; + break; + case Command::Command3: + ch = '3'; + break; + case Command::Command4: + ch = '4'; + break; + case Command::Command5: + ch = '5'; + break; + case Command::Command6: + ch = '6'; + break; + case Command::Command7: + ch = '7'; + break; + case Command::Command8: + ch = '8'; + break; + case Command::Command9: + ch = '9'; + break; + } + } + else if (cmdenum == Command::CommandPNT) + { + ch = '.'; + } + else if (cmdenum == Command::CommandBACK) + { + ch = 'x'; + } + else + { + return; + } + + int length = 0; + wchar_t* temp = new wchar_t[100]; + const wchar_t* data = m_selectedExpressionLastData->Data(); + int i = 0, j = 0; + int commandIndex = displayExpressionToken->CommandIndex; + + if (IsOperandTextCompletelySelected) + { + //Clear older text; + m_selectedExpressionLastData = L""; + if (ch == 'x') + { + temp = L'\0'; + commandIndex = 0; + } + else + { + temp[0] = ch; + temp[1] = L'\0'; + commandIndex = 1; + } + IsOperandTextCompletelySelected = false; + } + else + { + if (ch == 'x') + { + if (commandIndex == 0) + { + delete temp; + return; + } + + length = m_selectedExpressionLastData->Length(); + for (; j < length; ++j) + { + if (j == commandIndex - 1) + { + continue; + } + temp[i++] = data[j]; + } + temp[i] = '\0'; + commandIndex -= 1; + } + else + { + length = m_selectedExpressionLastData->Length() + 1; + if (length > 50) + { + return; + } + for (; i < length; ++i) + { + if (i == commandIndex) + { + temp[i] = ch; + continue; + } + temp[i] = data[j++]; + } + temp[i] = '\0'; + commandIndex += 1; + } + } + + String^ updatedData = ref new String(temp); + UpdateOperand(m_tokenPosition, updatedData); + displayExpressionToken->Token = updatedData; + IsOperandUpdatedUsingViewModel = true; + displayExpressionToken->CommandIndex = commandIndex; +} + +bool StandardCalculatorViewModel::IsOperator(Command cmdenum) +{ + if ((cmdenum == Command::Command0) || (cmdenum == Command::Command1) || (cmdenum == Command::Command2) || (cmdenum == Command::Command3) || (cmdenum == Command::Command4) || (cmdenum == Command::Command5) + || (cmdenum == Command::Command6) || (cmdenum == Command::Command7) || (cmdenum == Command::Command8) || (cmdenum == Command::Command9) || (cmdenum == Command::CommandPNT) || (cmdenum == Command::CommandBACK) + || (cmdenum == Command::CommandEXP) || (cmdenum == Command::CommandFE) || (cmdenum == Command::ModeBasic) || (cmdenum == Command::ModeBasic) || (cmdenum == Command::ModeProgrammer) || (cmdenum == Command::ModeScientific) + || (cmdenum == Command::CommandINV) || (cmdenum == Command::CommandCENTR) || (cmdenum == Command::CommandDEG) || (cmdenum == Command::CommandRAD) || (cmdenum == Command::CommandGRAD) + || ((cmdenum >= Command::CommandBINEDITSTART) && (cmdenum <= Command::CommandBINEDITEND))) + { + return false; + } + return true; +} + +void StandardCalculatorViewModel::OnButtonPressed(Object^ parameter) +{ + m_feedbackForButtonPress = CalculatorButtonPressedEventArgs::GetAuditoryFeedbackFromCommandParameter(parameter); + NumbersAndOperatorsEnum numOpEnum = CalculatorButtonPressedEventArgs::GetOperationFromCommandParameter(parameter); + Command cmdenum = ConvertToOperatorsEnum(numOpEnum); + bool isOperator = IsOperator(cmdenum); + + TraceLogger::GetInstance().UpdateFunctionUsage((int)numOpEnum); + + if (IsInError) + { + m_standardCalculatorManager->SendCommand(Command::CommandCLEAR); + + if (!IsRecoverableCommand((int)numOpEnum)) + { + return; + } + } + + if (IsEditingEnabled && + numOpEnum != NumbersAndOperatorsEnum::IsScientificMode && + numOpEnum != NumbersAndOperatorsEnum::IsStandardMode && + numOpEnum != NumbersAndOperatorsEnum::IsProgrammerMode && + numOpEnum != NumbersAndOperatorsEnum::FToE && + (numOpEnum != NumbersAndOperatorsEnum::Degree) && (numOpEnum != NumbersAndOperatorsEnum::Radians) && (numOpEnum != NumbersAndOperatorsEnum::Grads)) + { + if (!m_keyPressed) + { + SaveEditedCommand(m_selectedExpressionToken->TokenPosition, cmdenum); + } + } + else + { + if (numOpEnum == NumbersAndOperatorsEnum::IsStandardMode || + numOpEnum == NumbersAndOperatorsEnum::IsScientificMode || + numOpEnum == NumbersAndOperatorsEnum::IsProgrammerMode) + { + IsEditingEnabled = false; + } + if (numOpEnum == NumbersAndOperatorsEnum::Memory) + { + OnMemoryButtonPressed(); + } + else + { + if (numOpEnum == NumbersAndOperatorsEnum::Clear || + numOpEnum == NumbersAndOperatorsEnum::ClearEntry || + numOpEnum == NumbersAndOperatorsEnum::IsStandardMode || + numOpEnum == NumbersAndOperatorsEnum::IsProgrammerMode) + { + // On Clear('C') the F-E button needs to be UnChecked if it in Checked state. + // Also, the Primary Display Value should not show in exponential format. + // Hence the check below to ensure parity with Desktop Calculator. + // Clear the FE mode if the switching to StandardMode, since 'C'/'CE' in StandardMode + // doesn't honour the FE button. + if (IsFToEChecked) + { + IsFToEChecked = false; + } + } + if (numOpEnum == NumbersAndOperatorsEnum::Degree || numOpEnum == NumbersAndOperatorsEnum::Radians || numOpEnum == NumbersAndOperatorsEnum::Grads) + { + m_CurrentAngleType = numOpEnum; + } + if ((cmdenum == Command::Command0) || (cmdenum == Command::Command1) || (cmdenum == Command::Command2) || (cmdenum == Command::Command3) || (cmdenum == Command::Command4) || (cmdenum == Command::Command5) + || (cmdenum == Command::Command6) || (cmdenum == Command::Command7) || (cmdenum == Command::Command8) || (cmdenum == Command::Command9) || (cmdenum == Command::CommandPNT) || (cmdenum == Command::CommandBACK) || (cmdenum == Command::CommandEXP)) + { + IsOperatorCommand = false; + } + else + { + IsOperatorCommand = true; + } + + if (m_isLastOperationHistoryLoad && + ((numOpEnum != NumbersAndOperatorsEnum::Degree) && (numOpEnum != NumbersAndOperatorsEnum::Radians) && (numOpEnum != NumbersAndOperatorsEnum::Grads))) + { + IsFToEEnabled = true; + m_isLastOperationHistoryLoad = false; + } + + m_standardCalculatorManager->SendCommand(cmdenum); + } + } +} + +int StandardCalculatorViewModel::GetBitLengthType() +{ + if (IsQwordEnabled) + { + return QwordType; + } + else if (IsDwordEnabled) + { + return DwordType; + } + else if (IsWordEnabled) + { + return WordType; + } + else + { + return ByteType; + } +} + +int StandardCalculatorViewModel::GetNumberBase() +{ + if (CurrentRadixType == HEX_RADIX) + { + return HexBase; + } + else if (CurrentRadixType == DEC_RADIX) + { + return DecBase; + } + else if (CurrentRadixType == OCT_RADIX) + { + return OctBase; + } + else + { + return BinBase; + } + +} + +void StandardCalculatorViewModel::OnCopyCommand(Object^ parameter) +{ + CopyPasteManager::CopyToClipboard(GetRawDisplayValue()); + + String^ announcement = AppResourceProvider::GetInstance().GetResourceString(CalculatorResourceKeys::DisplayCopied); + Announcement = CalculatorAnnouncement::GetDisplayCopiedAnnouncement(announcement); +} + +void StandardCalculatorViewModel::OnPasteCommand(Object^ parameter) +{ + ViewMode mode; + int NumberBase = -1; + int bitLengthType = -1; + if (IsScientific) + { + mode = ViewMode::Scientific; + } + else if (IsProgrammer) + { + mode = ViewMode::Programmer; + NumberBase = GetNumberBase(); + bitLengthType = GetBitLengthType(); + } + else + { + mode = ViewMode::Standard; + } + // if there's nothing to copy early out + if (IsEditingEnabled || !CopyPasteManager::HasStringToPaste()) + { + return; + } + + // Ensure that the paste happens on the UI thread + CopyPasteManager::GetStringToPaste(mode, NavCategory::GetGroupType(mode), NumberBase, bitLengthType).then( + [this, mode](String^ pastedString) + { + OnPaste(pastedString, mode); + }, concurrency::task_continuation_context::use_current()); +} + +CalculationManager::Command StandardCalculatorViewModel::ConvertToOperatorsEnum(NumbersAndOperatorsEnum operation) +{ + return safe_cast(operation); +} + +void StandardCalculatorViewModel::OnPaste(String^ pastedString, ViewMode mode) +{ + // If pastedString is invalid("NoOp") then display pasteError else process the string + if (pastedString == StringReference(CopyPasteManager::PasteErrorString)) + { + this->DisplayPasteError(); + return; + } + + TraceLogger::GetInstance().LogValidInputPasted(mode); + bool isFirstLegalChar = true; + m_standardCalculatorManager->SendCommand(Command::CommandCENTR); + bool sendNegate = false; + bool processedExp = false; + bool processedDigit = false; + bool sentEquals = false; + bool isPreviousOperator = false; + + vector negateStack; + + // Iterate through each character pasted, and if it's valid, send it to the model. + auto it = pastedString->Begin(); + + while (it != pastedString->End()) + { + bool sendCommand = true; + bool canSendNegate = false; + + NumbersAndOperatorsEnum mappedNumOp = MapCharacterToButtonId(*it, canSendNegate); + + if (isFirstLegalChar || isPreviousOperator) + { + isFirstLegalChar = false; + isPreviousOperator = false; + + // If the character is a - sign, send negate + // after sending the next legal character. Send nothing now, or + // it will be ignored. + if (NumbersAndOperatorsEnum::Subtract == mappedNumOp) + { + sendNegate = true; + sendCommand = false; + } + + // Support (+) sign prefix + if (NumbersAndOperatorsEnum::Add == mappedNumOp) + { + sendCommand = false; + } + } + + if (mappedNumOp != NumbersAndOperatorsEnum::None) + { + switch (mappedNumOp) + { + // Opening parenthesis starts a new expression and pushes negation state onto the stack + case NumbersAndOperatorsEnum::OpenParenthesis: + negateStack.push_back(sendNegate); + sendNegate = false; + break; + + // Closing parenthesis pops the negation state off the stack and sends it down to the calc engine + case NumbersAndOperatorsEnum::CloseParenthesis: + if (!negateStack.empty()) + { + sendNegate = negateStack.back(); + negateStack.pop_back(); + canSendNegate = true; + } + else + { + // Don't send a closing parenthesis if a matching opening parenthesis hasn't been sent already + sendCommand = false; + } + break; + + case NumbersAndOperatorsEnum::Zero: + case NumbersAndOperatorsEnum::One: + case NumbersAndOperatorsEnum::Two: + case NumbersAndOperatorsEnum::Three: + case NumbersAndOperatorsEnum::Four: + case NumbersAndOperatorsEnum::Five: + case NumbersAndOperatorsEnum::Six: + case NumbersAndOperatorsEnum::Seven: + case NumbersAndOperatorsEnum::Eight: + case NumbersAndOperatorsEnum::Nine: + processedDigit = true; + break; + + case NumbersAndOperatorsEnum::Add: + case NumbersAndOperatorsEnum::Subtract: + case NumbersAndOperatorsEnum::Multiply: + case NumbersAndOperatorsEnum::Divide: + isPreviousOperator = true; + break; + } + + if (sendCommand) + { + sentEquals = (mappedNumOp == NumbersAndOperatorsEnum::Equals); + Command cmdenum = ConvertToOperatorsEnum(mappedNumOp); + m_standardCalculatorManager->SendCommand(cmdenum); + + // The CalcEngine state machine won't allow the negate command to be sent before any + // other digits, so instead a flag is set and the command is sent after the first appropriate + // command. + if (sendNegate) + { + if (canSendNegate) + { + Command cmdNegate = ConvertToOperatorsEnum(NumbersAndOperatorsEnum::Negate); + m_standardCalculatorManager->SendCommand(cmdNegate); + } + + // Can't send negate on a leading zero, so wait until the appropriate time to send it. + if (NumbersAndOperatorsEnum::Zero != mappedNumOp && NumbersAndOperatorsEnum::Decimal != mappedNumOp) + { + sendNegate = false; + } + } + } + } + + // Handle exponent and exponent sign (...e+... or ...e-...) + if (mappedNumOp == NumbersAndOperatorsEnum::Exp) + { + ++it; + if (!(MapCharacterToButtonId(*it, canSendNegate) == NumbersAndOperatorsEnum::Add)) + { + Command cmdNegate = ConvertToOperatorsEnum(NumbersAndOperatorsEnum::Negate); + m_standardCalculatorManager->SendCommand(cmdNegate); + } + } + + ++it; + } +} + +void StandardCalculatorViewModel::OnClearMemoryCommand( + Object^ parameter) +{ + m_standardCalculatorManager->MemorizedNumberClearAll(); + + int windowId = Utils::GetWindowId(); + TraceLogger::GetInstance().LogMemoryClearAll(windowId); + + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement(CalculatorResourceKeys::MemoryCleared, m_localizedMemoryCleared); + Announcement = CalculatorAnnouncement::GetMemoryClearedAnnouncement(announcement); +} + +void StandardCalculatorViewModel::OnPinUnpinCommand(Object^ parameter) +{ + SetViewPinnedState(!IsViewPinned()); +} + +bool StandardCalculatorViewModel::IsViewPinned() +{ + return m_IsCurrentViewPinned; +} + +void StandardCalculatorViewModel::SetViewPinnedState( + bool pinned) +{ + IsCurrentViewPinned = pinned; +} + +NumbersAndOperatorsEnum StandardCalculatorViewModel::MapCharacterToButtonId( + const wchar_t ch, + bool& canSendNegate) +{ + 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 = NumbersAndOperatorsEnum::Zero + static_cast(ch - L'0'); + canSendNegate = true; + break; + + case '*': + mappedValue = NumbersAndOperatorsEnum::Multiply; + break; + + case '+': + mappedValue = NumbersAndOperatorsEnum::Add; + break; + + case '-': + mappedValue = NumbersAndOperatorsEnum::Subtract; + break; + + case '/': + mappedValue = NumbersAndOperatorsEnum::Divide; + break; + + case '=': + mappedValue = NumbersAndOperatorsEnum::Equals; + break; + + case '(': + mappedValue = NumbersAndOperatorsEnum::OpenParenthesis; + break; + + case ')': + mappedValue = NumbersAndOperatorsEnum::CloseParenthesis; + break; + + case 'a': + case 'A': + mappedValue = NumbersAndOperatorsEnum::A; + break; + case 'b': + case 'B': + mappedValue = NumbersAndOperatorsEnum::B; + break; + case 'c': + case 'C': + mappedValue = NumbersAndOperatorsEnum::C; + break; + case 'd': + case 'D': + mappedValue = NumbersAndOperatorsEnum::D; + break; + case 'e': + case 'E': + // Only allow scientific notation in scientific mode + if (IsProgrammer) + { + mappedValue = NumbersAndOperatorsEnum::E; + } + else + { + mappedValue = NumbersAndOperatorsEnum::Exp; + } + break; + case 'f': + case 'F': + mappedValue = NumbersAndOperatorsEnum::F; + break; + default: + // For the decimalSeparator, we need to respect the user setting. + if (ch == m_decimalSeparator) + { + mappedValue = NumbersAndOperatorsEnum::Decimal; + } + break; + } + + if (mappedValue == NumbersAndOperatorsEnum::None) + { + if (LocalizationSettings::GetInstance().IsLocalizedDigit(ch)) + { + mappedValue = NumbersAndOperatorsEnum::Zero + static_cast(ch - LocalizationSettings::GetInstance().GetDigitSymbolFromEnUsDigit('0')); + canSendNegate = true; + } + } + + // Negate cannot be sent for leading zeroes + if (NumbersAndOperatorsEnum::Zero == mappedValue) + { + canSendNegate = false; + } + + return mappedValue; +} + +void StandardCalculatorViewModel::OnMemoryButtonPressed() +{ + m_standardCalculatorManager->MemorizeNumber(); + + int windowId = Utils::GetWindowId(); + TraceLogger::GetInstance().InsertIntoMemoryMap(windowId, IsStandard, IsScientific, IsProgrammer); + + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement( + CalculatorResourceKeys::MemorySave, + m_localizedMemorySavedAutomationFormat, + m_DisplayValue->Data()); + + Announcement = CalculatorAnnouncement::GetMemoryItemAddedAnnouncement(announcement); +} + +void StandardCalculatorViewModel::OnMemoryItemChanged(unsigned int indexOfMemory) +{ + if (indexOfMemory < MemorizedNumbers->Size) + { + MemoryItemViewModel^ memSlot = MemorizedNumbers->GetAt(indexOfMemory); + String^ localizedValue = memSlot->Value; + + wstring localizedIndex = to_wstring(indexOfMemory + 1); + LocalizationSettings::GetInstance().LocalizeDisplayValue(&localizedIndex); + + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement( + CalculatorResourceKeys::MemoryItemChanged, + m_localizedMemoryItemChangedAutomationFormat, + localizedIndex.c_str(), + localizedValue->Data()); + + Announcement = CalculatorAnnouncement::GetMemoryItemChangedAnnouncement(announcement); + } +} + +void StandardCalculatorViewModel::OnMemoryItemPressed(Object^ memoryItemPosition) +{ + if (MemorizedNumbers && MemorizedNumbers->Size > 0) + { + auto boxedPosition = safe_cast^>(memoryItemPosition); + m_standardCalculatorManager->MemorizedNumberLoad(boxedPosition->Value); + HideMemoryClicked(); + int windowId = Utils::GetWindowId(); + TraceLogger::GetInstance().LogMemoryUsed(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer, MemorizedNumbers->Size); + } +} + +void StandardCalculatorViewModel::OnMemoryAdd(Object^ memoryItemPosition) +{ + // M+ will add display to memorylist if memory list is empty. + int windowId = Utils::GetWindowId(); + + if (MemorizedNumbers) + { + auto boxedPosition = safe_cast^>(memoryItemPosition); + if (MemorizedNumbers->Size > 0) + { + TraceLogger::GetInstance().LogMemoryUsed(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer, MemorizedNumbers->Size); + TraceLogger::GetInstance().UpdateMemoryMap(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer); + } + else + { + TraceLogger::GetInstance().InsertIntoMemoryMap(windowId, IsStandard, IsScientific, IsProgrammer); + } + m_standardCalculatorManager->MemorizedNumberAdd(boxedPosition->Value); + } +} + +void StandardCalculatorViewModel::OnMemorySubtract(Object^ memoryItemPosition) +{ + int windowId = Utils::GetWindowId(); + + // M- will add negative of displayed number to memorylist if memory list is empty. + if (MemorizedNumbers) + { + auto boxedPosition = safe_cast^>(memoryItemPosition); + if (MemorizedNumbers->Size > 0) + { + TraceLogger::GetInstance().LogMemoryUsed(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer, MemorizedNumbers->Size); + TraceLogger::GetInstance().UpdateMemoryMap(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer); + } + else + { + TraceLogger::GetInstance().InsertIntoMemoryMap(windowId, IsStandard, IsScientific, IsProgrammer); + } + m_standardCalculatorManager->MemorizedNumberSubtract(boxedPosition->Value); + } +} + +void StandardCalculatorViewModel::OnMemoryClear(_In_ Object^ memoryItemPosition) +{ + if (MemorizedNumbers && MemorizedNumbers->Size > 0) + { + int windowId = Utils::GetWindowId(); + auto boxedPosition = safe_cast^>(memoryItemPosition); + + if (boxedPosition->Value >= 0) + { + unsigned int unsignedPosition = safe_cast(boxedPosition->Value); + m_standardCalculatorManager->MemorizedNumberClear(unsignedPosition); + + MemorizedNumbers->RemoveAt(unsignedPosition); + for (unsigned int i = 0; i < MemorizedNumbers->Size; i++) + { + MemorizedNumbers->GetAt(i)->Position = i; + } + + if (MemorizedNumbers->Size == 0) + { + IsMemoryEmpty = true; + } + + TraceLogger::GetInstance().LogMemoryUsed(windowId, boxedPosition->Value, IsStandard, IsScientific, IsProgrammer, MemorizedNumbers->Size); + TraceLogger::GetInstance().DeleteFromMemoryMap(windowId, boxedPosition->Value); + + wstring localizedIndex = to_wstring(boxedPosition->Value + 1); + LocalizationSettings::GetInstance().LocalizeDisplayValue(&localizedIndex); + + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement( + CalculatorResourceKeys::MemoryItemCleared, + m_localizedMemoryItemClearedAutomationFormat, + localizedIndex.c_str()); + + Announcement = CalculatorAnnouncement::GetMemoryClearedAnnouncement(announcement); + } + } +} + +Array^ StandardCalculatorViewModel::Serialize() +{ + DataWriter^ writer = ref new DataWriter(); + writer->WriteUInt32(static_cast(m_CurrentAngleType)); + writer->WriteBoolean(IsFToEChecked); + writer->WriteBoolean(IsCurrentViewPinned); + writer->WriteUInt32(static_cast(m_standardCalculatorManager->SerializeSavedDegreeMode())); + + // Serialize Memory + vector serializedMemory; + serializedMemory = m_standardCalculatorManager->GetSerializedMemory(); + size_t lengthOfSerializedMemory = serializedMemory.size(); + writer->WriteUInt32(static_cast(lengthOfSerializedMemory)); + for (auto data : serializedMemory) + { + writer->WriteInt32(data); + } + + // Serialize Primary Display + vector serializedPrimaryDisplay = m_standardCalculatorManager->GetSerializedPrimaryDisplay(); + writer->WriteUInt32(static_cast(serializedPrimaryDisplay.size())); + for (auto data : serializedPrimaryDisplay) + { + writer->WriteInt32(data); + } + + //For ProgrammerMode + writer->WriteUInt32(static_cast(CurrentRadixType)); + + //Serialize commands of calculator manager + vector serializedCommand = m_standardCalculatorManager->SerializeCommands(); + writer->WriteUInt32(static_cast(serializedCommand.size())); + writer->WriteBytes(ref new Array(serializedCommand.data(), static_cast(serializedCommand.size()))); + + if (IsInError) + { + Utils::SerializeCommandsAndTokens(m_tokens, m_commands, writer); + } + + //Convert viewmodel data in writer to bytes + IBuffer^ buffer = writer->DetachBuffer(); + DataReader^ reader = DataReader::FromBuffer(buffer); + Platform::Array^ viewModelDataAsBytes = ref new Array(buffer->Length); + reader->ReadBytes(viewModelDataAsBytes); + + // Return byte array + return viewModelDataAsBytes; +} + +void StandardCalculatorViewModel::Deserialize(Array^ state) +{ + // Read byte array into a buffer + DataWriter^ writer = ref new DataWriter(); + writer->WriteBytes(state); + IBuffer^ buffer = writer->DetachBuffer(); + + // Read view model data + if (buffer->Length != 0) + { + DataReader^ reader = DataReader::FromBuffer(buffer); + m_CurrentAngleType = ConvertIntegerToNumbersAndOperatorsEnum(reader->ReadUInt32()); + + IsFToEChecked = reader->ReadBoolean(); + IsCurrentViewPinned = reader->ReadBoolean(); + Command serializedDegreeMode = static_cast(reader->ReadUInt32()); + + m_standardCalculatorManager->SendCommand(serializedDegreeMode); + + // Deserialize Memory + UINT32 memoryDataLength = reader->ReadUInt32(); + vector serializedMemory; + for (unsigned int i = 0; i < memoryDataLength; i++) + { + serializedMemory.push_back(reader->ReadInt32()); + } + m_standardCalculatorManager->DeSerializeMemory(serializedMemory); + + // Serialize Primary Display + UINT32 serializedPrimaryDisplayLength = reader->ReadUInt32(); + vector serializedPrimaryDisplay; + for (unsigned int i = 0; i < serializedPrimaryDisplayLength; i++) + { + serializedPrimaryDisplay.push_back(reader->ReadInt32()); + } + m_standardCalculatorManager->DeSerializePrimaryDisplay(serializedPrimaryDisplay); + + CurrentRadixType = reader->ReadUInt32(); + //Read command data and Deserialize + UINT32 modeldatalength = reader->ReadUInt32(); + Array^ modelDataAsBytes = ref new Array(modeldatalength); + reader->ReadBytes(modelDataAsBytes); + m_standardCalculatorManager->DeSerializeCommands(vector(modelDataAsBytes->begin(), modelDataAsBytes->end())); + + // After recalculation. If there is an error then + // IsInError should be set synchronously. + if (IsInError) + { + shared_ptr>> commandVector = Utils::DeserializeCommands(reader); + shared_ptr>> tokenVector = Utils::DeserializeTokens(reader); + SetExpressionDisplay(tokenVector, commandVector); + } + } +} + +void StandardCalculatorViewModel::OnPropertyChanged(String^ propertyname) +{ + if (propertyname == CalculatorViewModelProperties::IsScientific) + { + if (IsScientific) + { + OnButtonPressed(NumbersAndOperatorsEnum::IsScientificMode); + } + } + else if (propertyname == CalculatorViewModelProperties::IsProgrammer) + { + if (IsProgrammer) + { + OnButtonPressed(NumbersAndOperatorsEnum::IsProgrammerMode); + } + } + else if (propertyname == CalculatorViewModelProperties::IsStandard) + { + if (IsStandard) + { + OnButtonPressed(NumbersAndOperatorsEnum::IsStandardMode); + } + } + else if (propertyname == CalculatorViewModelProperties::DisplayValue) + { + RaisePropertyChanged(CalculationResultAutomationName_PropertyName); + Announcement = GetDisplayUpdatedNarratorAnnouncement(); + } +} + +void StandardCalculatorViewModel::SetCalculatorType(ViewMode targetState) +{ + // Reset error state so that commands caused by the mode change are still + // sent if calc is currently in error state. + IsInError = false; + + // Setting one of these properties to true will set the others to false. + switch (targetState) + { + case ViewMode::Standard: + IsStandard = true; + ResetDisplay(); + SetPrecision(StandardModePrecision); + UpdateMaxIntDigits(); + break; + + case ViewMode::Scientific: + IsScientific = true; + ResetDisplay(); + SetPrecision(ScientificModePrecision); + break; + + case ViewMode::Programmer: + IsProgrammer = true; + ResetDisplay(); + SetPrecision(ProgrammerModePrecision); + break; + } +} + +Platform::String^ StandardCalculatorViewModel::GetRawDisplayValue() +{ + wstring rawValue; + + LocalizationSettings::GetInstance().RemoveGroupSeparators(DisplayValue->Data(), DisplayValue->Length(), &rawValue); + + return ref new Platform::String(rawValue.c_str()); +} + +// Given a format string, returns a string with the input display value inserted. +// 'format' is a localized string containing a %1 formatting mark where the display value should be inserted. +// 'displayValue' is a localized string containing a numerical value to be displayed to the user. +String^ StandardCalculatorViewModel::GetLocalizedStringFormat(String^ format, String^ displayValue) +{ + String^ localizedString = ref new String(LocalizationStringUtil::GetLocalizedString(format->Data(), displayValue->Data()).c_str()); + return localizedString; +} + + +void StandardCalculatorViewModel::ResetDisplay() +{ + AreHEXButtonsEnabled = false; + CurrentRadixType = (int)RADIX_TYPE::DEC_RADIX; + m_standardCalculatorManager->SetRadix(DEC_RADIX); + ProgModeRadixChange(); +} + +void StandardCalculatorViewModel::SetPrecision(int32_t precision) +{ + m_standardCalculatorManager->SetPrecision(precision); +} + +void StandardCalculatorViewModel::SwitchProgrammerModeBase(RADIX_TYPE radixType) +{ + if (IsInError) + { + m_standardCalculatorManager->SendCommand(Command::CommandCLEAR); + } + + AreHEXButtonsEnabled = (radixType == RADIX_TYPE::HEX_RADIX); + CurrentRadixType = (int)radixType; + m_standardCalculatorManager->SetRadix(radixType); + ProgModeRadixChange(); +} + +void StandardCalculatorViewModel::SetMemorizedNumbersString() +{ + m_standardCalculatorManager->SetMemorizedNumbersString(); +} + +ANGLE_TYPE GetAngleTypeFromCommand(Command command) +{ + switch (command) + { + case Command::CommandDEG: + return ANGLE_DEG; + case Command::CommandRAD: + return ANGLE_RAD; + case Command::CommandGRAD: + return ANGLE_GRAD; + default: + throw ref new Exception(E_FAIL, L"Invalid command type"); + } +} + +void StandardCalculatorViewModel::SaveEditedCommand(_In_ unsigned int tokenPosition, _In_ Command command) +{ + pair token; + bool fNegative = false; + bool handleOperand = false; + int nOpCode = static_cast(command); + wstring updatedToken = L""; + + shared_ptr tokenCommand; + IFTPlatformException(m_tokens->GetAt(tokenPosition, &token)); + + unsigned int tokenCommandIndex = token.second; + IFTPlatformException(m_commands->GetAt(tokenCommandIndex, &tokenCommand)); + + if (IsUnaryOp(nOpCode) && command != Command::CommandSIGN) + { + int angleCmd = static_cast(m_standardCalculatorManager->GetCurrentDegreeMode()); + ANGLE_TYPE angleType = GetAngleTypeFromCommand(static_cast(angleCmd)); + + if (IsTrigOp(nOpCode)) + { + shared_ptr spUnaryCommand = dynamic_pointer_cast(tokenCommand); + spUnaryCommand->SetCommands(angleCmd, nOpCode); + } + else + { + shared_ptr spUnaryCommand = dynamic_pointer_cast(tokenCommand); + spUnaryCommand->SetCommand(nOpCode); + } + + switch (nOpCode) + { + case static_cast(Command::CommandASIN) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandSIN), true, angleType); + break; + case static_cast(Command::CommandACOS) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandCOS), true, angleType); + break; + case static_cast(Command::CommandATAN) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandTAN), true, angleType); + break; + case static_cast(Command::CommandASINH) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandSINH), true, angleType); + break; + case static_cast(Command::CommandACOSH) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandCOSH), true, angleType); + break; + case static_cast(Command::CommandATANH) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandTANH), true, angleType); + break; + case static_cast(Command::CommandPOWE) : + updatedToken = CCalcEngine::OpCodeToUnaryString(static_cast(Command::CommandLN), true, angleType); + break; + default: + updatedToken = CCalcEngine::OpCodeToUnaryString(nOpCode, false, angleType); + } + if ((token.first.length() > 0) && (token.first[token.first.length() - 1] == L'(')) + { + wstring chOpenBrace = L"("; + updatedToken.append(chOpenBrace); + } + } + else if (IsBinOp(nOpCode)) + { + shared_ptr spBinaryCommand = dynamic_pointer_cast(tokenCommand); + spBinaryCommand->SetCommand(nOpCode); + updatedToken = CCalcEngine::OpCodeToString(nOpCode); + } + else if (IsOpnd(nOpCode) || command == Command::CommandBACK) + { + HandleUpdatedOperandData(command); + handleOperand = true; + } + else if (command == Command::CommandSIGN) + { + if (tokenCommand->GetCommandType() == CommandType::UnaryCommand) + { + shared_ptr spSignCommand = make_shared(nOpCode); + IFTPlatformException(m_commands->InsertAt(tokenCommandIndex + 1, spSignCommand)); + } + else + { + shared_ptr spOpndCommand = dynamic_pointer_cast(tokenCommand); + spOpndCommand->ToggleSign(); + updatedToken = spOpndCommand->GetToken(m_standardCalculatorManager->DecimalSeparator()); + } + IsOperandUpdatedUsingViewModel = true; + } + + if (!handleOperand) + { + IFTPlatformException(m_commands->SetAt(tokenCommandIndex, tokenCommand)); + + pair < wstring, int> selectedToken; + IFTPlatformException(m_tokens->GetAt(tokenPosition, &selectedToken)); + selectedToken.first = updatedToken; + IFTPlatformException(m_tokens->SetAt(tokenPosition, selectedToken)); + + DisplayExpressionToken^ displayExpressionToken = ExpressionTokens->GetAt(tokenPosition); + displayExpressionToken->Token = ref new Platform::String(updatedToken.c_str()); + + // Special casing + if (command == Command::CommandSIGN && tokenCommand->GetCommandType() == CommandType::UnaryCommand) + { + IsEditingEnabled = false; + Recalculate(); + } + } +} + +void StandardCalculatorViewModel::Recalculate(bool fromHistory) +{ + // Recalculate + Command currentDegreeMode = m_standardCalculatorManager->GetCurrentDegreeMode(); + shared_ptr >> savedCommands = make_shared >>(); + + vector currentCommands; + unsigned int commandListCount; + m_commands->GetSize(&commandListCount); + for (unsigned int i = 0; i < commandListCount; i++) + { + shared_ptr command; + IFTPlatformException(m_commands->GetAt(i, &command)); + + savedCommands->Append(command); + CommandType commandType = command->GetCommandType(); + + if (commandType == CommandType::UnaryCommand) + { + shared_ptr spCommand = dynamic_pointer_cast(command); + shared_ptr> unaryCommands = spCommand->GetCommands(); + unsigned int unaryCommandCount; + unaryCommands->GetSize(&unaryCommandCount); + + int nUCode; + for (unsigned int j = 0; j < unaryCommandCount; ++j) + { + IFTPlatformException(unaryCommands->GetAt(j, &nUCode)); + currentCommands.push_back(nUCode); + } + } + + if (commandType == CommandType::BinaryCommand) + { + shared_ptr spCommand = dynamic_pointer_cast(command); + currentCommands.push_back(spCommand->GetCommand()); + } + + if (commandType == CommandType::Parentheses) + { + shared_ptr spCommand = dynamic_pointer_cast(command); + currentCommands.push_back(spCommand->GetCommand()); + } + + if (commandType == CommandType::OperandCommand) + { + shared_ptr spCommand = dynamic_pointer_cast(command); + shared_ptr> opndCommands = spCommand->GetCommands(); + unsigned int opndCommandCount; + opndCommands->GetSize(&opndCommandCount); + bool fNeedIDCSign = spCommand->IsNegative(); + + int nOCode; + for (unsigned int j = 0; j < opndCommandCount; ++j) + { + IFTPlatformException(opndCommands->GetAt(j, &nOCode)); + currentCommands.push_back(nOCode); + + if (fNeedIDCSign && nOCode != IDC_0) + { + currentCommands.push_back(static_cast(CalculationManager::Command::CommandSIGN)); + fNeedIDCSign = false; + } + } + } + } + shared_ptr>> savedTokens = make_shared>>(); + + unsigned int tokenCount; + IFTPlatformException(m_tokens->GetSize(&tokenCount)); + + for (unsigned int i = 0; i < tokenCount; ++i) + { + pair currentToken; + IFTPlatformException(m_tokens->GetAt(i, ¤tToken)); + savedTokens->Append(currentToken); + } + + m_standardCalculatorManager->Reset(false); + if (IsScientific) + { + m_standardCalculatorManager->SendCommand(Command::ModeScientific); + } + + if (IsFToEChecked) + { + m_standardCalculatorManager->SendCommand(Command::CommandFE); + } + + m_standardCalculatorManager->SendCommand(currentDegreeMode); + size_t currentCommandsSize = currentCommands.size(); + for (size_t i = 0; i < currentCommandsSize; i++) + { + m_standardCalculatorManager->SendCommand(static_cast(currentCommands[i])); + } + + if (fromHistory) // This is for the cases where the expression is loaded from history + { + // To maintain F-E state of the engine, as the last operand hasn't reached engine by now + m_standardCalculatorManager->SendCommand(Command::CommandFE); + m_standardCalculatorManager->SendCommand(Command::CommandFE); + } + + // After recalculation. If there is an error then + // IsInError should be set synchronously. + if (IsInError) + { + SetExpressionDisplay(savedTokens, savedCommands); + } +} + +CommandType StandardCalculatorViewModel::GetSelectedTokenType(_In_ unsigned int tokenPosition) +{ + pairtoken; + shared_ptr tokenCommand; + IFTPlatformException(m_tokens->GetAt(tokenPosition, &token)); + + unsigned int tokenCommandIndex = token.second; + IFTPlatformException(m_commands->GetAt(tokenCommandIndex, &tokenCommand)); + + return tokenCommand->GetCommandType(); +} + +bool StandardCalculatorViewModel::IsOpnd(int nOpCode) +{ + + static Command opnd[] = { + Command::Command0, + Command::Command1, + Command::Command2, + Command::Command3, + Command::Command4, + Command::Command5, + Command::Command6, + Command::Command7, + Command::Command8, + Command::Command9, + Command::CommandPNT + }; + + for (int i = 0; i < ARRAYSIZE(opnd); i++) + { + if (nOpCode == static_cast(opnd[i])) + { + return true; + } + } + return false; +} + +bool StandardCalculatorViewModel::IsUnaryOp(int nOpCode) +{ + static Command unaryOp[] = { + Command::CommandSQRT, + Command::CommandFAC, + Command::CommandSQR, + Command::CommandLOG, + Command::CommandPOW10, + Command::CommandPOWE, + Command::CommandLN, + Command::CommandREC, + Command::CommandSIGN, + Command::CommandSINH, + Command::CommandASINH, + Command::CommandCOSH, + Command::CommandACOSH, + Command::CommandTANH, + Command::CommandATANH, + Command::CommandCUB + }; + + for (int i = 0; i < ARRAYSIZE(unaryOp); i++) + { + if (nOpCode == static_cast(unaryOp[i])) + { + return true; + } + } + + if (IsTrigOp(nOpCode)) + { + return true; + } + + return false; +} + +bool StandardCalculatorViewModel::IsTrigOp(int nOpCode) +{ + static Command trigOp[] = { + Command::CommandSIN, + Command::CommandCOS, + Command::CommandTAN, + Command::CommandASIN, + Command::CommandACOS, + Command::CommandATAN + }; + + for (int i = 0; i < ARRAYSIZE(trigOp); i++) + { + if (nOpCode == static_cast(trigOp[i])) + { + return true; + } + } + return false; +} + +bool StandardCalculatorViewModel::IsBinOp(int nOpCode) +{ + static Command binOp[] = { + Command::CommandADD, + Command::CommandSUB, + Command::CommandMUL, + Command::CommandDIV, + Command::CommandEXP, + Command::CommandROOT, + Command::CommandMOD, + Command::CommandPWR + }; + + for (int i = 0; i < ARRAYSIZE(binOp); i++) + { + if (nOpCode == static_cast(binOp[i])) + { + return true; + } + } + return false; +} + +bool StandardCalculatorViewModel::IsRecoverableCommand(int nOpCode) +{ + if (IsOpnd(nOpCode)) + { + return true; + } + + // Programmer mode, bit flipping + int minBinPos = static_cast(Command::CommandBINEDITSTART); + int maxBinPos = static_cast(Command::CommandBINEDITEND); + if (minBinPos <= nOpCode && nOpCode <= maxBinPos) + { + return true; + } + + static Command recoverableCommands[] = { + Command::CommandA, + Command::CommandB, + Command::CommandC, + Command::CommandD, + Command::CommandE, + Command::CommandF + }; + + for (int i = 0; i < ARRAYSIZE(recoverableCommands); i++) + { + if (nOpCode == static_cast(recoverableCommands[i])) + { + return true; + } + } + return false; +} + +size_t StandardCalculatorViewModel::LengthWithoutPadding(wstring str) +{ + size_t count = 0; + for (size_t i = 0; i < str.length(); i++) + { + if (str[i] != L' ') + { + count++; + } + } + return count; +} + +wstring StandardCalculatorViewModel::AddPadding(wstring binaryString) +{ + if (LocalizationSettings::GetInstance().GetEnglishValueFromLocalizedDigits(binaryString) == L"0") + { + return binaryString; + } + size_t pad = 4 - LengthWithoutPadding(binaryString) % 4; + if (pad == 4) + { + pad = 0; + } + wstring padString = L""; + for (size_t i = 0; i < pad; i++) + { + padString += L"0"; + } + return padString + binaryString; +} + +void StandardCalculatorViewModel::UpdateProgrammerPanelDisplay() +{ + wstring hexDisplayString; + wstring decimalDisplayString; + wstring octalDisplayString; + wstring binaryDisplayString; + if (!IsInError) + { + // we want the precision to be set to maximum value so that the autoconversions result as desired + int32_t precision = 64; + if (m_standardCalculatorManager->GetResultForRadix(16, precision) == L"") + { + hexDisplayString = DisplayValue->Data(); + decimalDisplayString = DisplayValue->Data(); + octalDisplayString = DisplayValue->Data(); + binaryDisplayString = DisplayValue->Data(); + } + else + { + hexDisplayString = m_standardCalculatorManager->GetResultForRadix(16, precision); + decimalDisplayString = m_standardCalculatorManager->GetResultForRadix(10, precision); + octalDisplayString = m_standardCalculatorManager->GetResultForRadix(8, precision); + binaryDisplayString = m_standardCalculatorManager->GetResultForRadix(2, precision); + } + } + const auto& localizer = LocalizationSettings::GetInstance(); + binaryDisplayString = AddPadding(binaryDisplayString); + + localizer.LocalizeDisplayValue(&hexDisplayString); + localizer.LocalizeDisplayValue(&decimalDisplayString); + localizer.LocalizeDisplayValue(&octalDisplayString); + localizer.LocalizeDisplayValue(&binaryDisplayString); + + HexDisplayValue = Utils::LRO + ref new Platform::String(hexDisplayString.c_str()) + Utils::PDF; + DecimalDisplayValue = Utils::LRO + ref new Platform::String(decimalDisplayString.c_str()) + Utils::PDF; + OctalDisplayValue = Utils::LRO + ref new Platform::String(octalDisplayString.c_str()) + Utils::PDF; + BinaryDisplayValue = Utils::LRO + ref new Platform::String(binaryDisplayString.c_str()) + Utils::PDF; + HexDisplayValue_AutomationName = GetLocalizedStringFormat(m_localizedHexaDecimalAutomationFormat, GetNarratorStringReadRawNumbers(HexDisplayValue)); + DecDisplayValue_AutomationName = GetLocalizedStringFormat(m_localizedDecimalAutomationFormat, DecimalDisplayValue); + OctDisplayValue_AutomationName = GetLocalizedStringFormat(m_localizedOctalAutomationFormat, GetNarratorStringReadRawNumbers(OctalDisplayValue)); + BinDisplayValue_AutomationName = GetLocalizedStringFormat(m_localizedBinaryAutomationFormat, GetNarratorStringReadRawNumbers(BinaryDisplayValue)); +} + +void StandardCalculatorViewModel::SwitchAngleType(NumbersAndOperatorsEnum num) +{ + OnButtonPressed(num); +} + +NumbersAndOperatorsEnum StandardCalculatorViewModel::ConvertIntegerToNumbersAndOperatorsEnum(unsigned int parameter) +{ + NumbersAndOperatorsEnum angletype; + switch (parameter) + { + case 321: + angletype = NumbersAndOperatorsEnum::Degree; + break; + case 322: + angletype = NumbersAndOperatorsEnum::Radians; + break; + case 323: + angletype = NumbersAndOperatorsEnum::Grads; + break; + default: + angletype = NumbersAndOperatorsEnum::Degree; + }; + return angletype; +} + +void StandardCalculatorViewModel::UpdateOperand(int pos, String^ text) +{ + pair p; + m_tokens->GetAt(pos, &p); + + String^ englishString = LocalizationSettings::GetInstance().GetEnglishValueFromLocalizedDigits(text->Data()); + p.first = englishString->Data(); + + int commandPos = p.second; + shared_ptr exprCmd; + m_commands->GetAt(commandPos, &exprCmd); + auto operandCommand = std::dynamic_pointer_cast(exprCmd); + + if (operandCommand != nullptr) + { + shared_ptr> commands = make_shared>(); + size_t length = p.first.length(); + if (length > 0) + { + int num = 0; + for (unsigned int i = 0; i < length; ++i) + { + if (p.first[i] == L'.') + { + num = static_cast(Command::CommandPNT); + } + else if (p.first[i] == L'e') + { + num = static_cast(Command::CommandEXP); + } + else if (p.first[i] == L'-') + { + num = static_cast(Command::CommandSIGN); + + if (i == 0) + { + shared_ptr spOpndCommand = dynamic_pointer_cast(exprCmd); + if (!spOpndCommand->IsNegative()) + { + spOpndCommand->ToggleSign(); + } + continue; + } + } + else + { + num = static_cast(p.first[i]) - ASCII_0; + num += IDC_0; + if (num == static_cast(Command::CommandMPLUS)) + { + continue; + } + } + commands->Append(num); + } + } + else + { + commands->Append(0); + } + operandCommand->SetCommands(commands); + } +} + +void StandardCalculatorViewModel::UpdatecommandsInRecordingMode() +{ + vector savedCommands = m_standardCalculatorManager->GetSavedCommands(); + shared_ptr> commands = make_shared>(); + bool isDecimal = false; + bool isNegative = false; + bool isExpMode = false; + bool ePlusMode = false; + bool eMinusMode = false; + + int num = 0; + Command val; + for (unsigned int i = 0; i < savedCommands.size(); ++i) + { + val = safe_cast(savedCommands[i]); + num = static_cast(val); + if (val == Command::CommandSIGN) + { + isNegative = true; + continue; + } + else if ((val >= Command::Command0 && val <= Command::Command9)) + { + } + else if (val == Command::CommandPNT) + { + isDecimal = true; + } + else if (val == Command::CommandEXP) + { + isExpMode = true; + } + else if (isExpMode && !ePlusMode && (val == Command::CommandMPLUS)) + { + ePlusMode = true; + continue; + } + else if (isExpMode && !eMinusMode && (val == Command::CommandMMINUS)) + { + eMinusMode = true; + continue; + } + else + { + //reset all vars + isDecimal = false; + isNegative = false; + isExpMode = false; + ePlusMode = false; + eMinusMode = false; + commands->Clear(); + continue; + } + commands->Append(num); + } + + unsigned int size = 0; + commands->GetSize(&size); + if (size > 0) + { + shared_ptr sp = make_shared(commands, isNegative, isDecimal, isExpMode); + m_commands->Append(sp); + } + Recalculate(); +} + +void StandardCalculatorViewModel::OnMaxDigitsReached() +{ + String^ announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement( + CalculatorResourceKeys::MaxDigitsReachedFormat, + m_localizedMaxDigitsReachedAutomationFormat, + m_CalculationResultAutomationName->Data()); + + Announcement = CalculatorAnnouncement::GetMaxDigitsReachedAnnouncement(announcement); +} + +void StandardCalculatorViewModel::OnBinaryOperatorReceived() +{ + Announcement = GetDisplayUpdatedNarratorAnnouncement(); +} + +NarratorAnnouncement^ StandardCalculatorViewModel::GetDisplayUpdatedNarratorAnnouncement() +{ + String^ announcement; + if (m_feedbackForButtonPress == nullptr || m_feedbackForButtonPress->IsEmpty()) + { + announcement = m_CalculationResultAutomationName; + } + else + { + announcement = LocalizationStringUtil::GetLocalizedNarratorAnnouncement( + CalculatorResourceKeys::ButtonPressFeedbackFormat, + m_localizedButtonPressFeedbackAutomationFormat, + m_CalculationResultAutomationName->Data(), + m_feedbackForButtonPress->Data()); + } + + // Make sure we don't accidentally repeat an announcement. + m_feedbackForButtonPress = nullptr; + + return CalculatorAnnouncement::GetDisplayUpdatedAnnouncement(announcement); +} diff --git a/src/CalcViewModel/StandardCalculatorViewModel.h b/src/CalcViewModel/StandardCalculatorViewModel.h new file mode 100644 index 00000000..df195713 --- /dev/null +++ b/src/CalcViewModel/StandardCalculatorViewModel.h @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once +#include "Common\Automation\NarratorAnnouncement.h" +#include "Common\DisplayExpressionToken.h" +#include "Common\CalculatorDisplay.h" +#include "HistoryViewModel.h" +#include "MemoryItemViewModel.h" +#include "Common\EngineResourceProvider.h" + +namespace CalculatorFunctionalTests +{ + class HistoryTests; +} + +namespace CalculatorUnitTests +{ + class MultiWindowUnitTests; + class TimerTests; +} + +namespace CalculatorApp +{ + namespace WS = Windows::System; + namespace CM = CalculationManager; + + namespace ViewModel + { +#define ASCII_0 48 + public delegate void HideMemoryClickedHandler(); + public delegate void ProgModeRadixChangeHandler(); + namespace CalculatorViewModelProperties + { + extern Platform::StringReference IsMemoryEmpty; + extern Platform::StringReference IsInError; + extern Platform::StringReference BinaryDisplayValue; + } + + [Windows::UI::Xaml::Data::Bindable] + public ref class StandardCalculatorViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + public: + StandardCalculatorViewModel(); + void UpdateOperand(int pos, Platform::String^ text); + void UpdatecommandsInRecordingMode(); + int GetBitLengthType(); + int GetNumberBase(); + + OBSERVABLE_OBJECT_CALLBACK(OnPropertyChanged); + OBSERVABLE_PROPERTY_RW(Platform::String^, DisplayValue); + OBSERVABLE_PROPERTY_RW(HistoryViewModel^, HistoryVM); + OBSERVABLE_PROPERTY_RW(bool, IsInError); + OBSERVABLE_PROPERTY_RW(bool, IsOperatorCommand); + OBSERVABLE_PROPERTY_RW(Platform::String^, DisplayStringExpression); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IVector^, ExpressionTokens); + OBSERVABLE_PROPERTY_RW(Platform::String^, DecimalDisplayValue); + OBSERVABLE_PROPERTY_RW(Platform::String^, HexDisplayValue); + OBSERVABLE_PROPERTY_RW(Platform::String^, OctalDisplayValue); + OBSERVABLE_PROPERTY_RW(Platform::String^, BinaryDisplayValue); + OBSERVABLE_PROPERTY_RW(Platform::String^, HexDisplayValue_AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, DecDisplayValue_AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, OctDisplayValue_AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, BinDisplayValue_AutomationName); + OBSERVABLE_PROPERTY_RW(bool, IsBinaryOperatorEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsUnaryOperatorEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsNegateEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsDecimalEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsCurrentViewPinned); + OBSERVABLE_PROPERTY_RW(Windows::Foundation::Collections::IVector^, MemorizedNumbers); + OBSERVABLE_PROPERTY_RW(bool, IsMemoryEmpty); + OBSERVABLE_PROPERTY_RW(bool, IsFToEChecked); + OBSERVABLE_PROPERTY_RW(bool, IsFToEEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsHyperbolicChecked); + OBSERVABLE_PROPERTY_RW(bool, AreHEXButtonsEnabled); + NAMED_OBSERVABLE_PROPERTY_RW(Platform::String^, CalculationResultAutomationName); + NAMED_OBSERVABLE_PROPERTY_RW(Platform::String^, CalculationExpressionAutomationName); + OBSERVABLE_PROPERTY_RW(bool, IsShiftProgrammerChecked); + OBSERVABLE_PROPERTY_RW(bool, IsQwordEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsDwordEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsWordEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsByteEnabled); + OBSERVABLE_PROPERTY_RW(Platform::String^, OpenParenthesisCount); + OBSERVABLE_PROPERTY_RW(int, CurrentRadixType); + OBSERVABLE_PROPERTY_RW(bool, AreTokensUpdated); + OBSERVABLE_PROPERTY_RW(bool, AreHistoryShortcutsEnabled); + OBSERVABLE_PROPERTY_RW(bool, AreProgrammerRadixOperatorsEnabled); + OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::Automation::NarratorAnnouncement^, Announcement); + + COMMAND_FOR_METHOD(CopyCommand, StandardCalculatorViewModel::OnCopyCommand); + COMMAND_FOR_METHOD(PasteCommand, StandardCalculatorViewModel::OnPasteCommand); + COMMAND_FOR_METHOD(ButtonPressed, StandardCalculatorViewModel::OnButtonPressed); + COMMAND_FOR_METHOD(ClearMemoryCommand, StandardCalculatorViewModel::OnClearMemoryCommand); + COMMAND_FOR_METHOD(PinUnpinAppBarButtonOnClicked, StandardCalculatorViewModel::OnPinUnpinCommand); + COMMAND_FOR_METHOD(MemoryItemPressed, StandardCalculatorViewModel::OnMemoryItemPressed); + COMMAND_FOR_METHOD(MemoryAdd, StandardCalculatorViewModel::OnMemoryAdd); + COMMAND_FOR_METHOD(MemorySubtract, StandardCalculatorViewModel::OnMemorySubtract); + + event HideMemoryClickedHandler^ HideMemoryClicked; + event ProgModeRadixChangeHandler^ ProgModeRadixChange; + + property bool IsShiftChecked { + bool get() { return m_isShiftChecked; } + void set(bool value) + { + if (m_isShiftChecked != value) + { + m_isShiftChecked = value; + RaisePropertyChanged(L"IsShiftChecked"); + } + } + } + + property bool IsBitFlipChecked { + bool get() { return m_isBitFlipChecked; } + void set(bool value) + { + if (m_isBitFlipChecked != value) + { + m_isBitFlipChecked = value; + IsBinaryBitFlippingEnabled = IsProgrammer && m_isBitFlipChecked; + AreProgrammerRadixOperatorsEnabled = IsProgrammer && !m_isBitFlipChecked; + RaisePropertyChanged(L"IsBitFlipChecked"); + } + } + } + + property bool IsBinaryBitFlippingEnabled { + bool get() { return m_isBinaryBitFlippingEnabled; } + void set(bool value) + { + if (m_isBinaryBitFlippingEnabled != value) + { + m_isBinaryBitFlippingEnabled = value; + RaisePropertyChanged(L"IsBinaryBitFlippingEnabled"); + } + } + } + + property bool IsStandard { + bool get() { return m_isStandard; } + void set(bool value) + { + if (m_isStandard != value) + { + m_isStandard = value; + if (value) + { + IsScientific = false; + IsProgrammer = false; + } + RaisePropertyChanged(L"IsStandard"); + } + } + } + + property bool IsScientific { + bool get() { return m_isScientific; } + void set(bool value) + { + if (m_isScientific != value) + { + m_isScientific = value; + if (value) + { + IsStandard = false; + IsProgrammer = false; + } + RaisePropertyChanged(L"IsScientific"); + } + } + } + + property bool IsProgrammer { + bool get() { return m_isProgrammer; } + void set(bool value) + { + if (m_isProgrammer != value) + { + m_isProgrammer = value; + if (!m_isProgrammer) + { + IsBitFlipChecked = false; + } + IsBinaryBitFlippingEnabled = m_isProgrammer && IsBitFlipChecked; + AreProgrammerRadixOperatorsEnabled = m_isProgrammer && !IsBitFlipChecked; + if (value) + { + IsStandard = false; + IsScientific = false; + } + RaisePropertyChanged(L"IsProgrammer"); + } + } + } + + property bool IsEditingEnabled { + bool get() { return m_isEditingEnabled; } + void set(bool value) { + if (m_isEditingEnabled != value) + { + // Numbers::Common::KeyboardShortcutManager::IsCalculatorInEditingMode = value; + m_isEditingEnabled = value; + bool currentEditToggleValue = !m_isEditingEnabled; + IsBinaryOperatorEnabled = currentEditToggleValue; + IsUnaryOperatorEnabled = currentEditToggleValue; + IsOperandEnabled = currentEditToggleValue; + IsNegateEnabled = currentEditToggleValue; + IsDecimalEnabled = currentEditToggleValue; + RaisePropertyChanged(L"IsEditingEnabled"); + } + } + } + + property bool IsEngineRecording { + bool get() { return m_standardCalculatorManager->IsEngineRecording(); } + } + + property bool IsOperandEnabled { + bool get() { return m_isOperandEnabled; } + void set(bool value) { + if (m_isOperandEnabled != value) + { + m_isOperandEnabled = value; + IsDecimalEnabled = value; + AreHEXButtonsEnabled = IsProgrammer; + IsFToEEnabled = value; + RaisePropertyChanged(L"IsOperandEnabled"); + } + } + } + + property int TokenPosition + { + int get() { return m_tokenPosition; } + void set(int value) { m_tokenPosition = value; } + } + + property Platform::String^ SelectedExpressionLastData + { + Platform::String^ get() { return m_selectedExpressionLastData; } + void set(Platform::String^ value) { m_selectedExpressionLastData = value; } + } + + property bool KeyPressed + { + bool get() { return m_keyPressed; } + void set(bool value) { m_keyPressed = value; } + } + + property bool IsOperandUpdatedUsingViewModel + { + bool get() { return m_operandUpdated; } + void set(bool value) { m_operandUpdated = value; } + } + + property bool IsOperandTextCompletelySelected + { + bool get() { return m_completeTextSelection; } + void set(bool value) { m_completeTextSelection = value; } + } + + property Platform::String^ LeftParenthesisAutomationName + { + Platform::String^ get() + { + return GetLeftParenthesisAutomationName(); + } + } + + internal: + void OnPaste(Platform::String^ pastedString, CalculatorApp::Common::ViewMode mode); + void OnCopyCommand(Platform::Object^ parameter); + void OnPasteCommand(Platform::Object^ parameter); + + NumbersAndOperatorsEnum MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate); + + //Memory feature related methods. They are internal because they need to called from the MainPage code-behind + void OnMemoryButtonPressed(); + void OnMemoryItemPressed(Platform::Object^ memoryItemPosition); + void OnMemoryAdd(Platform::Object^ memoryItemPosition); + void OnMemorySubtract(Platform::Object^ memoryItemPosition); + void OnMemoryClear(_In_ Platform::Object^ memoryItemPosition); + void OnPinUnpinCommand(Platform::Object^ parameter); + + void SetPrimaryDisplay(_In_ std::wstring const&displayString, _In_ bool isError); + void DisplayPasteError(); + void SetTokens(_Inout_ std::shared_ptr>> const &tokens); + void SetExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands); + void SetHistoryExpressionDisplay(_Inout_ std::shared_ptr>> const &tokens, _Inout_ std::shared_ptr>> const &commands); + void SetParenthesisCount(_In_ const std::wstring& parenthesisCount); + void OnMaxDigitsReached(); + void OnBinaryOperatorReceived(); + void OnMemoryItemChanged(unsigned int indexOfMemory); + + Platform::Array^ Serialize(); + void Deserialize(Platform::Array^ state); + + Platform::String^ GetLocalizedStringFormat(Platform::String^ format, Platform::String^ displayValue); + void OnPropertyChanged(Platform::String^ propertyname); + void SetCalculatorType(CalculatorApp::Common::ViewMode targetState); + + Platform::String^ GetRawDisplayValue(); + void Recalculate(bool fromHistory = false); + bool IsOperator(CalculationManager::Command cmdenum); + void FtoEButtonToggled(); + void SwitchProgrammerModeBase(RADIX_TYPE calculatorBase); + void SetMemorizedNumbersString(); + void SwitchAngleType(NumbersAndOperatorsEnum num); + void ResetDisplay(); + RADIX_TYPE GetCurrentRadixType() { return (RADIX_TYPE)m_CurrentRadixType; } + void SetPrecision(int32_t precision); + void UpdateMaxIntDigits() { m_standardCalculatorManager->UpdateMaxIntDigits(); } + NumbersAndOperatorsEnum GetCurrentAngleType() { return m_CurrentAngleType; } + + private: + void SetMemorizedNumbers(const std::vector& memorizedNumbers); + void UpdateProgrammerPanelDisplay(); + void HandleUpdatedOperandData(CalculationManager::Command cmdenum); + NumbersAndOperatorsEnum ConvertIntegerToNumbersAndOperatorsEnum(unsigned int parameter); + NumbersAndOperatorsEnum m_CurrentAngleType; + wchar_t m_decimalSeparator; + CalculatorDisplay m_calculatorDisplay; + CalculatorApp::EngineResourceProvider m_resourceProvider; + std::unique_ptr m_standardCalculatorManager; + Platform::String^ m_expressionAutomationNameFormat; + Platform::String^ m_localizedCalculationResultAutomationFormat; + Platform::String^ m_localizedCalculationResultDecimalAutomationFormat; + Platform::String^ m_localizedHexaDecimalAutomationFormat; + Platform::String^ m_localizedDecimalAutomationFormat; + Platform::String^ m_localizedOctalAutomationFormat; + Platform::String^ m_localizedBinaryAutomationFormat; + Platform::String^ m_localizedMaxDigitsReachedAutomationFormat; + Platform::String^ m_localizedButtonPressFeedbackAutomationFormat; + Platform::String^ m_localizedMemorySavedAutomationFormat; + Platform::String^ m_localizedMemoryItemChangedAutomationFormat; + Platform::String^ m_localizedMemoryItemClearedAutomationFormat; + Platform::String^ m_localizedMemoryCleared; + + bool m_pinned; + bool m_isOperandEnabled; + bool m_isEditingEnabled; + bool m_isStandard; + bool m_isScientific; + bool m_isProgrammer; + bool m_isBinaryBitFlippingEnabled; + bool m_isBitFlipChecked; + bool m_isShiftChecked; + bool m_isRtlLanguage; + int m_tokenPosition; + bool m_keyPressed; + bool m_operandUpdated; + bool m_completeTextSelection; + bool m_isLastOperationHistoryLoad; + Platform::String^ m_selectedExpressionLastData; + Common::DisplayExpressionToken^ m_selectedExpressionToken; + Platform::String^ m_leftParenthesisAutomationFormat; + + Platform::String^ LocalizeDisplayValue(_In_ std::wstring const &displayValue, _In_ bool isError); + Platform::String^ CalculateNarratorDisplayValue(_In_ std::wstring const &displayValue, _In_ Platform::String^ localizedDisplayValue, _In_ bool isError); + CalculatorApp::Common::Automation::NarratorAnnouncement^ GetDisplayUpdatedNarratorAnnouncement(); + Platform::String^ GetCalculatorExpressionAutomationName(); + Platform::String^ GetNarratorStringReadRawNumbers(_In_ Platform::String^ localizedDisplayValue); + + CalculationManager::Command ConvertToOperatorsEnum(NumbersAndOperatorsEnum operation); + void DisableButtons(CalculationManager::CommandType selectedExpressionCommandType); + Platform::String^ GetLeftParenthesisAutomationName(); + + Platform::String^ m_feedbackForButtonPress; + void OnButtonPressed(Platform::Object^ parameter); + void OnClearMemoryCommand(Platform::Object^ parameter); + std::wstring AddPadding(std::wstring); + size_t LengthWithoutPadding(std::wstring); + + std::shared_ptr>> m_tokens; + std::shared_ptr>> m_commands; + + // Token types + bool IsUnaryOp(int nOpCode); + bool IsBinOp(int nOpcode); + bool IsTrigOp(int nOpCode); + bool IsOpnd(int nOpCode); + bool IsRecoverableCommand(int nOpCode); + + CalculationManager::CommandType GetSelectedTokenType(_In_ unsigned int); + void SaveEditedCommand(_In_ unsigned int index, _In_ CalculationManager::Command command); + + bool IsViewPinned(); + void SetViewPinnedState(bool pinned); + + friend class CalculatorDisplay; + friend class CalculatorFunctionalTests::HistoryTests; + friend class CalculatorUnitTests::MultiWindowUnitTests; + friend class CalculatorUnitTests::TimerTests; + }; + } +} diff --git a/src/CalcViewModel/UnitConverterViewModel.cpp b/src/CalcViewModel/UnitConverterViewModel.cpp new file mode 100644 index 00000000..9fef2303 --- /dev/null +++ b/src/CalcViewModel/UnitConverterViewModel.cpp @@ -0,0 +1,1092 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "UnitConverterViewModel.h" +#include "Common\CalculatorButtonPressedEventArgs.h" +#include "Common\CopyPasteManager.h" +#include "Common\LocalizationStringUtil.h" +#include "Common\LocalizationService.h" +#include "Common\LocalizationSettings.h" +#include "DataLoaders\CurrencyHttpClient.h" +#include "DataLoaders\CurrencyDataLoader.h" +#include "DataLoaders\UnitConverterDataLoader.h" + +using namespace CalculatorApp; +using namespace CalculatorApp::Common; +using namespace CalculatorApp::Common::Automation; +using namespace CalculatorApp::ViewModel; +using namespace concurrency; +using namespace Platform; +using namespace Platform::Collections; +using namespace std; +using namespace Windows::Foundation; +using namespace Windows::Globalization::NumberFormatting; +using namespace Windows::System; +using namespace Windows::System::Threading; +using namespace Windows::System::UserProfile; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Automation::Peers; +using namespace Windows::ApplicationModel::Resources; +using namespace Windows::Storage; + +constexpr int EXPECTEDVIEWMODELDATATOKENS = 8; + +// interval is in 100 nanosecond units +constexpr unsigned int TIMER_INTERVAL_IN_MS = 10000; + +#ifdef UNIT_TESTS +#define TIMER_CALLBACK_CONTEXT CallbackContext::Any +#else +#define TIMER_CALLBACK_CONTEXT CallbackContext::Same +#endif + +const TimeSpan SUPPLEMENTARY_VALUES_INTERVAL = { 10 * TIMER_INTERVAL_IN_MS }; + +static Unit^ EMPTY_UNIT = ref new Unit(UCM::EMPTY_UNIT); + +constexpr size_t UNIT_LIST = 0; +constexpr size_t SELECTED_SOURCE_UNIT = 1; +constexpr size_t SELECTED_TARGET_UNIT = 2; + +// x millisecond delay before we consider conversion to be final +constexpr unsigned int CONVERSION_FINALIZED_DELAY_IN_MS = 1000; + +namespace CalculatorApp::ViewModel +{ + namespace UnitConverterViewModelProperties + { + StringReference CurrentCategory(L"CurrentCategory"); + StringReference Unit1(L"Unit1"); + StringReference Unit2(L"Unit2"); + StringReference Value1Active(L"Value1Active"); + StringReference Value2Active(L"Value2Active"); + StringReference Value1(L"Value1"); + StringReference Value2(L"Value2"); + StringReference Value1AutomationName(L"Value1AutomationName"); + StringReference Value2AutomationName(L"Value2AutomationName"); + StringReference SupplementaryVisibility(L"SupplementaryVisibility"); + StringReference SupplementaryResults(L"SupplementaryResults"); + StringReference Unit1AutomationName(L"Unit1AutomationName"); + StringReference Unit2AutomationName(L"Unit2AutomationName"); + StringReference CurrencySymbol1(L"CurrencySymbol1"); + StringReference CurrencySymbol2(L"CurrencySymbol2"); + StringReference CurrencySymbolVisibility(L"CurrencySymbolVisibility"); + StringReference CurrencyRatioEquality(L"CurrencyRatioEquality"); + StringReference CurrencyRatioEqualityAutomationName(L"CurrencyRatioEqualityAutomationName"); + StringReference NetworkBehavior(L"NetworkBehavior"); + StringReference CurrencyDataLoadFailed(L"CurrencyDataLoadFailed"); + StringReference CurrencyDataIsWeekOld(L"CurrencyDataIsWeekOld"); + StringReference IsCurrencyLoadingVisible(L"IsCurrencyLoadingVisible"); + } + + namespace UnitConverterResourceKeys + { + StringReference ValueFromFormat(L"Format_ValueFrom"); + StringReference ValueFromDecimalFormat(L"Format_ValueFrom_Decimal"); + StringReference ValueToFormat(L"Format_ValueTo"); + StringReference ConversionResultFormat(L"Format_ConversionResult"); + StringReference InputUnit_Name(L"InputUnit_Name"); + StringReference OutputUnit_Name(L"OutputUnit_Name"); + StringReference MaxDigitsReachedFormat(L"Format_MaxDigitsReached"); + StringReference UpdatingCurrencyRates(L"UpdatingCurrencyRates"); + StringReference CurrencyRatesUpdated(L"CurrencyRatesUpdated"); + StringReference CurrencyRatesUpdateFailed(L"CurrencyRatesUpdateFailed"); + } +} + +UnitConverterViewModel::UnitConverterViewModel(const shared_ptr& model) : + m_model(model), + m_resettingTimer(false), + m_value1cp(ConversionParameter::Source), + m_Value1Active(true), + m_Value2Active(false), + m_Value1("0"), + m_Value2("0"), + m_valueToUnlocalized(L"0"), + m_valueFromUnlocalized(L"0"), + m_relocalizeStringOnSwitch(false), + m_Categories(ref new Vector()), + m_Units(ref new Vector()), + m_SupplementaryResults(ref new Vector), + m_IsDropDownOpen(false), + m_IsDropDownEnabled(true), + m_IsCurrencyLoadingVisible(false), + m_isCurrencyDataLoaded(false), + m_lastAnnouncedFrom(L""), + m_lastAnnouncedTo(L""), + m_lastAnnouncedConversionResult(L""), + m_isValue1Updating(false), + m_isValue2Updating(false), + m_Announcement(nullptr), + m_Mode(ViewMode::None), + m_CurrencySymbol1(L""), + m_CurrencySymbol2(L""), + m_IsCurrencyCurrentCategory(false), + m_CurrencyRatioEquality(L""), + m_CurrencyRatioEqualityAutomationName(L""), + m_isInputBlocked(false), + m_CurrencyDataLoadFailed(false) +{ + m_model->SetViewModelCallback(make_shared(this)); + m_model->SetViewModelCurrencyCallback(make_shared(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; + + auto 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 UnitConverterViewModel::ResetView() +{ + m_model->SendCommand(UCM::Command::Reset); + m_IsFirstTime = true; + OnCategoryChanged(nullptr); +} + +void UnitConverterViewModel::PopulateData() +{ + InitializeView(); +} + +void UnitConverterViewModel::OnCategoryChanged(Object^ parameter) +{ + UCM::Category currentCategory = CurrentCategory->GetModelCategory(); + IsCurrencyCurrentCategory = currentCategory.id == NavCategory::Serialize(ViewMode::Currency); + + m_model->SendCommand(UCM::Command::Clear); + + m_isInputBlocked = false; + SetSelectedUnits(); + + IsCurrencyLoadingVisible = m_IsCurrencyCurrentCategory && !m_isCurrencyDataLoaded; + IsDropDownEnabled = m_Units->GetAt(0) != EMPTY_UNIT; + + UnitChanged->Execute(nullptr); +} + +void UnitConverterViewModel::SetSelectedUnits() +{ + UCM::CategorySelectionInitializer categoryInitializer = m_model->SetCurrentCategory(CurrentCategory->GetModelCategory()); + BuildUnitList(get(categoryInitializer)); + + UnitFrom = FindUnitInList(get(categoryInitializer)); + UnitTo = FindUnitInList(get(categoryInitializer)); +} + +void UnitConverterViewModel::BuildUnitList(const vector& modelUnitList) +{ + m_Units->Clear(); + for (const UCM::Unit& modelUnit : modelUnitList) + { + if (modelUnit.isWhimsical) + { + continue; + } + + m_Units->Append(ref new Unit(modelUnit)); + } + + if (m_Units->Size == 0) + { + m_Units->Append(EMPTY_UNIT); + } +} + +Unit^ UnitConverterViewModel::FindUnitInList(UCM::Unit target) +{ + for (Unit^ vmUnit : m_Units) + { + UCM::Unit modelUnit = vmUnit->GetModelUnit(); + if (modelUnit.id == target.id) + { + return vmUnit; + } + } + + return EMPTY_UNIT; +} + +void UnitConverterViewModel::OnUnitChanged(Object^ parameter) +{ + if ((m_Unit1 == nullptr) || (m_Unit2 == nullptr)) + { + // Return if both Unit1 & Unit2 are not set + return; + } + + m_model->SetCurrentUnitTypes(UnitFrom->GetModelUnit(), UnitTo->GetModelUnit()); + if (m_supplementaryResultsTimer != nullptr) + { + // End timer to show results immediately + m_supplementaryResultsTimer->Cancel(); + } + if (!m_IsFirstTime) + { + SaveUserPreferences(); + } + else + { + RestoreUserPreferences(); + m_IsFirstTime = false; + } +} + +void UnitConverterViewModel::OnSwitchActive(Platform::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; + } + + m_valueFromUnlocalized.swap(m_valueToUnlocalized); + Utils::Swap(&m_localizedValueFromFormat, &m_localizedValueToFormat); + + Utils::Swap(&m_Unit1AutomationName, &m_Unit2AutomationName); + RaisePropertyChanged(UnitConverterViewModelProperties::Unit1AutomationName); + RaisePropertyChanged(UnitConverterViewModelProperties::Unit2AutomationName); + + m_isInputBlocked = false; + m_model->SwitchActive(m_valueFromUnlocalized); +} + +String^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings) +{ + Platform::String^ result; + + if (stringToLocalize.empty()) + { + return result; + } + + m_decimalFormatter->IsDecimalPointAlwaysDisplayed = false; + m_decimalFormatter->FractionDigits = 0; + m_currencyFormatter->IsDecimalPointAlwaysDisplayed = false; + m_currencyFormatter->FractionDigits = 0; + + wstring::size_type posOfE = stringToLocalize.find(L'e'); + if (posOfE != wstring::npos) + { + wstring::size_type posOfSign = posOfE + 1; + wchar_t signOfE = stringToLocalize.at(posOfSign); + std::wstring significandStr(stringToLocalize.substr(0, posOfE)); + std::wstring 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. + wstring::size_type posOfDecimal = stringToLocalize.find(L'.'); + + bool hasDecimal = wstring::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 = static_cast(stringToLocalize.length() - (posOfDecimal + 1)); + m_currencyFormatter->FractionDigits = m_currencyMaxFractionDigits; + } + + if (IsCurrencyCurrentCategory) + { + wstring currencyResult = m_currencyFormatter->Format(stod(stringToLocalize))->Data(); + wstring currencyCode = m_currencyFormatter->Currency->Data(); + + // CurrencyFormatter always includes LangCode or Symbol. Make it include LangCode + // because this includes a non-breaking space. Remove the LangCode. + auto pos = currencyResult.find(currencyCode); + if (pos != wstring::npos) + { + currencyResult.erase(pos, currencyCode.length()); + pos = currencyResult.find(L'\u00a0'); // non-breaking space + if (pos != wstring::npos) + { + currencyResult.erase(pos, 1); + } + } + + result = ref new String(currencyResult.c_str()); + } + else + { + // Convert the input string to double using stod + // Then use the decimalFormatter to reformat the double to Platform String + result = m_decimalFormatter->Format(stod(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(stod("1.1")); + wstring formattedSampleWString = wstring(formattedSampleString->Data()); + + wstring resultWithDecimal = wstring(result->Data()); + size_t pos = resultWithDecimal.find(formattedSampleWString[1], 0); + if (pos != wstring::npos) + { + resultWithDecimal.replace(pos, 1, &m_decimalSeparator); + } + + // Copy back the edited string to the result + result = ref new String(resultWithDecimal.c_str()); + } + } + + wstring resultHolder = wstring(result->Data()); + if ((stringToLocalize.front() == L'-' && stod(stringToLocalize) == 0) || resultHolder.back() == L'-') + { + if (resultHolder.back() == L'-') + { + result = ref new String(resultHolder.erase(resultHolder.size() - 1, 1).c_str()); + } + result = L"-" + result; + } + result = Utils::LRE + result + Utils::PDF; + return result; +} + +void UnitConverterViewModel::DisplayPasteError() +{ + String^ errorMsg = AppResourceProvider::GetInstance().GetCEngineString(SIDS_DOMAIN); /*SIDS_DOMAIN is for "invalid input"*/ + Value1 = errorMsg; + Value2 = errorMsg; + m_relocalizeStringOnSwitch = false; +} + +void UnitConverterViewModel::UpdateDisplay(const wstring& from, const wstring& 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; + } +} + +void UnitConverterViewModel::UpdateSupplementaryResults(const std::vector>& suggestedValues) +{ + m_cacheMutex.lock(); + m_cachedSuggestedValues = suggestedValues; + m_cacheMutex.unlock(); + + // If we're already "ticking", reset the timer + if (m_supplementaryResultsTimer != nullptr) + { + m_resettingTimer = true; + m_supplementaryResultsTimer->Cancel(); + m_resettingTimer = false; + } + + // Schedule the timer + m_supplementaryResultsTimer = ThreadPoolTimer::CreateTimer( + ref new TimerElapsedHandler(this, &UnitConverterViewModel::SupplementaryResultsTimerTick, TIMER_CALLBACK_CONTEXT), + SUPPLEMENTARY_VALUES_INTERVAL, + ref new TimerDestroyedHandler(this, &UnitConverterViewModel::SupplementaryResultsTimerCancel, TIMER_CALLBACK_CONTEXT)); +} + +void UnitConverterViewModel::OnValueActivated(IActivatable^ control) +{ + control->IsActive = true; +} + +void UnitConverterViewModel::OnButtonPressed(Platform::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; + } + + static const vector OPERANDS = { + 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 (find(begin(OPERANDS), end(OPERANDS), command) != OPERANDS.end()) + { + if (m_isInputBlocked) + { + return; + } + + if (m_IsCurrencyCurrentCategory) + { + StartConversionResultTimer(); + } + } + + m_model->SendCommand(command); +} + +void UnitConverterViewModel::OnCopyCommand(Platform::Object^ parameter) +{ + //EventWriteClipboardCopy_Start(); + CopyPasteManager::CopyToClipboard(ref new Platform::String(m_valueFromUnlocalized.c_str())); + //EventWriteClipboardCopy_Stop(); +} + +void UnitConverterViewModel::OnPasteCommand(Platform::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. + CopyPasteManager::GetStringToPaste(m_Mode, NavCategory::GetGroupType(m_Mode)).then( + [this](String^ pastedString) + { + OnPaste(pastedString, m_Mode); + }, concurrency::task_continuation_context::use_current()); +} + +void UnitConverterViewModel::InitializeView() +{ + vector categories = m_model->GetCategories(); + for (UINT i = 0; i < categories.size(); i++) + { + Category^ category = ref new Category(categories[i]); + m_Categories->Append(category); + } + + RestoreUserPreferences(); + CurrentCategory = ref new Category(m_model->GetCurrentCategory()); +} + +void UnitConverterViewModel::OnPropertyChanged(Platform::String^ prop) +{ + static bool isCategoryChanging = false; + + if (prop->Equals(UnitConverterViewModelProperties::CurrentCategory)) + { + isCategoryChanging = true; + CategoryChanged->Execute(nullptr); + isCategoryChanging = false; + } + else if (prop->Equals(UnitConverterViewModelProperties::Unit1) || prop->Equals(UnitConverterViewModelProperties::Unit2)) + { + // 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(nullptr); + } + // Get the localized automation name for each CalculationResults field + if (prop->Equals(UnitConverterViewModelProperties::Unit1)) + { + UpdateValue1AutomationName(); + } + else + { + UpdateValue2AutomationName(); + } + } + else if (prop->Equals(UnitConverterViewModelProperties::Value1)) + { + UpdateValue1AutomationName(); + } + else if (prop->Equals(UnitConverterViewModelProperties::Value2)) + { + UpdateValue2AutomationName(); + } + else if (prop->Equals(UnitConverterViewModelProperties::Value1Active) || prop->Equals(UnitConverterViewModelProperties::Value2Active)) + { + // 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(nullptr); + } + + UpdateValue1AutomationName(); + UpdateValue2AutomationName(); + } + else if (prop->Equals(UnitConverterViewModelProperties::SupplementaryResults)) + { + RaisePropertyChanged(UnitConverterViewModelProperties::SupplementaryVisibility); + } + else if (prop->Equals(UnitConverterViewModelProperties::Value1AutomationName)) + { + m_isValue1Updating = false; + if (!m_isValue2Updating) + { + AnnounceConversionResult(); + } + } + else if (prop->Equals(UnitConverterViewModelProperties::Value2AutomationName)) + { + m_isValue2Updating = false; + if (!m_isValue1Updating) + { + AnnounceConversionResult(); + } + } + else if (prop->Equals(UnitConverterViewModelProperties::CurrencySymbol1) || prop->Equals(UnitConverterViewModelProperties::CurrencySymbol2)) + { + RaisePropertyChanged(UnitConverterViewModelProperties::CurrencySymbolVisibility); + } +} + +String^ UnitConverterViewModel::Serialize() +{ + wstringstream out(wstringstream::out); + const wchar_t * delimiter = L"[;;;]"; + out << std::to_wstring(m_resettingTimer) << delimiter; + out << std::to_wstring(static_cast(m_value1cp)) << delimiter; + out << m_Value1Active << delimiter << m_Value2Active << delimiter; + out << m_Value1->Data() << delimiter << m_Value2->Data() << delimiter; + out << m_valueFromUnlocalized << delimiter << m_valueToUnlocalized << delimiter << L"[###]"; + wstring unitConverterSerializedData = m_model->Serialize(); + + if (!unitConverterSerializedData.empty()) + { + out << m_model->Serialize() << L"[###]"; + String^ serializedData = ref new String(wstring(out.str()).c_str()); + return serializedData; + } + else + { + return nullptr; + } +} + +void UnitConverterViewModel::Deserialize(Platform::String^ state) +{ + wstring serializedData = wstring(state->Data()); + vector tokens = UCM::UnitConverter::StringToVector(serializedData, L"[###]"); + assert(tokens.size() >= 2); + vector viewModelData = UCM::UnitConverter::StringToVector(tokens[0], L"[;;;]"); + assert(viewModelData.size() == EXPECTEDVIEWMODELDATATOKENS); + m_resettingTimer = (viewModelData[0].compare(L"1") == 0); + m_value1cp = (ConversionParameter)_wtoi(viewModelData[1].c_str()); + m_Value1Active = (viewModelData[2].compare(L"1") == 0); + m_Value2Active = (viewModelData[3].compare(L"1") == 0); + m_Value1 = ref new String(viewModelData[4].c_str()); + m_Value2 = ref new String(viewModelData[5].c_str()); + m_valueFromUnlocalized = viewModelData[6]; + m_valueToUnlocalized = viewModelData[7]; + wstringstream modelData(wstringstream::out); + for (unsigned int i = 1; i < tokens.size(); i++) + { + modelData << tokens[i] << L"[###]"; + } + m_model->DeSerialize(modelData.str()); + InitializeView(); + RaisePropertyChanged(nullptr); // Update since all props have been updated. +} + +//Saving User Prefernces of Category and Associated-Units across Sessions. +void UnitConverterViewModel::SaveUserPreferences() +{ + if (UnitsAreValid()) + { + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + if (!m_IsCurrencyCurrentCategory) + { + auto userPreferences = m_model->SaveUserPreferences(); + localSettings->Values->Insert(ref new String(L"UnitConverterPreferences"), ref new String(userPreferences.c_str())); + } + 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->Insert(UnitConverterResourceKeys::CurrencyUnitFromKey, UnitFrom->Abbreviation); + localSettings->Values->Insert(UnitConverterResourceKeys::CurrencyUnitToKey, UnitTo->Abbreviation); + } + } +} + +//Restoring User Prefernces of Category and Associated-Units. +void UnitConverterViewModel::RestoreUserPreferences() +{ + if (!IsCurrencyCurrentCategory) + { + ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings; + if (localSettings->Values->HasKey(ref new String(L"UnitConverterPreferences"))) + { + String^ userPreferences = safe_cast(localSettings->Values->Lookup(ref new String(L"UnitConverterPreferences"))); + m_model->RestoreUserPreferences(userPreferences->Data()); + } + } +} + +void UnitConverterViewModel::OnCurrencyDataLoadFinished(bool didLoad) +{ + m_isCurrencyDataLoaded = true; + CurrencyDataLoadFailed = !didLoad; + ResetView(); + + StringReference key = didLoad ? UnitConverterResourceKeys::CurrencyRatesUpdated : UnitConverterResourceKeys::CurrencyRatesUpdateFailed; + String^ announcement = AppResourceProvider::GetInstance().GetResourceString(key); + Announcement = CalculatorAnnouncement::GetUpdateCurrencyRatesAnnouncement(announcement); +} + +void UnitConverterViewModel::OnCurrencyTimestampUpdated(_In_ const wstring& timestamp, bool isWeekOld) +{ + CurrencyDataIsWeekOld = isWeekOld; + CurrencyTimestamp = ref new String(timestamp.c_str()); +} + +void UnitConverterViewModel::RefreshCurrencyRatios() +{ + m_isCurrencyDataLoaded = false; + IsCurrencyLoadingVisible = true; + + String^ announcement = AppResourceProvider::GetInstance().GetResourceString(UnitConverterResourceKeys::UpdatingCurrencyRates); + Announcement = CalculatorAnnouncement::GetUpdateCurrencyRatesAnnouncement(announcement); + + auto refreshTask = create_task(m_model->RefreshCurrencyRatios()); + refreshTask.then([this](const pair& refreshResult) + { + bool didLoad = refreshResult.first; + wstring timestamp = refreshResult.second; + + OnCurrencyTimestampUpdated(timestamp, false /*isWeekOldData*/); + OnCurrencyDataLoadFinished(didLoad); + }, task_continuation_context::use_current()); +} + +void UnitConverterViewModel::OnNetworkBehaviorChanged(_In_ NetworkAccessBehavior newBehavior) +{ + CurrencyDataLoadFailed = false; + NetworkBehavior = newBehavior; +} + +UnitConversionManager::Command UnitConverterViewModel::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 UnitConverterViewModel::SupplementaryResultsTimerTick(ThreadPoolTimer^ timer) +{ + timer->Cancel(); +} + +void UnitConverterViewModel::SupplementaryResultsTimerCancel(ThreadPoolTimer^ timer) +{ + if (!m_resettingTimer) + { + RefreshSupplementaryResults(); + } +} + +void UnitConverterViewModel::RefreshSupplementaryResults() +{ + m_cacheMutex.lock(); + m_SupplementaryResults->Clear(); + + vector whimsicals; + + for (tuple suggestedValue : m_cachedSuggestedValues) + { + SupplementaryResult^ result = + ref new SupplementaryResult( + this->ConvertToLocalizedString(get<0>(suggestedValue), false), + ref new Unit(get<1>(suggestedValue))); + if (result->IsWhimsical()) + { + whimsicals.push_back(result); + } + else + { + m_SupplementaryResults->Append(result); + } + } + + if (whimsicals.size() > 0) + { + m_SupplementaryResults->Append(whimsicals[0]); + } + + m_cacheMutex.unlock(); + RaisePropertyChanged(UnitConverterViewModelProperties::SupplementaryResults); + //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 UnitConverterViewModel::AnnounceConversionResult() +{ + if ((m_valueFromUnlocalized != m_lastAnnouncedFrom + || m_valueToUnlocalized != m_lastAnnouncedTo) + && Unit1 != nullptr + && Unit2 != nullptr) + { + 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 UnitConverterViewModel::UpdateInputBlocked(_In_ const wstring& currencyInput) +{ + // currencyInput is in en-US and has the default decimal separator, so this is safe to do. + auto posOfDecimal = currencyInput.find(L'.'); + if (posOfDecimal != wstring::npos && IsCurrencyCurrentCategory) + { + m_isInputBlocked = (posOfDecimal + static_cast(m_currencyMaxFractionDigits) + 1 == currencyInput.length()); + } + else + { + m_isInputBlocked = false; + } +} + +NumbersAndOperatorsEnum UnitConverterViewModel::MapCharacterToButtonId( + const wchar_t ch, + bool& canSendNegate) +{ + static_assert(NumbersAndOperatorsEnum::Zero < NumbersAndOperatorsEnum::One, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::One < NumbersAndOperatorsEnum::Two, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Two < NumbersAndOperatorsEnum::Three, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Three < NumbersAndOperatorsEnum::Four, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Four < NumbersAndOperatorsEnum::Five, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Five < NumbersAndOperatorsEnum::Six, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Six < NumbersAndOperatorsEnum::Seven, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Seven < NumbersAndOperatorsEnum::Eight, "NumbersAndOperatorsEnum order is invalid"); + static_assert(NumbersAndOperatorsEnum::Eight < NumbersAndOperatorsEnum::Nine, "NumbersAndOperatorsEnum order is invalid"); + static_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 = NumbersAndOperatorsEnum::Zero + static_cast(ch - L'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; + } + } + + if (mappedValue == NumbersAndOperatorsEnum::None) + { + if (LocalizationSettings::GetInstance().IsLocalizedDigit(ch)) + { + mappedValue = NumbersAndOperatorsEnum::Zero + static_cast(ch - LocalizationSettings::GetInstance().GetDigitSymbolFromEnUsDigit(L'0')); + canSendNegate = true; + } + } + + return mappedValue; +} + +void UnitConverterViewModel::OnPaste(String^ stringToPaste, ViewMode mode) +{ + // If pastedString is invalid("NoOp") then display pasteError else process the string + if (stringToPaste == StringReference(CopyPasteManager::PasteErrorString)) + { + this->DisplayPasteError(); + return; + } + + TraceLogger::GetInstance().LogValidInputPasted(mode); + bool isFirstLegalChar = true; + bool sendNegate = false; + wstring accumulation = L""; + + for (auto it = stringToPaste->Begin(); it != stringToPaste->End(); it++) + { + 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::None != op && 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^ UnitConverterViewModel::GetLocalizedAutomationName(_In_ String^ displayvalue, _In_ String^ unitname, _In_ 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; + } + + wstring localizedResult = LocalizationStringUtil::GetLocalizedString(format->Data(), displayvalue->Data(), unitname->Data()); + return ref new String(localizedResult.c_str()); +} + +String^ UnitConverterViewModel::GetLocalizedConversionResultStringFormat(_In_ String^ fromValue, _In_ String^ fromUnit, _In_ String^ toValue, _In_ String^ toUnit) +{ + String^ localizedString = ref new String(LocalizationStringUtil::GetLocalizedString(m_localizedConversionResultFormat->Data(), fromValue->Data(), fromUnit->Data(), toValue->Data(), toUnit->Data()).c_str()); + return localizedString; +} + +void UnitConverterViewModel::UpdateValue1AutomationName() +{ + if (Unit1) + { + Value1AutomationName = GetLocalizedAutomationName(Value1, Unit1->AccessibleName, m_localizedValueFromFormat); + } +} + +void UnitConverterViewModel::UpdateValue2AutomationName() +{ + if (Unit2) + { + Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat); + } +} + +void UnitConverterViewModel::OnMaxDigitsReached() +{ + String^ format = AppResourceProvider::GetInstance().GetResourceString(UnitConverterResourceKeys::MaxDigitsReachedFormat); + const wstring& announcement = LocalizationStringUtil::GetLocalizedString(format->Data(), m_lastAnnouncedConversionResult->Data()); + Announcement = CalculatorAnnouncement::GetMaxDigitsReachedAnnouncement(StringReference(announcement.c_str())); +} + +bool UnitConverterViewModel::UnitsAreValid() +{ + return UnitFrom != nullptr && !UnitFrom->Abbreviation->IsEmpty() && UnitTo != nullptr && !UnitTo->Abbreviation->IsEmpty(); +} + +void UnitConverterViewModel::StartConversionResultTimer() +{ + m_conversionResultTaskHelper = make_unique( + CONVERSION_FINALIZED_DELAY_IN_MS, [this]() + { + if (UnitsAreValid()) + { + String^ valueFrom = m_Value1Active ? m_Value1 : m_Value2; + String^ valueTo = m_Value1Active ? m_Value2 : m_Value1; + TraceLogger::GetInstance().LogConversionResult( + valueFrom->Data(), + UnitFrom->ToString()->Data(), + valueTo->Data(), + UnitTo->ToString()->Data()); + } + }); +} + +String^ SupplementaryResult::GetLocalizedAutomationName() +{ + auto format = AppResourceProvider::GetInstance().GetResourceString("SupplementaryUnit_AutomationName"); + return ref new String(LocalizationStringUtil::GetLocalizedString( + format->Data(), + this->Value->Data(), + this->Unit->Name->Data()).c_str()); +} diff --git a/src/CalcViewModel/UnitConverterViewModel.h b/src/CalcViewModel/UnitConverterViewModel.h new file mode 100644 index 00000000..2405c7f1 --- /dev/null +++ b/src/CalcViewModel/UnitConverterViewModel.h @@ -0,0 +1,420 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "Common\NetworkManager.h" +#include "Common\Automation\NarratorAnnouncement.h" +#include "Common\ConversionResultTaskHelper.h" + +namespace CalculatorApp +{ + namespace ViewModel + { + [Windows::UI::Xaml::Data::Bindable] + public ref class Category sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + internal: + Category(const UnitConversionManager::Category& category) : + m_original(category) + { } + + public: + OBSERVABLE_OBJECT(); + + property Platform::String^ Name + { + Platform::String^ get() { return ref new Platform::String(m_original.name.c_str()); } + } + + property Windows::UI::Xaml::Visibility NegateVisibility + { + Windows::UI::Xaml::Visibility get() + { + return m_original.supportsNegative + ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + internal: + const UnitConversionManager::Category& GetModelCategory() const { return m_original; } + + private: + const UnitConversionManager::Category m_original; + }; + + [Windows::UI::Xaml::Data::Bindable] + public ref class Unit sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + internal: + Unit(const UnitConversionManager::Unit& unit) : + m_original(unit) + { } + + public: + OBSERVABLE_OBJECT(); + + property Platform::String^ Name + { + Platform::String^ get() { return ref new Platform::String(m_original.name.c_str()); } + } + + property Platform::String^ AccessibleName + { + Platform::String^ get() { return ref new Platform::String(m_original.accessibleName.c_str()); } + } + + property Platform::String^ Abbreviation + { + Platform::String^ get() { return ref new Platform::String(m_original.abbreviation.c_str()); } + } + + // This method is used to return the desired autonamtion name for default unit in UnitConveter combo box. + Platform::String^ ToString() override + { + return AccessibleName; + } + + internal: + const UnitConversionManager::Unit& GetModelUnit() const { return m_original; } + + private: + const UnitConversionManager::Unit m_original; + }; + + [Windows::UI::Xaml::Data::Bindable] + public ref class SupplementaryResult sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + internal: + SupplementaryResult(Platform::String^ value, Unit^ unit) : + m_Value(value), + m_Unit(unit) + {} + + bool IsWhimsical() const + { + return m_Unit->GetModelUnit().isWhimsical; + } + + Platform::String^ GetLocalizedAutomationName(); + + public: + OBSERVABLE_OBJECT(); + + OBSERVABLE_PROPERTY_R(Platform::String^, Value); + OBSERVABLE_PROPERTY_R(CalculatorApp::ViewModel::Unit^, Unit); + }; + + interface class IActivatable + { + virtual property bool IsActive; + }; + + template + ref class Activatable sealed : public IActivatable + { + private: + TActivatable m_activatable; + + public: + Activatable(TActivatable activatable) : m_activatable(activatable) + { } + + virtual property bool IsActive + { + bool get() { return m_activatable->IsActive; } + void set(bool value) { m_activatable->IsActive = value; } + } + }; + + template + IActivatable^ AsActivatable(TActivatable activatable) + { + return ref new Activatable(activatable); + } + + namespace UnitConverterViewModelProperties + { + extern Platform::StringReference CurrentCategory; + extern Platform::StringReference Unit1; + extern Platform::StringReference Unit2; + extern Platform::StringReference Value1Active; + extern Platform::StringReference Value2Active; + extern Platform::StringReference SupplementaryVisibility; + extern Platform::StringReference SupplementaryResults; + extern Platform::StringReference CurrencySymbol1; + extern Platform::StringReference CurrencySymbol2; + extern Platform::StringReference CurrencySymbolVisibility; + extern Platform::StringReference CurrencyRatioEquality; + extern Platform::StringReference NetworkBehavior; + extern Platform::StringReference CurrencyDataLoadFailed; + extern Platform::StringReference CurrencyDataIsWeekOld; + extern Platform::StringReference IsCurrencyLoadingVisible; + } + + [Windows::UI::Xaml::Data::Bindable] + public ref class UnitConverterViewModel sealed : public Windows::UI::Xaml::Data::INotifyPropertyChanged + { + internal: + UnitConverterViewModel(const std::shared_ptr& model); + + public: + OBSERVABLE_OBJECT_CALLBACK(OnPropertyChanged); + + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector^, Categories); + OBSERVABLE_PROPERTY_RW(Category^, CurrentCategory); + OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::ViewMode, Mode); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector^, Units); + OBSERVABLE_PROPERTY_RW(Platform::String^, CurrencySymbol1); + OBSERVABLE_PROPERTY_RW(Unit^, Unit1); + OBSERVABLE_PROPERTY_RW(Platform::String^, Value1); + OBSERVABLE_PROPERTY_RW(Platform::String^, CurrencySymbol2); + OBSERVABLE_PROPERTY_RW(Unit^, Unit2); + OBSERVABLE_PROPERTY_RW(Platform::String^, Value2); + OBSERVABLE_PROPERTY_R(Windows::Foundation::Collections::IObservableVector^, SupplementaryResults); + OBSERVABLE_PROPERTY_RW(bool, Value1Active); + OBSERVABLE_PROPERTY_RW(bool, Value2Active); + OBSERVABLE_PROPERTY_RW(Platform::String^, Value1AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, Value2AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, Unit1AutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, Unit2AutomationName); + OBSERVABLE_PROPERTY_RW(CalculatorApp::Common::Automation::NarratorAnnouncement^, Announcement); + OBSERVABLE_PROPERTY_RW(bool, IsDecimalEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsDropDownOpen); + OBSERVABLE_PROPERTY_RW(bool, IsDropDownEnabled); + OBSERVABLE_PROPERTY_RW(bool, IsCurrencyLoadingVisible); + OBSERVABLE_PROPERTY_RW(bool, IsCurrencyCurrentCategory); + OBSERVABLE_PROPERTY_RW(Platform::String^, CurrencyRatioEquality); + OBSERVABLE_PROPERTY_RW(Platform::String^, CurrencyRatioEqualityAutomationName); + OBSERVABLE_PROPERTY_RW(Platform::String^, CurrencyTimestamp); + OBSERVABLE_PROPERTY_RW(CalculatorApp::NetworkAccessBehavior, NetworkBehavior); + OBSERVABLE_PROPERTY_RW(bool, CurrencyDataLoadFailed); + OBSERVABLE_PROPERTY_RW(bool, CurrencyDataIsWeekOld); + + property Windows::UI::Xaml::Visibility SupplementaryVisibility + { + Windows::UI::Xaml::Visibility get() + { + return SupplementaryResults->Size > 0 + ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + property Windows::UI::Xaml::Visibility CurrencySymbolVisibility + { + Windows::UI::Xaml::Visibility get() + { + return (CurrencySymbol1->IsEmpty() || CurrencySymbol2->IsEmpty()) + ? Windows::UI::Xaml::Visibility::Collapsed + : Windows::UI::Xaml::Visibility::Visible; + } + } + + COMMAND_FOR_METHOD(CategoryChanged, UnitConverterViewModel::OnCategoryChanged); + COMMAND_FOR_METHOD(UnitChanged, UnitConverterViewModel::OnUnitChanged); + COMMAND_FOR_METHOD(SwitchActive, UnitConverterViewModel::OnSwitchActive); + COMMAND_FOR_METHOD(ButtonPressed, UnitConverterViewModel::OnButtonPressed); + COMMAND_FOR_METHOD(CopyCommand, UnitConverterViewModel::OnCopyCommand); + COMMAND_FOR_METHOD(PasteCommand, UnitConverterViewModel::OnPasteCommand); + + void AnnounceConversionResult(); + + internal: + void ResetView(); + void PopulateData(); + NumbersAndOperatorsEnum MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate); + void DisplayPasteError(); + void OnValueActivated(IActivatable^ control); + void OnPaste(Platform::String^ stringToPaste, CalculatorApp::Common::ViewMode mode); + + void OnCopyCommand(Platform::Object^ parameter); + void OnPasteCommand(Platform::Object^ parameter); + + Platform::String^ GetLocalizedAutomationName(_In_ Platform::String^ displayvalue, _In_ Platform::String^ unitname, _In_ Platform::String^ format); + Platform::String^ GetLocalizedConversionResultStringFormat(_In_ Platform::String^ fromValue, _In_ Platform::String^ fromUnit, _In_ Platform::String^ toValue, _In_ Platform::String^ toUnit); + void UpdateValue1AutomationName(); + void UpdateValue2AutomationName(); + Platform::String^ Serialize(); + void Deserialize(Platform::String^ state); + + //Saving And Restoring User Prefernces of Category and Associated-Units across Sessions. + void SaveUserPreferences(); + void RestoreUserPreferences(); + + void OnCurrencyDataLoadFinished(bool didLoad); + void OnCurrencyTimestampUpdated(_In_ const std::wstring& timestamp, bool isWeekOld); + void RefreshCurrencyRatios(); + void OnNetworkBehaviorChanged(_In_ CalculatorApp::NetworkAccessBehavior newBehavior); + + const std::wstring& GetValueFromUnlocalized() const + { + return m_valueFromUnlocalized; + } + const std::wstring& GetValueToUnlocalized() const + { + return m_valueToUnlocalized; + } + + // used by UnitConverterVMCallback + void UpdateDisplay(const std::wstring& from, const std::wstring& to); + void UpdateSupplementaryResults(const std::vector>& suggestedValues); + void OnMaxDigitsReached(); + + void BuildUnitList(const std::vector& modelUnitList); + Unit^ FindUnitInList(UnitConversionManager::Unit target); + void SetSelectedUnits(); + + private: + void InitializeView(); + void OnPropertyChanged(Platform::String^ prop); + void OnCategoryChanged(Platform::Object^ unused); + void OnUnitChanged(Platform::Object^ unused); + void OnSwitchActive(Platform::Object^ unused); + UnitConversionManager::Command CommandFromButtonId(CalculatorApp::NumbersAndOperatorsEnum button); + void SupplementaryResultsTimerTick(Windows::System::Threading::ThreadPoolTimer^ timer); + void SupplementaryResultsTimerCancel(Windows::System::Threading::ThreadPoolTimer^ timer); + void RefreshSupplementaryResults(); + void UpdateInputBlocked(_In_ const std::wstring& currencyInput); + bool UnitsAreValid(); + + void OnButtonPressed(Platform::Object^ parameter); + Platform::String^ ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings); + + void StartConversionResultTimer(); + + std::shared_ptr m_model; + wchar_t m_decimalSeparator; + + enum class ConversionParameter + { + Source, + Target + } m_value1cp; + property Platform::String^ ValueFrom + { + Platform::String^ get() { return m_value1cp == ConversionParameter::Source ? Value1 : Value2; } + void set(Platform::String^ value) { m_value1cp == ConversionParameter::Source ? Value1 = value : Value2 = value; } + } + property Unit^ UnitFrom + { + Unit^ get() { return m_value1cp == ConversionParameter::Source ? Unit1 : Unit2; } + void set(Unit^ value) { m_value1cp == ConversionParameter::Source ? Unit1 = value : Unit2 = value; } + } + property Platform::String^ ValueTo + { + Platform::String^ get() { return m_value1cp == ConversionParameter::Target ? Value1 : Value2; } + void set(Platform::String^ value) { m_value1cp == ConversionParameter::Target ? Value1 = value : Value2 = value; } + } + property Unit^ UnitTo + { + Unit^ get() { return m_value1cp == ConversionParameter::Target ? Unit1 : Unit2; } + void set(Unit^ value) { m_value1cp == ConversionParameter::Target ? Unit1 = value : Unit2 = value; } + } + void SwitchConversionParameters() + { + m_value1cp = m_value1cp == ConversionParameter::Source ? ConversionParameter::Target : ConversionParameter::Source; + } + + private: + bool m_isInputBlocked; + Windows::System::Threading::ThreadPoolTimer^ m_supplementaryResultsTimer; + bool m_resettingTimer; + std::vector> m_cachedSuggestedValues; + std::mutex m_cacheMutex; + Windows::Globalization::NumberFormatting::DecimalFormatter^ m_decimalFormatter; + Windows::Globalization::NumberFormatting::CurrencyFormatter^ m_currencyFormatter; + int m_currencyMaxFractionDigits; + std::wstring m_valueFromUnlocalized; + std::wstring m_valueToUnlocalized; + bool m_relocalizeStringOnSwitch; + // For Saving the User Preferences only if the Unit convertyer ViewModel is initialised for the first time + bool m_IsFirstTime; + + Platform::String^ m_localizedValueFromFormat; + Platform::String^ m_localizedValueFromDecimalFormat; + Platform::String^ m_localizedValueToFormat; + Platform::String^ m_localizedConversionResultFormat; + Platform::String^ m_localizedInputUnitName; + Platform::String^ m_localizedOutputUnitName; + + bool m_isValue1Updating; + bool m_isValue2Updating; + std::wstring m_lastAnnouncedFrom; + std::wstring m_lastAnnouncedTo; + Platform::String^ m_lastAnnouncedConversionResult; + + bool m_isCurrencyDataLoaded; + + std::unique_ptr m_conversionResultTaskHelper; + }; + + class UnitConverterVMCallback : public UnitConversionManager::IUnitConverterVMCallback + { + public: + UnitConverterVMCallback(UnitConverterViewModel^ viewModel) : m_viewModel(viewModel) + {} + + void DisplayCallback(const std::wstring& from, const std::wstring& to) override + { + m_viewModel->UpdateDisplay(from, to); + } + + void SuggestedValueCallback( + const std::vector>& suggestedValues) override + { + m_viewModel->UpdateSupplementaryResults(suggestedValues); + } + + void MaxDigitsReached() + { + m_viewModel->OnMaxDigitsReached(); + } + + private: + UnitConverterViewModel^ m_viewModel; + }; + + class ViewModelCurrencyCallback : public UnitConversionManager::IViewModelCurrencyCallback + { + public: + ViewModelCurrencyCallback(UnitConverterViewModel^ viewModel) : m_viewModel(viewModel) + {} + + void CurrencyDataLoadFinished(bool didLoad) override + { + m_viewModel->OnCurrencyDataLoadFinished(didLoad); + } + + void CurrencySymbolsCallback(const std::wstring& symbol1, const std::wstring& symbol2) override + { + Platform::String^ sym1 = Platform::StringReference(symbol1.c_str()); + Platform::String^ sym2 = Platform::StringReference(symbol2.c_str()); + + bool value1Active = m_viewModel->Value1Active; + m_viewModel->CurrencySymbol1 = value1Active ? sym1 : sym2; + m_viewModel->CurrencySymbol2 = value1Active ? sym2 : sym1; + } + + void CurrencyRatiosCallback(_In_ const std::wstring& ratioEquality, _In_ const std::wstring& accRatioEquality) override + { + m_viewModel->CurrencyRatioEquality = ref new Platform::String(ratioEquality.c_str()); + m_viewModel->CurrencyRatioEqualityAutomationName = ref new Platform::String(accRatioEquality.c_str()); + } + + void CurrencyTimestampCallback(_In_ const std::wstring& timestamp, bool isWeekOld) override + { + m_viewModel->OnCurrencyTimestampUpdated(timestamp, isWeekOld); + } + + void NetworkBehaviorChanged(_In_ int newBehavior) override + { + m_viewModel->OnNetworkBehaviorChanged(static_cast(newBehavior)); + } + + private: + UnitConverterViewModel^ m_viewModel; + }; + } +} diff --git a/src/CalcViewModel/ViewState.cpp b/src/CalcViewModel/ViewState.cpp new file mode 100644 index 00000000..cadfc299 --- /dev/null +++ b/src/CalcViewModel/ViewState.cpp @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" +#include "ViewState.h" + +namespace CalculatorApp +{ + namespace ViewState + { + Platform::StringReference Snap(L"Snap"); + Platform::StringReference DockedView(L"DockedView"); + + bool IsValidViewState(Platform::String^ viewState) + { + return viewState->Equals(ViewState::Snap) || viewState->Equals(ViewState::DockedView); + } + } +} diff --git a/src/CalcViewModel/ViewState.h b/src/CalcViewModel/ViewState.h new file mode 100644 index 00000000..8b70cff0 --- /dev/null +++ b/src/CalcViewModel/ViewState.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace CalculatorApp +{ + namespace ViewState + { + extern Platform::StringReference Snap; + extern Platform::StringReference DockedView; + + bool IsValidViewState(Platform::String^ viewState); + } +} diff --git a/src/CalcViewModel/packages.config b/src/CalcViewModel/packages.config new file mode 100644 index 00000000..cef4e13e --- /dev/null +++ b/src/CalcViewModel/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/CalcViewModel/pch.cpp b/src/CalcViewModel/pch.cpp new file mode 100644 index 00000000..7c5e1feb --- /dev/null +++ b/src/CalcViewModel/pch.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + diff --git a/src/CalcViewModel/pch.h b/src/CalcViewModel/pch.h new file mode 100644 index 00000000..33dba54a --- /dev/null +++ b/src/CalcViewModel/pch.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + + +#include "targetver.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// C++\WinRT Headers +#include "winrt\base.h" +#include "winrt\Windows.Foundation.Diagnostics.h" +#include "winrt\Windows.Globalization.h" +#include "winrt\Windows.Globalization.DateTimeFormatting.h" +#include "winrt\Windows.System.UserProfile.h" +#include "winrt\Windows.UI.Xaml.h" + +// The following namespaces exist as a convenience to resolve +// ambiguity for Windows types in the Windows::UI::Xaml::Automation::Peers +// namespace that only exist on RS3. +// Once the app switches to min version RS3, the namespaces can be removed. +// TODO - MSFT 12735088 +namespace StandardPeers = Windows::UI::Xaml::Automation::Peers; +namespace CalculatorApp::Common::Automation {} +namespace CustomPeers = CalculatorApp::Common::Automation; + +// CalcManager Headers +#include "CalcManager\CalculatorVector.h" +#include "CalcManager\ExpressionCommand.h" +#include "CalcManager\CalculatorResource.h" +#include "CalcManager\CalculatorManager.h" +#include "CalcManager\UnitConverter.h" + +// Project Headers +#include "Common\DelegateCommand.h" +#include "Common\Utils.h" +#include "Common\MyVirtualKey.h" +#include "Common\NavCategory.h" +#include "Common\TraceLogger.h" +#include "Common\CalculatorButtonUser.h" + diff --git a/src/CalcViewModel/targetver.h b/src/CalcViewModel/targetver.h new file mode 100644 index 00000000..18b255ab --- /dev/null +++ b/src/CalcViewModel/targetver.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include + diff --git a/src/Calculator.sln b/src/Calculator.sln new file mode 100644 index 00000000..cfcae417 --- /dev/null +++ b/src/Calculator.sln @@ -0,0 +1,110 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Calculator", "Calculator\Calculator.vcxproj", "{9447424A-0E05-4911-BEB8-E0354405F39A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalcManager", "CalcManager\CalcManager.vcxproj", "{311E866D-8B93-4609-A691-265941FEE101}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3A5DF651-B8A1-45CA-9135-964A6FC7F5D1}" + ProjectSection(SolutionItems) = preProject + nuget.config = nuget.config + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalcViewModel", "CalcViewModel\CalcViewModel.vcxproj", "{90E9761D-9262-4773-942D-CAEAE75D7140}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalculatorUnitTests_VS", "CalculatorUnitTests_VS\CalculatorUnitTests_VS.vcxproj", "{D3BAED2C-4B07-4E1D-8807-9D6499450349}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.ActiveCfg = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM.Build.0 = Debug|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|ARM64.Build.0 = Debug|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.ActiveCfg = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x64.Build.0 = Debug|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.ActiveCfg = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Debug|x86.Build.0 = Debug|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.ActiveCfg = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM.Build.0 = Release|ARM + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.ActiveCfg = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|ARM64.Build.0 = Release|ARM64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.ActiveCfg = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x64.Build.0 = Release|x64 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.ActiveCfg = Release|Win32 + {311E866D-8B93-4609-A691-265941FEE101}.Release|x86.Build.0 = Release|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.ActiveCfg = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Build.0 = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM.Deploy.0 = Debug|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.Build.0 = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.ActiveCfg = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.Build.0 = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x64.Deploy.0 = Debug|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.ActiveCfg = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.Build.0 = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Debug|x86.Deploy.0 = Debug|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.ActiveCfg = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.Build.0 = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM.Deploy.0 = Release|ARM + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.ActiveCfg = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.Build.0 = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|ARM64.Deploy.0 = Release|ARM64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.ActiveCfg = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.Build.0 = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x64.Deploy.0 = Release|x64 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.ActiveCfg = Release|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Build.0 = Release|Win32 + {9447424A-0E05-4911-BEB8-E0354405F39A}.Release|x86.Deploy.0 = Release|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.ActiveCfg = Debug|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM.Build.0 = Debug|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|ARM64.Build.0 = Debug|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x64.ActiveCfg = Debug|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x64.Build.0 = Debug|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x86.ActiveCfg = Debug|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Debug|x86.Build.0 = Debug|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM.ActiveCfg = Release|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM.Build.0 = Release|ARM + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM64.ActiveCfg = Release|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|ARM64.Build.0 = Release|ARM64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x64.ActiveCfg = Release|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x64.Build.0 = Release|x64 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x86.ActiveCfg = Release|Win32 + {90E9761D-9262-4773-942D-CAEAE75D7140}.Release|x86.Build.0 = Release|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|ARM.ActiveCfg = Debug|ARM + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x64.ActiveCfg = Debug|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x64.Build.0 = Debug|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x64.Deploy.0 = Debug|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x86.ActiveCfg = Debug|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x86.Build.0 = Debug|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Debug|x86.Deploy.0 = Debug|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|ARM.ActiveCfg = Release|ARM + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|ARM64.ActiveCfg = Release|ARM64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x64.ActiveCfg = Release|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x64.Build.0 = Release|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x64.Deploy.0 = Release|x64 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.ActiveCfg = Release|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.Build.0 = Release|Win32 + {D3BAED2C-4B07-4E1D-8807-9D6499450349}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0EA53F83-5FA6-46A1-A290-A3C6962D2CAC} + EndGlobalSection +EndGlobal diff --git a/src/Calculator/AboutFlyout.xaml b/src/Calculator/AboutFlyout.xaml new file mode 100644 index 00000000..cfce6fbc --- /dev/null +++ b/src/Calculator/AboutFlyout.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +