diff --git a/.github/workflows/SignClientFileList.txt b/.github/workflows/SignClientFileList.txt new file mode 100644 index 0000000000..1a17866f45 --- /dev/null +++ b/.github/workflows/SignClientFileList.txt @@ -0,0 +1 @@ +**/CommunityToolkit.* \ No newline at end of file diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml new file mode 100644 index 0000000000..1bf69e094f --- /dev/null +++ b/.github/workflows/dotnet-build.yml @@ -0,0 +1,270 @@ +name: .NET + +on: + push: + branches: + - main + tags: + - '*' + paths-ignore: + - README.md + pull_request: + branches: + - main + paths-ignore: + - README.md + +env: + CurrentSemanticVersionBase: '99.0.0' + PreviewNumber: ${{ github.run_number }} + CurrentSemanticVersion: '99.0.0-preview${{ github.run_number }}' + NugetPackageVersion: '99.0.0-preview${{ github.run_number }}' + NugetPackageVersionCamera: '99.0.0-preview${{ github.run_number }}' + NugetPackageVersionMediaElement: '99.0.0-preview${{ github.run_number }}' + NugetPackageVersionMaps: '99.0.0-preview${{ github.run_number }}' + TOOLKIT_NET_VERSION: '9.0.102' + LATEST_NET_VERSION: '9.0.x' + PathToLibrarySolution: 'src/CommunityToolkit.Maui.sln' + PathToSamplesSolution: 'samples/CommunityToolkit.Maui.Sample.sln' + PathToCommunityToolkitCsproj: 'src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj' + PathToCommunityToolkitCoreCsproj: 'src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj' + PathToCommunityToolkitCameraCsproj: 'src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj' + PathToCommunityToolkitMediaElementCsproj: 'src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj' + PathToCommunityToolkitMapsCsproj: 'src/CommunityToolkit.Maui.Maps/CommunityToolkit.Maui.Maps.csproj' + PathToCommunityToolkitSampleCsproj: 'samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj' + PathToCommunityToolkitUnitTestCsproj: 'src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj' + PathToCommunityToolkitAnalyzersCsproj: 'src/CommunityToolkit.Maui.Analyzers/CommunityToolkit.Maui.Analyzers.csproj' + PathToCommunityToolkitCameraAnalyzersCsproj: 'src/CommunityToolkit.Maui.Camera.Analyzers/CommunityToolkit.Maui.Camera.Analyzers.csproj' + PathToCommunityToolkitMediaElementAnalyzersCsproj: 'src/CommunityToolkit.Maui.MediaElement.Analyzers/CommunityToolkit.Maui.MediaElement.Analyzers.csproj' + PathToCommunityToolkitSourceGeneratorsCsproj: 'src/CommunityToolkit.Maui.SourceGenerators/CommunityToolkit.Maui.SourceGenerators.csproj' + PathToCommunityToolkitSourceGeneratorsInternalCsproj: 'src/CommunityToolkit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj' + PathToCommunityToolkitAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.Analyzers.CodeFixes/CommunityToolkit.Maui.Analyzers.CodeFixes.csproj' + PathToCommunityToolkitCameraAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes.csproj' + PathToCommunityToolkitMediaElementAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes.csproj' + PathToCommunityToolkitAnalyzersUnitTestCsproj: 'src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj' + DotNetMauiRollbackFile: 'https://maui.blob.core.windows.net/metadata/rollbacks/8.0.6.json' + CommunityToolkitSampleApp_Xcode_Version: '16.2' + CommunityToolkitLibrary_Xcode_Version: '16.2' + +jobs: + build_sample: + name: Build Sample App using Latest .NET SDK + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, macos-15] + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Set Xcode version + if: runner.os == 'macOS' + run: | + sudo xcode-select --switch /Applications/Xcode_${{ env.CommunityToolkitSampleApp_Xcode_Version }}.app/Contents/Developer + + - name: Install Latest .NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.LATEST_NET_VERSION }} + + - name: Install .NET MAUI Workload + run: dotnet workload install maui + + - name: Install Tizen Workload + run: | + Invoke-WebRequest 'https://raw.githubusercontent.com/Samsung/Tizen.NET/main/workload/scripts/workload-install.ps1' -OutFile 'workload-install.ps1' + .\workload-install.ps1 + shell: pwsh + + - name: Display dotnet info + run: dotnet --info + + - name: Build Community Toolkit Sample + run: dotnet build -c Release ${{ env.PathToCommunityToolkitSampleCsproj }} + + build_library: + name: Build Library + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, macos-15] + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Set NuGet Version to Tag Number + if: startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, '-mediaelement') + run: echo "NugetPackageVersion=${{ github.ref }}" >> $GITHUB_ENV + + - name: Set NuGet Version to Tag Number for Camera + if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-camera') + run: echo "NugetPackageVersionCamera=${{ github.ref }}" >> $GITHUB_ENV + + - name: Set NuGet Version to Tag Number for MediaElement + if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-mediaelement') + run: echo "NugetPackageVersionMediaElement=${{ github.ref }}" >> $GITHUB_ENV + + - name: Set NuGet Version to Tag Number for Maps + if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-maps') + run: echo "NugetPackageVersionMaps=${{ github.ref }}" >> $GITHUB_ENV + + - name: Set NuGet Version to PR Version + if: ${{ github.event_name == 'pull_request' }} + run: | + echo "NugetPackageVersion=${{ env.CurrentSemanticVersionBase }}-build-${{ github.event.pull_request.number }}.${{ github.run_number }}+${{ github.sha }}" + echo "NugetPackageVersionMediaElement=${{ env.CurrentSemanticVersionBase }}-build-${{ github.event.pull_request.number }}.${{ github.run_number }}+${{ github.sha }}" + echo "NugetPackageVersionMaps=${{ env.CurrentSemanticVersionBase }}-build-${{ github.event.pull_request.number }}.${{ github.run_number }}+${{ github.sha }}" + shell: bash + + - name: Set Xcode version + if: runner.os == 'macOS' + run: | + sudo xcode-select --switch /Applications/Xcode_${{ env.CommunityToolkitLibrary_Xcode_Version }}.app/Contents/Developer + + - name: Install .NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.TOOLKIT_NET_VERSION }} + + - name: Install .NET MAUI Workload + run: dotnet workload install maui --skip-sign-check --source https://api.nuget.org/v3/index.json + + - name: Install Tizen Workload + run: | + Invoke-WebRequest 'https://raw.githubusercontent.com/Samsung/Tizen.NET/main/workload/scripts/workload-install.ps1' -OutFile 'workload-install.ps1' + .\workload-install.ps1 + shell: pwsh + + - name: Display dotnet info + run: dotnet --info + + - name: Build CommunityToolkit.Maui + run: dotnet build -c Release ${{ env.PathToLibrarySolution }} + + - name: Run CommunityToolkit.Maui.UnitTests + run: dotnet test -c Release ${{ env.PathToLibrarySolution }} --settings ".runsettings" --collect "XPlat code coverage" --logger trx --results-directory ${{ runner.temp }} + + - name: Publish Test Results + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: Test Results + path: | + ${{ runner.temp }}/**/*.trx + + - name: Pack CommunityToolkit.Maui.Core NuGet + run: dotnet pack -c Release ${{ env.PathToCommunityToolkitCoreCsproj }} -p:PackageVersion=${{ env.NugetPackageVersion }} + + - name: Pack CommunityToolkit.Maui NuGet + run: dotnet pack -c Release ${{ env.PathToCommunityToolkitCsproj }} -p:PackageVersion=${{ env.NugetPackageVersion }} + + - name: Pack CommunityToolkit.Maui.Camera NuGet + run: dotnet pack -c Release ${{ env.PathToCommunityToolkitCameraCsproj }} -p:PackageVersion=${{ env.NugetPackageVersionCamera }} + + - name: Pack CommunityToolkit.Maui.MediaElement NuGet + run: dotnet pack -c Release ${{ env.PathToCommunityToolkitMediaElementCsproj }} -p:PackageVersion=${{ env.NugetPackageVersionMediaElement }} + + - name: Pack CommunityToolkit.Maui.Maps NuGet + run: dotnet pack -c Release ${{ env.PathToCommunityToolkitMapsCsproj }} -p:PackageVersion=${{ env.NugetPackageVersionMaps }} + + - name: Copy NuGet Packages to Staging Directory + if: runner.os == 'Windows' && !startsWith(github.ref, 'refs/tags/') + run: | + mkdir -p ${{ github.workspace }}/nuget + Get-ChildItem -Path "./src" -Recurse | Where-Object { $_.Extension -match "nupkg" } | Copy-Item -Destination "${{ github.workspace }}/nuget" + shell: pwsh + + - name: Upload Package List + uses: actions/upload-artifact@v4 + if: runner.os == 'Windows' + with: + name: nuget-list + if-no-files-found: error + path: | + ${{ github.workspace }}/.github/workflows/SignClientFileList.txt + + - name: Publish Packages + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: packages + path: ${{ github.workspace }}/nuget/ + + sign: + needs: [build_library] + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/rel/') }} + runs-on: windows-latest + permissions: + id-token: write # Required for requesting the JWT + + steps: + - name: Install .NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.TOOLKIT_NET_VERSION }} + + - name: Download NuGet List + uses: actions/download-artifact@v4 + with: + name: nuget-list + path: ./ + + - name: Download Package List + uses: actions/download-artifact@v4 + with: + name: packages + path: ./packages + + - name: Install Signing Tool + run: dotnet tool install --tool-path ./tools sign --version 0.9.1-beta.23356.1 + + - name: Sign Packages + run: > + ./tools/sign code azure-key-vault + **/*.nupkg + --base-directory "${{ github.workspace }}/packages" + --file-list "${{ github.workspace }}/SignClientFileList.txt" + --timestamp-url "http://timestamp.digicert.com" + --publisher-name ".NET Foundation" + --description "Community Toolkit MAUI" + --description-url "https://github.com/CommunityToolkit/Maui" + --azure-key-vault-url "${{ secrets.SIGN_KEY_VAULT_URL }}" + --azure-key-vault-client-id ${{ secrets.SIGN_CLIENT_ID }} + --azure-key-vault-client-secret "${{ secrets.SIGN_CLIENT_SECRET }}" + --azure-key-vault-tenant-id ${{ secrets.SIGN_TENANT_ID }} + --azure-key-vault-certificate "${{ secrets.SIGN_CERTIFICATE }}" + --verbosity Information + + - name: Publish Packages + uses: actions/upload-artifact@v4 + with: + name: signed-packages + if-no-files-found: error + path: | + ${{ github.workspace }}/packages/**/*.nupkg + + release: + if: ${{ startsWith(github.ref, 'refs/heads/rel/') }} + needs: [sign] + environment: nuget-release-gate # This gates this job until manually approved + runs-on: ubuntu-latest + + steps: + - name: Install .NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.TOOLKIT_NET_VERSION }} + + - name: Download signed packages for ${{ matrix.platform }} + uses: actions/download-artifact@v4 + with: + name: signed-packages + path: ./packages + + - name: Push to NuGet.org + run: > + dotnet nuget push + **/*.nupkg + --source https://api.nuget.org/v3/index.json + --api-key ${{ secrets.NUGET_PACKAGE_PUSH_TOKEN }} + --skip-duplicate diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4bd5000e1f..d528d40c3f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,13 +29,13 @@ variables: PathToCommunityToolkitAnalyzersBenchmarkCsproj: 'src/CommunityToolkit.Maui.Analyzers.Benchmarks/CommunityToolkit.Maui.Analyzers.Benchmarks.csproj' DotNetMauiRollbackFile: 'https://maui.blob.core.windows.net/metadata/rollbacks/8.0.6.json' CommunityToolkitSampleApp_Xcode_Version: '16.2' - CommunityToolkitLibrary_Xcode_Version: '16.1' + CommunityToolkitLibrary_Xcode_Version: '16.2' trigger: - branches: - include: - - main - - develop + # branches: + # include: + # - main + # - develop tags: include: - '*' @@ -43,15 +43,15 @@ trigger: exclude: - README.md -pr: - autoCancel: 'true' - branches: - include: - - main - - develop - paths: - exclude: - - README.md +# pr: +# autoCancel: 'true' +# branches: +# include: +# - main +# - develop +# paths: +# exclude: +# - README.md jobs: - job: build_sample @@ -323,7 +323,7 @@ jobs: testResultsFiles: '**/*.trx' searchFolder: $(Agent.TempDirectory) - - task: PublishCodeCoverageResults@2 + - task: PublishCodeCoverageResults@1 condition: eq(variables['Agent.OS'], 'Windows_NT') # Only run this step on Windows displayName: 'Publish Code Coverage Results' inputs: diff --git a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs index 92bca3cec4..b2da7e3fab 100644 --- a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs @@ -191,9 +191,11 @@ void SetupNavigationView() #if WINDOWS Loaded += delegate { - var navigationView = (Microsoft.UI.Xaml.Controls.NavigationView)flyout.Handler!.PlatformView!; - navigationView.IsPaneToggleButtonVisible = true; - navigationView.PaneDisplayMode = Microsoft.UI.Xaml.Controls.NavigationViewPaneDisplayMode.Auto; + if(flyout.Handler?.PlatformView is Microsoft.UI.Xaml.Controls.NavigationView navigationView) + { + navigationView.IsPaneToggleButtonVisible = true; + navigationView.PaneDisplayMode = Microsoft.UI.Xaml.Controls.NavigationViewPaneDisplayMode.Auto; + } }; #endif } diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs index e659b1568e..c88679bddc 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs @@ -3,12 +3,9 @@ namespace CommunityToolkit.Maui.Sample.Pages; -public abstract class BasePage : BasePage where TViewModel : BaseViewModel +public abstract class BasePage(TViewModel viewModel) : BasePage(viewModel) + where TViewModel : BaseViewModel { - protected BasePage(TViewModel viewModel) : base(viewModel) - { - } - public new TViewModel BindingContext => (TViewModel)base.BindingContext; } diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/StatusBarBehaviorPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/StatusBarBehaviorPage.xaml index f86bf5f884..7698437c54 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/StatusBarBehaviorPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/StatusBarBehaviorPage.xaml @@ -6,6 +6,8 @@ xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages" xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Behaviors" + xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls" + ios:Page.UseSafeArea="True" Title="StatusBarBehavior" x:DataType="vm:StatusBarBehaviorViewModel" x:TypeArguments="vm:StatusBarBehaviorViewModel" @@ -81,7 +83,7 @@ - + + \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj b/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj index 16fbe3e385..da1623e571 100644 --- a/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj +++ b/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileFolderDialog.tizen.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileFolderDialog.tizen.cs index 4bd1852ff7..940906a2fd 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileFolderDialog.tizen.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileFolderDialog.tizen.cs @@ -336,7 +336,12 @@ void ProcessSelect(string selectedItem) } else if (isFileSelectionMode) { - fileNameEntry!.Text = selectedItem; + if(fileNameEntry is null) + { + throw new InvalidOperationException($"{nameof(fileNameEntry)} cannot be null"); + } + + fileNameEntry.Text = selectedItem; } } diff --git a/src/CommunityToolkit.Maui.Core/Platform/StatusBar/StatusBar.ios.cs b/src/CommunityToolkit.Maui.Core/Platform/StatusBar/StatusBar.ios.cs index a4403260aa..82066d006d 100644 --- a/src/CommunityToolkit.Maui.Core/Platform/StatusBar/StatusBar.ios.cs +++ b/src/CommunityToolkit.Maui.Core/Platform/StatusBar/StatusBar.ios.cs @@ -11,91 +11,73 @@ static partial class StatusBar /// /// Method to update the status bar size. /// - public static void UpdateBarSize() + public static void SetBarSize(bool isUsingSafeArea) { - if (OperatingSystem.IsIOSVersionAtLeast(13)) + var communityToolkitStatusBarTag = new IntPtr(38482); + foreach (var window in UIApplication.SharedApplication.Windows) { - var statusBarTag = new IntPtr(38482); - foreach (var window in UIApplication.SharedApplication.Windows) + var statusBarFrame = window.WindowScene?.StatusBarManager?.StatusBarFrame; + if (statusBarFrame is null) { - var statusBar = window.ViewWithTag(statusBarTag); - var statusBarFrame = window.WindowScene?.StatusBarManager?.StatusBarFrame; - if (statusBarFrame is null) - { - continue; - } - - statusBar ??= new UIView(statusBarFrame.Value); - statusBar.Tag = statusBarTag; - statusBar.Frame = UIApplication.SharedApplication.StatusBarFrame; - var statusBarSubViews = window.Subviews.Where(x => x.Tag == statusBarTag).ToList(); - foreach (var statusBarSubView in statusBarSubViews) - { - statusBarSubView.RemoveFromSuperview(); - } - - window.AddSubview(statusBar); - - TryUpdateStatusBarAppearance(window); + continue; } - } - else - { - if (UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) is UIView statusBar) + + var statusBar = window.ViewWithTag(communityToolkitStatusBarTag) ?? new UIView(statusBarFrame.Value); + statusBar.Tag = communityToolkitStatusBarTag; + statusBar.Frame = GetStatusBarFrame(window, isUsingSafeArea); + + var statusBarSubViews = window.Subviews.Where(x => x.Tag == communityToolkitStatusBarTag).ToList(); + foreach (var statusBarSubView in statusBarSubViews) { - statusBar.Frame = UIApplication.SharedApplication.StatusBarFrame; + statusBarSubView.RemoveFromSuperview(); } - TryUpdateStatusBarAppearance(); + window.AddSubview(statusBar); + + TryUpdateStatusBarAppearance(window); } } static void PlatformSetColor(Color color) { var uiColor = color.ToPlatform(); - - if (OperatingSystem.IsIOSVersionAtLeast(13)) + + var statusBarTag = new IntPtr(38482); + foreach (var window in UIApplication.SharedApplication.Windows) { - var statusBarTag = new IntPtr(38482); - foreach (var window in UIApplication.SharedApplication.Windows) + var statusBar = window.ViewWithTag(statusBarTag); + var statusBarFrame = window.WindowScene?.StatusBarManager?.StatusBarFrame; + if (statusBarFrame is null) { - var statusBar = window.ViewWithTag(statusBarTag); - var statusBarFrame = window.WindowScene?.StatusBarManager?.StatusBarFrame; - if (statusBarFrame is null) - { - continue; - } - - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - // window.ViewWithTag(tag) can return null - statusBar ??= new UIView(statusBarFrame.Value); - statusBar.Tag = statusBarTag; - statusBar.BackgroundColor = uiColor; - statusBar.TintColor = uiColor; - statusBar.Frame = UIApplication.SharedApplication.StatusBarFrame; - var statusBarSubViews = window.Subviews.Where(x => x.Tag == statusBarTag).ToList(); - foreach (var statusBarSubView in statusBarSubViews) - { - statusBarSubView.RemoveFromSuperview(); - } - - window.AddSubview(statusBar); - - TryUpdateStatusBarAppearance(window); + continue; } - } - else - { - if (UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) is UIView statusBar - && statusBar.RespondsToSelector(new ObjCRuntime.Selector("setBackgroundColor:"))) + + statusBar ??= new UIView(statusBarFrame.Value); + statusBar.Tag = statusBarTag; + statusBar.BackgroundColor = uiColor; + statusBar.TintColor = uiColor; + + var statusBarSubViews = window.Subviews.Where(x => x.Tag == statusBarTag).ToList(); + foreach (var statusBarSubView in statusBarSubViews) { - statusBar.BackgroundColor = uiColor; + statusBarSubView.RemoveFromSuperview(); } - TryUpdateStatusBarAppearance(); + window.AddSubview(statusBar); + + TryUpdateStatusBarAppearance(window); } } + static CGRect GetStatusBarFrame(in UIWindow window, in bool isUsingSafeArea) + { + var statusBarFrame = UIApplication.SharedApplication.StatusBarFrame; + + return isUsingSafeArea + ? new CGRect(statusBarFrame.X, statusBarFrame.Y, statusBarFrame.Width, window.SafeAreaInsets.Top) + : statusBarFrame; + } + static void PlatformSetStyle(StatusBarStyle statusBarStyle) { var uiStyle = statusBarStyle switch @@ -113,22 +95,14 @@ static void PlatformSetStyle(StatusBarStyle statusBarStyle) static bool TryUpdateStatusBarAppearance() { - if (OperatingSystem.IsIOSVersionAtLeast(13)) - { - var didUpdateAllStatusBars = true; + var didUpdateAllStatusBars = true; - foreach (var window in UIApplication.SharedApplication.Windows) - { - didUpdateAllStatusBars &= TryUpdateStatusBarAppearance(window); - } - - return didUpdateAllStatusBars; - } - else + foreach (var window in UIApplication.SharedApplication.Windows) { - var window = UIApplication.SharedApplication.KeyWindow; - return TryUpdateStatusBarAppearance(window); + didUpdateAllStatusBars &= TryUpdateStatusBarAppearance(window); } + + return didUpdateAllStatusBars; } static bool TryUpdateStatusBarAppearance(UIWindow? window) diff --git a/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs index 22d0f1de97..3e7cdf15f2 100644 --- a/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs @@ -14,7 +14,7 @@ public enum MathOperatorPrecedence /// High High, /// Constant - Constant, + Constant } /// @@ -25,14 +25,13 @@ public enum MathOperatorPrecedence /// /// Name /// Number of Numerals -/// Math Operator Preference /// Calculation Function public sealed class MathOperator( string name, int numericCount, - MathOperatorPrecedence precedence, - Func calculateFunc) + Func calculateFunc) { + /// /// Name /// @@ -43,13 +42,8 @@ public sealed class MathOperator( /// public int NumericCount { get; } = numericCount; - /// - /// Math Operator Precedence - /// - public MathOperatorPrecedence Precedence { get; } = precedence; - /// /// Calculation Function /// - public Func CalculateFunc { get; } = calculateFunc; + public Func CalculateFunc { get; } = calculateFunc; } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs index 771c9231ff..a6ab9e45d6 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs @@ -29,8 +29,33 @@ public void MathExpressionConverter_ReturnsCorrectResult(string expression, doub var convertResult = ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(x, mathExpressionTargetType, expression, cultureInfo) ?? throw new NullReferenceException(); var convertFromResult = mathExpressionConverter.ConvertFrom(x, expression); + Assert.True(convertFromResult is not null); Assert.True(Math.Abs((double)convertResult - expectedResult) < tolerance); - Assert.True(Math.Abs(convertFromResult - expectedResult) < tolerance); + Assert.True(Math.Abs((double)convertFromResult - expectedResult) < tolerance); + } + + [Theory] + [InlineData("3 < x", 2d, false)] + [InlineData("x > 3", 2d, false)] + [InlineData("3 < x == x > 3", 2d, true)] + [InlineData("3 <= x != 3 >= x", 2d, true)] + [InlineData("x >= 1", 2d, true)] + [InlineData("x <= 3", 2d, true)] + [InlineData("x >= 1 && (x <= 3 || x >= 0)", 2d, true)] + [InlineData("true", 2d, true)] + [InlineData("false", 2d, false)] + [InlineData("-x > 2", 3d, false)] + [InlineData("!!! (---x > 2)", 3d, true)] + public void MathExpressionConverter_WithComparisonOperator_ReturnsCorrectBooleanResult(string expression, double x, bool expectedResult) + { + var mathExpressionConverter = new MathExpressionConverter(); + + var convertResult = ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(x, mathExpressionTargetType, expression, cultureInfo) ?? throw new NullReferenceException(); + var convertFromResult = mathExpressionConverter.ConvertFrom(x, expression); + + Assert.True(convertFromResult is not null); + Assert.True((bool)convertResult == expectedResult); + Assert.True((bool)convertFromResult == expectedResult); } [Theory] @@ -38,31 +63,166 @@ public void MathExpressionConverter_ReturnsCorrectResult(string expression, doub [InlineData("(x1 + x) * x1", new object[] { 2d, 3d }, 15d)] [InlineData("3 + x * x1 / (1 - 5)^x1", new object[] { 4d, 2d }, 3.5d)] [InlineData("3 + 4 * 2 + cos(100 + x) / (x1 - 5)^2 + pow(x0, 2)", new object[] { 20d, 1d }, 411.05088631065792d)] - public void MathExpressionConverter_WithMultiplyVariable_ReturnsCorrectResult(string expression, object[] variables, double expectedResult) + public void MathExpressionConverter_WithMultipleVariable_ReturnsCorrectResult(string expression, object[] values, double expectedResult) { var mathExpressionConverter = new MultiMathExpressionConverter(); - var result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + var result = mathExpressionConverter.Convert(values, mathExpressionTargetType, expression); + Assert.NotNull(result); Assert.True(Math.Abs((double)result - expectedResult) < tolerance); } [Theory] - [InlineData("1 + 3 + 5 + (3 - 2))")] - [InlineData("1 + 2) + (9")] - [InlineData("100 + pow(2)")] - public void MathExpressionConverterThrowsArgumentException(string expression) + [InlineData("x && x1", new object?[] { true, true }, true)] + [InlineData("x && x1", new object?[] { true, false }, false)] + [InlineData("x && x1", new object?[] { false, true }, false)] + [InlineData("x && 3 == 4", new object?[] { false }, false)] + [InlineData("x && x1", new object?[] { "Cat", "Dog" }, "Dog")] + [InlineData("x && x1", new object?[] { false, "Cat" }, false)] + [InlineData("x && x1", new object?[] { "Cat", false }, false)] + [InlineData("x && x1", new object?[] { "", false }, "")] + [InlineData("x && x1", new object?[] { false, "" }, false)] + [InlineData("x && x1", new object?[] { null, "Cat" }, null)] + [InlineData("x && x1", new object?[] { "Cat", null }, null)] + [InlineData("x && x1", new object?[] { "", null }, "")] + [InlineData("x && x1", new object?[] { null, "" }, null)] + [InlineData("x || x1", new object?[] { true, true }, true)] + [InlineData("x || x1", new object?[] { false, true }, true)] + [InlineData("x || x1", new object?[] { true, false }, true)] + [InlineData("x || 3 == 4", new object?[] { false }, false)] + [InlineData("x || x1", new object?[] { "Cat", "Dog" }, "Cat")] + [InlineData("x || x1", new object?[] { false, "Cat" }, "Cat")] + [InlineData("x || x1", new object?[] { "Cat", false }, "Cat")] + [InlineData("x || x1", new object?[] { "", false }, false)] + [InlineData("x || x1", new object?[] { false, "" }, "")] + [InlineData("x || x1", new object?[] { null, "Cat" }, "Cat")] + [InlineData("x || x1", new object?[] { "Cat", null }, "Cat")] + [InlineData("x || x1", new object?[] { "", null }, null)] + [InlineData("x || x1", new object?[] { null, "" }, "")] + [InlineData("x || x1", new object?[] { false, new int[] { 1, 2, 3 } }, new int[] { 1, 2, 3 })] + public void MultiMathExpressionConverter_WithMultipleVariable_ReturnsCorrectLogicalResult(string expression, object?[] variables, object? expectedResult) { - var mathExpressionConverter = new MathExpressionConverter(); + var mathExpressionConverter = new MultiMathExpressionConverter(); + var result = mathExpressionConverter.Convert(variables, typeof(object), expression); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(true, true, true, true)] + [InlineData(true, false, false, true)] + [InlineData(false, true, false, true)] + [InlineData(false, false, false, false)] + [InlineData("Cat", "Dog", "Dog", "Cat")] + [InlineData(false, "Cat", false, "Cat")] + [InlineData("Cat", false, false, "Cat")] + [InlineData("", false, "", false)] + [InlineData(false, "", false, "")] + [InlineData(null, "Cat", null, "Cat")] + [InlineData("Cat", null, null, "Cat")] + [InlineData("", null, "", null)] + [InlineData(null, "", null, "")] + public void MultiMathExpressionConverter_WithAlternateLogicalOperators_ReturnsSameEvaluation(object? x, object? x1, object? expectedAndResult, object? expectedOrResult) + { + var variables = new object?[] { x, x1 }; + var mathExpressionConverter = new MultiMathExpressionConverter(); + var andResult = mathExpressionConverter.Convert(variables, typeof(object), "x && x1"); + var alternateAndResult = mathExpressionConverter.Convert(variables, typeof(object), "x and x1"); + Assert.Equal(andResult, expectedAndResult); + Assert.Equal(alternateAndResult, expectedAndResult); + var orResult = mathExpressionConverter.Convert(variables, typeof(object), "x || x1"); + var alternateOrResult = mathExpressionConverter.Convert(variables, typeof(object), "x or x1"); + Assert.Equal(orResult, expectedOrResult); + Assert.Equal(alternateOrResult, expectedOrResult); + } + + [Theory] + [InlineData("x >= x1", "x ge x1")] + [InlineData("x > x1", "x gt x1")] + [InlineData("x <= x1", "x le x1")] + [InlineData("x < x1", "x lt x1")] + public void MultiMathExpressionConverter_WithAlternateCompareOperators_ReturnsSameEvaluation(string expression, string alternateExpression) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + for (var i = 0; i <= 2; i++) + { + for (var j = 0; j <= 2; j++) + { + var variables = new object?[] { i, j }; + var result = mathExpressionConverter.Convert(variables, typeof(object), expression); + var alternateResult = mathExpressionConverter.Convert(variables, typeof(object), alternateExpression); + Assert.NotNull(result); + Assert.NotNull(alternateResult); + Assert.Equal(result, alternateResult); + } + } + } + + [Theory] + [InlineData("x == 3 && x1", new object?[] { 3d, 4d }, 4d)] + [InlineData("x != 3 || x1", new object?[] { 3d, 4d }, 4d)] + [InlineData("x + x1 || true", new object?[] { 3d, 4d }, 7d)] + [InlineData("x + x1 && false", new object?[] { 2d, -2d }, 0d)] + public void MathExpressionConverter_WithBooleanOperator_ReturnsCorrectNumberResult(string expression, object[] variables, double expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("x != 3 && x1", new object?[] { 3d, 4d }, false)] + [InlineData("x == 3 || x1", new object?[] { 3d, 4d }, true)] + public void MathExpressionConverter_WithBooleanOperator_ReturnsCorrectBooleanResult(string expression, object[] variables, bool expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); - Assert.Throws(() => ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(0d, mathExpressionTargetType, expression, cultureInfo)); - Assert.Throws(() => mathExpressionConverter.ConvertFrom(0d, expression)); + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("x == 3 && x1", new object?[] { 3d, null })] + [InlineData("x != 3 || x1", new object?[] { 3d, null })] + [InlineData("x == 3 ? x1 : x2", new object?[] { 3d, null, 5d })] + [InlineData("x != 3 ? x1 : x2", new object?[] { 3d, 4d, null })] + public void MathExpressionConverter_ReturnsCorrectNullResult(string expression, object[] variables) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is null); + } + + [Theory] + [InlineData("x == x1", new object?[] { 2d, 2d }, true)] + [InlineData("x == x1", new object?[] { 2d, null }, false)] + [InlineData("x == x1", new object?[] { null, 2d }, false)] + [InlineData("x == x1", new object?[] { null, null }, true)] + [InlineData("(x ? x1 : x2) == null", new object?[] { true, null, 2d }, true)] + public void MathExpressionConverter_WithEqualityOperator_ReturnsCorrectBooleanResult(string expression, object[] variables, bool expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); } [Theory] [InlineData(2.5)] [InlineData('c')] [InlineData(true)] + [InlineData("1 + 3 + 5 + (3 - 2))")] + [InlineData("1 + 2) + (9")] + [InlineData("100 + pow(2)")] public void MultiMathExpressionConverterInvalidParameterThrowsArgumentException(object parameter) { var mathExpressionConverter = new MultiMathExpressionConverter(); @@ -74,7 +234,7 @@ public void MultiMathExpressionConverterInvalidParameterThrowsArgumentException( public void MultiMathExpressionConverterInvalidValuesReturnsNull() { var mathExpressionConverter = new MultiMathExpressionConverter(); - var result = mathExpressionConverter.Convert([0d, null], mathExpressionTargetType, "x", cultureInfo); + var result = mathExpressionConverter.Convert([0d, null], mathExpressionTargetType, "x + x1", cultureInfo); result.Should().BeNull(); } @@ -85,9 +245,9 @@ public void MathExpressionConverterNullInputTest() Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(0.0, null, "x", null)); Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(0.0, null, null, null)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), "x", null)); + Assert.True(((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), "x", null) is null); Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), null, null)); - Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(null, typeof(bool), null, null)); + Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(null, typeof(bool), null, null)); } [Fact] diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs index 507af7c4c1..521342b6cd 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs @@ -78,48 +78,53 @@ public StatusBarApplyOn ApplyOn /// #if IOS - protected override void OnAttachedTo(Page bindable, UIKit.UIView platformView) + protected override void OnAttachedTo(Page page, UIKit.UIView platformView) #elif ANDROID - protected override void OnAttachedTo(Page bindable, Android.Views.View platformView) + protected override void OnAttachedTo(Page page, Android.Views.View platformView) #else - protected override void OnAttachedTo(Page bindable, object platformView) + protected override void OnAttachedTo(Page page, object platformView) #endif { - base.OnAttachedTo(bindable, platformView); + base.OnAttachedTo(page, platformView); if (ApplyOn is StatusBarApplyOn.OnBehaviorAttachedTo) { +#if IOS + StatusBar.SetBarSize(Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page.GetUseSafeArea(page)); +#endif + StatusBar.SetColor(StatusBarColor); StatusBar.SetStyle(StatusBarStyle); } - bindable.NavigatedTo += OnPageNavigatedTo; + page.NavigatedTo += OnPageNavigatedTo; #if IOS - bindable.SizeChanged += OnPageSizeChanged; + page.SizeChanged += OnPageSizeChanged; #endif } /// #if IOS - protected override void OnDetachedFrom(Page bindable, UIKit.UIView platformView) + protected override void OnDetachedFrom(Page page, UIKit.UIView platformView) #elif ANDROID - protected override void OnDetachedFrom(Page bindable, Android.Views.View platformView) + protected override void OnDetachedFrom(Page page, Android.Views.View platformView) #else - protected override void OnDetachedFrom(Page bindable, object platformView) + protected override void OnDetachedFrom(Page page, object platformView) #endif { #if IOS - bindable.SizeChanged -= OnPageSizeChanged; + page.SizeChanged -= OnPageSizeChanged; #endif - base.OnDetachedFrom(bindable, platformView); + base.OnDetachedFrom(page, platformView); - bindable.NavigatedTo -= OnPageNavigatedTo; + page.NavigatedTo -= OnPageNavigatedTo; } #if IOS static void OnPageSizeChanged(object? sender, EventArgs e) { - StatusBar.UpdateBarSize(); + ArgumentNullException.ThrowIfNull(sender); + StatusBar.SetBarSize(Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page.GetUseSafeArea((Page)sender)); } #endif diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs index 77223c0580..28167ff982 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs @@ -1,95 +1,148 @@ -using System.Globalization; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using CommunityToolkit.Maui.Core; namespace CommunityToolkit.Maui.Converters; -sealed partial class MathExpression +enum MathTokenType { - const NumberStyles numberStyle = NumberStyles.Float | NumberStyles.AllowThousands; + Value, + Operator, +} - static readonly IFormatProvider formatProvider = new CultureInfo("en-US"); +sealed record MathToken(MathTokenType Type, string Text, object? Value); +sealed partial class MathExpression +{ readonly IReadOnlyList operators; - internal MathExpression(string expression, IEnumerable? arguments = null) + internal MathExpression(in string expression, in IReadOnlyList arguments) { ArgumentException.ThrowIfNullOrEmpty(expression, "Expression can't be null or empty."); - - var argumentList = arguments?.ToList() ?? []; + ArgumentNullException.ThrowIfNull(arguments, "Arguments cannot be null."); Expression = expression.ToLower(); - var operators = new List - { - new ("+", 2, MathOperatorPrecedence.Low, x => x[0] + x[1]), - new ("-", 2, MathOperatorPrecedence.Low, x => x[0] - x[1]), - new ("*", 2, MathOperatorPrecedence.Medium, x => x[0] * x[1]), - new ("/", 2, MathOperatorPrecedence.Medium, x => x[0] / x[1]), - new ("%", 2, MathOperatorPrecedence.Medium, x => x[0] % x[1]), - new ("abs", 1, MathOperatorPrecedence.Medium, x => Math.Abs(x[0])), - new ("acos", 1, MathOperatorPrecedence.Medium, x => Math.Acos(x[0])), - new ("asin", 1, MathOperatorPrecedence.Medium, x => Math.Asin(x[0])), - new ("atan", 1, MathOperatorPrecedence.Medium, x => Math.Atan(x[0])), - new ("atan2", 2, MathOperatorPrecedence.Medium, x => Math.Atan2(x[0], x[1])), - new ("ceiling", 1, MathOperatorPrecedence.Medium, x => Math.Ceiling(x[0])), - new ("cos", 1, MathOperatorPrecedence.Medium, x => Math.Cos(x[0])), - new ("cosh", 1, MathOperatorPrecedence.Medium, x => Math.Cosh(x[0])), - new ("exp", 1, MathOperatorPrecedence.Medium, x => Math.Exp(x[0])), - new ("floor", 1, MathOperatorPrecedence.Medium, x => Math.Floor(x[0])), - new ("ieeeremainder", 2, MathOperatorPrecedence.Medium, x => Math.IEEERemainder(x[0], x[1])), - new ("log", 2, MathOperatorPrecedence.Medium, x => Math.Log(x[0], x[1])), - new ("log10", 1, MathOperatorPrecedence.Medium, x => Math.Log10(x[0])), - new ("max", 2, MathOperatorPrecedence.Medium, x => Math.Max(x[0], x[1])), - new ("min", 2, MathOperatorPrecedence.Medium, x => Math.Min(x[0], x[1])), - new ("pow", 2, MathOperatorPrecedence.Medium, x => Math.Pow(x[0], x[1])), - new ("round", 2, MathOperatorPrecedence.Medium, x => Math.Round(x[0], Convert.ToInt32(x[1]))), - new ("sign", 1, MathOperatorPrecedence.Medium, x => Math.Sign(x[0])), - new ("sin", 1, MathOperatorPrecedence.Medium, x => Math.Sin(x[0])), - new ("sinh", 1, MathOperatorPrecedence.Medium, x => Math.Sinh(x[0])), - new ("sqrt", 1, MathOperatorPrecedence.Medium, x => Math.Sqrt(x[0])), - new ("tan", 1, MathOperatorPrecedence.Medium, x => Math.Tan(x[0])), - new ("tanh", 1, MathOperatorPrecedence.Medium, x => Math.Tanh(x[0])), - new ("truncate", 1, MathOperatorPrecedence.Medium, x => Math.Truncate(x[0])), - new ("^", 2, MathOperatorPrecedence.High, x => Math.Pow(x[0], x[1])), - new ("pi", 0, MathOperatorPrecedence.Constant, _ => Math.PI), - new ("e", 0, MathOperatorPrecedence.Constant, _ => Math.E), - }; - - if (argumentList.Count > 0) + List operators = + [ + new ("+", 2, x => Convert.ToDouble(x[0]) + Convert.ToDouble(x[1])), + new ("-", 2, x => Convert.ToDouble(x[0]) - Convert.ToDouble(x[1])), + new ("*", 2, x => Convert.ToDouble(x[0]) * Convert.ToDouble(x[1])), + new ("/", 2, x => Convert.ToDouble(x[0]) / Convert.ToDouble(x[1])), + new ("%", 2, x => Convert.ToDouble(x[0]) % Convert.ToDouble(x[1])), + + new ("and", 2, x => ConvertToBoolean(x[0]) ? x[1] : x[0]), + new ("or", 2, x => ConvertToBoolean(x[0]) ? x[0] : x[1]), + + new ("==", 2, x => object.Equals(x[0], x[1])), + new ("!=", 2, x => !object.Equals(x[0], x[1])), + + new ("ge", 2, x => Convert.ToDouble(x[0]) >= Convert.ToDouble(x[1])), + new ("gt", 2, x => Convert.ToDouble(x[0]) > Convert.ToDouble(x[1])), + new ("le", 2, x => Convert.ToDouble(x[0]) <= Convert.ToDouble(x[1])), + new ("lt", 2, x => Convert.ToDouble(x[0]) < Convert.ToDouble(x[1])), + new ("neg", 1, x => -Convert.ToDouble(x[0])), + new ("not", 1, x => !ConvertToBoolean(x[0])), + new ("if", 3, x => ConvertToBoolean(x[0]) ? x[1] : x[2]), + + new ("abs", 1, x => Math.Abs(Convert.ToDouble(x[0]))), + new ("acos", 1, x => Math.Acos(Convert.ToDouble(x[0]))), + new ("asin", 1, x => Math.Asin(Convert.ToDouble(x[0]))), + new ("atan", 1, x => Math.Atan(Convert.ToDouble(x[0]))), + new ("atan2", 2, x => Math.Atan2(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("ceiling", 1, x => Math.Ceiling(Convert.ToDouble(x[0]))), + new ("cos", 1, x => Math.Cos(Convert.ToDouble(x[0]))), + new ("cosh", 1, x => Math.Cosh(Convert.ToDouble(x[0]))), + new ("exp", 1, x => Math.Exp(Convert.ToDouble(x[0]))), + new ("floor", 1, x => Math.Floor(Convert.ToDouble(x[0]))), + new ("ieeeremainder", 2, x => Math.IEEERemainder(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("log", 2, x => Math.Log(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("log10", 1, x => Math.Log10(Convert.ToDouble(x[0]))), + new ("max", 2, x => Math.Max(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("min", 2, x => Math.Min(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("pow", 2, x => Math.Pow(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("round", 2, x => Math.Round(Convert.ToDouble(x[0]), Convert.ToInt32(x[1]))), + new ("sign", 1, x => Math.Sign(Convert.ToDouble(x[0]))), + new ("sin", 1, x => Math.Sin(Convert.ToDouble(x[0]))), + new ("sinh", 1, x => Math.Sinh(Convert.ToDouble(x[0]))), + new ("sqrt", 1, x => Math.Sqrt(Convert.ToDouble(x[0]))), + new ("tan", 1, x => Math.Tan(Convert.ToDouble(x[0]))), + new ("tanh", 1, x => Math.Tanh(Convert.ToDouble(x[0]))), + new ("truncate", 1, x => Math.Truncate(Convert.ToDouble(x[0]))), + new ("int", 1, x => Convert.ToInt32(x[0])), + new ("double", 1, x => Convert.ToDouble(x[0])), + new ("bool", 1, x => Convert.ToBoolean(x[0])), + new ("str", 1, x => x[0]?.ToString()), + new ("len", 1, x => x[0]?.ToString()?.Length), + new ("^", 2, x => Math.Pow(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("pi", 0, _ => Math.PI), + new ("e", 0, _ => Math.E), + new ("true", 0, _ => true), + new ("false", 0, _ => false), + new ("null", 0, _ => null), + ]; + + if (arguments.Count > 0) { - operators.Add(new MathOperator("x", 0, MathOperatorPrecedence.Constant, _ => argumentList[0])); + var firstArgument = arguments[0]; + operators.Add(new MathOperator("x", 0, _ => firstArgument)); } - for (var i = 0; i < argumentList.Count; i++) + for (var i = 0; i < arguments.Count; i++) { - var index = i; - operators.Add(new MathOperator($"x{i}", 0, MathOperatorPrecedence.Constant, _ => argumentList[index])); + var currentArgument = arguments[i]; + operators.Add(new MathOperator($"x{i}", 0, _ => currentArgument)); } this.operators = operators; } - internal string Expression { get; } + static ReadOnlyDictionary BinaryMappingDictionary { get; } = new Dictionary + { + { "<", "lt" }, + { "<=", "le" }, + { ">", "gt" }, + { ">=", "ge" }, + { "&&", "and" }, + { "||", "or" } + }.AsReadOnly(); + + static ReadOnlyDictionary UnaryMappingDictionary { get; } = new Dictionary + { + { "-", "neg" }, + { "!", "not" } + }.AsReadOnly(); - public double Calculate() + string Expression { get; } + + List RPN { get; } = []; + + int ExpressionIndex { get; set; } + + Match PatternMatch { get; set; } = Match.Empty; + + public object? CalculateResult() { - var rpn = GetReversePolishNotation(Expression); + if (!ParseExpression()) + { + throw new ArgumentException("Math Expression Invalid. Failed to parse math expression."); + } - var stack = new Stack(); + var stack = new Stack(); - foreach (var value in rpn) + foreach (var token in RPN) { - if (double.TryParse(value, numberStyle, formatProvider, out var numeric)) + if (token.Type is MathTokenType.Value) { - stack.Push(numeric); + stack.Push(token.Value); continue; } - var mathOperator = operators.FirstOrDefault(x => x.Name == value) ?? - throw new ArgumentException($"Invalid math expression. Can't find operator or value with name \"{value}\"."); + var mathOperator = operators.FirstOrDefault(x => x.Name == token.Text) ?? throw new ArgumentException($"Math Expression Invalid. Can't find operator or value with name \"{token.Text}\"."); - if (mathOperator.Precedence is MathOperatorPrecedence.Constant) + if (mathOperator.NumericCount is 0) { stack.Push(mathOperator.CalculateFunc([])); continue; @@ -99,146 +152,310 @@ public double Calculate() if (stack.Count < operatorNumericCount) { - throw new ArgumentException("Invalid math expression."); + throw new ArgumentException($"Math Expression Invalid. Insufficient parameters to operator \"{mathOperator.Name}\"."); } - var args = new List(); + bool containsNullArgument = false; + List args = []; + for (var j = 0; j < operatorNumericCount; j++) { - args.Add(stack.Pop()); + object? val = stack.Pop(); + args.Add(val); + containsNullArgument = containsNullArgument || val is null; } args.Reverse(); - stack.Push(mathOperator.CalculateFunc([.. args])); + containsNullArgument = mathOperator.Name switch + { + "if" => args[0] is null, + "and" or "or" or "==" or "!=" => false, + _ => containsNullArgument + }; + + stack.Push(!containsNullArgument ? mathOperator.CalculateFunc([.. args]) : null); } - if (stack.Count != 1) + return stack.Count switch { - throw new ArgumentException("Invalid math expression."); + 0 => throw new InvalidOperationException($"Math Expression Invalid. Stack is unexpectedly empty."), + > 1 => throw new InvalidOperationException($"Math Expression Invalid. Stack unexpectedly contains multiple items ({stack.Count}) items when it should contain only the final result."), + _ => stack.Pop() + }; + } + + [GeneratedRegex("""^(\w+)\(""")] + private static partial Regex EvaluateFunctionStart(); + + [GeneratedRegex("""^(\,)""")] + private static partial Regex EvaluateComma(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex EvaluateFunctionEnd(); + + [GeneratedRegex("""^(\?)""")] + private static partial Regex EvaluateConditionalStart(); + + [GeneratedRegex("""^(\:)""")] + private static partial Regex EvaluateConditionalElse(); + + [GeneratedRegex("""^(\|\||or)""")] + private static partial Regex EvaluateLogicalOROperator(); + + [GeneratedRegex("""^(\&\&|and)""")] + private static partial Regex EvaluateLogicalAndOperator(); + + [GeneratedRegex("""^(==|!=|eq|ne)""")] + private static partial Regex EvaluateEqualityOperators(); + + [GeneratedRegex("""^(\<\=|\>\=|\<|\>|le|ge|lt|gt)""")] + private static partial Regex EvaluateCompareOperators(); + + [GeneratedRegex("""^(\+|\-)""")] + private static partial Regex EvaluateSumOperators(); + + [GeneratedRegex("""^(\*|\/|\%)""")] + private static partial Regex EvaluateProductOperators(); + + [GeneratedRegex("""^(\^)""")] + private static partial Regex EvaluatePowerOperator(); + + [GeneratedRegex("""^(\-|\!)""")] + private static partial Regex EvaluateUnaryOperators(); + + [GeneratedRegex("""^(\-?\d+\.\d+|\-?\d+)""")] + private static partial Regex EvaluateNumberPattern(); + + [GeneratedRegex("""^["]([^"]*)["]""")] + private static partial Regex EvaluateStringPattern(); + + [GeneratedRegex("""^(\w+)""")] + private static partial Regex EvaluateConstants(); + + [GeneratedRegex("""^(\()""")] + private static partial Regex EvaluateParenStart(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex EvaluateParenEnd(); + + [GeneratedRegex("""^\s*""")] + private static partial Regex EvaluateWhitespace(); + + static bool ConvertToBoolean(object? b) => b switch + { + bool x => x, + null => false, + double doubleValue => doubleValue != 0 && !double.IsNaN(doubleValue), + string stringValue => !string.IsNullOrEmpty(stringValue), + _ => Convert.ToBoolean(b) + }; + + bool ParsePattern(Regex regex) + { + var whitespaceMatch = EvaluateWhitespace().Match(Expression[ExpressionIndex..]); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; + } + + PatternMatch = regex.Match(Expression[ExpressionIndex..]); + if (!PatternMatch.Success) + { + return false; } + ExpressionIndex += PatternMatch.Length; - return stack.Pop(); + whitespaceMatch = EvaluateWhitespace().Match(Expression[ExpressionIndex..]); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; + } + + return true; } - [GeneratedRegex(@"(? GetReversePolishNotation(string expression) + bool ParseExpr() { - var matches = MathExpressionRegexPattern().Matches(expression) ?? throw new ArgumentException("Invalid math expression."); + return ParseConditional(); + } - var output = new List(); - var stack = new Stack<(string Name, MathOperatorPrecedence Precedence)>(); + bool ParseConditional() + { + if (!ParseLogicalOR()) + { + return false; + } - foreach (var match in matches.Cast()) + if (!ParsePattern(EvaluateConditionalStart())) { - if (string.IsNullOrEmpty(match?.Value)) - { - continue; - } + return true; + } + + if (!ParseLogicalOR()) + { + return false; + } + + if (!ParsePattern(EvaluateConditionalElse())) + { + return false; + } + + if (!ParseLogicalOR()) + { + return false; + } + + RPN.Add(new MathToken(MathTokenType.Operator, "if", null)); + return true; + } + + bool ParseLogicalOR() => ParseBinaryOperators(EvaluateLogicalOROperator(), ParseLogicalAnd); + + bool ParseLogicalAnd() => ParseBinaryOperators(EvaluateLogicalAndOperator(), ParseEquality); + + bool ParseEquality() => ParseBinaryOperators(EvaluateEqualityOperators(), ParseCompare); - var value = match.Value; + bool ParseCompare() => ParseBinaryOperators(EvaluateCompareOperators(), ParseSum); - if (double.TryParse(value, numberStyle, formatProvider, out var numeric)) + bool ParseSum() => ParseBinaryOperators(EvaluateSumOperators(), ParseProduct); + + bool ParseProduct() => ParseBinaryOperators(EvaluateProductOperators(), ParsePower); + + bool ParsePower() => ParseBinaryOperators(EvaluatePowerOperator(), ParsePrimary); + + bool ParseBinaryOperators(Regex BinaryOperators, Func ParseNext) + { + if (!ParseNext()) + { + return false; + } + int index = ExpressionIndex; + while (ParsePattern(BinaryOperators)) + { + string _operator = PatternMatch.Groups[1].Value; + if (BinaryMappingDictionary.TryGetValue(_operator, out var value)) { - if (numeric < 0) - { - var isNegative = output.Count == 0 || stack.Count != 0; - - if (!isNegative) - { - stack.Push(("-", MathOperatorPrecedence.Low)); - output.Add(Math.Abs(numeric).ToString(formatProvider)); - continue; - } - } - - output.Add(value); - continue; + _operator = value; + } + if (!ParseNext()) + { + ExpressionIndex = index; + return false; } + RPN.Add(new MathToken(MathTokenType.Operator, _operator, null)); + index = ExpressionIndex; + } + return true; + } - var mathOperator = operators.FirstOrDefault(x => x.Name == value); - if (mathOperator is not null) + bool ParsePrimary() + { + if (ParsePattern(EvaluateNumberPattern())) + { + string _number = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _number, double.Parse(_number))); + return true; + } + + if (ParsePattern(EvaluateStringPattern())) + { + string _string = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _string, _string)); + return true; + } + + if (ParseFunction()) + { + return true; + } + + if (ParsePattern(EvaluateConstants())) + { + string _constant = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Operator, _constant, null)); + return true; + } + + int index = ExpressionIndex; + if (ParsePattern(EvaluateParenStart())) + { + if (!ParseExpr()) { - if (mathOperator.Precedence is MathOperatorPrecedence.Constant) - { - output.Add(value); - continue; - } - - while (stack.Count > 0) - { - var (_, precedence) = stack.Peek(); - if (precedence >= mathOperator.Precedence) - { - output.Add(stack.Pop().Name); - } - else - { - break; - } - } - - stack.Push((value, mathOperator.Precedence)); + ExpressionIndex = index; + return false; } - else if (value is "(") + if (!ParsePattern(EvaluateParenEnd())) { - stack.Push((value, MathOperatorPrecedence.Lowest)); + ExpressionIndex = index; + return false; } - else if (value is ")") + return true; + } + + index = ExpressionIndex; + if (ParsePattern(EvaluateUnaryOperators())) + { + string _operator = PatternMatch.Groups[1].Value; + if (UnaryMappingDictionary.TryGetValue(_operator, out var value)) { - var isFound = false; - for (var i = stack.Count - 1; i >= 0; i--) - { - if (stack.Count == 0) - { - throw new ArgumentException("Invalid math expression."); - } - - var stackValue = stack.Pop().Name; - if (stackValue is "(") - { - isFound = true; - break; - } - - output.Add(stackValue); - } - - if (!isFound) - { - throw new ArgumentException("Invalid math expression."); - } + _operator = value; } - else if (value is ",") + if (!ParsePrimary()) { - while (stack.Count > 0) - { - var (_, precedence) = stack.Peek(); - if (precedence >= MathOperatorPrecedence.Low) - { - output.Add(stack.Pop().Name); - } - else - { - break; - } - } + ExpressionIndex = index; + return false; } + RPN.Add(new MathToken(MathTokenType.Operator, _operator, null)); + return true; + } + + return false; + } + + bool ParseFunction() + { + int index = ExpressionIndex; + if (!ParsePattern(EvaluateFunctionStart())) + { + return false; } - for (var i = stack.Count - 1; i >= 0; i--) + string text = PatternMatch.Groups[0].Value; + string functionName = PatternMatch.Groups[1].Value; + + if (!ParseExpr()) + { + ExpressionIndex = index; + return false; + } + + while (ParsePattern(EvaluateComma())) { - var (name, _) = stack.Pop(); - if (name is "(") + if (!ParseExpr()) { - throw new ArgumentException("Invalid math expression."); + ExpressionIndex = index; + return false; } + index = ExpressionIndex; + } - output.Add(name); + if (!ParsePattern(EvaluateFunctionEnd())) + { + ExpressionIndex = index; + return false; } - return output; + RPN.Add(new MathToken(MathTokenType.Operator, functionName, null)); + + return true; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs index 8d29adcadf..4f72e1d4f3 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs @@ -6,23 +6,22 @@ namespace CommunityToolkit.Maui.Converters; /// Converters for Math expressions /// [AcceptEmptyServiceProvider] -public partial class MathExpressionConverter : BaseConverterOneWay +public partial class MathExpressionConverter : BaseConverterOneWay { /// - public override double DefaultConvertReturnValue { get; set; } = 0.0d; + public override object? DefaultConvertReturnValue { get; set; } = 0.0d; /// /// Calculate the incoming expression string with one variable. /// - /// The variable X for an expression + /// The variable X for an expression /// The expression to calculate. /// The culture to use in the converter. This is not implemented. /// A The result of calculating an expression. - public override double ConvertFrom(double value, string parameter, CultureInfo? culture = null) + public override object? ConvertFrom(object? inputValue, string parameter, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(parameter); - var mathExpression = new MathExpression(parameter, [value]); - return mathExpression.Calculate(); + return new MathExpression(parameter, [inputValue]).CalculateResult(); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs index ca0325f8af..14dea65fae 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs @@ -18,7 +18,6 @@ public class MultiMathExpressionConverter : MultiValueConverterExtension, ICommu /// The expression to calculate. /// The culture to use in the converter. This is not implemented. /// A The result of calculating an expression. - [return: NotNullIfNotNull(nameof(values))] public object? Convert(object?[]? values, Type targetType, [NotNull] object? parameter, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(targetType); @@ -29,22 +28,9 @@ public class MultiMathExpressionConverter : MultiValueConverterExtension, ICommu throw new ArgumentException("The parameter should be of type String."); } - if (values is null || values.Any(x => !double.TryParse(x?.ToString(), out _))) - { - return null; - } - - var args = new List(); - foreach (var value in values) - { - var valueString = value?.ToString() ?? throw new ArgumentException("Values cannot be null."); - - var xValue = double.Parse(valueString); - args.Add(xValue); - } - - var math = new MathExpression(expression, args); - return math.Calculate(); + return values is null + ? null + : new MathExpression(expression, values).CalculateResult(); } ///