diff --git a/.editorconfig b/.editorconfig index 125f2c1..fa22add 100644 --- a/.editorconfig +++ b/.editorconfig @@ -211,3 +211,4 @@ dotnet_diagnostic.SA1633.severity = silent # https://github.com/JosefPihrt/Roslynator/blob/master/docs/analyzers/RCS1090.md dotnet_diagnostic.CA2007.severity = silent dotnet_diagnostic.RCS1090.severity = silent +dotnet_diagnostic.CA1805.severity = none diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index b797991..556f434 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest name: Publish Documentation steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - uses: nikeee/docfx-action@v1.0.0 name: Build Documentation with: diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index a594549..296bc84 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,9 +16,12 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: | + 3.1.x + 6.0.x + 7.0.x - name: Add NuGet source run: dotnet nuget add source --username oliverbooth --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/oliverbooth/index.json" @@ -29,5 +32,11 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - - name: Test - run: dotnet test --no-build --verbosity normal --configuration Release + - name: Test .NET Core 3.1 + run: dotnet test --no-build --verbosity normal --configuration Release --framework netcoreapp3.1 + + - name: Test .NET 6 + run: dotnet test --no-build --verbosity normal --configuration Release --framework net6.0 + + - name: Test .NET 7 + run: dotnet test --no-build --verbosity normal --configuration Release --framework net7.0 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f953fb7..8d37b9a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x @@ -26,16 +26,63 @@ jobs: run: dotnet restore - name: Build - run: dotnet build -c Debug + run: dotnet build --configuration Debug --no-restore -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} - name: Build NuGet package run: | mkdir build - dotnet pack X10D -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} - dotnet pack X10D.Unity -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D --configuration Debug --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.DSharpPlus --configuration Debug --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.Hosting --configuration Debug --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.Unity --configuration Debug --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='nightly' -p:BuildNumber=${{ github.run_number }} - name: Push NuGet Package to GitHub run: dotnet nuget push "build/*" --source "github" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate - name: Push NuGet Package to nuget.org run: dotnet nuget push "build/*" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build + path: build/ + + - name: Checkout upm branch + uses: actions/checkout@v3 + with: + ref: upm + path: upm + + - name: Build package.json + run: | + dotnet run --project ./X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj "./X10D/bin/Debug/netstandard2.1/X10D.dll" + cp package.json upm/package.json + + - name: Copy built artifacts to upm + run: | + cd upm + cp ../X10D/bin/Debug/netstandard2.1/X10D.dll ./X10D.dll + cp ../X10D/bin/Debug/netstandard2.1/X10D.xml ./X10D.xml + cp ../X10D.Unity/bin/Debug/netstandard2.1/X10D.Unity.dll ./X10D.Unity.dll + cp ../X10D.Unity/bin/Debug/netstandard2.1/X10D.Unity.xml ./X10D.Unity.xml + + - name: Check for changes + run: | + cd upm + git diff --quiet + continue-on-error: true + + - name: Commit update + if: ${{ success() }} + run: | + cd upm + git config user.name github-actions + git config user.email github-actions@github.com + git add X10D.dll + git add X10D.Unity.dll + git add X10D.xml + git add X10D.Unity.xml + git add package.json + git commit -m "Update upm branch ($GITHUB_SHA)" + git push diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index bc6e4f0..f66cfa7 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x @@ -26,13 +26,15 @@ jobs: run: dotnet restore - name: Build - run: dotnet build -c Release + run: dotnet build --configuration Release --no-restore -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} - name: Build NuGet package run: | mkdir build - dotnet pack X10D -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} - dotnet pack X10D.Unity -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.DSharpPlus --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.Hosting --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} + dotnet pack X10D.Unity --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} - name: Push NuGet Package to GitHub run: dotnet nuget push "build/*" --source "github" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate @@ -40,8 +42,53 @@ jobs: - name: Push NuGet Package to nuget.org run: dotnet nuget push "build/*" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build + path: build/ + - name: Create Release uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: true + + - name: Checkout upm branch + uses: actions/checkout@v3 + with: + ref: upm + path: upm + + - name: Build package.json + run: | + dotnet run --project ./X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj "./X10D/bin/Release/netstandard2.1/X10D.dll" + cp package.json upm/package.json + + - name: Copy built artifacts to upm + run: | + cd upm + cp ../X10D/bin/Release/netstandard2.1/X10D.dll ./X10D.dll + cp ../X10D/bin/Release/netstandard2.1/X10D.xml ./X10D.xml + cp ../X10D.Unity/bin/Release/netstandard2.1/X10D.Unity.dll ./X10D.Unity.dll + cp ../X10D.Unity/bin/Release/netstandard2.1/X10D.Unity.xml ./X10D.Unity.xml + + - name: Check for changes + run: | + cd upm + git diff --quiet + continue-on-error: true + + - name: Commit update + if: ${{ success() }} + run: | + cd upm + git config user.name github-actions + git config user.email github-actions@github.com + git add X10D.dll + git add X10D.Unity.dll + git add X10D.xml + git add X10D.Unity.xml + git add package.json + git commit -m "Update upm branch ($GITHUB_SHA)" + git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba10ad3..49fbc0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x @@ -26,13 +26,15 @@ jobs: run: dotnet restore - name: Build - run: dotnet build -c Release + run: dotnet build --configuration Release --no-restore - name: Build NuGet package run: | mkdir build - dotnet pack X10D -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build - dotnet pack X10D.Unity -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build + dotnet pack X10D --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build + dotnet pack X10D.DSharpPlus --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build + dotnet pack X10D.Hosting --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build + dotnet pack X10D.Unity --configuration Release --no-build -p:SymbolPackageFormat=snupkg --include-symbols --include-source -o build - name: Push NuGet Package to GitHub run: dotnet nuget push "build/*" --source "github" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate @@ -40,8 +42,53 @@ jobs: - name: Push NuGet Package to nuget.org run: dotnet nuget push "build/*" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build + path: build/ + - name: Create Release uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false + + - name: Checkout upm branch + uses: actions/checkout@v3 + with: + ref: upm + path: upm + + - name: Build package.json + run: | + dotnet run --project ./X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj "./X10D/bin/Release/netstandard2.1/X10D.dll" + cp package.json upm/package.json + + - name: Copy built artifacts to upm + run: | + cd upm + cp ../X10D/bin/Release/netstandard2.1/X10D.dll ./X10D.dll + cp ../X10D/bin/Release/netstandard2.1/X10D.xml ./X10D.xml + cp ../X10D.Unity/bin/Release/netstandard2.1/X10D.Unity.dll ./X10D.Unity.dll + cp ../X10D.Unity/bin/Release/netstandard2.1/X10D.Unity.xml ./X10D.Unity.xml + + - name: Check for changes + run: | + cd upm + git diff --quiet + continue-on-error: true + + - name: Commit update + if: ${{ success() }} + run: | + cd upm + git config user.name github-actions + git config user.email github-actions@github.com + git add X10D.dll + git add X10D.Unity.dll + git add X10D.xml + git add X10D.Unity.xml + git add package.json + git commit -m "Update upm branch ($GITHUB_SHA)" + git push diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 62c00cf..3ac8f1b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -14,7 +14,7 @@ jobs: uses: actions/setup-java@v1 with: java-version: 1.11 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Cache SonarCloud packages diff --git a/.github/workflows/source_validator.yml b/.github/workflows/source_validator.yml index 89d15b1..cefdd50 100644 --- a/.github/workflows/source_validator.yml +++ b/.github/workflows/source_validator.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x diff --git a/.github/workflows/unity.yml b/.github/workflows/unity.yml index 22bd476..9db7ec3 100644 --- a/.github/workflows/unity.yml +++ b/.github/workflows/unity.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x @@ -40,7 +40,7 @@ jobs: cp -r ./X10D.Unity/bin/Release/netstandard2.1/X10D.Unity.dll ./X10D.Unity.Tests/Assets/Libraries/X10D.Unity.dll - name: Unity - Test runner - uses: game-ci/unity-test-runner@v2.0.2 + uses: game-ci/unity-test-runner@v2.1.0 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a5caa91..dc5c548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,205 +1,392 @@ # Changelog -## [3.1.0] +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 3.2.0 - [Unreleased] + ### Added + +- Added new library X10D.DSharpPlus. +- Added new library X10D.Hosting. +- Added .NET 7 target. +- X10D: Added `IntrinsicExtensions` and `IntrinsicUtility` which offer methods for vectors in `System.Runtime.Instrinsics`. (#70) +- X10D: Added `MathUtility.Bias(float, float)` and `MathUtility.Bias(double, double)`. +- X10D: Added `MathUtility.ExponentialDecay(float, float, float)` and `MathUtility.ExponentialDecay(double, double, double)`. +- X10D: Added `MathUtility.InverseLerp(float, float, float)` and `MathUtility.InverseLerp(double, double, double)`. +- X10D: Added `MathUtility.Pulse(float, float, float)` and `MathUtility.Pulse(double, double, double)`. +- X10D: Added `MathUtility.Sawtooth(float)` and `MathUtility.Sawtooth(double)`. +- X10D: Added `MathUtility.ScaleRange(float, float, float, float, float)` + and `MathUtility.ScaleRange(double, double, double, double, double)` +- X10D: Added `MathUtility.Sigmoid(float)` and `MathUtility.Sigmoid(double)`. +- X10D: Added `MathUtility.SmoothStep(float, float, float)` and `MathUtility.SmoothStep(double, double, double)`. +- X10D: Added `Circle`, `CircleF`, `Cuboid`, `Ellipse`, `EllipseF`, `Line3D`, `Line`, `LineF`, `Polygon`, `PolygonF`, `Polyhedron`, + and `Sphere`, to complement System.Drawing structs such as `Point` and `Rectangle`. +- X10D: Added `Color.Deconstruct()` - with optional alpha parameter. +- X10D: Added `Color.GetClosestConsoleColor()`. +- X10D: Added `DateTime.GetIso8601WeekOfYear()` and `DateTimeOffset.GetIso8601WeekOfYear()`. +- X10D: Added `DirectoryInfo.Clear()`. +- X10D: Added `double.LinearToGamma([gamma])` and `float.LinearToGamma([gamma])`. (#60) +- X10D: Added `double.GammaToLinear([gamma])` and `float.GammaToLinear([gamma])`. (#60) +- X10D: Added `GreatestCommonFactor` for built-in integer types. +- X10D: Added `IEnumerable.ConcatOne(T)`. +- X10D: Added `IEnumerable.CountWhereNot(Func)`. +- X10D: Added `IEnumerable.FirstWhereNot(Func)`. +- X10D: Added `IEnumerable.FirstWhereNotOrDefault(Func)`. +- X10D: Added `IEnumerable.LastWhereNot(Func)`. +- X10D: Added `IEnumerable.LastWhereNotOrDefault(Func)`. +- X10D: Added `IEnumerable.MinMax()` and `IEnumerable.MinMaxBy()`. (#72) +- X10D: Added `IEnumerable.WhereNot(Func)`. +- X10D: Added `IEnumerable.WhereNotNull()`. +- X10D: Added `IEnumerable.Grep(string[, bool])`. +- X10D: Added `IList.RemoveRange(Range)`. +- X10D: Added `IList.Swap(IList)`. (#62) +- X10D: Added `IReadOnlyList.IndexOf(T[, int[, int]])`. +- X10D: Added `IReadOnlyList.Slice(int[, int]])`. +- X10D: Added `LowestCommonMultiple` for built-in integer types. +- X10D: Added `Wrap(T[, T])` for built-in numeric types. (#60) +- X10D: Added `Nullable.TryGetValue(out T)`. (#61) +- X10D: Added `Point.IsOnLine(LineF)`, `Point.IsOnLine(PointF, PointF)`, and `Point.IsOnLine(Vector2, Vector2)`. +- X10D: Added `PointF.IsOnLine(LineF)`, `PointF.IsOnLine(PointF, PointF)`, and `PointF.IsOnLine(Vector2, Vector2)`. +- X10D: Added `Point.ToSize()`. +- X10D: Added `Point.ToSizeF()`. +- X10D: Added `Point.ToVector2()`. +- X10D: Added `PointF.Round([float])`. +- X10D: Added `PointF.ToSizeF()`. +- X10D: Added `PointF.ToVector2()` for .NET < 6. +- X10D: Added `PopCount()` for built-in integer types. +- X10D: Added `Quaternion.ToAxisAngle(out float, out float)`. +- X10D: Added `Quaternion.ToVector3()`. +- X10D: Added `Random.NextFrom(Span)` and `Random.NextFrom(ReadOnlySpan)`. +- X10D: Added `ReadOnlySpan.CountSubstring(char)`. +- X10D: Added `ReadOnlySpan.CountSubstring(ReadOnlySpan[, StringComparison])`. +- X10D: Added `ReadOnlySpan.ToTimeSpan()`. +- X10D: Added `ReadOnlySpan.Split(T)`. +- X10D: Added `ReadOnlySpan.Split(ReadOnlySpan)`. +- X10D: Added `RoundUpToPowerOf2()` for built-in integer types. +- X10D: Added `Saturate()` for built-in floating-point types. +- X10D: Added `Size.ToPoint()`. +- X10D: Added `Size.ToPointF()`. +- X10D: Added `Size.ToVector2()`. +- X10D: Added `Span.CountSubstring(char)`. +- X10D: Added `Span.CountSubstring(Span[, StringComparison])`. +- X10D: Added `Span.Split(T)`. +- X10D: Added `Span.Split(Span)`. +- X10D: Added `string.CountSubstring(char)`. +- X10D: Added `string.CountSubstring(string[, StringComparison])`. +- X10D: Added `string.EnsureStartsWith()` and `string.EnsureEndsWith()`. +- X10D: Added `string.IsEmpty()`. +- X10D: Added `string.IsWhiteSpace()`. +- X10D: Added `string.IsNullOrEmpty()`. +- X10D: Added `string.IsNullOrWhiteSpace()`. +- X10D: Added `TextReader.EnumerateLines()` and `TextReader.EnumerateLinesAsync()`. +- X10D: Added `TimeSpan.TryParse(ReadOnlySpan, out TimeSpan)`. +- X10D: Added `Quaternion.Multiply(Vector3)` - this functions as an equivalent to Unity's `Quaternion * Vector3` operator. +- X10D: Added `Vector2.Deconstruct()`. +- X10D: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)`. +- X10D: Added `Vector2.Round([float])`. +- X10D: Added `Vector2.ToPointF()`. +- X10D: Added `Vector2.ToSizeF()`. +- X10D: Added `Vector3.Deconstruct()`. +- X10D: Added `Vector3.Round([float])`. +- X10D: Added `Vector4.Deconstruct()`. +- X10D: Added `Vector4.Round([float])`. +- X10D: `ComplexSqrt` is now exposed for all frameworks. +- X10D.Unity: Added `DebugUtility`, which mimics `UnityEngine.Debug` while offering more useful primitive drawing methods. +- X10D.Unity: Added `System.Drawing.Color.ToUnityColor()`. +- X10D.Unity: Added `System.Drawing.Color.ToUnityColor32()`. +- X10D.Unity: Added `Color.Deconstruct()` - with optional alpha parameter. +- X10D.Unity: Added `Color.GetClosestConsoleColor()`. +- X10D.Unity: Added `Color.ToSystemDrawingColor()`. +- X10D.Unity: Added `Color32.Deconstruct()` - with optional alpha parameter. +- X10D.Unity: Added `Color32.GetClosestConsoleColor()`. +- X10D.Unity: Added `Color32.ToSystemDrawingColor()`. +- X10D.Unity: Added `Point.ToUnityVector2()`. +- X10D.Unity: Added `Point.ToUnityVector2Int()`. +- X10D.Unity: Added `PointF.ToUnityVector2()`. +- X10D.Unity: Added `Rect.ToSystemRectangleF()`. +- X10D.Unity: Added `RectInt.ToSystemRectangle()`. +- X10D.Unity: Added `RectInt.ToSystemRectangleF()`. +- X10D.Unity: Added `Rectangle.ToUnityRect()`. +- X10D.Unity: Added `Rectangle.ToUnityRectInt()`. +- X10D.Unity: Added `RectangleF.ToUnityRect()`. +- X10D.Unity: Added `Size.ToUnityVector2()`. +- X10D.Unity: Added `Size.ToUnityVector2Int()`. +- X10D.Unity: Added `SizeF.ToUnityVector2()`. +- X10D.Unity: Added `Vector2.Deconstruct()`. +- X10D.Unity: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)`. +- X10D.Unity: Added `Vector2Int.IsOnLine(LineF)`, `Vector2Int.IsOnLine(PointF, PointF)`, `Vector2Int.IsOnLine(Vector2, Vector2)`, + and `Vector2Int.IsOnLine(Vector2Int, Vector2Int)`. +- X10D.Unity: Added `Vector2.Round([float])`. +- X10D.Unity: Added `Vector2.ToSystemPointF()`. +- X10D.Unity: Added `Vector2.ToSystemSizeF()`. +- X10D.Unity: Added `Vector2Int.Deconstruct()`. +- X10D.Unity: Added `Vector2Int.ToSystemPoint()`. +- X10D.Unity: Added `Vector2Int.ToSystemSize()`. +- X10D.Unity: Added `Vector2Int.ToSystemVector()`. +- X10D.Unity: Added `Vector2Int.WithX()`. +- X10D.Unity: Added `Vector2Int.WithY()`. +- X10D.Unity: Added `Vector3.Deconstruct()`. +- X10D.Unity: Added `Vector3.Round([float])`. +- X10D.Unity: Added `Vector3Int.Deconstruct()`. +- X10D.Unity: Added `Vector3Int.ToSystemVector()`. +- X10D.Unity: Added `Vector3Int.WithX()`. +- X10D.Unity: Added `Vector3Int.WithY()`. +- X10D.Unity: Added `Vector3Int.WithZ()`. +- X10D.Unity: Added `Vector4.Deconstruct()`. +- X10D.Unity: Added `Vector4.Round([float])` +- X10D.Unity: Added `WaitForFrames` yield instruction. +- X10D.Unity: Added `WaitForKeyDown` yield instruction. +- X10D.Unity: Added `WaitForKeyUp` yield instruction. +- X10D.Unity: Added `WaitForSecondsNoAlloc` yield instruction. +- X10D.Unity: Added `WaitForSecondsRealtimeNoAlloc` yield instruction. +- X10D.Unity: Added `WaitForTimeSpanNoAlloc` yield instruction. +- X10D.Unity: Added `WaitForTimeSpanRealtimeNoAlloc` yield instruction. + +### Fixed + +- X10D: `T[].Clear` will now correctly clear the specified range of elements by + using the [GetOffsetAndLength](https://learn.microsoft.com/en-us/dotnet/api/system.range.getoffsetandlength?view=net-7.0) + method. +- X10D: `Stream.ReadSingle(Endianness)` now returns a float instead of a double. +- X10D: Fixed `Stream.ReadDouble(Endianness)`, `Stream.ReadSingle(Endianness)`, `Stream.WriteDouble(double, Endianness)`, + `Stream.WriteSingle(float, Endianness)` writing and reading the wrong endianness. + +### Changed + +- X10D: Performance for integer `Unpack` has been dramatically improved using vector parallelization. (#70) +- X10D: Performance for `MemberInfo.HasCustomAttribute` has been improved. (#70) +- X10D: Performance for `Rune.Repeat` has been improved. (#70) +- X10D: `TimeSpanParser.TryParse` now accepts a nullable string, and returns false if this input is null or empty. +- X10D.Unity: `Singleton` now caches instance where possible. +- X10D.Unity: Conversions between equivalent structures in `System.Numerics` and `UnityEngine` now make use of "unsafe" + reinterpretation, rather than constructing new instances. (#70) + +## [3.1.0] - 2022-05-13 + +### Added + - Reintroduced Unity support -- X10D: Added `Color.Inverted()` (#54) -- X10D: Added `Color.WithA()` (#55) -- X10D: Added `Color.WithB()` (#55) -- X10D: Added `Color.WithG()` (#55) -- X10D: Added `Color.WithR()` (#55) -- X10D: Added `ICollection.ClearAndDisposeAll()` -- X10D: Added `ICollection.ClearAndDisposeAllAsync()` -- X10D: Added `IEnumerable.For()` (#50) -- X10D: Added `IEnumerable.ForEach()` (#50) -- X10D: Added `IEnumerable.DisposeAll()` -- X10D: Added `IEnumerable.DisposeAllAsync()` -- X10D: Added `char.IsEmoji` -- X10D: Added `ReadOnlySpan.Count(Predicate)` -- X10D: Added `Rune.IsEmoji` -- X10D: Added `Span.Count(Predicate)` -- X10D: Added `string.IsEmoji` -- X10D: Added `Vector2.WithX()` (#56) -- X10D: Added `Vector2.WithY()` (#56) -- X10D: Added `Vector3.WithX()` (#56) -- X10D: Added `Vector3.WithY()` (#56) -- X10D: Added `Vector3.WithZ()` (#56) -- X10D: Added `Vector4.WithX()` (#56) -- X10D: Added `Vector4.WithY()` (#56) -- X10D: Added `Vector4.WithZ()` (#56) -- X10D: Added `Vector4.WithW()` (#56) -- X10D.Unity: Added `Singleton` class -- X10D.Unity: Added `Color.Inverted()` (#54) -- X10D.Unity: Added `Color.WithA()` (#55) -- X10D.Unity: Added `Color.WithB()` (#55) -- X10D.Unity: Added `Color.WithG()` (#55) -- X10D.Unity: Added `Color.WithR()` (#55) -- X10D.Unity: Added `Color32.Inverted()` (#54) -- X10D.Unity: Added `Color32.WithA()` (#55) -- X10D.Unity: Added `Color32.WithB()` (#55) -- X10D.Unity: Added `Color32.WithG()` (#55) -- X10D.Unity: Added `Color32.WithR()` (#55) -- X10D.Unity: Added `Component.GetComponentsInChildrenOnly()` -- X10D.Unity: Added `GameObject.GetComponentsInChildrenOnly()` -- X10D.Unity: Added `GameObject.LookAt(GameObject[, Vector3])` -- X10D.Unity: Added `GameObject.LookAt(Transform[, Vector3])` -- X10D.Unity: Added `GameObject.LookAt(Vector3[, Vector3])` -- X10D.Unity: Added `GameObject.SetLayerRecursively(int)` (#57) -- X10D.Unity: Added `GameObject.SetParent(GameObject[, bool])` -- X10D.Unity: Added `GameObject.SetParent(Transform[, bool])` -- X10D.Unity: Added `System.Numerics.Quaternion.ToUnityQuaternion()` -- X10D.Unity: Added `Quaternion.ToSystemQuaternion()` -- X10D.Unity: Added `Random.NextColorArgb()` -- X10D.Unity: Added `Random.NextColor32Argb()` -- X10D.Unity: Added `Random.NextColorRgb()` -- X10D.Unity: Added `Random.NextColor32Rgb()` -- X10D.Unity: Added `Random.NextRotation()` -- X10D.Unity: Added `Random.NextRotationUniform()` -- X10D.Unity: Added `Random.NextUnitVector2()` -- X10D.Unity: Added `Random.NextUnitVector3()` -- X10D.Unity: Added `Transform.LookAt(GameObject[, Vector3])` -- X10D.Unity: Added `Transform.SetParent(GameObject[, bool])` -- X10D.Unity: Added `System.Numerics.Vector2.ToUnityVector()` -- X10D.Unity: Added `System.Numerics.Vector3.ToUnityVector()` -- X10D.Unity: Added `System.Numerics.Vector4.ToUnityVector()` -- X10D.Unity: Added `Vector2.ToSystemVector()` -- X10D.Unity: Added `Vector3.ToSystemVector()` -- X10D.Unity: Added `Vector4.ToSystemVector()` -- X10D.Unity: Added `Vector2.WithX()` (#56) -- X10D.Unity: Added `Vector2.WithY()` (#56) -- X10D.Unity: Added `Vector3.WithX()` (#56) -- X10D.Unity: Added `Vector3.WithY()` (#56) -- X10D.Unity: Added `Vector3.WithZ()` (#56) -- X10D.Unity: Added `Vector4.WithX()` (#56) -- X10D.Unity: Added `Vector4.WithY()` (#56) -- X10D.Unity: Added `Vector4.WithZ()` (#56) -- X10D.Unity: Added `Vector4.WithW()` (#56) +- X10D: Added `Color.Inverted()`. (#54) +- X10D: Added `Color.WithA()`. (#55) +- X10D: Added `Color.WithB()`. (#55) +- X10D: Added `Color.WithG()`. (#55) +- X10D: Added `Color.WithR()`. (#55) +- X10D: Added `ICollection.ClearAndDisposeAll()`. +- X10D: Added `ICollection.ClearAndDisposeAllAsync()`. +- X10D: Added `IEnumerable.For()`. (#50) +- X10D: Added `IEnumerable.ForEach()`. (#50) +- X10D: Added `IEnumerable.DisposeAll()`. +- X10D: Added `IEnumerable.DisposeAllAsync()`. +- X10D: Added `char.IsEmoji`. +- X10D: Added `ReadOnlySpan.Count(Predicate)`. +- X10D: Added `Rune.IsEmoji`. +- X10D: Added `Span.Count(Predicate)`. +- X10D: Added `string.IsEmoji`. +- X10D: Added `Vector2.WithX()`. (#56) +- X10D: Added `Vector2.WithY()`. (#56) +- X10D: Added `Vector3.WithX()`. (#56) +- X10D: Added `Vector3.WithY()`. (#56) +- X10D: Added `Vector3.WithZ()`. (#56) +- X10D: Added `Vector4.WithX()`. (#56) +- X10D: Added `Vector4.WithY()`. (#56) +- X10D: Added `Vector4.WithZ()`. (#56) +- X10D: Added `Vector4.WithW()`. (#56) +- X10D.Unity: Added `Singleton` class. +- X10D.Unity: Added `Color.Inverted()`. (#54) +- X10D.Unity: Added `Color.WithA()`. (#55) +- X10D.Unity: Added `Color.WithB()`. (#55) +- X10D.Unity: Added `Color.WithG()`. (#55) +- X10D.Unity: Added `Color.WithR()`. (#55) +- X10D.Unity: Added `Color32.Inverted()`. (#54) +- X10D.Unity: Added `Color32.WithA()`. (#55) +- X10D.Unity: Added `Color32.WithB()`. (#55) +- X10D.Unity: Added `Color32.WithG()`. (#55) +- X10D.Unity: Added `Color32.WithR()`. (#55) +- X10D.Unity: Added `Component.GetComponentsInChildrenOnly()`. +- X10D.Unity: Added `GameObject.GetComponentsInChildrenOnly()`. +- X10D.Unity: Added `GameObject.LookAt(GameObject[, Vector3])`. +- X10D.Unity: Added `GameObject.LookAt(Transform[, Vector3])`. +- X10D.Unity: Added `GameObject.LookAt(Vector3[, Vector3])`. +- X10D.Unity: Added `GameObject.SetLayerRecursively(int)`. (#57) +- X10D.Unity: Added `GameObject.SetParent(GameObject[, bool])`. +- X10D.Unity: Added `GameObject.SetParent(Transform[, bool])`. +- X10D.Unity: Added `System.Numerics.Quaternion.ToUnityQuaternion()`. +- X10D.Unity: Added `Quaternion.ToSystemQuaternion()`. +- X10D.Unity: Added `Random.NextColorArgb()`. +- X10D.Unity: Added `Random.NextColor32Argb()`. +- X10D.Unity: Added `Random.NextColorRgb()`. +- X10D.Unity: Added `Random.NextColor32Rgb()`. +- X10D.Unity: Added `Random.NextRotation()`. +- X10D.Unity: Added `Random.NextRotationUniform()`. +- X10D.Unity: Added `Random.NextUnitVector2()`. +- X10D.Unity: Added `Random.NextUnitVector3()`. +- X10D.Unity: Added `Transform.LookAt(GameObject[, Vector3])`. +- X10D.Unity: Added `Transform.SetParent(GameObject[, bool])`. +- X10D.Unity: Added `System.Numerics.Vector2.ToUnityVector()`. +- X10D.Unity: Added `System.Numerics.Vector3.ToUnityVector()`. +- X10D.Unity: Added `System.Numerics.Vector4.ToUnityVector()`. +- X10D.Unity: Added `Vector2.ToSystemVector()`. +- X10D.Unity: Added `Vector3.ToSystemVector()`. +- X10D.Unity: Added `Vector4.ToSystemVector()`. +- X10D.Unity: Added `Vector2.WithX()`. (#56) +- X10D.Unity: Added `Vector2.WithY()`. (#56) +- X10D.Unity: Added `Vector3.WithX()`. (#56) +- X10D.Unity: Added `Vector3.WithY()`. (#56) +- X10D.Unity: Added `Vector3.WithZ()`. (#56) +- X10D.Unity: Added `Vector4.WithX()`. (#56) +- X10D.Unity: Added `Vector4.WithY()`. (#56) +- X10D.Unity: Added `Vector4.WithZ()`. (#56) +- X10D.Unity: Added `Vector4.WithW()`. (#56) -## [3.0.0] +## [3.0.0] - 2022-04-30 -In the midst of writing these release notes, I may have missed some important changes. If you notice an API change that is not documented here, +In the midst of writing these release notes, I may have missed some important changes. If you notice an API change that is not +documented here, please [open an issue](https://github.com/oliverbooth/X10D/issues)! ### Added -- Added `T.AsArrayValue()` -- Added `T.AsEnumerableValue()` -- Added `T.RepeatValue(int)` -- Added `T.ToJson([JsonSerializerOptions])` -- Added `string.FromJson([JsonSerializerOptions])` -- Added `T[].AsReadOnly()` -- Added `T[].Clear([Range])` and `T[].Clear(int, int)` -- Added `DateTime.IsLeapYear()` -- Added `DateTimeOffset.IsLeapYear()` -- Added `Endianness` enum -- Added `FileInfo.GetHash()` -- Added `FileInfo.TryWriteHash(Span, out int)` -- Added `IComparable.Clamp(T, T)` - this supersedes `Clamp` defined for hard-coded numeric types (#24) -- Added `IComparable.GreaterThan(T2)` (#22) -- Added `IComparable.GreaterThanOrEqualTo(T2)` (#22) -- Added `IComparable.LessThan(T2)` (#22) -- Added `IComparable.LessThanOrEqualTo(T2)` (#22) -- Added `IComparable.Max(T)` (#23) -- Added `IComparable.Min(T)` (#23) + +- Added `T.AsArrayValue()`. +- Added `T.AsEnumerableValue()`. +- Added `T.RepeatValue(int)`. +- Added `T.ToJson([JsonSerializerOptions])`. +- Added `string.FromJson([JsonSerializerOptions])`. +- Added `T[].AsReadOnly()`. +- Added `T[].Clear([Range])` and `T[].Clear(int, int)`. +- Added `DateTime.IsLeapYear()`. +- Added `DateTimeOffset.IsLeapYear()`. +- Added `Endianness` enum. +- Added `FileInfo.GetHash()`. +- Added `FileInfo.TryWriteHash(Span, out int)`. +- Added `IComparable.Clamp(T, T)` - this supersedes `Clamp` defined for hard-coded numeric types. (#24) +- Added `IComparable.GreaterThan(T2)`. (#22) +- Added `IComparable.GreaterThanOrEqualTo(T2)`. (#22) +- Added `IComparable.LessThan(T2)`. (#22) +- Added `IComparable.LessThanOrEqualTo(T2)`. (#22) +- Added `IComparable.Max(T)`. (#23) +- Added `IComparable.Min(T)`. (#23) - Added `IDictionary.AddOrUpdate()` -- Added `IEnumerable.Product()` and `IEnumerable.Product(Func, TResult)` for all built-in numeric types, computing the product of all (optionally transformed) elements -- Added `IList.Fill(T)` and `IList.Fill(T, int, int)` -- Added `IPAddress.IsIPv4()` and `IPAddress.IsIPv6()` -- Added `IReadOnlyList.Pack8Bit()` -- Added `IReadOnlyList.Pack16Bit()` -- Added `IReadOnlyList.Pack32Bit()` -- Added `IReadOnlyList.Pack64Bit()` -- Added `MemberInfo.HasCustomAttribute()` and `MemberInfo.HasCustomAttribute(Type)` -- Added `defaultValue` overload for `MemberInfo.SelectFromCustomAttribute()` -- Added `Random.Next()` (returns a random field from a specified enum type) -- Added `Random.NextByte([byte[, byte]])` -- Added `Random.NextColorArgb()`, returning a random color in RGB space, including random alpha -- Added `Random.NextColorRgb()`, returning a random color in RGB space with alpha 255 -- Added `Random.NextDouble(double[, double])` -- Added `Random.NextInt16([short[, short]])` -- Added `Random.NextSingle(float[, float])` (#34) -- Added `Random.NextUnitVector2()` -- Added `Random.NextUnitVector3()` -- Added `Random.NextRotation()` -- Added `Rune.Repeat(int)` -- Added `byte.IsEven()` -- Added `byte.IsOdd()` -- Added `byte.IsPrime()` -- Added `byte.UnpackBits()` -- Added `byte.RangeTo(byte)`, `byte.RangeTo(short)`, `byte.RangeTo(int)`, and `byte.RangeTo(long)` -- Added `int.Mod(int)` -- Added `int.RangeTo(int)`, and `int.RangeTo(long)` -- Added `int.UnpackBits()` -- Added `long.Mod(long)` -- Added `long.RangeTo(long)` -- Added `long.UnpackBits()` -- Added `sbyte.IsEven()` -- Added `sbyte.IsOdd()` -- Added `sbyte.IsPrime()` -- Added `sbyte.Mod(sbyte)` -- Added `short.IsEven()` -- Added `short.IsOdd()` -- Added `short.Mod(short)` -- Added `short.RangeTo(short)`, `short.RangeTo(int)`, and `short.RangeTo(long)` -- Added `short.UnpackBits()` -- Added `string.IsPalindrome()` -- Added `Stream.GetHash()` -- Added `Stream.TryWriteHash(Span, out int)` -- Added `Stream.ReadInt16([Endian])` -- Added `Stream.ReadInt32([Endian])` -- Added `Stream.ReadInt64([Endian])` -- Added `Stream.ReadUInt16([Endian])` -- Added `Stream.ReadUInt32([Endian])` -- Added `Stream.ReadUInt64([Endian])` -- Added `Stream.Write(short, [Endian])` -- Added `Stream.Write(int, [Endian])` -- Added `Stream.Write(long, [Endian])` -- Added `Stream.Write(ushort, [Endian])` -- Added `Stream.Write(uint, [Endian])` -- Added `Stream.Write(ulong, [Endian])` -- Added `TimeSpan.Ago()` -- Added `TimeSpan.FromNow()` -- Added `TimeSpanParser.TryParse` which supersedes `TimeSpanParser.Parse` -- Added `Sqrt()` and `ComplexSqrt()` for all built-in decimal types -- Added `All()` and `Any()` for `Span` and `ReadOnlySpan`, mimicking the corresponding methods in `System.Linq` -- Added `Sign()` for built-in numeric types. For unsigned types, this never returns -1 -- Added `Type.Implements()` and `Type.Implements(Type)` (#25) -- Added `Type.Inherits()` and `Type.Inherits(Type)` (#25) -- Added `DigitalRoot` function for built-in integer types -- Added `Factorial` function for built-in integer types -- Added `HostToNetworkOrder` and `NetworkToHostOrder` for `short`, `int`, and `long` -- Added `IsLeapYear` function for `DateTime` and `DateTimeOffset`, as well as built-in numeric types -- Added `MultiplicativePersistence` function for built-in integer types -- Added `RotateLeft` and `RotateRight` for built-in integer types -- Added trigonometric functions for built-in numeric types, including `Acos()`, `Asin()`, `Atan()`, `Atan2()`, `Cos()`, `Sin()`, `Tan()` (#49) -- Added time-related extension methods for built-in numeric types, including `Milliseconds()`, `Seconds()`, `Minutes()`, `Hours()`, `Days()`, and `Weeks()`. `Ticks()` is also available, but only for integers; not floating point -- Added `StringBuilderReader` (inheriting `TextReader`) which allows reading from a `StringBuilder` without consuming the underlying string -- Added extension methods for `System.Decimal` +- Added `IEnumerable.Product()` and `IEnumerable.Product(Func, TResult)` for all built-in + numeric types, computing the product of all (optionally transformed) elements. +- Added `IList.Fill(T)` and `IList.Fill(T, int, int)`. +- Added `IPAddress.IsIPv4()` and `IPAddress.IsIPv6()`. +- Added `IReadOnlyList.Pack8Bit()`. +- Added `IReadOnlyList.Pack16Bit()`. +- Added `IReadOnlyList.Pack32Bit()`. +- Added `IReadOnlyList.Pack64Bit()`. +- Added `MemberInfo.HasCustomAttribute()` and `MemberInfo.HasCustomAttribute(Type)`. +- Added `defaultValue` overload for `MemberInfo.SelectFromCustomAttribute()`. +- Added `Random.Next()` (returns a random field from a specified enum type). +- Added `Random.NextByte([byte[, byte]])`. +- Added `Random.NextColorArgb()`, returning a random color in RGB space, including random alpha. +- Added `Random.NextColorRgb()`, returning a random color in RGB space with alpha 255. +- Added `Random.NextDouble(double[, double])`. +- Added `Random.NextInt16([short[, short]])`. +- Added `Random.NextSingle(float[, float])`. (#34) +- Added `Random.NextUnitVector2()`. +- Added `Random.NextUnitVector3()`. +- Added `Random.NextRotation()`. +- Added `Rune.Repeat(int)`. +- Added `byte.IsEven()`. +- Added `byte.IsOdd()`. +- Added `byte.IsPrime()`. +- Added `byte.UnpackBits()`. +- Added `byte.RangeTo(byte)`, `byte.RangeTo(short)`, `byte.RangeTo(int)`, and `byte.RangeTo(long)`. +- Added `int.Mod(int)`. +- Added `int.RangeTo(int)`, and `int.RangeTo(long)`. +- Added `int.UnpackBits()`. +- Added `long.Mod(long)`. +- Added `long.RangeTo(long)`. +- Added `long.UnpackBits()`. +- Added `sbyte.IsEven()`. +- Added `sbyte.IsOdd()`. +- Added `sbyte.IsPrime()`. +- Added `sbyte.Mod(sbyte)`. +- Added `short.IsEven()`. +- Added `short.IsOdd()`. +- Added `short.Mod(short)`. +- Added `short.RangeTo(short)`, `short.RangeTo(int)`, and `short.RangeTo(long)`. +- Added `short.UnpackBits()`. +- Added `string.IsPalindrome()`. +- Added `Stream.GetHash()`. +- Added `Stream.TryWriteHash(Span, out int)`. +- Added `Stream.ReadInt16([Endian])`. +- Added `Stream.ReadInt32([Endian])`. +- Added `Stream.ReadInt64([Endian])`. +- Added `Stream.ReadUInt16([Endian])`. +- Added `Stream.ReadUInt32([Endian])`. +- Added `Stream.ReadUInt64([Endian])`. +- Added `Stream.Write(short, [Endian])`. +- Added `Stream.Write(int, [Endian])`. +- Added `Stream.Write(long, [Endian])`. +- Added `Stream.Write(ushort, [Endian])`. +- Added `Stream.Write(uint, [Endian])`. +- Added `Stream.Write(ulong, [Endian])`. +- Added `TimeSpan.Ago()`. +- Added `TimeSpan.FromNow()`. +- Added `TimeSpanParser.TryParse` which supersedes `TimeSpanParser.Parse`. +- Added `Sqrt()` and `ComplexSqrt()` for all built-in decimal types. +- Added `All()` and `Any()` for `Span` and `ReadOnlySpan`, mimicking the corresponding methods in `System.Linq`. +- Added `Sign()` for built-in numeric types. For unsigned types, this never returns -1. +- Added `Type.Implements()` and `Type.Implements(Type)`. (#25) +- Added `Type.Inherits()` and `Type.Inherits(Type)`. (#25) +- Added `DigitalRoot` function for built-in integer types. +- Added `Factorial` function for built-in integer types. +- Added `HostToNetworkOrder` and `NetworkToHostOrder` for `short`, `int`, and `long`. +- Added `IsLeapYear` function for `DateTime` and `DateTimeOffset`, as well as built-in numeric types. +- Added `MultiplicativePersistence` function for built-in integer types. +- Added `RotateLeft` and `RotateRight` for built-in integer types. +- Added trigonometric functions for built-in numeric types, + including `Acos()`, `Asin()`, `Atan()`, `Atan2()`, `Cos()`, `Sin()`, `Tan()` (#49) +- Added time-related extension methods for built-in numeric types, + including `Milliseconds()`, `Seconds()`, `Minutes()`, `Hours()`, `Days()`, and `Weeks()`. `Ticks()` is also available, but only + for integers; not floating point. +- Added `StringBuilderReader` (inheriting `TextReader`) which allows reading from a `StringBuilder` without consuming the + underlying string. +- Added extension methods for `System.Decimal`. ### Changed -- Updated to .NET 6 (#45) -- Methods defined to accept `byte[]` have been changed accept `IReadOnlyList` -- Extension methods are now defined in appropriate child namespaces to reduce the risk of name collisions (#7) -- `char[].Random(int)`, `char[].Random(int, int)`, `IEnumerable.Random(int)`, and `IEnumerable.Random(int, int)` have been redefined as `Random.NextString(IReadOnlyList, int)` -- `IComparable.Between(T, T)` has been redefined as `IComparable.Between(T2, T3, [InclusiveOptions])` to allow comparison of disparate yet comparable types, and also offers inclusivity options -- `DateTime` extensions now wrap `DateTimeOffset` extensions -- `DateTime.ToUnixTimestamp([bool])` has been redefined as `DateTime.ToUnixTimeMilliseconds()` and `DateTime.ToUnixTimeSeconds()` -- `Dictionary.ToGetParameters()`, `IDictionary.ToGetParameters()`, and `IReadOnlyDictionary.ToGetParameters()`, has been redefined as `IEnumerable>.ToGetParameters()` -- `Dictionary.ToConnectionString()`, `IDictionary.ToConnectionString()`, and `IReadOnlyDictionary.ToConnectionString()`, has been redefined as `IEnumerable>.ToConnectionString()` -- `IList.OneOf([Random])` and `IEnumerable.OneOf([Random])` have been redefined as `Random.NextFrom(IEnumerable)` to fall in line with the naming convention of `System.Random` (#21) -- `IList.Shuffle([Random])` now uses a Fisher-Yates shuffle implementation -- `IList.Shuffle([Random])` has been repurposed to shuffle the list in place, rather than returning a new enumerable - - `IEnumerable.Shuffle([Random])` has been renamed to `IEnumerable.Shuffled([Random])` to avoid confusion with `IList.Shuffle([Random])` -- `Random.CoinToss()` has been redefined as `Random.NextBoolean()` to fall in line with the naming convention of `System.Random` -- `Random.OneOf(T[])` and `Random.OneOf(IList)` have been redefined as `Random.NextFrom(IEnumerable)` to fall in line with the naming convention of `System.Random` -- `Enum.Next([bool])` and `Enum.Previous([bool])` have been redefined as `Enum.Next()`, `Enum.Previous()`, `Enum.NextUnchecked()`, `Enum.PreviousUnchecked()`. The `Unchecked` variants of these methods do not perform index validation, and will throw `IndexOutOfRangeException` when attempting to access an invalid index. The checked variants will perform a modulo to wrap the index -- Seperated `string.WithAlternative(string, [bool])` to `string.WithEmptyAlternative(string)` and `string.WithWhiteSpaceAlternative(string)` -- `string.AsNullIfEmpty()` and `string.AsNullIfWhiteSpace()` now accept a nullable `string` + +- Updated to .NET 6. (#45) +- Methods defined to accept `byte[]` have been changed accept `IReadOnlyList`. +- Extension methods are now defined in appropriate child namespaces to reduce the risk of name collisions. (#7) +- `char[].Random(int)`, `char[].Random(int, int)`, `IEnumerable.Random(int)`, and `IEnumerable.Random(int, int)` have + been redefined as `Random.NextString(IReadOnlyList, int)`. +- `IComparable.Between(T, T)` has been redefined as `IComparable.Between(T2, T3, [InclusiveOptions])` to allow comparison + of disparate yet comparable types, and also offers inclusivity options. +- `DateTime` extensions now wrap `DateTimeOffset` extensions. +- `DateTime.ToUnixTimestamp([bool])` has been redefined as `DateTime.ToUnixTimeMilliseconds()` and `DateTime.ToUnixTimeSeconds()`. +- `Dictionary.ToGetParameters()`, `IDictionary.ToGetParameters()`, + and `IReadOnlyDictionary.ToGetParameters()`, has been redefined as `IEnumerable>.ToGetParameters()`. +- `Dictionary.ToConnectionString()`, `IDictionary.ToConnectionString()`, + and `IReadOnlyDictionary.ToConnectionString()`, has been redefined + as `IEnumerable>.ToConnectionString()`. +- `IList.OneOf([Random])` and `IEnumerable.OneOf([Random])` have been redefined as `Random.NextFrom(IEnumerable)` to + fall in line with the naming convention of `System.Random`. (#21) +- `IList.Shuffle([Random])` now uses a Fisher-Yates shuffle implementation. +- `IList.Shuffle([Random])` has been repurposed to shuffle the list in place, rather than returning a new enumerable. + - `IEnumerable.Shuffle([Random])` has been renamed to `IEnumerable.Shuffled([Random])` to avoid confusion + with `IList.Shuffle([Random])`. +- `Random.CoinToss()` has been redefined as `Random.NextBoolean()` to fall in line with the naming convention of `System.Random`. +- `Random.OneOf(T[])` and `Random.OneOf(IList)` have been redefined as `Random.NextFrom(IEnumerable)` to fall in + line with the naming convention of `System.Random`. +- `Enum.Next([bool])` and `Enum.Previous([bool])` have been redefined. + as `Enum.Next()`, `Enum.Previous()`, `Enum.NextUnchecked()`, `Enum.PreviousUnchecked()`. The `Unchecked` variants of these + methods do not perform index validation, and will throw `IndexOutOfRangeException` when attempting to access an invalid index. + The checked variants will perform a modulo to wrap the index. +- Seperated `string.WithAlternative(string, [bool])` to `string.WithEmptyAlternative(string)` + and `string.WithWhiteSpaceAlternative(string)`. +- `string.AsNullIfEmpty()` and `string.AsNullIfWhiteSpace()` now accept a nullable `string`. - `IEnumerable.GetString([Encoding])` has been renamed to `IReadOnlyList.ToString` and its `Encoding` parameter has - been made non-optional + been made non-optional. - Fixed a bug where `IEnumerable>.ToConnectionString()` would not sanitize types with custom `ToString()` - implementation (#20) - Renamed `string.Random(int[, Random])` to `string.Randomize(int[, Random])` -- Redefined `char[].Random(int)`, `char[].Random(int, Random)`, `IEnumerable.Random(int)`, and `IEnumerable.Random(int, Random)`, as `Random.NextString(IReadOnlyList, int)` + implementation. (#20) + Renamed `string.Random(int[, Random])` to `string.Randomize(int[, Random])`. +- Redefined `char[].Random(int)`, `char[].Random(int, Random)`, `IEnumerable.Random(int)`, + and `IEnumerable.Random(int, Random)`, as `Random.NextString(IReadOnlyList, int)`. - Improved performance for: - `string.IsLower()` - `string.IsUpper()` @@ -207,46 +394,52 @@ please [open an issue](https://github.com/oliverbooth/X10D/issues)! - `TimeSpanParser` ### Removed -- Indefinitely suspended Unity support, until such a time that Unity can be updated to a newer version of .NET. See: https://forum.unity.com/threads/unity-future-net-development-status.1092205/ -- Removed `bool.And(bool)` -- Removed `bool.NAnd(bool)` -- Removed `bool.NOr(bool)` -- Removed `bool.Not(bool)` -- Removed `bool.Or(bool)` -- Removed `bool.ToByte()` -- Removed `bool.ToInt16()` -- Removed `bool.ToInt64()` -- Removed `bool.XNOr()` -- Removed `bool.XOr()` -- Removed `IConvertible.To([IFormatProvider])` (#13) -- Removed `IConvertible.ToOrDefault([IFormatProvider])` (#13) -- Removed `IConvertible.ToOrDefault(out T, [IFormatProvider])` (#13) -- Removed `IConvertible.ToOrNull([IFormatProvider])` (#13) -- Removed `IConvertible.ToOrNull(out T, [IFormatProvider])` (#13) -- Removed `IConvertible.ToOrOther(T, [IFormatProvider])` (#13) -- Removed `IConvertible.ToOrOther(out T, T, [IFormatProvider])` (#13) -- Removed `IEnumerable.Split(int)` - this functionality is now provided by .NET in the form of the `Chunk` method. See: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-6.0 -- Removed `MemberInfo.GetDefaultValue()` (use `SelectFromCustomAttribute()` instead) -- Removed `MemberInfo.GetDescription()` (use `SelectFromCustomAttribute()` instead) -- Removed `int.ToBoolean()` -- Removed `long.ToBoolean()` -- Removed `short.ToBoolean()` -- Removed `uint.ToBoolean()` -- Removed `ushort.ToBoolean()` -- Removed `ulong.ToBoolean()` -- Removed `WaitHandle.WaitOneAsync()`. For suspensions of execution during asynchronous operations, favour `TaskCompletionSource` or `TaskCompletionSource`. See: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource?view=net-6.0 and https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1?view=net-6.0 + +- Indefinitely suspended Unity support, until such a time that Unity can be updated to a newer version of .NET. + See: https://forum.unity.com/threads/unity-future-net-development-status.1092205/ +- Removed `bool.And(bool)`. +- Removed `bool.NAnd(bool)`. +- Removed `bool.NOr(bool)`. +- Removed `bool.Not(bool)`. +- Removed `bool.Or(bool)`. +- Removed `bool.ToByte()`. +- Removed `bool.ToInt16()`. +- Removed `bool.ToInt64()`. +- Removed `bool.XNOr()`. +- Removed `bool.XOr()`. +- Removed `IConvertible.To([IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrDefault([IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrDefault(out T, [IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrNull([IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrNull(out T, [IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrOther(T, [IFormatProvider])`. (#13) +- Removed `IConvertible.ToOrOther(out T, T, [IFormatProvider])`. (#13) +- Removed `IEnumerable.Split(int)` - this functionality is now provided by .NET in the form of the `Chunk` method. + See: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-6.0. +- Removed `MemberInfo.GetDefaultValue()` (use `SelectFromCustomAttribute()` instead). +- Removed `MemberInfo.GetDescription()` (use `SelectFromCustomAttribute()` instead). +- Removed `int.ToBoolean()`. +- Removed `long.ToBoolean()`. +- Removed `short.ToBoolean()`. +- Removed `uint.ToBoolean()`. +- Removed `ushort.ToBoolean()`. +- Removed `ulong.ToBoolean()`. +- Removed `WaitHandle.WaitOneAsync()`. For suspensions of execution during asynchronous operations, favour `TaskCompletionSource` + or `TaskCompletionSource`. + See: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource?view=net-6.0 + and https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1?view=net-6.0 ## [2.6.0] - 2020-10-20 ### Added -- Add `string.AsNullIfEmpty()` +- Add `string.AsNullIfEmpty()`. - Returns the current string, or `null` if the current string is null or empty. -- Add `string.AsNullIfWhiteSpace()` +- Add `string.AsNullIfWhiteSpace()`. - Returns the current string, or `null` if the current string is null, empty, or consists of only whitespace. -- Add `string.Reverse()` - - Reverses the current string -- Add `string.WithAlternative()` +- Add `string.Reverse()`. + - Reverses the current string. +- Add `string.WithAlternative()`. - Returns the current string, or an alternative value if the current string is null or empty, or optionally if the current string consists of only whitespace. @@ -262,30 +455,30 @@ please [open an issue](https://github.com/oliverbooth/X10D/issues)! ### Added -- `WaitHandle.WaitOneAsync()` - - Wraps `WaitHandle.WaitOne` as a `Task` -- Add support for Unity 2019.4.3f1 - - Add `GameObject.LookAt(GameObject)` - - Rotates the Transform on the current GameObject so that it faces the Transform on another GameObject - - Add `GameObject.LookAt(Transform)` - - Rotates the Transform on the current GameObject so that it faces another transform +- `WaitHandle.WaitOneAsync()`. + - Wraps `WaitHandle.WaitOne` as a `Task`. +- Add support for Unity 2019.4.3f1. + - Add `GameObject.LookAt(GameObject)`. + - Rotates the Transform on the current GameObject so that it faces the Transform on another GameObject. + - Add `GameObject.LookAt(Transform)`. + - Rotates the Transform on the current GameObject so that it faces another transform. - Add `Transform.LookAt(GameObject)` - - Rotates the current Transform so that it faces the Transform on another GameObject - - Add `Vector3.Round([float])` - - Returns a rounded Vector3 by calling `float.Round()` on each component - - Add `Vector3.WithX(float)` - - Returns a Vector3 with a new X component value - - Add `Vector3.WithY(float)` - - Returns a Vector3 with a new Y component value - - Add `Vector3.WithZ(float)` - - Returns a Vector3 with a new Z component value - - Add `Vector3.WithXY(float, float)` - - Returns a Vector3 with new X and Y component values - - Add `Vector3.WithXZ(float, float)` - - Returns a Vector3 with new X and Z component values - - Add `Vector3.WithYZ(float, float)` - - Returns a Vector3 with new Y and Z component values - - Add `BetterBehavior` (experimental wrapper over `MonoBehaviour`) + - Rotates the current Transform so that it faces the Transform on another GameObject. + - Add `Vector3.Round([float])`. + - Returns a rounded Vector3 by calling `float.Round()` on each component. + - Add `Vector3.WithX(float)`. + - Returns a Vector3 with a new X component value. + - Add `Vector3.WithY(float)`. + - Returns a Vector3 with a new Y component value. + - Add `Vector3.WithZ(float)`. + - Returns a Vector3 with a new Z component value. + - Add `Vector3.WithXY(float, float)`. + - Returns a Vector3 with new X and Y component values. + - Add `Vector3.WithXZ(float, float)`. + - Returns a Vector3 with new X and Z component values. + - Add `Vector3.WithYZ(float, float)`. + - Returns a Vector3 with new Y and Z component values. + - Add `BetterBehavior` (experimental wrapper over `MonoBehaviour`). ### Changed @@ -299,18 +492,18 @@ please [open an issue](https://github.com/oliverbooth/X10D/issues)! ### Added -- Add `string.ChangeEncoding(Encoding, Encoding)` - - Converts this string from one encoding to another -- Add `string.IsLower()` - - Determines if all alpha characters in this string are considered lowercase -- Add `string.IsUpper()` - - Determines if all alpha characters in this string are considered uppercase +- Add `string.ChangeEncoding(Encoding, Encoding)`. + - Converts this string from one encoding to another. +- Add `string.IsLower()`. + - Determines if all alpha characters in this string are considered lowercase. +- Add `string.IsUpper()`. + - Determines if all alpha characters in this string are considered uppercase. - Various extension methods with regards to reflection: - - `GetDefaultValue` and `GetDefaultValue` - gets the value stored in the member's `DefaultValue` attribute - - `GetDescription`- gets the value stored in the member's `Description` attribute + - `GetDefaultValue` and `GetDefaultValue` - gets the value stored in the member's `DefaultValue` attribute. + - `GetDescription`- gets the value stored in the member's `Description` attribute. - `SelectFromCustomAttribute` - Internally calls `GetCustomAttribute` and passes it to a `Func` so that - specific members may be selected + specific members may be selected. ### Changed @@ -324,26 +517,26 @@ please [open an issue](https://github.com/oliverbooth/X10D/issues)! ### Added -- Add `bool bool.And(bool)` - - Performs logical AND -- Add `bool bool.Or(bool)` - - Performs logical OR -- Add `bool bool.Not(bool)` - - Performs logical NOT -- Add `bool bool.XOr(bool)` - - Performs Logical XOR -- Add `bool bool.NAnd(bool)` - - Performs logical NAND -- Add `bool bool.NOr(bool)` - - Performs logical NOR -- Add `bool bool.XNOr(bool)` - - Performs logical XNOR -- Add `byte bool.ToByte()` - - 1 if `true`, 0 otherwise -- Add `short bool.ToInt16()` - - 1 if `true`, 0 otherwise -- Add `long bool.ToInt64()` - - 1 if `true`, 0 otherwise +- Add `bool bool.And(bool)`. + - Performs logical AND. +- Add `bool bool.Or(bool)`. + - Performs logical OR. +- Add `bool bool.Not(bool)`. + - Performs logical NOT. +- Add `bool bool.XOr(bool)`. + - Performs Logical XOR. +- Add `bool bool.NAnd(bool)`. + - Performs logical NAND. +- Add `bool bool.NOr(bool)`. + - Performs logical NOR. +- Add `bool bool.XNOr(bool)`. + - Performs logical XNOR. +- Add `byte bool.ToByte()`. + - 1 if `true`, 0 otherwise. +- Add `short bool.ToInt16()`. + - 1 if `true`, 0 otherwise. +- Add `long bool.ToInt64()`. + - 1 if `true`, 0 otherwise. ### Changed @@ -357,23 +550,24 @@ please [open an issue](https://github.com/oliverbooth/X10D/issues)! ### Added -- Add `IEnumerable.Split(int)` - - Performs the same operation as the previously defined `IEnumerable.Chunkify(int)`, except now accepts any type `T` +- Add `IEnumerable.Split(int)`. + - Performs the same operation as the previously defined `IEnumerable.Chunkify(int)`, except now accepts any type `T`. ### Changed -- Fix `DateTime.Last(DayOfWeek)` implementation - - Now returns the correct date/time for a given day of the week +- Fix `DateTime.Last(DayOfWeek)` implementation. + - Now returns the correct date/time for a given day of the week. ### Removed -- Remove `IEnumerable.Chunkify(int)` - - Replaced by a method of the same behaviour `IEnumerable.Split(int)` +- Remove `IEnumerable.Chunkify(int)`. + - Replaced by a method of the same behaviour `IEnumerable.Split(int)`. ## Earlier versions -### ***Not documented*** +Earlier versions of this package are undocumented and unlisted from package results. +[unreleased]: https://github.com/oliverbooth/X10D/compare/v3.1.0...develop [3.1.0]: https://github.com/oliverbooth/X10D/releases/tag/v3.1.0 [3.0.0]: https://github.com/oliverbooth/X10D/releases/tag/v3.0.0 [2.6.0]: https://github.com/oliverbooth/X10D/releases/tag/2.6.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d525e32..c1d7f77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ or submit a pull request. ### Pull request guidelines -This project uses C# 10.0 language features, and adheres to StyleCop rules with some minor adjustments. +This project uses C# 11.0 language features where feasible, and adheres to StyleCop rules with some minor adjustments. There is an `.editorconfig` included in this repository. For quick and painless pull requests, ensure that the analyzer does not throw warnings. @@ -13,7 +13,7 @@ throw warnings. Below are a few pointers to which you may refer, but keep in mind this is not an exhaustive list: -- Use C# 10.0 features where possible +- Use C# 11.0 features where possible - Try to ensure code is CLS-compliant. Where this is not possible, decorate methods with `CLSCompliantAttribute` and pass `false` - Follow all .NET guidelines and coding conventions. See https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions @@ -33,11 +33,11 @@ When in doubt, follow .NET guidelines. ### Tests When introducing a new extension method, you must ensure that you have also defined a unit test that asserts its correct behavior. -The code style guidelines and code-analysis rules apply to the `X10D.Tests` equally as much as `X10D`, although documentation may +The code style guidelines and code-analysis rules apply to the `X10D.Tests` as much as `X10D`, although documentation may be briefer. Refer to existing tests as a guideline. ### Disclaimer -In the event of a code style violation, a pull request may left open (or closed entirely) without merging. Keep in mind this does +In the event of a code style violation, a pull request may be left open (or closed entirely) without merging. Keep in mind this does not mean the theory or implementation of the method is inherently bad or rejected entirely (although if this is the case, it will be outlined) diff --git a/LICENSE.md b/LICENSE.md index ed7c8c7..62ddbca 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2022 Oliver Booth +Copyright (c) 2019-2023 Oliver Booth Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index eb336f7..85d2dbb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -

+

-GitHub Workflow Status -GitHub Issues -Coverage -NuGet Downloads -Stable Version -Nightly Version -MIT License +GitHub Workflow Status +GitHub Issues +Coverage +NuGet Downloads +Stable Version +Nightly Version +MIT License

### About @@ -17,18 +17,14 @@ X10D (pronounced *extend*), is a .NET package that provides extension methods fo ## Installation ### NuGet installation ```ps -Install-Package X10D -Version 3.1.0 +Install-Package X10D -Version 3.2.0 ``` ### Manual installation Download the [latest release](https://github.com/oliverbooth/X10D/releases/latest) from this repository and adding a direct assembly reference for your chosen platform. ### Unity installation -Starting with Unity 2021.2, support for .NET Standard 2.1 has been added. With this change, I am confident providing support for this version for the time being, with only minimal feature-loss. -To add X10D into your Unity project, goto the [Package Manager window](https://docs.unity3d.com/Manual/upm-ui.html), and choose to install from a Git URL, and use the URL https://github.com/oliverbooth/X10D.git#upm - -Parity with the main branch of X10D, and full .NET 6 feature support, is planned - but a timeline is not yet available. Unity plan to add .NET 6 support in the near future. -For more information, see [this forum post](https://forum.unity.com/threads/unity-future-net-development-status.1092205/). +For the Unity installation guide, refer to the [README.md in X10D.Unity](X10D.Unity/README.md). ## Features I'm planning on writing complete and extensive documentation in the near future. As of this time, feel free to browse the source or the API using your favourite IDE. @@ -38,4 +34,4 @@ For those familiar with the 2.6.0 API, please read [CHANGELOG.md](CHANGELOG.md) Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). ## License -X10D is released under the MIT License. See [here](https://github.com/oliverbooth/X10D/blob/master/LICENSE.md) for more details. +X10D is released under the MIT License. See [here](https://github.com/oliverbooth/X10D/blob/main/LICENSE.md) for more details. diff --git a/X10D.DSharpPlus/X10D.DSharpPlus.csproj b/X10D.DSharpPlus/X10D.DSharpPlus.csproj new file mode 100644 index 0000000..0bef06b --- /dev/null +++ b/X10D.DSharpPlus/X10D.DSharpPlus.csproj @@ -0,0 +1,70 @@ + + + + net7.0;net6.0;netstandard2.1 + 11.0 + true + true + Oliver Booth + en + https://github.com/oliverbooth/X10D + git + Extension methods on crack. + LICENSE.md + branding_Icon.png + + dotnet extension-methods + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) + true + 3.2.0 + enable + true + true + true + true + pdbonly + true + + + + true + + + + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix).0 + $(VersionPrefix).0 + + + + $(VersionPrefix)-$(VersionSuffix).$(BuildNumber) + $(VersionPrefix).$(BuildNumber) + $(VersionPrefix).$(BuildNumber) + + + + $(VersionPrefix) + $(VersionPrefix).0 + $(VersionPrefix).0 + + + + + + + + + True + + + + True + + + + True + + + + + diff --git a/X10D.DSharpPlus/src/Assembly.cs b/X10D.DSharpPlus/src/Assembly.cs new file mode 100644 index 0000000..4e11466 --- /dev/null +++ b/X10D.DSharpPlus/src/Assembly.cs @@ -0,0 +1 @@ +[assembly: CLSCompliant(false)] diff --git a/X10D.DSharpPlus/src/DiscordChannelExtensions.cs b/X10D.DSharpPlus/src/DiscordChannelExtensions.cs new file mode 100644 index 0000000..2734af9 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordChannelExtensions.cs @@ -0,0 +1,80 @@ +using DSharpPlus; +using DSharpPlus.Entities; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordChannelExtensions +{ + /// + /// Gets the category of this channel. + /// + /// The channel whose category to retrieve. + /// + /// The category of , or itself if it is already a category; + /// if this channel is not defined in a category. + /// + /// is . + public static DiscordChannel? GetCategory(this DiscordChannel channel) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(channel); +#else + if (channel is null) + { + throw new ArgumentNullException(nameof(channel)); + } +#endif + + while (true) + { + if (channel.IsCategory) + { + return channel; + } + + if (channel.Parent is not { } parent) + { + return null; + } + + channel = parent; + } + } + + /// + /// Normalizes a so that the internal client is assured to be a specified value. + /// + /// The to normalize. + /// The target client. + /// + /// A whose public values will match , but whose internal client + /// is . + /// + /// + /// is + /// -or- + /// is + /// + public static async Task NormalizeClientAsync(this DiscordChannel channel, DiscordClient client) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(channel); + ArgumentNullException.ThrowIfNull(client); +#else + if (channel is null) + { + throw new ArgumentNullException(nameof(channel)); + } + + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + return await client.GetChannelAsync(channel.Id).ConfigureAwait(false); + } +} diff --git a/X10D.DSharpPlus/src/DiscordClientExtensions.cs b/X10D.DSharpPlus/src/DiscordClientExtensions.cs new file mode 100644 index 0000000..f3d8964 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordClientExtensions.cs @@ -0,0 +1,79 @@ +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordClientExtensions +{ + /// + /// Instructs the client to automatically join all existing threads, and any newly-created threads. + /// + /// The whose events should be subscribed. + /// + /// to automatically rejoin a thread if this client was removed; otherwise, + /// . + /// + /// is . + public static void AutoJoinThreads(this DiscordClient client, bool rejoinIfRemoved = true) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(client); +#else + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + client.GuildAvailable += (_, args) => args.Guild.JoinAllThreadsAsync(); + client.ThreadCreated += (_, args) => args.Thread.JoinThreadAsync(); + + if (rejoinIfRemoved) + { + client.ThreadMembersUpdated += (_, args) => + { + if (args.RemovedMembers.Any(m => m.Id == client.CurrentUser.Id)) + return args.Thread.JoinThreadAsync(); + + return Task.CompletedTask; + }; + } + } + + /// + /// Gets a user by their ID. If the user is not found, is returned instead of + /// being thrown. + /// + /// The Discord client. + /// The ID of the user to retrieve. + /// is . + public static async Task GetUserOrNullAsync(this DiscordClient client, ulong userId) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(client); +#else + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + try + { + // we should never use exceptions for flow control but this is D#+ we're talking about. + // NotFoundException isn't even documented, and yet it gets thrown when a user doesn't exist. + // so this method should hopefully clearly express that - and at least using exceptions for flow control *here*, + // removes the need to do the same in consumer code. + // god I hate this. + return await client.GetUserAsync(userId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } +} diff --git a/X10D.DSharpPlus/src/DiscordEmbedBuilderExtensions.cs b/X10D.DSharpPlus/src/DiscordEmbedBuilderExtensions.cs new file mode 100644 index 0000000..4a23a34 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordEmbedBuilderExtensions.cs @@ -0,0 +1,239 @@ +using DSharpPlus.Entities; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordEmbedBuilderExtensions +{ + /// + /// Adds a field of any value type to the embed. + /// + /// The to modify. + /// The name of the embed field. + /// The value of the embed field. + /// to display this field inline; otherwise, . + /// The type of . + /// The current instance of ; that is, . + /// is . + public static DiscordEmbedBuilder AddField( + this DiscordEmbedBuilder builder, + string name, + T? value, + bool inline = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } +#endif + + return builder.AddField(name, value?.ToString(), inline); + } + + /// + /// Conditionally adds a field to the embed. + /// + /// The to modify. + /// The condition whose value is used to determine whether the field will be added. + /// The name of the embed field. + /// The value of the embed field. + /// to display this field inline; otherwise, . + /// The type of . + /// The current instance of ; that is, . + /// is . + public static DiscordEmbedBuilder AddFieldIf( + this DiscordEmbedBuilder builder, + bool condition, + string name, + T? value, + bool inline = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } +#endif + + if (condition) + { + builder.AddField(name, value?.ToString(), inline); + } + + return builder; + } + + /// + /// Conditionally adds a field to the embed. + /// + /// The to modify. + /// The predicate whose return value is used to determine whether the field will be added. + /// The name of the embed field. + /// The value of the embed field. + /// to display this field inline; otherwise, . + /// The type of . + /// The current instance of ; that is, . + /// + /// is . + /// -or- + /// is . + /// + public static DiscordEmbedBuilder AddFieldIf( + this DiscordEmbedBuilder builder, + Func predicate, + string name, + T? value, + bool inline = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + if (predicate.Invoke()) + { + builder.AddField(name, value?.ToString(), inline); + } + + return builder; + } + + /// + /// Conditionally adds a field to the embed. + /// + /// The to modify. + /// The predicate whose return value is used to determine whether the field will be added. + /// The name of the embed field. + /// The delegate whose return value will be used as the value of the embed field. + /// to display this field inline; otherwise, . + /// The return type of . + /// The current instance of ; that is, . + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + public static DiscordEmbedBuilder AddFieldIf( + this DiscordEmbedBuilder builder, + Func predicate, + string name, + Func valueFactory, + bool inline = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(valueFactory); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (valueFactory is null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } +#endif + + if (predicate.Invoke()) + { + builder.AddField(name, valueFactory.Invoke()?.ToString(), inline); + } + + return builder; + } + + /// + /// Conditionally adds a field to the embed. + /// + /// The to modify. + /// The condition whose value is used to determine whether the field will be added. + /// The name of the embed field. + /// The delegate whose return value will be used as the value of the embed field. + /// to display this field inline; otherwise, . + /// The return type of . + /// The current instance of ; that is, . + /// + /// is . + /// -or- + /// is . + /// + public static DiscordEmbedBuilder AddFieldIf( + this DiscordEmbedBuilder builder, + bool condition, + string name, + Func valueFactory, + bool inline = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(valueFactory); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (valueFactory is null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } +#endif + + if (condition) + { + builder.AddField(name, valueFactory.Invoke()?.ToString(), inline); + } + + return builder; + } + + /// + /// Sets the embed's author. + /// + /// The embed builder to modify. + /// The author. + /// The current instance of . + public static DiscordEmbedBuilder WithAuthor(this DiscordEmbedBuilder builder, DiscordUser user) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(user); +#else + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (user is null) + { + throw new ArgumentNullException(nameof(user)); + } +#endif + + return builder.WithAuthor(user.GetUsernameWithDiscriminator(), iconUrl: user.AvatarUrl); + } +} diff --git a/X10D.DSharpPlus/src/DiscordGuildExtensions.cs b/X10D.DSharpPlus/src/DiscordGuildExtensions.cs new file mode 100644 index 0000000..c03016b --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordGuildExtensions.cs @@ -0,0 +1,97 @@ +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordGuildExtensions +{ + /// + /// Joins all active threads in the guild that this client has permission to view. + /// + /// The guild whose active threads to join. + /// is . + public static async Task JoinAllThreadsAsync(this DiscordGuild guild) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(guild); +#else + if (guild is null) + { + throw new ArgumentNullException(nameof(guild)); + } +#endif + + await Task.WhenAll(guild.Threads.Values.Select(t => t.JoinThreadAsync())).ConfigureAwait(false); + } + + /// + /// Gets a guild member by their ID. If the member is not found, is returned instead of + /// being thrown. + /// + /// The guild whose member list to search. + /// The ID of the member to retrieve. + /// is . + public static async Task GetMemberOrNullAsync(this DiscordGuild guild, ulong userId) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(guild); +#else + if (guild is null) + { + throw new ArgumentNullException(nameof(guild)); + } +#endif + + try + { + // we should never use exceptions for flow control but this is D#+ we're talking about. + // NotFoundException isn't even documented, and yet it gets thrown when a member doesn't exist. + // so this method should hopefully clearly express that - and at least using exceptions for flow control *here*, + // removes the need to do the same in consumer code. + // god I hate this. + return await guild.GetMemberAsync(userId).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + + /// + /// Normalizes a so that the internal client is assured to be a specified value. + /// + /// The to normalize. + /// The target client. + /// + /// A whose public values will match , but whose internal client is + /// . + /// + /// + /// is + /// -or- + /// is + /// + public static async Task NormalizeClientAsync(this DiscordGuild guild, DiscordClient client) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(guild); + ArgumentNullException.ThrowIfNull(client); +#else + if (guild is null) + { + throw new ArgumentNullException(nameof(guild)); + } + + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + return await client.GetGuildAsync(guild.Id).ConfigureAwait(false); + } +} diff --git a/X10D.DSharpPlus/src/DiscordMemberExtensions.cs b/X10D.DSharpPlus/src/DiscordMemberExtensions.cs new file mode 100644 index 0000000..3fd2020 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordMemberExtensions.cs @@ -0,0 +1,73 @@ +using DSharpPlus; +using DSharpPlus.Entities; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordMemberExtensions +{ + /// + /// Returns a value indicating whether this member has the specified role. + /// + /// The member whose roles to search. + /// The role for which to check. + /// + /// if has the role; otherwise, . + /// + public static bool HasRole(this DiscordMember member, DiscordRole role) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(member); + ArgumentNullException.ThrowIfNull(role); +#else + if (member is null) + { + throw new ArgumentNullException(nameof(member)); + } + + if (role is null) + { + throw new ArgumentNullException(nameof(role)); + } +#endif + + return member.Roles.Contains(role); + } + + /// + /// Normalizes a so that the internal client is assured to be a specified value. + /// + /// The to normalize. + /// The target client. + /// + /// A whose public values will match , but whose internal client + /// is . + /// + /// + /// is + /// -or- + /// is + /// + public static async Task NormalizeClientAsync(this DiscordMember member, DiscordClient client) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(member); + ArgumentNullException.ThrowIfNull(client); +#else + if (member is null) + { + throw new ArgumentNullException(nameof(member)); + } + + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + DiscordGuild guild = await member.Guild.NormalizeClientAsync(client).ConfigureAwait(false); + return await guild.GetMemberAsync(member.Id).ConfigureAwait(false); + } +} diff --git a/X10D.DSharpPlus/src/DiscordMessageExtensions.cs b/X10D.DSharpPlus/src/DiscordMessageExtensions.cs new file mode 100644 index 0000000..f8317c7 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordMessageExtensions.cs @@ -0,0 +1,89 @@ +using DSharpPlus; +using DSharpPlus.Entities; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordMessageExtensions +{ + /// + /// Deletes this message after a specified delay. + /// + /// The message to delete. + /// The delay before deletion. + /// The reason for the deletion. + /// is . + public static async Task DeleteAfterAsync(this DiscordMessage message, TimeSpan delay, string? reason = null) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(message); +#else + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } +#endif + + await Task.Delay(delay).ConfigureAwait(false); + await message.DeleteAsync(reason).ConfigureAwait(false); + } + + /// + /// Deletes the message as created by this task after a specified delay. + /// + /// The task whose result should be deleted. + /// The delay before deletion. + /// The reason for the deletion. + /// is . + public static async Task DeleteAfterAsync(this Task task, TimeSpan delay, string? reason = null) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(task); +#else + if (task is null) + { + throw new ArgumentNullException(nameof(task)); + } +#endif + + DiscordMessage message = await task.ConfigureAwait(false); + await message.DeleteAfterAsync(delay, reason).ConfigureAwait(false); + } + + /// + /// Normalizes a so that the internal client is assured to be a specified value. + /// + /// The to normalize. + /// The target client. + /// + /// A whose public values will match , but whose internal client + /// is . + /// + /// + /// is + /// -or- + /// is + /// + public static async Task NormalizeClientAsync(this DiscordMessage message, DiscordClient client) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(client); +#else + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + DiscordChannel channel = await message.Channel.NormalizeClientAsync(client).ConfigureAwait(false); + return await channel.GetMessageAsync(message.Id).ConfigureAwait(false); + } +} diff --git a/X10D.DSharpPlus/src/DiscordUserExtensions.cs b/X10D.DSharpPlus/src/DiscordUserExtensions.cs new file mode 100644 index 0000000..6ab8314 --- /dev/null +++ b/X10D.DSharpPlus/src/DiscordUserExtensions.cs @@ -0,0 +1,158 @@ +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace X10D.DSharpPlus; + +/// +/// Extension methods for . +/// +public static class DiscordUserExtensions +{ + /// + /// Returns the current as a member of the specified guild. + /// + /// The user to transform. + /// The guild whose member list to search. + /// + /// A whose is equal to , or + /// if this user is not in the specified . + /// + /// + /// is . + /// -or- + /// is . + /// + public static async Task GetAsMemberOfAsync(this DiscordUser user, DiscordGuild guild) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(guild); +#else + if (user is null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (guild is null) + { + throw new ArgumentNullException(nameof(guild)); + } +#endif + + if (user is DiscordMember member && member.Guild == guild) + { + return member; + } + + if (guild.Members.TryGetValue(user.Id, out member!)) + { + return member; + } + + try + { + return await guild.GetMemberAsync(user.Id).ConfigureAwait(false); + } + catch (NotFoundException) + { + return null; + } + } + + /// + /// Returns the user's username with the discriminator, in the format username#discriminator. + /// + /// The user whose username and discriminator to retrieve. + /// A string in the format username#discriminator + /// is . + public static string GetUsernameWithDiscriminator(this DiscordUser user) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(user); +#else + if (user is null) + { + throw new ArgumentNullException(nameof(user)); + } +#endif + + return $"{user.Username}#{user.Discriminator}"; + } + + /// + /// Returns a value indicating whether the current user is in the specified guild. + /// + /// The user to check. + /// The guild whose member list to search. + /// + /// if is a member of ; otherwise, + /// . + /// + public static async Task IsInGuildAsync(this DiscordUser user, DiscordGuild guild) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(guild); +#else + if (user is null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (guild is null) + { + throw new ArgumentNullException(nameof(guild)); + } +#endif + + if (guild.Members.TryGetValue(user.Id, out _)) + { + return true; + } + + try + { + DiscordMember? member = await guild.GetMemberAsync(user.Id).ConfigureAwait(false); + return member is not null; + } + catch (NotFoundException) + { + return false; + } + } + + /// + /// Normalizes a so that the internal client is assured to be a specified value. + /// + /// The to normalize. + /// The target client. + /// + /// A whose public values will match , but whose internal client is + /// . + /// + /// + /// is + /// -or- + /// is + /// + public static async Task NormalizeClientAsync(this DiscordUser user, DiscordClient client) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(client); +#else + if (user is null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } +#endif + + return await client.GetUserAsync(user.Id).ConfigureAwait(false); + } +} diff --git a/X10D.DSharpPlus/src/MentionUtility.cs b/X10D.DSharpPlus/src/MentionUtility.cs new file mode 100644 index 0000000..efda930 --- /dev/null +++ b/X10D.DSharpPlus/src/MentionUtility.cs @@ -0,0 +1,329 @@ +using System.Globalization; + +namespace X10D.DSharpPlus; + +/// +/// Provides methods for encoding and decoding Discord mention strings. +/// +/// +/// The implementations in this class are designed to resemble MentionUtils as provided by Discord.NET. The source is +/// available +/// +/// here +/// . +/// +public static class MentionUtility +{ + /// + /// Returns a channel mention string built from the specified channel ID. + /// + /// The ID of the channel to mention. + /// A channel mention string in the format <#123>. + public static string MentionChannel(decimal id) + { + return $"<#{id:N0}>"; + } + + /// + /// Returns a channel mention string built from the specified channel ID. + /// + /// The ID of the channel to mention. + /// A channel mention string in the format <#123>. + [CLSCompliant(false)] + public static string MentionChannel(ulong id) + { + return $"<#{id}>"; + } + + /// + /// Returns a role mention string built from the specified channel ID. + /// + /// The ID of the role to mention. + /// A role mention string in the format <@&123>. + public static string MentionRole(decimal id) + { + return $"<@&{id:N0}>"; + } + + /// + /// Returns a role mention string built from the specified role ID. + /// + /// The ID of the role to mention. + /// A role mention string in the format <@&123>. + [CLSCompliant(false)] + public static string MentionRole(ulong id) + { + return $"<@&{id}>"; + } + + /// + /// Returns a user mention string built from the specified user ID. + /// + /// The ID of the user to mention. + /// A user mention string in the format <@123>. + [CLSCompliant(false)] + public static string MentionUser(decimal id) + { + return MentionUser(id, false); + } + + /// + /// Returns a user mention string built from the specified user ID. + /// + /// The ID of the user to mention. + /// + /// if the mention string should account for nicknames; otherwise, . + /// + /// + /// A user mention string in the format <@!123> if is , + /// or in the format <@123> if is . + /// + [CLSCompliant(false)] + public static string MentionUser(decimal id, bool nickname) + { + return nickname ? $"<@!{id:N0}>" : $"<@{id:N0}>"; + } + + /// + /// Returns a user mention string built from the specified user ID. + /// + /// The ID of the user to mention. + /// A user mention string in the format <@123>. + [CLSCompliant(false)] + public static string MentionUser(ulong id) + { + return MentionUser(id, false); + } + + /// + /// Returns a user mention string built from the specified user ID. + /// + /// The ID of the user to mention. + /// + /// if the mention string should account for nicknames; otherwise, . + /// + /// + /// A user mention string in the format <@!123> if is , + /// or in the format <@123> if is . + /// + [CLSCompliant(false)] + public static string MentionUser(ulong id, bool nickname) + { + return nickname ? $"<@!{id}>" : $"<@{id}>"; + } + + /// + /// Parses a provided channel mention string to a decimal value representing the channel ID. A return value indicates + /// whether the parse succeeded. + /// + /// A string containing a mention string to parse, in the format <#123>. + /// + /// When this method returns, contains the decimal value representing the channel ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + public static bool TryParseChannel(string? value, out decimal result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 3 || value[0] != '<' || value[1] != '#' || value[^1] != '>') + { + return false; + } + + value = value.Substring(2, value.Length - 3); // <#123> + if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong actual)) + { + return false; + } + + result = actual; + return true; + } + + /// + /// Parses a provided channel mention string to a 64-bit unsigned integer representing the channel ID. A return value + /// indicates whether the parse succeeded. + /// + /// A string containing a mention string to parse, in the format <#123>. + /// + /// When this method returns, contains the 64-bit unsigned integer value representing the channel ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + [CLSCompliant(false)] + public static bool TryParseChannel(string? value, out ulong result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 3 || value[0] != '<' || value[1] != '#' || value[^1] != '>') + { + return false; + } + + value = value.Substring(2, value.Length - 3); // <#123> + return ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); + } + + /// + /// Parses a provided role mention string to a decimal value representing the role ID. A return value indicates whether + /// the parse succeeded. + /// + /// A string containing a mention string to parse, in the format <@&123>. + /// + /// When this method returns, contains the decimal value representing the role ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + public static bool TryParseRole(string? value, out decimal result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 4 || value[0] != '<' || value[1] != '@' || value[2] != '&' || value[^1] != '>') + { + return false; + } + + value = value.Substring(3, value.Length - 4); // <@&123> + if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong actual)) + { + return false; + } + + result = actual; + return true; + } + + /// + /// Parses a provided role mention string to a 64-bit unsigned integer representing the role ID. A return value indicates + /// whether the parse succeeded. + /// + /// A string containing a mention string to parse, in the format <@&123>. + /// + /// When this method returns, contains the 64-bit unsigned integer value representing the role ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + [CLSCompliant(false)] + public static bool TryParseRole(string? value, out ulong result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 4 || value[0] != '<' || value[1] != '@' || value[2] != '&' || value[^1] != '>') + { + return false; + } + + value = value.Substring(3, value.Length - 4); // <@&123> + return ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); + } + + /// + /// Parses a provided user mention string to a decimal value representing the user ID. A return value indicates whether + /// the parse succeeded. + /// + /// + /// A string containing a mention string to parse, in the format <@123> or <@!123>. + /// + /// + /// When this method returns, contains the decimal value representing the user ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + public static bool TryParseUser(string? value, out decimal result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 3 || value[0] != '<' || value[1] != '@' || value[^1] != '>') + { + return false; + } + + if (value.Length >= 4 && value[2] == '!') + { + value = value.Substring(3, value.Length - 4); // <@!123> + } + else + { + value = value.Substring(2, value.Length - 3); // <@123> + } + + if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong actual)) + { + return false; + } + + result = actual; + return true; + } + + /// + /// Parses a provided user mention string to a 64-bit unsigned integer representing the user ID. A return value indicates + /// whether the parse succeeded. + /// + /// + /// A string containing a mention string to parse, in the format <@123> or <@!123>. + /// + /// + /// When this method returns, contains the 64-bit unsigned integer value representing the user ID contained within + /// , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the + /// parameter is or , is not of the correct + /// format, or represents a number less than or greater than . + /// + /// if the parse was successful; otherwise, . + [CLSCompliant(false)] + public static bool TryParseUser(string? value, out ulong result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (value.Length < 3 || value[0] != '<' || value[1] != '@' || value[^1] != '>') + { + return false; + } + + if (value.Length >= 4 && value[2] == '!') + { + value = value.Substring(3, value.Length - 4); // <@!123> + } + else + { + value = value.Substring(2, value.Length - 3); // <@123> + } + + return ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); + } +} diff --git a/X10D.Hosting/X10D.Hosting.csproj b/X10D.Hosting/X10D.Hosting.csproj new file mode 100644 index 0000000..af34b97 --- /dev/null +++ b/X10D.Hosting/X10D.Hosting.csproj @@ -0,0 +1,69 @@ + + + + net7.0;net6.0;netstandard2.1 + 11.0 + true + true + Oliver Booth + en + https://github.com/oliverbooth/X10D + git + Extension methods on crack. + LICENSE.md + branding_Icon.png + + dotnet extension-methods + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) + true + 3.2.0 + enable + true + true + true + pdbonly + true + + + + true + + + + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix).0 + $(VersionPrefix).0 + + + + $(VersionPrefix)-$(VersionSuffix).$(BuildNumber) + $(VersionPrefix).$(BuildNumber) + $(VersionPrefix).$(BuildNumber) + + + + $(VersionPrefix) + $(VersionPrefix).0 + $(VersionPrefix).0 + + + + + + + + + True + + + + True + + + + True + + + + + diff --git a/X10D.Hosting/src/Assembly.cs b/X10D.Hosting/src/Assembly.cs new file mode 100644 index 0000000..f547610 --- /dev/null +++ b/X10D.Hosting/src/Assembly.cs @@ -0,0 +1 @@ +[assembly: CLSCompliant(true)] diff --git a/X10D.Hosting/src/DependencyInjection/ServiceCollectionExtensions.cs b/X10D.Hosting/src/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9a0bafe --- /dev/null +++ b/X10D.Hosting/src/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace X10D.Hosting.DependencyInjection; + +/// +/// Dependency injection extensions for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds an registration for the given type, while simultaneously adding it as a singleton. + /// + /// The to add the service to. + /// The type of the service to add. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddHostedSingleton(this IServiceCollection services) + where TService : class, IHostedService + { + services.AddSingleton(); + return services.AddSingleton(provider => provider.GetRequiredService()); + } + + /// + /// Adds an registration for the given type, while simultaneously adding it as a singleton. + /// + /// The to add the service to. + /// The type of the service to register and the implementation to use. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddHostedSingleton(this IServiceCollection services, Type type) + { + services.AddSingleton(type); + return services.AddSingleton(provider => (IHostedService)provider.GetRequiredService(type)); + } +} diff --git a/X10D.SourceGenerator/EmojiRegexGenerator.cs b/X10D.SourceGenerator/EmojiRegexGenerator.cs index 9b44b2f..2d67b1d 100644 --- a/X10D.SourceGenerator/EmojiRegexGenerator.cs +++ b/X10D.SourceGenerator/EmojiRegexGenerator.cs @@ -17,6 +17,7 @@ internal sealed class EmojiRegexGenerator : ISourceGenerator public void Initialize(GeneratorInitializationContext context) { string response = HttpClient.GetStringAsync(TwemojiRegexUrl).GetAwaiter().GetResult(); + var regex = new Regex(@"export default /(?.+)/g;", RegexOptions.Compiled, Regex.InfiniteMatchTimeout); using var reader = new StringReader(response); while (reader.ReadLine() is { } line) @@ -26,7 +27,7 @@ internal sealed class EmojiRegexGenerator : ISourceGenerator continue; } - Match match = Regex.Match(line, @"export default /(?.+)/g;"); + Match match = regex.Match(line); if (!match.Success) { continue; diff --git a/X10D.SourceGenerator/X10D.SourceGenerator.csproj b/X10D.SourceGenerator/X10D.SourceGenerator.csproj index 8220df9..e012f98 100644 --- a/X10D.SourceGenerator/X10D.SourceGenerator.csproj +++ b/X10D.SourceGenerator/X10D.SourceGenerator.csproj @@ -2,9 +2,11 @@ netstandard2.0 - 10.0 + 11.0 enable enable + true + true diff --git a/X10D.SourceValidator/X10D.SourceValidator.csproj b/X10D.SourceValidator/X10D.SourceValidator.csproj index 8957a5d..97f0be0 100644 --- a/X10D.SourceValidator/X10D.SourceValidator.csproj +++ b/X10D.SourceValidator/X10D.SourceValidator.csproj @@ -2,9 +2,12 @@ Exe - net6.0 + net7.0 + 11.0 enable enable + true + true diff --git a/X10D.Tests/X10D.Tests.csproj b/X10D.Tests/X10D.Tests.csproj index 6bba44b..41d2e68 100644 --- a/X10D.Tests/X10D.Tests.csproj +++ b/X10D.Tests/X10D.Tests.csproj @@ -1,24 +1,29 @@ - netstandard2.1;net6.0 - 10.0 + net7.0;net6.0;netcoreapp3.1 + 11.0 false enable true + json,cobertura + true - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/X10D.Tests/src/Collections/ArrayTests.AsReadOnly.cs b/X10D.Tests/src/Collections/ArrayTests.AsReadOnly.cs new file mode 100644 index 0000000..ed46847 --- /dev/null +++ b/X10D.Tests/src/Collections/ArrayTests.AsReadOnly.cs @@ -0,0 +1,42 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class ArrayTests +{ + [TestClass] + public class AsReadOnlyTests + { + [TestMethod] + public void AsReadOnly_ShouldReturnReadOnlyCollection_WhenArrayIsNotNull() + { + int[] array = {1, 2, 3}; + IReadOnlyCollection result = array.AsReadOnly(); + Assert.IsInstanceOfType(result, typeof(IReadOnlyCollection)); + } + + [TestMethod] + public void AsReadOnly_ShouldThrowArgumentNullException_WhenArrayIsNull() + { + int[]? array = null; + Assert.ThrowsException(() => array!.AsReadOnly()); + } + + [TestMethod] + public void AsReadOnly_ShouldReturnCorrectCount_WhenArrayIsNotEmpty() + { + int[] array = {1, 2, 3}; + IReadOnlyCollection result = array.AsReadOnly(); + Assert.AreEqual(array.Length, result.Count); + } + + [TestMethod] + public void AsReadOnly_ShouldReturnEmptyCollection_WhenArrayIsEmpty() + { + int[] array = Array.Empty(); + IReadOnlyCollection result = array.AsReadOnly(); + Assert.AreEqual(0, result.Count); + } + } +} diff --git a/X10D.Tests/src/Collections/ArrayTests.Clear.cs b/X10D.Tests/src/Collections/ArrayTests.Clear.cs new file mode 100644 index 0000000..34c36bb --- /dev/null +++ b/X10D.Tests/src/Collections/ArrayTests.Clear.cs @@ -0,0 +1,63 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class ArrayTests +{ + [TestClass] + public class ClearTests + { + [TestMethod] + public void Clear_ShouldClearTheArray() + { + var array = new int?[] {1, 2, 3, null, 4}; + + array.Clear(); + + Assert.IsTrue(array.All(x => x == null)); + } + + [TestMethod] + public void Clear_ShouldDoNothing_WhenArrayIsEmpty() + { + int[] array = Array.Empty(); + array.Clear(); + } + + [TestMethod] + public void Clear_WithRange_ShouldClearTheSpecifiedRangeOfTheArray() + { + var array = new int?[] {1, 2, 3, null, 4}; + + array.Clear(1..4); + + Assert.AreEqual(5, array.Length); + Assert.AreEqual(1, array[0]); + Assert.AreEqual(4, array[4]); + Assert.IsTrue(array[1..4].All(x => x == null)); + } + + [TestMethod] + public void Clear_WithIndexAndLength_ShouldClearTheSpecifiedRangeOfTheArray() + { + var array = new int?[] {1, 2, 3, null, 4}; + + array.Clear(1, 3); + + Assert.AreEqual(5, array.Length); + Assert.AreEqual(1, array[0]); + Assert.AreEqual(4, array[4]); + Assert.IsTrue(array[1..4].All(x => x == null)); + } + + [TestMethod] + public void Clear_ShouldThrowArgumentNullException_WhenArrayIsNull() + { + int[] array = null!; + Assert.ThrowsException(() => array.Clear()); + Assert.ThrowsException(() => array.Clear(0, 1)); + Assert.ThrowsException(() => array.Clear(..1)); + } + } +} diff --git a/X10D.Tests/src/Collections/ArrayTests.cs b/X10D.Tests/src/Collections/ArrayTests.cs index d5d745f..cdeb996 100644 --- a/X10D.Tests/src/Collections/ArrayTests.cs +++ b/X10D.Tests/src/Collections/ArrayTests.cs @@ -1,49 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using X10D.Collections; namespace X10D.Tests.Collections; [TestClass] -public class ArrayTests +public partial class ArrayTests { - [TestMethod] - public void AsReadOnlyShouldBeReadOnly() - { - var array = new object[] {1, "f", true}; - var readOnly = array.AsReadOnly(); - Assert.IsNotNull(readOnly); - Assert.IsTrue(readOnly.Count == 3); - - // ReSharper disable once ConvertTypeCheckToNullCheck - Assert.IsTrue(readOnly is IReadOnlyCollection); - } - - [TestMethod] - public void AsReadOnlyNullShouldThrow() - { - object[]? array = null; - Assert.ThrowsException(array!.AsReadOnly); - } - - [CLSCompliant(false)] - [TestMethod] - [DataRow] - [DataRow(1)] - [DataRow(1, 2, 3)] - [DataRow(1, 2, 3, 4, 5)] - public void ClearShouldFillDefault(params int[] args) - { - args.Clear(); - - int[] clearedArray = Enumerable.Repeat(0, args.Length).ToArray(); - CollectionAssert.AreEqual(clearedArray, args); - } - - [TestMethod] - public void ClearNullShouldThrow() - { - int[]? array = null; - Assert.ThrowsException(array!.Clear); - Assert.ThrowsException(() => array!.Clear(0, 0)); - } } diff --git a/X10D.Tests/src/Collections/BoolListTests.cs b/X10D.Tests/src/Collections/BoolListTests.cs index e67aafc..0cd5259 100644 --- a/X10D.Tests/src/Collections/BoolListTests.cs +++ b/X10D.Tests/src/Collections/BoolListTests.cs @@ -7,7 +7,7 @@ namespace X10D.Tests.Collections; public class BoolListTests { [TestMethod] - public void Pack8Bit_Should_Pack_Correctly() + public void PackByte_Should_Pack_Correctly() { var array = new[] {true, false, true, false, true, false, true, false}; Assert.AreEqual(85, array.PackByte()); // 01010101 diff --git a/X10D.Tests/src/Collections/ByteTests.cs b/X10D.Tests/src/Collections/ByteTests.cs index 15c28a8..6030065 100644 --- a/X10D.Tests/src/Collections/ByteTests.cs +++ b/X10D.Tests/src/Collections/ByteTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Runtime.Intrinsics.X86; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Collections; namespace X10D.Tests.Collections; @@ -7,9 +8,10 @@ namespace X10D.Tests.Collections; public class ByteTests { [TestMethod] - public void UnpackBits_ShouldUnpackToArrayCorrectly() + public void Unpack_ShouldUnpackToArrayCorrectly() { - bool[] bits = ((byte)0b11010100).Unpack(); + const byte value = 0b11010100; + bool[] bits = value.Unpack(); Assert.AreEqual(8, bits.Length); @@ -24,10 +26,30 @@ public class ByteTests } [TestMethod] - public void UnpackBits_ShouldUnpackToSpanCorrectly() + public void Unpack_ShouldUnpackToSpanCorrectly() { + const byte value = 0b11010100; Span bits = stackalloc bool[8]; - ((byte)0b11010100).Unpack(bits); + value.Unpack(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + } + +#if NET5_0_OR_GREATER + + [TestMethod] + public void UnpackInternal_Fallback_ShouldUnpackToSpanCorrectly() + { + const byte value = 0b11010100; + Span bits = stackalloc bool[8]; + value.UnpackInternal_Fallback(bits); Assert.IsFalse(bits[0]); Assert.IsFalse(bits[1]); @@ -40,18 +62,43 @@ public class ByteTests } [TestMethod] - public void UnpackBits_ShouldRepackEqually() + public void UnpackInternal_Ssse3_ShouldUnpackToSpanCorrectly() { - Assert.AreEqual(0b11010100, ((byte)0b11010100).Unpack().PackByte()); + if (!Sse3.IsSupported) + { + return; + } + + const byte value = 0b11010100; + Span bits = stackalloc bool[8]; + value.UnpackInternal_Ssse3(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + } +#endif + + [TestMethod] + public void Unpack_ShouldRepackEqually() + { + const byte value = 0b11010100; + Assert.AreEqual(value, value.Unpack().PackByte()); } [TestMethod] - public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + public void Unpack_ShouldThrow_GivenTooSmallSpan() { Assert.ThrowsException(() => { + const byte value = 0b11010100; Span bits = stackalloc bool[0]; - ((byte)0b11010100).Unpack(bits); + value.Unpack(bits); }); } } diff --git a/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAll.cs b/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAll.cs new file mode 100644 index 0000000..a0bed51 --- /dev/null +++ b/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAll.cs @@ -0,0 +1,45 @@ +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class CollectionTests +{ + [TestClass] + public class ClearAndDisposeAllTests + { + [TestMethod] + public void ClearAndDisposeAll_ShouldClearAndDisposeAllItems_WhenCalledWithValidList() + { + var mock1 = new Mock(); + var mock2 = new Mock(); + var mock3 = new Mock(); + var list = new List {mock1.Object, mock2.Object, mock3.Object}; + + list.ClearAndDisposeAll(); + + mock1.Verify(i => i.Dispose(), Times.Once); + mock2.Verify(i => i.Dispose(), Times.Once); + mock3.Verify(i => i.Dispose(), Times.Once); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void ClearAndDisposeAll_ShouldThrowArgumentNullException_WhenCalledWithNullList() + { + List? list = null; + Assert.ThrowsException(() => list!.ClearAndDisposeAll()); + } + + [TestMethod] + public void ClearAndDisposeAll_ShouldThrowInvalidOperationException_WhenCalledWithReadOnlyList() + { + var mock = new Mock(); + var list = new ReadOnlyCollection(new List {mock.Object}); + + Assert.ThrowsException(() => list.ClearAndDisposeAll()); + } + } +} diff --git a/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAllAsync.cs b/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAllAsync.cs new file mode 100644 index 0000000..1301763 --- /dev/null +++ b/X10D.Tests/src/Collections/CollectionTests.ClearAndDisposeAllAsync.cs @@ -0,0 +1,45 @@ +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class CollectionTests +{ + [TestClass] + public class ClearAndDisposeAllAsyncTests + { + [TestMethod] + public async Task ClearAndDisposeAllAsync_ShouldClearAndDisposeAllItems_WhenCalledWithValidList() + { + var mock1 = new Mock(); + var mock2 = new Mock(); + var mock3 = new Mock(); + var list = new List {mock1.Object, mock2.Object, mock3.Object}; + + await list.ClearAndDisposeAllAsync().ConfigureAwait(false); + + mock1.Verify(i => i.DisposeAsync(), Times.Once); + mock2.Verify(i => i.DisposeAsync(), Times.Once); + mock3.Verify(i => i.DisposeAsync(), Times.Once); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public async Task ClearAndDisposeAllAsync_ShouldThrowArgumentNullException_WhenCalledWithNullList() + { + List? list = null; + await Assert.ThrowsExceptionAsync(list!.ClearAndDisposeAllAsync).ConfigureAwait(false); + } + + [TestMethod] + public async Task ClearAndDisposeAllAsync_ShouldThrowInvalidOperationException_WhenCalledWithReadOnlyList() + { + var mock = new Mock(); + var list = new ReadOnlyCollection(new List {mock.Object}); + + await Assert.ThrowsExceptionAsync(list.ClearAndDisposeAllAsync).ConfigureAwait(false); + } + } +} diff --git a/X10D.Tests/src/Collections/CollectionTests.cs b/X10D.Tests/src/Collections/CollectionTests.cs index 835a6d6..9642b16 100644 --- a/X10D.Tests/src/Collections/CollectionTests.cs +++ b/X10D.Tests/src/Collections/CollectionTests.cs @@ -1,77 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using X10D.Collections; namespace X10D.Tests.Collections; [TestClass] -public class CollectionTests +public partial class CollectionTests { - [TestMethod] - public void ClearAndDisposeAll_ShouldDispose_GivenCollection() - { - var collection = new List {new(), new(), new()}; - var copy = new List(collection); - - collection.ClearAndDisposeAll(); - - Assert.IsTrue(copy.All(x => x.IsDisposed)); - Assert.AreEqual(0, collection.Count); - } - - [TestMethod] - public async Task ClearAndDisposeAllAsync_ShouldDispose_GivenCollection() - { - var collection = new List {new(), new(), new()}; - var copy = new List(collection); - - await collection.ClearAndDisposeAllAsync(); - - Assert.IsTrue(copy.All(x => x.IsDisposed)); - Assert.AreEqual(0, collection.Count); - } - - [TestMethod] - public void ClearAndDisposeAll_ShouldThrow_GivenNull() - { - List? collection = null; - Assert.ThrowsException(() => collection!.ClearAndDisposeAll()); - } - - [TestMethod] - public void ClearAndDisposeAllAsync_ShouldThrow_GivenNull() - { - List? collection = null; - Assert.ThrowsExceptionAsync(async () => await collection!.ClearAndDisposeAllAsync()); - } - - [TestMethod] - public void ClearAndDisposeAll_ShouldThrow_GivenReadOnlyCollection() - { - var collection = new List().AsReadOnly(); - Assert.ThrowsException(() => collection.ClearAndDisposeAll()); - } - - [TestMethod] - public void ClearAndDisposeAllAsync_ShouldThrow_GivenReadOnlyCollection() - { - var collection = new List().AsReadOnly(); - Assert.ThrowsExceptionAsync(async () => await collection.ClearAndDisposeAllAsync()); - } - - private class Disposable : IDisposable, IAsyncDisposable - { - public bool IsDisposed { get; private set; } - - public void Dispose() - { - Assert.IsTrue(IsDisposed = true); - } - -#pragma warning disable CS1998 - public async ValueTask DisposeAsync() -#pragma warning restore CS1998 - { - Assert.IsTrue(IsDisposed = true); - } - } } diff --git a/X10D.Tests/src/Collections/DictionaryTests.cs b/X10D.Tests/src/Collections/DictionaryTests.cs index 9fb8be2..907b1f9 100644 --- a/X10D.Tests/src/Collections/DictionaryTests.cs +++ b/X10D.Tests/src/Collections/DictionaryTests.cs @@ -7,7 +7,7 @@ namespace X10D.Tests.Collections; public class DictionaryTests { [TestMethod] - public void AddOrUpdate_ShouldAddNewKey_IfNotExists() + public void AddOrUpdate_ShouldAddNewKey_IfNotExists_GivenConcreteDictionary() { var dictionary = new Dictionary(); Assert.IsFalse(dictionary.ContainsKey(1)); @@ -32,7 +32,32 @@ public class DictionaryTests } [TestMethod] - public void AddOrUpdate_ShouldUpdateKey_IfExists() + public void AddOrUpdate_ShouldAddNewKey_IfNotExists_GivenIDictionary() + { + IDictionary dictionary = new Dictionary(); + Assert.IsFalse(dictionary.ContainsKey(1)); + + dictionary.AddOrUpdate(1, "one", (_, _) => string.Empty); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("one", dictionary[1]); + + dictionary.Clear(); + Assert.IsFalse(dictionary.ContainsKey(1)); + + dictionary.AddOrUpdate(1, _ => "one", (_, _) => string.Empty); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("one", dictionary[1]); + + dictionary.Clear(); + Assert.IsFalse(dictionary.ContainsKey(1)); + + dictionary.AddOrUpdate(1, (_, _) => "one", (_, _, _) => string.Empty, 0); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("one", dictionary[1]); + } + + [TestMethod] + public void AddOrUpdate_ShouldUpdateKey_IfExists_GivenConcreteDirection() { var dictionary = new Dictionary {[1] = "one"}; Assert.IsTrue(dictionary.ContainsKey(1)); @@ -60,7 +85,35 @@ public class DictionaryTests } [TestMethod] - public void AddOrUpdate_ShouldThrow_GivenNullDictionary() + public void AddOrUpdate_ShouldUpdateKey_IfExists_GivenIDictionary() + { + IDictionary dictionary = new Dictionary {[1] = "one"}; + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("one", dictionary[1]); + + dictionary.AddOrUpdate(1, string.Empty, (_, _) => "two"); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("two", dictionary[1]); + + dictionary.Clear(); + Assert.IsFalse(dictionary.ContainsKey(1)); + dictionary[1] = "one"; + + dictionary.AddOrUpdate(1, _ => string.Empty, (_, _) => "two"); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("two", dictionary[1]); + + dictionary.Clear(); + Assert.IsFalse(dictionary.ContainsKey(1)); + dictionary[1] = "one"; + + dictionary.AddOrUpdate(1, (_, _) => string.Empty, (_, _, _) => "two", 0); + Assert.IsTrue(dictionary.ContainsKey(1)); + Assert.AreEqual("two", dictionary[1]); + } + + [TestMethod] + public void AddOrUpdate_ShouldThrow_GivenNullDictionary_GivenConcreteDictionary() { Dictionary? dictionary = null; Assert.ThrowsException(() => dictionary!.AddOrUpdate(1, string.Empty, (_, _) => string.Empty)); @@ -71,7 +124,18 @@ public class DictionaryTests } [TestMethod] - public void AddOrUpdate_ShouldThrow_GivenNullUpdateValueFactory() + public void AddOrUpdate_ShouldThrow_GivenNullDictionary_GivenIDictionary() + { + IDictionary? dictionary = null; + Assert.ThrowsException(() => dictionary!.AddOrUpdate(1, string.Empty, (_, _) => string.Empty)); + Assert.ThrowsException(() => + dictionary!.AddOrUpdate(1, _ => string.Empty, (_, _) => string.Empty)); + Assert.ThrowsException(() => + dictionary!.AddOrUpdate(1, (_, _) => string.Empty, (_, _, _) => string.Empty, 0)); + } + + [TestMethod] + public void AddOrUpdate_ShouldThrow_GivenNullUpdateValueFactory_GivenConcreteDictionary() { var dictionary = new Dictionary(); Assert.ThrowsException(() => dictionary.AddOrUpdate(1, string.Empty, null!)); @@ -80,7 +144,16 @@ public class DictionaryTests } [TestMethod] - public void AddOrUpdate_ShouldThrow_GivenNullAddValueFactory() + public void AddOrUpdate_ShouldThrow_GivenNullUpdateValueFactory_GivenIDictionary() + { + IDictionary dictionary = new Dictionary(); + Assert.ThrowsException(() => dictionary.AddOrUpdate(1, string.Empty, null!)); + Assert.ThrowsException(() => dictionary.AddOrUpdate(1, _ => string.Empty, null!)); + Assert.ThrowsException(() => dictionary.AddOrUpdate(1, (_, _) => string.Empty, null!, 0)); + } + + [TestMethod] + public void AddOrUpdate_ShouldThrow_GivenNullAddValueFactory_GivenConcreteDictionary() { var dictionary = new Dictionary(); Func? addValueFactory = null; @@ -88,6 +161,15 @@ public class DictionaryTests Assert.ThrowsException(() => dictionary.AddOrUpdate(1, null!, (_, _, _) => "one", 0)); } + [TestMethod] + public void AddOrUpdate_ShouldThrow_GivenNullAddValueFactory_GivenIDictionary() + { + IDictionary dictionary = new Dictionary(); + Func? addValueFactory = null; + Assert.ThrowsException(() => dictionary.AddOrUpdate(1, addValueFactory!, (_, _) => "one")); + Assert.ThrowsException(() => dictionary.AddOrUpdate(1, null!, (_, _, _) => "one", 0)); + } + [TestMethod] public void ToConnectionString_ShouldReturnConnectionString() { diff --git a/X10D.Tests/src/Collections/EnumerableTests.DisposeAll.cs b/X10D.Tests/src/Collections/EnumerableTests.DisposeAll.cs new file mode 100644 index 0000000..f616b62 --- /dev/null +++ b/X10D.Tests/src/Collections/EnumerableTests.DisposeAll.cs @@ -0,0 +1,34 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class EnumerableTests +{ + [TestClass] + public class DisposeAllTests + { + [TestMethod] + public void DisposeAll_ShouldDisposeAllItems_WhenCalledWithValidList() + { + var mock1 = new Mock(); + var mock2 = new Mock(); + var mock3 = new Mock(); + var list = new List {mock1.Object, mock2.Object, null!, mock3.Object}; + + list.DisposeAll(); + + mock1.Verify(i => i.Dispose(), Times.Once); + mock2.Verify(i => i.Dispose(), Times.Once); + mock3.Verify(i => i.Dispose(), Times.Once); + } + + [TestMethod] + public void DisposeAll_ShouldThrowArgumentNullException_WhenCalledWithNullList() + { + List? list = null; + Assert.ThrowsException(() => list!.DisposeAll()); + } + } +} diff --git a/X10D.Tests/src/Collections/EnumerableTests.DisposeAllAsync.cs b/X10D.Tests/src/Collections/EnumerableTests.DisposeAllAsync.cs new file mode 100644 index 0000000..d683940 --- /dev/null +++ b/X10D.Tests/src/Collections/EnumerableTests.DisposeAllAsync.cs @@ -0,0 +1,34 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +public partial class EnumerableTests +{ + [TestClass] + public class DisposeAllAsyncTests + { + [TestMethod] + public async Task DisposeAllAsync_ShouldDisposeAllItems_WhenCalledWithValidList() + { + var mock1 = new Mock(); + var mock2 = new Mock(); + var mock3 = new Mock(); + var list = new List {mock1.Object, mock2.Object, null!, mock3.Object}; + + await list.DisposeAllAsync().ConfigureAwait(false); + + mock1.Verify(i => i.DisposeAsync(), Times.Once); + mock2.Verify(i => i.DisposeAsync(), Times.Once); + mock3.Verify(i => i.DisposeAsync(), Times.Once); + } + + [TestMethod] + public async Task DisposeAllAsync_ShouldThrowArgumentNullException_WhenCalledWithNullList() + { + List? list = null; + await Assert.ThrowsExceptionAsync(() => list!.DisposeAllAsync()).ConfigureAwait(false); + } + } +} diff --git a/X10D.Tests/src/Collections/EnumerableTests.cs b/X10D.Tests/src/Collections/EnumerableTests.cs index 533a00e..e008e87 100644 --- a/X10D.Tests/src/Collections/EnumerableTests.cs +++ b/X10D.Tests/src/Collections/EnumerableTests.cs @@ -1,39 +1,112 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Collections; +using X10D.Core; namespace X10D.Tests.Collections; [TestClass] -public class EnumerableTests +public partial class EnumerableTests { [TestMethod] - public void DisposeAll_ShouldDispose_GivenCollection() + public void CountWhereNot_ShouldReturnCorrectCount_GivenSequence() { - var collection = new List {new(), new(), new()}; - collection.DisposeAll(); - Assert.IsTrue(collection.All(x => x.IsDisposed)); + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + int count = enumerable.CountWhereNot(x => x % 2 == 0); + Assert.AreEqual(2, count); } [TestMethod] - public async Task DisposeAllAsync_ShouldDispose_GivenCollection() + public void CountWhereNot_ShouldThrowArgumentNullException_GivenNullSource() { - var collection = new List {new(), new(), new()}; - await collection.DisposeAllAsync(); - Assert.IsTrue(collection.All(x => x.IsDisposed)); + Assert.ThrowsException(() => ((IEnumerable?)null)!.CountWhereNot(x => x % 2 == 0)); } [TestMethod] - public void DisposeAll_ShouldThrow_GivenNull() + public void CountWhereNot_ShouldThrowArgumentNullException_GivenNullPredicate() { - List? collection = null; - Assert.ThrowsException(() => collection!.DisposeAll()); + Assert.ThrowsException(() => Enumerable.Empty().CountWhereNot(null!)); } [TestMethod] - public async Task DisposeAllAsync_ShouldThrow_GivenNull() + public void CountWhereNot_ShouldThrowOverflowException_GivenLargeSource() { - List? collection = null; - await Assert.ThrowsExceptionAsync(async () => await collection!.DisposeAllAsync()); + IEnumerable GetValues() + { + while (true) + { + yield return 1; + } + + // ReSharper disable once IteratorNeverReturns + } + + Assert.ThrowsException(() => GetValues().CountWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void FirstWhereNot_ShouldReturnCorrectElements_GivenSequence() + { + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + int result = enumerable.FirstWhereNot(x => x % 2 == 0); + Assert.AreEqual(7, result); + } + + [TestMethod] + public void FirstWhereNot_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IEnumerable?)null)!.FirstWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void FirstWhereNot_ShouldThrowArgumentNullException_GivenNullPredicate() + { + Assert.ThrowsException(() => Enumerable.Range(0, 1).FirstWhereNot(null!)); + } + + [TestMethod] + public void FirstWhereNot_ShouldThrowInvalidOperationException_GivenEmptySource() + { + Assert.ThrowsException(() => Enumerable.Empty().FirstWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void FirstWhereNot_ShouldThrowInvalidOperationException_GivenSourceWithNoMatchingElements() + { + Assert.ThrowsException(() => 2.AsArrayValue().FirstWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void FirstWhereNotOrDefault_ShouldReturnCorrectElements_GivenSequence() + { + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + int result = enumerable.FirstWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(7, result); + } + + [TestMethod] + public void FirstWhereNotOrDefault_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IEnumerable?)null)!.FirstWhereNotOrDefault(x => x % 2 == 0)); + } + + [TestMethod] + public void FirstWhereNotOrDefault_ShouldThrowArgumentNullException_GivenNullPredicate() + { + Assert.ThrowsException(() => Enumerable.Empty().FirstWhereNotOrDefault(null!)); + } + + [TestMethod] + public void FirstWhereNotOrDefault_ShouldReturnDefault_GivenEmptySource() + { + int result = Enumerable.Empty().FirstWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(default, result); + } + + [TestMethod] + public void FirstWhereNotOrDefault_ShouldReturnDefault_GivenSourceWithNoMatchingElements() + { + int result = 2.AsArrayValue().FirstWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(default, result); } [TestMethod] @@ -94,6 +167,72 @@ public class EnumerableTests Assert.ThrowsException(() => source.ForEach(null!)); } + [TestMethod] + public void LastWhereNot_ShouldReturnCorrectElements_GivenSequence() + { + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + int result = enumerable.LastWhereNot(x => x % 2 == 0); + Assert.AreEqual(9, result); + } + + [TestMethod] + public void LastWhereNot_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IEnumerable?)null)!.LastWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void LastWhereNot_ShouldThrowArgumentNullException_GivenNullPredicate() + { + Assert.ThrowsException(() => Array.Empty().LastWhereNot(null!)); + } + + [TestMethod] + public void LastWhereNot_ShouldThrowInvalidOperationException_GivenEmptySource() + { + Assert.ThrowsException(() => Array.Empty().LastWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void LastWhereNot_ShouldThrowInvalidOperationException_GivenSourceWithNoMatchingElements() + { + Assert.ThrowsException(() => 2.AsArrayValue().LastWhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void LastWhereNotOrDefault_ShouldReturnCorrectElements_GivenSequence() + { + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + int result = enumerable.LastWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(9, result); + } + + [TestMethod] + public void LastWhereNotOrDefault_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IEnumerable?)null)!.LastWhereNotOrDefault(x => x % 2 == 0)); + } + + [TestMethod] + public void LastWhereNotOrDefault_ShouldThrowArgumentNullException_GivenNullPredicate() + { + Assert.ThrowsException(() => Array.Empty().LastWhereNotOrDefault(null!)); + } + + [TestMethod] + public void LastWhereNotOrDefault_ShouldReturnDefault_GivenEmptySource() + { + int result = Array.Empty().LastWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(default, result); + } + + [TestMethod] + public void LastWhereNotOrDefault_ShouldReturnDefault_GivenSourceWithNoMatchingElements() + { + int result = 2.AsArrayValue().LastWhereNotOrDefault(x => x % 2 == 0); + Assert.AreEqual(default, result); + } + [TestMethod] public void Shuffled_ShouldThrow_GivenNull() { @@ -112,25 +251,56 @@ public class EnumerableTests CollectionAssert.AreNotEqual(array, shuffled); } + [TestMethod] + public void WhereNot_ShouldReturnCorrectElements_GivenSequence() + { + var enumerable = new[] {2, 4, 6, 7, 8, 9, 10}; + IEnumerable result = enumerable.WhereNot(x => x % 2 == 0); + CollectionAssert.AreEqual(new[] {7, 9}, result.ToArray()); + } + + [TestMethod] + public void WhereNot_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IEnumerable?)null)!.WhereNot(x => x % 2 == 0)); + } + + [TestMethod] + public void WhereNot_ShouldThrowArgumentNullException_GivenNullPredicate() + { + Assert.ThrowsException(() => Enumerable.Empty().WhereNot(null!)); + } + + [TestMethod] + public void WhereNotNull_ShouldContainNoNullElements() + { + object?[] array = Enumerable.Repeat(new object(), 10).ToArray(); + array[1] = null; + array[2] = null; + array[8] = null; + array[9] = null; + + const int expectedCount = 6; + var actualCount = 0; + + foreach (object o in array.WhereNotNull()) + { + Assert.IsNotNull(o); + actualCount++; + } + + Assert.AreEqual(expectedCount, actualCount); + } + + [TestMethod] + public void WhereNotNull_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.WhereNotNull()); + } + private class DummyClass { public int Value { get; set; } } - - private class Disposable : IDisposable, IAsyncDisposable - { - public bool IsDisposed { get; private set; } - - public void Dispose() - { - Assert.IsTrue(IsDisposed = true); - } - -#pragma warning disable CS1998 - public async ValueTask DisposeAsync() -#pragma warning restore CS1998 - { - Assert.IsTrue(IsDisposed = true); - } - } } diff --git a/X10D.Tests/src/Collections/Int16Tests.cs b/X10D.Tests/src/Collections/Int16Tests.cs index d077744..e5df97b 100644 --- a/X10D.Tests/src/Collections/Int16Tests.cs +++ b/X10D.Tests/src/Collections/Int16Tests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Runtime.Intrinsics.X86; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Collections; namespace X10D.Tests.Collections; @@ -7,10 +8,11 @@ namespace X10D.Tests.Collections; public class Int16Tests { [TestMethod] - public void UnpackBits_ShouldUnpackToArrayCorrectly() + public void Unpack_ShouldUnpackToArrayCorrectly() { - bool[] bits = ((short)0b11010100).Unpack(); - + const short value = 0b11010100; + bool[] bits = value.Unpack(); + Assert.AreEqual(16, bits.Length); Assert.IsFalse(bits[0]); @@ -29,10 +31,11 @@ public class Int16Tests } [TestMethod] - public void UnpackBits_ShouldUnpackToSpanCorrectly() + public void Unpack_ShouldUnpackToSpanCorrectly() { + const short value = 0b11010100; Span bits = stackalloc bool[16]; - ((short)0b11010100).Unpack(bits); + value.Unpack(bits); Assert.IsFalse(bits[0]); Assert.IsFalse(bits[1]); @@ -50,18 +53,71 @@ public class Int16Tests } [TestMethod] - public void UnpackBits_ShouldRepackEqually() + public void Unpack_ShouldUnpackToSpanCorrectly_GivenFallbackImplementation() { - Assert.AreEqual(0b11010100, ((short)0b11010100).Unpack().PackInt16()); + const short value = 0b11010100; + Span bits = stackalloc bool[16]; + value.UnpackInternal_Fallback(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 16; index++) + { + Assert.IsFalse(bits[index]); + } + } + +#if NET5_0_OR_GREATER + [TestMethod] + public void UnpackInternal_Ssse3_ShouldUnpackToSpanCorrectly() + { + if (!Sse3.IsSupported) + { + return; + } + + const short value = 0b11010100; + Span bits = stackalloc bool[16]; + value.UnpackInternal_Ssse3(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 16; index++) + { + Assert.IsFalse(bits[index]); + } + } +#endif + + [TestMethod] + public void Unpack_ShouldRepackEqually() + { + const short value = 0b11010100; + Assert.AreEqual(value, value.Unpack().PackInt16()); } [TestMethod] - public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + public void Unpack_ShouldThrow_GivenTooSmallSpan() { Assert.ThrowsException(() => { + const short value = 0b11010100; Span bits = stackalloc bool[0]; - ((short)0b11010100).Unpack(bits); + value.Unpack(bits); }); } } diff --git a/X10D.Tests/src/Collections/Int32Tests.cs b/X10D.Tests/src/Collections/Int32Tests.cs index cd86c75..c48e0a0 100644 --- a/X10D.Tests/src/Collections/Int32Tests.cs +++ b/X10D.Tests/src/Collections/Int32Tests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Runtime.Intrinsics.X86; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Collections; namespace X10D.Tests.Collections; @@ -7,10 +8,11 @@ namespace X10D.Tests.Collections; public class Int32Tests { [TestMethod] - public void UnpackBits_ShouldUnpackToArrayCorrectly() + public void Unpack_ShouldUnpackToArrayCorrectly() { - bool[] bits = 0b11010100.Unpack(); - + const int value = 0b11010100; + bool[] bits = value.Unpack(); + Assert.AreEqual(32, bits.Length); Assert.IsFalse(bits[0]); @@ -29,10 +31,11 @@ public class Int32Tests } [TestMethod] - public void UnpackBits_ShouldUnpackToSpanCorrectly() + public void Unpack_ShouldUnpackToSpanCorrectly() { + const int value = 0b11010100; Span bits = stackalloc bool[32]; - 0b11010100.Unpack(bits); + value.Unpack(bits); Assert.IsFalse(bits[0]); Assert.IsFalse(bits[1]); @@ -50,18 +53,98 @@ public class Int32Tests } [TestMethod] - public void UnpackBits_ShouldRepackEqually() + public void UnpackInternal_Fallback_ShouldUnpackToSpanCorrectly() { - Assert.AreEqual(0b11010100, 0b11010100.Unpack().PackInt32()); + const int value = 0b11010100; + Span bits = stackalloc bool[32]; + value.UnpackInternal_Fallback(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 32; index++) + { + Assert.IsFalse(bits[index]); + } + } + +#if NET5_0_OR_GREATER + [TestMethod] + public void UnpackInternal_Ssse3_ShouldUnpackToSpanCorrectly() + { + if (!Ssse3.IsSupported) + { + return; + } + + const int value = 0b11010100; + Span bits = stackalloc bool[32]; + value.UnpackInternal_Ssse3(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 32; index++) + { + Assert.IsFalse(bits[index]); + } } [TestMethod] - public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + public void UnpackInternal_Avx2_ShouldUnpackToSpanCorrectly() + { + if (!Avx2.IsSupported) + { + return; + } + + const int value = 0b11010100; + Span bits = stackalloc bool[32]; + value.UnpackInternal_Avx2(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 32; index++) + { + Assert.IsFalse(bits[index]); + } + } +#endif + + [TestMethod] + public void Unpack_ShouldRepackEqually() + { + const int value = 0b11010100; + Assert.AreEqual(value, value.Unpack().PackInt32()); + } + + [TestMethod] + public void Unpack_ShouldThrow_GivenTooSmallSpan() { Assert.ThrowsException(() => { + const int value = 0b11010100; Span bits = stackalloc bool[0]; - 0b11010100.Unpack(bits); + value.Unpack(bits); }); } } diff --git a/X10D.Tests/src/Collections/ListTests.cs b/X10D.Tests/src/Collections/ListTests.cs index f62adc4..06c079a 100644 --- a/X10D.Tests/src/Collections/ListTests.cs +++ b/X10D.Tests/src/Collections/ListTests.cs @@ -79,6 +79,124 @@ public class ListTests Assert.ThrowsException(() => list!.Fill(0, 0, 0)); } + [TestMethod] + public void IndexOf_ShouldReturnCorrectValue_FromStartOfList() + { + int[] array = {0, 1, 2, 3, 4}; + Assert.AreEqual(2, array.IndexOf(2)); + Assert.AreEqual(2, array.IndexOf(2, 0)); + Assert.AreEqual(2, array.IndexOf(2, 0, 5)); + } + + [TestMethod] + public void IndexOf_ShouldReturnCorrectValue_GivenSubRange() + { + int[] array = {0, 1, 2, 3, 4, 0}; + Assert.AreEqual(0, array.IndexOf(0)); + Assert.AreEqual(0, array.IndexOf(0, 0)); + Assert.AreEqual(0, array.IndexOf(0, 0, 5)); + + Assert.AreEqual(5, array.IndexOf(0, 1)); + Assert.AreEqual(5, array.IndexOf(0, 1, 5)); + } + + [TestMethod] + public void IndexOf_ShouldReturnNegative1_ForEmptyList() + { + int[] array = Array.Empty(); + Assert.AreEqual(-1, array.IndexOf(0)); + Assert.AreEqual(-1, array.IndexOf(0, 0)); + Assert.AreEqual(-1, array.IndexOf(0, 0, 0)); + } + + [TestMethod] + public void IndexOf_ShouldThrowArgumentNullException_GivenNullList() + { + int[]? array = null; + Assert.ThrowsException(() => array!.IndexOf(0)); + Assert.ThrowsException(() => array!.IndexOf(0, 0)); + Assert.ThrowsException(() => array!.IndexOf(0, 0, 0)); + } + + [TestMethod] + public void IndexOf_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount() + { + int[] array = Array.Empty(); + Assert.ThrowsException(() => array.IndexOf(0, 0, -1)); + } + + [TestMethod] + public void IndexOf_ShouldThrowArgumentOutOfRangeException_GivenNegativeStartIndex() + { + int[] array = Array.Empty(); + Assert.ThrowsException(() => array.IndexOf(0, -1)); + Assert.ThrowsException(() => array.IndexOf(0, -1, 0)); + } + + [TestMethod] + public void IndexOf_ShouldThrowArgumentOutOfRangeException_GivenInvalidStartIndexCountPair() + { + int[] array = {0, 1, 2}; + Assert.ThrowsException(() => array.IndexOf(0, 2, 4)); + } + + [TestMethod] + public void Random_ShouldReturnContainedObject_GivenNotNull() + { + var list = new List(Enumerable.Range(1, 52)); // 52! chance of being shuffled to the same order + int random = list.Random(); + + Assert.IsTrue(list.Contains(random)); + } + + [TestMethod] + public void Random_ShouldThrow_GivenNull() + { + Assert.ThrowsException(() => ((List?)null)!.Random()); + } + + [TestMethod] + public void RemoveRange_ShouldThrowArgumentNullException_GivenNull() + { + Assert.ThrowsException(() => ((List?)null)!.RemoveRange(new Range())); + } + + [TestMethod] + public void RemoveRange_ShouldThrowArgumentException_GivenEndIndexLessThanStart() + { + Assert.ThrowsException(() => new List().RemoveRange(2..0)); + } + + [TestMethod] + public void RemoveRange_ShouldThrowArgumentOutOfRangeException_GivenEndIndexGreaterThanOrEqualToCount() + { + Assert.ThrowsException(() => new List().RemoveRange(..0)); + Assert.ThrowsException(() => new List {1}.RemoveRange(..2)); + } + + [TestMethod] + public void RemoveRange_ShouldRemoveElements_GivenList() + { + var list = new List + { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + }; + + Assert.AreEqual(10, list.Count); + list.RemoveRange(2..5); + Assert.AreEqual(6, list.Count); + CollectionAssert.AreEqual(new[] {1, 2, 7, 8, 9, 10}, list); + } + [TestMethod] public void Shuffle_ShouldReorder_GivenNotNull() { @@ -99,17 +217,107 @@ public class ListTests } [TestMethod] - public void Random_ShouldReturnContainedObject_GivenNotNull() + public void Slice_ShouldReturnCorrectValue_GivenStartIndex() { - var list = new List(Enumerable.Range(1, 52)); // 52! chance of being shuffled to the same order - int random = list.Random(); - - Assert.IsTrue(list.Contains(random)); + int[] array = {0, 1, 2, 3, 4, 5}; + CollectionAssert.AreEqual(new[] {2, 3, 4, 5}, array.Slice(2).ToArray()); } [TestMethod] - public void Random_ShouldThrow_GivenNull() + public void Slice_ShouldReturnCorrectValue_GivenStartIndexAndLength() { - Assert.ThrowsException(() => ((List?)null)!.Random()); + int[] array = {0, 1, 2, 3, 4, 5}; + CollectionAssert.AreEqual(new[] {2, 3, 4}, array.Slice(2, 3).ToArray()); + } + + [TestMethod] + public void Slice_ShouldReturnEmptyList_ForEmptyList() + { + int[] array = Array.Empty(); + CollectionAssert.AreEqual(Array.Empty(), array.Slice(0).ToArray()); + CollectionAssert.AreEqual(Array.Empty(), array.Slice(0, 0).ToArray()); + } + + [TestMethod] + public void Slice_ShouldThrowArgumentNullException_GivenNullList() + { + int[]? array = null; + Assert.ThrowsException(() => array!.Slice(0)); + Assert.ThrowsException(() => array!.Slice(0, 0)); + } + + [TestMethod] + public void Slice_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount() + { + int[] array = Array.Empty(); + Assert.ThrowsException(() => array.Slice(0, -1)); + } + + [TestMethod] + public void Slice_ShouldThrowArgumentOutOfRangeException_GivenNegativeStartIndex() + { + int[] array = Array.Empty(); + Assert.ThrowsException(() => array.Slice(-1)); + Assert.ThrowsException(() => array.Slice(-1, 0)); + } + + [TestMethod] + public void Slice_ShouldThrowArgumentOutOfRangeException_GivenInvalidStartIndexCountPair() + { + int[] array = {0, 1, 2}; + Assert.ThrowsException(() => array.Slice(2, 4)); + } + + [TestMethod] + public void Swap_ShouldThrowArgumentNullException_GivenNullSource() + { + Assert.ThrowsException(() => ((IList?)null)!.Swap(new List())); + } + + [TestMethod] + public void Swap_ShouldThrowArgumentNullException_GivenNullTarget() + { + Assert.ThrowsException(() => new List().Swap(null!)); + } + + [TestMethod] + public void Swap_ShouldSwapElements_GivenMatchingElementCount() + { + var first = new List {1, 2, 3}; + var second = new List {4, 5, 6}; + + first.Swap(second); + + CollectionAssert.AreEqual(new[] {4, 5, 6}, first, string.Join(' ', first)); + CollectionAssert.AreEqual(new[] {1, 2, 3}, second, string.Join(' ', second)); + + first.Swap(second); + + CollectionAssert.AreEqual(new[] {1, 2, 3}, first, string.Join(' ', first)); + CollectionAssert.AreEqual(new[] {4, 5, 6}, second, string.Join(' ', second)); + } + + [TestMethod] + public void Swap_ShouldSwapElements_GivenDifferentElementCount() + { + var first = new List + { + 1, + 2, + 3, + 4, + 5 + }; + var second = new List {6, 7}; + + first.Swap(second); + + CollectionAssert.AreEqual(new[] {6, 7}, first, string.Join(' ', first)); + CollectionAssert.AreEqual(new[] {1, 2, 3, 4, 5}, second, string.Join(' ', second)); + + first.Swap(second); + + CollectionAssert.AreEqual(new[] {1, 2, 3, 4, 5}, first, string.Join(' ', first)); + CollectionAssert.AreEqual(new[] {6, 7}, second, string.Join(' ', second)); } } diff --git a/X10D.Tests/src/Collections/SpanTest.cs b/X10D.Tests/src/Collections/SpanTest.cs new file mode 100644 index 0000000..7506ca9 --- /dev/null +++ b/X10D.Tests/src/Collections/SpanTest.cs @@ -0,0 +1,476 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class SpanTest +{ + [TestMethod] + public void Count_ShouldReturn0_GivenEmptySpan() + { + Span span = Span.Empty; + + int count = span.Count(2); + + Assert.AreEqual(0, count); + } + + [TestMethod] + public void Count_ShouldReturn0_GivenEmptyReadOnlySpan() + { + ReadOnlySpan span = ReadOnlySpan.Empty; + + int count = span.Count(2); + + Assert.AreEqual(0, count); + } + + [TestMethod] + public void Count_ShouldReturn8_GivenSpanWith8MatchingElements() + { + Span span = stackalloc int[16] {1, 2, 3, 2, 5, 2, 7, 2, 9, 2, 11, 2, 13, 2, 15, 2}; + + int count = span.Count(2); + + Assert.AreEqual(8, count); + } + + [TestMethod] + public void Count_ShouldReturn8_GivenReadOnlySpanWith8MatchingElements() + { + ReadOnlySpan span = stackalloc int[16] {1, 2, 3, 2, 5, 2, 7, 2, 9, 2, 11, 2, 13, 2, 15, 2}; + + int count = span.Count(2); + + Assert.AreEqual(8, count); + } + + [TestMethod] + public void Split_OnEmptySpan_ShouldYieldNothing_UsingCharDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = ReadOnlySpan.Empty; + + var index = 0; + foreach (ReadOnlySpan unused in span.Split(' ')) + { + index++; + } + + Assert.AreEqual(0, index); + } + + [TestMethod] + public void Split_OnEmptySpan_ShouldYieldNothing_UsingCharDelimiter_GivenSpan() + { + Span span = Span.Empty; + + var index = 0; + foreach (ReadOnlySpan unused in span.Split(' ')) + { + index++; + } + + Assert.AreEqual(0, index); + } + + [TestMethod] + public void Split_OnEmptySpan_ShouldYieldNothing_UsingStringDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = ReadOnlySpan.Empty; + + var index = 0; + foreach (ReadOnlySpan unused in span.Split(" ")) + { + index++; + } + + Assert.AreEqual(0, index); + } + + [TestMethod] + public void Split_OnEmptySpan_ShouldYieldNothing_UsingStringDelimiter_GivenSpan() + { + Span span = Span.Empty; + + var index = 0; + foreach (ReadOnlySpan unused in span.Split(" ")) + { + index++; + } + + Assert.AreEqual(0, index); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1_UsingCharDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1_UsingCharDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1_UsingStringDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWord_ShouldYieldLength1_UsingStringDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWordWithoutDelimiter_ShouldYieldLength1_UsingCharDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWordWithoutDelimiter_ShouldYieldLength1_UsingCharDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWordWithoutDelimiter_ShouldYieldLength1_UsingStringDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnOneWordWithoutDelimiter_ShouldYieldLength1_UsingStringDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + if (index == 0) + { + Assert.AreEqual("Hello", subSpan.ToString()); + } + + index++; + } + + Assert.AreEqual(1, index); + } + + [TestMethod] + public void Split_OnTwoWords_ShouldYieldLength2_UsingCharDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(2, index); + } + + [TestMethod] + public void Split_OnTwoWords_ShouldYieldLength2_UsingCharDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello World ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(2, index); + } + + [TestMethod] + public void Split_OnTwoWords_ShouldYieldLength2_UsingStringDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(2, index); + } + + [TestMethod] + public void Split_OnTwoWords_ShouldYieldLength2_UsingStringDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello World ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(2, index); + } + + [TestMethod] + public void Split_OnThreeWords_ShouldYieldLength3_UsingCharDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello, the World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello,", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("the", subSpan.ToString()); + break; + case 2: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(3, index); + } + + [TestMethod] + public void Split_OnThreeWords_ShouldYieldLength3_UsingCharDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello, the World ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(' ')) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello,", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("the", subSpan.ToString()); + break; + case 2: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(3, index); + } + + [TestMethod] + public void Split_OnThreeWords_ShouldYieldLength3_UsingStringDelimiter_GivenReadOnlySpan() + { + ReadOnlySpan span = "Hello, the World ".AsSpan(); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello,", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("the", subSpan.ToString()); + break; + case 2: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(3, index); + } + + [TestMethod] + public void Split_OnThreeWords_ShouldYieldLength3_UsingStringDelimiter_GivenSpan() + { + ReadOnlySpan source = "Hello, the World ".AsSpan(); + Span span = stackalloc char[source.Length]; + source.CopyTo(span); + + var index = 0; + foreach (ReadOnlySpan subSpan in span.Split(" ")) + { + switch (index) + { + case 0: + Assert.AreEqual("Hello,", subSpan.ToString()); + break; + case 1: + Assert.AreEqual("the", subSpan.ToString()); + break; + case 2: + Assert.AreEqual("World", subSpan.ToString()); + break; + } + + index++; + } + + Assert.AreEqual(3, index); + } +} diff --git a/X10D.Tests/src/Core/EnumTests.cs b/X10D.Tests/src/Core/EnumTests.cs index 8e7889d..a96ecc0 100644 --- a/X10D.Tests/src/Core/EnumTests.cs +++ b/X10D.Tests/src/Core/EnumTests.cs @@ -11,7 +11,7 @@ public class EnumTests // it's clearly Monday as defined by ISO 8601. // but Microsoft can't fix this without breaking compatibility. // I have feelings... - + [TestMethod] public void Next() { @@ -23,7 +23,7 @@ public class EnumTests Assert.AreEqual(DayOfWeek.Saturday, DayOfWeek.Friday.Next()); Assert.AreEqual(DayOfWeek.Sunday, DayOfWeek.Saturday.Next()); // Saturday is the "last" day. wrap to "first" } - + [TestMethod] public void NextUnchecked() { @@ -35,7 +35,7 @@ public class EnumTests Assert.AreEqual(DayOfWeek.Saturday, DayOfWeek.Friday.NextUnchecked()); Assert.ThrowsException(() => DayOfWeek.Saturday.NextUnchecked()); } - + [TestMethod] public void Previous() { @@ -47,7 +47,7 @@ public class EnumTests Assert.AreEqual(DayOfWeek.Thursday, DayOfWeek.Friday.Previous()); Assert.AreEqual(DayOfWeek.Friday, DayOfWeek.Saturday.Previous()); } - + [TestMethod] public void PreviousUnchecked() { diff --git a/X10D.Tests/src/Core/IntrinsicTests.cs b/X10D.Tests/src/Core/IntrinsicTests.cs new file mode 100644 index 0000000..c1ea98f --- /dev/null +++ b/X10D.Tests/src/Core/IntrinsicTests.cs @@ -0,0 +1,226 @@ +#if NET6_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Core; + +namespace X10D.Tests.Core; + +[TestClass] +public class IntrinsicTests +{ + [TestMethod] + public void CorrectBoolean_ShouldReturnExpectedVector64Result_GivenInputVector() + { + var inputVector = Vector64.Create(0, 1, 2, 0, 3, 0, 0, (byte)4); + var expectedResult = Vector64.Create(0, 1, 1, 0, 1, 0, 0, (byte)1); + + Vector64 result = inputVector.CorrectBoolean(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void CorrectBooleanInternal_Fallback_ShouldReturnExpectedVector128Result_GivenInputVector() + { + var inputVector = Vector128.Create(0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, 0, 7, (byte)8); + var expectedResult = Vector128.Create(0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, (byte)1); + + Vector128 result = inputVector.CorrectBooleanInternal_Fallback(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void CorrectBooleanInternal_Sse2_ShouldReturnExpectedVector128Result_GivenInputVector() + { + if (!Sse2.IsSupported) + { + return; + } + + var inputVector = Vector128.Create(0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, 0, 7, (byte)8); + var expectedResult = Vector128.Create(0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, (byte)1); + + Vector128 result = inputVector.CorrectBooleanInternal_Sse2(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void CorrectBooleanInternal_Avx2_ShouldReturnExpectedVector256Result_GivenInputVector() + { + if (!Avx2.IsSupported) + { + return; + } + + var inputVector = Vector256.Create(0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, 0, 7, 8, 0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, + 0, 7, (byte)8); + var expectedResult = Vector256.Create(0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, + 0, 0, 1, (byte)1); + + Vector256 result = inputVector.CorrectBooleanInternal_Avx2(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void CorrectBooleanInternal_Fallback_ShouldReturnExpectedVector256Result_GivenInputVector() + { + var inputVector = Vector256.Create(0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, 0, 7, 8, 0, 1, 2, 0, 3, 0, 0, 4, 5, 0, 0, 6, 0, + 0, 7, (byte)8); + var expectedResult = Vector256.Create(0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, + 0, 0, 1, (byte)1); + + Vector256 result = inputVector.CorrectBooleanInternal_Fallback(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void HorizontalOr_ShouldReturnCombinedVector_GivenInputVector128OfUInt32() + { + Vector128 left = Vector128.Create(1U, 2U, 3U, 4U); + Vector128 right = Vector128.Create(5U, 6U, 7U, 8U); + + Vector128 expected = Vector128.Create(3U, 7U, 7U, 15U); + Vector128 actual = IntrinsicUtility.HorizontalOr(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void HorizontalOrInternal_Sse_ShouldReturnCombinedVector_GivenInputVector128OfInt32() + { + Vector128 left = Vector128.Create(1, 2, 3, 4); + Vector128 right = Vector128.Create(5, 6, 7, 8); + + Vector128 expected = Vector128.Create(3, 7, 7, 15); + Vector128 actual = IntrinsicUtility.HorizontalOr_Sse(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void HorizontalOrInternal_Fallback_ShouldReturnCombinedVector_GivenInputVector128OfInt32() + { + Vector128 left = Vector128.Create(1, 2, 3, 4); + Vector128 right = Vector128.Create(5, 6, 7, 8); + + Vector128 expected = Vector128.Create(3, 7, 7, 15); + Vector128 actual = IntrinsicUtility.HorizontalOrInternal_Fallback(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Multiply_ShouldReturnMultipliedVector_GivenInputVector128OfInt64() + { + Vector128 left = Vector128.Create(6L, 4L); + Vector128 right = Vector128.Create(2L, 3L); + + Vector128 expected = Vector128.Create(12L, 12L); + Vector128 actual = IntrinsicUtility.Multiply(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiplyInternal_Sse2_ShouldReturnMultipliedVector_GivenInputVector128OfUInt64() + { + if (!Sse2.IsSupported) + { + return; + } + + Vector128 left = Vector128.Create(6UL, 4UL); + Vector128 right = Vector128.Create(2UL, 3UL); + + Vector128 expected = Vector128.Create(12UL, 12UL); + Vector128 actual = IntrinsicUtility.MultiplyInternal_Sse2(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiplyInternal_Fallback_ShouldReturnMultipliedVector_GivenInputVector128OfUInt64() + { + Vector128 left = Vector128.Create(6UL, 4UL); + Vector128 right = Vector128.Create(2UL, 3UL); + + Vector128 expected = Vector128.Create(12UL, 12UL); + Vector128 actual = IntrinsicUtility.MultiplyInternal_Fallback(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Multiply_ShouldReturnMultipliedVector_GivenInputVector256OfInt64() + { + Vector256 left = Vector256.Create(4L, 6L, 8L, 10L); + Vector256 right = Vector256.Create(2L, 3L, 4L, 5L); + + Vector256 expected = Vector256.Create(8L, 18L, 32L, 50L); + Vector256 actual = IntrinsicUtility.Multiply(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiplyInternal_Avx2_ShouldReturnMultipliedVector_GivenInputVector256OfUInt64() + { + if (!Avx2.IsSupported) + { + return; + } + + Vector256 left = Vector256.Create(4UL, 6UL, 8UL, 10UL); + Vector256 right = Vector256.Create(2UL, 3UL, 4UL, 5UL); + + Vector256 expected = Vector256.Create(8UL, 18UL, 32UL, 50UL); + Vector256 actual = IntrinsicUtility.MultiplyInternal_Avx2(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiplyInternal_Fallback_ShouldReturnMultipliedVector_GivenInputVector256OfUInt64() + { + Vector256 left = Vector256.Create(4UL, 6UL, 8UL, 10UL); + Vector256 right = Vector256.Create(2UL, 3UL, 4UL, 5UL); + + Vector256 expected = Vector256.Create(8UL, 18UL, 32UL, 50UL); + Vector256 actual = IntrinsicUtility.MultiplyInternal_Fallback(left, right); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReverseElementsInternal_Fallback_ShouldReturnExpectedVector128Result_GivenInputVector() + { + var inputVector = Vector128.Create(42UL, 69UL); + var expectedResult = Vector128.Create(69UL, 42UL); + + Vector128 result = inputVector.ReverseElementsInternal_Fallback(); + + Assert.AreEqual(expectedResult, result); + } + + [TestMethod] + public void ReverseElementsInternal_Sse2_ShouldReturnExpectedVector128Result_GivenInputVector() + { + if (!Sse2.IsSupported) + { + return; + } + + var inputVector = Vector128.Create(42UL, 69UL); + var expectedResult = Vector128.Create(69UL, 42UL); + + Vector128 result = inputVector.ReverseElementsInternal_Sse2(); + + Assert.AreEqual(expectedResult, result); + } +} +#endif diff --git a/X10D.Tests/src/Core/NullableTests.cs b/X10D.Tests/src/Core/NullableTests.cs new file mode 100644 index 0000000..eda6883 --- /dev/null +++ b/X10D.Tests/src/Core/NullableTests.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Core; + +namespace X10D.Tests.Core; + +[TestClass] +public class NullableTests +{ + [TestMethod] + public void TryGetValue_ShouldBeTrue_GivenValue() + { + int? value = 42; + Assert.IsTrue(value.TryGetValue(out int returnedValue)); + Assert.AreEqual(value, returnedValue); + } + + [TestMethod] + public void TryGetValue_ShouldBeFalse_GivenNull() + { + int? value = null; + Assert.IsFalse(value.TryGetValue(out int returnedValue)); + Assert.AreEqual(default, returnedValue); + } +} diff --git a/X10D.Tests/src/Core/RandomTests.cs b/X10D.Tests/src/Core/RandomTests.cs index 41d182b..9824c01 100644 --- a/X10D.Tests/src/Core/RandomTests.cs +++ b/X10D.Tests/src/Core/RandomTests.cs @@ -1,4 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; using X10D.Core; namespace X10D.Tests.Core; @@ -126,6 +127,48 @@ public class RandomTests Assert.AreEqual(0, random.NextFrom(Source())); } + [TestMethod] + public void NextFromSpan_ShouldThrow_GivenNullRandom() + { + Random? random = null; + Assert.ThrowsException(() => + { + Span span = stackalloc int[1]; + return random!.NextFrom(span); + }); + } + + [TestMethod] + public void NextFromReadOnlySpan_ShouldThrow_GivenNullRandom() + { + Random? random = null; + Assert.ThrowsException(() => + { + Span span = stackalloc int[1]; + return random!.NextFrom(span.AsReadOnly()); + }); + } + + [TestMethod] + public void NextFromSpan_ShouldReturnOnlyValue_GivenSpanWithLength1() + { + Span span = stackalloc int[1]; + span[0] = 42; + + var random = new Random(1234); + Assert.AreEqual(42, random.NextFrom(span)); + } + + [TestMethod] + public void NextFromReadOnlySpan_ShouldReturnOnlyValue_GivenSpanWithLength1() + { + Span span = stackalloc int[1]; + span[0] = 42; + + var random = new Random(1234); + Assert.AreEqual(42, random.NextFrom(span.AsReadOnly())); + } + [TestMethod] public void NextInt16_ShouldBe13076_GivenSeed1234() { @@ -183,6 +226,9 @@ public class RandomTests Random? random = null; Assert.ThrowsException(() => random!.NextSingle(10)); Assert.ThrowsException(() => random!.NextSingle(0, 10)); +#if !NET6_0_OR_GREATER + Assert.ThrowsException(() => random!.NextSingle()); +#endif } [TestMethod] diff --git a/X10D.Tests/src/Core/SpanTest.cs b/X10D.Tests/src/Core/SpanTest.cs new file mode 100644 index 0000000..e242c12 --- /dev/null +++ b/X10D.Tests/src/Core/SpanTest.cs @@ -0,0 +1,570 @@ +#if NET5_0_OR_GREATER +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +#endif +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Core; + +namespace X10D.Tests.Core; + +[TestClass] +public class SpanTest +{ + [TestMethod] + public void Contains_ShouldReturnFalse_GivenReadOnlySpanWithNoMatchingElements_UsingByteEnum() + { + ReadOnlySpan span = stackalloc EnumByte[1] {EnumByte.B}; + + Assert.IsFalse(span.Contains(EnumByte.A)); + Assert.IsFalse(span.Contains(EnumByte.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenReadOnlySpanWithNoMatchingElements_UsingInt16Enum() + { + ReadOnlySpan span = stackalloc EnumInt16[1] {EnumInt16.B}; + + Assert.IsFalse(span.Contains(EnumInt16.A)); + Assert.IsFalse(span.Contains(EnumInt16.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenReadOnlySpanWithNoMatchingElements_UsingInt32Enum() + { + ReadOnlySpan span = stackalloc EnumInt32[1] {EnumInt32.B}; + + Assert.IsFalse(span.Contains(EnumInt32.A)); + Assert.IsFalse(span.Contains(EnumInt32.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenReadOnlySpanWithNoMatchingElements_UsingInt64Enum() + { + ReadOnlySpan span = stackalloc EnumInt64[1] {EnumInt64.B}; + + Assert.IsFalse(span.Contains(EnumInt64.A)); + Assert.IsFalse(span.Contains(EnumInt64.C)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenReadOnlySpanWithMatchingElements_UsingByteEnum() + { + ReadOnlySpan span = stackalloc EnumByte[1] {EnumByte.B}; + + Assert.IsTrue(span.Contains(EnumByte.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenReadOnlySpanWithMatchingElements_UsingInt16Enum() + { + ReadOnlySpan span = stackalloc EnumInt16[1] {EnumInt16.B}; + + Assert.IsTrue(span.Contains(EnumInt16.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenReadOnlySpanWithMatchingElements_UsingInt32Enum() + { + ReadOnlySpan span = stackalloc EnumInt32[1] {EnumInt32.B}; + + Assert.IsTrue(span.Contains(EnumInt32.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenReadOnlySpanWithMatchingElements_UsingInt64Enum() + { + ReadOnlySpan span = stackalloc EnumInt64[1] {EnumInt64.B}; + + Assert.IsTrue(span.Contains(EnumInt64.B)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenSpanWithNoMatchingElements_UsingByteEnum() + { + Span span = stackalloc EnumByte[1] {EnumByte.B}; + + Assert.IsFalse(span.Contains(EnumByte.A)); + Assert.IsFalse(span.Contains(EnumByte.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenSpanWithNoMatchingElements_UsingInt16Enum() + { + Span span = stackalloc EnumInt16[1] {EnumInt16.B}; + + Assert.IsFalse(span.Contains(EnumInt16.A)); + Assert.IsFalse(span.Contains(EnumInt16.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenSpanWithNoMatchingElements_UsingInt32Enum() + { + Span span = stackalloc EnumInt32[1] {EnumInt32.B}; + + Assert.IsFalse(span.Contains(EnumInt32.A)); + Assert.IsFalse(span.Contains(EnumInt32.C)); + } + + [TestMethod] + public void Contains_ShouldReturnFalse_GivenSpanWithNoMatchingElements_UsingInt64Enum() + { + Span span = stackalloc EnumInt64[1] {EnumInt64.B}; + + Assert.IsFalse(span.Contains(EnumInt64.A)); + Assert.IsFalse(span.Contains(EnumInt64.C)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenSpanWithMatchingElements_UsingByteEnum() + { + Span span = stackalloc EnumByte[1] {EnumByte.B}; + + Assert.IsTrue(span.Contains(EnumByte.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenSpanWithMatchingElements_UsingInt16Enum() + { + Span span = stackalloc EnumInt16[1] {EnumInt16.B}; + + Assert.IsTrue(span.Contains(EnumInt16.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenSpanWithMatchingElements_UsingInt32Enum() + { + Span span = stackalloc EnumInt32[1] {EnumInt32.B}; + + Assert.IsTrue(span.Contains(EnumInt32.B)); + } + + [TestMethod] + public void Contains_ShouldReturnTrue_GivenSpanWithMatchingElements_UsingInt64Enum() + { + Span span = stackalloc EnumInt64[1] {EnumInt64.B}; + + Assert.IsTrue(span.Contains(EnumInt64.B)); + } + + [TestMethod] + public void PackByte_ShouldThrowArgumentException_GivenSpanLengthGreaterThan8() + { + Assert.ThrowsException(() => + { + Span span = stackalloc bool[9]; + return span.PackByte(); + }); + } + + [TestMethod] + public void PackInt16_ShouldThrowArgumentException_GivenSpanLengthGreaterThan16() + { + Assert.ThrowsException(() => + { + Span span = stackalloc bool[17]; + return span.PackInt16(); + }); + } + + [TestMethod] + public void PackInt32_ShouldThrowArgumentException_GivenSpanLengthGreaterThan32() + { + Assert.ThrowsException(() => + { + Span span = stackalloc bool[33]; + return span.PackInt32(); + }); + } + + [TestMethod] + public void PackInt64_ShouldThrowArgumentException_GivenSpanLengthGreaterThan64() + { + Assert.ThrowsException(() => + { + Span span = stackalloc bool[65]; + return span.PackInt64(); + }); + } + + [TestMethod] + public void PackByteInternal_Fallback_ShouldReturnCorrectByte_GivenReadOnlySpan_Using() + { + const byte expected = 0b00110011; + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + byte actual = span.PackByteInternal_Fallback(); + + Assert.AreEqual(expected, actual); + } + +#if NET5_0_OR_GREATER + [TestMethod] + public void PackByteInternal_Sse2_ShouldReturnCorrectByte_GivenReadOnlySpan_Using() + { + if (!Sse2.IsSupported) + { + return; + } + + const byte expected = 0b00110011; + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + byte actual = span.PackByteInternal_Sse2(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackByteInternal_AdvSimd_ShouldReturnCorrectByte_GivenReadOnlySpan_Using() + { + if (!AdvSimd.IsSupported) + { + return; + } + + const byte expected = 0b00110011; + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + byte actual = span.PackByteInternal_AdvSimd(); + + Assert.AreEqual(expected, actual); + } +#endif + + [TestMethod] + public void PackInt16_ShouldReturnSameAsPackByte_WhenSpanHasLength8() + { + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + short expected = span.PackByte(); + short actual = span.PackInt16(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt16Internal_Fallback_ShouldReturnCorrectInt16_GivenReadOnlySpan() + { + const short expected = 0b00101101_11010100; + ReadOnlySpan span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + short actual = span.PackInt16Internal_Fallback(); + + Assert.AreEqual(expected, actual); + } + +#if NET5_0_OR_GREATER + [TestMethod] + public void PackInt16Internal_Sse2_ShouldReturnCorrectInt16_GivenReadOnlySpan_Using() + { + if (!Sse2.IsSupported) + { + return; + } + + const short expected = 0b00101101_11010100; + ReadOnlySpan span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + short actual = span.PackInt16Internal_Sse2(); + + Assert.AreEqual(expected, actual); + } +#endif + + [TestMethod] + public void PackInt32Internal_Fallback_ShouldReturnCorrectInt32_GivenReadOnlySpan() + { + const int expected = 0b01010101_10101010_01010101_10101010; + ReadOnlySpan span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + int actual = span.PackInt32Internal_Fallback(); + + Assert.AreEqual(expected, actual); + } + +#if NET5_0_OR_GREATER + [TestMethod] + public void PackInt32Internal_Sse2_ShouldReturnCorrectInt32_GivenReadOnlySpan() + { + if (!Sse2.IsSupported) + { + return; + } + + const int expected = 0b01010101_10101010_01010101_10101010; + ReadOnlySpan span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + int actual = span.PackInt32Internal_Sse2(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt32Internal_Avx2_ShouldReturnCorrectInt32_GivenReadOnlySpan() + { + if (!Avx2.IsSupported) + { + return; + } + + const int expected = 0b01010101_10101010_01010101_10101010; + ReadOnlySpan span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + int actual = span.PackInt32Internal_Avx2(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt32Internal_AdvSimd_ShouldReturnCorrectInt32_GivenReadOnlySpan() + { + if (!AdvSimd.IsSupported) + { + return; + } + + const int expected = 0b01010101_10101010_01010101_10101010; + ReadOnlySpan span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + int actual = span.PackInt32Internal_AdvSimd(); + + Assert.AreEqual(expected, actual); + } +#endif + + [TestMethod] + public void PackInt32_ShouldReturnSameAsPackByte_WhenSpanHasLength8_UsingReadOnlySpan() + { + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + int expected = span.PackByte(); + int actual = span.PackInt32(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt32_ShouldReturnSameAsPackByte_WhenSpanHasLength8_UsingSpan() + { + Span span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + int expected = span.PackByte(); + int actual = span.PackInt32(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt32_ShouldReturnSameAsPackInt16_WhenSpanHasLength16_UsingReadOnlySpan() + { + ReadOnlySpan span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + int expected = span.PackInt16(); + int actual = span.PackInt32(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt32_ShouldReturnSameAsPackInt16_WhenSpanHasLength16_UsingSpan() + { + Span span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + int expected = span.PackInt16(); + int actual = span.PackInt32(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnCorrectInt64_GivenReadOnlySpan() + { + const long expected = 0b01010101_11010110_01101001_11010110_00010010_10010111_00101100_10100101; + ReadOnlySpan span = stackalloc bool[64] + { + true, false, true, false, false, true, false, true, false, false, true, true, false, true, false, false, true, + true, true, false, true, false, false, true, false, true, false, false, true, false, false, false, false, true, + true, false, true, false, true, true, true, false, false, true, false, true, true, false, false, true, true, + false, true, false, true, true, true, false, true, false, true, false, true, false, + }; + + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnCorrectInt64_GivenSpan() + { + const long expected = 0b01010101_11010110_01101001_11010110_00010010_10010111_00101100_10100101; + Span span = stackalloc bool[64] + { + true, false, true, false, false, true, false, true, false, false, true, true, false, true, false, false, true, + true, true, false, true, false, false, true, false, true, false, false, true, false, false, false, false, true, + true, false, true, false, true, true, true, false, false, true, false, true, true, false, false, true, true, + false, true, false, true, true, true, false, true, false, true, false, true, false, + }; + + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackByte_WhenSpanHasLength8_UsingReadOnlySpan() + { + ReadOnlySpan span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + long expected = span.PackByte(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackByte_WhenSpanHasLength8_UsingSpan() + { + Span span = stackalloc bool[8] {true, true, false, false, true, true, false, false}; + + long expected = span.PackByte(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackInt16_WhenSpanHasLength16_UsingReadOnlySpan() + { + ReadOnlySpan span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + long expected = span.PackInt16(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackInt16_WhenSpanHasLength16_UsingSpan() + { + Span span = stackalloc bool[16] + { + false, false, true, false, true, false, true, true, true, false, true, true, false, true, false, false, + }; + + long expected = span.PackInt16(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackInt32_WhenSpanHasLength16_UsingReadOnlySpan() + { + ReadOnlySpan span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + long expected = span.PackInt32(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldReturnSameAsPackInt32_WhenSpanHasLength16_UsingSpan() + { + Span span = stackalloc bool[32] + { + false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, + }; + + long expected = span.PackInt32(); + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldFallbackAndReturnCorrectValue_GivenNonPowerOfTwoLength_UsingReadOnlySpan() + { + const long expected = 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_01010011; + ReadOnlySpan span = stackalloc bool[10] {true, true, false, false, true, false, true, false, true, false}; + + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void PackInt64_ShouldFallbackAndReturnCorrectValue_GivenNonPowerOfTwoLength_UsingSpan() + { + const long expected = 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_01010011; + Span span = stackalloc bool[10] {true, true, false, false, true, false, true, false, true, false}; + + long actual = span.PackInt64(); + + Assert.AreEqual(expected, actual); + } + + private enum EnumByte : byte + { + A, + B, + C + } + + private enum EnumInt16 : short + { + A, + B, + C + } + + private enum EnumInt32 + { + A, + B, + C + } + + private enum EnumInt64 : long + { + A, + B, + C + } +} diff --git a/X10D.Tests/src/Drawing/CircleFTests.cs b/X10D.Tests/src/Drawing/CircleFTests.cs new file mode 100644 index 0000000..d8e8256 --- /dev/null +++ b/X10D.Tests/src/Drawing/CircleFTests.cs @@ -0,0 +1,167 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class CircleFTests +{ + [TestMethod] + public void Area_ShouldBePiRadiusRadius_GivenUnitCircle() + { + var unitCircle = CircleF.Unit; + Assert.AreEqual(MathF.PI, unitCircle.Area); + } + + [TestMethod] + public void Circumference_ShouldBe2PiRadius_GivenUnitCircle() + { + var unitCircle = CircleF.Unit; + Assert.AreEqual(2 * MathF.PI, unitCircle.Circumference, 1e-6f); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(-1, CircleF.Empty.CompareTo(CircleF.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(1, CircleF.Unit.CompareTo(CircleF.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyCircleAndUnitCircleAsObject() + { + Assert.AreEqual(-1, CircleF.Empty.CompareTo((object)CircleF.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, CircleF.Unit.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitCircle() + { + var unitCircle = CircleF.Unit; + Assert.AreEqual(0, unitCircle.CompareTo(unitCircle)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => CircleF.Unit.CompareTo(new object())); + } + + [TestMethod] + public void Diameter_ShouldBe2_GivenUnitCircle() + { + Assert.AreEqual(2.0f, CircleF.Unit.Diameter, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitCircles() + { + var unitCircle1 = CircleF.Unit; + var unitCircle2 = CircleF.Unit; + Assert.AreEqual(unitCircle1, unitCircle2); + Assert.IsTrue(unitCircle1 == unitCircle2); + Assert.IsFalse(unitCircle1 != unitCircle2); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenUnitCirclesAsObjects() + { + CircleF unitCircle1 = CircleF.Unit; + object unitCircle2 = CircleF.Unit; + Assert.AreEqual(unitCircle1, unitCircle2); + Assert.IsTrue(unitCircle1.Equals(unitCircle2)); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentCircles() + { + Assert.AreNotEqual(CircleF.Unit, CircleF.Empty); + Assert.IsFalse(CircleF.Unit == CircleF.Empty); + Assert.IsTrue(CircleF.Unit != CircleF.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.AreNotEqual((object?)null, CircleF.Empty); + Assert.IsFalse(CircleF.Empty.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = CircleF.Empty.GetHashCode(); + Assert.AreEqual(hashCode, CircleF.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = CircleF.Unit.GetHashCode(); + Assert.AreEqual(hashCode, CircleF.Unit.GetHashCode()); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentCircle_GivenCircle() + { + CircleF unitCircle = CircleF.Unit; + Circle converted = (Circle)unitCircle; + + Assert.AreEqual(unitCircle, converted); + Assert.AreEqual(unitCircle.Radius, converted.Radius); + Assert.AreEqual(unitCircle.Center, converted.Center); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(CircleF.Unit > CircleF.Empty); + Assert.IsTrue(CircleF.Unit >= CircleF.Empty); + Assert.IsFalse(CircleF.Unit < CircleF.Empty); + Assert.IsFalse(CircleF.Unit <= CircleF.Empty); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentCircle_GivenCircle() + { + Circle unitCircle = Circle.Unit; + CircleF converted = unitCircle; + + Assert.AreEqual(unitCircle, converted); + Assert.AreEqual(unitCircle.Radius, converted.Radius); + Assert.AreEqual(unitCircle.Center, converted.Center); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(CircleF.Empty < CircleF.Unit); + Assert.IsTrue(CircleF.Empty <= CircleF.Unit); + Assert.IsFalse(CircleF.Empty > CircleF.Unit); + Assert.IsFalse(CircleF.Empty >= CircleF.Unit); + } + + [TestMethod] + public void Radius_ShouldBe0_GivenEmptyCircle() + { + Assert.AreEqual(0.0f, CircleF.Empty.Radius, 1e-6f); + } + + [TestMethod] + public void Radius_ShouldBe1_GivenUnitCircle() + { + Assert.AreEqual(1.0f, CircleF.Unit.Radius, 1e-6f); + } +} diff --git a/X10D.Tests/src/Drawing/CircleTests.cs b/X10D.Tests/src/Drawing/CircleTests.cs new file mode 100644 index 0000000..3151299 --- /dev/null +++ b/X10D.Tests/src/Drawing/CircleTests.cs @@ -0,0 +1,145 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class CircleTests +{ + [TestMethod] + public void Area_ShouldBePiRadiusRadius_GivenUnitCircle() + { + var unitCircle = Circle.Unit; + Assert.AreEqual(MathF.PI * unitCircle.Radius * unitCircle.Radius, unitCircle.Area); + } + + [TestMethod] + public void Circumference_ShouldBe2PiRadius_GivenUnitCircle() + { + var unitCircle = Circle.Unit; + Assert.AreEqual(2.0f * MathF.PI * unitCircle.Radius, unitCircle.Circumference, 1e-6f); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(-1, Circle.Empty.CompareTo(Circle.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(1, Circle.Unit.CompareTo(Circle.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyCircleAndUnitCircleAsObject() + { + Assert.AreEqual(-1, Circle.Empty.CompareTo((object)Circle.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, Circle.Unit.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitCircle() + { + var unitCircle = Circle.Unit; + Assert.AreEqual(0, unitCircle.CompareTo(unitCircle)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => Circle.Unit.CompareTo(new object())); + } + + [TestMethod] + public void Diameter_ShouldBe2_GivenUnitCircle() + { + Assert.AreEqual(2.0f, Circle.Unit.Diameter, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitCircles() + { + var unitCircle1 = Circle.Unit; + var unitCircle2 = Circle.Unit; + Assert.AreEqual(unitCircle1, unitCircle2); + Assert.IsTrue(unitCircle1 == unitCircle2); + Assert.IsFalse(unitCircle1 != unitCircle2); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenUnitCirclesAsObjects() + { + Circle unitCircle1 = Circle.Unit; + object unitCircle2 = Circle.Unit; + Assert.AreEqual(unitCircle1, unitCircle2); + Assert.IsTrue(unitCircle1.Equals(unitCircle2)); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentCircles() + { + Assert.AreNotEqual(Circle.Unit, Circle.Empty); + Assert.IsFalse(Circle.Unit == Circle.Empty); + Assert.IsTrue(Circle.Unit != Circle.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.AreNotEqual((object?)null, Circle.Empty); + Assert.IsFalse(Circle.Empty.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Circle.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Circle.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Circle.Unit.GetHashCode(); + Assert.AreEqual(hashCode, Circle.Unit.GetHashCode()); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(Circle.Unit > Circle.Empty); + Assert.IsTrue(Circle.Unit >= Circle.Empty); + Assert.IsFalse(Circle.Unit < Circle.Empty); + Assert.IsFalse(Circle.Unit <= Circle.Empty); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(Circle.Empty < Circle.Unit); + Assert.IsTrue(Circle.Empty <= Circle.Unit); + Assert.IsFalse(Circle.Empty > Circle.Unit); + Assert.IsFalse(Circle.Empty >= Circle.Unit); + } + + [TestMethod] + public void Radius_ShouldBe0_GivenEmptyCircle() + { + Assert.AreEqual(0, Circle.Empty.Radius); + } + + [TestMethod] + public void Radius_ShouldBe1_GivenUnitCircle() + { + Assert.AreEqual(1, Circle.Unit.Radius); + } +} diff --git a/X10D.Tests/src/Drawing/ColorTests.cs b/X10D.Tests/src/Drawing/ColorTests.cs index 09c642c..a7ecec9 100644 --- a/X10D.Tests/src/Drawing/ColorTests.cs +++ b/X10D.Tests/src/Drawing/ColorTests.cs @@ -16,6 +16,203 @@ public class ColorTests private static readonly Color Magenta = Color.FromArgb(255, 0, 255); private static readonly Color Yellow = Color.FromArgb(255, 255, 0); + [TestMethod] + public void Deconstruct_ShouldDeconstructColor_GivenColor() + { + (byte r, byte g, byte b) = Black; + Assert.AreEqual(0, r); + Assert.AreEqual(0, g); + Assert.AreEqual(0, b); + + (byte a, r, g, b) = Black; + Assert.AreEqual(255, a); + Assert.AreEqual(0, r); + Assert.AreEqual(0, g); + Assert.AreEqual(0, b); + + (r, g, b) = Red; + Assert.AreEqual(255, r); + Assert.AreEqual(0, g); + Assert.AreEqual(0, b); + + (a, r, g, b) = Red; + Assert.AreEqual(255, a); + Assert.AreEqual(255, r); + Assert.AreEqual(0, g); + Assert.AreEqual(0, b); + + (r, g, b) = Green; + Assert.AreEqual(0, r); + Assert.AreEqual(255, g); + Assert.AreEqual(0, b); + + (a, r, g, b) = Green; + Assert.AreEqual(255, a); + Assert.AreEqual(0, r); + Assert.AreEqual(255, g); + Assert.AreEqual(0, b); + + (r, g, b) = Blue; + Assert.AreEqual(0, r); + Assert.AreEqual(0, g); + Assert.AreEqual(255, b); + + (a, r, g, b) = Blue; + Assert.AreEqual(255, a); + Assert.AreEqual(0, r); + Assert.AreEqual(0, g); + Assert.AreEqual(255, b); + } + + [TestMethod] + public void GetClosestConsoleColor_ShouldReturnClosestColor_GivenValidColor() + { + Assert.AreEqual(ConsoleColor.White, Color.Transparent.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.AliceBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.AntiqueWhite.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.Aqua.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Aquamarine.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Azure.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Beige.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Bisque.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Black, Color.Black.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.BlanchedAlmond.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Blue, Color.Blue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.BlueViolet.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.Brown.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.BurlyWood.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.CadetBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.Chartreuse.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Chocolate.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Coral.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.CornflowerBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Cornsilk.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Red, Color.Crimson.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.Cyan.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkBlue, Color.DarkBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.DarkCyan.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.DarkGoldenrod.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.DarkGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGreen, Color.DarkGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.DarkKhaki.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.DarkMagenta.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.DarkOliveGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.DarkOrange.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.DarkOrchid.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.DarkRed.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.DarkSalmon.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.DarkSeaGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.DarkSlateBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGreen, Color.DarkSlateGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.DarkTurquoise.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.DarkViolet.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Magenta, Color.DeepPink.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.DeepSkyBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.DimGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.DodgerBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.Firebrick.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.FloralWhite.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, Color.ForestGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Magenta, Color.Fuchsia.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Gainsboro.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.GhostWhite.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.Gold.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Goldenrod.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.Gray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, Color.Green.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.GreenYellow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Honeydew.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.HotPink.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.IndianRed.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.Indigo.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Ivory.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Khaki.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Lavender.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.LavenderBlush.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.LawnGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.LemonChiffon.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightCoral.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.LightCyan.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.LightGoldenrodYellow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightPink.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightSalmon.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.LightSeaGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightSkyBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.LightSlateGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.LightSteelBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.LightYellow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, Color.Lime.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, Color.LimeGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Linen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Magenta, Color.Magenta.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.Maroon.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.MediumAquamarine.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Blue, Color.MediumBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.MediumOrchid.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.MediumPurple.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.MediumSeaGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.MediumSlateBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.MediumSpringGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.MediumTurquoise.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.MediumVioletRed.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkBlue, Color.MidnightBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.MintCream.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.MistyRose.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Moccasin.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.NavajoWhite.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkBlue, Color.Navy.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.OldLace.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.Olive.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.OliveDrab.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Orange.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Red, Color.OrangeRed.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Orchid.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.PaleGoldenrod.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.PaleGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.PaleTurquoise.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.PaleVioletRed.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.PapayaWhip.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.PeachPuff.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Peru.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Pink.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Plum.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.PowderBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.Purple.GetClosestConsoleColor()); +#if NET6_0_OR_GREATER + Assert.AreEqual(ConsoleColor.DarkMagenta, Color.RebeccaPurple.GetClosestConsoleColor()); +#endif + Assert.AreEqual(ConsoleColor.Red, Color.Red.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.RosyBrown.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.RoyalBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.SaddleBrown.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Salmon.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.SandyBrown.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.SeaGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.SeaShell.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkRed, Color.Sienna.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Silver.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.SkyBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.SlateBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.SlateGray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Snow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.SpringGreen.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.SteelBlue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Tan.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkCyan, Color.Teal.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Thistle.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkYellow, Color.Tomato.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.Turquoise.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.DarkGray, Color.Violet.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.Wheat.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.White.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.WhiteSmoke.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.Yellow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.YellowGreen.GetClosestConsoleColor()); + } + [TestMethod] public void Inverted_ShouldReturnInvertedColor() { diff --git a/X10D.Tests/src/Drawing/CuboidTests.cs b/X10D.Tests/src/Drawing/CuboidTests.cs new file mode 100644 index 0000000..451a02b --- /dev/null +++ b/X10D.Tests/src/Drawing/CuboidTests.cs @@ -0,0 +1,82 @@ +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class CuboidTests +{ + [TestMethod] + public void Corners_ShouldBeCorrect_GivenCubeOfSize1() + { + Cuboid cube = Cuboid.Cube; + + Assert.AreEqual(new Vector3(0.5f, 0.5f, -0.5f), cube.FrontTopRight); + Assert.AreEqual(new Vector3(-0.5f, 0.5f, -0.5f), cube.FrontTopLeft); + Assert.AreEqual(new Vector3(0.5f, -0.5f, -0.5f), cube.FrontBottomRight); + Assert.AreEqual(new Vector3(-0.5f, -0.5f, -0.5f), cube.FrontBottomLeft); + Assert.AreEqual(new Vector3(0.5f, 0.5f, 0.5f), cube.BackTopRight); + Assert.AreEqual(new Vector3(-0.5f, 0.5f, 0.5f), cube.BackTopLeft); + Assert.AreEqual(new Vector3(0.5f, -0.5f, 0.5f), cube.BackBottomRight); + Assert.AreEqual(new Vector3(-0.5f, -0.5f, 0.5f), cube.BackBottomLeft); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoCubesOfSize1() + { + var cube1 = Cuboid.Cube; + var cube2 = Cuboid.Cube; + Assert.AreEqual(cube1, cube2); + Assert.IsTrue(cube1 == cube2); + Assert.IsFalse(cube1 != cube2); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentCubes() + { + Assert.AreNotEqual(Cuboid.Cube, Cuboid.Empty); + Assert.IsFalse(Cuboid.Cube == Cuboid.Empty); + Assert.IsTrue(Cuboid.Cube != Cuboid.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(Cuboid.Cube.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Cuboid.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Cuboid.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenCubeOfSize1() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Cuboid.Cube.GetHashCode(); + Assert.AreEqual(hashCode, Cuboid.Cube.GetHashCode()); + } + + [TestMethod] + public void Size_ShouldBeOne_GivenCubeOfSize1() + { + Assert.AreEqual(Vector3.One, Cuboid.Cube.Size); + } + + [TestMethod] + public void Size_ShouldBeOne_GivenRotatedCube() + { + Assert.AreEqual(Vector3.One, new Cuboid(0, 0, 0, 1, 1, 1, 90, 0, 0).Size); + } + + [TestMethod] + public void Volume_ShouldBe1_GivenCubeOfSize1() + { + Assert.AreEqual(1.0f, Cuboid.Cube.Volume, 1e-6f); + } +} diff --git a/X10D.Tests/src/Drawing/EllipseFTests.cs b/X10D.Tests/src/Drawing/EllipseFTests.cs new file mode 100644 index 0000000..97b5af5 --- /dev/null +++ b/X10D.Tests/src/Drawing/EllipseFTests.cs @@ -0,0 +1,156 @@ +using System.Drawing; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class EllipseFTests +{ + [TestMethod] + public void Area_ShouldBePiRadiusRadius_GivenUnitEllipse() + { + var unitEllipse = EllipseF.Unit; + Assert.AreEqual(MathF.PI, unitEllipse.Area, 1e-6f); + } + + [TestMethod] + public void ApproximateCircumference_ShouldBe2PiRadius_GivenUnitEllipse() + { + var unitEllipse = EllipseF.Unit; + Assert.AreEqual(2 * MathF.PI, unitEllipse.ApproximateCircumference, 1e-6f); + } + + [TestMethod] + public void Constructor_ShouldGiveCorrectEllipse() + { + var ellipse = new EllipseF(PointF.Empty, new SizeF(2, 1)); + Assert.AreEqual(new PointF(0, 0), ellipse.Center); + Assert.AreEqual(new SizeF(2, 1), ellipse.Radius); + + ellipse = new EllipseF(0, 0, 2, 1); + Assert.AreEqual(new PointF(0, 0), ellipse.Center); + Assert.AreEqual(new SizeF(2, 1), ellipse.Radius); + + ellipse = new EllipseF(PointF.Empty, new Vector2(2, 1)); + Assert.AreEqual(new PointF(0, 0), ellipse.Center); + Assert.AreEqual(new SizeF(2, 1), ellipse.Radius); + + ellipse = new EllipseF(Vector2.Zero, new Vector2(2, 1)); + Assert.AreEqual(new PointF(0, 0), ellipse.Center); + Assert.AreEqual(new SizeF(2, 1), ellipse.Radius); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitEllipses() + { + var unitEllipse1 = EllipseF.Unit; + var unitEllipse2 = EllipseF.Unit; + Assert.AreEqual(unitEllipse1, unitEllipse2); + Assert.IsTrue(unitEllipse1 == unitEllipse2); + Assert.IsFalse(unitEllipse1 != unitEllipse2); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentEllipses() + { + Assert.AreNotEqual(EllipseF.Unit, EllipseF.Empty); + Assert.IsFalse(EllipseF.Unit == EllipseF.Empty); + Assert.IsTrue(EllipseF.Unit != EllipseF.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(EllipseF.Unit.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyEllipse() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = EllipseF.Empty.GetHashCode(); + Assert.AreEqual(hashCode, EllipseF.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitEllipse() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = EllipseF.Unit.GetHashCode(); + Assert.AreEqual(hashCode, EllipseF.Unit.GetHashCode()); + } + + [TestMethod] + public void HorizontalRadius_ShouldBe0_GivenEmptyEllipse() + { + Assert.AreEqual(0, EllipseF.Empty.HorizontalRadius); + } + + [TestMethod] + public void HorizontalRadius_ShouldBe1_GivenUnitEllipse() + { + Assert.AreEqual(1, EllipseF.Unit.HorizontalRadius); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentEllipse_GivenEllipse() + { + EllipseF unitEllipse = EllipseF.Unit; + Ellipse converted = (Ellipse)unitEllipse; + + Assert.AreEqual(unitEllipse, converted); + Assert.AreEqual(unitEllipse.HorizontalRadius, converted.HorizontalRadius); + Assert.AreEqual(unitEllipse.VerticalRadius, converted.VerticalRadius); + Assert.AreEqual(unitEllipse.Center, converted.Center); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentEllipse_GivenCircle() + { + Circle unitCircle = Circle.Unit; + EllipseF converted = unitCircle; + + Assert.AreEqual(unitCircle, converted); + Assert.AreEqual(unitCircle.Radius, converted.HorizontalRadius); + Assert.AreEqual(unitCircle.Radius, converted.VerticalRadius); + Assert.AreEqual(unitCircle.Center, converted.Center); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentEllipse_GivenCircleF() + { + CircleF unitCircle = CircleF.Unit; + EllipseF converted = unitCircle; + + Assert.AreEqual(unitCircle, converted); + Assert.AreEqual(unitCircle.Radius, converted.HorizontalRadius); + Assert.AreEqual(unitCircle.Radius, converted.VerticalRadius); + Assert.AreEqual(unitCircle.Center, converted.Center); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentEllipse_GivenEllipse() + { + Ellipse unitEllipse = Ellipse.Unit; + EllipseF converted = unitEllipse; + + Assert.AreEqual(unitEllipse, converted); + Assert.AreEqual(unitEllipse.HorizontalRadius, converted.HorizontalRadius); + Assert.AreEqual(unitEllipse.VerticalRadius, converted.VerticalRadius); + Assert.AreEqual(unitEllipse.Center, converted.Center); + } + + [TestMethod] + public void VerticalRadius_ShouldBe0_GivenEmptyEllipse() + { + Assert.AreEqual(0, EllipseF.Empty.VerticalRadius); + } + + [TestMethod] + public void VerticalRadius_ShouldBe1_GivenUnitEllipse() + { + Assert.AreEqual(1, EllipseF.Unit.VerticalRadius); + } +} diff --git a/X10D.Tests/src/Drawing/EllipseTests.cs b/X10D.Tests/src/Drawing/EllipseTests.cs new file mode 100644 index 0000000..c5482a3 --- /dev/null +++ b/X10D.Tests/src/Drawing/EllipseTests.cs @@ -0,0 +1,111 @@ +using System.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class EllipseTests +{ + [TestMethod] + public void Area_ShouldBePiRadiusRadius_GivenUnitEllipse() + { + var unitEllipse = Ellipse.Unit; + Assert.AreEqual(MathF.PI, unitEllipse.Area, 1e-6f); + } + + [TestMethod] + public void ApproximateCircumference_ShouldBe2PiRadius_GivenUnitEllipse() + { + var unitEllipse = Ellipse.Unit; + Assert.AreEqual(2 * MathF.PI, unitEllipse.ApproximateCircumference, 1e-6f); + } + + [TestMethod] + public void Constructor_ShouldGiveCorrectEllipse() + { + var ellipse = new Ellipse(Point.Empty, new Size(2, 1)); + Assert.AreEqual(new Point(0, 0), ellipse.Center); + Assert.AreEqual(new Size(2, 1), ellipse.Radius); + + ellipse = new Ellipse(0, 0, 2, 1); + Assert.AreEqual(new Point(0, 0), ellipse.Center); + Assert.AreEqual(new Size(2, 1), ellipse.Radius); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitEllipses() + { + var unitEllipse1 = Ellipse.Unit; + var unitEllipse2 = Ellipse.Unit; + Assert.AreEqual(unitEllipse1, unitEllipse2); + Assert.IsTrue(unitEllipse1 == unitEllipse2); + Assert.IsFalse(unitEllipse1 != unitEllipse2); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentEllipses() + { + Assert.AreNotEqual(Ellipse.Unit, Ellipse.Empty); + Assert.IsFalse(Ellipse.Unit == Ellipse.Empty); + Assert.IsTrue(Ellipse.Unit != Ellipse.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(Ellipse.Unit.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyEllipse() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Ellipse.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Ellipse.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitEllipse() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Ellipse.Unit.GetHashCode(); + Assert.AreEqual(hashCode, Ellipse.Unit.GetHashCode()); + } + + [TestMethod] + public void HorizontalRadius_ShouldBe0_GivenEmptyEllipse() + { + Assert.AreEqual(0, Ellipse.Empty.HorizontalRadius); + } + + [TestMethod] + public void HorizontalRadius_ShouldBe1_GivenUnitEllipse() + { + Assert.AreEqual(1, Ellipse.Unit.HorizontalRadius); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentEllipse_GivenCircle() + { + Circle unitCircle = Circle.Unit; + Ellipse converted = unitCircle; + + Assert.AreEqual(unitCircle, converted); + Assert.AreEqual(unitCircle.Radius, converted.HorizontalRadius); + Assert.AreEqual(unitCircle.Radius, converted.VerticalRadius); + Assert.AreEqual(unitCircle.Center, converted.Center); + } + + [TestMethod] + public void VerticalRadius_ShouldBe0_GivenEmptyEllipse() + { + Assert.AreEqual(0, Ellipse.Empty.VerticalRadius); + } + + [TestMethod] + public void VerticalRadius_ShouldBe1_GivenUnitEllipse() + { + Assert.AreEqual(1, Ellipse.Unit.VerticalRadius); + } +} diff --git a/X10D.Tests/src/Drawing/Line3DTests.cs b/X10D.Tests/src/Drawing/Line3DTests.cs new file mode 100644 index 0000000..8cbc7cf --- /dev/null +++ b/X10D.Tests/src/Drawing/Line3DTests.cs @@ -0,0 +1,188 @@ +using System.Drawing; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class Line3DTests +{ + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyAndOne() + { + Assert.AreEqual(-1, Line3D.Empty.CompareTo(Line3D.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyLineAndOneLineAsObject() + { + Assert.AreEqual(-1, Line3D.Empty.CompareTo((object)Line3D.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, Line3D.One.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenOneAndEmpty() + { + Assert.AreEqual(1, Line3D.One.CompareTo(Line3D.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitLine() + { + var unitLine3D = Line3D.One; + Assert.AreEqual(0, unitLine3D.CompareTo(unitLine3D)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => Line3D.Empty.CompareTo(new object())); + } + + [TestMethod] + public void Length_ShouldBe0_GivenEmptyLine() + { + Assert.AreEqual(0.0f, Line3D.Empty.Length); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitXLine() + { + Assert.AreEqual(1.0f, Line3D.UnitX.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitYLine() + { + Assert.AreEqual(1.0f, Line3D.UnitY.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitZLine() + { + Assert.AreEqual(1.0f, Line3D.UnitZ.Length, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitLines() + { + Line3D first = Line3D.One; + Line3D second = Line3D.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentLines() + { + Assert.AreNotEqual(Line3D.One, Line3D.Empty); + Assert.IsFalse(Line3D.One == Line3D.Empty); + Assert.IsTrue(Line3D.One != Line3D.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(Line3D.One.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line3D.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Line3D.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line3D.One.GetHashCode(); + Assert.AreEqual(hashCode, Line3D.One.GetHashCode()); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentLine_GivenLine() + { + Line3D oneLine = new Line3D(Vector3.Zero, Vector3.UnitX + Vector3.UnitY); + Line converted = (Line)oneLine; + + var expectedStart = new Point((int)oneLine.Start.X, (int)oneLine.Start.Y); + var expectedEnd = new Point((int)oneLine.End.X, (int)oneLine.End.Y); + + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentLineF_GivenLine() + { + Line3D oneLine = new Line3D(Vector3.Zero, Vector3.UnitX + Vector3.UnitY); + LineF converted = (LineF)oneLine; + + var expectedStart = new PointF(oneLine.Start.X, oneLine.Start.Y); + var expectedEnd = new PointF(oneLine.End.X, oneLine.End.Y); + + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(Line3D.One > Line3D.Empty); + Assert.IsTrue(Line3D.One >= Line3D.Empty); + Assert.IsFalse(Line3D.One < Line3D.Empty); + Assert.IsFalse(Line3D.One <= Line3D.Empty); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentLine_GivenLine() + { + Line oneLine = Line.One; + Line3D converted = oneLine; + + var expectedStart = new Vector3(oneLine.Start.X, oneLine.Start.Y, 0.0f); + var expectedEnd = new Vector3(oneLine.End.X, oneLine.End.Y, 0.0f); + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentLine_GivenLineF() + { + LineF oneLine = LineF.One; + Line3D converted = oneLine; + + var expectedStart = new Vector3(oneLine.Start.X, oneLine.Start.Y, 0.0f); + var expectedEnd = new Vector3(oneLine.End.X, oneLine.End.Y, 0.0f); + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(Line3D.Empty < Line3D.One); + Assert.IsTrue(Line3D.Empty <= Line3D.One); + Assert.IsFalse(Line3D.Empty > Line3D.One); + Assert.IsFalse(Line3D.Empty >= Line3D.One); + } +} diff --git a/X10D.Tests/src/Drawing/LineFTests.cs b/X10D.Tests/src/Drawing/LineFTests.cs new file mode 100644 index 0000000..582bde3 --- /dev/null +++ b/X10D.Tests/src/Drawing/LineFTests.cs @@ -0,0 +1,146 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class LineFTests +{ + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyAndOne() + { + Assert.AreEqual(-1, LineF.Empty.CompareTo(LineF.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyLineAndOneLineAsObject() + { + Assert.AreEqual(-1, LineF.Empty.CompareTo((object)LineF.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, LineF.One.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenOneAndEmpty() + { + Assert.AreEqual(1, LineF.One.CompareTo(LineF.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitLine() + { + var unitLineF = LineF.One; + Assert.AreEqual(0, unitLineF.CompareTo(unitLineF)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => LineF.Empty.CompareTo(new object())); + } + + [TestMethod] + public void Length_ShouldBe0_GivenEmptyLine() + { + Assert.AreEqual(0.0f, LineF.Empty.Length); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitXLine() + { + Assert.AreEqual(1.0f, LineF.UnitX.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitYLine() + { + Assert.AreEqual(1.0f, LineF.UnitY.Length, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitLines() + { + LineF first = LineF.One; + LineF second = LineF.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentLines() + { + Assert.AreNotEqual(LineF.One, LineF.Empty); + Assert.IsFalse(LineF.One == LineF.Empty); + Assert.IsTrue(LineF.One != LineF.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(LineF.One.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = LineF.Empty.GetHashCode(); + Assert.AreEqual(hashCode, LineF.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = LineF.One.GetHashCode(); + Assert.AreEqual(hashCode, LineF.One.GetHashCode()); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentLine_GivenLine() + { + LineF oneLine = LineF.One; + Line converted = (Line)oneLine; + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(oneLine.Start, converted.Start); + Assert.AreEqual(oneLine.End, converted.End); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(LineF.One > LineF.Empty); + Assert.IsTrue(LineF.One >= LineF.Empty); + Assert.IsFalse(LineF.One < LineF.Empty); + Assert.IsFalse(LineF.One <= LineF.Empty); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentLine_GivenLine() + { + Line oneLine = Line.One; + LineF converted = oneLine; + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(oneLine.Start, converted.Start); + Assert.AreEqual(oneLine.End, converted.End); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(LineF.Empty < LineF.One); + Assert.IsTrue(LineF.Empty <= LineF.One); + Assert.IsFalse(LineF.Empty > LineF.One); + Assert.IsFalse(LineF.Empty >= LineF.One); + } +} diff --git a/X10D.Tests/src/Drawing/LineTests.cs b/X10D.Tests/src/Drawing/LineTests.cs new file mode 100644 index 0000000..2a690e8 --- /dev/null +++ b/X10D.Tests/src/Drawing/LineTests.cs @@ -0,0 +1,122 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class LineTests +{ + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyAndOne() + { + Assert.AreEqual(-1, Line.Empty.CompareTo(Line.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyLineAndOneLineAsObject() + { + Assert.AreEqual(-1, Line.Empty.CompareTo((object)Line.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, Line.One.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenOneAndEmpty() + { + Assert.AreEqual(1, Line.One.CompareTo(Line.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitLine() + { + var unitLine = Line.One; + Assert.AreEqual(0, unitLine.CompareTo(unitLine)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => Line.Empty.CompareTo(new object())); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitLines() + { + Line first = Line.One; + Line second = Line.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentLines() + { + Assert.AreNotEqual(Line.One, Line.Empty); + Assert.IsFalse(Line.One == Line.Empty); + Assert.IsTrue(Line.One != Line.Empty); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(Line.One.Equals(null)); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Line.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line.One.GetHashCode(); + Assert.AreEqual(hashCode, Line.One.GetHashCode()); + } + + [TestMethod] + public void Length_ShouldBe0_GivenEmptyLine() + { + Assert.AreEqual(0.0f, Line.Empty.Length); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitXLine() + { + Assert.AreEqual(1.0f, Line.UnitX.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitYLine() + { + Assert.AreEqual(1.0f, Line.UnitY.Length, 1e-6f); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(Line.One > Line.Empty); + Assert.IsTrue(Line.One >= Line.Empty); + Assert.IsFalse(Line.One < Line.Empty); + Assert.IsFalse(Line.One <= Line.Empty); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(Line.Empty < Line.One); + Assert.IsTrue(Line.Empty <= Line.One); + Assert.IsFalse(Line.Empty > Line.One); + Assert.IsFalse(Line.Empty >= Line.One); + } +} diff --git a/X10D.Tests/src/Drawing/PointFTests.cs b/X10D.Tests/src/Drawing/PointFTests.cs new file mode 100644 index 0000000..87b6f63 --- /dev/null +++ b/X10D.Tests/src/Drawing/PointFTests.cs @@ -0,0 +1,65 @@ +using System.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +#if !NET6_0_OR_GREATER +using X10D.Core; +#endif +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class PointFTests +{ + [TestMethod] + public void IsOnLine_ShouldReturnTrue_GivenPointOnLine() + { + var point = new PointF(1.0f, 0.0f); + var line = new LineF(PointF.Empty, new PointF(2.0f, 0.0f)); + + Assert.IsTrue(point.IsOnLine(line)); + Assert.IsTrue(point.IsOnLine(line.Start, line.End)); + Assert.IsTrue(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void IsOnLine_ShouldReturnFalse_GivenPointNotOnLine() + { + var point = new PointF(1.0f, 1.0f); + var line = new LineF(PointF.Empty, new PointF(2.0f, 0.0f)); + + Assert.IsFalse(point.IsOnLine(line)); + Assert.IsFalse(point.IsOnLine(line.Start, line.End)); + Assert.IsFalse(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var point = new PointF(1.5f, 2.6f); + var rounded = point.Round(); + + Assert.AreEqual(2, rounded.X); + Assert.AreEqual(3, rounded.Y); + } + + [TestMethod] + public void Round_ShouldRoundToNearest10_GivenPrecision10() + { + var point = new PointF(1.5f, 25.2f); + var rounded = point.Round(10); + + Assert.AreEqual(0, rounded.X); + Assert.AreEqual(30, rounded.Y); + } + + [TestMethod] + public void ToSizeF_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + var point = new PointF(random.NextSingle(), random.NextSingle()); + var size = point.ToSizeF(); + + Assert.AreEqual(point.X, size.Width, 1e-6f); + Assert.AreEqual(point.Y, size.Height, 1e-6f); + } +} diff --git a/X10D.Tests/src/Drawing/PointTests.cs b/X10D.Tests/src/Drawing/PointTests.cs new file mode 100644 index 0000000..19e5890 --- /dev/null +++ b/X10D.Tests/src/Drawing/PointTests.cs @@ -0,0 +1,64 @@ +using System.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class PointTests +{ + [TestMethod] + public void IsOnLine_ShouldReturnTrue_GivenPointOnLine() + { + var point = new Point(1, 0); + var line = new Line(Point.Empty, new Point(2, 0)); + + Assert.IsTrue(point.IsOnLine(line)); + Assert.IsTrue(point.IsOnLine(line.Start, line.End)); + Assert.IsTrue(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void IsOnLine_ShouldReturnFalse_GivenPointNotOnLine() + { + var point = new Point(1, 1); + var line = new Line(Point.Empty, new Point(2, 0)); + + Assert.IsFalse(point.IsOnLine(line)); + Assert.IsFalse(point.IsOnLine(line.Start, line.End)); + Assert.IsFalse(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void ToSize_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + var point = new Point(random.Next(), random.Next()); + var size = point.ToSize(); + + Assert.AreEqual(point.X, size.Width); + Assert.AreEqual(point.Y, size.Height); + } + + [TestMethod] + public void ToSizeF_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + var point = new Point(random.Next(), random.Next()); + var size = point.ToSizeF(); + + Assert.AreEqual(point.X, size.Width, 1e-6f); + Assert.AreEqual(point.Y, size.Height, 1e-6f); + } + + [TestMethod] + public void ToVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var point = new Point(random.Next(), random.Next()); + var size = point.ToVector2(); + + Assert.AreEqual(point.X, size.X, 1e-6f); + Assert.AreEqual(point.Y, size.Y, 1e-6f); + } +} diff --git a/X10D.Tests/src/Drawing/PolygonFTests.cs b/X10D.Tests/src/Drawing/PolygonFTests.cs new file mode 100644 index 0000000..6f7cb99 --- /dev/null +++ b/X10D.Tests/src/Drawing/PolygonFTests.cs @@ -0,0 +1,247 @@ +using System.Drawing; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class PolygonFTests +{ + [TestMethod] + public void AddVertices_ShouldAddVertices() + { + var polygon = PolygonF.Empty; + polygon.AddVertices(new[] {new PointF(1, 2), new PointF(3, 4)}); + Assert.AreEqual(2, polygon.VertexCount); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, PolygonF.Empty.VertexCount); + } + + [TestMethod] + public void AddVertices_ShouldThrowArgumentNullException_GivenNullEnumerableOfPointF() + { + var polygon = PolygonF.Empty; + IEnumerable vertices = null!; + Assert.ThrowsException(() => polygon.AddVertices(vertices)); + } + + [TestMethod] + public void AddVertices_ShouldThrowArgumentNullException_GivenNullEnumerableOfVector2() + { + var polygon = PolygonF.Empty; + IEnumerable vertices = null!; + Assert.ThrowsException(() => polygon.AddVertices(vertices)); + } + + [TestMethod] + public void ClearVertices_ShouldClearVertices() + { + var polygon = PolygonF.Empty; + polygon.AddVertices(new[] {new Vector2(1, 2), new Vector2(3, 4)}); + Assert.AreEqual(2, polygon.VertexCount); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, PolygonF.Empty.VertexCount); + + polygon.ClearVertices(); + Assert.AreEqual(0, polygon.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldPopulateVertices_GivenPolygon() + { + var pointPolygon = new PolygonF(new[] {new PointF(1, 2), new PointF(3, 4)}); + var vectorPolygon = new PolygonF(new[] {new Vector2(1, 2), new Vector2(3, 4)}); + + Assert.AreEqual(2, pointPolygon.VertexCount); + Assert.AreEqual(2, vectorPolygon.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldThrowArgumentNullException_GivenNullEnumerableOfPointF() + { + IEnumerable vertices = null!; + Assert.ThrowsException(() => new PolygonF(vertices)); + } + + [TestMethod] + public void Constructor_ShouldThrowArgumentNullException_GivenNullEnumerableOfVector2() + { + IEnumerable vertices = null!; + Assert.ThrowsException(() => new PolygonF(vertices)); + } + + [TestMethod] + public void CopyConstructor_ShouldCopyVertices_GivenPolygon() + { + var first = PolygonF.Empty; + first.AddVertices(new[] {new PointF(1, 2), new PointF(3, 4)}); + + var second = new PolygonF(first); + Assert.AreEqual(2, first.VertexCount); + Assert.AreEqual(2, second.VertexCount); + + // we cannot use CollectionAssert here for reasons I am not entirely sure of. + // it seems to dislike casting from IReadOnlyList to ICollection. but okay. + Assert.IsTrue(first.Vertices.SequenceEqual(second.Vertices)); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, PolygonF.Empty.VertexCount); + } + + [TestMethod] + public void CopyConstructor_ShouldThrowArgumentNullException_GivenNullPolygonF() + { + PolygonF polygon = null!; + Assert.ThrowsException(() => new PolygonF(polygon)); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoEmptyPolygons() + { + var first = PolygonF.Empty; + var second = PolygonF.Empty; + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoHexagons() + { + PolygonF first = CreateHexagon(); + PolygonF second = CreateHexagon(); + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenHexagonAndEmptyPolygon() + { + PolygonF first = CreateHexagon(); + PolygonF second = PolygonF.Empty; + + Assert.AreNotEqual(first, second); + Assert.AreNotEqual(second, first); + Assert.IsFalse(first.Equals(second)); + Assert.IsFalse(second.Equals(first)); + Assert.IsFalse(first == second); + Assert.IsFalse(second == first); + Assert.IsTrue(first != second); + Assert.IsTrue(second != first); + } + + [TestMethod] + public void FromPolygon_ShouldThrowArgumentNullException_GivenNullPolygonF() + { + Assert.ThrowsException(() => PolygonF.FromPolygon(null!)); + } + + [TestMethod] + public void IsConvex_ShouldBeFalse_GivenEmptyPolygon() + { + Assert.IsFalse(PolygonF.Empty.IsConvex); + } + + [TestMethod] + public void IsConvex_ShouldBeTrue_GivenHexagon() + { + Assert.IsTrue(CreateHexagon().IsConvex); + } + + [TestMethod] + public void IsConvex_ShouldBeFalse_GivenConcavePolygon() + { + Assert.IsFalse(CreateConcavePolygon().IsConvex); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentCircle_GivenCircle() + { + PolygonF polygon = CreateHexagon(); + Polygon converted = (Polygon)polygon; + + Assert.AreEqual(polygon, converted); + Assert.AreEqual(polygon.IsConvex, converted.IsConvex); + Assert.AreEqual(polygon.VertexCount, converted.VertexCount); + + Assert.IsTrue(polygon.Vertices.SequenceEqual(converted.Vertices.Select(p => (PointF)p))); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentCircle_GivenCircle() + { + Polygon polygon = PolygonTests.CreateHexagon(); + PolygonF converted = polygon; + + Assert.AreEqual(polygon, converted); + Assert.AreEqual(polygon.IsConvex, converted.IsConvex); + Assert.AreEqual(polygon.VertexCount, converted.VertexCount); + + Assert.IsTrue(converted.Vertices.SequenceEqual(polygon.Vertices.Select(p => (PointF)p))); + } + + [TestMethod] + public void PointCount_ShouldBe1_GivenPolygonFWith1Point() + { + var polygon = new PolygonF(); + polygon.AddVertex(new Point(1, 1)); + + Assert.AreEqual(1, polygon.VertexCount); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, PolygonF.Empty.VertexCount); + } + + [TestMethod] + public void PointCount_ShouldBe0_GivenEmptyPolygon() + { + Assert.AreEqual(0, PolygonF.Empty.VertexCount); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = PolygonF.Empty.GetHashCode(); + Assert.AreEqual(hashCode, PolygonF.Empty.GetHashCode()); + } + + internal static PolygonF CreateHexagon() + { + var hexagon = new PolygonF(); + hexagon.AddVertex(new Vector2(0, 0)); + hexagon.AddVertex(new Vector2(1, 0)); + hexagon.AddVertex(new Vector2(1, 1)); + hexagon.AddVertex(new Vector2(0, 1)); + hexagon.AddVertex(new Vector2(-1, 1)); + hexagon.AddVertex(new Vector2(-1, 0)); + return hexagon; + } + + internal static PolygonF CreateConcavePolygon() + { + var hexagon = new PolygonF(); + hexagon.AddVertex(new Vector2(0, 0)); + hexagon.AddVertex(new Vector2(2, 0)); + hexagon.AddVertex(new Vector2(1, 1)); + hexagon.AddVertex(new Vector2(2, 1)); + hexagon.AddVertex(new Vector2(0, 1)); + return hexagon; + } +} diff --git a/X10D.Tests/src/Drawing/PolygonTests.cs b/X10D.Tests/src/Drawing/PolygonTests.cs new file mode 100644 index 0000000..f8d55ae --- /dev/null +++ b/X10D.Tests/src/Drawing/PolygonTests.cs @@ -0,0 +1,228 @@ +using System.Drawing; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class PolygonTests +{ + [TestMethod] + public void AddVertices_ShouldAddVertices() + { + var polygon = Polygon.Empty; + polygon.AddVertices(new[] {new Point(1, 2), new Point(3, 4)}); + Assert.AreEqual(2, polygon.VertexCount); + + + // assert that the empty polygon was not modified + Assert.AreEqual(0, Polygon.Empty.VertexCount); + } + + [TestMethod] + public void AddVertices_ShouldThrowArgumentNullException_GivenNullEnumerable() + { + var polygon = Polygon.Empty; + IEnumerable vertices = null!; + Assert.ThrowsException(() => polygon.AddVertices(vertices)); + } + + [TestMethod] + public void ClearVertices_ShouldClearVertices() + { + var polygon = Polygon.Empty; + polygon.AddVertices(new[] {new Point(1, 2), new Point(3, 4)}); + Assert.AreEqual(2, polygon.VertexCount); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, Polygon.Empty.VertexCount); + + polygon.ClearVertices(); + Assert.AreEqual(0, polygon.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldPopulateVertices_GivenPolygon() + { + var pointPolygon = new Polygon(new[] {new Point(1, 2), new Point(3, 4)}); + + Assert.AreEqual(2, pointPolygon.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldThrowArgumentNullException_GivenNullEnumerableOfPoint() + { + IEnumerable vertices = null!; + Assert.ThrowsException(() => new Polygon(vertices)); + } + + [TestMethod] + public void CopyConstructor_ShouldCopyVertices_GivenPolygon() + { + var first = Polygon.Empty; + first.AddVertices(new[] {new Point(1, 2), new Point(3, 4)}); + + var second = new Polygon(first); + Assert.AreEqual(2, first.VertexCount); + Assert.AreEqual(2, second.VertexCount); + + // we cannot use CollectionAssert here for reasons I am not entirely sure of. + // it seems to dislike casting from IReadOnlyList to ICollection. but okay. + Assert.IsTrue(first.Vertices.SequenceEqual(second.Vertices)); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, Polygon.Empty.VertexCount); + } + + [TestMethod] + public void CopyConstructor_ShouldThrowArgumentNullException_GivenNullPolygon() + { + Polygon polygon = null!; + Assert.ThrowsException(() => new Polygon(polygon)); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoEmptyPolygons() + { + var first = Polygon.Empty; + var second = Polygon.Empty; + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoHexagons() + { + Polygon first = CreateHexagon(); + Polygon second = CreateHexagon(); + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenHexagonAndEmptyPolygon() + { + Polygon first = CreateHexagon(); + Polygon second = Polygon.Empty; + + Assert.AreNotEqual(first, second); + Assert.AreNotEqual(second, first); + Assert.IsFalse(first.Equals(second)); + Assert.IsFalse(second.Equals(first)); + Assert.IsFalse(first == second); + Assert.IsFalse(second == first); + Assert.IsTrue(first != second); + Assert.IsTrue(second != first); + } + + [TestMethod] + public void FromPolygonF_ShouldReturnEquivalentPolygon_GivenPolygon() + { + PolygonF hexagon = CreateHexagonF(); + + Polygon expected = CreateHexagon(); + Polygon actual = Polygon.FromPolygonF(hexagon); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void FromPolygonF_ShouldThrowArgumentNullException_GivenNullPolygon() + { + Assert.ThrowsException(() => Polygon.FromPolygonF(null!)); + } + + [TestMethod] + public void IsConvex_ShouldBeFalse_GivenEmptyPolygon() + { + Assert.IsFalse(Polygon.Empty.IsConvex); + } + + [TestMethod] + public void IsConvex_ShouldBeTrue_GivenHexagon() + { + Assert.IsTrue(CreateHexagon().IsConvex); + } + + [TestMethod] + public void IsConvex_ShouldBeFalse_GivenConcavePolygon() + { + Assert.IsFalse(CreateConcavePolygon().IsConvex); + } + + [TestMethod] + public void PointCount_ShouldBe1_GivenPolygonWith1Point() + { + var polygon = Polygon.Empty; + polygon.AddVertex(new Point(1, 1)); + + Assert.AreEqual(1, polygon.VertexCount); + + // assert that the empty polygon was not modified + Assert.AreEqual(0, Polygon.Empty.VertexCount); + } + + [TestMethod] + public void PointCount_ShouldBe0_GivenEmptyPolygon() + { + Assert.AreEqual(0, Polygon.Empty.VertexCount); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Polygon.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Polygon.Empty.GetHashCode()); + } + + internal static Polygon CreateHexagon() + { + var hexagon = new Polygon(); + hexagon.AddVertex(new Point(0, 0)); + hexagon.AddVertex(new Point(1, 0)); + hexagon.AddVertex(new Point(1, 1)); + hexagon.AddVertex(new Point(0, 1)); + hexagon.AddVertex(new Point(-1, 1)); + hexagon.AddVertex(new Point(-1, 0)); + return hexagon; + } + + internal static PolygonF CreateHexagonF() + { + var hexagon = new PolygonF(); + hexagon.AddVertex(new PointF(0, 0)); + hexagon.AddVertex(new PointF(1, 0)); + hexagon.AddVertex(new PointF(1, 1)); + hexagon.AddVertex(new PointF(0, 1)); + hexagon.AddVertex(new PointF(-1, 1)); + hexagon.AddVertex(new PointF(-1, 0)); + return hexagon; + } + + internal static Polygon CreateConcavePolygon() + { + var hexagon = new Polygon(); + hexagon.AddVertex(new Point(0, 0)); + hexagon.AddVertex(new Point(2, 0)); + hexagon.AddVertex(new Point(1, 1)); + hexagon.AddVertex(new Point(2, 1)); + hexagon.AddVertex(new Point(0, 1)); + return hexagon; + } +} diff --git a/X10D.Tests/src/Drawing/PolyhedronTests.cs b/X10D.Tests/src/Drawing/PolyhedronTests.cs new file mode 100644 index 0000000..f45ea1b --- /dev/null +++ b/X10D.Tests/src/Drawing/PolyhedronTests.cs @@ -0,0 +1,215 @@ +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class PolyhedronTests +{ + [TestMethod] + public void AddVertices_ShouldAddVertices() + { + var polyhedron = Polyhedron.Empty; + polyhedron.AddVertices(new[] {new Vector3(1, 2, 3), new Vector3(4, 5, 6)}); + Assert.AreEqual(2, polyhedron.VertexCount); + + // assert that the empty polyhedron was not modified + Assert.AreEqual(0, Polyhedron.Empty.VertexCount); + } + + [TestMethod] + public void AddVertices_ShouldThrowArgumentNullException_GivenNullEnumerableOfVector3() + { + var polygon = Polyhedron.Empty; + IEnumerable vertices = null!; + Assert.ThrowsException(() => polygon.AddVertices(vertices)); + } + + [TestMethod] + public void ClearVertices_ShouldClearVertices() + { + var polyhedron = Polyhedron.Empty; + polyhedron.AddVertices(new[] {new Vector3(1, 2, 3), new Vector3(4, 5, 6)}); + Assert.AreEqual(2, polyhedron.VertexCount); + + // assert that the empty polyhedron was not modified + Assert.AreEqual(0, Polyhedron.Empty.VertexCount); + + polyhedron.ClearVertices(); + Assert.AreEqual(0, polyhedron.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldPopulateVertices_GivenPolyhedron() + { + var polyhedron = new Polyhedron(new[] {new Vector3(1, 2, 3), new Vector3(4, 5, 6)}); + Assert.AreEqual(2, polyhedron.VertexCount); + } + + [TestMethod] + public void Constructor_ShouldThrowArgumentNullException_GivenNullEnumerableOfVector3() + { + IEnumerable vertices = null!; + Assert.ThrowsException(() => new Polyhedron(vertices)); + } + + [TestMethod] + public void CopyConstructor_ShouldCopyVertices_GivenPolyhedron() + { + var first = Polyhedron.Empty; + first.AddVertices(new[] {new Vector3(1, 2, 3), new Vector3(4, 5, 6)}); + + var second = new Polyhedron(first); + Assert.AreEqual(2, first.VertexCount); + Assert.AreEqual(2, second.VertexCount); + + // we cannot use CollectionAssert here for reasons I am not entirely sure of. + // it seems to dislike casting from IReadOnlyList to ICollection. but okay. + Assert.IsTrue(first.Vertices.SequenceEqual(second.Vertices)); + + // assert that the empty polyhedron was not modified + Assert.AreEqual(0, Polyhedron.Empty.VertexCount); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoEmptyPolyhedrons() + { + var first = Polyhedron.Empty; + var second = Polyhedron.Empty; + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoHexagons() + { + Polyhedron first = CreateHexagon(); + Polyhedron second = CreateHexagon(); + + Assert.AreEqual(first, second); + Assert.AreEqual(second, first); + Assert.IsTrue(first.Equals(second)); + Assert.IsTrue(second.Equals(first)); + Assert.IsTrue(first == second); + Assert.IsTrue(second == first); + Assert.IsFalse(first != second); + Assert.IsFalse(second != first); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenHexagonAndEmptyPolyhedron() + { + Polyhedron first = CreateHexagon(); + Polyhedron second = Polyhedron.Empty; + + Assert.AreNotEqual(first, second); + Assert.AreNotEqual(second, first); + Assert.IsFalse(first.Equals(second)); + Assert.IsFalse(second.Equals(first)); + Assert.IsFalse(first == second); + Assert.IsFalse(second == first); + Assert.IsTrue(first != second); + Assert.IsTrue(second != first); + } + + [TestMethod] + public void FromPolygon_ShouldThrowArgumentNullException_GivenNullPolygonF() + { + Assert.ThrowsException(() => Polyhedron.FromPolygon(null!)); + } + + [TestMethod] + public void FromPolygonF_ShouldThrowArgumentNullException_GivenNullPolygonF() + { + Assert.ThrowsException(() => Polyhedron.FromPolygonF(null!)); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentPolyhedron_GivenPolyhedron() + { + Polygon polygon = PolygonTests.CreateHexagon(); + Polyhedron converted = polygon; + + Assert.AreEqual(polygon, converted); + Assert.AreEqual(polygon.VertexCount, converted.VertexCount); + + Assert.IsTrue(converted.Vertices.SequenceEqual(polygon.Vertices.Select(p => + { + var point = p.ToVector2(); + return new Vector3(point.X, point.Y, 0); + }))); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentPolyhedron_GivenPolyhedronF() + { + PolygonF polygon = PolygonFTests.CreateHexagon(); + Polyhedron converted = polygon; + + Assert.AreEqual(polygon, converted); + Assert.AreEqual(polygon.VertexCount, converted.VertexCount); + + Assert.IsTrue(converted.Vertices.SequenceEqual(polygon.Vertices.Select(v => + { + var point = v.ToVector2(); + return new Vector3(point.X, point.Y, 0); + }))); + } + + [TestMethod] + public void PointCount_ShouldBe1_GivenPolyhedronWith1Point() + { + var polyhedron = new Polyhedron(); + polyhedron.AddVertex(Vector3.One); + + Assert.AreEqual(1, polyhedron.VertexCount); + + // assert that the empty polyhedron was not modified + Assert.AreEqual(0, Polyhedron.Empty.VertexCount); + } + + [TestMethod] + public void PointCount_ShouldBe0_GivenEmptyPolyhedron() + { + Assert.AreEqual(0, Polyhedron.Empty.VertexCount); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Polyhedron.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Polyhedron.Empty.GetHashCode()); + } + + internal static Polyhedron CreateHexagon() + { + var hexagon = new Polyhedron(); + hexagon.AddVertex(new Vector3(0, 0, 0)); + hexagon.AddVertex(new Vector3(1, 0, 0)); + hexagon.AddVertex(new Vector3(1, 1, 0)); + hexagon.AddVertex(new Vector3(0, 1, 0)); + hexagon.AddVertex(new Vector3(-1, 1, 0)); + hexagon.AddVertex(new Vector3(-1, 0, 0)); + return hexagon; + } + + internal static Polyhedron CreateConcavePolyhedron() + { + var hexagon = new Polyhedron(); + hexagon.AddVertex(new Vector3(0, 0, 0)); + hexagon.AddVertex(new Vector3(2, 0, 0)); + hexagon.AddVertex(new Vector3(2, 1, 0)); + hexagon.AddVertex(new Vector3(2, 1, 0)); + hexagon.AddVertex(new Vector3(0, 1, 0)); + return hexagon; + } +} diff --git a/X10D.Tests/src/Drawing/RandomTests.cs b/X10D.Tests/src/Drawing/RandomTests.cs index 07d3360..5dfce5f 100644 --- a/X10D.Tests/src/Drawing/RandomTests.cs +++ b/X10D.Tests/src/Drawing/RandomTests.cs @@ -13,21 +13,21 @@ public class RandomTests var random = new Random(1234); Assert.AreEqual(Color.FromArgb(51, 21, 21, 229), random.NextColorArgb()); } - + [TestMethod] public void NextColorArgb_ShouldThrow_GivenNull() { Random? random = null; Assert.ThrowsException(() => random!.NextColorArgb()); } - + [TestMethod] public void NextColorRgb_ShouldReturn1515e5_GivenSeed1234() { var random = new Random(1234); Assert.AreEqual(Color.FromArgb(255, 21, 21, 229), random.NextColorRgb()); } - + [TestMethod] public void NextColorRgb_ShouldThrow_GivenNull() { diff --git a/X10D.Tests/src/Drawing/SizeTests.cs b/X10D.Tests/src/Drawing/SizeTests.cs new file mode 100644 index 0000000..8dbd957 --- /dev/null +++ b/X10D.Tests/src/Drawing/SizeTests.cs @@ -0,0 +1,42 @@ +using System.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class SizeTests +{ + [TestMethod] + public void ToPoint_ShouldReturnPoint_WithEquivalentMembers() + { + var random = new Random(); + var size = new Size(random.Next(), random.Next()); + var point = size.ToPoint(); + + Assert.AreEqual(size.Width, point.X); + Assert.AreEqual(size.Height, point.Y); + } + + [TestMethod] + public void ToPointF_ShouldReturnPoint_WithEquivalentMembers() + { + var random = new Random(); + var size = new Size(random.Next(), random.Next()); + var point = size.ToPointF(); + + Assert.AreEqual(size.Width, point.X, 1e-6f); + Assert.AreEqual(size.Height, point.Y, 1e-6f); + } + + [TestMethod] + public void ToVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var point = new Size(random.Next(), random.Next()); + var size = point.ToVector2(); + + Assert.AreEqual(point.Width, size.X, 1e-6f); + Assert.AreEqual(point.Height, size.Y, 1e-6f); + } +} diff --git a/X10D.Tests/src/Drawing/SphereTests.cs b/X10D.Tests/src/Drawing/SphereTests.cs new file mode 100644 index 0000000..3457b35 --- /dev/null +++ b/X10D.Tests/src/Drawing/SphereTests.cs @@ -0,0 +1,135 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class SphereTests +{ + [TestMethod] + public void Circumference_ShouldBe2PiRadius_GivenUnitCircle() + { + var unitSphere = Sphere.Unit; + Assert.AreEqual(2.0f * MathF.PI * unitSphere.Radius, unitSphere.Circumference, 1e-6f); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(-1, Sphere.Empty.CompareTo(Sphere.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenUnitCircleAndEmpty() + { + Assert.AreEqual(1, Sphere.Unit.CompareTo(Sphere.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyCircleAndUnitCircleAsObject() + { + Assert.AreEqual(-1, Sphere.Empty.CompareTo((object)Sphere.Unit)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, Sphere.Unit.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitCircle() + { + var unitCircle = Sphere.Unit; + Assert.AreEqual(0, unitCircle.CompareTo(unitCircle)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => Sphere.Unit.CompareTo(new object())); + } + + [TestMethod] + public void Diameter_ShouldBe2_GivenUnitSphere() + { + Assert.AreEqual(2.0f, Sphere.Unit.Diameter, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitCircles() + { + var unitCircle1 = Sphere.Unit; + var unitCircle2 = Sphere.Unit; + Assert.AreEqual(unitCircle1, unitCircle2); + Assert.IsTrue(unitCircle1 == unitCircle2); + Assert.IsFalse(unitCircle1 != unitCircle2); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentObjects() + { + Assert.IsFalse(Sphere.Unit.Equals(null)); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentCircles() + { + Assert.AreNotEqual(Sphere.Unit, Sphere.Empty); + Assert.IsFalse(Sphere.Unit == Sphere.Empty); + Assert.IsTrue(Sphere.Unit != Sphere.Empty); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Sphere.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Sphere.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitCircle() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Sphere.Unit.GetHashCode(); + Assert.AreEqual(hashCode, Sphere.Unit.GetHashCode()); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(Sphere.Unit > Sphere.Empty); + Assert.IsTrue(Sphere.Unit >= Sphere.Empty); + Assert.IsFalse(Sphere.Unit < Sphere.Empty); + Assert.IsFalse(Sphere.Unit <= Sphere.Empty); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(Sphere.Empty < Sphere.Unit); + Assert.IsTrue(Sphere.Empty <= Sphere.Unit); + Assert.IsFalse(Sphere.Empty > Sphere.Unit); + Assert.IsFalse(Sphere.Empty >= Sphere.Unit); + } + + [TestMethod] + public void Radius_ShouldBe0_GivenEmptySphere() + { + Assert.AreEqual(0, Sphere.Empty.Radius); + } + + [TestMethod] + public void Radius_ShouldBe1_GivenUnitSphere() + { + Assert.AreEqual(1, Sphere.Unit.Radius); + } + + [TestMethod] + public void Volume_ShouldBe4Over3TimesPi_GivenUnitCircle() + { + var unitSphere = Sphere.Unit; + Assert.AreEqual(4.0f / 3.0f * MathF.PI, unitSphere.Volume); + } +} diff --git a/X10D.Tests/src/Hosting/ServiceCollectionTests.cs b/X10D.Tests/src/Hosting/ServiceCollectionTests.cs new file mode 100644 index 0000000..a4c673c --- /dev/null +++ b/X10D.Tests/src/Hosting/ServiceCollectionTests.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Hosting.DependencyInjection; + +namespace X10D.Tests.Hosting; + +[TestClass] +public class ServiceCollectionTests +{ + [TestMethod] + public void AddHostedSingleton_ShouldRegisterServiceAsSingletonAndAsHostedService() + { + var services = new ServiceCollection(); + + services.AddHostedSingleton(); + + var serviceProvider = services.BuildServiceProvider(); + var service = serviceProvider.GetService(); + var hostedService = serviceProvider.GetService(); + + Assert.IsNotNull(service); + Assert.IsNotNull(hostedService); + Assert.IsInstanceOfType(service, typeof(TestService)); + Assert.IsInstanceOfType(hostedService, typeof(TestService)); + Assert.AreSame(service, hostedService); + } + + [TestMethod] + public void AddHostedSingleton_ShouldRegisterServiceTypeAsSingletonAndAsHostedService() + { + var services = new ServiceCollection(); + + services.AddHostedSingleton(typeof(TestService)); + + var serviceProvider = services.BuildServiceProvider(); + var service = serviceProvider.GetService(); + var hostedService = serviceProvider.GetService(); + + Assert.IsNotNull(service); + Assert.IsNotNull(hostedService); + Assert.IsInstanceOfType(service, typeof(TestService)); + Assert.IsInstanceOfType(hostedService, typeof(TestService)); + Assert.AreSame(service, hostedService); + } + + private sealed class TestService : IHostedService + { + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/X10D.Tests/src/IO/DirectoryInfoTests.cs b/X10D.Tests/src/IO/DirectoryInfoTests.cs new file mode 100644 index 0000000..aa3e46e --- /dev/null +++ b/X10D.Tests/src/IO/DirectoryInfoTests.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +[TestClass] +public class DirectoryInfoTests +{ + [TestMethod] + public void Clear_ShouldClear_GivenValidDirectory() + { + string tempDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempDirectory = new DirectoryInfo(tempDirectoryPath); + + tempDirectory.Create(); + Assert.IsTrue(tempDirectory.Exists); + + var file = new FileInfo(Path.Combine(tempDirectory.FullName, "file")); + file.Create().Close(); + + var childDirectory = new DirectoryInfo(Path.Combine(tempDirectory.FullName, "child")); + childDirectory.Create(); + + var childFile = new FileInfo(Path.Combine(childDirectory.FullName, "childFile")); + childFile.Create().Close(); + + Assert.AreEqual(1, tempDirectory.GetFiles().Length); + Assert.AreEqual(1, tempDirectory.GetDirectories().Length); + tempDirectory.Clear(); + Assert.AreEqual(0, tempDirectory.GetFiles().Length); + Assert.AreEqual(0, tempDirectory.GetDirectories().Length); + Assert.IsTrue(tempDirectory.Exists); + + tempDirectory.Delete(); + } + + [TestMethod] + public void Clear_ShouldThrowArgumentNullException_GivenNull() + { + Assert.ThrowsException(() => ((DirectoryInfo?)null)!.Clear()); + } + + [TestMethod] + public void Clear_ShouldThrowDirectoryNotFoundException_GivenInvalidDirectory() + { + var directory = new DirectoryInfo(@"123:/@12#3"); + Assert.ThrowsException(() => directory.Clear()); + } + + [TestMethod] + public void Clear_ShouldThrowDirectoryNotFoundException_GivenNonExistentDirectory() + { + var directory = new DirectoryInfo(@"/@12#3"); + Assert.IsFalse(directory.Exists); + Assert.ThrowsException(() => directory.Clear()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadDecimal.cs b/X10D.Tests/src/IO/StreamTests.ReadDecimal.cs new file mode 100644 index 0000000..0c65320 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadDecimal.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadDecimal_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadDecimal()); + Assert.ThrowsException(() => stream.ReadDecimal(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadDecimal(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadDecimal_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadDecimal()); + Assert.ThrowsException(() => stream.ReadDecimal(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadDecimal(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadDecimal_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadDecimal((Endianness)(-1))); + } + + [TestMethod] + public void ReadDecimal_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] + { + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x68 + }; + stream.Write(bytes); + stream.Position = 0; + + const decimal expected = 420.0m; + decimal actual = stream.ReadDecimal(Endianness.BigEndian); + + Assert.AreEqual(16, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadDecimal_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] + { + 0x68, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 + }; + stream.Write(bytes); + stream.Position = 0; + + const decimal expected = 420.0m; + decimal actual = stream.ReadDecimal(Endianness.LittleEndian); + + Assert.AreEqual(16, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadDouble.cs b/X10D.Tests/src/IO/StreamTests.ReadDouble.cs new file mode 100644 index 0000000..352df60 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadDouble.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadDouble_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadDouble()); + Assert.ThrowsException(() => stream.ReadDouble(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadDouble(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadDouble_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadDouble()); + Assert.ThrowsException(() => stream.ReadDouble(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadDouble(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadDouble_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadDouble((Endianness)(-1))); + } + + [TestMethod] + public void ReadDouble_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x40, 0x7A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const double expected = 420.0; + double actual = stream.ReadDouble(Endianness.BigEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadDouble_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x7A, 0x40}; + stream.Write(bytes); + stream.Position = 0; + + const double expected = 420.0; + double actual = stream.ReadDouble(Endianness.LittleEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadInt16.cs b/X10D.Tests/src/IO/StreamTests.ReadInt16.cs new file mode 100644 index 0000000..b41c82a --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadInt16.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadInt16_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadInt16()); + Assert.ThrowsException(() => stream.ReadInt16(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt16(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt16_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadInt16()); + Assert.ThrowsException(() => stream.ReadInt16(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt16(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt16_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadInt16((Endianness)(-1))); + } + + [TestMethod] + public void ReadInt16_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const short expected = 420; + short actual = stream.ReadInt16(Endianness.BigEndian); + + Assert.AreEqual(2, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadInt16_ShouldReadLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01}; + stream.Write(bytes); + stream.Position = 0; + + const short expected = 420; + short actual = stream.ReadInt16(Endianness.LittleEndian); + + Assert.AreEqual(2, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadInt32.cs b/X10D.Tests/src/IO/StreamTests.ReadInt32.cs new file mode 100644 index 0000000..60442fe --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadInt32.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadInt32_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadInt32()); + Assert.ThrowsException(() => stream.ReadInt32(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt32(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt32_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadInt32()); + Assert.ThrowsException(() => stream.ReadInt32(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt32(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt32_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadInt32((Endianness)(-1))); + } + + [TestMethod] + public void ReadInt32_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const int expected = 420; + int actual = stream.ReadInt32(Endianness.BigEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadInt32_ShouldReadLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const int expected = 420; + int actual = stream.ReadInt32(Endianness.LittleEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadInt64.cs b/X10D.Tests/src/IO/StreamTests.ReadInt64.cs new file mode 100644 index 0000000..23554b1 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadInt64.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadInt64_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadInt64()); + Assert.ThrowsException(() => stream.ReadInt64(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt64(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt64_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadInt64()); + Assert.ThrowsException(() => stream.ReadInt64(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadInt64(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadInt64_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadInt64((Endianness)(-1))); + } + + [TestMethod] + public void ReadInt64_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const long expected = 420; + long actual = stream.ReadInt64(Endianness.BigEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadInt64_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const long expected = 420; + long actual = stream.ReadInt64(Endianness.LittleEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadSingle.cs b/X10D.Tests/src/IO/StreamTests.ReadSingle.cs new file mode 100644 index 0000000..35976c5 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadSingle.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void ReadSingle_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadSingle()); + Assert.ThrowsException(() => stream.ReadSingle(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadSingle(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadSingle_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadSingle()); + Assert.ThrowsException(() => stream.ReadSingle(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadSingle(Endianness.BigEndian)); + } + + [TestMethod] + public void ReadSingle_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadSingle((Endianness)(-1))); + } + + [TestMethod] + public void ReadSingle_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x43, 0xD2, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const float expected = 420.0f; + float actual = stream.ReadSingle(Endianness.BigEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ReadSingle_ShouldReadLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0xD2, 0x43}; + stream.Write(bytes); + stream.Position = 0; + + const float expected = 420.0f; + float actual = stream.ReadSingle(Endianness.LittleEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadUInt16.cs b/X10D.Tests/src/IO/StreamTests.ReadUInt16.cs new file mode 100644 index 0000000..b28c3e7 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadUInt16.cs @@ -0,0 +1,72 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt16_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadUInt16()); + Assert.ThrowsException(() => stream.ReadUInt16(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt16(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt16_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadUInt16()); + Assert.ThrowsException(() => stream.ReadUInt16(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt16(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt16_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadUInt16((Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt16_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const ushort expected = 420; + ushort actual = stream.ReadUInt16(Endianness.BigEndian); + + Assert.AreEqual(2, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt16_ShouldReadLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01}; + stream.Write(bytes); + stream.Position = 0; + + const ushort expected = 420; + ushort actual = stream.ReadUInt16(Endianness.LittleEndian); + + Assert.AreEqual(2, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadUInt32.cs b/X10D.Tests/src/IO/StreamTests.ReadUInt32.cs new file mode 100644 index 0000000..6112b80 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadUInt32.cs @@ -0,0 +1,72 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt32_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadUInt32()); + Assert.ThrowsException(() => stream.ReadUInt32(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt32(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt32_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadUInt32()); + Assert.ThrowsException(() => stream.ReadUInt32(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt32(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt32_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadUInt32((Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt32_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const uint expected = 420; + uint actual = stream.ReadUInt32(Endianness.BigEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt32_ShouldReadLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const uint expected = 420; + uint actual = stream.ReadUInt32(Endianness.LittleEndian); + + Assert.AreEqual(4, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.ReadUInt64.cs b/X10D.Tests/src/IO/StreamTests.ReadUInt64.cs new file mode 100644 index 0000000..6e5a0c7 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.ReadUInt64.cs @@ -0,0 +1,72 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt64_ShouldThrowArgumentException_GivenNonReadableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.ReadUInt64()); + Assert.ThrowsException(() => stream.ReadUInt64(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt64(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt64_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.ReadUInt64()); + Assert.ThrowsException(() => stream.ReadUInt64(Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.ReadUInt64(Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt64_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.ReadUInt64((Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt64_ShouldReadBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA4}; + stream.Write(bytes); + stream.Position = 0; + + const ulong expected = 420; + ulong actual = stream.ReadUInt64(Endianness.BigEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [CLSCompliant(false)] + public void ReadUInt64_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + ReadOnlySpan bytes = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + stream.Write(bytes); + stream.Position = 0; + + const ulong expected = 420; + ulong actual = stream.ReadUInt64(Endianness.LittleEndian); + + Assert.AreEqual(8, stream.Position); + Assert.AreEqual(expected, actual); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteDecimal.cs b/X10D.Tests/src/IO/StreamTests.WriteDecimal.cs new file mode 100644 index 0000000..1412f37 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteDecimal.cs @@ -0,0 +1,77 @@ +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteDecimal_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420.0m, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0m, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteDecimal_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420.0m, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0m, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteDecimal_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420.0m, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420.0m, (Endianness)(-1))); + } + + [TestMethod] + public void WriteDecimal_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0m, Endianness.BigEndian); + Assert.AreEqual(16, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[16]; + ReadOnlySpan expected = stackalloc byte[] + { + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x68 + }; + int read = stream.Read(actual); + + Assert.AreEqual(16, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteDecimal_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0m, Endianness.LittleEndian); + Assert.AreEqual(16, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[16]; + ReadOnlySpan expected = stackalloc byte[] + { + 0x68, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 + }; + int read = stream.Read(actual); + + Trace.WriteLine(string.Join(", ", actual.ToArray().Select(b => $"0x{b:X2}"))); + + Assert.AreEqual(16, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteDouble.cs b/X10D.Tests/src/IO/StreamTests.WriteDouble.cs new file mode 100644 index 0000000..5605830 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteDouble.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteDouble_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420.0, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteDouble_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420.0, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteDouble_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420.0, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420.0, (Endianness)(-1))); + } + + [TestMethod] + public void WriteDouble_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0, Endianness.BigEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0x40, 0x7A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteDouble_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0, Endianness.LittleEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x7A, 0x40}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteInt16.cs b/X10D.Tests/src/IO/StreamTests.WriteInt16.cs new file mode 100644 index 0000000..1e0a1e9 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteInt16.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteInt16_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write((short)420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write((short)420, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt16_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write((short)420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write((short)420, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt16_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write((short)420, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write((short)420, (Endianness)(-1))); + } + + [TestMethod] + public void WriteInt16_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write((short)420, Endianness.BigEndian); + Assert.AreEqual(2, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[2]; + ReadOnlySpan expected = stackalloc byte[] {0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(2, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteInt16_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write((short)420, Endianness.LittleEndian); + Assert.AreEqual(2, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[2]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01}; + int read = stream.Read(actual); + + Assert.AreEqual(2, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteInt32.cs b/X10D.Tests/src/IO/StreamTests.WriteInt32.cs new file mode 100644 index 0000000..dae3144 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteInt32.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteInt32_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt32_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt32_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420, (Endianness)(-1))); + } + + [TestMethod] + public void WriteInt32_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420, Endianness.BigEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteInt32_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420, Endianness.LittleEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteInt64.cs b/X10D.Tests/src/IO/StreamTests.WriteInt64.cs new file mode 100644 index 0000000..0f5e6d0 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteInt64.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteInt64_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420L, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420L, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt64_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420L, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420L, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteInt64_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420L, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420L, (Endianness)(-1))); + } + + [TestMethod] + public void WriteInt64_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420L, Endianness.BigEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteInt64_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420L, Endianness.LittleEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteSingle.cs b/X10D.Tests/src/IO/StreamTests.WriteSingle.cs new file mode 100644 index 0000000..5da14c0 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteSingle.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + public void WriteSingle_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420.0f, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0f, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteSingle_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420.0f, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420.0f, Endianness.BigEndian)); + } + + [TestMethod] + public void WriteSingle_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420.0f, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420.0f, (Endianness)(-1))); + } + + [TestMethod] + public void WriteSingle_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0f, Endianness.BigEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0x43, 0xD2, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + public void WriteSingle_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420.0f, Endianness.LittleEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0xD2, 0x43}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteUInt16.cs b/X10D.Tests/src/IO/StreamTests.WriteUInt16.cs new file mode 100644 index 0000000..fe73ec7 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteUInt16.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt16_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write((ushort)420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write((ushort)420, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt16_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write((ushort)420, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write((ushort)420, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt16_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write((ushort)420, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write((ushort)420, (Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt16_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write((ushort)420, Endianness.BigEndian); + Assert.AreEqual(2, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[2]; + ReadOnlySpan expected = stackalloc byte[] {0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(2, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt16_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write((ushort)420, Endianness.LittleEndian); + Assert.AreEqual(2, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[2]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01}; + int read = stream.Read(actual); + + Assert.AreEqual(2, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteUInt32.cs b/X10D.Tests/src/IO/StreamTests.WriteUInt32.cs new file mode 100644 index 0000000..e28e946 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteUInt32.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt32_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420U, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420U, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt32_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420U, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420U, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt32_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420U, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420U, (Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt32_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420U, Endianness.BigEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt32_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420U, Endianness.LittleEndian); + Assert.AreEqual(4, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[4]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(4, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.WriteUInt64.cs b/X10D.Tests/src/IO/StreamTests.WriteUInt64.cs new file mode 100644 index 0000000..ef8065b --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.WriteUInt64.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class StreamTests +{ + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt64_ShouldThrowArgumentException_GivenNonWriteableStream() + { + Stream stream = new DummyStream(); + Assert.ThrowsException(() => stream.Write(420UL, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420UL, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt64_ShouldThrowArgumentNullException_GivenNullStream() + { + Stream stream = null!; + Assert.ThrowsException(() => stream.Write(420UL, Endianness.LittleEndian)); + Assert.ThrowsException(() => stream.Write(420UL, Endianness.BigEndian)); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt64_ShouldThrowArgumentOutOfRangeException_GivenInvalidEndiannessValue() + { + // we don't need to enclose this stream in a using declaration, since disposing a + // null stream is meaningless. NullStream.Dispose actually does nothing, anyway. + // that - coupled with the fact that encapsulating the stream in a using declaration causes the + // analyser to trip up and think the stream is disposed by the time the local is captured in + // assertion lambda - means this line is fine as it is. please do not change. + Stream stream = Stream.Null; + Assert.ThrowsException(() => stream.Write(420UL, (Endianness)(-1))); + Assert.ThrowsException(() => stream.Write(420UL, (Endianness)(-1))); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt64_ShouldWriteBigEndian_GivenBigEndian() + { + using var stream = new MemoryStream(); + stream.Write(420UL, Endianness.BigEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA4}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } + + [TestMethod] + [CLSCompliant(false)] + public void WriteUInt64_ShouldWriteLittleEndian_GivenLittleEndian() + { + using var stream = new MemoryStream(); + stream.Write(420UL, Endianness.LittleEndian); + Assert.AreEqual(8, stream.Position); + stream.Position = 0; + + Span actual = stackalloc byte[8]; + ReadOnlySpan expected = stackalloc byte[] {0xA4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + int read = stream.Read(actual); + + Assert.AreEqual(8, read); + CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.cs b/X10D.Tests/src/IO/StreamTests.cs index 77809fd..2bb5d3c 100644 --- a/X10D.Tests/src/IO/StreamTests.cs +++ b/X10D.Tests/src/IO/StreamTests.cs @@ -7,7 +7,7 @@ using X10D.IO; namespace X10D.Tests.IO; [TestClass] -public class StreamTests +public partial class StreamTests { [TestMethod] public void GetHashSha1ShouldBeCorrect() @@ -99,341 +99,6 @@ public class StreamTests Stream.Null.TryWriteHash(Span.Empty, out _)); } - [TestMethod] - public void Write_ShouldThrow_GivenUndefinedEndianness() - { - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0.0f, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0.0, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0.0m, (Endianness)(-1)); - }); - - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write((short)0, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0L, (Endianness)(-1)); - }); - - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write((ushort)0, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0U, (Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.Write(0UL, (Endianness)(-1)); - }); - } - - [TestMethod] - public void Read_ShouldThrow_GivenNullStream() - { - Stream? stream = null; - Assert.ThrowsException(() => stream!.ReadSingle()); - Assert.ThrowsException(() => stream!.ReadDouble()); - Assert.ThrowsException(() => stream!.ReadDecimal()); - Assert.ThrowsException(() => stream!.ReadInt16()); - Assert.ThrowsException(() => stream!.ReadInt32()); - Assert.ThrowsException(() => stream!.ReadInt64()); - Assert.ThrowsException(() => stream!.ReadUInt16()); - Assert.ThrowsException(() => stream!.ReadUInt32()); - Assert.ThrowsException(() => stream!.ReadUInt64()); - } - - [TestMethod] - public void Write_ShouldThrow_GivenNullStream() - { - Stream? stream = null; - Assert.ThrowsException(() => stream!.Write(0.0f, Endianness.LittleEndian)); - Assert.ThrowsException(() => stream!.Write(0.0, Endianness.LittleEndian)); - Assert.ThrowsException(() => stream!.Write(0.0m, Endianness.LittleEndian)); - Assert.ThrowsException(() => stream!.Write((short)0)); - Assert.ThrowsException(() => stream!.Write(0)); - Assert.ThrowsException(() => stream!.Write(0L)); - Assert.ThrowsException(() => stream!.Write((ushort)0)); - Assert.ThrowsException(() => stream!.Write(0U)); - Assert.ThrowsException(() => stream!.Write(0UL)); - } - - [TestMethod] - public void Read_ShouldThrow_GivenUndefinedEndianness() - { - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadSingle((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadDouble((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadDecimal((Endianness)(-1)); - }); - - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadInt16((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadInt32((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadInt64((Endianness)(-1)); - }); - - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadUInt16((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadUInt32((Endianness)(-1)); - }); - Assert.ThrowsException(() => - { - using var stream = new MemoryStream(); - return stream.ReadUInt64((Endianness)(-1)); - }); - } - - [TestMethod] - public void ReadDouble_WriteDouble_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420.0, BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0, stream.ReadDouble(), 1e-6); - - stream.Position = 0; - stream.Write(420.0, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420.0, stream.ReadDouble(Endianness.LittleEndian), 1e-6); - - stream.Position = 0; - stream.Write(420.0, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0, stream.ReadDouble(Endianness.BigEndian), 1e-6); - } - - [TestMethod] - public void ReadDecimal_WriteSingle_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420.0m, BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0m, stream.ReadDecimal()); - - stream.Position = 0; - stream.Write(420.0m, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420.0m, stream.ReadDecimal(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write(420.0m, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0m, stream.ReadDecimal(Endianness.BigEndian)); - } - - [TestMethod] - public void ReadSingle_WriteSingle_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420.0f, BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0f, stream.ReadSingle(), 1e-6f); - - stream.Position = 0; - stream.Write(420.0f, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420.0f, stream.ReadSingle(Endianness.LittleEndian), 1e-6f); - - stream.Position = 0; - stream.Write(420.0f, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420.0f, stream.ReadSingle(Endianness.BigEndian), 1e-6f); - } - - [TestMethod] - public void ReadInt16_WriteInt16_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write((short)420); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt16()); - - stream.Position = 0; - stream.Write((short)420, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt16(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write((short)420, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt16(Endianness.BigEndian)); - } - - [TestMethod] - public void ReadInt32_WriteInt32_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt32()); - - stream.Position = 0; - stream.Write(420, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt32(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write(420, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420, stream.ReadInt32(Endianness.BigEndian)); - } - - [TestMethod] - public void ReadInt64_WriteInt64_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420L); - - stream.Position = 0; - Assert.AreEqual(420L, stream.ReadInt64()); - - stream.Position = 0; - stream.Write(420L, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420L, stream.ReadInt64(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write(420L, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420L, stream.ReadInt64(Endianness.BigEndian)); - } - - [TestMethod] - [CLSCompliant(false)] - public void ReadUInt16_WriteUInt16_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write((ushort)420); - - stream.Position = 0; - Assert.AreEqual((ushort)420, stream.ReadUInt16()); - - stream.Position = 0; - stream.Write((ushort)420, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual((ushort)420, stream.ReadUInt16(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write((ushort)420, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual((ushort)420, stream.ReadUInt16(Endianness.BigEndian)); - } - - [TestMethod] - [CLSCompliant(false)] - public void ReadUInt32_WriteUInt32_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420U); - - stream.Position = 0; - Assert.AreEqual(420U, stream.ReadUInt32()); - - stream.Position = 0; - stream.Write(420U, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420U, stream.ReadUInt32(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write(420U, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420U, stream.ReadUInt32(Endianness.BigEndian)); - } - - [TestMethod] - [CLSCompliant(false)] - public void ReadUInt64_WriteUInt64_ShouldBeSymmetric() - { - using var stream = new MemoryStream(); - stream.Write(420UL); - - stream.Position = 0; - Assert.AreEqual(420UL, stream.ReadUInt64()); - - stream.Position = 0; - stream.Write(420UL, Endianness.LittleEndian); - - stream.Position = 0; - Assert.AreEqual(420UL, stream.ReadUInt64(Endianness.LittleEndian)); - - stream.Position = 0; - stream.Write(420UL, Endianness.BigEndian); - - stream.Position = 0; - Assert.AreEqual(420UL, stream.ReadUInt64(Endianness.BigEndian)); - } - private class DummyStream : Stream { public DummyStream(bool readable = false) @@ -492,17 +157,15 @@ public class StreamTests protected override void HashCore(byte[] array, int ibStart, int cbSize) { - throw new NotImplementedException(); } protected override byte[] HashFinal() { - throw new NotImplementedException(); + return Array.Empty(); } public override void Initialize() { - throw new NotImplementedException(); } } @@ -510,17 +173,15 @@ public class StreamTests { protected override void HashCore(byte[] array, int ibStart, int cbSize) { - throw new NotImplementedException(); } protected override byte[] HashFinal() { - throw new NotImplementedException(); + return Array.Empty(); } public override void Initialize() { - throw new NotImplementedException(); } } } diff --git a/X10D.Tests/src/IO/TextReaderTests.cs b/X10D.Tests/src/IO/TextReaderTests.cs new file mode 100644 index 0000000..6e10ef4 --- /dev/null +++ b/X10D.Tests/src/IO/TextReaderTests.cs @@ -0,0 +1,83 @@ +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.IO; + +namespace X10D.Tests.IO; + +[TestClass] +public class TextReaderTests +{ + [TestMethod] + public void EnumerateLines_ShouldYield10Lines_Given10LineString() + { + using var stream = new MemoryStream(); + using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) + { + for (var index = 0; index < 10; index++) + { + writer.WriteLine(index); + } + } + + stream.Position = 0; + using var reader = new StreamReader(stream, Encoding.UTF8); + var lineCount = 0; + + foreach (string _ in reader.EnumerateLines()) + { + lineCount++; + } + + Assert.AreEqual(10, lineCount); + } + + [TestMethod] + public async Task EnumerateLinesAsync_ShouldYield10Lines_Given10LineString() + { + using var stream = new MemoryStream(); + await using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) + { + for (var index = 0; index < 10; index++) + { + writer.WriteLine(index); + } + } + + stream.Position = 0; + using var reader = new StreamReader(stream, Encoding.UTF8); + var lineCount = 0; + + await foreach (string _ in reader.EnumerateLinesAsync().ConfigureAwait(false)) + { + lineCount++; + } + + Assert.AreEqual(10, lineCount); + } + + [TestMethod] + public void EnumerateLines_ShouldThrowArgumentNullException_GivenNullSource() + { + TextReader reader = null!; + Assert.ThrowsException(() => + { + foreach (string _ in reader.EnumerateLines()) + { + // loop body is intentionally empty + } + }); + } + + [TestMethod] + public async Task EnumerateLinesAsync_ShouldThrowArgumentNullException_GivenNullSource() + { + TextReader reader = null!; + await Assert.ThrowsExceptionAsync(async () => + { + await foreach (string _ in reader.EnumerateLinesAsync().ConfigureAwait(false)) + { + // loop body is intentionally empty + } + }).ConfigureAwait(false); + } +} diff --git a/X10D.Tests/src/Linq/ByteTests.cs b/X10D.Tests/src/Linq/ByteTests.cs index 1c269b4..e98fe31 100644 --- a/X10D.Tests/src/Linq/ByteTests.cs +++ b/X10D.Tests/src/Linq/ByteTests.cs @@ -34,6 +34,14 @@ public class ByteTests // Π_(i=1)^n (2i) will overflow at i=4 for byte } + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } + [TestMethod] public void RangeTo_Byte_ShouldYieldCorrectValues() { diff --git a/X10D.Tests/src/Linq/DecimalTests.cs b/X10D.Tests/src/Linq/DecimalTests.cs index abb3f7b..6fd9da4 100644 --- a/X10D.Tests/src/Linq/DecimalTests.cs +++ b/X10D.Tests/src/Linq/DecimalTests.cs @@ -41,4 +41,12 @@ public class DecimalTests Assert.AreEqual(185794560m, Enumerable.Range(1, 9).Product(Double)); Assert.AreEqual(3715891200m, Enumerable.Range(1, 10).Product(Double)); } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/DoubleTests.cs b/X10D.Tests/src/Linq/DoubleTests.cs index 8e33bdd..1a54fbd 100644 --- a/X10D.Tests/src/Linq/DoubleTests.cs +++ b/X10D.Tests/src/Linq/DoubleTests.cs @@ -41,4 +41,12 @@ public class DoubleTests Assert.AreEqual(185794560.0, Enumerable.Range(1, 9).Product(Double)); Assert.AreEqual(3715891200.0, Enumerable.Range(1, 10).Product(Double)); } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/EnumerableTests.cs b/X10D.Tests/src/Linq/EnumerableTests.cs new file mode 100644 index 0000000..5318d4b --- /dev/null +++ b/X10D.Tests/src/Linq/EnumerableTests.cs @@ -0,0 +1,239 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Linq; + +namespace X10D.Tests.Linq; + +[TestClass] +public class EnumerableTests +{ + [TestMethod] + public void ConcatOne_ShouldReturnConcatenatedSequence_GivenValidSequenceAndValue() + { + IEnumerable source = new[] {"Hello"}; + string[] expected = {"Hello", "World"}; + + string[] actual = source.ConcatOne("World").ToArray(); + + Assert.AreEqual(2, actual.Length); + CollectionAssert.AreEqual(expected, actual); + } + + [TestMethod] + public void ConcatOne_ShouldReturnSingletonSequence_GivenEmptySequenceAndValidValue() + { + IEnumerable source = Enumerable.Empty(); + string[] expected = {"Foobar"}; + + string[] actual = source.ConcatOne("Foobar").ToArray(); + + Assert.AreEqual(1, actual.Length); + CollectionAssert.AreEqual(expected, actual); + } + + [TestMethod] + public void ConcatOne_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable? source = null; + Assert.ThrowsException(() => source!.ConcatOne("Foobar").ToArray()); + } + + [TestMethod] + public void MinMax_ShouldReturnCorrectValues_UsingDefaultComparer() + { + IEnumerable source = Enumerable.Range(1, 10); + (int minimum, int maximum) = source.MinMax(); + Assert.AreEqual(1, minimum); + Assert.AreEqual(10, maximum); + + source = Enumerable.Range(1, 10).ToArray(); + (minimum, maximum) = source.MinMax(); + Assert.AreEqual(1, minimum); + Assert.AreEqual(10, maximum); + } + + [TestMethod] + public void MinMax_ShouldReturnCorrectSelectedValues_UsingDefaultComparer() + { + IEnumerable source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}); + (int minimum, int maximum) = source.MinMax(p => p.Age); + Assert.AreEqual(1, minimum); + Assert.AreEqual(10, maximum); + + source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}).ToArray(); + (minimum, maximum) = source.MinMax(p => p.Age); + Assert.AreEqual(1, minimum); + Assert.AreEqual(10, maximum); + } + + [TestMethod] + public void MinMax_ShouldReturnOppositeSelectedValues_UsingInverseComparer() + { + IEnumerable source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}); + (int minimum, int maximum) = source.MinMax(p => p.Age, new InverseComparer()); + Assert.AreEqual(10, minimum); + Assert.AreEqual(1, maximum); + + source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}).ToArray(); + (minimum, maximum) = source.MinMax(p => p.Age, new InverseComparer()); + Assert.AreEqual(10, minimum); + Assert.AreEqual(1, maximum); + } + + [TestMethod] + public void MinMax_ShouldReturnOppositeValues_UsingInverseComparer() + { + (int minimum, int maximum) = Enumerable.Range(1, 10).MinMax(new InverseComparer()); + Assert.AreEqual(10, minimum); + Assert.AreEqual(1, maximum); + + (minimum, maximum) = Enumerable.Range(1, 10).ToArray().MinMax(new InverseComparer()); + Assert.AreEqual(10, minimum); + Assert.AreEqual(1, maximum); + } + + [TestMethod] + public void MinMax_ShouldThrowArgumentNullException_GivenNullSelector() + { + IEnumerable source = Enumerable.Empty(); + Assert.ThrowsException(() => source.MinMax((Func)(null!))); + Assert.ThrowsException(() => source.MinMax((Func)(null!), null)); + } + + [TestMethod] + public void MinMax_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable? source = null; + Assert.ThrowsException(() => source!.MinMax()); + Assert.ThrowsException(() => source!.MinMax(v => v)); + Assert.ThrowsException(() => source!.MinMax(null)); + Assert.ThrowsException(() => source!.MinMax(v => v, null)); + } + + [TestMethod] + public void MinMax_ShouldThrowInvalidOperationException_GivenEmptySource() + { + Assert.ThrowsException(() => Enumerable.Empty().MinMax()); + Assert.ThrowsException(() => Array.Empty().MinMax()); + Assert.ThrowsException(() => new List().MinMax()); + + Assert.ThrowsException(() => Enumerable.Empty().MinMax(i => i * 2)); + Assert.ThrowsException(() => Array.Empty().MinMax(i => i * 2)); + Assert.ThrowsException(() => new List().MinMax(i => i * 2)); + } + + [TestMethod] + public void MinMaxBy_ShouldReturnCorrectSelectedValues_UsingDefaultComparer() + { + IEnumerable source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}); + (Person minimum, Person maximum) = source.MinMaxBy(p => p.Age); + Assert.AreEqual(1, minimum.Age); + Assert.AreEqual(10, maximum.Age); + + source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}).ToArray(); + (minimum, maximum) = source.MinMaxBy(p => p.Age); + Assert.AreEqual(1, minimum.Age); + Assert.AreEqual(10, maximum.Age); + } + + [TestMethod] + public void MinMaxBy_ShouldReturnOppositeSelectedValues_UsingInverseComparer() + { + IEnumerable source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}); + (Person minimum, Person maximum) = source.MinMaxBy(p => p.Age, new InverseComparer()); + Assert.AreEqual(10, minimum.Age); + Assert.AreEqual(1, maximum.Age); + + source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}).ToArray(); + (minimum, maximum) = source.MinMaxBy(p => p.Age, new InverseComparer()); + Assert.AreEqual(10, minimum.Age); + Assert.AreEqual(1, maximum.Age); + } + + [TestMethod] + public void MinMaxBy_ShouldThrowArgumentNullException_GivenNullSelector() + { + Person[] source = Enumerable.Range(1, 10).Select(i => new Person {Age = i}).ToArray(); + + Assert.ThrowsException(() => source.MinMaxBy((Func)null!)); + Assert.ThrowsException(() => source.MinMaxBy((Func)null!, null)); + } + + [TestMethod] + public void MinMaxBy_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable? source = null; + Assert.ThrowsException(() => source!.MinMaxBy(p => p.Age)); + Assert.ThrowsException(() => source!.MinMaxBy(p => p.Age, null)); + } + + [TestMethod] + public void MinMaxBy_ShouldThrowInvalidOperationException_GivenEmptySource() + { + Assert.ThrowsException(() => + { + IEnumerable source = Enumerable.Empty(); + return source.MinMaxBy(p => p.Age); + }); + + Assert.ThrowsException(() => + { + Person[] source = Array.Empty(); + return source.MinMaxBy(p => p.Age); + }); + } + + private struct InverseComparer : IComparer where T : IComparable + { + public int Compare(T? x, T? y) + { + if (x is null) + { + return y is null ? 0 : 1; + } + + return y is null ? -1 : y.CompareTo(x); + } + } + + private struct Person : IComparable, IComparable + { + public int Age { get; set; } + + public static bool operator <(Person left, Person right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator >(Person left, Person right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator <=(Person left, Person right) + { + return left.CompareTo(right) <= 0; + } + + public static bool operator >=(Person left, Person right) + { + return left.CompareTo(right) >= 0; + } + + public int CompareTo(Person other) + { + return Age.CompareTo(other.Age); + } + + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + return obj is Person other + ? CompareTo(other) + : throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + } +} diff --git a/X10D.Tests/src/Linq/Int16Tests.cs b/X10D.Tests/src/Linq/Int16Tests.cs index c0f0636..aa23e62 100644 --- a/X10D.Tests/src/Linq/Int16Tests.cs +++ b/X10D.Tests/src/Linq/Int16Tests.cs @@ -10,7 +10,7 @@ public class Int16Tests public void ProductShouldBeCorrect() { short Cast(int i) => (short)i; - + Assert.AreEqual(0, Enumerable.Range(0, 10).Select(Cast).Product()); Assert.AreEqual(1, Enumerable.Range(1, 1).Select(Cast).Product()); Assert.AreEqual(2, Enumerable.Range(1, 2).Select(Cast).Product()); @@ -38,6 +38,13 @@ public class Int16Tests // Π_(i=1)^n (2i) will overflow at i=6 for short } + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + } + [TestMethod] public void RangeTo_Int16_ShouldYieldCorrectValues() { diff --git a/X10D.Tests/src/Linq/Int32Tests.cs b/X10D.Tests/src/Linq/Int32Tests.cs index efc0870..dc45267 100644 --- a/X10D.Tests/src/Linq/Int32Tests.cs +++ b/X10D.Tests/src/Linq/Int32Tests.cs @@ -41,6 +41,14 @@ public class Int32Tests // Π_(i=1)^n (2i) will overflow at i=10 for int } + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } + [TestMethod] public void RangeTo_Int32_ShouldYieldCorrectValues() { diff --git a/X10D.Tests/src/Linq/Int64Tests.cs b/X10D.Tests/src/Linq/Int64Tests.cs index 5535eeb..ebbb1fa 100644 --- a/X10D.Tests/src/Linq/Int64Tests.cs +++ b/X10D.Tests/src/Linq/Int64Tests.cs @@ -42,6 +42,14 @@ public class Int64Tests Assert.AreEqual(3715891200, Enumerable.Range(1, 10).Product(Double)); } + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } + [TestMethod] public void RangeTo_Int64_ShouldYieldCorrectValues() { diff --git a/X10D.Tests/src/Linq/SByteTests.cs b/X10D.Tests/src/Linq/SByteTests.cs index 3e360f9..826464c 100644 --- a/X10D.Tests/src/Linq/SByteTests.cs +++ b/X10D.Tests/src/Linq/SByteTests.cs @@ -34,4 +34,12 @@ public class SByteTests // Π_(i=1)^(n(i*2)) will overflow at i=4 for sbyte } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/SingleTests.cs b/X10D.Tests/src/Linq/SingleTests.cs index b504972..87cdb30 100644 --- a/X10D.Tests/src/Linq/SingleTests.cs +++ b/X10D.Tests/src/Linq/SingleTests.cs @@ -41,4 +41,12 @@ public class SingleTests Assert.AreEqual(185794560f, Enumerable.Range(1, 9).Product(Double)); Assert.AreEqual(3715891200f, Enumerable.Range(1, 10).Product(Double)); } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/UInt16Tests.cs b/X10D.Tests/src/Linq/UInt16Tests.cs index bf496e8..a45eaa6 100644 --- a/X10D.Tests/src/Linq/UInt16Tests.cs +++ b/X10D.Tests/src/Linq/UInt16Tests.cs @@ -40,4 +40,12 @@ public class UInt16Tests // Π_(i=1)^n (2i) will overflow at i=7 for ushort } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/UInt32Tests.cs b/X10D.Tests/src/Linq/UInt32Tests.cs index 060592e..cfb0ad6 100644 --- a/X10D.Tests/src/Linq/UInt32Tests.cs +++ b/X10D.Tests/src/Linq/UInt32Tests.cs @@ -42,4 +42,12 @@ public class UInt32Tests Assert.AreEqual(185794560U, Enumerable.Range(1, 9).Product(Double)); Assert.AreEqual(3715891200U, Enumerable.Range(1, 10).Product(Double)); } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Linq/UInt64Tests.cs b/X10D.Tests/src/Linq/UInt64Tests.cs index c9c2409..3b1c585 100644 --- a/X10D.Tests/src/Linq/UInt64Tests.cs +++ b/X10D.Tests/src/Linq/UInt64Tests.cs @@ -42,4 +42,12 @@ public class UInt64Tests Assert.AreEqual(185794560UL, Enumerable.Range(1, 9).Product(Double)); Assert.AreEqual(3715891200UL, Enumerable.Range(1, 10).Product(Double)); } + + [TestMethod] + public void Product_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Product()); + Assert.ThrowsException(() => source.Product(v => v)); + } } diff --git a/X10D.Tests/src/Math/ByteTests.Wrap.cs b/X10D.Tests/src/Math/ByteTests.Wrap.cs new file mode 100644 index 0000000..1713242 --- /dev/null +++ b/X10D.Tests/src/Math/ByteTests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class ByteTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const byte value = 10; + const byte low = 10; + const byte high = 20; + + byte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const byte value = 20; + const byte low = 10; + const byte high = 20; + + byte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const byte value = 30; + const byte low = 10; + const byte high = 20; + + byte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const byte value = 5; + const byte low = 10; + const byte high = 20; + + byte result = value.Wrap(low, high); + + Assert.AreEqual(11, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const byte value = 15; + const byte low = 10; + const byte high = 20; + + byte result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const byte value = 10; + const byte length = 10; + + byte result = value.Wrap(length); + + Assert.AreEqual(0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const byte value = 5; + const byte length = 10; + + byte result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const byte value = 15; + const byte length = 10; + + byte result = value.Wrap(length); + + Assert.AreEqual(5, result); + } + } +} diff --git a/X10D.Tests/src/Math/ByteTests.cs b/X10D.Tests/src/Math/ByteTests.cs index cfc4386..166754f 100644 --- a/X10D.Tests/src/Math/ByteTests.cs +++ b/X10D.Tests/src/Math/ByteTests.cs @@ -4,7 +4,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class ByteTests +public partial class ByteTests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -30,6 +30,28 @@ public class ByteTests Assert.AreEqual(3628800L, ((byte)10).Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const byte first = 5; + const byte second = 7; + + byte multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const byte first = 12; + const byte second = 18; + + byte multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { @@ -50,6 +72,65 @@ public class ByteTests Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const byte value1 = 2; + const byte value2 = 3; + const byte expected = 6; + + byte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const byte value1 = 0; + const byte value2 = 10; + const byte expected = 0; + + byte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const byte value1 = 1; + const byte value2 = 10; + const byte expected = 10; + + byte result1 = value1.LowestCommonMultiple(value2); + byte result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const byte value1 = 5; + const byte value2 = 5; + const byte expected = 5; + + byte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, ((byte)10).MultiplicativePersistence()); + Assert.AreEqual(1, ((byte)201).MultiplicativePersistence()); + Assert.AreEqual(1, ((byte)200).MultiplicativePersistence()); + Assert.AreEqual(1, ((byte)207).MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/ComparableTests.cs b/X10D.Tests/src/Math/ComparableTests.cs index dd71c7d..f0417f0 100644 --- a/X10D.Tests/src/Math/ComparableTests.cs +++ b/X10D.Tests/src/Math/ComparableTests.cs @@ -85,6 +85,13 @@ public class ComparableTests Assert.ThrowsException(() => 0.Clamp(6, 5)); } + [TestMethod] + public void Clamp_ShouldThrowArgumentNullException_GivenNullValue() + { + string comparable = null!; + Assert.ThrowsException(() => comparable.Clamp(string.Empty, string.Empty)); + } + [TestMethod] public void GreaterThan_5_6_ShouldBeFalse() { diff --git a/X10D.Tests/src/Math/DecimalTests.Wrap.cs b/X10D.Tests/src/Math/DecimalTests.Wrap.cs new file mode 100644 index 0000000..a49fd30 --- /dev/null +++ b/X10D.Tests/src/Math/DecimalTests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class DecimalTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const decimal value = 10; + const decimal low = 10; + const decimal high = 20; + + decimal result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const decimal value = 20; + const decimal low = 10; + const decimal high = 20; + + decimal result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const decimal value = 30; + const decimal low = 10; + const decimal high = 20; + + decimal result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const decimal value = 5; + const decimal low = 10; + const decimal high = 20; + + decimal result = value.Wrap(low, high); + + Assert.AreEqual(15.0m, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const decimal value = 15; + const decimal low = 10; + const decimal high = 20; + + decimal result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const decimal value = 10; + const decimal length = 10; + + decimal result = value.Wrap(length); + + Assert.AreEqual(0.0m, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const decimal value = 5; + const decimal length = 10; + + decimal result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const decimal value = 15; + const decimal length = 10; + + decimal result = value.Wrap(length); + + Assert.AreEqual(5.0m, result); + } + } +} diff --git a/X10D.Tests/src/Math/DecimalTests.cs b/X10D.Tests/src/Math/DecimalTests.cs index b84b1cf..a3df1c2 100644 --- a/X10D.Tests/src/Math/DecimalTests.cs +++ b/X10D.Tests/src/Math/DecimalTests.cs @@ -5,9 +5,8 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class DecimalTests +public partial class DecimalTests { -#if NETCOREAPP3_0_OR_GREATER [TestMethod] public void ComplexSqrt_ShouldBeCorrect_GivenReal() { @@ -26,7 +25,6 @@ public class DecimalTests Assert.AreEqual(new Complex(0, 3.0), (-9.0m).ComplexSqrt()); Assert.AreEqual(new Complex(0, 4.0), (-16.0m).ComplexSqrt()); } -#endif [TestMethod] public void IsEven_ShouldBeFalse_GivenOddNumber() @@ -82,6 +80,24 @@ public class DecimalTests Assert.AreEqual(10.0m, 7.5m.Round(5)); } + [TestMethod] + public void Saturate_ShouldClampValueTo1_GivenGreaterThan1() + { + Assert.AreEqual(1.0m, 1.5m.Saturate(), 1e-6m); + } + + [TestMethod] + public void Saturate_ShouldClampValueTo0_GivenLessThan0() + { + Assert.AreEqual(0.0m, (-0.5m).Saturate(), 1e-6m); + } + + [TestMethod] + public void Saturate_ShouldReturnValue_GivenValueBetween0And1() + { + Assert.AreEqual(0.5m, 0.5m.Saturate(), 1e-6m); + } + [TestMethod] public void Sign_ShouldBeMinus1_GivenNegative() { diff --git a/X10D.Tests/src/Math/DoubleTests.Wrap.cs b/X10D.Tests/src/Math/DoubleTests.Wrap.cs new file mode 100644 index 0000000..ff53476 --- /dev/null +++ b/X10D.Tests/src/Math/DoubleTests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class DoubleTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const double value = 10; + const double low = 10; + const double high = 20; + + double result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const double value = 20; + const double low = 10; + const double high = 20; + + double result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const double value = 30; + const double low = 10; + const double high = 20; + + double result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const double value = 5; + const double low = 10; + const double high = 20; + + double result = value.Wrap(low, high); + + Assert.AreEqual(15.0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const double value = 15; + const double low = 10; + const double high = 20; + + double result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const double value = 10; + const double length = 10; + + double result = value.Wrap(length); + + Assert.AreEqual(0.0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const double value = 5; + const double length = 10; + + double result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const double value = 15; + const double length = 10; + + double result = value.Wrap(length); + + Assert.AreEqual(5.0, result); + } + } +} diff --git a/X10D.Tests/src/Math/DoubleTests.cs b/X10D.Tests/src/Math/DoubleTests.cs index 96034bc..dc5e838 100644 --- a/X10D.Tests/src/Math/DoubleTests.cs +++ b/X10D.Tests/src/Math/DoubleTests.cs @@ -5,7 +5,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class DoubleTests +public partial class DoubleTests { [TestMethod] public void DegreesToRadians_ShouldBeCorrect() @@ -29,7 +29,6 @@ public class DoubleTests Assert.AreEqual(12.0, 0.20943951023931953.RadiansToDegrees(), 1e-6); } -#if NETCOREAPP3_0_OR_GREATER [TestMethod] public void ComplexSqrt_ShouldBeCorrect_GivenReal() { @@ -61,7 +60,6 @@ public class DoubleTests { Assert.AreEqual(Complex.NaN, double.NaN.ComplexSqrt()); } -#endif [TestMethod] public void IsEven_ShouldBeFalse_GivenOddNumber() @@ -117,6 +115,24 @@ public class DoubleTests Assert.AreEqual(10.0, 7.5.Round(5), 1e-6); } + [TestMethod] + public void Saturate_ShouldClampValueTo1_GivenGreaterThan1() + { + Assert.AreEqual(1.0, 1.5.Saturate(), 1e-6); + } + + [TestMethod] + public void Saturate_ShouldClampValueTo0_GivenLessThan0() + { + Assert.AreEqual(0.0, (-0.5).Saturate(), 1e-6); + } + + [TestMethod] + public void Saturate_ShouldReturnValue_GivenValueBetween0And1() + { + Assert.AreEqual(0.5, 0.5.Saturate(), 1e-6); + } + [TestMethod] public void Sign_ShouldBeMinus1_GivenNegative() { diff --git a/X10D.Tests/src/Math/Int16Tests.Wrap.cs b/X10D.Tests/src/Math/Int16Tests.Wrap.cs new file mode 100644 index 0000000..31054f4 --- /dev/null +++ b/X10D.Tests/src/Math/Int16Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class Int16Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const short value = 10; + const short low = 10; + const short high = 20; + + short result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const short value = 20; + const short low = 10; + const short high = 20; + + short result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const short value = 30; + const short low = 10; + const short high = 20; + + short result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const short value = 5; + const short low = 10; + const short high = 20; + + short result = value.Wrap(low, high); + + Assert.AreEqual(15, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const short value = 15; + const short low = 10; + const short high = 20; + + short result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const short value = 10; + const short length = 10; + + short result = value.Wrap(length); + + Assert.AreEqual(0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const short value = 5; + const short length = 10; + + short result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const short value = 15; + const short length = 10; + + short result = value.Wrap(length); + + Assert.AreEqual(5, result); + } + } +} diff --git a/X10D.Tests/src/Math/Int16Tests.cs b/X10D.Tests/src/Math/Int16Tests.cs index 15edb9a..1cfac50 100644 --- a/X10D.Tests/src/Math/Int16Tests.cs +++ b/X10D.Tests/src/Math/Int16Tests.cs @@ -4,7 +4,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class Int16Tests +public partial class Int16Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -30,12 +30,34 @@ public class Int16Tests Assert.AreEqual(3628800L, ((short)10).Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const short first = 5; + const short second = 7; + + short multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const short first = 12; + const short second = 18; + + short multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { const short one = 1; const short two = 2; - + Assert.IsFalse(one.IsEven()); Assert.IsTrue(two.IsEven()); } @@ -45,11 +67,82 @@ public class Int16Tests { const short one = 1; const short two = 2; - + Assert.IsTrue(one.IsOdd()); Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const short value1 = 2; + const short value2 = 3; + const short expected = 6; + + short result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const short value1 = 0; + const short value2 = 10; + const short expected = 0; + + short result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const short value1 = 1; + const short value2 = 10; + const short expected = 10; + + short result1 = value1.LowestCommonMultiple(value2); + short result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const short value1 = 5; + const short value2 = 5; + const short expected = 5; + + short result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithNegativeValues() + { + const short value1 = -2; + const short value2 = 3; + const short expected = -6; + + short result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, ((short)10).MultiplicativePersistence()); + Assert.AreEqual(1, ((short)201).MultiplicativePersistence()); + Assert.AreEqual(1, ((short)200).MultiplicativePersistence()); + Assert.AreEqual(1, ((short)20007).MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/Int32Tests.Wrap.cs b/X10D.Tests/src/Math/Int32Tests.Wrap.cs new file mode 100644 index 0000000..ef8a29b --- /dev/null +++ b/X10D.Tests/src/Math/Int32Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class Int32Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const int value = 10; + const int low = 10; + const int high = 20; + + int result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const int value = 20; + const int low = 10; + const int high = 20; + + int result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const int value = 30; + const int low = 10; + const int high = 20; + + int result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const int value = 5; + const int low = 10; + const int high = 20; + + int result = value.Wrap(low, high); + + Assert.AreEqual(15, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const int value = 15; + const int low = 10; + const int high = 20; + + int result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const int value = 10; + const int length = 10; + + int result = value.Wrap(length); + + Assert.AreEqual(0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const int value = 5; + const int length = 10; + + int result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const int value = 15; + const int length = 10; + + int result = value.Wrap(length); + + Assert.AreEqual(5, result); + } + } +} diff --git a/X10D.Tests/src/Math/Int32Tests.cs b/X10D.Tests/src/Math/Int32Tests.cs index db0d5fb..66246d3 100644 --- a/X10D.Tests/src/Math/Int32Tests.cs +++ b/X10D.Tests/src/Math/Int32Tests.cs @@ -4,7 +4,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class Int32Tests +public partial class Int32Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -30,12 +30,34 @@ public class Int32Tests Assert.AreEqual(3628800L, 10.Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const int first = 5; + const int second = 7; + + int multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const int first = 12; + const int second = 18; + + int multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { const int one = 1; const int two = 2; - + Assert.IsFalse(one.IsEven()); Assert.IsTrue(two.IsEven()); } @@ -45,11 +67,82 @@ public class Int32Tests { const int one = 1; const int two = 2; - + Assert.IsTrue(one.IsOdd()); Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const int value1 = 2; + const int value2 = 3; + const int expected = 6; + + int result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const int value1 = 0; + const int value2 = 10; + const int expected = 0; + + int result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const int value1 = 1; + const int value2 = 10; + const int expected = 10; + + int result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const int value1 = 5; + const int value2 = 5; + const int expected = 5; + + int result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithNegativeValues() + { + const int value1 = -2; + const int value2 = 3; + const int expected = -6; + + int result1 = value1.LowestCommonMultiple(value2); + int result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, 10.MultiplicativePersistence()); + Assert.AreEqual(1, 201.MultiplicativePersistence()); + Assert.AreEqual(1, 200.MultiplicativePersistence()); + Assert.AreEqual(1, 20007.MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/Int64Tests.Wrap.cs b/X10D.Tests/src/Math/Int64Tests.Wrap.cs new file mode 100644 index 0000000..dd2a27d --- /dev/null +++ b/X10D.Tests/src/Math/Int64Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class Int64Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const long value = 10; + const long low = 10; + const long high = 20; + + long result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const long value = 20; + const long low = 10; + const long high = 20; + + long result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const long value = 30; + const long low = 10; + const long high = 20; + + long result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const long value = 5; + const long low = 10; + const long high = 20; + + long result = value.Wrap(low, high); + + Assert.AreEqual(15L, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const long value = 15; + const long low = 10; + const long high = 20; + + long result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const long value = 10; + const long length = 10; + + long result = value.Wrap(length); + + Assert.AreEqual(0L, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const long value = 5; + const long length = 10; + + long result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const long value = 15; + const long length = 10; + + long result = value.Wrap(length); + + Assert.AreEqual(5L, result); + } + } +} diff --git a/X10D.Tests/src/Math/Int64Tests.cs b/X10D.Tests/src/Math/Int64Tests.cs index 44acb55..cf6d345 100644 --- a/X10D.Tests/src/Math/Int64Tests.cs +++ b/X10D.Tests/src/Math/Int64Tests.cs @@ -4,7 +4,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class Int64Tests +public partial class Int64Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -30,12 +30,34 @@ public class Int64Tests Assert.AreEqual(3628800L, 10L.Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const long first = 5L; + const long second = 7L; + + long multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1L, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const long first = 12L; + const long second = 18L; + + long multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6L, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { const long one = 1; const long two = 2; - + Assert.IsFalse(one.IsEven()); Assert.IsTrue(two.IsEven()); } @@ -45,11 +67,82 @@ public class Int64Tests { const long one = 1; const long two = 2; - + Assert.IsTrue(one.IsOdd()); Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const long value1 = 2; + const long value2 = 3; + const long expected = 6; + + long result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const long value1 = 0; + const long value2 = 10; + const long expected = 0; + + long result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const long value1 = 1; + const long value2 = 10; + const long expected = 10; + + long result1 = value1.LowestCommonMultiple(value2); + long result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const long value1 = 5; + const long value2 = 5; + const long expected = 5; + + long result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithNegativeValues() + { + const long value1 = -2; + const long value2 = 3; + const long expected = -6; + + long result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, 10L.MultiplicativePersistence()); + Assert.AreEqual(1, 201L.MultiplicativePersistence()); + Assert.AreEqual(1, 200L.MultiplicativePersistence()); + Assert.AreEqual(1, 20007L.MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/MathUtilityTests.cs b/X10D.Tests/src/Math/MathUtilityTests.cs new file mode 100644 index 0000000..a40f18c --- /dev/null +++ b/X10D.Tests/src/Math/MathUtilityTests.cs @@ -0,0 +1,427 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +#if !NET6_0_OR_GREATER +using X10D.Core; +#endif +using X10D.Math; + +namespace X10D.Tests.Math; + +[TestClass] +public class MathUtilityTests +{ + [TestMethod] + public void Bias_ReturnsCorrectResult_WhenBiasIsLessThanPointFive() + { + double doubleResult = MathUtility.Bias(0.5, 0.3); + float floatResult = MathUtility.Bias(0.5f, 0.3f); + + Assert.AreEqual(0.3, doubleResult, 1e-4); + Assert.AreEqual(0.3f, floatResult, 1e-4f); + } + + [TestMethod] + public void Bias_ReturnsCorrectResult_WhenBiasIsEqualToPointFive() + { + double doubleResult = MathUtility.Bias(0.5, 0.5); + float floatResult = MathUtility.Bias(0.5f, 0.5f); + + Assert.AreEqual(0.5, doubleResult, 1e-4); + Assert.AreEqual(0.5f, floatResult, 1e-4f); + } + + [TestMethod] + public void Bias_ReturnsCorrectResult_WhenBiasIsGreaterThanPointFive() + { + double doubleResult = MathUtility.Bias(0.5, 0.8); + float floatResult = MathUtility.Bias(0.5f, 0.8f); + + Assert.AreEqual(0.8, doubleResult, 1e-4); + Assert.AreEqual(0.8f, floatResult, 1e-4f); + } + + [TestMethod] + public void ExponentialDecay_ShouldReturnCorrectValue_GivenDouble() + { + const double value = 100.0; + const double alpha = 0.5; + const double decay = 0.1; + + const double expected = 95.122942; + double actual = MathUtility.ExponentialDecay(value, alpha, decay); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void ExponentialDecay_ShouldReturnCorrectValue_GivenSingle() + { + const float value = 100.0f; + const float alpha = 0.5f; + const float decay = 0.1f; + + const float expected = 95.12295f; + float actual = MathUtility.ExponentialDecay(value, alpha, decay); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void GammaToLinear_ShouldReturnQuarter_GivenQuarterAndGamma1() + { + double doubleResult = MathUtility.GammaToLinear(0.25, 1.0); + float floatResult = MathUtility.GammaToLinear(0.25f, 1.0f); + + Assert.AreEqual(0.25, doubleResult, 1e-6); + Assert.AreEqual(0.25f, floatResult, 1e-6f); + } + + [TestMethod] + public void GammaToLinear_ShouldReturn1_Given1AndDefaultGamma() + { + double doubleResult = MathUtility.GammaToLinear(1.0); + float floatResult = MathUtility.GammaToLinear(1.0f); + + Assert.AreEqual(1.0, doubleResult); + Assert.AreEqual(1.0f, floatResult); + } + + [TestMethod] + public void InverseLerp_ShouldReturn0_5_Given0_5_0_1() + { + double doubleResult = MathUtility.InverseLerp(0.5, 0.0, 1.0); + float floatResult = MathUtility.InverseLerp(0.5f, 0f, 1f); + + Assert.AreEqual(0.5, doubleResult, 1e-6); + Assert.AreEqual(0.5f, floatResult, 1e-6f); + } + + [TestMethod] + public void InverseLerp_ShouldReturn0_5_Given5_0_10() + { + double doubleResult = MathUtility.InverseLerp(5.0, 0.0, 10.0); + float floatResult = MathUtility.InverseLerp(5f, 0f, 10f); + + Assert.AreEqual(0.5, doubleResult, 1e-6); + Assert.AreEqual(0.5f, floatResult, 1e-6f); + } + + [TestMethod] + public void InverseLerp_ShouldReturn0_GivenTwoEqualValues() + { + var random = new Random(); + double doubleA = random.NextDouble(); + double doubleB = random.NextDouble(); + + float floatA = random.NextSingle(); + float floatB = random.NextSingle(); + + double doubleResult = MathUtility.InverseLerp(doubleA, doubleB, doubleB); + float floatResult = MathUtility.InverseLerp(floatA, floatB, floatB); + + Assert.AreEqual(0.0, doubleResult, 1e-6); + Assert.AreEqual(0.0f, floatResult, 1e-6f); + } + + [TestMethod] + public void Lerp_ShouldReturnHigher_GivenAlpha1() + { + Assert.AreEqual(20.0f, MathUtility.Lerp(10.0f, 20.0f, 1.0f)); + Assert.AreEqual(20.0, MathUtility.Lerp(10.0, 20.0, 1.0)); + } + + [TestMethod] + public void Lerp_ShouldReturnLower_GivenAlpha0() + { + Assert.AreEqual(10.0f, MathUtility.Lerp(10.0f, 20.0f, 0.0f)); + Assert.AreEqual(10.0, MathUtility.Lerp(10.0, 20.0, 0.0)); + } + + [TestMethod] + public void Lerp_ShouldReturnMidPoint_GivenAlphaPoint5() + { + Assert.AreEqual(15.0f, MathUtility.Lerp(10.0f, 20.0f, 0.5f)); + Assert.AreEqual(15.0, MathUtility.Lerp(10.0, 20.0, 0.5)); + } + + [TestMethod] + public void LinearToGamma_ShouldReturnQuarter_GivenQuarterAndGamma1() + { + double doubleResult = MathUtility.LinearToGamma(0.25, 1.0); + float floatResult = MathUtility.LinearToGamma(0.25f, 1.0f); + + Assert.AreEqual(0.25, doubleResult); + Assert.AreEqual(0.25f, floatResult); + } + + [TestMethod] + public void LinearToGamma_ShouldReturn1_Given1AndDefaultGamma() + { + double doubleResult = MathUtility.LinearToGamma(1.0); + float floatResult = MathUtility.LinearToGamma(1.0f); + + Assert.AreEqual(1.0, doubleResult); + Assert.AreEqual(1.0f, floatResult); + } + + [TestMethod] + public void Pulse_ShouldReturn1_GivenDoubleValueWithinBounds() + { + const double value = 0.5; + const double lower = 0.0; + const double upper = 1.0; + + double result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(1.0, result, 1e-6); + } + + [TestMethod] + public void Pulse_ShouldReturn0_GivenDoubleValueLessThanLowerBound() + { + const double value = -1.0; + const double lower = 0.0; + const double upper = 1.0; + + double result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(0.0, result, 1e-6); + } + + [TestMethod] + public void Pulse_ShouldReturn0_GivenDoubleValueGreaterThanUpperBound() + { + const double value = 2.0; + const double lower = 0.0; + const double upper = 1.0; + + double result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(0.0, result, 1e-6); + } + + [TestMethod] + public void Pulse_ShouldReturn1_GivenSingleValueWithinBounds() + { + const float value = 0.5f; + const float lower = 0.0f; + const float upper = 1.0f; + + float result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(1.0f, result, 1e-6f); + } + + [TestMethod] + public void Pulse_ShouldReturn0_GivenSingleValueLessThanLowerBound() + { + const float value = -1.0f; + const float lower = 0.0f; + const float upper = 1.0f; + + float result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(0.0f, result, 1e-6f); + } + + [TestMethod] + public void Pulse_ShouldReturn0_GivenSingleValueGreaterThanUpperBound() + { + const float value = 2.0f; + const float lower = 0.0f; + const float upper = 1.0f; + + float result = MathUtility.Pulse(value, lower, upper); + + Assert.AreEqual(0.0f, result, 1e-6f); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_Given0Point5AsDouble() + { + const double value = 0.5; + + const double expected = 0.5; + double actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_Given0Point5AsSingle() + { + const float value = 0.5f; + + const float expected = 0.5f; + float actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_Given1Point5AsDouble() + { + const double value = 1.5; + + const double expected = 0.5; + double actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_Given1Point5AsSingle() + { + const float value = 1.5f; + + const float expected = 0.5f; + float actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_GivenNegative1Point5AsDouble() + { + const double value = -1.5; + + const double expected = 0.5; + double actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sawtooth_ShouldReturn0Point5_GivenNegative1Point5AsSingle() + { + const float value = -1.5f; + + const float expected = 0.5f; + float actual = MathUtility.Sawtooth(value); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void ScaleRangeDouble_ShouldScaleRange_GivenItsValues() + { + double result = MathUtility.ScaleRange(0.5, 0.0, 1.0, 5.0, 10.0); + Assert.AreEqual(7.5, result); + } + + [TestMethod] + public void ScaleRangeSingle_ShouldScaleRange_GivenItsValues() + { + float result = MathUtility.ScaleRange(0.5f, 0.0f, 1.0f, 5.0f, 10.0f); + Assert.AreEqual(7.5f, result); + } + + [TestMethod] + public void Sigmoid_ReturnsExpectedValue_UsingDouble() + { + const double input = 0.5f; + const double expected = 0.622459331f; + + double actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sigmoid_ReturnsExpectedValue_UsingSingle() + { + const float input = 0.5f; + const float expected = 0.622459331f; + + float actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void Sigmoid_ReturnsZeroWhenInputIsNegativeInfinity_UsingDouble() + { + const double input = double.NegativeInfinity; + const double expected = 0f; + + double actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sigmoid_ReturnsZeroWhenInputIsNegativeInfinity_UsingSingle() + { + const float input = float.NegativeInfinity; + const float expected = 0f; + + float actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void Sigmoid_ReturnsOneWhenInputIsPositiveInfinity_UsingDouble() + { + const double input = double.PositiveInfinity; + const double expected = 1f; + + double actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sigmoid_ReturnsOneWhenInputIsPositiveInfinity_UsingSingle() + { + const float input = float.PositiveInfinity; + const float expected = 1f; + + float actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + [TestMethod] + public void Sigmoid_ReturnsZeroPointFiveWhenInputIsZero_UsingDouble() + { + const double input = 0f; + const double expected = 0.5f; + + double actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6); + } + + [TestMethod] + public void Sigmoid_ReturnsZeroPointFiveWhenInputIsZero_UsingSingle() + { + const float input = 0f; + const float expected = 0.5f; + + float actual = MathUtility.Sigmoid(input); + + Assert.AreEqual(expected, actual, 1e-6f); + } + + + [TestMethod] + public void SmoothStep_ShouldReturnHigher_GivenAlpha1() + { + Assert.AreEqual(20.0f, MathUtility.SmoothStep(10.0f, 20.0f, 1.0f)); + Assert.AreEqual(20.0, MathUtility.SmoothStep(10.0, 20.0, 1.0)); + } + + [TestMethod] + public void SmoothStep_ShouldReturnLower_GivenAlpha0() + { + Assert.AreEqual(10.0f, MathUtility.SmoothStep(10.0f, 20.0f, 0.0f)); + Assert.AreEqual(10.0, MathUtility.SmoothStep(10.0, 20.0, 0.0)); + } + + [TestMethod] + public void SmoothStep_ShouldReturnMidPoint_GivenAlphaPoint5() + { + Assert.AreEqual(15.0f, MathUtility.SmoothStep(10.0f, 20.0f, 0.5f)); + Assert.AreEqual(15.0, MathUtility.SmoothStep(10.0, 20.0, 0.5)); + } +} diff --git a/X10D.Tests/src/Math/SByteTests.Wrap.cs b/X10D.Tests/src/Math/SByteTests.Wrap.cs new file mode 100644 index 0000000..64b473a --- /dev/null +++ b/X10D.Tests/src/Math/SByteTests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class SByteTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const sbyte value = 10; + const sbyte low = 10; + const sbyte high = 20; + + sbyte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const sbyte value = 20; + const sbyte low = 10; + const sbyte high = 20; + + sbyte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const sbyte value = 30; + const sbyte low = 10; + const sbyte high = 20; + + sbyte result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const sbyte value = 5; + const sbyte low = 10; + const sbyte high = 20; + + sbyte result = value.Wrap(low, high); + + Assert.AreEqual(15, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const sbyte value = 15; + const sbyte low = 10; + const sbyte high = 20; + + sbyte result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const sbyte value = 10; + const sbyte length = 10; + + sbyte result = value.Wrap(length); + + Assert.AreEqual(0, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const sbyte value = 5; + const sbyte length = 10; + + sbyte result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const sbyte value = 15; + const sbyte length = 10; + + sbyte result = value.Wrap(length); + + Assert.AreEqual(5, result); + } + } +} diff --git a/X10D.Tests/src/Math/SByteTests.cs b/X10D.Tests/src/Math/SByteTests.cs index 73b97a2..3874515 100644 --- a/X10D.Tests/src/Math/SByteTests.cs +++ b/X10D.Tests/src/Math/SByteTests.cs @@ -5,7 +5,7 @@ namespace X10D.Tests.Math; [TestClass] [CLSCompliant(false)] -public class SByteTests +public partial class SByteTests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -31,6 +31,28 @@ public class SByteTests Assert.AreEqual(3628800L, ((sbyte)10).Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const sbyte first = 5; + const sbyte second = 7; + + sbyte multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const sbyte first = 12; + const sbyte second = 18; + + sbyte multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { @@ -51,6 +73,77 @@ public class SByteTests Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const sbyte value1 = 2; + const sbyte value2 = 3; + const sbyte expected = 6; + + sbyte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const sbyte value1 = 0; + const sbyte value2 = 10; + const sbyte expected = 0; + + sbyte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const sbyte value1 = 1; + const sbyte value2 = 10; + const sbyte expected = 10; + + sbyte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const sbyte value1 = 5; + const sbyte value2 = 5; + const sbyte expected = 5; + + sbyte result1 = value1.LowestCommonMultiple(value2); + sbyte result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithNegativeValues() + { + const sbyte value1 = -2; + const sbyte value2 = 3; + const sbyte expected = -6; + + sbyte result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, ((sbyte)10).MultiplicativePersistence()); + Assert.AreEqual(1, ((sbyte)20).MultiplicativePersistence()); + Assert.AreEqual(1, ((sbyte)101).MultiplicativePersistence()); + Assert.AreEqual(1, ((sbyte)120).MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/SingleTests.Wrap.cs b/X10D.Tests/src/Math/SingleTests.Wrap.cs new file mode 100644 index 0000000..5d75ccb --- /dev/null +++ b/X10D.Tests/src/Math/SingleTests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class SingleTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const float value = 10; + const float low = 10; + const float high = 20; + + float result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const float value = 20; + const float low = 10; + const float high = 20; + + float result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const float value = 30; + const float low = 10; + const float high = 20; + + float result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const float value = 5; + const float low = 10; + const float high = 20; + + float result = value.Wrap(low, high); + + Assert.AreEqual(15.0f, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const float value = 15; + const float low = 10; + const float high = 20; + + float result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const float value = 10; + const float length = 10; + + float result = value.Wrap(length); + + Assert.AreEqual(0.0f, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const float value = 5; + const float length = 10; + + float result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const float value = 15; + const float length = 10; + + float result = value.Wrap(length); + + Assert.AreEqual(5.0f, result); + } + } +} diff --git a/X10D.Tests/src/Math/SingleTests.cs b/X10D.Tests/src/Math/SingleTests.cs index 82c2c0d..a6ecddc 100644 --- a/X10D.Tests/src/Math/SingleTests.cs +++ b/X10D.Tests/src/Math/SingleTests.cs @@ -5,7 +5,7 @@ using X10D.Math; namespace X10D.Tests.Math; [TestClass] -public class SingleTests +public partial class SingleTests { [TestMethod] public void DegreesToRadians_ShouldBeCorrect() @@ -29,7 +29,6 @@ public class SingleTests Assert.AreEqual(12.0f, 0.20943952f.RadiansToDegrees(), 1e-6f); } -#if NETCOREAPP3_0_OR_GREATER [TestMethod] public void ComplexSqrt_ShouldBeCorrect_GivenReal() { @@ -61,7 +60,6 @@ public class SingleTests { Assert.AreEqual(Complex.NaN, float.NaN.ComplexSqrt()); } -#endif [TestMethod] public void IsEven_ShouldBeFalse_GivenOddNumber() @@ -117,6 +115,24 @@ public class SingleTests Assert.AreEqual(10.0f, 7.5f.Round(5), 1e-6f); } + [TestMethod] + public void Saturate_ShouldClampValueTo1_GivenGreaterThan1() + { + Assert.AreEqual(1.0f, 1.5f.Saturate(), 1e-6f); + } + + [TestMethod] + public void Saturate_ShouldClampValueTo0_GivenLessThan0() + { + Assert.AreEqual(0.0f, (-0.5f).Saturate(), 1e-6f); + } + + [TestMethod] + public void Saturate_ShouldReturnValue_GivenValueBetween0And1() + { + Assert.AreEqual(0.5f, 0.5f.Saturate(), 1e-6f); + } + [TestMethod] public void Sign_ShouldBeMinus1_GivenNegative() { diff --git a/X10D.Tests/src/Math/UInt16Tests.Wrap.cs b/X10D.Tests/src/Math/UInt16Tests.Wrap.cs new file mode 100644 index 0000000..9704639 --- /dev/null +++ b/X10D.Tests/src/Math/UInt16Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class UInt16Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const ushort value = 10; + const ushort low = 10; + const ushort high = 20; + + ushort result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const ushort value = 20; + const ushort low = 10; + const ushort high = 20; + + ushort result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const ushort value = 30; + const ushort low = 10; + const ushort high = 20; + + ushort result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const ushort value = 5; + const ushort low = 10; + const ushort high = 20; + + ushort result = value.Wrap(low, high); + + Assert.AreEqual(11U, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const ushort value = 15; + const ushort low = 10; + const ushort high = 20; + + ushort result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const ushort value = 10; + const ushort length = 10; + + ushort result = value.Wrap(length); + + Assert.AreEqual(0U, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const ushort value = 5; + const ushort length = 10; + + ushort result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const ushort value = 15; + const ushort length = 10; + + ushort result = value.Wrap(length); + + Assert.AreEqual(5U, result); + } + } +} diff --git a/X10D.Tests/src/Math/UInt16Tests.cs b/X10D.Tests/src/Math/UInt16Tests.cs index ac8575a..d873829 100644 --- a/X10D.Tests/src/Math/UInt16Tests.cs +++ b/X10D.Tests/src/Math/UInt16Tests.cs @@ -5,7 +5,7 @@ namespace X10D.Tests.Math; [TestClass] [CLSCompliant(false)] -public class UInt16Tests +public partial class UInt16Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -14,7 +14,7 @@ public class UInt16Tests Assert.AreEqual(4, value.DigitalRoot()); Assert.AreEqual(4, (-value).DigitalRoot()); } - + [TestMethod] public void FactorialShouldBeCorrect() { @@ -31,12 +31,34 @@ public class UInt16Tests Assert.AreEqual(3628800UL, ((ushort)10).Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const ushort first = 5; + const ushort second = 7; + + ushort multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const ushort first = 12; + const ushort second = 18; + + ushort multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { const ushort one = 1; const ushort two = 2; - + Assert.IsFalse(one.IsEven()); Assert.IsTrue(two.IsEven()); } @@ -46,11 +68,70 @@ public class UInt16Tests { const ushort one = 1; const ushort two = 2; - + Assert.IsTrue(one.IsOdd()); Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const ushort value1 = 2; + const ushort value2 = 3; + const ushort expected = 6; + + ushort result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const ushort value1 = 0; + const ushort value2 = 10; + const ushort expected = 0; + + ushort result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const ushort value1 = 1; + const ushort value2 = 10; + const ushort expected = 10; + + ushort result1 = value1.LowestCommonMultiple(value2); + ushort result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const ushort value1 = 5; + const ushort value2 = 5; + const ushort expected = 5; + + ushort result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, ((ushort)10).MultiplicativePersistence()); + Assert.AreEqual(1, ((ushort)201).MultiplicativePersistence()); + Assert.AreEqual(1, ((ushort)200).MultiplicativePersistence()); + Assert.AreEqual(1, ((ushort)20007).MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/UInt32Tests.Wrap.cs b/X10D.Tests/src/Math/UInt32Tests.Wrap.cs new file mode 100644 index 0000000..0fefee7 --- /dev/null +++ b/X10D.Tests/src/Math/UInt32Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class UInt32Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const uint value = 10; + const uint low = 10; + const uint high = 20; + + uint result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const uint value = 20; + const uint low = 10; + const uint high = 20; + + uint result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const uint value = 30; + const uint low = 10; + const uint high = 20; + + uint result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const uint value = 5; + const uint low = 10; + const uint high = 20; + + uint result = value.Wrap(low, high); + + Assert.AreEqual(11U, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const uint value = 15; + const uint low = 10; + const uint high = 20; + + uint result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const uint value = 10; + const uint length = 10; + + uint result = value.Wrap(length); + + Assert.AreEqual(0U, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const uint value = 5; + const uint length = 10; + + uint result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const uint value = 15; + const uint length = 10; + + uint result = value.Wrap(length); + + Assert.AreEqual(5U, result); + } + } +} diff --git a/X10D.Tests/src/Math/UInt32Tests.cs b/X10D.Tests/src/Math/UInt32Tests.cs index 6c06407..4454642 100644 --- a/X10D.Tests/src/Math/UInt32Tests.cs +++ b/X10D.Tests/src/Math/UInt32Tests.cs @@ -5,7 +5,7 @@ namespace X10D.Tests.Math; [TestClass] [CLSCompliant(false)] -public class UInt32Tests +public partial class UInt32Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -31,6 +31,28 @@ public class UInt32Tests Assert.AreEqual(3628800UL, 10U.Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const uint first = 5U; + const uint second = 7U; + + uint multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1U, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const uint first = 12U; + const uint second = 18U; + + uint multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6U, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { @@ -51,6 +73,65 @@ public class UInt32Tests Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const uint value1 = 2; + const uint value2 = 3; + const uint expected = 6; + + uint result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const uint value1 = 0; + const uint value2 = 10; + const uint expected = 0; + + uint result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const uint value1 = 1; + const uint value2 = 10; + const uint expected = 10; + + uint result1 = value1.LowestCommonMultiple(value2); + uint result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const uint value1 = 5; + const uint value2 = 5; + const uint expected = 5; + + uint result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, 10U.MultiplicativePersistence()); + Assert.AreEqual(1, 201U.MultiplicativePersistence()); + Assert.AreEqual(1, 200U.MultiplicativePersistence()); + Assert.AreEqual(1, 20007U.MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Math/UInt64Tests.Wrap.cs b/X10D.Tests/src/Math/UInt64Tests.Wrap.cs new file mode 100644 index 0000000..e28c49b --- /dev/null +++ b/X10D.Tests/src/Math/UInt64Tests.Wrap.cs @@ -0,0 +1,104 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class UInt64Tests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + const ulong value = 10; + const ulong low = 10; + const ulong high = 20; + + ulong result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + const ulong value = 20; + const ulong low = 10; + const ulong high = 20; + + ulong result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + const ulong value = 30; + const ulong low = 10; + const ulong high = 20; + + ulong result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + const ulong value = 5; + const ulong low = 10; + const ulong high = 20; + + ulong result = value.Wrap(low, high); + + Assert.AreEqual(11UL, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + const ulong value = 15; + const ulong low = 10; + const ulong high = 20; + + ulong result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + const ulong value = 10; + const ulong length = 10; + + ulong result = value.Wrap(length); + + Assert.AreEqual(0UL, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + const ulong value = 5; + const ulong length = 10; + + ulong result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + const ulong value = 15; + const ulong length = 10; + + ulong result = value.Wrap(length); + + Assert.AreEqual(5UL, result); + } + } +} diff --git a/X10D.Tests/src/Math/UInt64Tests.cs b/X10D.Tests/src/Math/UInt64Tests.cs index d18a7d6..c5e81ba 100644 --- a/X10D.Tests/src/Math/UInt64Tests.cs +++ b/X10D.Tests/src/Math/UInt64Tests.cs @@ -5,7 +5,7 @@ namespace X10D.Tests.Math; [TestClass] [CLSCompliant(false)] -public class UInt64Tests +public partial class UInt64Tests { [TestMethod] public void DigitalRootShouldBeCorrect() @@ -35,12 +35,34 @@ public class UInt64Tests Assert.AreEqual(3628800UL, 10UL.Factorial()); } + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + const ulong first = 5UL; + const ulong second = 7UL; + + ulong multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1UL, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + const ulong first = 12UL; + const ulong second = 18UL; + + ulong multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6UL, multiple); + } + [TestMethod] public void IsEvenShouldBeCorrect() { const ulong one = 1; const ulong two = 2; - + Assert.IsFalse(one.IsEven()); Assert.IsTrue(two.IsEven()); } @@ -50,11 +72,68 @@ public class UInt64Tests { const ulong one = 1; const ulong two = 2; - + Assert.IsTrue(one.IsOdd()); Assert.IsFalse(two.IsOdd()); } + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + const ulong value1 = 2; + const ulong value2 = 3; + const ulong expected = 6; + + ulong result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + const ulong value1 = 0; + const ulong value2 = 10; + const ulong expected = 0; + + ulong result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + const ulong value1 = 1; + const ulong value2 = 10; + const ulong expected = 10; + + ulong result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + const ulong value1 = 5; + const ulong value2 = 5; + const ulong expected = 5; + + ulong result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, 10UL.MultiplicativePersistence()); + Assert.AreEqual(1, 201UL.MultiplicativePersistence()); + Assert.AreEqual(1, 200UL.MultiplicativePersistence()); + Assert.AreEqual(1, 20007UL.MultiplicativePersistence()); + } + [TestMethod] public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() { diff --git a/X10D.Tests/src/Net/IPAddressTests.cs b/X10D.Tests/src/Net/IPAddressTests.cs index ac86bf6..443d6fb 100644 --- a/X10D.Tests/src/Net/IPAddressTests.cs +++ b/X10D.Tests/src/Net/IPAddressTests.cs @@ -29,6 +29,13 @@ public class IPAddressTests Assert.IsFalse(_ipv6Address.IsIPv4()); } + [TestMethod] + public void IsIPv4_ShouldThrowArgumentNullException_GivenNullAddress() + { + IPAddress address = null!; + Assert.ThrowsException(() => address.IsIPv4()); + } + [TestMethod] public void IsIPv6_ShouldBeFalse_GivenIPv4() { @@ -40,4 +47,11 @@ public class IPAddressTests { Assert.IsTrue(_ipv6Address.IsIPv6()); } + + [TestMethod] + public void IsIPv6_ShouldThrowArgumentNullException_GivenNullAddress() + { + IPAddress address = null!; + Assert.ThrowsException(() => address.IsIPv6()); + } } diff --git a/X10D.Tests/src/Numerics/ByteTests.cs b/X10D.Tests/src/Numerics/ByteTests.cs index d4890b2..b9f875a 100644 --- a/X10D.Tests/src/Numerics/ByteTests.cs +++ b/X10D.Tests/src/Numerics/ByteTests.cs @@ -6,6 +6,24 @@ namespace X10D.Tests.Numerics; [TestClass] public class ByteTests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, ((byte)0).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, ((byte)0b11010101).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe8_Given11111111() + { + Assert.AreEqual(8, ((byte)0b11111111).PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -39,4 +57,29 @@ public class ByteTests const byte value = 181; // 10110101 Assert.AreEqual(value, value.RotateRight(8)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4, ((byte)3).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((byte)5).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((byte)6).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((byte)7).RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (byte)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0, ((byte)0).RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/Int16Tests.cs b/X10D.Tests/src/Numerics/Int16Tests.cs index 11a4a3d..1368cd2 100644 --- a/X10D.Tests/src/Numerics/Int16Tests.cs +++ b/X10D.Tests/src/Numerics/Int16Tests.cs @@ -6,6 +6,24 @@ namespace X10D.Tests.Numerics; [TestClass] public class Int16Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, ((short)0).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, ((short)0b11010101).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe15_Given0111111111111111() + { + Assert.AreEqual(15, ((short)0b0111111111111111).PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -41,4 +59,29 @@ public class Int16Tests const short value = 2896; // 00001011 01010000 Assert.AreEqual(value, value.RotateRight(16)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4, ((short)3).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((short)5).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((short)6).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((short)7).RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (short)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0, ((short)0).RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/Int32Tests.cs b/X10D.Tests/src/Numerics/Int32Tests.cs index 874b5af..121ac68 100644 --- a/X10D.Tests/src/Numerics/Int32Tests.cs +++ b/X10D.Tests/src/Numerics/Int32Tests.cs @@ -6,6 +6,24 @@ namespace X10D.Tests.Numerics; [TestClass] public class Int32Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, ((uint)0).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, ((uint)0b11010101).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe31_Given11111111111111111111111111111111() + { + Assert.AreEqual(31, 0b01111111111111111111111111111111.PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -39,4 +57,29 @@ public class Int32Tests const int value = 284719; // 00000000 00000100 01011000 00101111 Assert.AreEqual(value, value.RotateRight(32)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4, 3.RoundUpToPowerOf2()); + Assert.AreEqual(8, 5.RoundUpToPowerOf2()); + Assert.AreEqual(8, 6.RoundUpToPowerOf2()); + Assert.AreEqual(8, 7.RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (int)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0, 0.RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/Int64Tests.cs b/X10D.Tests/src/Numerics/Int64Tests.cs index 2dff9cf..d151c6a 100644 --- a/X10D.Tests/src/Numerics/Int64Tests.cs +++ b/X10D.Tests/src/Numerics/Int64Tests.cs @@ -6,6 +6,24 @@ namespace X10D.Tests.Numerics; [TestClass] public class Int64Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, 0L.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, 0b11010101L.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe63_Given0111111111111111111111111111111111111111111111111111111111111111() + { + Assert.AreEqual(63, 0b0111111111111111111111111111111111111111111111111111111111111111L.PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -39,4 +57,29 @@ public class Int64Tests const long value = 5972019251303316844; // 01010010 11100000 11011111 11011110 00110001 10111010 01111101 01101100 Assert.AreEqual(value, value.RotateRight(64)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4L, 3L.RoundUpToPowerOf2()); + Assert.AreEqual(8L, 5L.RoundUpToPowerOf2()); + Assert.AreEqual(8L, 6L.RoundUpToPowerOf2()); + Assert.AreEqual(8L, 7L.RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (long)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0L, 0L.RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/QuaternionTests.cs b/X10D.Tests/src/Numerics/QuaternionTests.cs new file mode 100644 index 0000000..22e4fec --- /dev/null +++ b/X10D.Tests/src/Numerics/QuaternionTests.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Numerics; + +namespace X10D.Tests.Numerics; + +[TestClass] +public class QuaternionTests +{ + [TestMethod] + public void ToAxisAngle_ShouldGiveAngle180VectorZero_GivenQuaternion() + { + Vector3 axis = Vector3.UnitY; + const float angle = MathF.PI; + var quaternion = Quaternion.CreateFromAxisAngle(axis, angle); + + (Vector3 Axis, float Angle) axisAngle = quaternion.ToAxisAngle(); + Assert.AreEqual(axis, axisAngle.Axis); + Assert.AreEqual(angle, axisAngle.Angle); + } + + [TestMethod] + public void ToVector3_ShouldReturnZeroVector_GivenIdentityQuaternion() + { + Assert.AreEqual(Vector3.Zero, Quaternion.Identity.ToVector3()); + } + + [TestMethod] + public void ToVector3_ShouldReturnVector_0_PI_0_GivenQuaternionCreatedFrom_PI_0_0() + { + Quaternion quaternion = Quaternion.CreateFromYawPitchRoll(MathF.PI, 0, 0); + var expected = new Vector3(0, MathF.PI, 0); + var actual = quaternion.ToVector3(); + + Assert.AreEqual(expected.X, actual.X, 1e-5f); + Assert.AreEqual(expected.Y, actual.Y, 1e-5f); + Assert.AreEqual(expected.Z, actual.Z, 1e-5f); + } +} diff --git a/X10D.Tests/src/Numerics/SByteTests.cs b/X10D.Tests/src/Numerics/SByteTests.cs index c24ebcf..b16dbb9 100644 --- a/X10D.Tests/src/Numerics/SByteTests.cs +++ b/X10D.Tests/src/Numerics/SByteTests.cs @@ -7,6 +7,24 @@ namespace X10D.Tests.Numerics; [CLSCompliant(false)] public class SByteTests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, ((sbyte)0).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe4_Given01010101() + { + Assert.AreEqual(4, ((sbyte)0b01010101).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe7_Given01111111() + { + Assert.AreEqual(7, ((sbyte)0b01111111).PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -40,4 +58,29 @@ public class SByteTests const sbyte value = 117; // 01110101 Assert.AreEqual(value, value.RotateRight(8)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4, ((sbyte)3).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((sbyte)5).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((sbyte)6).RoundUpToPowerOf2()); + Assert.AreEqual(8, ((sbyte)7).RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 7; i++) + { + var value = (sbyte)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0, ((sbyte)0).RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/UInt16Tests.cs b/X10D.Tests/src/Numerics/UInt16Tests.cs index 23a5d39..98072d1 100644 --- a/X10D.Tests/src/Numerics/UInt16Tests.cs +++ b/X10D.Tests/src/Numerics/UInt16Tests.cs @@ -7,6 +7,24 @@ namespace X10D.Tests.Numerics; [CLSCompliant(false)] public class UInt16Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, ((ushort)0).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, ((ushort)0b11010101).PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe16_Given1111111111111111() + { + Assert.AreEqual(16, ((ushort)0b1111111111111111).PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -42,4 +60,29 @@ public class UInt16Tests const ushort value = 2896; // 00001011 01010000 Assert.AreEqual(value, value.RotateRight(16)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4U, ((ushort)3).RoundUpToPowerOf2()); + Assert.AreEqual(8U, ((ushort)5).RoundUpToPowerOf2()); + Assert.AreEqual(8U, ((ushort)6).RoundUpToPowerOf2()); + Assert.AreEqual(8U, ((ushort)7).RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (ushort)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0U, ((ushort)0).RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/UInt32Tests.cs b/X10D.Tests/src/Numerics/UInt32Tests.cs index 5686953..dbaae05 100644 --- a/X10D.Tests/src/Numerics/UInt32Tests.cs +++ b/X10D.Tests/src/Numerics/UInt32Tests.cs @@ -7,6 +7,24 @@ namespace X10D.Tests.Numerics; [CLSCompliant(false)] public class UInt32Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, 0U.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, 0b11010101U.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe32_Given11111111111111111111111111111111() + { + Assert.AreEqual(32, 0b11111111111111111111111111111111U.PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -40,4 +58,29 @@ public class UInt32Tests const uint value = 284719; // 00000000 00000100 01011000 00101111 Assert.AreEqual(value, value.RotateRight(32)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4U, 3U.RoundUpToPowerOf2()); + Assert.AreEqual(8U, 5U.RoundUpToPowerOf2()); + Assert.AreEqual(8U, 6U.RoundUpToPowerOf2()); + Assert.AreEqual(8U, 7U.RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (uint)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0U, 0U.RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/UInt64Tests.cs b/X10D.Tests/src/Numerics/UInt64Tests.cs index 7d2f026..41211dc 100644 --- a/X10D.Tests/src/Numerics/UInt64Tests.cs +++ b/X10D.Tests/src/Numerics/UInt64Tests.cs @@ -7,6 +7,24 @@ namespace X10D.Tests.Numerics; [CLSCompliant(false)] public class UInt64Tests { + [TestMethod] + public void PopCount_ShouldBe0_Given0() + { + Assert.AreEqual(0, 0UL.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe5_Given11010101() + { + Assert.AreEqual(5, 0b11010101UL.PopCount()); + } + + [TestMethod] + public void PopCount_ShouldBe64_Given1111111111111111111111111111111111111111111111111111111111111111() + { + Assert.AreEqual(64, 0b1111111111111111111111111111111111111111111111111111111111111111UL.PopCount()); + } + [TestMethod] public void RotateLeft_ShouldRotateCorrectly() { @@ -40,4 +58,29 @@ public class UInt64Tests const ulong value = 5972019251303316844; // 01010010 11100000 11011111 11011110 00110001 10111010 01111101 01101100 Assert.AreEqual(value, value.RotateRight(64)); } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnRoundedValue_GivenValue() + { + Assert.AreEqual(4UL, 3UL.RoundUpToPowerOf2()); + Assert.AreEqual(8UL, 5UL.RoundUpToPowerOf2()); + Assert.AreEqual(8UL, 6UL.RoundUpToPowerOf2()); + Assert.AreEqual(8UL, 7UL.RoundUpToPowerOf2()); + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturnSameValue_GivenPowerOf2() + { + for (var i = 0; i < 8; i++) + { + var value = (ulong)System.Math.Pow(2, i); + Assert.AreEqual(value, value.RoundUpToPowerOf2()); + } + } + + [TestMethod] + public void RoundUpToPowerOf2_ShouldReturn0_Given0() + { + Assert.AreEqual(0UL, 0UL.RoundUpToPowerOf2()); + } } diff --git a/X10D.Tests/src/Numerics/Vector2Tests.cs b/X10D.Tests/src/Numerics/Vector2Tests.cs index 72c1ddb..a0497e8 100644 --- a/X10D.Tests/src/Numerics/Vector2Tests.cs +++ b/X10D.Tests/src/Numerics/Vector2Tests.cs @@ -1,5 +1,9 @@ using System.Numerics; using Microsoft.VisualStudio.TestTools.UnitTesting; +#if !NET6_0_OR_GREATER +using X10D.Core; +#endif +using X10D.Drawing; using X10D.Numerics; namespace X10D.Tests.Numerics; @@ -7,6 +11,84 @@ namespace X10D.Tests.Numerics; [TestClass] public class Vector2Tests { + [TestMethod] + public void Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector2(1, 2); + (float x, float y) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + } + + [TestMethod] + public void IsOnLine_ShouldReturnTrue_ForPointOnLine() + { + Vector2 start = Vector2.Zero; + Vector2 end = Vector2.UnitX; + Vector2 point = new Vector2(0.5f, 0.0f); + var line = new LineF(start, end); + + Assert.IsTrue(point.IsOnLine(line)); + Assert.IsTrue(point.IsOnLine(line.Start, line.End)); + Assert.IsTrue(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void IsOnLine_ShouldReturnFalse_ForPointNotOnLine() + { + Vector2 start = Vector2.Zero; + Vector2 end = Vector2.UnitX; + Vector2 point = new Vector2(0.5f, 1.0f); + var line = new LineF(start, end); + + Assert.IsFalse(point.IsOnLine(line)); + Assert.IsFalse(point.IsOnLine(line.Start, line.End)); + Assert.IsFalse(point.IsOnLine(line.Start.ToVector2(), line.End.ToVector2())); + } + + [TestMethod] + public void Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector2(1.5f, 2.6f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.X); + Assert.AreEqual(3, rounded.Y); + } + + [TestMethod] + public void Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector2(1.5f, 25.2f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.X); + Assert.AreEqual(30, rounded.Y); + } + + [TestMethod] + public void ToPointF_ShouldReturnPoint_WithEquivalentMembers() + { + var random = new Random(); + var vector = new Vector2(random.NextSingle(), random.NextSingle()); + var point = vector.ToPointF(); + + Assert.AreEqual(vector.X, point.X, 1e-6f); + Assert.AreEqual(vector.Y, point.Y, 1e-6f); + } + + [TestMethod] + public void ToSizeF_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + var vector = new Vector2(random.NextSingle(), random.NextSingle()); + var size = vector.ToSizeF(); + + Assert.AreEqual(vector.X, size.Width); + Assert.AreEqual(vector.Y, size.Height); + } + [TestMethod] public void WithX_ShouldReturnVectorWithNewX_GivenVector() { diff --git a/X10D.Tests/src/Numerics/Vector3Tests.cs b/X10D.Tests/src/Numerics/Vector3Tests.cs index 285a253..10b1ad0 100644 --- a/X10D.Tests/src/Numerics/Vector3Tests.cs +++ b/X10D.Tests/src/Numerics/Vector3Tests.cs @@ -7,6 +7,39 @@ namespace X10D.Tests.Numerics; [TestClass] public class Vector3Tests { + [TestMethod] + public void Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector3(1, 2, 3); + (float x, float y, float z) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + Assert.AreEqual(3, z); + } + + [TestMethod] + public void Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector3(1.5f, 2.6f, -5.2f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.X); + Assert.AreEqual(3, rounded.Y); + Assert.AreEqual(-5, rounded.Z); + } + + [TestMethod] + public void Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector3(1.5f, 25.2f, -12.5f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.X); + Assert.AreEqual(30, rounded.Y); + Assert.AreEqual(-10, rounded.Z); + } + [TestMethod] public void WithX_ShouldReturnVectorWithNewX_GivenVector() { diff --git a/X10D.Tests/src/Numerics/Vector4Tests.cs b/X10D.Tests/src/Numerics/Vector4Tests.cs index 4a0b927..88dbac7 100644 --- a/X10D.Tests/src/Numerics/Vector4Tests.cs +++ b/X10D.Tests/src/Numerics/Vector4Tests.cs @@ -7,6 +7,42 @@ namespace X10D.Tests.Numerics; [TestClass] public class Vector4Tests { + [TestMethod] + public void Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector4(1, 2, 3, 4); + (float x, float y, float z, float w) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + Assert.AreEqual(3, z); + Assert.AreEqual(4, w); + } + + [TestMethod] + public void Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector4(1.5f, 2.6f, -5.2f, 0.3f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.X); + Assert.AreEqual(3, rounded.Y); + Assert.AreEqual(-5, rounded.Z); + Assert.AreEqual(0, rounded.W); + } + + [TestMethod] + public void Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector4(1.5f, 25.2f, -12.5f, 101.2f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.X); + Assert.AreEqual(30, rounded.Y); + Assert.AreEqual(-10, rounded.Z); + Assert.AreEqual(100, rounded.W); + } + [TestMethod] public void WithW_ShouldReturnVectorWithNewW_GivenVector() { diff --git a/X10D.Tests/src/Reflection/MemberInfoTests.cs b/X10D.Tests/src/Reflection/MemberInfoTests.cs index 24d2675..1774b36 100644 --- a/X10D.Tests/src/Reflection/MemberInfoTests.cs +++ b/X10D.Tests/src/Reflection/MemberInfoTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Reflection; namespace X10D.Tests.Reflection; @@ -69,10 +70,14 @@ public class MemberInfoTests Assert.ThrowsException(() => (type!.SelectFromCustomAttribute((CLSCompliantAttribute attribute) => attribute.IsCompliant, true))); - Assert.ThrowsException(() => - { - Func? selector = null; - typeof(int).SelectFromCustomAttribute(selector!); - }); + const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.Static; + var memberInfo = typeof(int).GetMembers(bindingFlags)[0]; + Func? selector = null; + + Assert.ThrowsException(() => typeof(int).SelectFromCustomAttribute(selector!)); + Assert.ThrowsException(() => typeof(int).SelectFromCustomAttribute(selector!, default)); + Assert.ThrowsException(() => memberInfo.SelectFromCustomAttribute(selector!)); + Assert.ThrowsException(() => memberInfo.SelectFromCustomAttribute(selector!, default)); } } diff --git a/X10D.Tests/src/Reflection/TypeTests.cs b/X10D.Tests/src/Reflection/TypeTests.cs index 4324afb..f76b7fd 100644 --- a/X10D.Tests/src/Reflection/TypeTests.cs +++ b/X10D.Tests/src/Reflection/TypeTests.cs @@ -32,6 +32,7 @@ public class TypeTests { Assert.ThrowsException(() => typeof(object).Inherits(null!)); Assert.ThrowsException(() => ((Type?)null)!.Inherits(typeof(object))); + Assert.ThrowsException(() => ((Type?)null)!.Inherits()); } [TestMethod] @@ -47,7 +48,8 @@ public class TypeTests public void Implements_ShouldThrow_GivenNull() { Assert.ThrowsException(() => typeof(object).Implements(null!)); - Assert.ThrowsException(() => ((Type?)null)!.Implements(typeof(object))); + Assert.ThrowsException(() => ((Type?)null)!.Implements(typeof(IComparable))); + Assert.ThrowsException(() => ((Type?)null)!.Implements()); } [TestMethod] diff --git a/X10D.Tests/src/Text/CharSpanTests.cs b/X10D.Tests/src/Text/CharSpanTests.cs new file mode 100644 index 0000000..fd32588 --- /dev/null +++ b/X10D.Tests/src/Text/CharSpanTests.cs @@ -0,0 +1,63 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Text; + +namespace X10D.Tests.Text; + +[TestClass] +public class CharSpanTests +{ + [TestMethod] + public void CountSubstring_ShouldHonor_StringComparison() + { + var readOnlySpan = "Hello World".AsSpan(); + Span span = stackalloc char[readOnlySpan.Length]; + readOnlySpan.CopyTo(span); + + Assert.AreEqual(0, readOnlySpan.CountSubstring('E')); + Assert.AreEqual(0, readOnlySpan.CountSubstring("E".AsSpan())); + Assert.AreEqual(1, readOnlySpan.CountSubstring("E".AsSpan(), StringComparison.OrdinalIgnoreCase)); + + Assert.AreEqual(0, span.CountSubstring('E')); + Assert.AreEqual(0, span.CountSubstring("E".AsSpan())); + Assert.AreEqual(1, span.CountSubstring("E".AsSpan(), StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn0_GivenNoInstanceChar() + { + var readOnlySpan = "Hello World".AsSpan(); + Span span = stackalloc char[readOnlySpan.Length]; + readOnlySpan.CopyTo(span); + + Assert.AreEqual(0, readOnlySpan.CountSubstring('z')); + Assert.AreEqual(0, readOnlySpan.CountSubstring("z".AsSpan())); + Assert.AreEqual(0, readOnlySpan.CountSubstring("z".AsSpan(), StringComparison.OrdinalIgnoreCase)); + + Assert.AreEqual(0, span.CountSubstring('z')); + Assert.AreEqual(0, span.CountSubstring("z".AsSpan())); + Assert.AreEqual(0, span.CountSubstring("z".AsSpan(), StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn1_GivenSingleInstanceChar() + { + var readOnlySpan = "Hello World".AsSpan(); + Span span = stackalloc char[readOnlySpan.Length]; + readOnlySpan.CopyTo(span); + + Assert.AreEqual(1, readOnlySpan.CountSubstring('e')); + Assert.AreEqual(1, readOnlySpan.CountSubstring("e".AsSpan())); + Assert.AreEqual(1, readOnlySpan.CountSubstring("e".AsSpan(), StringComparison.OrdinalIgnoreCase)); + + Assert.AreEqual(1, span.CountSubstring('e')); + Assert.AreEqual(1, span.CountSubstring("e".AsSpan())); + Assert.AreEqual(1, span.CountSubstring("e".AsSpan(), StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn0_GivenEmptyHaystack() + { + Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring('\0')); + Assert.AreEqual(0, string.Empty.AsSpan().CountSubstring(string.Empty.AsSpan(), StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/X10D.Tests/src/Text/CharTests.cs b/X10D.Tests/src/Text/CharTests.cs index 7963f21..7ace536 100644 --- a/X10D.Tests/src/Text/CharTests.cs +++ b/X10D.Tests/src/Text/CharTests.cs @@ -1,5 +1,4 @@ -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Text; namespace X10D.Tests.Text; diff --git a/X10D.Tests/src/Text/EnumerableTests.cs b/X10D.Tests/src/Text/EnumerableTests.cs new file mode 100644 index 0000000..8a73cb3 --- /dev/null +++ b/X10D.Tests/src/Text/EnumerableTests.cs @@ -0,0 +1,85 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Text; + +namespace X10D.Tests.Text; + +[TestClass] +public class EnumerableTests +{ + [TestMethod] + public void Grep_ShouldFilterCorrectly_GivenPattern() + { + int year = DateTime.Now.Year; + var source = new[] {"Hello", "World", "String 123", $"The year is {year}"}; + var expectedResult = new[] {"String 123", $"The year is {year}"}; + + const string pattern = /*lang=regex*/@"[0-9]+"; + string[] actualResult = source.Grep(pattern).ToArray(); + + CollectionAssert.AreEqual(expectedResult, actualResult); + } + + [TestMethod] + public void Grep_ShouldYieldNoResults_GivenEmptySource() + { + string[] source = Array.Empty(); + string[] expectedResult = Array.Empty(); + + const string pattern = /*lang=regex*/@"[0-9]+"; + string[] actualResult = source.Grep(pattern).ToArray(); + + CollectionAssert.AreEqual(expectedResult, actualResult); + } + + [TestMethod] + public void Grep_ShouldMatchUpperCase_GivenIgnoreCaseTrue() + { + int year = DateTime.Now.Year; + var source = new[] {"Hello", "WORLD", "String 123", $"The year is {year}"}; + var expectedResult = new[] {"WORLD"}; + + const string pattern = /*lang=regex*/@"world"; + string[] actualResult = source.Grep(pattern, true).ToArray(); + + CollectionAssert.AreEqual(expectedResult, actualResult); + } + + [TestMethod] + public void Grep_ShouldNotMatchUpperCase_GivenIgnoreCaseFalse() + { + int year = DateTime.Now.Year; + var source = new[] {"Hello", "WORLD", "String 123", $"The year is {year}"}; + + const string pattern = /*lang=regex*/@"world"; + string[] actualResult = source.Grep(pattern, false).ToArray(); + + Assert.AreEqual(0, actualResult.Length); + } + + [TestMethod] + public void Grep_ShouldThrowArgumentNullException_GivenNullPattern() + { + IEnumerable source = Enumerable.Empty(); + Assert.ThrowsException(() => source.Grep(null!).ToArray()); + Assert.ThrowsException(() => source.Grep(null!, false).ToArray()); + } + + [TestMethod] + public void Grep_ShouldThrowArgumentNullException_GivenNullSource() + { + IEnumerable source = null!; + Assert.ThrowsException(() => source.Grep("foo").ToArray()); + Assert.ThrowsException(() => source.Grep("foo", false).ToArray()); + } + + [TestMethod] + public void Grep_ShouldYieldNoElements_GivenNoMatchingStrings() + { + var source = new[] {"Hello", "World", "String"}; + + const string pattern = /*lang=regex*/@"[0-9]+"; + string[] actualResult = source.Grep(pattern).ToArray(); + + Assert.AreEqual(0, actualResult.Length); + } +} diff --git a/X10D.Tests/src/Text/RuneTests.cs b/X10D.Tests/src/Text/RuneTests.cs index c8181b1..48e7501 100644 --- a/X10D.Tests/src/Text/RuneTests.cs +++ b/X10D.Tests/src/Text/RuneTests.cs @@ -1,4 +1,4 @@ -#if NETCOREAPP3_0_OR_GREATER +#if NET5_0_OR_GREATER using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Text; @@ -28,7 +28,7 @@ public class RuneTests } [TestMethod] - public void RepeatShouldBeCorrect() + public void Repeat_ShouldRepeatRune_GivenValidCount() { const string expected = "aaaaaaaaaa"; var rune = new Rune('a'); @@ -38,7 +38,7 @@ public class RuneTests } [TestMethod] - public void RepeatOneCountShouldBeLength1String() + public void Repeat_ShouldReturnStringOfLength1_GivenCountOf1() { string repeated = new Rune('a').Repeat(1); Assert.AreEqual(1, repeated.Length); @@ -46,15 +46,48 @@ public class RuneTests } [TestMethod] - public void RepeatZeroCountShouldBeEmpty() + public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount() + { + var rune = new Rune('a'); + Assert.ThrowsException(() => rune.Repeat(-1)); + } + + [TestMethod] + public void Repeat_ShouldReturnEmptyString_GivenCountOf0() { Assert.AreEqual(string.Empty, new Rune('a').Repeat(0)); } [TestMethod] - public void RepeatNegativeCountShouldThrow() + public void RepeatCodepoint_0000_007F_ShouldReturnString() { - Assert.ThrowsException(() => new Rune('a').Repeat(-1)); + string repeated = new Rune(69).Repeat(16); + Assert.AreEqual(16, repeated.Length); + Assert.AreEqual("EEEEEEEEEEEEEEEE", repeated); + } + + [TestMethod] + public void RepeatCodepoint_0080_07FF_ShouldReturnString() + { + string repeated = new Rune(192).Repeat(8); + Assert.AreEqual(8, repeated.Length); + Assert.AreEqual("ÀÀÀÀÀÀÀÀ", repeated); + } + + [TestMethod] + public void RepeatCodepoint_0800_FFFF_ShouldReturnString() + { + string repeated = new Rune(0x0800).Repeat(5); + Assert.AreEqual(5, repeated.Length); + Assert.AreEqual("ࠀࠀࠀࠀࠀ", repeated); + } + + [TestMethod] + public void RepeatCodepointBeyondU10000ShouldReturnString() + { + string repeated = new Rune('\uD800', '\uDC00').Repeat(6); + Assert.AreEqual(12, repeated.Length); + Assert.AreEqual("𐀀𐀀𐀀𐀀𐀀𐀀", repeated); } } #endif diff --git a/X10D.Tests/src/Text/StringTests.cs b/X10D.Tests/src/Text/StringTests.cs index fa99f40..0798b6e 100644 --- a/X10D.Tests/src/Text/StringTests.cs +++ b/X10D.Tests/src/Text/StringTests.cs @@ -101,6 +101,149 @@ public class StringTests Assert.ThrowsException(() => "Hello World".ChangeEncoding(Encoding.UTF8, null!)); } + [TestMethod] + public void CountSubstring_ShouldHonor_StringComparison() + { + Assert.AreEqual(0, "Hello World".CountSubstring('E')); + Assert.AreEqual(0, "Hello World".CountSubstring("E")); + Assert.AreEqual(1, "Hello World".CountSubstring("E", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn0_GivenNoInstanceChar() + { + Assert.AreEqual(0, "Hello World".CountSubstring('z')); + Assert.AreEqual(0, "Hello World".CountSubstring("z")); + Assert.AreEqual(0, "Hello World".CountSubstring("z", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn1_GivenSingleInstanceChar() + { + Assert.AreEqual(1, "Hello World".CountSubstring('e')); + Assert.AreEqual(1, "Hello World".CountSubstring("e")); + Assert.AreEqual(1, "Hello World".CountSubstring("e", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldReturn0_GivenEmptyHaystack() + { + Assert.AreEqual(0, string.Empty.CountSubstring('\0')); + Assert.AreEqual(0, string.Empty.CountSubstring(string.Empty)); + Assert.AreEqual(0, string.Empty.CountSubstring(string.Empty, StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void CountSubstring_ShouldThrow_GivenNullHaystack() + { + Assert.ThrowsException(() => ((string?)null!).CountSubstring('\0')); + Assert.ThrowsException(() => ((string?)null!).CountSubstring(string.Empty)); + Assert.ThrowsException(() => + ((string?)null!).CountSubstring(string.Empty, StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldPrependChar_GivenEndsWithReturnFalse() + { + const string value = "Hello Worl"; + const char substring = 'd'; + + Assert.AreEqual("Hello World", value.EnsureEndsWith(substring)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldReturnChar_GivenEndsWithReturnTrue() + { + const string value = "A"; + const char substring = 'A'; + + Assert.AreEqual(value, value.EnsureEndsWith(substring)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldPrependChar_GivenEndsWithReturnFalse() + { + const string value = "B"; + const char substring = 'A'; + + Assert.AreEqual("AB", value.EnsureStartsWith(substring)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldReturnChar_GivenEndsWithReturnTrue() + { + const string value = "A"; + const char substring = 'A'; + + Assert.AreEqual(value, value.EnsureStartsWith(substring)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldAppendSubstring_GivenEndsWithReturnFalse() + { + const string value = "Hello "; + const string substring = "World"; + + Assert.AreEqual("Hello World", value.EnsureEndsWith(substring)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldReturnString_GivenEndsWithReturnTrue() + { + const string substring = "World"; + + Assert.AreEqual(substring, substring.EnsureEndsWith(substring)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldReturnString_GivenEmptySourceString() + { + const string substring = "World"; + + Assert.AreEqual(substring, string.Empty.EnsureEndsWith(substring)); + } + + [TestMethod] + public void EnsureEndsWith_ShouldReturnString_GivenEmptySubString() + { + const string substring = "World"; + + Assert.AreEqual(substring, substring.EnsureEndsWith(string.Empty)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldAppendSubstring_GivenEndsWithReturnFalse() + { + const string value = "World"; + const string substring = "Hello "; + + Assert.AreEqual("Hello World", value.EnsureStartsWith(substring)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldReturnString_GivenEndsWithReturnTrue() + { + const string substring = "World"; + + Assert.AreEqual(substring, substring.EnsureStartsWith(substring)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldReturnString_GivenEmptySourceString() + { + const string substring = "World"; + + Assert.AreEqual(substring, string.Empty.EnsureStartsWith(substring)); + } + + [TestMethod] + public void EnsureStartsWith_ShouldReturnString_GivenEmptySubString() + { + const string substring = "World"; + + Assert.AreEqual(substring, substring.EnsureStartsWith(string.Empty)); + } + [TestMethod] public void EnumParse_ShouldReturnCorrectValue_GivenString() { @@ -237,6 +380,39 @@ public class StringTests Assert.IsFalse("World".IsEmoji()); } + [TestMethod] + public void IsEmoji_ShouldThrowArgumentNullException_GivenNullInput() + { + string value = null!; + Assert.ThrowsException(() => value.IsEmoji()); + } + + [TestMethod] + public void IsEmpty_ShouldReturnTrue_GivenEmptyString() + { + Assert.IsTrue("".IsEmpty()); + Assert.IsTrue(string.Empty.IsEmpty()); + } + + [TestMethod] + public void IsEmpty_ShouldReturnFalse_GivenWhiteSpaceString() + { + Assert.IsFalse(" ".IsEmpty()); + } + + [TestMethod] + public void IsEmpty_ShouldReturnFalse_GivenNonEmptyString() + { + Assert.IsFalse("Hello World".IsEmpty()); + } + + [TestMethod] + public void IsEmpty_ShouldThrowArgumentNullException_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.IsEmpty()); + } + [TestMethod] public void IsLower_ShouldReturnTrue_GivenLowercaseString() { @@ -262,6 +438,58 @@ public class StringTests Assert.ThrowsException(() => value!.IsLower()); } + [TestMethod] + public void IsNullOrEmpty_ShouldReturnTrue_GivenEmptyString() + { + Assert.IsTrue("".IsNullOrEmpty()); + Assert.IsTrue(string.Empty.IsNullOrEmpty()); + } + + [TestMethod] + public void IsNullOrEmpty_ShouldReturnFalse_GivenWhiteSpaceString() + { + Assert.IsFalse(" ".IsNullOrEmpty()); + } + + [TestMethod] + public void IsNullOrEmpty_ShouldReturnFalse_GivenNonEmptyString() + { + Assert.IsFalse("Hello World".IsNullOrEmpty()); + } + + [TestMethod] + public void IsNullOrEmpty_ShouldReturnTrue_GivenNullString() + { + string? value = null; + Assert.IsTrue(value.IsNullOrEmpty()); + } + + [TestMethod] + public void IsNullOrWhiteSpace_ShouldReturnTrue_GivenEmptyString() + { + Assert.IsTrue("".IsNullOrWhiteSpace()); + Assert.IsTrue(string.Empty.IsNullOrWhiteSpace()); + } + + [TestMethod] + public void IsNullOrWhiteSpace_ShouldReturnTrue_GivenWhiteSpaceString() + { + Assert.IsTrue(" ".IsNullOrWhiteSpace()); + } + + [TestMethod] + public void IsNullOrWhiteSpace_ShouldReturnFalse_GivenNonEmptyString() + { + Assert.IsFalse("Hello World".IsNullOrWhiteSpace()); + } + + [TestMethod] + public void IsNullOrWhiteSpace_ShouldReturnTrue_GivenNullString() + { + string? value = null; + Assert.IsTrue(value.IsNullOrWhiteSpace()); + } + [TestMethod] public void IsPalindrome_ShouldBeCorrect_GivenString() { @@ -317,6 +545,32 @@ public class StringTests Assert.ThrowsException(() => value!.IsUpper()); } + [TestMethod] + public void IsWhiteSpace_ShouldReturnTrue_GivenEmptyString() + { + Assert.IsTrue("".IsWhiteSpace()); + Assert.IsTrue(string.Empty.IsWhiteSpace()); + } + + [TestMethod] + public void IsWhiteSpace_ShouldReturnTrue_GivenWhiteSpaceString() + { + Assert.IsTrue(" ".IsWhiteSpace()); + } + + [TestMethod] + public void IsWhiteSpace_ShouldReturnFalse_GivenNonEmptyString() + { + Assert.IsFalse("Hello World".IsWhiteSpace()); + } + + [TestMethod] + public void IsWhiteSpace_ShouldThrowArgumentNullException_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.IsWhiteSpace()); + } + [TestMethod] public void Randomize_ShouldReorder_GivenString() { @@ -450,6 +704,66 @@ public class StringTests Assert.ThrowsException(() => value!.Split(0).ToArray()); } + [TestMethod] + public void StartsWithAny_ShouldReturnFalse_GivenNullInput() + { + string? value = null; + Assert.IsFalse(value.StartsWithAny()); + Assert.IsFalse(value.StartsWithAny(StringComparison.Ordinal)); + } + + [TestMethod] + public void StartsWithAny_ShouldReturnFalse_GivenEmptyInput() + { + Assert.IsFalse(string.Empty.StartsWithAny()); + Assert.IsFalse(string.Empty.StartsWithAny(StringComparison.Ordinal)); + } + + [TestMethod] + public void StartsWithAny_ShouldReturnFalse_GivenValuesThatDontMatch() + { + const string value = "Hello World"; + Assert.IsFalse(value.StartsWithAny("World", "Goodbye")); + Assert.IsFalse(value.StartsWithAny(StringComparison.Ordinal, "World", "Goodbye")); + } + + [TestMethod] + public void StartsWithAny_ShouldReturnTrue_GivenValuesThatMatch() + { + const string value = "Hello World"; + Assert.IsTrue(value.StartsWithAny("World", "Hello")); + Assert.IsTrue(value.StartsWithAny(StringComparison.Ordinal, "World", "Hello")); + } + + [TestMethod] + public void StartsWithAny_ShouldReturnTrue_GivenValuesThatMatch_WithIgnoreCaseComparison() + { + const string value = "Hello World"; + Assert.IsTrue(value.StartsWithAny(StringComparison.OrdinalIgnoreCase, "WORLD", "HELLO")); + } + + [TestMethod] + public void StartsWithAny_ShouldReturnFalse_GivenEmptyValues() + { + const string input = "Hello World"; + Assert.IsFalse(input.StartsWithAny()); + } + + [TestMethod] + public void StartsWithAny_ShouldThrowArgumentNullException_GivenNullValues() + { + Assert.ThrowsException(() => string.Empty.StartsWithAny(null!)); + Assert.ThrowsException(() => string.Empty.StartsWithAny(StringComparison.Ordinal, null!)); + } + + [TestMethod] + public void StartsWithAny_ShouldThrowArgumentNullException_GivenANullValue() + { + var values = new[] {"Hello", null!, "World"}; + Assert.ThrowsException(() => "Foobar".StartsWithAny(values)); + Assert.ThrowsException(() => "Foobar".StartsWithAny(StringComparison.Ordinal, values)); + } + [TestMethod] public void WithEmptyAlternative_ShouldBeCorrect() { diff --git a/X10D.Tests/src/Time/CharSpanTests.cs b/X10D.Tests/src/Time/CharSpanTests.cs new file mode 100644 index 0000000..6a3f4cf --- /dev/null +++ b/X10D.Tests/src/Time/CharSpanTests.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Time; + +namespace X10D.Tests.Time; + +[TestClass] +public class CharSpanTests +{ + [TestMethod] + public void ToTimeSpan_ShouldReturnCorrectTimeSpan_GivenSpanOfCharacters() + { + ReadOnlySpan value = "1y 1mo 1w 1d 1h 1m 1s 1ms".AsSpan(); + + TimeSpan expected = TimeSpan.FromMilliseconds(1); + expected += TimeSpan.FromSeconds(1); + expected += TimeSpan.FromMinutes(1); + expected += TimeSpan.FromHours(1); + expected += TimeSpan.FromDays(1); + expected += TimeSpan.FromDays(7); + expected += TimeSpan.FromDays(30); + expected += TimeSpan.FromDays(365); + + Assert.AreEqual(expected, value.ToTimeSpan()); + } + + [TestMethod] + public void ToTimeSpan_ShouldReturnZero_GivenInvalidSpanOfCharacters() + { + Assert.AreEqual(TimeSpan.Zero, "Hello World".AsSpan().ToTimeSpan()); + } +} diff --git a/X10D.Tests/src/Time/DateTimeOffsetTests.cs b/X10D.Tests/src/Time/DateTimeOffsetTests.cs index 4deba4b..971d58d 100644 --- a/X10D.Tests/src/Time/DateTimeOffsetTests.cs +++ b/X10D.Tests/src/Time/DateTimeOffsetTests.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Time; namespace X10D.Tests.Time; @@ -7,12 +7,36 @@ namespace X10D.Tests.Time; public class DateTimeOffsetTests { [TestMethod] - public void Age_ShouldBeDifference_Given1Jan2000() + public void Age_ShouldBe17_Given31December1991Birthday_And30December2017Date() { - DateTimeOffset birthday = new DateTime(2000, 1, 1); - DateTimeOffset today = DateTime.Now.Date; + var reference = new DateTime(2017, 12, 30); + DateTimeOffset birthday = new DateTime(1999, 12, 31); - Assert.AreEqual(today.Year - birthday.Year, birthday.Age()); + int age = birthday.Age(reference); + + Assert.AreEqual(17, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And1January2018Date() + { + var reference = new DateTime(2018, 1, 1); + DateTimeOffset birthday = new DateTime(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And31December2017Date() + { + var reference = new DateTime(2017, 12, 31); + DateTimeOffset birthday = new DateTime(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); } [TestMethod] @@ -32,9 +56,37 @@ public class DateTimeOffsetTests [TestMethod] public void FirstDayOfMonth_ShouldBe1st_GivenToday() { - DateTimeOffset today = DateTime.Now.Date; + DateTimeOffset today = DateTime.UtcNow.Date; + var first = new DateTimeOffset(today.Year, today.Month, 1, 0, 0, 0, today.Offset); - Assert.AreEqual(new DateTime(today.Year, today.Month, 1), today.FirstDayOfMonth()); + Assert.AreEqual(first, today.FirstDayOfMonth()); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given4January1970() + { + DateTimeOffset date = new DateTime(1970, 1, 4); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given31December1969() + { + DateTimeOffset date = new DateTime(1969, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn53_Given31December1970() + { + DateTimeOffset date = new DateTime(1970, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(53, iso8601WeekOfYear); } [TestMethod] diff --git a/X10D.Tests/src/Time/DateTimeTests.cs b/X10D.Tests/src/Time/DateTimeTests.cs index b6c3174..7fc9f50 100644 --- a/X10D.Tests/src/Time/DateTimeTests.cs +++ b/X10D.Tests/src/Time/DateTimeTests.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Time; namespace X10D.Tests.Time; @@ -7,12 +7,36 @@ namespace X10D.Tests.Time; public class DateTimeTests { [TestMethod] - public void Age_ShouldBeDifference_Given1Jan2000() + public void Age_ShouldBe17_Given31December1991Birthday_And30December2017Date() { - var birthday = new DateTime(2000, 1, 1); - DateTime today = DateTime.Now.Date; + var reference = new DateTime(2017, 12, 30); + var birthday = new DateTime(1999, 12, 31); - Assert.AreEqual(today.Year - birthday.Year, birthday.Age()); + int age = birthday.Age(reference); + + Assert.AreEqual(17, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And1January2018Date() + { + var reference = new DateTime(2018, 1, 1); + var birthday = new DateTime(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And31December2017Date() + { + var reference = new DateTime(2017, 12, 31); + var birthday = new DateTime(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); } [TestMethod] @@ -37,6 +61,33 @@ public class DateTimeTests Assert.AreEqual(new DateTime(today.Year, today.Month, 1), today.FirstDayOfMonth()); } + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given4January1970() + { + var date = new DateTime(1970, 1, 4); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given31December1969() + { + var date = new DateTime(1969, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn53_Given31December1970() + { + var date = new DateTime(1970, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(53, iso8601WeekOfYear); + } + [TestMethod] public void IsLeapYear_ShouldBeFalse_Given1999() { diff --git a/X10D.Tests/src/Time/TimeSpanParserTests.cs b/X10D.Tests/src/Time/TimeSpanParserTests.cs index b671070..f1ba85c 100644 --- a/X10D.Tests/src/Time/TimeSpanParserTests.cs +++ b/X10D.Tests/src/Time/TimeSpanParserTests.cs @@ -7,9 +7,58 @@ namespace X10D.Tests.Time; public class TimeSpanParserTests { [TestMethod] - public void TryParse_ShouldThrow_GivenNullString() + public void TryParse_ShouldReturnTrue_GivenWellFormedTimeSpan() { - string? value = null; - Assert.ThrowsException(() => TimeSpanParser.TryParse(value!, out _)); + bool result = TimeSpanParser.TryParse("3d6h", out TimeSpan timeSpan); + Assert.IsTrue(result); + Assert.AreEqual(TimeSpan.FromDays(3) + TimeSpan.FromHours(6), timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenMalformedTimeSpan() + { + bool result = TimeSpanParser.TryParse("asdf", out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenEmptySpan() + { + bool result = TimeSpanParser.TryParse(ReadOnlySpan.Empty, out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenWhiteSpaceSpan() + { + bool result = TimeSpanParser.TryParse(" ".AsSpan(), out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenEmptyString() + { + bool result = TimeSpanParser.TryParse(string.Empty, out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenWhiteSpaceString() + { + bool result = TimeSpanParser.TryParse(" ", out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); + } + + [TestMethod] + public void TryParse_ShouldReturnFalse_GivenNull() + { + bool result = TimeSpanParser.TryParse(null, out TimeSpan timeSpan); + Assert.IsFalse(result); + Assert.AreEqual(default, timeSpan); } } diff --git a/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity b/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity new file mode 100644 index 0000000..c1a5fbe --- /dev/null +++ b/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity @@ -0,0 +1,354 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &192863441 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 192863443} + - component: {fileID: 192863442} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &192863442 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 192863441} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0fac6b15ed0b420ba300fc1ac10ef01a, type: 3} + m_Name: + m_EditorClassIdentifier: + _hexagonPoints: + - {x: -0.5, y: 0.5} + - {x: -0.25, y: 1} + - {x: 0.25, y: 1} + - {x: 0.5, y: 0.5} + - {x: 0.25, y: 0} + - {x: -0.25, y: 0} +--- !u!4 &192863443 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 192863441} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &585803459 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 585803461} + - component: {fileID: 585803460} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &585803460 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 585803459} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &585803461 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 585803459} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1189625736 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1189625739} + - component: {fileID: 1189625738} + - component: {fileID: 1189625737} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1189625737 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1189625736} + m_Enabled: 1 +--- !u!20 &1189625738 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1189625736} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1189625739 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1189625736} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity.meta b/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity.meta new file mode 100644 index 0000000..90ba21a --- /dev/null +++ b/X10D.Unity.Tests/Assets/Scenes/DebugExIntegrationTests.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f2337eeeb085a25408461d996bb20a9c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity b/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity new file mode 100644 index 0000000..ca0f32a --- /dev/null +++ b/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity @@ -0,0 +1,347 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &736700400 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 736700402} + - component: {fileID: 736700401} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &736700401 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 736700400} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 67d53e2f993d4a5ba0eb34431d1846cd, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!4 &736700402 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 736700400} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1077233431 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1077233433} + - component: {fileID: 1077233432} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &1077233432 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1077233431} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &1077233433 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1077233431} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1698122894 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1698122897} + - component: {fileID: 1698122896} + - component: {fileID: 1698122895} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1698122895 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1698122894} + m_Enabled: 1 +--- !u!20 &1698122896 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1698122894} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1698122897 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1698122894} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity.meta b/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity.meta new file mode 100644 index 0000000..3499fd2 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Scenes/YieldInstructionIntegrationTests.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b95b5f3924bd65b4bb0b7703abdd4fe5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs b/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs new file mode 100644 index 0000000..1c625e5 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs @@ -0,0 +1,40 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Drawing; +using Color = UnityEngine.Color; + +namespace X10D.Unity.Tests +{ + internal sealed class DebugUtilityIntegrationTests : MonoBehaviour + { + private void Update() + { + DebugUtility.DrawLine(Vector3.zero, Vector3.right, Color.red); + DebugUtility.DrawLine(Vector3.zero, Vector3.up, Color.green); + DebugUtility.DrawLine(Vector3.zero, Vector3.forward, Color.blue); + + DebugUtility.DrawWireCube(new Vector3(1.5f, 0.5f, 0), Vector3.one * 0.5f, Color.yellow); + DebugUtility.DrawRectangle(new Vector2(-1.5f, 0.5f), Vector2.one * -0.5f, Color.cyan); + + var circle = new CircleF(0.0f, 0.0f, 0.5f); + DebugUtility.DrawCircle(circle, 25, new Vector2(-3.0f, 0.5f), Color.magenta); + + var ellipse = new EllipseF(0.0f, 0.0f, 1.0f, 0.5f); + DebugUtility.DrawEllipse(ellipse, 25, new Vector2(0.0f, 1.5f), Color.white); + + var hexagon = new PolygonF(); + hexagon.AddVertex(new Vector2(-0.5f, 0.5f)); + hexagon.AddVertex(new Vector2(-0.25f, 1.0f)); + hexagon.AddVertex(new Vector2(0.25f, 1.0f)); + hexagon.AddVertex(new Vector2(0.5f, 0.5f)); + hexagon.AddVertex(new Vector2(0.25f, 0)); + hexagon.AddVertex(new Vector2(-0.25f, 0)); + DebugUtility.DrawPolygon(hexagon, new Vector2(3.0f, 0.0f), Color.white); + + var sphere = new Sphere(System.Numerics.Vector3.Zero, 0.5f); + DebugUtility.DrawSphere(sphere, 25, new Vector2(0.0f, -1.5f), Color.white); + + DebugUtility.Assert(true); + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs.meta new file mode 100644 index 0000000..197dc9b --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/DebugUtilityIntegrationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0fac6b15ed0b420ba300fc1ac10ef01a +timeCreated: 1654080788 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/Color32Tests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/Color32Tests.cs index ec25662..3667c4a 100644 --- a/X10D.Unity.Tests/Assets/Tests/Drawing/Color32Tests.cs +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/Color32Tests.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -8,14 +9,53 @@ namespace X10D.Unity.Tests.Drawing { public class Color32Tests { - private static readonly Color32 Black = new(0, 0, 0, 1); - private static readonly Color32 White = new(255, 255, 255, 1); - private static readonly Color32 Red = new(255, 0, 0, 1); - private static readonly Color32 Green = new(0, 255, 0, 1); - private static readonly Color32 Blue = new(0, 0, 255, 1); - private static readonly Color32 Cyan = new(0, 255, 255, 1); - private static readonly Color32 Magenta = new(255, 0, 255, 1); - private static readonly Color32 Yellow = new(255, 255, 0, 1); + private static readonly Color32 Black = new(0, 0, 0, 255); + private static readonly Color32 White = new(255, 255, 255, 255); + private static readonly Color32 Red = new(255, 0, 0, 255); + private static readonly Color32 Green = new(0, 255, 0, 255); + private static readonly Color32 Blue = new(0, 0, 255, 255); + private static readonly Color32 Cyan = new(0, 255, 255, 255); + private static readonly Color32 Magenta = new(255, 0, 255, 255); + private static readonly Color32 Yellow = new(255, 255, 0, 255); + + [UnityTest] + public IEnumerator Deconstruct_ShouldDeconstruct_ToCorrectValues() + { + byte a, r, g, b; + + (r, g, b) = White; + Assert.AreEqual(255, r); + Assert.AreEqual(255, g); + Assert.AreEqual(255, b); + + (a, r, g, b) = Yellow; + Assert.AreEqual(255, a); + Assert.AreEqual(255, r); + Assert.AreEqual(255, g); + Assert.AreEqual(0, b); + + yield break; + } + + [UnityTest] + public IEnumerator GetClosestConsoleColor_ShouldReturnClosestColor_GivenValidColor() + { + // I know it's just casting... but aim for 100% coverage babyyyy + + Assert.AreEqual(ConsoleColor.Red, ((Color32)Color.red).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, ((Color32)Color.green).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Blue, ((Color32)Color.blue).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, ((Color32)Color.white).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Black, ((Color32)Color.black).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, ((Color32)Color.yellow).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, ((Color32)Color.cyan).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Magenta, ((Color32)Color.magenta).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, ((Color32)Color.gray).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, ((Color32)Color.grey).GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Black, ((Color32)Color.clear).GetClosestConsoleColor()); + + yield break; + } [UnityTest] public IEnumerator Inverted_ShouldReturnInvertedColor() @@ -43,6 +83,28 @@ namespace X10D.Unity.Tests.Drawing yield break; } + [UnityTest] + public IEnumerator ToSystemDrawingColor_ShouldReturnEquivalentColor() + { + System.Drawing.Color expected = System.Drawing.Color.FromArgb(255, 255, 255); + System.Drawing.Color actual = White.ToSystemDrawingColor(); + + Assert.AreEqual(expected, actual); + + yield break; + } + + [UnityTest] + public IEnumerator ToUnityColor32_ShouldReturnEquivalentColor() + { + Color32 expected = White; + Color32 actual = System.Drawing.Color.FromArgb(255, 255, 255).ToUnityColor32(); + + Assert.AreEqual(expected, actual); + + yield break; + } + [UnityTest] public IEnumerator WithA0_ShouldReturnSameColor_GivenWhite() { diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/ColorTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/ColorTests.cs index c27ad2c..78031d4 100644 --- a/X10D.Unity.Tests/Assets/Tests/Drawing/ColorTests.cs +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/ColorTests.cs @@ -1,4 +1,7 @@ -using System.Collections; +using System; +using System.Collections; +using System.Linq; +using System.Reflection; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -17,6 +20,43 @@ namespace X10D.Unity.Tests.Drawing private static readonly Color Magenta = new(1, 0, 1); private static readonly Color Yellow = new(1, 1, 0); + [UnityTest] + public IEnumerator Deconstruct_ShouldDeconstruct_ToCorrectValues() + { + float a, r, g, b; + + (r, g, b) = White; + Assert.AreEqual(1.0f, r); + Assert.AreEqual(1.0f, g); + Assert.AreEqual(1.0f, b); + + (a, r, g, b) = Yellow; + Assert.AreEqual(1.0f, a); + Assert.AreEqual(1.0f, r); + Assert.AreEqual(1.0f, g); + Assert.AreEqual(0.0f, b); + + yield break; + } + + [UnityTest] + public IEnumerator GetClosestConsoleColor_ShouldReturnClosestColor_GivenValidColor() + { + Assert.AreEqual(ConsoleColor.Red, Color.red.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Green, Color.green.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Blue, Color.blue.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.White, Color.white.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Black, Color.black.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Yellow, Color.yellow.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Cyan, Color.cyan.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Magenta, Color.magenta.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.gray.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Gray, Color.grey.GetClosestConsoleColor()); + Assert.AreEqual(ConsoleColor.Black, Color.clear.GetClosestConsoleColor()); + + yield break; + } + [UnityTest] public IEnumerator Inverted_ShouldReturnInvertedColor() { @@ -43,6 +83,28 @@ namespace X10D.Unity.Tests.Drawing yield break; } + [UnityTest] + public IEnumerator ToSystemDrawingColor_ShouldReturnEquivalentColor() + { + System.Drawing.Color expected = System.Drawing.Color.FromArgb(255, 255, 255); + System.Drawing.Color actual = White.ToSystemDrawingColor(); + + Assert.AreEqual(expected, actual); + + yield break; + } + + [UnityTest] + public IEnumerator ToUnityColor_ShouldReturnEquivalentColor() + { + Color expected = White; + Color actual = System.Drawing.Color.FromArgb(255, 255, 255).ToUnityColor(); + + Assert.AreEqual(expected, actual); + + yield break; + } + [UnityTest] public IEnumerator WithA0_ShouldReturnSameColor_GivenWhite() { diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs new file mode 100644 index 0000000..83d70ea --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Core; +using X10D.Unity.Drawing; + +namespace X10D.Unity.Tests.Drawing +{ + public class PointFTests + { + [UnityTest] + public IEnumerator ToUnityVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var point = new PointF(random.NextSingle(), random.NextSingle()); + var vector = point.ToUnityVector2(); + + Assert.AreEqual(point.X, vector.x, 1e-6f); + Assert.AreEqual(point.Y, vector.y, 1e-6f); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs.meta new file mode 100644 index 0000000..751b242 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/PointFTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d90695756d1d4760aef2523486b1b41e +timeCreated: 1653743243 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs new file mode 100644 index 0000000..36b8554 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Unity.Drawing; + +namespace X10D.Unity.Tests.Drawing +{ + public class PointTests + { + [UnityTest] + public IEnumerator ToUnityVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var point = new Point(random.Next(), random.Next()); + var vector = point.ToUnityVector2(); + + Assert.AreEqual(point.X, vector.x); + Assert.AreEqual(point.Y, vector.y); + + yield break; + } + + [UnityTest] + public IEnumerator ToUnityVector2Int_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var point = new Point(random.Next(), random.Next()); + var vector = point.ToUnityVector2Int(); + + Assert.AreEqual(point.X, vector.x); + Assert.AreEqual(point.Y, vector.y); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs.meta new file mode 100644 index 0000000..8a5fdf4 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/PointTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f465794fdc394d05a34229f34e5199e2 +timeCreated: 1653742987 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs new file mode 100644 index 0000000..3bf96b6 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs @@ -0,0 +1,42 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using X10D.Unity.Drawing; +using Random = System.Random; + +namespace X10D.Unity.Tests.Drawing +{ + public class RectIntTests + { + [UnityTest] + public IEnumerator ToSystemRectangle_ShouldReturnRectangleF_WithEquivalentMembers() + { + var random = new Random(); + var rect = new RectInt(random.Next(), random.Next(), random.Next(), random.Next()); + var rectangle = rect.ToSystemRectangle(); + + Assert.AreEqual(rect.x, rectangle.X); + Assert.AreEqual(rect.y, rectangle.Y); + Assert.AreEqual(rect.width, rectangle.Width); + Assert.AreEqual(rect.height, rectangle.Height); + + yield break; + } + + [UnityTest] + public IEnumerator ToSystemRectangleF_ShouldReturnRectangleF_WithEquivalentMembers() + { + var random = new Random(); + var rect = new RectInt(random.Next(), random.Next(), random.Next(), random.Next()); + var rectangle = rect.ToSystemRectangleF(); + + Assert.AreEqual(rect.x, rectangle.X); + Assert.AreEqual(rect.y, rectangle.Y); + Assert.AreEqual(rect.width, rectangle.Width); + Assert.AreEqual(rect.height, rectangle.Height); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs.meta new file mode 100644 index 0000000..462b6b5 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectIntTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 18f2e8fbc200475ca5fe7857a457a874 +timeCreated: 1654077768 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs new file mode 100644 index 0000000..eb5a94d --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs @@ -0,0 +1,28 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using X10D.Core; +using X10D.Unity.Drawing; +using Random = System.Random; + +namespace X10D.Unity.Tests.Drawing +{ + public class RectTests + { + [UnityTest] + public IEnumerator ToSystemRectangleF_ShouldReturnRectangleF_WithEquivalentMembers() + { + var random = new Random(); + var rect = new Rect(random.NextSingle(), random.NextSingle(), random.NextSingle(), random.NextSingle()); + var rectangle = rect.ToSystemRectangleF(); + + Assert.AreEqual(rect.x, rectangle.X, 1e-6f); + Assert.AreEqual(rect.y, rectangle.Y, 1e-6f); + Assert.AreEqual(rect.width, rectangle.Width, 1e-6f); + Assert.AreEqual(rect.height, rectangle.Height, 1e-6f); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs.meta new file mode 100644 index 0000000..ee14568 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bb1ec5372c354f06b39e03649b9307db +timeCreated: 1653743583 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs new file mode 100644 index 0000000..32676c4 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs @@ -0,0 +1,28 @@ +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Core; +using X10D.Unity.Drawing; +using Random = System.Random; + +namespace X10D.Unity.Tests.Drawing +{ + public class RectangleFTests + { + [UnityTest] + public IEnumerator ToUnityRect_ShouldReturnRect_WithEquivalentMembers() + { + var random = new Random(); + var rectangle = new RectangleF(random.NextSingle(), random.NextSingle(), random.NextSingle(), random.NextSingle()); + var rect = rectangle.ToUnityRect(); + + Assert.AreEqual(rectangle.X, rect.x, 1e-6f); + Assert.AreEqual(rectangle.Y, rect.y, 1e-6f); + Assert.AreEqual(rectangle.Width, rect.width, 1e-6f); + Assert.AreEqual(rectangle.Height, rect.height, 1e-6f); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs.meta new file mode 100644 index 0000000..8cec772 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleFTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f38cbc892021405cad2b52de1f960a00 +timeCreated: 1653743640 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs new file mode 100644 index 0000000..632e5d6 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs @@ -0,0 +1,42 @@ +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Unity.Drawing; +using Random = System.Random; + +namespace X10D.Unity.Tests.Drawing +{ + public class RectangleTests + { + [UnityTest] + public IEnumerator ToUnityRect_ShouldReturnRect_WithEquivalentMembers() + { + var random = new Random(); + var rectangle = new Rectangle(random.Next(), random.Next(), random.Next(), random.Next()); + var rect = rectangle.ToUnityRect(); + + Assert.AreEqual(rectangle.X, rect.x); + Assert.AreEqual(rectangle.Y, rect.y); + Assert.AreEqual(rectangle.Width, rect.width); + Assert.AreEqual(rectangle.Height, rect.height); + + yield break; + } + + [UnityTest] + public IEnumerator ToUnityRectInt_ShouldReturnRect_WithEquivalentMembers() + { + var random = new Random(); + var rectangle = new Rectangle(random.Next(), random.Next(), random.Next(), random.Next()); + var rect = rectangle.ToUnityRectInt(); + + Assert.AreEqual(rectangle.X, rect.x); + Assert.AreEqual(rectangle.Y, rect.y); + Assert.AreEqual(rectangle.Width, rect.width); + Assert.AreEqual(rectangle.Height, rect.height); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs.meta new file mode 100644 index 0000000..2551e69 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/RectangleTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9c74177035d1452a8a7ca08c0a27124b +timeCreated: 1653743677 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs new file mode 100644 index 0000000..e677867 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Core; +using X10D.Unity.Drawing; + +namespace X10D.Unity.Tests.Drawing +{ + public class SizeFTests + { + [UnityTest] + public IEnumerator ToUnityVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var size = new SizeF(random.NextSingle(), random.NextSingle()); + var vector = size.ToUnityVector2(); + + Assert.AreEqual(size.Width, vector.x, 1e-6f); + Assert.AreEqual(size.Height, vector.y, 1e-6f); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs.meta new file mode 100644 index 0000000..ac99418 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeFTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b93fe56510de4ddcb9354bde7f10c362 +timeCreated: 1653743377 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs new file mode 100644 index 0000000..0832375 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using System.Drawing; +using NUnit.Framework; +using UnityEngine.TestTools; +using X10D.Unity.Drawing; + +namespace X10D.Unity.Tests.Drawing +{ + public class SizeTests + { + [UnityTest] + public IEnumerator ToUnityVector2_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var size = new Size(random.Next(), random.Next()); + var vector = size.ToUnityVector2(); + + Assert.AreEqual(size.Width, vector.x); + Assert.AreEqual(size.Height, vector.y); + + yield break; + } + + [UnityTest] + public IEnumerator ToUnityVector2Int_ShouldReturnVector_WithEquivalentMembers() + { + var random = new Random(); + var size = new Size(random.Next(), random.Next()); + var vector = size.ToUnityVector2Int(); + + Assert.AreEqual(size.Width, vector.x); + Assert.AreEqual(size.Height, vector.y); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs.meta new file mode 100644 index 0000000..572e5f7 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Drawing/SizeTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c748bfe02fce4b459df7ef2779c2a486 +timeCreated: 1653743400 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs new file mode 100644 index 0000000..eb1b217 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs @@ -0,0 +1,88 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using X10D.Unity.Numerics; +using Random = System.Random; + +namespace X10D.Unity.Tests.Numerics +{ + public class Vector2IntTests + { + [UnityTest] + public IEnumerator Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector2Int(1, 2); + (int x, int y) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + + yield break; + } + + [UnityTest] + public IEnumerator ToSystemPoint_ShouldReturnPoint_WithEquivalentMembers() + { + var random = new Random(); + int x = random.Next(); + int y = random.Next(); + + var vector = new Vector2Int(x, y); + var point = vector.ToSystemPoint(); + + Assert.AreEqual(vector.x, point.X); + Assert.AreEqual(vector.y, point.Y); + + yield break; + } + + [UnityTest] + public IEnumerator ToSystemSize_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + int x = random.Next(); + int y = random.Next(); + + var vector = new Vector2Int(x, y); + var point = vector.ToSystemSize(); + + Assert.AreEqual(vector.x, point.Width); + Assert.AreEqual(vector.y, point.Height); + + yield break; + } + + [UnityTest] + public IEnumerator WithX_ShouldReturnVectorWithNewX_GivenVector() + { + Assert.AreEqual(Vector2Int.up, Vector2Int.one.WithX(0)); + Assert.AreEqual(Vector2Int.zero, Vector2Int.zero.WithX(0)); + Assert.AreEqual(Vector2Int.zero, Vector2Int.right.WithX(0)); + Assert.AreEqual(Vector2Int.up, Vector2Int.up.WithX(0)); + + Assert.AreEqual(Vector2Int.one, Vector2Int.one.WithX(1)); + Assert.AreEqual(Vector2Int.right, Vector2Int.zero.WithX(1)); + Assert.AreEqual(Vector2Int.right, Vector2Int.right.WithX(1)); + Assert.AreEqual(Vector2Int.one, Vector2Int.up.WithX(1)); + + yield break; + } + + [UnityTest] + public IEnumerator WithY_ShouldReturnVectorWithNewY_GivenVector() + { + Assert.AreEqual(Vector2Int.right, Vector2Int.one.WithY(0)); + Assert.AreEqual(Vector2Int.zero, Vector2Int.zero.WithY(0)); + Assert.AreEqual(Vector2Int.right, Vector2Int.right.WithY(0)); + Assert.AreEqual(Vector2Int.zero, Vector2Int.up.WithY(0)); + + Assert.AreEqual(Vector2Int.one, Vector2Int.one.WithY(1)); + Assert.AreEqual(Vector2Int.up, Vector2Int.zero.WithY(1)); + Assert.AreEqual(Vector2Int.one, Vector2Int.right.WithY(1)); + Assert.AreEqual(Vector2Int.up, Vector2Int.up.WithY(1)); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs.meta new file mode 100644 index 0000000..7a62c97 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2IntTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ad987e96afa849e6b0626ba7d7720f7b +timeCreated: 1653993201 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2Tests.cs b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2Tests.cs index 3fc09e2..3be36d0 100644 --- a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2Tests.cs +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector2Tests.cs @@ -10,6 +10,74 @@ namespace X10D.Unity.Tests.Numerics { public class Vector2Tests { + [UnityTest] + public IEnumerator Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector2(1, 2); + (float x, float y) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector2(1.5f, 2.6f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.x); + Assert.AreEqual(3, rounded.y); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector2(1.5f, 25.2f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.x); + Assert.AreEqual(30, rounded.y); + + yield break; + } + + [UnityTest] + public IEnumerator ToSystemPointF_ShouldReturnPoint_WithEquivalentMembers() + { + var random = new Random(); + float x = random.NextSingle(); + float y = random.NextSingle(); + + var vector = new Vector2(x, y); + var point = vector.ToSystemPointF(); + + Assert.AreEqual(vector.x, point.X, 1e-6f); + Assert.AreEqual(vector.y, point.Y, 1e-6f); + + yield break; + } + + [UnityTest] + public IEnumerator ToSystemSizeF_ShouldReturnSize_WithEquivalentMembers() + { + var random = new Random(); + float x = random.NextSingle(); + float y = random.NextSingle(); + + var vector = new Vector2(x, y); + var point = vector.ToSystemSizeF(); + + Assert.AreEqual(vector.x, point.Width, 1e-6f); + Assert.AreEqual(vector.y, point.Height, 1e-6f); + + yield break; + } + [UnityTest] public IEnumerator ToSystemVector_ShouldReturnVector_WithEqualComponents() { diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs new file mode 100644 index 0000000..21bf438 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs @@ -0,0 +1,78 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using X10D.Unity.Numerics; + +namespace X10D.Unity.Tests.Numerics +{ + public class Vector3IntTests + { + [UnityTest] + public IEnumerator Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector3Int(1, 2, 3); + (float x, float y, float z) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + Assert.AreEqual(3, z); + + yield break; + } + + [UnityTest] + public IEnumerator WithX_ShouldReturnVectorWithNewX_GivenVector() + { + Assert.AreEqual(new Vector3Int(0, 1, 1), Vector3Int.one.WithX(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.zero.WithX(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.right.WithX(0)); + Assert.AreEqual(Vector3Int.up, Vector3Int.up.WithX(0)); + Assert.AreEqual(Vector3Int.forward, Vector3Int.forward.WithX(0)); + + Assert.AreEqual(Vector3Int.one, Vector3Int.one.WithX(1)); + Assert.AreEqual(Vector3Int.right, Vector3Int.zero.WithX(1)); + Assert.AreEqual(Vector3Int.right, Vector3Int.right.WithX(1)); + Assert.AreEqual(new Vector3Int(1, 1, 0), Vector3Int.up.WithX(1)); + Assert.AreEqual(new Vector3Int(1, 0, 1), Vector3Int.forward.WithX(1)); + + yield break; + } + + [UnityTest] + public IEnumerator WithY_ShouldReturnVectorWithNewY_GivenVector() + { + Assert.AreEqual(new Vector3Int(1, 0, 1), Vector3Int.one.WithY(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.zero.WithY(0)); + Assert.AreEqual(Vector3Int.right, Vector3Int.right.WithY(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.up.WithY(0)); + Assert.AreEqual(Vector3Int.forward, Vector3Int.forward.WithY(0)); + + Assert.AreEqual(Vector3Int.one, Vector3Int.one.WithY(1)); + Assert.AreEqual(Vector3Int.up, Vector3Int.zero.WithY(1)); + Assert.AreEqual(new Vector3Int(1, 1, 0), Vector3Int.right.WithY(1)); + Assert.AreEqual(Vector3Int.up, Vector3Int.up.WithY(1)); + Assert.AreEqual(new Vector3Int(0, 1, 1), Vector3Int.forward.WithY(1)); + + yield break; + } + + [UnityTest] + public IEnumerator WithZ_ShouldReturnVectorWithNewZ_GivenVector() + { + Assert.AreEqual(new Vector3Int(1, 1, 0), Vector3Int.one.WithZ(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.zero.WithZ(0)); + Assert.AreEqual(Vector3Int.right, Vector3Int.right.WithZ(0)); + Assert.AreEqual(Vector3Int.up, Vector3Int.up.WithZ(0)); + Assert.AreEqual(Vector3Int.zero, Vector3Int.forward.WithZ(0)); + + Assert.AreEqual(Vector3Int.one, Vector3Int.one.WithZ(1)); + Assert.AreEqual(Vector3Int.forward, Vector3Int.zero.WithZ(1)); + Assert.AreEqual(new Vector3Int(1, 0, 1), Vector3Int.right.WithZ(1)); + Assert.AreEqual(new Vector3Int(0, 1, 1), Vector3Int.up.WithZ(1)); + Assert.AreEqual(Vector3Int.forward, Vector3Int.forward.WithZ(1)); + + yield break; + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs.meta new file mode 100644 index 0000000..1959799 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3IntTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e474d98fd3ee48159980aaa88040cfb3 +timeCreated: 1653993371 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3Tests.cs b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3Tests.cs index f14c7e3..08ebed9 100644 --- a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3Tests.cs +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector3Tests.cs @@ -10,6 +10,45 @@ namespace X10D.Unity.Tests.Numerics { public class Vector3Tests { + [UnityTest] + public IEnumerator Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector3(1, 2, 3); + (float x, float y, float z) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + Assert.AreEqual(3, z); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector3(1.5f, 2.6f, -5.2f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.x); + Assert.AreEqual(3, rounded.y); + Assert.AreEqual(-5, rounded.z); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector3(1.5f, 25.2f, -12.5f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.x); + Assert.AreEqual(30, rounded.y); + Assert.AreEqual(-10, rounded.z); + + yield break; + } + [UnityTest] public IEnumerator ToSystemVector_ShouldReturnVector_WithEqualComponents() { diff --git a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector4Tests.cs b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector4Tests.cs index d400512..b943406 100644 --- a/X10D.Unity.Tests/Assets/Tests/Numerics/Vector4Tests.cs +++ b/X10D.Unity.Tests/Assets/Tests/Numerics/Vector4Tests.cs @@ -10,6 +10,48 @@ namespace X10D.Unity.Tests.Numerics { public class Vector4Tests { + [UnityTest] + public IEnumerator Deconstruct_ShouldReturnCorrectValues() + { + var vector = new Vector4(1, 2, 3, 4); + (float x, float y, float z, float w) = vector; + + Assert.AreEqual(1, x); + Assert.AreEqual(2, y); + Assert.AreEqual(3, z); + Assert.AreEqual(4, w); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearestInteger_GivenNoParameters() + { + var vector = new Vector4(1.5f, 2.6f, -5.2f, 0.3f); + var rounded = vector.Round(); + + Assert.AreEqual(2, rounded.x); + Assert.AreEqual(3, rounded.y); + Assert.AreEqual(-5, rounded.z); + Assert.AreEqual(0, rounded.w); + + yield break; + } + + [UnityTest] + public IEnumerator Round_ShouldRoundToNearest10_GivenPrecision10() + { + var vector = new Vector4(1.5f, 25.2f, -12.5f, 101.2f); + var rounded = vector.Round(10); + + Assert.AreEqual(0, rounded.x); + Assert.AreEqual(30, rounded.y); + Assert.AreEqual(-10, rounded.z); + Assert.AreEqual(100, rounded.w); + + yield break; + } + [UnityTest] public IEnumerator ToSystemVector_ShouldReturnVector_WithEqualComponents() { diff --git a/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs b/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs new file mode 100644 index 0000000..90dc18e --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs @@ -0,0 +1,36 @@ +using System.Collections; +using UnityEngine; + +namespace X10D.Unity.Tests +{ + public class YieldInstructionIntegrationTests : MonoBehaviour + { + private void Start() + { + StartCoroutine(CO_WaitForAnyKeyDown()); + StartCoroutine(CO_WaitForSpaceKeyDown()); + StartCoroutine(CO_WaitForSpaceKeyUp()); + } + + private IEnumerator CO_WaitForAnyKeyDown() + { + Debug.Log("Waiting for any key to be pressed..."); + yield return new WaitForKeyDown(); + Debug.Log("Key was pressed!"); + } + + private IEnumerator CO_WaitForSpaceKeyDown() + { + Debug.Log("Waiting for Space key to be pressed..."); + yield return new WaitForKeyDown(KeyCode.Space); + Debug.Log("Space key was pressed!"); + } + + private IEnumerator CO_WaitForSpaceKeyUp() + { + Debug.Log("Waiting for Space key to be released..."); + yield return new WaitForKeyUp(KeyCode.Space); + Debug.Log("Space key was released!"); + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs.meta new file mode 100644 index 0000000..ae63f4d --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/YieldInstructionIntegrationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 67d53e2f993d4a5ba0eb34431d1846cd +timeCreated: 1657791682 \ No newline at end of file diff --git a/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs b/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs new file mode 100644 index 0000000..14893d0 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using UTime = UnityEngine.Time; + +namespace X10D.Unity.Tests +{ + public class YieldInstructionTests : MonoBehaviour + { + [UnityTest] + public IEnumerator WaitForFrames_ShouldYieldCorrectNumberOfFrames() + { + int frameCount = UTime.frameCount; + yield return new WaitForFrames(10); + Assert.AreEqual(frameCount + 10, UTime.frameCount, $"{frameCount + 10} == {UTime.frameCount}"); + } + + [UnityTest] + public IEnumerator WaitForSecondsNoAlloc_ShouldYieldForCorrectTime() + { + float time = UTime.time; + yield return new WaitForSecondsNoAlloc(2); + Assert.AreEqual(time + 2, UTime.time, 1e-2, $"{time + 2} == {UTime.time}"); + } + + [UnityTest] + public IEnumerator WaitForSecondsRealtimeNoAlloc_ShouldYieldForCorrectTime() + { + float time = UTime.time; + yield return new WaitForSecondsRealtimeNoAlloc(2); + Assert.AreEqual(time + 2, UTime.time, 1e-2, $"{time + 2} == {UTime.time}"); + } + + [UnityTest] + public IEnumerator WaitForTimeSpan_ShouldYieldForCorrectTime() + { + float time = UTime.time; + yield return new WaitForTimeSpan(TimeSpan.FromSeconds(2)); + if (System.Math.Abs(UTime.time - (time + 2)) < 1e-2) + { + Assert.Pass($"{time + 2} == {UTime.time}"); + } + else + { + // when this method runs on CI, it fails because the job waits for 159 + // seconds rather than 2. I have no idea why. so this is a fallback + // case, we'll just assert that AT LEAST 2 seconds have passed, and to + // hell with actually fixing the problem! + Assert.IsTrue(UTime.time > time + 1.98, $"{UTime.time} > {time + 2}"); + } + } + + [UnityTest] + public IEnumerator WaitForTimeSpanRealtime_ShouldYieldForCorrectTime() + { + float time = UTime.time; + yield return new WaitForTimeSpanRealtime(TimeSpan.FromSeconds(2)); + Assert.AreEqual(time + 2, UTime.time, 1e-2, $"{time + 2} == {UTime.time}"); + } + } +} diff --git a/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs.meta b/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs.meta new file mode 100644 index 0000000..ae81ce1 --- /dev/null +++ b/X10D.Unity.Tests/Assets/Tests/YieldInstructionTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7d35eefdf5b43278a6f6aa268a71091 +timeCreated: 1657795834 \ No newline at end of file diff --git a/X10D.Unity.Tests/Packages/manifest.json b/X10D.Unity.Tests/Packages/manifest.json index 6f13d5a..97fb71f 100644 --- a/X10D.Unity.Tests/Packages/manifest.json +++ b/X10D.Unity.Tests/Packages/manifest.json @@ -1,15 +1,15 @@ { "dependencies": { - "com.unity.collab-proxy": "1.15.17", + "com.unity.collab-proxy": "2.0.1", "com.unity.feature.development": "1.0.1", - "com.unity.ide.rider": "3.0.14", - "com.unity.ide.visualstudio": "2.0.15", + "com.unity.ide.rider": "3.0.18", + "com.unity.ide.visualstudio": "2.0.17", "com.unity.ide.vscode": "1.2.5", "com.unity.test-framework": "1.1.31", "com.unity.textmeshpro": "3.0.6", "com.unity.timeline": "1.6.4", "com.unity.ugui": "1.0.0", - "com.unity.visualscripting": "1.7.7", + "com.unity.visualscripting": "1.8.0", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/X10D.Unity.Tests/Packages/packages-lock.json b/X10D.Unity.Tests/Packages/packages-lock.json index d81f525..3172caf 100644 --- a/X10D.Unity.Tests/Packages/packages-lock.json +++ b/X10D.Unity.Tests/Packages/packages-lock.json @@ -1,12 +1,10 @@ { "dependencies": { "com.unity.collab-proxy": { - "version": "1.15.17", + "version": "2.0.1", "depth": 0, "source": "registry", - "dependencies": { - "com.unity.services.core": "1.0.1" - }, + "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.editorcoroutines": { @@ -28,17 +26,17 @@ "depth": 0, "source": "builtin", "dependencies": { - "com.unity.ide.visualstudio": "2.0.14", - "com.unity.ide.rider": "3.0.13", + "com.unity.ide.visualstudio": "2.0.17", + "com.unity.ide.rider": "3.0.18", "com.unity.ide.vscode": "1.2.5", "com.unity.editorcoroutines": "1.0.0", - "com.unity.performance.profile-analyzer": "1.1.1", + "com.unity.performance.profile-analyzer": "1.2.2", "com.unity.test-framework": "1.1.31", - "com.unity.testtools.codecoverage": "1.0.1" + "com.unity.testtools.codecoverage": "1.2.2" } }, "com.unity.ide.rider": { - "version": "3.0.14", + "version": "3.0.18", "depth": 0, "source": "registry", "dependencies": { @@ -47,7 +45,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.15", + "version": "2.0.17", "depth": 0, "source": "registry", "dependencies": { @@ -62,31 +60,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, - "com.unity.nuget.newtonsoft-json": { - "version": "3.0.2", - "depth": 2, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, "com.unity.performance.profile-analyzer": { - "version": "1.1.1", + "version": "1.2.2", "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, - "com.unity.services.core": { - "version": "1.3.1", - "depth": 1, - "source": "registry", - "dependencies": { - "com.unity.modules.unitywebrequest": "1.0.0", - "com.unity.nuget.newtonsoft-json": "3.0.2", - "com.unity.modules.androidjni": "1.0.0" - }, - "url": "https://packages.unity.com" - }, "com.unity.settings-manager": { "version": "1.0.3", "depth": 2, @@ -106,7 +86,7 @@ "url": "https://packages.unity.com" }, "com.unity.testtools.codecoverage": { - "version": "1.0.1", + "version": "1.2.2", "depth": 1, "source": "registry", "dependencies": { @@ -146,7 +126,7 @@ } }, "com.unity.visualscripting": { - "version": "1.7.7", + "version": "1.8.0", "depth": 0, "source": "registry", "dependencies": { diff --git a/X10D.Unity.Tests/ProjectSettings/ProjectVersion.txt b/X10D.Unity.Tests/ProjectSettings/ProjectVersion.txt index 3dcb827..bca3d02 100644 --- a/X10D.Unity.Tests/ProjectSettings/ProjectVersion.txt +++ b/X10D.Unity.Tests/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2021.3.2f1 -m_EditorVersionWithRevision: 2021.3.2f1 (d6360bedb9a0) +m_EditorVersion: 2021.3.21f1 +m_EditorVersionWithRevision: 2021.3.21f1 (1b156197d683) diff --git a/X10D.Unity/README.md b/X10D.Unity/README.md new file mode 100644 index 0000000..f0e6d4c --- /dev/null +++ b/X10D.Unity/README.md @@ -0,0 +1,36 @@ +

+

+GitHub Workflow Status + +MIT License +

+ +### About +X10D (pronounced *extend*), is a .NET package that provides extension methods for numerous types. The purpose of this library is to simplify a codebase by reducing the need for repeated code when performing common operations. Simplify your codebase. Take advantage of .NET. Use extension methods. + +*(I'm also [dogfooding](https://www.pcmag.com/encyclopedia/term/dogfooding) this library, so there's that.)* + + +### Preface +Parity with the main branch of X10D, and full .NET feature support, is planned. Unity plan to add CoreCLR and native NuGet support in the future, but no timeline is available. +For more information, see [this forum post](https://forum.unity.com/threads/unity-future-net-development-status.1092205/). + +## Installation +You must be using Unity 2021.3 LTS or later to add this package. +### Using the Unity Package Manager (UPM) +To install X10D in Unity, follow the steps blow: +1. Navigate to the [Package Manager window](https://docs.unity3d.com/Manual/upm-ui.html), under `Window > Package Manager` +2. Hit the `+` icon and select `Add package from git URL...` +3. Enter the following URL: https://github.com/oliverbooth/X10D.git#upm and hit the Add button +4. Profit! + +The [upm](https://github.com/oliverbooth/X10D/tree/upm) branch contains the latest nightly - that is the bleeding edge version of X10D. +If you'd like to remain on a stable release, specify a commit hash after the `#` instead of `upm`. +The latest current stable is 3.1.0, which is commit [9f3b09661b09e83690318370eea808e70dd4a72c](https://github.com/oliverbooth/X10D/commit/9f3b09661b09e83690318370eea808e70dd4a72c). +Keep in mind that referencing a specific commit rather than the `upm` branch will prevent the auto-updater in Unity from detecting new versions. + +## Contributing +Contributions are welcome. See [CONTRIBUTING.md](../CONTRIBUTING.md). + +## License +X10D is released under the MIT License. See [here](https://github.com/oliverbooth/X10D/blob/main/LICENSE.md) for more details. diff --git a/X10D.Unity/X10D.Unity.csproj b/X10D.Unity/X10D.Unity.csproj index 271cd77..efb745c 100644 --- a/X10D.Unity/X10D.Unity.csproj +++ b/X10D.Unity/X10D.Unity.csproj @@ -2,7 +2,7 @@ netstandard2.1 - 10.0 + 11.0 true true Oliver Booth @@ -11,14 +11,22 @@ git Extension methods on crack. LICENSE.md - icon.png + branding_Icon.png dotnet extension-methods + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) true - 3.1.0 + 3.2.0 enable true true + true + pdbonly + true + + + + true @@ -48,7 +56,7 @@ - + True diff --git a/X10D.Unity/X10D.Unity.csproj.DotSettings b/X10D.Unity/X10D.Unity.csproj.DotSettings new file mode 100644 index 0000000..cc2d8d0 --- /dev/null +++ b/X10D.Unity/X10D.Unity.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/X10D.Unity/branding_Unity.png b/X10D.Unity/branding_Unity.png new file mode 100644 index 0000000..c46d202 Binary files /dev/null and b/X10D.Unity/branding_Unity.png differ diff --git a/X10D.Unity/src/Assembly.cs b/X10D.Unity/src/Assembly.cs new file mode 100644 index 0000000..4e11466 --- /dev/null +++ b/X10D.Unity/src/Assembly.cs @@ -0,0 +1 @@ +[assembly: CLSCompliant(false)] diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Circle.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Circle.cs new file mode 100644 index 0000000..e5c6cb4 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Circle.cs @@ -0,0 +1,333 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Numerics; +using X10D.Unity.Numerics; +using Quaternion = System.Numerics.Quaternion; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a circle with the specified color. + /// + /// The center point of the circle. + /// The radius of the circle. + /// The number of segments to generate. + public static void DrawCircle(Vector2 center, float radius, int segments) + { + DrawCircle(center, radius, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The center point of the circle. + /// The radius of the circle. + /// The number of segments to generate. + /// The color of the circle. + public static void DrawCircle(Vector2 center, float radius, int segments, in Color color) + { + DrawCircle(center, radius, segments, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The center point of the circle. + /// The radius of the circle. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + public static void DrawCircle(Vector2 center, float radius, int segments, in Color color, float duration) + { + DrawCircle(center, radius, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The center point of the circle. + /// The radius of the circle. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(Vector2 center, float radius, int segments, in Color color, float duration, bool depthTest) + { + DrawCircle(center, radius, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a circle. + /// + /// The center point of the circle. + /// The radius of the circle. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(Vector2 center, float radius, int segments, in Vector3 offset, in Color color, float duration, + bool depthTest) + { + DrawCircle(new CircleF(center.ToSystemVector(), radius), segments, offset, color, duration, depthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + public static void DrawCircle(in Circle circle, int segments) + { + DrawCircle((CircleF)circle, segments, Vector2.zero, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + public static void DrawCircle(in Circle circle, int segments, in Vector3 offset) + { + DrawCircle((CircleF)circle, segments, offset, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + public static void DrawCircle(in Circle circle, int segments, in Color color) + { + DrawCircle((CircleF)circle, segments, Vector2.zero, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + public static void DrawCircle(in Circle circle, int segments, in Vector3 offset, in Color color) + { + DrawCircle((CircleF)circle, segments, offset, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + public static void DrawCircle(in Circle circle, int segments, in Color color, float duration) + { + DrawCircle((CircleF)circle, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + public static void DrawCircle(in Circle circle, int segments, in Vector3 offset, in Color color, float duration) + { + DrawCircle((CircleF)circle, segments, offset, color, duration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(in Circle circle, int segments, in Color color, float duration, bool depthTest) + { + DrawCircle((CircleF)circle, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a circle. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(in Circle circle, int segments, in Vector3 offset, in Color color, float duration, + bool depthTest) + { + DrawCircle((CircleF)circle, segments, offset, color, duration, depthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + public static void DrawCircle(in CircleF circle, int segments) + { + DrawCircle(circle, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + public static void DrawCircle(in CircleF circle, int segments, in Vector3 offset) + { + DrawCircle(circle, segments, offset, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + public static void DrawCircle(in CircleF circle, int segments, in Color color) + { + DrawCircle(circle, segments, Vector2.zero, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + public static void DrawCircle(in CircleF circle, int segments, in Vector3 offset, in Color color) + { + DrawCircle(circle, segments, offset, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + public static void DrawCircle(in CircleF circle, int segments, in Color color, float duration) + { + DrawCircle(circle, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + public static void DrawCircle(in CircleF circle, int segments, in Vector3 offset, in Color color, float duration) + { + DrawCircle(circle, segments, offset, color, duration, DefaultDepthTest); + } + + /// + /// Draws a circle with the specified color and duration. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(in CircleF circle, int segments, in Color color, float duration, bool depthTest) + { + DrawCircle(circle, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a circle. + /// + /// The circle to draw. + /// The number of segments to generate. + /// The drawing offset of the circle. + /// The color of the circle. + /// + /// The duration of the circle's visibility, in seconds. If 0 is passed, the circle is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the circle be obscured by objects closer to the camera. + /// + public static void DrawCircle(in CircleF circle, int segments, in Vector3 offset, in Color color, float duration, + bool depthTest) + { + DrawPolyhedron(CreateCircle(circle.Radius, segments, Vector3.zero), offset, color, duration, depthTest); + } + + private static Polyhedron CreateCircle(float radius, int segments, in Vector3 axis) + { + const float max = 2.0f * MathF.PI; + float step = max / segments; + + var points = new List(); + for (var theta = 0f; theta < max; theta += step) + { + float x = radius * MathF.Cos(theta); + float y = radius * MathF.Sin(theta); + var vector = new System.Numerics.Vector3(x, y, 0); + + if (axis != Vector3.zero) + { + vector = Quaternion.CreateFromAxisAngle(axis.ToSystemVector(), MathF.PI / 2.0f).Multiply(vector); + } + + points.Add(vector); + } + + return new Polyhedron(points); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Ellipse.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Ellipse.cs new file mode 100644 index 0000000..a4994f6 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Ellipse.cs @@ -0,0 +1,406 @@ +using UnityEngine; +using X10D.Drawing; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws an ellipse with the specified color. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + /// The number of segments to generate. + public static void DrawEllipse(Vector2 center, Vector2 radius, int segments) + { + DrawEllipse(center, radius.x, radius.y, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + public static void DrawEllipse(Vector2 center, Vector2 radius, int segments, in Color color) + { + DrawEllipse(center, radius.x, radius.y, segments, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(Vector2 center, Vector2 radius, int segments, in Color color, float duration) + { + DrawEllipse(center, radius.x, radius.y, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Vector2 center, Vector2 radius, int segments, in Color color, float duration, bool depthTest) + { + DrawEllipse(center, radius.x, radius.y, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws an ellipse. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Vector2 center, Vector2 radius, int segments, Vector2 offset, in Color color, float duration, + bool depthTest) + { + DrawEllipse(new EllipseF(center.x, center.y, radius.x, radius.y), segments, offset, color, duration, depthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The center point of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// The number of segments to generate. + public static void DrawEllipse(Vector2 center, float radiusX, float radiusY, int segments) + { + DrawEllipse(center, radiusX, radiusY, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The center point of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + public static void DrawEllipse(Vector2 center, float radiusX, float radiusY, int segments, in Color color) + { + DrawEllipse(center, radiusX, radiusY, segments, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The center point of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(Vector2 center, float radiusX, float radiusY, int segments, in Color color, float duration) + { + DrawEllipse(center, radiusX, radiusY, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The center point of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Vector2 center, float radiusX, float radiusY, int segments, in Color color, float duration, + bool depthTest) + { + DrawEllipse(center, radiusX, radiusY, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws an ellipse. + /// + /// The center point of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Vector2 center, float radiusX, float radiusY, int segments, Vector2 offset, in Color color, + float duration, bool depthTest) + { + DrawEllipse(new EllipseF(center.x, center.y, radiusX, radiusY), segments, offset, color, duration, depthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + public static void DrawEllipse(Ellipse ellipse, int segments) + { + DrawEllipse((EllipseF)ellipse, segments, Vector2.zero, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + public static void DrawEllipse(Ellipse ellipse, int segments, Vector2 offset) + { + DrawEllipse((EllipseF)ellipse, segments, offset, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + public static void DrawEllipse(Ellipse ellipse, int segments, in Color color) + { + DrawEllipse((EllipseF)ellipse, segments, Vector2.zero, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + public static void DrawEllipse(Ellipse ellipse, int segments, Vector2 offset, in Color color) + { + DrawEllipse((EllipseF)ellipse, segments, offset, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(Ellipse ellipse, int segments, in Color color, float duration) + { + DrawEllipse((EllipseF)ellipse, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(Ellipse ellipse, int segments, Vector2 offset, in Color color, float duration) + { + DrawEllipse((EllipseF)ellipse, segments, offset, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Ellipse ellipse, int segments, in Color color, float duration, bool depthTest) + { + DrawEllipse((EllipseF)ellipse, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws an ellipse. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(Ellipse ellipse, int segments, Vector2 offset, in Color color, float duration, bool depthTest) + { + DrawEllipse((EllipseF)ellipse, segments, offset, color, duration, depthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + public static void DrawEllipse(EllipseF ellipse, int segments) + { + DrawEllipse(ellipse, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + public static void DrawEllipse(EllipseF ellipse, int segments, Vector2 offset) + { + DrawEllipse(ellipse, segments, offset, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + public static void DrawEllipse(EllipseF ellipse, int segments, in Color color) + { + DrawEllipse(ellipse, segments, Vector2.zero, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + public static void DrawEllipse(EllipseF ellipse, int segments, Vector2 offset, in Color color) + { + DrawEllipse(ellipse, segments, offset, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(EllipseF ellipse, int segments, in Color color, float duration) + { + DrawEllipse(ellipse, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + public static void DrawEllipse(EllipseF ellipse, int segments, Vector2 offset, in Color color, float duration) + { + DrawEllipse(ellipse, segments, offset, color, duration, DefaultDepthTest); + } + + /// + /// Draws an ellipse with the specified color and duration. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(EllipseF ellipse, int segments, in Color color, float duration, bool depthTest) + { + DrawEllipse(ellipse, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws an ellipse. + /// + /// The ellipse to draw. + /// The number of segments to generate. + /// The drawing offset of the ellipse. + /// The color of the ellipse. + /// + /// The duration of the ellipse's visibility, in seconds. If 0 is passed, the ellipse is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the ellipse be obscured by objects closer to the camera. + /// + public static void DrawEllipse(EllipseF ellipse, int segments, Vector2 offset, in Color color, float duration, bool depthTest) + { + DrawPolygon(CreateEllipse(ellipse.HorizontalRadius, ellipse.VerticalRadius, segments), offset, color, duration, + depthTest); + } + + private static PolygonF CreateEllipse(float radiusX, float radiusY, int segments) + { + const float max = 2.0f * MathF.PI; + float step = max / segments; + + var points = new List(); + for (var theta = 0f; theta < max; theta += step) + { + float x = radiusX * MathF.Cos(theta); + float y = radiusY * MathF.Sin(theta); + points.Add(new System.Numerics.Vector2(x, y)); + } + + return new PolygonF(points); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Line.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Line.cs new file mode 100644 index 0000000..1439724 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Line.cs @@ -0,0 +1,209 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a line between start and end points. + /// + /// The starting point. + /// The ending point. + public static void DrawLine(Vector3 start, Vector3 end) + { + DrawLine(start, end, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The starting point. + /// The ending point. + /// The color of the line. + public static void DrawLine(Vector3 start, Vector3 end, in Color color) + { + DrawLine(start, end, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The starting point. + /// The ending point. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawLine(Vector3 start, Vector3 end, in Color color, float duration) + { + DrawLine(start, end, color, duration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The starting point. + /// The ending point. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawLine(Vector3 start, Vector3 end, in Color color, float duration, bool depthTest) + { + Debug.DrawLine(start, end, color, duration, depthTest); + } + + /// + /// Draws a line between start and end points. + /// + /// The line to draw. + public static void DrawLine(Line line) + { + DrawLine(line, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + public static void DrawLine(Line line, in Color color) + { + DrawLine(line, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawLine(Line line, in Color color, float duration) + { + DrawLine(line, color, duration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawLine(Line line, in Color color, float duration, bool depthTest) + { + Debug.DrawLine(line.Start.ToUnityVector2(), line.End.ToUnityVector2(), color, duration, depthTest); + } + + /// + /// Draws a line between start and end points. + /// + /// The line to draw. + public static void DrawLine(LineF line) + { + DrawLine(line, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + public static void DrawLine(LineF line, in Color color) + { + DrawLine(line, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawLine(LineF line, in Color color, float duration) + { + DrawLine(line, color, duration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawLine(LineF line, in Color color, float duration, bool depthTest) + { + Debug.DrawLine(line.Start.ToUnityVector2(), line.End.ToUnityVector2(), color, duration, depthTest); + } + + /// + /// Draws a line between start and end points. + /// + /// The line to draw. + public static void DrawLine(Line3D line) + { + DrawLine(line, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + public static void DrawLine(Line3D line, in Color color) + { + DrawLine(line, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawLine(Line3D line, in Color color, float duration) + { + DrawLine(line, color, duration, DefaultDepthTest); + } + + /// + /// Draws a line between start and end points, with the specified color. + /// + /// The line to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawLine(Line3D line, in Color color, float duration, bool depthTest) + { + Debug.DrawLine(line.Start.ToUnityVector(), line.End.ToUnityVector(), color, duration, depthTest); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Polygon.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Polygon.cs new file mode 100644 index 0000000..1e0fa9f --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Polygon.cs @@ -0,0 +1,232 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Drawing; +using PointF = System.Drawing.PointF; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a polygon. + /// + /// The polygon to draw. + public static void DrawPolygon(Polygon polygon) + { + DrawPolygon((PolygonF)polygon, Vector2.zero, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + public static void DrawPolygon(Polygon polygon, in Vector3 offset) + { + DrawPolygon((PolygonF)polygon, offset, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + public static void DrawPolygon(Polygon polygon, in Color color) + { + DrawPolygon((PolygonF)polygon, Vector2.zero, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + public static void DrawPolygon(Polygon polygon, in Vector3 offset, in Color color) + { + DrawPolygon((PolygonF)polygon, offset, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + public static void DrawPolygon(Polygon polygon, in Color color, float duration) + { + DrawPolygon((PolygonF)polygon, Vector2.zero, color, duration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + public static void DrawPolygon(Polygon polygon, in Vector3 offset, in Color color, float duration) + { + DrawPolygon((PolygonF)polygon, offset, color, duration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawPolygon(Polygon polygon, in Color color, float duration, bool depthTest) + { + DrawPolygon((PolygonF)polygon, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawPolygon(Polygon polygon, in Vector3 offset, in Color color, float duration, bool depthTest) + { + DrawPolygon((PolygonF)polygon, offset, color, duration, depthTest); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + public static void DrawPolygon(PolygonF polygon) + { + DrawPolygon(polygon, Vector2.zero, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + public static void DrawPolygon(PolygonF polygon, in Vector3 offset) + { + DrawPolygon(polygon, offset, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + public static void DrawPolygon(PolygonF polygon, in Color color) + { + DrawPolygon(polygon, Vector2.zero, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + public static void DrawPolygon(PolygonF polygon, in Vector3 offset, in Color color) + { + DrawPolygon(polygon, offset, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + public static void DrawPolygon(PolygonF polygon, in Color color, float duration) + { + DrawPolygon(polygon, Vector2.zero, color, duration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + public static void DrawPolygon(PolygonF polygon, in Vector3 offset, in Color color, float duration) + { + DrawPolygon(polygon, offset, color, duration, true); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawPolygon(PolygonF polygon, in Color color, float duration, bool depthTest) + { + DrawPolygon(polygon, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a polygon. + /// + /// The polygon to draw. + /// The drawing offset of the polygon. + /// The color to use for drawing. + /// + /// The duration of the polygon's visibility, in seconds. If 0 is passed, the polygon is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + /// is . + public static void DrawPolygon(PolygonF polygon, in Vector3 offset, in Color color, float duration, bool depthTest) + { + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } + + IReadOnlyList points = polygon.Vertices; + if (points.Count < 2) + { + return; + } + + for (var i = 0; i < points.Count; i++) + { + int j = (i + 1) % points.Count; + Vector3 start = (Vector3)points[i].ToUnityVector2() + offset; + Vector3 end = (Vector3)points[j].ToUnityVector2() + offset; + + DrawLine(start, end, color, duration, depthTest); + } + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Polyhedron.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Polyhedron.cs new file mode 100644 index 0000000..7ab51a0 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Polyhedron.cs @@ -0,0 +1,129 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + public static void DrawPolyhedron(Polyhedron polyhedron) + { + DrawPolyhedron(polyhedron, Vector2.zero, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The drawing offset of the polyhedron. + public static void DrawPolyhedron(Polyhedron polyhedron, in Vector3 offset) + { + DrawPolyhedron(polyhedron, offset, Color.white, DefaultDrawDuration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The color to use for drawing. + public static void DrawPolyhedron(Polyhedron polyhedron, in Color color) + { + DrawPolyhedron(polyhedron, Vector2.zero, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The drawing offset of the polyhedron. + /// The color to use for drawing. + public static void DrawPolyhedron(Polyhedron polyhedron, in Vector3 offset, in Color color) + { + DrawPolyhedron(polyhedron, offset, color, DefaultDrawDuration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The color to use for drawing. + /// + /// The duration of the polyhedron's visibility, in seconds. If 0 is passed, the polyhedron is visible for a single frame. + /// + public static void DrawPolyhedron(Polyhedron polyhedron, in Color color, float duration) + { + DrawPolyhedron(polyhedron, Vector2.zero, color, duration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The drawing offset of the polyhedron. + /// The color to use for drawing. + /// + /// The duration of the polyhedron's visibility, in seconds. If 0 is passed, the polyhedron is visible for a single frame. + /// + public static void DrawPolyhedron(Polyhedron polyhedron, in Vector3 offset, in Color color, float duration) + { + DrawPolyhedron(polyhedron, offset, color, duration, true); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The color to use for drawing. + /// + /// The duration of the polyhedron's visibility, in seconds. If 0 is passed, the polyhedron is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawPolyhedron(Polyhedron polyhedron, in Color color, float duration, bool depthTest) + { + DrawPolyhedron(polyhedron, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a polyhedron. + /// + /// The polyhedron to draw. + /// The drawing offset of the polyhedron. + /// The color to use for drawing. + /// + /// The duration of the polyhedron's visibility, in seconds. If 0 is passed, the polyhedron is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + /// is . + public static void DrawPolyhedron(Polyhedron polyhedron, in Vector3 offset, in Color color, float duration, bool depthTest) + { + if (polyhedron is null) + { + throw new ArgumentNullException(nameof(polyhedron)); + } + + IReadOnlyList points = polyhedron.Vertices; + if (points.Count < 2) + { + return; + } + + for (var i = 0; i < points.Count; i++) + { + int j = (i + 1) % points.Count; + Vector3 start = points[i].ToUnityVector() + offset; + Vector3 end = points[j].ToUnityVector() + offset; + + DrawLine(start, end, color, duration, depthTest); + } + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Ray.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Ray.cs new file mode 100644 index 0000000..475a80c --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Ray.cs @@ -0,0 +1,108 @@ +using UnityEngine; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a ray. + /// + /// The ray to draw. + public static void DrawRay(Ray ray) + { + DrawRay(ray, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The ray to draw. + /// The color of the line. + public static void DrawRay(Ray ray, in Color color) + { + DrawRay(ray, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The ray to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawRay(Ray ray, in Color color, float duration) + { + DrawRay(ray, color, duration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The ray to draw. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawRay(Ray ray, in Color color, float duration, bool depthTest) + { + Debug.DrawRay(ray.origin, ray.direction, color, duration, depthTest); + } + + /// + /// Draws a ray. + /// + /// The starting point. + /// The direction. + public static void DrawRay(Vector3 start, Vector3 direction) + { + DrawRay(start, direction, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The starting point. + /// The direction. + /// The color of the line. + public static void DrawRay(Vector3 start, Vector3 direction, in Color color) + { + DrawRay(start, direction, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The starting point. + /// The direction. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + public static void DrawRay(Vector3 start, Vector3 direction, in Color color, float duration) + { + DrawRay(start, direction, color, duration, DefaultDepthTest); + } + + /// + /// Draws a ray. + /// + /// The starting point. + /// The direction. + /// The color of the line. + /// + /// The duration of the line's visibility, in seconds. If 0 is passed, the line is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the line be obscured by objects closer to the camera. + /// + public static void DrawRay(Vector3 start, Vector3 direction, in Color color, float duration, bool depthTest) + { + Debug.DrawRay(start, direction, color, duration, depthTest); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Rectangle.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Rectangle.cs new file mode 100644 index 0000000..e8377d5 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Rectangle.cs @@ -0,0 +1,234 @@ +using System.Drawing; +using UnityEngine; +using X10D.Unity.Drawing; +using Color = UnityEngine.Color; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a rectangle. + /// + /// The center point. + /// The extents of the box. + public static void DrawRectangle(Vector2 center, Vector2 size) + { + DrawRectangle(center, size, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + public static void DrawRectangle(Vector2 center, Vector2 size, in Color color) + { + DrawRectangle(center, size, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawRectangle(Vector2 center, Vector2 size, in Color color, float duration) + { + DrawRectangle(center, size, color, duration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawRectangle(Vector2 center, Vector2 size, in Color color, float duration, bool depthTest) + { + DrawRectangle(new Rect(center, size), color, duration, depthTest); + } + + /// + /// Draws a rectangle with the specified color. + /// + /// The rectangle to draw. + /// The color of the box. + public static void DrawRectangle(Rect rect, in Color color) + { + DrawRectangle(rect, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawRectangle(Rect rect, in Color color, float duration) + { + DrawRectangle(rect, color, duration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawRectangle(Rect rect, in Color color, float duration, bool depthTest) + { + var topLeft = new Vector2(rect.xMin, rect.yMin); + var topRight = new Vector2(rect.xMax, rect.yMin); + var bottomLeft = new Vector2(rect.xMin, rect.yMax); + var bottomRight = new Vector2(rect.xMax, rect.yMax); + + DrawLine(topLeft, topRight, color, duration, depthTest); + DrawLine(topRight, bottomRight, color, duration, depthTest); + DrawLine(bottomRight, bottomLeft, color, duration, depthTest); + DrawLine(bottomLeft, topLeft, color, duration, depthTest); + } + + /// + /// Draws a rectangle with the specified color. + /// + /// The rectangle to draw. + /// The color of the box. + public static void DrawRectangle(RectInt rect, in Color color) + { + DrawRectangle(rect, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawRectangle(RectInt rect, in Color color, float duration) + { + DrawRectangle(rect, color, duration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawRectangle(RectInt rect, in Color color, float duration, bool depthTest) + { + DrawRectangle(new Rect(rect.center, rect.size), color, duration, depthTest); + } + + /// + /// Draws a rectangle with the specified color. + /// + /// The rectangle to draw. + /// The color of the box. + public static void DrawRectangle(Rectangle rectangle, in Color color) + { + DrawRectangle(rectangle, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawRectangle(Rectangle rectangle, in Color color, float duration) + { + DrawRectangle(rectangle, color, duration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawRectangle(Rectangle rectangle, in Color color, float duration, bool depthTest) + { + var origin = new Vector2(rectangle.X + rectangle.Width / 2.0f, rectangle.Y + rectangle.Height / 2.0f); + var rect = new Rect(origin, rectangle.Size.ToUnityVector2()); + DrawRectangle(rect, color, duration, depthTest); + } + + /// + /// Draws a rectangle with the specified color. + /// + /// The rectangle to draw. + /// The color of the box. + public static void DrawRectangle(RectangleF rectangle, in Color color) + { + DrawRectangle(rectangle, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawRectangle(RectangleF rectangle, in Color color, float duration) + { + DrawRectangle(rectangle, color, duration, DefaultDepthTest); + } + + /// + /// Draws a rectangle with the specified color and duration. + /// + /// The rectangle to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawRectangle(RectangleF rectangle, in Color color, float duration, bool depthTest) + { + var origin = new Vector2(rectangle.X + rectangle.Width / 2.0f, rectangle.Y + rectangle.Height / 2.0f); + var rect = new Rect(origin, rectangle.Size.ToUnityVector2()); + DrawRectangle(rect, color, duration, depthTest); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.Sphere.cs b/X10D.Unity/src/DebugUtility/DebugUtility.Sphere.cs new file mode 100644 index 0000000..1f75c0e --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.Sphere.cs @@ -0,0 +1,198 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws a sphere with the specified color. + /// + /// The center point of the sphere. + /// The radius of the sphere. + /// The number of segments to generate. + public static void DrawSphere(Vector3 center, float radius, int segments) + { + DrawSphere(center, radius, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color. + /// + /// The center point of the sphere. + /// The radius of the sphere. + /// The number of segments to generate. + /// The color of the sphere. + public static void DrawSphere(Vector3 center, float radius, int segments, in Color color) + { + DrawSphere(center, radius, segments, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color and duration. + /// + /// The center point of the sphere. + /// The radius of the sphere. + /// The number of segments to generate. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + public static void DrawSphere(Vector3 center, float radius, int segments, in Color color, float duration) + { + DrawSphere(center, radius, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color and duration. + /// + /// The center point of the sphere. + /// The radius of the sphere. + /// The number of segments to generate. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the sphere be obscured by objects closer to the camera. + /// + public static void DrawSphere(Vector3 center, float radius, int segments, in Color color, float duration, bool depthTest) + { + DrawSphere(center, radius, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a sphere. + /// + /// The center point of the sphere. + /// The radius of the sphere. + /// The number of segments to generate. + /// The drawing offset of the sphere. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the sphere be obscured by objects closer to the camera. + /// + public static void DrawSphere(Vector3 center, float radius, int segments, Vector2 offset, in Color color, float duration, + bool depthTest) + { + DrawSphere(new Sphere(center.ToSystemVector(), radius), segments, offset, color, duration, depthTest); + } + + /// + /// Draws a sphere with the specified color. + /// + /// The sphere to draw. + /// The number of segments to generate. + public static void DrawSphere(Sphere sphere, int segments) + { + DrawSphere(sphere, segments, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The drawing offset of the sphere. + public static void DrawSphere(Sphere sphere, int segments, Vector2 offset) + { + DrawSphere(sphere, segments, offset, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The color of the sphere. + public static void DrawSphere(Sphere sphere, int segments, in Color color) + { + DrawSphere(sphere, segments, Vector2.zero, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The drawing offset of the sphere. + /// The color of the sphere. + public static void DrawSphere(Sphere sphere, int segments, Vector2 offset, in Color color) + { + DrawSphere(sphere, segments, offset, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color and duration. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + public static void DrawSphere(Sphere sphere, int segments, in Color color, float duration) + { + DrawSphere(sphere, segments, Vector2.zero, color, duration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color and duration. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The drawing offset of the sphere. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + public static void DrawSphere(Sphere sphere, int segments, Vector2 offset, in Color color, float duration) + { + DrawSphere(sphere, segments, offset, color, duration, DefaultDepthTest); + } + + /// + /// Draws a sphere with the specified color and duration. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the sphere be obscured by objects closer to the camera. + /// + public static void DrawSphere(Sphere sphere, int segments, in Color color, float duration, bool depthTest) + { + DrawSphere(sphere, segments, Vector2.zero, color, duration, depthTest); + } + + /// + /// Draws a sphere. + /// + /// The sphere to draw. + /// The number of segments to generate. + /// The drawing offset of the sphere. + /// The color of the sphere. + /// + /// The duration of the sphere's visibility, in seconds. If 0 is passed, the sphere is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the sphere be obscured by objects closer to the camera. + /// + public static void DrawSphere(Sphere sphere, int segments, in Vector3 offset, in Color color, float duration, bool depthTest) + { + DrawPolyhedron(CreateCircle(sphere.Radius, segments, Vector3.zero), offset, color, duration, depthTest); + DrawPolyhedron(CreateCircle(sphere.Radius, segments, Vector3.left), offset, color, duration, depthTest); + DrawPolyhedron(CreateCircle(sphere.Radius, segments, Vector3.up), offset, color, duration, depthTest); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.WireCube.cs b/X10D.Unity/src/DebugUtility/DebugUtility.WireCube.cs new file mode 100644 index 0000000..1a550c2 --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.WireCube.cs @@ -0,0 +1,234 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity; + +public static partial class DebugUtility +{ + /// + /// Draws an axis-aligned bounding box. + /// + /// The bounding box to draw. + public static void DrawWireCube(in Bounds bounds) + { + DrawWireCube(bounds.center, bounds.size, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an axis-aligned bounding box. + /// + /// The bounding box to draw. + /// The color of the box. + public static void DrawWireCube(in Bounds bounds, in Color color) + { + DrawWireCube(bounds.center, bounds.size, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws an axis-aligned bounding box. + /// + /// The bounding box to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawWireCube(in Bounds bounds, in Color color, float duration) + { + DrawWireCube(bounds.center, bounds.size, color, duration, DefaultDepthTest); + } + + /// + /// Draws an axis-aligned bounding box. + /// + /// The bounding box to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawWireCube(in Bounds bounds, in Color color, float duration, bool depthTest) + { + DrawWireCube(bounds.center, bounds.size, color, duration, depthTest); + } + + /// + /// Draws a wireframe cube with a center and a size. + /// + /// The center point. + /// The extents of the box. + public static void DrawWireCube(Vector3 center, Vector3 size) + { + DrawWireCube(center, size, Color.white, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified orientation. + /// + /// The center point. + /// The extents of the box. + /// The orientation of the box. + public static void DrawWireCube(Vector3 center, Vector3 size, Quaternion orientation) + { + DrawWireCube(new Cuboid(center.ToSystemVector(), size.ToSystemVector(), orientation.ToSystemQuaternion()), Color.white, + DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified color. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + public static void DrawWireCube(Vector3 center, Vector3 size, in Color color) + { + DrawWireCube(center, size, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified orientation and color. + /// + /// The center point. + /// The extents of the box. + /// The orientation of the box. + /// The color of the box. + public static void DrawWireCube(Vector3 center, Vector3 size, Quaternion orientation, in Color color) + { + DrawWireCube(new Cuboid(center.ToSystemVector(), size.ToSystemVector(), orientation.ToSystemQuaternion()), color, + DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified color and duration. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawWireCube(Vector3 center, Vector3 size, in Color color, float duration) + { + DrawWireCube(center, size, color, duration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified orientation, color, and duration. + /// + /// The center point. + /// The extents of the box. + /// The orientation of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawWireCube(Vector3 center, Vector3 size, Quaternion orientation, in Color color, float duration) + { + DrawWireCube(new Cuboid(center.ToSystemVector(), size.ToSystemVector(), orientation.ToSystemQuaternion()), color, + duration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified color and duration. + /// + /// The center point. + /// The extents of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawWireCube(Vector3 center, Vector3 size, in Color color, float duration, bool depthTest) + { + DrawWireCube(new Cuboid(center.ToSystemVector(), size.ToSystemVector()), color, duration, depthTest); + } + + /// + /// Draws a wireframe cube with the specified orientation, color, and duration. + /// + /// The center point. + /// The extents of the box. + /// The orientation of the box. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawWireCube(Vector3 center, Vector3 size, Quaternion orientation, in Color color, float duration, + bool depthTest) + { + DrawWireCube(new Cuboid(center.ToSystemVector(), size.ToSystemVector(), orientation.ToSystemQuaternion()), color, + duration, depthTest); + } + + /// + /// Draws a wireframe cube with the specified color. + /// + /// The cuboid to draw. + /// The color of the box. + public static void DrawWireCube(in Cuboid cuboid, in Color color) + { + DrawWireCube(cuboid, color, DefaultDrawDuration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified color and duration. + /// + /// The cuboid to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + public static void DrawWireCube(in Cuboid cuboid, in Color color, float duration) + { + DrawWireCube(cuboid, color, duration, DefaultDepthTest); + } + + /// + /// Draws a wireframe cube with the specified color and duration. + /// + /// The cuboid to draw. + /// The color of the box. + /// + /// The duration of the box's visibility, in seconds. If 0 is passed, the box is visible for a single frame. + /// + /// + /// if depth test should be applied; otherwise, . Passing + /// will have the box be obscured by objects closer to the camera. + /// + public static void DrawWireCube(in Cuboid cuboid, in Color color, float duration, bool depthTest) + { + Vector3 frontTopLeft = cuboid.FrontTopLeft.ToUnityVector(); + Vector3 frontTopRight = cuboid.FrontTopRight.ToUnityVector(); + Vector3 frontBottomRight = cuboid.FrontBottomRight.ToUnityVector(); + Vector3 frontBottomLeft = cuboid.FrontBottomLeft.ToUnityVector(); + Vector3 backTopLeft = cuboid.BackTopLeft.ToUnityVector(); + Vector3 backTopRight = cuboid.BackTopRight.ToUnityVector(); + Vector3 backBottomRight = cuboid.BackBottomRight.ToUnityVector(); + Vector3 backBottomLeft = cuboid.BackBottomLeft.ToUnityVector(); + + Debug.DrawLine(frontTopLeft, frontTopRight, color, duration, depthTest); + Debug.DrawLine(frontTopRight, frontBottomRight, color, duration, depthTest); + Debug.DrawLine(frontBottomRight, frontBottomLeft, color, duration, depthTest); + Debug.DrawLine(frontBottomLeft, frontTopLeft, color, duration, depthTest); + + Debug.DrawLine(backTopLeft, backTopRight, color, duration, depthTest); + Debug.DrawLine(backTopRight, backBottomRight, color, duration, depthTest); + Debug.DrawLine(backBottomRight, backBottomLeft, color, duration, depthTest); + Debug.DrawLine(backBottomLeft, backTopLeft, color, duration, depthTest); + + Debug.DrawLine(frontTopLeft, backTopLeft, color, duration, depthTest); + Debug.DrawLine(frontTopRight, backTopRight, color, duration, depthTest); + Debug.DrawLine(frontBottomRight, backBottomRight, color, duration, depthTest); + Debug.DrawLine(frontBottomLeft, backBottomLeft, color, duration, depthTest); + } +} diff --git a/X10D.Unity/src/DebugUtility/DebugUtility.cs b/X10D.Unity/src/DebugUtility/DebugUtility.cs new file mode 100644 index 0000000..0a2133c --- /dev/null +++ b/X10D.Unity/src/DebugUtility/DebugUtility.cs @@ -0,0 +1,417 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using UnityEngine; +using Debug = UnityEngine.Debug; +using Object = UnityEngine.Object; + +namespace X10D.Unity; + +/// +/// An extended version of Unity's utility class which offers support for drawing simple +/// primitives. +/// +public static partial class DebugUtility +{ + /// + /// The default value to use for the duration parameter. + /// + private const float DefaultDrawDuration = 0.0f; + + /// + /// The default value to use for the depthTest parameter. + /// + private const bool DefaultDepthTest = true; + + /// + /// Gets a value indicating whether this is a debug build. + /// + /// if this is a debug build; otherwise, . + // ReSharper disable once InconsistentNaming + public static bool isDebugBuild + { + get => Debug.isDebugBuild; + } + + /// + /// Gets a value indicating whether the developer console is visible. + /// + /// if the developer console is visible; otherwise, . + // ReSharper disable once InconsistentNaming + public static bool isDeveloperConsoleVisible + { + get => Debug.developerConsoleVisible; + } + + /// + /// Gets the default Unity debug logger. + /// + /// The Unity debug logger. + // ReSharper disable once InconsistentNaming + public static ILogger unityLogger + { + get => Debug.unityLogger; + } + + /// + /// Asserts a condition. + /// + /// The condition to assert. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, "Assertion failed"); + } + + /// + /// Asserts a condition. + /// + /// The condition to assert. + /// The object to which the assertion applies. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition, Object context) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, (object)"Assertion failed", context); + } + + /// + /// Asserts a condition. + /// + /// The condition to assert. + /// The message to log. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition, string? message) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, message); + } + + /// + /// Asserts a condition. + /// + /// The condition to assert. + /// The message to log. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition, T? message) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, message?.ToString()); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The condition to assert. + /// The message to log. + /// The object to which the assertion applies. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition, string? message, Object? context) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, (object?)message, context); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The condition to assert. + /// The message to log. + /// The object to which the assertion applies. + [Conditional("UNITY_ASSERTIONS")] + [AssertionMethod] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void Assert(bool condition, T? message, Object? context) + { + if (condition) + { + return; + } + + unityLogger.Log(LogType.Assert, (object?)message?.ToString(), context); + } + + /// + /// Pauses the editor. + /// + public static void Break() + { + Debug.Break(); + } + + /// + /// Clears the developer console. + /// + public static void ClearDeveloperConsole() + { + Debug.ClearDeveloperConsole(); + } + + /// + /// Populate an unmanaged buffer with the current managed call stack as a sequence of UTF-8 bytes, without allocating GC + /// memory. + /// + /// The target buffer to receive the callstack text. + /// The maximum number of bytes to write. + /// The project folder path, to clean up path names. + /// The number of bytes written into the buffer. + [MustUseReturnValue("Fewer bytes may be returned than requested.")] + public static unsafe int ExtractStackTraceNoAlloc(byte* buffer, int bufferMax, string projectFolder) + { + return Debug.ExtractStackTraceNoAlloc(buffer, bufferMax, projectFolder); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The message to log. + public static void Log(string? message) + { + Debug.Log(message); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The message to log. + public static void Log(T message) + { + Log(message?.ToString()); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void Log(string message, Object? context) + { + Debug.Log(message, context); + } + + /// + /// Logs a message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void Log(T message, Object? context) + { + Debug.Log(message?.ToString(), context); + } + + /// + /// Logs an assertion message to the Unity Console. + /// + /// The message to log. + [Conditional("UNITY_ASSERTIONS")] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void LogAssertion(string? message) + { + unityLogger.Log(LogType.Assert, message); + } + + /// + /// Logs an assertion message to the Unity Console. + /// + /// The message to log. + [Conditional("UNITY_ASSERTIONS")] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void LogAssertion(T message) + { + unityLogger.Log(LogType.Assert, message?.ToString()); + } + + /// + /// Logs an assertion message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + [Conditional("UNITY_ASSERTIONS")] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void LogAssertion(string message, Object? context) + { + unityLogger.Log(LogType.Assert, (object?)message, context); + } + + /// + /// Logs an assertion message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + [Conditional("UNITY_ASSERTIONS")] + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void LogAssertion(T? message, Object? context) + { + unityLogger.Log(LogType.Assert, (object?)message?.ToString(), context); + } + + /// + /// Logs an error message to the Unity Console. + /// + /// The message to log. + public static void LogError(string? message) + { + Debug.LogError(message); + } + + /// + /// Logs an error message to the Unity Console. + /// + /// The message to log. + public static void LogError(T? message) + { + LogError(message?.ToString()); + } + + /// + /// Logs an error message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void LogError(string message, Object? context) + { + Debug.LogError(message, context); + } + + /// + /// Logs an error message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void LogError(T? message, Object? context) + { + Debug.LogError(message?.ToString(), context); + } + + /// + /// Logs a formatted error message to the Unity Console. + /// + /// The format string of the message to log. + /// The format arguments. + public static void LogErrorFormat(string? format, params object?[]? args) + { + Debug.LogErrorFormat(format, args); + } + + /// + /// Logs a formatted error message to the Unity Console. + /// + /// The object to which this message applies. + /// The format string of the message to log. + /// The format arguments. + public static void LogErrorFormat(Object context, string? format, params object?[]? args) + { + Debug.LogErrorFormat(context, format, args); + } + + /// + /// Logs a formatted message to the Unity Console. + /// + /// The format string of the message to log. + /// The format arguments. + public static void LogFormat(string? format, params object?[]? args) + { + Debug.LogFormat(format, args); + } + + /// + /// Logs a formatted message to the Unity Console. + /// + /// The object to which this message applies. + /// The format string of the message to log. + /// The format arguments. + public static void LogFormat(Object context, string? format, params object?[]? args) + { + Debug.LogFormat(context, format, args); + } + + /// + /// Logs a warning message to the Unity Console. + /// + /// The message to log. + public static void LogWarning(string? message) + { + Debug.LogWarning(message); + } + + /// + /// Logs a warning message to the Unity Console. + /// + /// The message to log. + public static void LogWarning(T? message) + { + LogWarning(message?.ToString()); + } + + /// + /// Logs a warning message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void LogWarning(string message, Object? context) + { + Debug.LogWarning(message, context); + } + + /// + /// Logs a warning message to the Unity Console. + /// + /// The message to log. + /// The object to which the message applies. + public static void LogWarning(T? message, Object? context) + { + Debug.LogWarning(message?.ToString(), context); + } + + /// + /// Logs a formatted warning message to the Unity Console. + /// + /// The format string of the message to log. + /// The format arguments. + public static void LogWarningFormat(string? format, params object?[]? args) + { + Debug.LogWarningFormat(format, args); + } + + /// + /// Logs a formatted warning message to the Unity Console. + /// + /// The object to which this message applies. + /// The format string of the message to log. + /// The format arguments. + public static void LogWarningFormat(Object context, string? format, params object?[]? args) + { + Debug.LogWarningFormat(context, format, args); + } +} diff --git a/X10D.Unity/src/Drawing/Color32Extensions.cs b/X10D.Unity/src/Drawing/Color32Extensions.cs index 363f14d..a505c94 100644 --- a/X10D.Unity/src/Drawing/Color32Extensions.cs +++ b/X10D.Unity/src/Drawing/Color32Extensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using UnityEngine; +using X10D.Drawing; namespace X10D.Unity.Drawing; @@ -9,6 +10,72 @@ namespace X10D.Unity.Drawing; /// public static class Color32Extensions { + /// + /// Deconstructs the current color into its RGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color32 color, out byte a, out byte r, out byte g, out byte b) + { + a = color.a; + (r, g, b) = color; + } + + /// + /// Deconstructs the current color into its RGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color32 color, out byte r, out byte g, out byte b) + { + r = color.r; + g = color.g; + b = color.b; + } + + /// + /// Returns a which most closely resembles the current color. + /// + /// The source color. + /// The closest . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ConsoleColor GetClosestConsoleColor(this Color32 color) + { + return color.ToSystemDrawingColor().GetClosestConsoleColor(); + } + /// /// Returns a new with the red, green, and blue components inverted. Alpha is not affected. /// @@ -21,6 +88,30 @@ public static class Color32Extensions return new Color32((byte)(255 - color.r), (byte)(255 - color.g), (byte)(255 - color.b), color.a); } + /// + /// Converts the current color to a . + /// + /// The color to convert. + /// The converted color. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static System.Drawing.Color ToSystemDrawingColor(this Color32 color) + { + return System.Drawing.Color.FromArgb(color.a, color.r, color.g, color.b); + } + + /// + /// Converts the current color to a . + /// + /// The color to convert. + /// The converted color. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color32 ToUnityColor32(this System.Drawing.Color color) + { + return new Color32(color.R, color.G, color.B, color.A); + } + /// /// Returns a vector whose red, green, and blue components are the same as the specified color, and whose alpha component /// is a new value. diff --git a/X10D.Unity/src/Drawing/ColorExtensions.cs b/X10D.Unity/src/Drawing/ColorExtensions.cs index 95a6561..06fcb6a 100644 --- a/X10D.Unity/src/Drawing/ColorExtensions.cs +++ b/X10D.Unity/src/Drawing/ColorExtensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using UnityEngine; +using X10D.Drawing; namespace X10D.Unity.Drawing; @@ -9,6 +10,72 @@ namespace X10D.Unity.Drawing; /// public static class ColorExtensions { + /// + /// Deconstructs the current color into its ARGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color color, out float a, out float r, out float g, out float b) + { + a = color.a; + (r, g, b) = color; + } + + /// + /// Deconstructs the current color into its RGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color color, out float r, out float g, out float b) + { + r = color.r; + g = color.g; + b = color.b; + } + + /// + /// Returns a which most closely resembles the current color. + /// + /// The source color. + /// The closest . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ConsoleColor GetClosestConsoleColor(this Color color) + { + return color.ToSystemDrawingColor().GetClosestConsoleColor(); + } + /// /// Returns a new with the red, green, and blue components inverted. Alpha is not affected. /// @@ -21,6 +88,35 @@ public static class ColorExtensions return new Color(1f - color.r, 1f - color.g, 1f - color.b, color.a); } + /// + /// Converts the current color to a . + /// + /// The color to convert. + /// The converted color. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static System.Drawing.Color ToSystemDrawingColor(this Color color) + { + return System.Drawing.Color.FromArgb( + (int)(color.a * 255f), + (int)(color.r * 255f), + (int)(color.g * 255f), + (int)(color.b * 255f) + ); + } + + /// + /// Converts the current color to a . + /// + /// The color to convert. + /// The converted color. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color ToUnityColor(this System.Drawing.Color color) + { + return new Color(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + } + /// /// Returns a vector whose red, green, and blue components are the same as the specified color, and whose alpha component /// is a new value. diff --git a/X10D.Unity/src/Drawing/PointExtensions.cs b/X10D.Unity/src/Drawing/PointExtensions.cs new file mode 100644 index 0000000..68f2a5f --- /dev/null +++ b/X10D.Unity/src/Drawing/PointExtensions.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PointExtensions +{ + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToUnityVector2(this Point point) + { + return new Vector2(point.X, point.Y); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int ToUnityVector2Int(this Point value) + { + return UnsafeUtility.As(ref value); + } +} diff --git a/X10D.Unity/src/Drawing/PointFExtensions.cs b/X10D.Unity/src/Drawing/PointFExtensions.cs new file mode 100644 index 0000000..7f58e51 --- /dev/null +++ b/X10D.Unity/src/Drawing/PointFExtensions.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PointFExtensions +{ + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this PointF point, Vector2 start, Vector2 end) + { + return point.IsOnLine(start.ToSystemVector(), end.ToSystemVector()); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToUnityVector2(this PointF point) + { + return UnsafeUtility.As(ref point); + } +} diff --git a/X10D.Unity/src/Drawing/PolygonExtensions.cs b/X10D.Unity/src/Drawing/PolygonExtensions.cs new file mode 100644 index 0000000..8a72df8 --- /dev/null +++ b/X10D.Unity/src/Drawing/PolygonExtensions.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PolygonExtensions +{ + /// + /// Adds a vertex to this polygon. + /// + /// The polygon whose points to update. + /// The point to add. + public static void AddVertex(this Polygon polygon, Vector2Int point) + { + polygon.AddVertex(point.ToSystemPoint()); + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// The polygon whose vertices to update. + /// The vertices to add. + public static void AddVertices(this Polygon polygon, IEnumerable vertices) + { + foreach (Vector2Int vertex in vertices) + { + polygon.AddVertex(vertex); + } + } +} diff --git a/X10D.Unity/src/Drawing/PolygonFExtensions.cs b/X10D.Unity/src/Drawing/PolygonFExtensions.cs new file mode 100644 index 0000000..0042904 --- /dev/null +++ b/X10D.Unity/src/Drawing/PolygonFExtensions.cs @@ -0,0 +1,57 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PolygonFExtensions +{ + /// + /// Adds a point to this polygon. + /// + /// The polygon whose vertices to update. + /// The vertex to add. + public static void AddVertex(this PolygonF polygon, Vector2Int vertex) + { + polygon.AddVertex(vertex.ToSystemPoint()); + } + + /// + /// Adds a point to this polygon. + /// + /// The polygon whose vertices to update. + /// The vertex to add. + public static void AddVertex(this PolygonF polygon, Vector2 vertex) + { + polygon.AddVertex(vertex.ToSystemPointF()); + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// The polygon whose vertices to update. + /// The vertices to add. + public static void AddVertices(this PolygonF polygon, IEnumerable vertices) + { + foreach (Vector2Int vertex in vertices) + { + polygon.AddVertex(vertex); + } + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// The polygon whose vertices to update. + /// The vertices to add. + public static void AddVertices(this PolygonF polygon, IEnumerable vertices) + { + foreach (Vector2 vertex in vertices) + { + polygon.AddVertex(vertex); + } + } +} diff --git a/X10D.Unity/src/Drawing/PolyhedronExtensions.cs b/X10D.Unity/src/Drawing/PolyhedronExtensions.cs new file mode 100644 index 0000000..108b9a8 --- /dev/null +++ b/X10D.Unity/src/Drawing/PolyhedronExtensions.cs @@ -0,0 +1,57 @@ +using UnityEngine; +using X10D.Drawing; +using X10D.Unity.Numerics; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PolyhedronExtensions +{ + /// + /// Adds a vertex to this polyhedron. + /// + /// The polyhedron whose vertices to update. + /// The vertex to add. + public static void AddVertex(this Polyhedron polyhedron, Vector3Int vertex) + { + polyhedron.AddVertex(vertex.ToSystemVector()); + } + + /// + /// Adds a vertex to this polyhedron. + /// + /// The polyhedron whose vertices to update. + /// The vertex to add. + public static void AddVertex(this Polyhedron polyhedron, Vector3 vertex) + { + polyhedron.AddVertex(vertex.ToSystemVector()); + } + + /// + /// Adds a collection of vertices to this polyhedron. + /// + /// The polyhedron whose vertices to update. + /// The vertices to add. + public static void AddVertices(this Polyhedron polyhedron, IEnumerable vertices) + { + foreach (Vector3Int vertex in vertices) + { + polyhedron.AddVertex(vertex); + } + } + + /// + /// Adds a collection of vertices to this polyhedron. + /// + /// The polyhedron whose vertices to update. + /// The vertices to add. + public static void AddVertices(this Polyhedron polyhedron, IEnumerable vertices) + { + foreach (Vector3 vertex in vertices) + { + polyhedron.AddVertex(vertex); + } + } +} diff --git a/X10D.Unity/src/Drawing/RectExtensions.cs b/X10D.Unity/src/Drawing/RectExtensions.cs new file mode 100644 index 0000000..71db4d6 --- /dev/null +++ b/X10D.Unity/src/Drawing/RectExtensions.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class RectExtensions +{ + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF ToSystemRectangleF(this Rect rectangle) + { + return UnsafeUtility.As(ref rectangle); + } +} diff --git a/X10D.Unity/src/Drawing/RectIntExtensions.cs b/X10D.Unity/src/Drawing/RectIntExtensions.cs new file mode 100644 index 0000000..184bf74 --- /dev/null +++ b/X10D.Unity/src/Drawing/RectIntExtensions.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class RectIntExtensions +{ + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle ToSystemRectangle(this RectInt rectangle) + { + return UnsafeUtility.As(ref rectangle); + } + + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF ToSystemRectangleF(this RectInt rectangle) + { + // REMARKS: implicit conversion already exists, this method is largely pointless + return rectangle.ToSystemRectangle(); + } +} diff --git a/X10D.Unity/src/Drawing/RectangleExtensions.cs b/X10D.Unity/src/Drawing/RectangleExtensions.cs new file mode 100644 index 0000000..7326fdf --- /dev/null +++ b/X10D.Unity/src/Drawing/RectangleExtensions.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class RectangleExtensions +{ + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rect ToUnityRect(this Rectangle rectangle) + { + return new Rect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectInt ToUnityRectInt(this Rectangle rectangle) + { + return UnsafeUtility.As(ref rectangle); + } +} diff --git a/X10D.Unity/src/Drawing/RectangleFExtensions.cs b/X10D.Unity/src/Drawing/RectangleFExtensions.cs new file mode 100644 index 0000000..6eaf66c --- /dev/null +++ b/X10D.Unity/src/Drawing/RectangleFExtensions.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class RectangleFExtensions +{ + /// + /// Converts the current to a . + /// + /// The rectangle to convert. + /// The converted rectangle. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rect ToUnityRect(this RectangleF rectangle) + { + return new Rect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } +} diff --git a/X10D.Unity/src/Drawing/SizeExtensions.cs b/X10D.Unity/src/Drawing/SizeExtensions.cs new file mode 100644 index 0000000..bef5855 --- /dev/null +++ b/X10D.Unity/src/Drawing/SizeExtensions.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class SizeExtensions +{ + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToUnityVector2(this Size size) + { + // REMARKS: implicit conversion already exists, this method is largely pointless + return size.ToUnityVector2Int(); + } + + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int ToUnityVector2Int(this Size size) + { + return UnsafeUtility.As(ref size); + } +} diff --git a/X10D.Unity/src/Drawing/SizeFExtensions.cs b/X10D.Unity/src/Drawing/SizeFExtensions.cs new file mode 100644 index 0000000..d9f1047 --- /dev/null +++ b/X10D.Unity/src/Drawing/SizeFExtensions.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace X10D.Unity.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class SizeFExtensions +{ + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToUnityVector2(this SizeF size) + { + return UnsafeUtility.As(ref size); + } +} diff --git a/X10D.Unity/src/Numerics/QuaternionExtensions.cs b/X10D.Unity/src/Numerics/QuaternionExtensions.cs index 2c6355e..5511bbc 100644 --- a/X10D.Unity/src/Numerics/QuaternionExtensions.cs +++ b/X10D.Unity/src/Numerics/QuaternionExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine; namespace X10D.Unity.Numerics; @@ -18,7 +19,7 @@ public static class QuaternionExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static System.Numerics.Quaternion ToSystemQuaternion(this Quaternion quaternion) { - return new System.Numerics.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w); + return UnsafeUtility.As(ref quaternion); } /// @@ -30,6 +31,6 @@ public static class QuaternionExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion ToUnityQuaternion(this System.Numerics.Quaternion quaternion) { - return new Quaternion(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W); + return UnsafeUtility.As(ref quaternion); } } diff --git a/X10D.Unity/src/Numerics/Vector2Extensions.cs b/X10D.Unity/src/Numerics/Vector2Extensions.cs index 32ff372..6e608f8 100644 --- a/X10D.Unity/src/Numerics/Vector2Extensions.cs +++ b/X10D.Unity/src/Numerics/Vector2Extensions.cs @@ -1,6 +1,11 @@ using System.Diagnostics.Contracts; +using System.Drawing; using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine; +using X10D.Drawing; +using X10D.Math; +using X10D.Numerics; namespace X10D.Unity.Numerics; @@ -9,6 +14,119 @@ namespace X10D.Unity.Numerics; /// public static class Vector2Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + public static void Deconstruct(this Vector2 vector, out float x, out float y) + { + x = vector.x; + y = vector.y; + } + + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The line on which the point may lie. + /// + /// if lies on the line defined by ; otherwise + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2 point, LineF line) + { + return point.ToSystemVector().IsOnLine(line); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2 point, PointF start, PointF end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2 point, Vector2 start, Vector2 end) + { + return point.ToSystemVector().IsOnLine(start.ToSystemVector(), end.ToSystemVector()); + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Round(this Vector2 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Round(this Vector2 vector, float nearest) + { + float x = vector.x.Round(nearest); + float y = vector.y.Round(nearest); + return new Vector2(x, y); + } + + /// + /// Converts the current into a . + /// + /// The vector to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF ToSystemPointF(this Vector2 vector) + { + return UnsafeUtility.As(ref vector); + } + + /// + /// Converts the current into a . + /// + /// The vector to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF ToSystemSizeF(this Vector2 vector) + { + return UnsafeUtility.As(ref vector); + } + /// /// Converts the current vector to a . /// @@ -18,7 +136,7 @@ public static class Vector2Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static System.Numerics.Vector2 ToSystemVector(this Vector2 vector) { - return new System.Numerics.Vector2(vector.x, vector.y); + return UnsafeUtility.As(ref vector); } /// @@ -30,7 +148,7 @@ public static class Vector2Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ToUnityVector(this System.Numerics.Vector2 vector) { - return new Vector2(vector.X, vector.Y); + return UnsafeUtility.As(ref vector); } /// diff --git a/X10D.Unity/src/Numerics/Vector2IntExtensions.cs b/X10D.Unity/src/Numerics/Vector2IntExtensions.cs new file mode 100644 index 0000000..babf2d9 --- /dev/null +++ b/X10D.Unity/src/Numerics/Vector2IntExtensions.cs @@ -0,0 +1,160 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Runtime.CompilerServices; +using UnityEngine; +using X10D.Drawing; + +namespace X10D.Unity.Numerics; + +/// +/// Numeric-extensions for . +/// +public static class Vector2IntExtensions +{ + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + public static void Deconstruct(this Vector2Int vector, out int x, out int y) + { + x = vector.x; + y = vector.y; + } + + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The line on which the point may lie. + /// + /// if lies on the line defined by ; otherwise + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2Int point, LineF line) + { + return point.ToSystemPoint().IsOnLine(line); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2Int point, PointF start, PointF end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2Int point, Vector2Int start, Vector2Int end) + { + return point.ToSystemPoint().IsOnLine(new LineF(start.ToSystemVector(), end.ToSystemVector())); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOnLine(this Vector2Int point, Vector2 start, Vector2 end) + { + return point.ToSystemPoint().IsOnLine(new LineF(start.ToSystemVector(), end.ToSystemVector())); + } + + /// + /// Converts the current into a . + /// + /// The vector to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point ToSystemPoint(this Vector2Int vector) + { + return new Point(vector.x, vector.y); + } + + /// + /// Converts the current into a . + /// + /// The vector to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size ToSystemSize(this Vector2Int vector) + { + return new Size(vector.x, vector.y); + } + + /// + /// Converts the current vector to a . + /// + /// The vector to convert. + /// The converted vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static System.Numerics.Vector2 ToSystemVector(this Vector2Int vector) + { + return new System.Numerics.Vector2(vector.x, vector.y); + } + + /// + /// Returns a vector whose Y component is the same as the specified vector, and whose X component is a new value. + /// + /// The vector to copy. + /// The new X component value. + /// + /// A new instance of whose components is the same as that of + /// , and whose component is . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int WithX(this Vector2Int vector, int x) + { + return vector with {x = x}; + } + + /// + /// Returns a vector whose X component is the same as the specified vector, and whose Y component is a new value. + /// + /// The vector to copy. + /// The new Y component value. + /// + /// A new instance of whose components is the same as that of + /// , and whose component is . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int WithY(this Vector2Int vector, int y) + { + return vector with {y = y}; + } +} diff --git a/X10D.Unity/src/Numerics/Vector3Extensions.cs b/X10D.Unity/src/Numerics/Vector3Extensions.cs index 1b725ce..64dc0e1 100644 --- a/X10D.Unity/src/Numerics/Vector3Extensions.cs +++ b/X10D.Unity/src/Numerics/Vector3Extensions.cs @@ -1,6 +1,8 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine; +using X10D.Math; namespace X10D.Unity.Numerics; @@ -9,6 +11,48 @@ namespace X10D.Unity.Numerics; /// public static class Vector3Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + /// The Z component value. + public static void Deconstruct(this Vector3 vector, out float x, out float y, out float z) + { + x = vector.x; + y = vector.y; + z = vector.z; + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Round(this Vector3 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Round(this Vector3 vector, float nearest) + { + float x = vector.x.Round(nearest); + float y = vector.y.Round(nearest); + float z = vector.z.Round(nearest); + return new Vector3(x, y, z); + } + /// /// Converts the current vector to a . /// @@ -18,7 +62,7 @@ public static class Vector3Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static System.Numerics.Vector3 ToSystemVector(this Vector3 vector) { - return new System.Numerics.Vector3(vector.x, vector.y, vector.z); + return UnsafeUtility.As(ref vector); } /// @@ -30,7 +74,7 @@ public static class Vector3Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 ToUnityVector(this System.Numerics.Vector3 vector) { - return new Vector3(vector.X, vector.Y, vector.Z); + return UnsafeUtility.As(ref vector); } /// diff --git a/X10D.Unity/src/Numerics/Vector3IntExtensions.cs b/X10D.Unity/src/Numerics/Vector3IntExtensions.cs new file mode 100644 index 0000000..16b2e4d --- /dev/null +++ b/X10D.Unity/src/Numerics/Vector3IntExtensions.cs @@ -0,0 +1,88 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace X10D.Unity.Numerics; + +/// +/// Numeric-extensions for . +/// +public static class Vector3IntExtensions +{ + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + /// The Z component value. + public static void Deconstruct(this Vector3Int vector, out int x, out int y, out int z) + { + x = vector.x; + y = vector.y; + z = vector.z; + } + + /// + /// Converts the current vector to a . + /// + /// The vector to convert. + /// The converted vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static System.Numerics.Vector3 ToSystemVector(this Vector3Int vector) + { + return new System.Numerics.Vector3(vector.x, vector.y, vector.z); + } + + /// + /// Returns a vector whose Y and Z components are the same as the specified vector, and whose X component is a new value. + /// + /// The vector to copy. + /// The new X component value. + /// + /// A new instance of whose and + /// components are the same as that of , and whose component is + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Int WithX(this Vector3Int vector, int x) + { + return vector with {x = x}; + } + + /// + /// Returns a vector whose X and Z components are the same as the specified vector, and whose Y component is a new value. + /// + /// The vector to copy. + /// The new Y component value. + /// + /// A new instance of whose and + /// components are the same as that of , and whose component is + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Int WithY(this Vector3Int vector, int y) + { + return vector with {y = y}; + } + + /// + /// Returns a vector whose X and Y components are the same as the specified vector, and whose Z component is a new value. + /// + /// The vector to copy. + /// The new Z component value. + /// + /// A new instance of whose and + /// components are the same as that of , and whose component is + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Int WithZ(this Vector3Int vector, int z) + { + return vector with {z = z}; + } +} diff --git a/X10D.Unity/src/Numerics/Vector4Extensions.cs b/X10D.Unity/src/Numerics/Vector4Extensions.cs index ca2587f..e17ba25 100644 --- a/X10D.Unity/src/Numerics/Vector4Extensions.cs +++ b/X10D.Unity/src/Numerics/Vector4Extensions.cs @@ -1,6 +1,8 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine; +using X10D.Math; namespace X10D.Unity.Numerics; @@ -9,6 +11,51 @@ namespace X10D.Unity.Numerics; /// public static class Vector4Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + /// The Z component value. + /// The W component value. + public static void Deconstruct(this Vector4 vector, out float x, out float y, out float z, out float w) + { + x = vector.x; + y = vector.y; + z = vector.z; + w = vector.w; + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Round(this Vector4 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Round(this Vector4 vector, float nearest) + { + float x = vector.x.Round(nearest); + float y = vector.y.Round(nearest); + float z = vector.z.Round(nearest); + float w = vector.w.Round(nearest); + return new Vector4(x, y, z, w); + } + /// /// Converts the current vector to a . /// @@ -18,7 +65,7 @@ public static class Vector4Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static System.Numerics.Vector4 ToSystemVector(this Vector4 vector) { - return new System.Numerics.Vector4(vector.x, vector.y, vector.z, vector.w); + return UnsafeUtility.As(ref vector); } /// @@ -30,7 +77,7 @@ public static class Vector4Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4 ToUnityVector(this System.Numerics.Vector4 vector) { - return new Vector4(vector.X, vector.Y, vector.Z, vector.W); + return UnsafeUtility.As(ref vector); } /// diff --git a/X10D.Unity/src/Singleton.cs b/X10D.Unity/src/Singleton.cs index 8e506db..6d75df7 100644 --- a/X10D.Unity/src/Singleton.cs +++ b/X10D.Unity/src/Singleton.cs @@ -11,14 +11,25 @@ public abstract class Singleton : MonoBehaviour where T : Singleton { private static Lazy s_instanceLazy = new(CreateLazyInstanceInternal, false); + private static T? s_instance; /// /// Gets the instance of the singleton. /// /// The singleton instance. +#pragma warning disable CA1000 public static T Instance +#pragma warning restore CA1000 { - get => s_instanceLazy.Value; + get => s_instance ? s_instance! : s_instanceLazy.Value; + } + + /// + /// Called when the script instance is being loaded. + /// + protected virtual void Awake() + { + s_instance = (T?)this; } /// @@ -26,17 +37,24 @@ public abstract class Singleton : MonoBehaviour /// protected virtual void OnDestroy() { + s_instance = null; s_instanceLazy = new Lazy(CreateLazyInstanceInternal, false); } private static T CreateLazyInstanceInternal() { + if (s_instance) + { + return s_instance!; + } + if (FindObjectOfType() is { } instance) { + s_instance = instance; return instance; } var gameObject = new GameObject {name = typeof(T).Name}; - return gameObject.AddComponent(); + return s_instance = gameObject.AddComponent(); } } diff --git a/X10D.Unity/src/WaitForFrames.cs b/X10D.Unity/src/WaitForFrames.cs new file mode 100644 index 0000000..d6d69a8 --- /dev/null +++ b/X10D.Unity/src/WaitForFrames.cs @@ -0,0 +1,40 @@ +using System.Collections; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction that waits for a specific number of frames. +/// +public struct WaitForFrames : IEnumerator +{ + private readonly int _frameCount; + private int _frameIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The frame count. + public WaitForFrames(int frameCount) + { + _frameCount = frameCount; + _frameIndex = 0; + } + + /// + public object Current + { + get => _frameCount; + } + + /// + public bool MoveNext() + { + return ++_frameIndex <= _frameCount; + } + + /// + public void Reset() + { + _frameIndex = 0; + } +} diff --git a/X10D.Unity/src/WaitForKeyDown.cs b/X10D.Unity/src/WaitForKeyDown.cs new file mode 100644 index 0000000..0ffaaf4 --- /dev/null +++ b/X10D.Unity/src/WaitForKeyDown.cs @@ -0,0 +1,38 @@ +using System.Collections; +using UnityEngine; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction that waits for a key to be pressed. +/// +public readonly struct WaitForKeyDown : IEnumerator +{ + private readonly KeyCode _keyCode; + + /// + /// Initializes a new instance of the struct. + /// + /// The key to wait for. + public WaitForKeyDown(KeyCode keyCode) + { + _keyCode = keyCode; + } + + /// + public object Current + { + get => _keyCode == KeyCode.None ? Input.anyKeyDown : Input.GetKeyDown(_keyCode); + } + + /// + public bool MoveNext() + { + return !(_keyCode == KeyCode.None ? Input.anyKeyDown : Input.GetKeyDown(_keyCode)); + } + + /// + public void Reset() + { + } +} diff --git a/X10D.Unity/src/WaitForKeyUp.cs b/X10D.Unity/src/WaitForKeyUp.cs new file mode 100644 index 0000000..9cea34d --- /dev/null +++ b/X10D.Unity/src/WaitForKeyUp.cs @@ -0,0 +1,38 @@ +using System.Collections; +using UnityEngine; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction that waits for a key to be released. +/// +public readonly struct WaitForKeyUp : IEnumerator +{ + private readonly KeyCode _keyCode; + + /// + /// Initializes a new instance of the struct. + /// + /// The key to wait for. + public WaitForKeyUp(KeyCode keyCode) + { + _keyCode = keyCode; + } + + /// + public object Current + { + get => _keyCode == KeyCode.None || Input.GetKeyUp(_keyCode); + } + + /// + public bool MoveNext() + { + return !(_keyCode == KeyCode.None || Input.GetKeyUp(_keyCode)); + } + + /// + public void Reset() + { + } +} diff --git a/X10D.Unity/src/WaitForSecondsNoAlloc.cs b/X10D.Unity/src/WaitForSecondsNoAlloc.cs new file mode 100644 index 0000000..c10b751 --- /dev/null +++ b/X10D.Unity/src/WaitForSecondsNoAlloc.cs @@ -0,0 +1,41 @@ +using System.Collections; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction which waits for a specified amount of seconds. +/// +/// This struct exists as an allocation-free alternative to . +public struct WaitForSecondsNoAlloc : IEnumerator +{ + private readonly float _duration; + private float _delta; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration of the pause, in seconds. + public WaitForSecondsNoAlloc(float duration) + { + _duration = duration; + _delta = 0f; + } + + /// + public object Current + { + get => _delta; + } + + /// + public bool MoveNext() + { + _delta += UnityEngine.Time.deltaTime; + return _delta < _duration; + } + + /// + public void Reset() + { + } +} diff --git a/X10D.Unity/src/WaitForSecondsRealtimeNoAlloc.cs b/X10D.Unity/src/WaitForSecondsRealtimeNoAlloc.cs new file mode 100644 index 0000000..3335336 --- /dev/null +++ b/X10D.Unity/src/WaitForSecondsRealtimeNoAlloc.cs @@ -0,0 +1,38 @@ +using System.Collections; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction which waits for a given amount of time, as provided by a . +/// +/// This struct exists as an allocation-free alternative to . +public readonly struct WaitForSecondsRealtimeNoAlloc : IEnumerator +{ + private readonly DateTime _expectedEnd; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration of the pause, in seconds. + public WaitForSecondsRealtimeNoAlloc(float duration) + { + _expectedEnd = DateTime.Now + TimeSpan.FromSeconds(duration); + } + + /// + public object Current + { + get => DateTime.Now; + } + + /// + public bool MoveNext() + { + return DateTime.Now < _expectedEnd; + } + + /// + public void Reset() + { + } +} diff --git a/X10D.Unity/src/WaitForTimeSpan.cs b/X10D.Unity/src/WaitForTimeSpan.cs new file mode 100644 index 0000000..11b83c1 --- /dev/null +++ b/X10D.Unity/src/WaitForTimeSpan.cs @@ -0,0 +1,42 @@ +using System.Collections; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction which waits for a given amount of time, as provided by a . +/// +public struct WaitForTimeSpan : IEnumerator +{ + private readonly TimeSpan _duration; + private readonly DateTime _start; + private DateTime _current; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration of the pause. + public WaitForTimeSpan(TimeSpan duration) + { + _duration = duration; + _start = DateTime.Now; + _current = _start; + } + + /// + public object Current + { + get => _current; + } + + /// + public bool MoveNext() + { + _current += TimeSpan.FromSeconds(UnityEngine.Time.deltaTime); + return _current < _start + _duration; + } + + /// + public void Reset() + { + } +} diff --git a/X10D.Unity/src/WaitForTimeSpanRealtime.cs b/X10D.Unity/src/WaitForTimeSpanRealtime.cs new file mode 100644 index 0000000..28ed33f --- /dev/null +++ b/X10D.Unity/src/WaitForTimeSpanRealtime.cs @@ -0,0 +1,37 @@ +using System.Collections; + +namespace X10D.Unity; + +/// +/// Represents a yield instruction which waits for a given amount of time, as provided by a . +/// +public readonly struct WaitForTimeSpanRealtime : IEnumerator +{ + private readonly DateTime _expectedEnd; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration of the pause. + public WaitForTimeSpanRealtime(TimeSpan duration) + { + _expectedEnd = DateTime.Now + duration; + } + + /// + public object Current + { + get => DateTime.Now; + } + + /// + public bool MoveNext() + { + return DateTime.Now < _expectedEnd; + } + + /// + public void Reset() + { + } +} diff --git a/X10D.UpmPackageGenerator/Program.cs b/X10D.UpmPackageGenerator/Program.cs new file mode 100644 index 0000000..62b1747 --- /dev/null +++ b/X10D.UpmPackageGenerator/Program.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using System.Text.Json; + +string version; + +string? githubSha = Environment.GetEnvironmentVariable("GITHUB_SHA"); +Console.WriteLine(string.IsNullOrWhiteSpace(githubSha) + ? "GITHUB_SHA environment variable not found. This is not a CI run." + : $"Building from commit {githubSha}."); + +if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0])) +{ + Console.WriteLine("No input file specified. Attempting to use GITHUB_SHA."); + version = githubSha ?? "0.0.0"; +} +else +{ + string path = args[0]; + var assembly = Assembly.LoadFrom(path); + var attribute = assembly.GetCustomAttribute(); + if (attribute is null || string.IsNullOrWhiteSpace(attribute.InformationalVersion)) + { + Console.WriteLine("AssemblyInformationalVersionAttribute not found. Attempting to use GITHUB_SHA."); + version = githubSha ?? "0.0.0"; + } + else + { + Console.WriteLine("AssemblyInformationalVersionAttribute found."); + version = attribute.InformationalVersion; + } +} + +Console.WriteLine($"Building for version {version}."); + +var package = new +{ + name = "me.olivr.x10d", + author = new {name = "Oliver Booth", email = "me@olivr.me", url = "https://oliverbooth.dev"}, + displayName = "X10D", + version, + unity = "2021.3", + description = "Extension methods on crack", + keywords = new[] {"dotnet", "extension-methods"}, + changelogUrl = "https://github.com/oliverbooth/X10D/blob/main/CHANGELOG.md", + licensesUrl = "https://github.com/oliverbooth/X10D/blob/main/LICENSE.md" +}; + +using FileStream stream = File.Open("package.json", FileMode.Create, FileAccess.ReadWrite); +Console.WriteLine("Serializing package.json."); +JsonSerializer.Serialize(stream, package, new JsonSerializerOptions {WriteIndented = true}); +stream.Position = 0; +stream.CopyTo(Console.OpenStandardOutput()); diff --git a/X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj b/X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj new file mode 100644 index 0000000..277ab31 --- /dev/null +++ b/X10D.UpmPackageGenerator/X10D.UpmPackageGenerator.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + enable + enable + true + true + + + diff --git a/X10D.sln b/X10D.sln index e6f0bcc..dcd6a5a 100644 --- a/X10D.sln +++ b/X10D.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28902.138 @@ -15,7 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CONTRIBUTING.md = CONTRIBUTING.md LICENSE.md = LICENSE.md README.md = README.md - icon.png = icon.png + branding_Icon.png = branding_Icon.png EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.SourceValidator", "X10D.SourceValidator\X10D.SourceValidator.csproj", "{84750149-9068-4780-AFDE-CDA1AC57007D}" @@ -24,6 +24,25 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.Unity", "X10D.Unity\X1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.SourceGenerator", "X10D.SourceGenerator\X10D.SourceGenerator.csproj", "{077A5D33-AD55-4C55-8A67-972CEBC32C7A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.DSharpPlus", "X10D.DSharpPlus\X10D.DSharpPlus.csproj", "{675D3B25-7EA0-4FC3-B513-8DF27874F2CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.Hosting", "X10D.Hosting\X10D.Hosting.csproj", "{B04AF429-30CF-4B69-81BA-38F560CA9126}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{1FC74E58-F3BA-4F1A-8693-5F80895DA69D}" + ProjectSection(SolutionItems) = preProject + .github\workflows\activate-unity.yml = .github\workflows\activate-unity.yml + .github\workflows\docfx.yml = .github\workflows\docfx.yml + .github\workflows\dotnet.yml = .github\workflows\dotnet.yml + .github\workflows\nightly.yml = .github\workflows\nightly.yml + .github\workflows\prerelease.yml = .github\workflows\prerelease.yml + .github\workflows\release.yml = .github\workflows\release.yml + .github\workflows\sonarcloud.yml = .github\workflows\sonarcloud.yml + .github\workflows\source_validator.yml = .github\workflows\source_validator.yml + .github\workflows\unity.yml = .github\workflows\unity.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X10D.UpmPackageGenerator", "X10D.UpmPackageGenerator\X10D.UpmPackageGenerator.csproj", "{CCBF047D-1B01-45EC-8D89-B00B4AC482CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,6 +69,18 @@ Global {077A5D33-AD55-4C55-8A67-972CEBC32C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {077A5D33-AD55-4C55-8A67-972CEBC32C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {077A5D33-AD55-4C55-8A67-972CEBC32C7A}.Release|Any CPU.Build.0 = Release|Any CPU + {675D3B25-7EA0-4FC3-B513-8DF27874F2CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {675D3B25-7EA0-4FC3-B513-8DF27874F2CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {675D3B25-7EA0-4FC3-B513-8DF27874F2CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {675D3B25-7EA0-4FC3-B513-8DF27874F2CF}.Release|Any CPU.Build.0 = Release|Any CPU + {B04AF429-30CF-4B69-81BA-38F560CA9126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B04AF429-30CF-4B69-81BA-38F560CA9126}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B04AF429-30CF-4B69-81BA-38F560CA9126}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B04AF429-30CF-4B69-81BA-38F560CA9126}.Release|Any CPU.Build.0 = Release|Any CPU + {CCBF047D-1B01-45EC-8D89-B00B4AC482CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCBF047D-1B01-45EC-8D89-B00B4AC482CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCBF047D-1B01-45EC-8D89-B00B4AC482CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCBF047D-1B01-45EC-8D89-B00B4AC482CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -57,4 +88,6 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6F733785-8837-410C-BD91-C4AD1F0A6914} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection EndGlobal diff --git a/X10D/Resource.Designer.cs b/X10D/Resource.Designer.cs deleted file mode 100644 index 02e56e9..0000000 --- a/X10D/Resource.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace X10D { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("X10D.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Must specify valid information for parsing in the string.. - /// - internal static string EnumParseEmptyStringException { - get { - return ResourceManager.GetString("EnumParseEmptyStringException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type provided must be an Enum.. - /// - internal static string EnumParseNotEnumException { - get { - return ResourceManager.GetString("EnumParseNotEnumException", resourceCulture); - } - } - } -} diff --git a/X10D/Resource.resx b/X10D/Resource.resx deleted file mode 100644 index 3a48c16..0000000 --- a/X10D/Resource.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - Must specify valid information for parsing in the string. - - - Type provided must be an Enum. - - \ No newline at end of file diff --git a/X10D/X10D.csproj b/X10D/X10D.csproj index 346271b..2afcbcb 100644 --- a/X10D/X10D.csproj +++ b/X10D/X10D.csproj @@ -1,8 +1,8 @@ - + - net6.0;netstandard2.1 - 10.0 + net7.0;net6.0;netstandard2.1 + 11.0 true true Oliver Booth @@ -11,14 +11,23 @@ git Extension methods on crack. LICENSE.md - icon.png + branding_Icon.png dotnet extension-methods + README.md + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) true - 3.1.0 + 3.2.0 enable true true + true + pdbonly + true + + + + true @@ -40,7 +49,7 @@ - + True @@ -48,6 +57,10 @@ True + + True + + True @@ -55,11 +68,6 @@ - - True - True - Resource.resx - True True @@ -68,10 +76,6 @@ - - ResXFileCodeGenerator - Resource.Designer.cs - ResXFileCodeGenerator ExceptionMessages.Designer.cs @@ -79,9 +83,14 @@ - + - \ No newline at end of file + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/X10D/src/Assembly.cs b/X10D/src/Assembly.cs index f547610..a2c26c1 100644 --- a/X10D/src/Assembly.cs +++ b/X10D/src/Assembly.cs @@ -1 +1,4 @@ -[assembly: CLSCompliant(true)] +using System.Runtime.CompilerServices; + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("X10D.Tests")] diff --git a/X10D/src/Collections/ArrayExtensions.cs b/X10D/src/Collections/ArrayExtensions.cs index 816fabd..c6ed75b 100644 --- a/X10D/src/Collections/ArrayExtensions.cs +++ b/X10D/src/Collections/ArrayExtensions.cs @@ -58,14 +58,8 @@ public static class ArrayExtensions } #endif - int index = range.Start.Value; - int end = range.End.Value; - if (range.End.IsFromEnd) - { - end = array.Length - end; - } - - array.Clear(index, end - index); + (int offset, int length) = range.GetOffsetAndLength(array.Length); + array.Clear(offset, length); } /// diff --git a/X10D/src/Collections/BoolListExtensions.cs b/X10D/src/Collections/BoolListExtensions.cs index ee70050..347d19b 100644 --- a/X10D/src/Collections/BoolListExtensions.cs +++ b/X10D/src/Collections/BoolListExtensions.cs @@ -29,7 +29,7 @@ public static class BoolListExtensions if (source.Count > 8) { - throw new ArgumentException("Source cannot contain more than than 8 elements.", nameof(source)); + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); } byte result = 0; @@ -63,7 +63,7 @@ public static class BoolListExtensions if (source.Count > 16) { - throw new ArgumentException("Source cannot contain more than than 16 elements.", nameof(source)); + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); } short result = 0; @@ -97,7 +97,7 @@ public static class BoolListExtensions if (source.Count > 32) { - throw new ArgumentException("Source cannot contain more than than 32 elements.", nameof(source)); + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); } var result = 0; @@ -131,7 +131,7 @@ public static class BoolListExtensions if (source.Count > 64) { - throw new ArgumentException("Source cannot contain more than than 64 elements.", nameof(source)); + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); } var result = 0L; diff --git a/X10D/src/Collections/ByteExtensions.cs b/X10D/src/Collections/ByteExtensions.cs index be77738..d299cf4 100644 --- a/X10D/src/Collections/ByteExtensions.cs +++ b/X10D/src/Collections/ByteExtensions.cs @@ -1,4 +1,11 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif namespace X10D.Collections; @@ -15,11 +22,16 @@ public static class ByteExtensions /// The value to unpack. /// An array of with length 8. [Pure] +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static bool[] Unpack(this byte value) { - Span buffer = stackalloc bool[Size]; + var buffer = new bool[Size]; value.Unpack(buffer); - return buffer.ToArray(); + return buffer; } /// @@ -28,16 +40,67 @@ public static class ByteExtensions /// The value to unpack. /// When this method returns, contains the unpacked booleans from . /// is not large enough to contain the result. + [ExcludeFromCodeCoverage] +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static void Unpack(this byte value, Span destination) { if (destination.Length < Size) { - throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination)); } +#if NETCOREAPP3_0_OR_GREATER + if (Sse3.IsSupported) + { + UnpackInternal_Ssse3(value, destination); + return; + } +#endif + + UnpackInternal_Fallback(value, destination); + } + +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static void UnpackInternal_Fallback(this byte value, Span destination) + { for (var index = 0; index < Size; index++) { destination[index] = (value & (1 << index)) != 0; } } + +#if NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal unsafe static void UnpackInternal_Ssse3(this byte value, Span destination) + { + fixed (bool* pDestination = destination) + { + var mask2 = Vector128.Create( + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 + ); + var mask1 = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1); + + Vector128 vec = Vector128.Create(value).AsByte(); + Vector128 shuffle = Ssse3.Shuffle(vec, mask1); + Vector128 and = Sse2.AndNot(shuffle, mask2); + Vector128 cmp = Sse2.CompareEqual(and, Vector128.Zero); + Vector128 correctness = Sse2.And(cmp, Vector128.Create((byte)0x01)); + + Sse2.StoreScalar((long*)pDestination, correctness.AsInt64()); + } + } +#endif } diff --git a/X10D/src/Collections/CollectionExtensions.cs b/X10D/src/Collections/CollectionExtensions.cs index 6bad398..3554586 100644 --- a/X10D/src/Collections/CollectionExtensions.cs +++ b/X10D/src/Collections/CollectionExtensions.cs @@ -16,14 +16,18 @@ public static class CollectionExtensions /// public static void ClearAndDisposeAll(this ICollection source) where T : IDisposable { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); } +#endif if (source.IsReadOnly) { - throw new InvalidOperationException("Collection is read-only. Try using DisposeAll instead."); + throw new InvalidOperationException(ExceptionMessages.CollectionIsReadOnly_DisposeAll); } foreach (T item in source) @@ -51,14 +55,18 @@ public static class CollectionExtensions /// public static async Task ClearAndDisposeAllAsync(this ICollection source) where T : IAsyncDisposable { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); } +#endif if (source.IsReadOnly) { - throw new InvalidOperationException("Collection is read-only. Try using DisposeAllAsync instead."); + throw new InvalidOperationException(ExceptionMessages.CollectionIsReadOnly_DisposeAllAsync); } foreach (T item in source) @@ -69,7 +77,7 @@ public static class CollectionExtensions continue; } - await item.DisposeAsync(); + await item.DisposeAsync().ConfigureAwait(false); } source.Clear(); diff --git a/X10D/src/Collections/DictionaryExtensions.cs b/X10D/src/Collections/DictionaryExtensions.cs index 86ae231..493088b 100644 --- a/X10D/src/Collections/DictionaryExtensions.cs +++ b/X10D/src/Collections/DictionaryExtensions.cs @@ -1,4 +1,7 @@ using System.Diagnostics.Contracts; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif using System.Web; namespace X10D.Collections; @@ -8,6 +11,64 @@ namespace X10D.Collections; /// public static class DictionaryExtensions { + /// + /// Adds a key/value pair to the if the key does not already exist, or updates a + /// key/value pair in the by using the specified function if the key already + /// exists. + /// + /// The dictionary to update. + /// The key to be added or whose value should be updated. + /// The value to be added for an absent key. + /// + /// The function used to generate a new value for an existing key based on the key's existing value. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// The new value for the key. This will be either be (if the key was absent) or the result + /// of (if the key was present). + /// + /// + /// is . + /// -or- + /// is . + /// + public static TValue AddOrUpdate(this Dictionary dictionary, TKey key, TValue addValue, + Func updateValueFactory) + where TKey : notnull + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(updateValueFactory); +#else + if (dictionary is null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + if (updateValueFactory is null) + { + throw new ArgumentNullException(nameof(updateValueFactory)); + } +#endif + +#if NET6_0_OR_GREATER + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists); + return value = exists ? updateValueFactory(key, value!) : addValue; +#else + if (dictionary.TryGetValue(key, out TValue? old)) + { + TValue updated = updateValueFactory(key, old); + dictionary[key] = updated; + + return updated; + } + + dictionary.Add(key, addValue); + return addValue; +#endif + } + /// /// Adds a key/value pair to the if the key does not already exist, or updates a /// key/value pair in the by using the specified function if the key already @@ -36,31 +97,97 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(updateValueFactory); #else if (dictionary is null) { throw new ArgumentNullException(nameof(dictionary)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(updateValueFactory); -#else + if (updateValueFactory is null) { throw new ArgumentNullException(nameof(updateValueFactory)); } #endif - if (dictionary.ContainsKey(key)) + if (dictionary.TryGetValue(key, out TValue? old)) { - dictionary[key] = updateValueFactory(key, dictionary[key]); - } - else - { - dictionary.Add(key, addValue); + TValue updated = updateValueFactory(key, old); + dictionary[key] = updated; + + return updated; } - return dictionary[key]; + dictionary.Add(key, addValue); + return addValue; + } + + /// + /// Uses the specified functions to add a key/value pair to the if the key does + /// not already exist, or to update a key/value pair in the if the key already + /// exists. + /// + /// The dictionary to update. + /// The key to be added or whose value should be updated. + /// The function used to generate a value for an absent key. + /// + /// The function used to generate a new value for an existing key based on the key's existing value. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// The new value for the key. This will be either be the result of (if the key was + /// absent) or the result of (if the key was present). + /// + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + public static TValue AddOrUpdate(this Dictionary dictionary, TKey key, + Func addValueFactory, Func updateValueFactory) + where TKey : notnull + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(addValueFactory); + ArgumentNullException.ThrowIfNull(updateValueFactory); +#else + if (dictionary is null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + if (addValueFactory is null) + { + throw new ArgumentNullException(nameof(addValueFactory)); + } + + if (updateValueFactory is null) + { + throw new ArgumentNullException(nameof(updateValueFactory)); + } +#endif + +#if NET6_0_OR_GREATER + ref TValue? value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists); + return value = exists ? updateValueFactory(key, value!) : addValueFactory(key); +#else + if (dictionary.TryGetValue(key, out TValue? old)) + { + TValue updated = updateValueFactory(key, old); + dictionary[key] = updated; + + return updated; + } + + TValue add = addValueFactory(key); + dictionary.Add(key, add); + + return add; +#endif } /// @@ -93,39 +220,111 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(addValueFactory); + ArgumentNullException.ThrowIfNull(updateValueFactory); #else if (dictionary is null) { throw new ArgumentNullException(nameof(dictionary)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(addValueFactory); -#else + if (addValueFactory is null) { throw new ArgumentNullException(nameof(addValueFactory)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(updateValueFactory); -#else + if (updateValueFactory is null) { throw new ArgumentNullException(nameof(updateValueFactory)); } #endif - if (dictionary.ContainsKey(key)) + if (dictionary.TryGetValue(key, out TValue? old)) { - dictionary[key] = updateValueFactory(key, dictionary[key]); - } - else - { - dictionary.Add(key, addValueFactory(key)); + TValue updated = updateValueFactory(key, old); + dictionary[key] = updated; + + return updated; } - return dictionary[key]; + TValue add = addValueFactory(key); + dictionary.Add(key, add); + + return add; + } + + /// + /// Uses the specified functions and argument to add a key/value pair to the if + /// the key does not already exist, or to update a key/value pair in the if th + /// key already exists. + /// + /// The dictionary to update. + /// The key to be added or whose value should be updated. + /// The function used to generate a value for an absent key. + /// + /// The function used to generate a new value for an existing key based on the key's existing value. + /// + /// + /// An argument to pass into and . + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// The type of an argument to pass into and . + /// + /// + /// The new value for the key. This will be either be the result of (if the key was + /// absent) or the result of (if the key was present). + /// + /// + /// is . + /// -or- + /// is . + /// -or- + /// is . + /// + public static TValue AddOrUpdate(this Dictionary dictionary, TKey key, + Func addValueFactory, Func updateValueFactory, TArg factoryArgument) + where TKey : notnull + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(addValueFactory); + ArgumentNullException.ThrowIfNull(updateValueFactory); +#else + if (dictionary is null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + if (addValueFactory is null) + { + throw new ArgumentNullException(nameof(addValueFactory)); + } + + if (updateValueFactory is null) + { + throw new ArgumentNullException(nameof(updateValueFactory)); + } +#endif + +#if NET6_0_OR_GREATER + ref TValue? value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists); + return value = exists ? updateValueFactory(key, value!, factoryArgument) : addValueFactory(key, factoryArgument); +#else + if (dictionary.TryGetValue(key, out TValue? old)) + { + TValue updated = updateValueFactory(key, old, factoryArgument); + dictionary[key] = updated; + + return updated; + } + + TValue add = addValueFactory(key, factoryArgument); + dictionary.Add(key, add); + + return add; +#endif } /// @@ -164,39 +363,37 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(addValueFactory); + ArgumentNullException.ThrowIfNull(updateValueFactory); #else if (dictionary is null) { throw new ArgumentNullException(nameof(dictionary)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(addValueFactory); -#else + if (addValueFactory is null) { throw new ArgumentNullException(nameof(addValueFactory)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(updateValueFactory); -#else + if (updateValueFactory is null) { throw new ArgumentNullException(nameof(updateValueFactory)); } #endif - if (dictionary.ContainsKey(key)) + if (dictionary.TryGetValue(key, out TValue? old)) { - dictionary[key] = updateValueFactory(key, dictionary[key], factoryArgument); - } - else - { - dictionary.Add(key, addValueFactory(key, factoryArgument)); + TValue updated = updateValueFactory(key, old, factoryArgument); + dictionary[key] = updated; + + return updated; } - return dictionary[key]; + TValue add = addValueFactory(key, factoryArgument); + dictionary.Add(key, add); + + return add; } /// @@ -227,7 +424,7 @@ public static class DictionaryExtensions return string.Empty; } - return value.Contains(' ') ? $"\"{value}\"" : value; + return value.Contains(' ', StringComparison.Ordinal) ? $"\"{value}\"" : value; } static string GetQueryParameter(KeyValuePair pair) @@ -260,15 +457,13 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); #else if (source is null) { throw new ArgumentNullException(nameof(source)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(selector); -#else + if (selector is null) { throw new ArgumentNullException(nameof(selector)); @@ -282,7 +477,7 @@ public static class DictionaryExtensions return string.Empty; } - return value.Contains(' ') ? $"\"{value}\"" : value; + return value.Contains(' ', StringComparison.Ordinal) ? $"\"{value}\"" : value; } string GetQueryParameter(KeyValuePair pair) @@ -321,23 +516,19 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); + ArgumentNullException.ThrowIfNull(valueSelector); #else if (source is null) { throw new ArgumentNullException(nameof(source)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(keySelector); -#else + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(valueSelector); -#else + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); @@ -351,7 +542,7 @@ public static class DictionaryExtensions return string.Empty; } - return value.Contains(' ') ? $"\"{value}\"" : value; + return value.Contains(' ', StringComparison.Ordinal) ? $"\"{value}\"" : value; } string GetQueryParameter(KeyValuePair pair) @@ -415,15 +606,13 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); #else if (source is null) { throw new ArgumentNullException(nameof(source)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(selector); -#else + if (selector is null) { throw new ArgumentNullException(nameof(selector)); @@ -468,23 +657,19 @@ public static class DictionaryExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); + ArgumentNullException.ThrowIfNull(valueSelector); #else if (source is null) { throw new ArgumentNullException(nameof(source)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(keySelector); -#else + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(valueSelector); -#else + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); diff --git a/X10D/src/Collections/EnumerableExtensions.cs b/X10D/src/Collections/EnumerableExtensions.cs index 4a75370..78707e1 100644 --- a/X10D/src/Collections/EnumerableExtensions.cs +++ b/X10D/src/Collections/EnumerableExtensions.cs @@ -7,6 +7,108 @@ namespace X10D.Collections; /// public static class EnumerableExtensions { + /// + /// Returns a number that represents how many elements in the specified sequence do not satisfy a condition. + /// + /// A sequence that contains elements to be tested and counted. + /// A function to test each element for a condition. + /// The type of the elements of . + /// + /// A number that represents how many elements in the sequence do not satisfy the condition in the + /// function. + /// + /// or is null. + /// + /// The number of elements in is larger than . + /// + [Pure] + public static int CountWhereNot(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.Count(item => !predicate(item)); + } + + /// + /// Returns the first element in a sequence that does not satisfy a specified condition. + /// + /// An to return an element from. + /// A function to test each element for a condition. + /// The type of the elements in + /// The first element in the sequence that fails the test in the specified predicate function. + /// or is null. + /// + /// No element satisfies the condition in predicate. + /// -or- + /// The source sequence is empty. + /// + [Pure] + public static TSource FirstWhereNot(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.First(item => !predicate(item)); + } + + /// + /// Returns the first element in a sequence that does not satisfy a specified condition. + /// + /// An to return an element from. + /// A function to test each element for a condition. + /// The type of the elements in + /// + /// if is empty or if no element passes the test specified + /// by ; otherwise, the first element in that fails the test + /// specified by . + /// + /// or is null. + [Pure] + public static TSource? FirstWhereNotOrDefault(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.FirstOrDefault(item => !predicate(item)); + } + /// /// Performs the specified action on each element of the . /// @@ -25,6 +127,10 @@ public static class EnumerableExtensions /// public static void For(this IEnumerable source, Action action) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(action); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); @@ -34,6 +140,7 @@ public static class EnumerableExtensions { throw new ArgumentNullException(nameof(action)); } +#endif var index = 0; foreach (T item in source) @@ -59,6 +166,10 @@ public static class EnumerableExtensions /// public static void ForEach(this IEnumerable source, Action action) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(action); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); @@ -68,6 +179,7 @@ public static class EnumerableExtensions { throw new ArgumentNullException(nameof(action)); } +#endif foreach (T item in source) { @@ -84,10 +196,14 @@ public static class EnumerableExtensions /// public static void DisposeAll(this IEnumerable source) where T : IDisposable { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); } +#endif foreach (T item in source) { @@ -111,10 +227,14 @@ public static class EnumerableExtensions /// public static async Task DisposeAllAsync(this IEnumerable source) where T : IAsyncDisposable { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else if (source is null) { throw new ArgumentNullException(nameof(source)); } +#endif foreach (T item in source) { @@ -124,10 +244,77 @@ public static class EnumerableExtensions continue; } - await item.DisposeAsync(); + await item.DisposeAsync().ConfigureAwait(false); } } + /// + /// Returns the last element in a sequence that does not satisfy a specified condition. + /// + /// An to return an element from. + /// A function to test each element for a condition. + /// The type of the elements in + /// The last element in the sequence that fails the test in the specified predicate function. + /// or is null. + /// + /// No element satisfies the condition in predicate. + /// -or- + /// The source sequence is empty. + /// + [Pure] + public static TSource LastWhereNot(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.Last(item => !predicate(item)); + } + + /// + /// Returns the last element in a sequence that does not satisfy a specified condition. + /// + /// An to return an element from. + /// A function to test each element for a condition. + /// The type of the elements in + /// + /// if is empty or if no element passes the test specified + /// by ; otherwise, the last element in that fails the test + /// specified by . + /// + /// or is null. + [Pure] + public static TSource? LastWhereNotOrDefault(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.LastOrDefault(item => !predicate(item)); + } + /// /// Reorganizes the elements in an enumerable by implementing a Fisher-Yates shuffle, and returns th shuffled result. /// @@ -152,4 +339,59 @@ public static class EnumerableExtensions list.Shuffle(random); return list.AsReadOnly(); } + + /// + /// Filters a sequence of values based on a predicate, such that all elements in the result do not match the predicate. + /// + /// An to filter. + /// A function to test each element for a condition. + /// The type of the elements of . + /// + /// An that contains elements from the input sequence that do not satisfy the condition. + /// + /// or is null. + [Pure] + public static IEnumerable WhereNot(this IEnumerable source, Func predicate) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } +#endif + + return source.Where(item => !predicate(item)); + } + + /// + /// Filters a sequence of values by omitting elements that are ( in + /// Visual Basic). + /// + /// An to filter. + /// The type of the elements of . + /// + /// An that contains elements from the input sequence that are not + /// ( in Visual Basic). + /// + public static IEnumerable WhereNotNull(this IEnumerable source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return source.Where(item => item is not null).Select(item => item!); + } } diff --git a/X10D/src/Collections/Int16Extensions.cs b/X10D/src/Collections/Int16Extensions.cs index 366cb12..aaffabe 100644 --- a/X10D/src/Collections/Int16Extensions.cs +++ b/X10D/src/Collections/Int16Extensions.cs @@ -1,4 +1,11 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif namespace X10D.Collections; @@ -15,11 +22,16 @@ public static class Int16Extensions /// The value to unpack. /// An array of with length 16. [Pure] +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static bool[] Unpack(this short value) { - Span buffer = stackalloc bool[Size]; - value.Unpack(buffer); - return buffer.ToArray(); + var ret = new bool[Size]; + value.Unpack(ret); + return ret; } /// @@ -28,16 +40,69 @@ public static class Int16Extensions /// The value to unpack. /// When this method returns, contains the unpacked booleans from . /// is not large enough to contain the result. + [ExcludeFromCodeCoverage] +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static void Unpack(this short value, Span destination) { if (destination.Length < Size) { - throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination)); } +#if NETCOREAPP3_0_OR_GREATER + if (Sse3.IsSupported) + { + UnpackInternal_Ssse3(value, destination); + return; + } +#endif + + UnpackInternal_Fallback(value, destination); + } + +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static void UnpackInternal_Fallback(this short value, Span destination) + { for (var index = 0; index < Size; index++) { destination[index] = (value & (1 << index)) != 0; } } + +#if NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal unsafe static void UnpackInternal_Ssse3(this short value, Span destination) + { + fixed (bool* pDestination = destination) + { + var mask2 = Vector128.Create( + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 + ); + + var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1); + var one = Vector128.Create((byte)0x01); + + Vector128 vec = Vector128.Create(value).AsByte(); + Vector128 shuffle = Ssse3.Shuffle(vec, mask1Lo); + Vector128 and = Sse2.AndNot(shuffle, mask2); + Vector128 cmp = Sse2.CompareEqual(and, Vector128.Zero); + Vector128 correctness = Sse2.And(cmp, one); + + Sse2.Store((byte*)pDestination, correctness); + } + } +#endif } diff --git a/X10D/src/Collections/Int32Extensions.cs b/X10D/src/Collections/Int32Extensions.cs index 82da040..f718c73 100644 --- a/X10D/src/Collections/Int32Extensions.cs +++ b/X10D/src/Collections/Int32Extensions.cs @@ -1,4 +1,11 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif namespace X10D.Collections; @@ -17,9 +24,9 @@ public static class Int32Extensions [Pure] public static bool[] Unpack(this int value) { - Span buffer = stackalloc bool[Size]; - value.Unpack(buffer); - return buffer.ToArray(); + var ret = new bool[Size]; + value.Unpack(ret); + return ret; } /// @@ -28,16 +35,105 @@ public static class Int32Extensions /// The value to unpack. /// When this method returns, contains the unpacked booleans from . /// is not large enough to contain the result. + [ExcludeFromCodeCoverage] public static void Unpack(this int value, Span destination) { if (destination.Length < Size) { - throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination)); } +#if NETCOREAPP3_0_OR_GREATER + if (Avx2.IsSupported) + { + UnpackInternal_Avx2(value, destination); + return; + } + + if (Sse3.IsSupported) + { + UnpackInternal_Ssse3(value, destination); + return; + } +#endif + + UnpackInternal_Fallback(value, destination); + } + +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static void UnpackInternal_Fallback(this int value, Span destination) + { for (var index = 0; index < Size; index++) { destination[index] = (value & (1 << index)) != 0; } } + +#if NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP3_1_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static unsafe void UnpackInternal_Ssse3(this int value, Span destination) + { + fixed (bool* pDestination = destination) + { + var mask2 = Vector128.Create( + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 + ); + var mask1Lo = Vector128.Create((byte)0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1); + var mask1Hi = Vector128.Create((byte)2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3); + + var one = Vector128.Create((byte)0x01); + + Vector128 vec = Vector128.Create(value).AsByte(); + Vector128 shuffle = Ssse3.Shuffle(vec, mask1Lo); + Vector128 and = Sse2.AndNot(shuffle, mask2); + Vector128 cmp = Sse2.CompareEqual(and, Vector128.Zero); + Vector128 correctness = Sse2.And(cmp, one); + + Sse2.Store((byte*)pDestination, correctness); + + shuffle = Ssse3.Shuffle(vec, mask1Hi); + and = Sse2.AndNot(shuffle, mask2); + cmp = Sse2.CompareEqual(and, Vector128.Zero); + correctness = Sse2.And(cmp, one); + + Sse2.Store((byte*)pDestination + 16, correctness); + } + } + + internal static unsafe void UnpackInternal_Avx2(this int value, Span destination) + { + fixed (bool* pDestination = destination) + { + var mask1 = Vector256.Create( + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 + ).AsByte(); + var mask2 = Vector256.Create( + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 + ); + + Vector256 vec = Vector256.Create(value).AsByte(); + Vector256 shuffle = Avx2.Shuffle(vec, mask1); + Vector256 and = Avx2.AndNot(shuffle, mask2); + Vector256 cmp = Avx2.CompareEqual(and, Vector256.Zero); + Vector256 correctness = Avx2.And(cmp, Vector256.Create((byte)0x01)); + + Avx.Store((byte*)pDestination, correctness); + } + } +#endif } diff --git a/X10D/src/Collections/Int64Extensions.cs b/X10D/src/Collections/Int64Extensions.cs index d9c07ba..5b31a1d 100644 --- a/X10D/src/Collections/Int64Extensions.cs +++ b/X10D/src/Collections/Int64Extensions.cs @@ -17,9 +17,9 @@ public static class Int64Extensions [Pure] public static bool[] Unpack(this long value) { - Span buffer = stackalloc bool[Size]; - value.Unpack(buffer); - return buffer.ToArray(); + var ret = new bool[Size]; + value.Unpack(ret); + return ret; } /// @@ -32,7 +32,7 @@ public static class Int64Extensions { if (destination.Length < Size) { - throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination)); } for (var index = 0; index < Size; index++) diff --git a/X10D/src/Collections/ListExtensions.cs b/X10D/src/Collections/ListExtensions.cs index bb0b601..69292b4 100644 --- a/X10D/src/Collections/ListExtensions.cs +++ b/X10D/src/Collections/ListExtensions.cs @@ -1,6 +1,8 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.Contracts; using X10D.Core; +#pragma warning disable CA5394 + namespace X10D.Collections; /// @@ -86,6 +88,131 @@ public static class ListExtensions } } + /// + /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire + /// . + /// + /// The list to search + /// + /// The object to locate in the . The value can be for reference + /// types. + /// + /// The type of elements in . + /// + /// The zero-based index of the first occurrence of item within the entire , if found; otherwise, + /// -1. + /// + /// is . + public static int IndexOf(this IReadOnlyList source, T? item) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return source.IndexOf(item, 0, source.Count); + } + + /// + /// Searches for the specified object and returns the zero-based index of the first occurrence within the range of + /// elements in the that extends from the specified index to the last element. + /// + /// The list to search + /// + /// The object to locate in the . The value can be for reference + /// types. + /// + /// The zero-based starting index of the search. 0 (zero) is valid in an empty list. + /// The type of elements in . + /// + /// The zero-based index of the first occurrence of item within the range of elements in the + /// that starts at index and contains count number of elements, if found; otherwise, -1. + /// + /// is . + /// + /// is outside the range of valid indexes for the . + /// + public static int IndexOf(this IReadOnlyList source, T? item, int startIndex) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return source.IndexOf(item, startIndex, source.Count - startIndex); + } + + /// + /// Searches for the specified object and returns the zero-based index of the first occurrence within the range of + /// elements in the that starts at the specified index and contains the specified number + /// of elements. + /// + /// The list to search + /// + /// The object to locate in the . The value can be for reference + /// types. + /// + /// The zero-based starting index of the search. 0 (zero) is valid in an empty list. + /// The number of elements in the section to search. + /// The type of elements in . + /// + /// The zero-based index of the first occurrence of item within the range of elements in the + /// that starts at index and contains count number of elements, if found; otherwise, -1. + /// + /// is . + /// + /// + /// is outside the range of valid indexes for the . + /// + /// -or- + /// is less than 0. + /// -or- + /// + /// and do not specify a valid section in the + /// . + /// + /// + public static int IndexOf(this IReadOnlyList source, T? item, int startIndex, int count) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + if (startIndex < 0 || startIndex > source.Count) + { + throw new ArgumentOutOfRangeException(nameof(startIndex), ExceptionMessages.IndexOutOfRange); + } + + if (count < 0 || count > source.Count - startIndex) + { + throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.CountMustBeInRange); + } + + int endIndex = startIndex + count; + for (int index = startIndex; index < endIndex; index++) + { + if (EqualityComparer.Default.Equals(source[index]!, item!)) + { + return index; + } + } + + return -1; + } + /// /// Returns a random element from the current list using a specified instance. /// @@ -119,6 +246,48 @@ public static class ListExtensions return random.NextFrom(source); } + /// + /// Removes a range of elements from the list. + /// + /// The list whose elements to remove. + /// The range of elements to remove. + /// The type of the elements in . + /// is . + /// defines an invalid range. + /// + /// defines an end index whose value is greater than or equal to the count of elements in the + /// list. + /// + public static void RemoveRange(this IList source, Range range) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + int start = range.Start.IsFromEnd ? source.Count - range.Start.Value : range.Start.Value; + int end = range.End.IsFromEnd ? source.Count - range.End.Value : range.End.Value; + + if (end < start) + { + throw new ArgumentException(ExceptionMessages.EndIndexLessThanStartIndex); + } + + if (end >= source.Count) + { + throw new ArgumentOutOfRangeException(nameof(range), ExceptionMessages.EndIndexGreaterThanCount); + } + + for (int index = end; index >= start; index--) + { + source.RemoveAt(index); + } + } + /// /// Reorganizes the elements in a list by implementing a Fisher-Yates shuffle. /// @@ -149,4 +318,130 @@ public static class ListExtensions (source[count], source[index]) = (source[index], source[count]); } } + + /// + /// Forms a slice out of the current list that begins at a specified index. + /// + /// The list to slice. + /// The index at which to begin the slice. + /// The type of elements in . + /// + /// A list that consists of all elements of the current list from to the end of the list. + /// + /// is . + /// + /// is less than zero or greater than . + /// + public static IReadOnlyList Slice(this IReadOnlyList source, int start) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return source.Slice(start, source.Count - start); + } + + /// + /// Forms a slice out of the current list that begins at a specified index for a specified length. + /// + /// The list to slice. + /// The index at which to begin the slice. + /// The desired length for the slice. + /// The type of elements in . + /// + /// A list that consists of all elements of the current list from to the end of the list. + /// + /// is . + /// + /// or + is less than zero or greater than + /// . + /// + public static IReadOnlyList Slice(this IReadOnlyList source, int start, int length) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + if (start < 0 || start > source.Count) + { + throw new ArgumentOutOfRangeException(nameof(start), ExceptionMessages.IndexOutOfRange); + } + + if (length < 0 || length > source.Count - start) + { + throw new ArgumentOutOfRangeException(nameof(length), ExceptionMessages.CountMustBeInRange); + } + + var sliced = new List(); + + int endIndex = start + length; + for (int index = start; index < endIndex; index++) + { + sliced.Add(source[index]); + } + + return sliced.AsReadOnly(); + } + + /// + /// Swaps all elements in a list with the elements in another list. + /// + /// The first list. + /// The second list. + /// The type of the elements in and . + /// + /// is . + /// -or- + /// is . + /// + public static void Swap(this IList source, IList other) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(other); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } +#endif + + int min = System.Math.Min(source.Count, other.Count); + for (var index = 0; index < min; index++) + { + (source[index], other[index]) = (other[index], source[index]); + } + + if (other.Count < source.Count) + { + for (int index = min; index < source.Count;) + { + other.Add(source[index]); + source.RemoveAt(index); + } + } + else if (source.Count < other.Count) + { + for (int index = min; index < other.Count;) + { + source.Add(other[index]); + other.RemoveAt(index); + } + } + } } diff --git a/X10D/src/Collections/SpanExtensions.cs b/X10D/src/Collections/SpanExtensions.cs new file mode 100644 index 0000000..78d6c8b --- /dev/null +++ b/X10D/src/Collections/SpanExtensions.cs @@ -0,0 +1,115 @@ +namespace X10D.Collections; + +/// +/// Extension methods for and +/// +public static class SpanExtensions +{ + /// + /// Returns the number of times that a specified element appears in a span of elements of the same type. + /// + /// The source to search. + /// The element to count. + /// The type of elements in . + /// The number of times that appears in . + public static int Count(this in Span source, T element) + where T : IEquatable + { + return source.AsReadOnly().Count(element); + } + + /// + /// Returns the number of times that a specified element appears in a read-only span of elements of the same type. + /// + /// The source to search. + /// The element to count. + /// The type of elements in . + /// The number of times that appears in . + public static int Count(this in ReadOnlySpan source, T element) + where T : IEquatable + { + var count = 0; + + for (var index = 0; index < source.Length; index++) + { + T item = source[index]; + if (item.Equals(element)) + { + count++; + } + } + + return count; + } + + /// + /// Returns a read-only wrapper for the current span. + /// + /// The source span. + /// The type of elements in . + /// A which wraps the elements in . + public static ReadOnlySpan AsReadOnly(this in Span source) + { + return source; + } + + /// + /// Splits a span of elements into sub-spans based on a delimiting element. + /// + /// The span to split. + /// The delimiting element. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in Span source, T delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a delimiting element. + /// + /// The span to split. + /// The delimiting element. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in ReadOnlySpan source, T delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a span of delimiting elements. + /// + /// The span to split. + /// The span of delimiting elements. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in Span source, in ReadOnlySpan delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } + + /// + /// Splits a span of elements into sub-spans based on a span of delimiting elements. + /// + /// The span to split. + /// The span of delimiting elements. + /// The type of elements in . + /// + /// An enumerator which wraps and delimits the elements based on . + /// + public static SpanSplitEnumerator Split(this in ReadOnlySpan source, in ReadOnlySpan delimiter) + where T : struct, IEquatable + { + return new SpanSplitEnumerator(source, delimiter); + } +} diff --git a/X10D/src/Collections/SpanSplitEnumerator.cs b/X10D/src/Collections/SpanSplitEnumerator.cs new file mode 100644 index 0000000..c99d9e7 --- /dev/null +++ b/X10D/src/Collections/SpanSplitEnumerator.cs @@ -0,0 +1,87 @@ +namespace X10D.Collections; + +/// +/// Enumerates the elements of a . +/// +/// The type of elements in the span. +public ref struct SpanSplitEnumerator where T : struct, IEquatable +{ + private ReadOnlySpan _source; + private readonly ReadOnlySpan _delimiterSpan; + private readonly T _delimiter; + private readonly bool _usingSpanDelimiter; + + /// + /// Initializes a new instance of the struct. + /// + /// The source span. + /// The delimiting span of elements. + public SpanSplitEnumerator(ReadOnlySpan source, ReadOnlySpan delimiter) + { + _usingSpanDelimiter = true; + _source = source; + _delimiter = default; + _delimiterSpan = delimiter; + Current = ReadOnlySpan.Empty; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The source span. + /// The delimiting element. + public SpanSplitEnumerator(ReadOnlySpan source, T delimiter) + { + _usingSpanDelimiter = false; + _source = source; + _delimiter = delimiter; + _delimiterSpan = ReadOnlySpan.Empty; + Current = ReadOnlySpan.Empty; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + /// The element in the at the current position of the enumerator. + public ReadOnlySpan Current { get; private set; } + + /// + /// Returns the current enumerator. + /// + /// The current instance of . + /// + /// This method exists to provide the ability to enumerate within a foreach loop. It should not be called + /// manually. + /// + public readonly SpanSplitEnumerator GetEnumerator() + { + return this; + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the span. + /// + public bool MoveNext() + { + if (_source.Length == 0) + { + return false; + } + + int index = _usingSpanDelimiter ? _source.IndexOf(_delimiterSpan) : _source.IndexOf(_delimiter); + if (index == -1) + { + Current = _source; + _source = ReadOnlySpan.Empty; + return true; + } + + Current = _source[..index]; + _source = _source[(index + 1)..]; + return true; + } +} diff --git a/X10D/src/Core/IntrinsicExtensions.cs b/X10D/src/Core/IntrinsicExtensions.cs new file mode 100644 index 0000000..03916ac --- /dev/null +++ b/X10D/src/Core/IntrinsicExtensions.cs @@ -0,0 +1,190 @@ +#if NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace X10D.Core; + +/// +/// Extension methods for SIMD vectors, namely , and +/// . +/// +public static class IntrinsicExtensions +{ + /// + /// + /// Correcting of into 0 and 1 depend on their boolean truthiness. + /// + /// Operation:
+ /// + /// for (int i = 0; i < 8; i++) { + /// dest[i] = vector[i] == 0 ? 0 : 1; + /// } + /// + ///
+ /// Vector of byte to correct. + /// + /// A of which remapped back to 0 and 1 based on boolean truthiness. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static Vector64 CorrectBoolean(this Vector64 vector) + { + Vector64 output = IntrinsicUtility.GetUninitializedVector64(); + + for (var i = 0; i < Vector64.Count; i++) + { + ref byte writeElement = ref Unsafe.Add(ref Unsafe.As, byte>(ref output), i); +#if NET7_0_OR_GREATER + writeElement = vector[i] == 0 ? (byte)0 : (byte)1; +#else + byte element = Unsafe.Add(ref Unsafe.As, byte>(ref vector), i); + writeElement = element == 0 ? (byte)0 : (byte)1; +#endif + } + + return output; + } + + /// + /// + /// Correcting of into 0 and 1 depend on their boolean truthiness. + /// + /// Operation:
+ /// + /// for (int i = 0; i < 16; i++) { + /// dest[i] = vector[i] == 0 ? 0 : 1; + /// } + /// + ///
+ /// Vector of byte to correct. + /// + /// A of which remapped back to 0 and 1 based on boolean truthiness. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector128 CorrectBoolean(this Vector128 vector) + { + return Sse2.IsSupported ? CorrectBooleanInternal_Sse2(vector) : CorrectBooleanInternal_Fallback(vector); + } + + /// + /// + /// Correcting of into 0 and 1 depend on their boolean truthiness. + /// + /// Operation:
+ /// + /// for (int i = 0; i < 32; i++) { + /// dest[i] = vector[i] == 0 ? 0 : 1; + /// } + /// + ///
+ /// Vector of byte to correct. + /// + /// A of which remapped back to 0 and 1 based on boolean truthiness. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector256 CorrectBoolean(this Vector256 vector) + { + return Avx2.IsSupported ? CorrectBooleanInternal_Avx2(vector) : CorrectBooleanInternal_Fallback(vector); + } + + /// + /// + /// Reverse position of 2 64-bit unsigned integer. + /// + /// Operation:
+ /// + /// dest[1] = vector[0]; + /// dest[0] = vector[1]; + /// + ///
+ /// Input vector. + /// + /// A of with elements the same as input vector except their positions + /// (or indices) are reversed. + /// + [Pure] + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector128 ReverseElements(this Vector128 vector) + { + return Sse2.IsSupported ? ReverseElementsInternal_Sse2(vector) : ReverseElementsInternal_Fallback(vector); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 CorrectBooleanInternal_Fallback(this Vector128 vector) + { + Vector128 output = IntrinsicUtility.GetUninitializedVector128(); + + for (var index = 0; index < Vector128.Count; index++) + { + Unsafe.Add(ref Unsafe.As, byte>(ref output), index) = + Unsafe.Add(ref Unsafe.As, byte>(ref vector), index) == 0 ? (byte)0 : (byte)1; + } + + return output; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 CorrectBooleanInternal_Sse2(this Vector128 vector) + { + Vector128 cmp = Sse2.CompareEqual(vector, Vector128.Zero); + Vector128 result = Sse2.AndNot(cmp, Vector128.Create((byte)1)); + + return result; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector256 CorrectBooleanInternal_Fallback(this Vector256 vector) + { + Vector256 output = IntrinsicUtility.GetUninitializedVector256(); + + for (var index = 0; index < Vector256.Count; index++) + { + Unsafe.Add(ref Unsafe.As, byte>(ref output), index) = + Unsafe.Add(ref Unsafe.As, byte>(ref vector), index) == 0 ? (byte)0 : (byte)1; + } + + return output; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector256 CorrectBooleanInternal_Avx2(this Vector256 vector) + { + Vector256 cmp = Avx2.CompareEqual(vector, Vector256.Zero); + Vector256 result = Avx2.AndNot(cmp, Vector256.Create((byte)1)); + + return result; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 ReverseElementsInternal_Fallback(this Vector128 vector) + { + Vector128 output = IntrinsicUtility.GetUninitializedVector128(); + + Unsafe.As, ulong>(ref output) = Unsafe.Add(ref Unsafe.As, ulong>(ref vector), 1); + Unsafe.Add(ref Unsafe.As, ulong>(ref output), 1) = Unsafe.As, ulong>(ref vector); + + return output; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 ReverseElementsInternal_Sse2(this Vector128 vector) + { + return Sse2.Shuffle(vector.AsDouble(), vector.AsDouble(), 0b01).AsUInt64(); + } +} +#endif diff --git a/X10D/src/Core/IntrinsicUtility.cs b/X10D/src/Core/IntrinsicUtility.cs new file mode 100644 index 0000000..3bf6974 --- /dev/null +++ b/X10D/src/Core/IntrinsicUtility.cs @@ -0,0 +1,320 @@ +#if NETCOREAPP3_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace X10D.Core; + +/// +/// Provides utility methods for SIMD vector that is currently missing on common hardware instruction set. +/// +public static class IntrinsicUtility +{ + // NOTE: + // ANY METHOD THAT OPERATE ON ANYTHING THAT ISN'T FLOAT IS NOT SSE COMPATIBLE, MUST BE SSE2 AND BEYONDS + // FOR API CONSISTENCY. + + /// + /// + /// Multiply packed 64-bit unsigned integer elements in a and b and truncate the results to 64-bit integer. + /// + /// Operation:
+ /// + /// dest[0] = left[0] * right[0]; + /// dest[1] = left[1] * right[1]; + /// + ///
+ /// Left vector. + /// Right vector. + /// The truncated product vector. + [Pure] + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector128 Multiply(Vector128 left, Vector128 right) + { + if (Sse2.IsSupported) + { + return MultiplyInternal_Sse2(left, right); + } + + return MultiplyInternal_Fallback(left, right); + } + + /// + /// + /// Multiply packed 64-bit unsigned integer elements in a and b and truncate the results to 64-bit integer. + /// + /// Operation:
+ /// + /// dest[0] = left[0] * right[0]; + /// dest[1] = left[1] * right[1]; + /// dest[2] = left[2] * right[2]; + /// dest[3] = left[3] * right[3]; + /// + ///
+ /// Left vector. + /// Right vector. + /// + /// A of whose elements is 64-bit truncated product of lhs and rhs. + /// + [Pure] + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector256 Multiply(Vector256 lhs, Vector256 rhs) + { + if (Avx2.IsSupported) + { + return MultiplyInternal_Avx2(lhs, rhs); + } + + return MultiplyInternal_Fallback(lhs, rhs); + } + + /// + /// + /// Multiply packed 64-bit signed integer elements in a and b and truncate the results to 64-bit integer. + /// + /// Operation:
+ /// + /// dest[0] = left[0] * right[0]; + /// dest[1] = left[1] * right[1]; + /// + ///
+ /// Left vector. + /// Right vector. + /// + /// A of whose elements is 64-bit truncated product of lhs and rhs. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static Vector128 Multiply(Vector128 lhs, Vector128 rhs) + { + return Multiply(lhs.AsUInt64(), rhs.AsUInt64()).AsInt64(); + } + + /// + /// + /// Multiply packed 64-bit signed integer elements in a and b and truncate the results to 64-bit integer. + /// + /// Operation:
+ /// + /// dest[0] = left[0] * right[0]; + /// dest[1] = left[1] * right[1]; + /// dest[2] = left[2] * right[2]; + /// dest[3] = left[3] * right[3]; + /// + ///
+ /// Left vector. + /// Right vector. + /// + /// A of whose elements is 64-bit truncated product of lhs and rhs. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static Vector256 Multiply(Vector256 lhs, Vector256 rhs) + { + return Multiply(lhs.AsUInt64(), rhs.AsUInt64()).AsInt64(); + } + + /// + /// + /// Horizontally apply OR operation on adjacent pairs of single-precision (32-bit) floating-point elements in lhs and + /// rhs. + /// + /// Operation:
+ /// + /// dest[0] = left[0] | left[1]; + /// dest[1] = left[2] | left[3]; + /// dest[2] = right[0] | right[1]; + /// dest[3] = right[2] | right[3]; + /// + ///
+ /// Left vector. + /// Right vector. + /// + /// A of with all elements is result of OR operation on adjacent pairs of + /// elements in lhs and rhs. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static Vector128 HorizontalOr(Vector128 left, Vector128 right) + { + if (Sse.IsSupported) + { + return HorizontalOr_Sse(left, right); + } + + return HorizontalOrInternal_Fallback(left, right); + } + + /// + /// + /// Horizontally apply OR operation on adjacent pairs of 32-bit unsigned integer elements in lhs and rhs. + /// + /// Operation:
+ /// + /// dest[0] = left[0] | left[1]; + /// dest[1] = left[2] | left[3]; + /// dest[2] = right[0] | right[1]; + /// dest[3] = right[2] | right[3]; + /// + ///
+ /// Left vector. + /// Right vector. + /// + /// A of with all elements is result of OR operation on adjacent pairs of + /// elements in lhs and rhs. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [CLSCompliant(false)] + public static Vector128 HorizontalOr(Vector128 left, Vector128 right) + { + return HorizontalOr(left.AsInt32(), right.AsInt32()).AsUInt32(); + } + + // Helper methods + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector64 GetUninitializedVector64() where T : struct + { +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out Vector64 output); + return output; +#else + return default; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 GetUninitializedVector128() where T : struct + { +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out Vector128 output); + return output; +#else + return default; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector256 GetUninitializedVector256() where T : struct + { +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out Vector256 output); + return output; +#else + return default; +#endif + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 HorizontalOr_Sse(Vector128 left, Vector128 right) + { + Vector128 leftSingle = left.AsSingle(); + Vector128 rightSingle = right.AsSingle(); + + // first = { left[0] ; left[2] ; right[0] ; right[2] } + // second = { left[1] ; left[3] ; right[1] ; right[3] } + Vector128 first = Sse.Shuffle(leftSingle, rightSingle, 0b10_00_10_00); + Vector128 second = Sse.Shuffle(leftSingle, rightSingle, 0b11_01_11_01); + + return Sse.Or(first, second).AsInt32(); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 HorizontalOrInternal_Fallback(Vector128 left, Vector128 right) + { + Vector128 output = GetUninitializedVector128(); + + ref int outputInteger = ref Unsafe.As, int>(ref output); + ref int leftInteger = ref Unsafe.As, int>(ref left); + ref int rightInteger = ref Unsafe.As, int>(ref right); + + outputInteger = leftInteger | Unsafe.Add(ref leftInteger, 1); + + Unsafe.Add(ref outputInteger, 1) = Unsafe.Add(ref leftInteger, 2) | Unsafe.Add(ref leftInteger, 3); + Unsafe.Add(ref outputInteger, 2) = rightInteger | Unsafe.Add(ref rightInteger, 1); + Unsafe.Add(ref outputInteger, 3) = Unsafe.Add(ref rightInteger, 2) | Unsafe.Add(ref rightInteger, 3); + + return output; + } + + [Pure] + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 MultiplyInternal_Fallback(Vector128 left, Vector128 right) + { + ulong leftInteger1 = Unsafe.As, ulong>(ref left); + ulong rightInteger1 = Unsafe.As, ulong>(ref right); + ulong result1 = leftInteger1 * rightInteger1; + + ulong leftInteger2 = Unsafe.Add(ref Unsafe.As, ulong>(ref left), 1); + ulong rightInteger2 = Unsafe.Add(ref Unsafe.As, ulong>(ref right), 1); + ulong result2 = leftInteger2 * rightInteger2; + + Vector128 output = Vector128.Create(result1, result2); + + return output; + } + + [Pure] + [CLSCompliant(false)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector128 MultiplyInternal_Sse2(Vector128 left, Vector128 right) + { + // https://stackoverflow.com/questions/17863411/sse-multiplication-of-2-64-bit-integers + + Vector128 ac = Sse2.Multiply(left.AsUInt32(), right.AsUInt32()); + Vector128 b = Sse2.ShiftRightLogical(left, 32).AsUInt32(); + Vector128 bc = Sse2.Multiply(b, right.AsUInt32()); + Vector128 d = Sse2.ShiftRightLogical(right, 32).AsUInt32(); + Vector128 ad = Sse2.Multiply(left.AsUInt32(), d); + Vector128 high = Sse2.Add(bc, ad); + high = Sse2.ShiftLeftLogical(high, 32); + + return Sse2.Add(high, ac); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector256 MultiplyInternal_Fallback(Vector256 left, Vector256 right) + { + Vector256 output = GetUninitializedVector256(); + + for (var index = 0; index < Vector256.Count; index++) + { + Unsafe.Add(ref Unsafe.As, ulong>(ref output), index) = + Unsafe.Add(ref Unsafe.As, ulong>(ref left), index) * + Unsafe.Add(ref Unsafe.As, ulong>(ref right), index); + } + + return output; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static Vector256 MultiplyInternal_Avx2(Vector256 left, Vector256 right) + { + // https://stackoverflow.com/questions/17863411/sse-multiplication-of-2-64-bit-integers + + Vector256 ac = Avx2.Multiply(left.AsUInt32(), right.AsUInt32()); + Vector256 b = Avx2.ShiftRightLogical(left, 32).AsUInt32(); + Vector256 bc = Avx2.Multiply(b, right.AsUInt32()); + Vector256 d = Avx2.ShiftRightLogical(right, 32).AsUInt32(); + Vector256 ad = Avx2.Multiply(left.AsUInt32(), d); + Vector256 high = Avx2.Add(bc, ad); + high = Avx2.ShiftLeftLogical(high, 32); + + return Avx2.Add(high, ac); + } +} + +#endif diff --git a/X10D/src/Core/NullableExtensions.cs b/X10D/src/Core/NullableExtensions.cs new file mode 100644 index 0000000..541fbd2 --- /dev/null +++ b/X10D/src/Core/NullableExtensions.cs @@ -0,0 +1,35 @@ +namespace X10D.Core; + +/// +/// Extension methods for +/// +public static class NullableExtensions +{ + /// + /// Attempts to get the value of a , and returns a value indicating the success of the + /// operation. + /// + /// The nullable value. + /// + /// When this method returns, contains the result of , if + /// is ; otherwise, returns the default value for + /// . + /// + /// The type of the value. + /// + /// if the value's is ; otherwise, + /// . + /// + public static bool TryGetValue(this T? value, out T result) + where T : struct + { + if (value.HasValue) + { + result = value.Value; + return true; + } + + result = default; + return false; + } +} diff --git a/X10D/src/Core/RandomExtensions.cs b/X10D/src/Core/RandomExtensions.cs index a111704..32acc3b 100644 --- a/X10D/src/Core/RandomExtensions.cs +++ b/X10D/src/Core/RandomExtensions.cs @@ -2,6 +2,8 @@ using System.Text; using X10D.Math; +#pragma warning disable CA5394 + namespace X10D.Core; /// @@ -9,7 +11,9 @@ namespace X10D.Core; /// public static class RandomExtensions { +#if !NET6_0_OR_GREATER private static readonly Random Shared = new(); +#endif /// /// Returns a random value that defined in a specified enum. @@ -153,15 +157,13 @@ public static class RandomExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(random); + ArgumentNullException.ThrowIfNull(source); #else if (random is null) { throw new ArgumentNullException(nameof(random)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(source); -#else + if (source is null) { throw new ArgumentNullException(nameof(source)); @@ -181,6 +183,77 @@ public static class RandomExtensions return list[random.Next(list.Count)]; } + /// + /// Returns a random element from the specified span of elements using the current instance. + /// + /// The element type. + /// The instance. + /// The span of elements from which to draw. + /// A random element of type from . + /// + /// is is + /// -or- + /// is . + /// + /// + /// + /// Span<int> span = stackalloc span[5]; + /// // populate the span ... + /// + /// var random = new Random(); + /// var number = random.NextFrom(span); + /// + /// + public static T NextFrom(this Random random, Span source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(random); +#else + if (random is null) + { + throw new ArgumentNullException(nameof(random)); + } +#endif + + return source[random.Next(source.Length)]; + } + + /// + /// Returns a random element from the specified readonly span of elements using the current + /// instance. + /// + /// The element type. + /// The instance. + /// The readonly span of elements from which to draw. + /// A random element of type from . + /// + /// is is + /// -or- + /// is . + /// + /// + /// + /// Span<int> span = stackalloc span[5]; + /// // populate the span ... + /// + /// var random = new Random(); + /// var number = random.NextFrom(span.AsReadOnly()); + /// + /// + public static T NextFrom(this Random random, ReadOnlySpan source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(random); +#else + if (random is null) + { + throw new ArgumentNullException(nameof(random)); + } +#endif + + return source[random.Next(source.Length)]; + } + /// /// Returns a non-negative random integer. /// @@ -459,15 +532,13 @@ public static class RandomExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(random); + ArgumentNullException.ThrowIfNull(source); #else if (random is null) { throw new ArgumentNullException(nameof(random)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(source); -#else + if (source is null) { throw new ArgumentNullException(nameof(source)); diff --git a/X10D/src/Core/SpanExtensions.cs b/X10D/src/Core/SpanExtensions.cs new file mode 100644 index 0000000..5447cb5 --- /dev/null +++ b/X10D/src/Core/SpanExtensions.cs @@ -0,0 +1,569 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics.Arm; +#endif + +#if NET7_0_OR_GREATER +using System.Diagnostics; +#endif + +namespace X10D.Core; + +/// +/// Extension methods for and . +/// +public static class SpanExtensions +{ +#if NETCOREAPP3_0_OR_GREATER + private const ulong IntegerPackingMagic = 0x0102040810204080; + + [ExcludeFromCodeCoverage] + private static Vector128 IntegerPackingMagicV128 + { + get => Vector128.Create(IntegerPackingMagic); + } + + [ExcludeFromCodeCoverage] + private static Vector256 IntegerPackingMagicV256 + { + get => Vector256.Create(IntegerPackingMagic); + } +#endif + + /// + /// Returns a value indicating whether a specific enumeration value is contained with the current span of elements. + /// + /// The type of the elements in . + /// The span of elements. + /// The value to search for. + /// + /// if is contained with ; otherwise, + /// . + /// + /// The size of is unsupported. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool Contains(this Span span, T value) where T : struct, Enum + { + return Contains((ReadOnlySpan)span, value); + } + + /// + /// Returns a value indicating whether a specific enumeration value is contained with the current readonly span of + /// elements. + /// + /// The type of the elements in . + /// The readonly span of elements. + /// The value to search for. + /// + /// if is contained with ; otherwise, + /// . + /// + /// The size of is unsupported. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool Contains(this ReadOnlySpan span, T value) where T : struct, Enum + { +#if NET6_0_OR_GREATER + unsafe + { +#pragma warning disable CS8500 + switch (sizeof(T)) +#pragma warning restore CS8500 + { + case 1: + { + ref byte enums = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + return MemoryMarshal.CreateSpan(ref enums, span.Length).Contains(Unsafe.As(ref value)); + } + + case 2: + { + ref ushort enums = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + return MemoryMarshal.CreateSpan(ref enums, span.Length).Contains(Unsafe.As(ref value)); + } + + case 4: + { + ref uint enums = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + return MemoryMarshal.CreateSpan(ref enums, span.Length).Contains(Unsafe.As(ref value)); + } + + case 8: + { + ref ulong enums = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + return MemoryMarshal.CreateSpan(ref enums, span.Length).Contains(Unsafe.As(ref value)); + } + + // dotcover disable + //NOSONAR + default: +#if NET7_0_OR_GREATER + throw new UnreachableException(ExceptionMessages.EnumSizeIsUnexpected); +#else + throw new ArgumentException(ExceptionMessages.EnumSizeIsUnexpected); +#endif + //NOSONAR + // dotcover enable + } + } +#else + foreach (var it in span) + { + if (EqualityComparer.Default.Equals(it, value)) + { + return true; + } + } + + return false; +#endif + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// An 8-bit unsigned integer containing the packed booleans. + /// contains more than 8 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static byte PackByte(this Span source) + { + return PackByte((ReadOnlySpan)source); + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// An 8-bit unsigned integer containing the packed booleans. + /// contains more than 8 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + [ExcludeFromCodeCoverage] + public static byte PackByte(this ReadOnlySpan source) + { + if (source.Length > 8) + { + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); + } + + if (source.Length < 8) + { + return PackByteInternal_Fallback(source); + } + +#if NETCOREAPP3_0_OR_GREATER + if (!BitConverter.IsLittleEndian) + { + return PackByteInternal_Fallback(source); + } + + if (Sse2.IsSupported) + { + return PackByteInternal_Sse2(source); + } + + if (AdvSimd.IsSupported) + { + return PackByteInternal_AdvSimd(source); + } +#endif + + return PackByteInternal_Fallback(source); + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 16-bit signed integer containing the packed booleans. + /// contains more than 16 elements. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short PackInt16(this Span source) + { + return PackInt16((ReadOnlySpan)source); + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 16-bit signed integer containing the packed booleans. + /// contains more than 16 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + [ExcludeFromCodeCoverage] + public static short PackInt16(this ReadOnlySpan source) + { + switch (source.Length) + { + case > 16: + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); + case 8: + return PackByte(source); + case 16: + if (!BitConverter.IsLittleEndian) + { + goto default; + } + +#if NETCOREAPP3_0_OR_GREATER + if (Sse2.IsSupported) + { + return PackInt16Internal_Sse2(source); + } +#endif + + goto default; + case < 16: + return PackInt16Internal_Fallback(source); + + default: + return PackInt16Internal_Fallback(source); + } + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 32-bit signed integer containing the packed booleans. + /// contains more than 32 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static int PackInt32(this Span source) + { + return PackInt32((ReadOnlySpan)source); + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 32-bit signed integer containing the packed booleans. + /// contains more than 32 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + [ExcludeFromCodeCoverage] + public static int PackInt32(this ReadOnlySpan source) + { + switch (source.Length) + { + case > 32: + throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); + + case 8: + return PackByte(source); + + case 16: + return PackInt16(source); + + case 32: +#if NETCOREAPP3_0_OR_GREATER + if (!BitConverter.IsLittleEndian) + { + goto default; + } + + if (Avx2.IsSupported) + { + return PackInt32Internal_Avx2(source); + } + + if (Sse2.IsSupported) + { + return PackInt32Internal_Sse2(source); + } + + if (AdvSimd.IsSupported) + { + return PackInt32Internal_AdvSimd(source); + } +#endif + goto default; + + default: + return PackInt32Internal_Fallback(source); + } + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 64-bit signed integer containing the packed booleans. + /// contains more than 64 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static long PackInt64(this Span source) + { + return PackInt64((ReadOnlySpan)source); + } + + /// + /// Packs a of booleans into a . + /// + /// The span of booleans to pack. + /// A 64-bit signed integer containing the packed booleans. + /// contains more than 64 elements. + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static long PackInt64(this ReadOnlySpan source) + { + switch (source.Length) + { + case > 64: throw new ArgumentException(ExceptionMessages.SourceSpanIsTooLarge, nameof(source)); + case 8: return PackByte(source); + case 16: return PackInt16(source); + case 32: return PackInt32(source); + // ReSharper disable once RedundantCast + case 64: return (long)PackInt32(source[..32]) | ((long)PackInt32(source[32..]) << 32); + + default: + long result = 0; + + for (var index = 0; index < source.Length; index++) + { + result |= source[index] ? 1U << index : 0; + } + + return result; + } + } + + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static byte PackByteInternal_Fallback(this ReadOnlySpan source) + { + byte result = 0; + + for (var index = 0; index < source.Length; index++) + { + result |= (byte)(source[index] ? 1 << index : 0); + } + + return result; + } + + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static short PackInt16Internal_Fallback(this ReadOnlySpan source) + { + short result = 0; + + for (var index = 0; index < source.Length; index++) + { + result |= (short)(source[index] ? 1 << index : 0); + } + + return result; + } + + [Pure] +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static int PackInt32Internal_Fallback(this ReadOnlySpan source) + { + var result = 0; + + for (var index = 0; index < source.Length; index++) + { + result |= source[index] ? 1 << index : 0; + } + + return result; + } + +#if NETCOREAPP3_0_OR_GREATER + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static byte PackByteInternal_Sse2(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector128 load = Sse2.LoadScalarVector128((ulong*)pSource).AsByte(); + return unchecked((byte)(IntegerPackingMagic * load.CorrectBoolean().AsUInt64().GetElement(0) >> 56)); + } + } + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static short PackInt16Internal_Sse2(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector128 load = Sse2.LoadVector128((byte*)pSource); + Vector128 correct = load.CorrectBoolean().AsUInt64(); + Vector128 multiply = IntrinsicUtility.Multiply(IntegerPackingMagicV128, correct); + Vector128 shift = Sse2.ShiftRightLogical(multiply, 56); + + return (short)(shift.GetElement(0) | (shift.GetElement(1) << 8)); + } + } + } + + // dotcover disable + //NOSONAR + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static int PackInt32Internal_AdvSimd(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector128 vector1 = AdvSimd.LoadVector128((byte*)pSource).CorrectBoolean().AsUInt64(); + Vector128 vector2 = AdvSimd.LoadVector128((byte*)(pSource + 16)).CorrectBoolean().AsUInt64(); + + Vector128 calc1 = IntrinsicUtility.Multiply(IntegerPackingMagicV128, vector1); + Vector128 calc2 = IntrinsicUtility.Multiply(IntegerPackingMagicV128, vector2); + + calc1 = AdvSimd.ShiftRightLogical(calc1, 56); + calc2 = AdvSimd.ShiftRightLogical(calc2, 56); + + Vector128 shift1 = AdvSimd.ShiftLogical(calc1, Vector128.Create(0, 8)); + Vector128 shift2 = AdvSimd.ShiftLogical(calc2, Vector128.Create(16, 24)); + + return (int)(shift1.GetElement(0) | shift1.GetElement(1) | shift2.GetElement(0) | shift2.GetElement(1)); + } + } + } + //NOSONAR + // dotcover enable + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static int PackInt32Internal_Avx2(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector256 load = Avx.LoadVector256((byte*)pSource); + Vector256 correct = load.CorrectBoolean().AsUInt64(); + + Vector256 multiply = IntrinsicUtility.Multiply(IntegerPackingMagicV256, correct); + Vector256 shift = Avx2.ShiftRightLogical(multiply, 56); + shift = Avx2.ShiftLeftLogicalVariable(shift, Vector256.Create(0UL, 8, 16, 24)); + + Vector256 p1 = Avx2.Permute4x64(shift, 0b10_11_00_01); + Vector256 or1 = Avx2.Or(shift, p1); + Vector256 p2 = Avx2.Permute4x64(or1, 0b00_00_10_10); + Vector256 or2 = Avx2.Or(or1, p2); + + return (int)or2.GetElement(0); + } + } + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static int PackInt32Internal_Sse2(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector128 load = Sse2.LoadVector128((byte*)pSource); + Vector128 correct = load.CorrectBoolean().AsUInt64(); + + Vector128 multiply = IntrinsicUtility.Multiply(IntegerPackingMagicV128, correct); + Vector128 shift1 = Sse2.ShiftRightLogical(multiply, 56); + + load = Sse2.LoadVector128((byte*)(pSource + 16)); + correct = load.CorrectBoolean().AsUInt64(); + + multiply = IntrinsicUtility.Multiply(IntegerPackingMagicV128, correct); + Vector128 shift2 = Sse2.ShiftRightLogical(multiply, 56); + + ulong shift1Element0 = shift1.GetElement(0); + ulong shift1Element1 = (shift1.GetElement(1) << 8); + ulong shift2Element0 = (shift2.GetElement(0) << 16); + ulong shift2Element1 = (shift2.GetElement(1) << 24); + return (int)(shift1Element0 | shift1Element1 | shift2Element0 | shift2Element1); + } + } + } + +#if NET5_0_OR_GREATER + // dotcover disable + //NOSONAR + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static byte PackByteInternal_AdvSimd(this ReadOnlySpan source) + { + unsafe + { + fixed (bool* pSource = source) + { + Vector64 load = AdvSimd.LoadVector64((byte*)pSource); + return unchecked((byte)(IntegerPackingMagic * load.CorrectBoolean().AsUInt64().GetElement(0) >> 56)); + } + } + } + //NOSONAR + // dotcover enable +#endif +#endif +} diff --git a/X10D/src/Drawing/Circle.cs b/X10D/src/Drawing/Circle.cs new file mode 100644 index 0000000..75e3f23 --- /dev/null +++ b/X10D/src/Drawing/Circle.cs @@ -0,0 +1,304 @@ +using System.Drawing; + +namespace X10D.Drawing; + +/// +/// Represents a circle that is composed of a 32-bit signed integer center point and radius. +/// +public readonly struct Circle : IEquatable, IComparable, IComparable +{ + /// + /// The empty circle. That is, a circle whose center point is (0, 0) and whose radius is 0. + /// + public static readonly Circle Empty = new(); + + /// + /// The unit circle. That is, a circle whose center point is (0, 0) and whose radius is 1. + /// + public static readonly Circle Unit = new(0, 0, 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate of the center point. + /// The Y coordinate of the center point. + /// The radius of the circle. + public Circle(int centerX, int centerY, int radius) + : this(new Point(centerX, centerY), radius) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the circle. + /// The radius of the circle. + public Circle(Point center, int radius) + { + Center = center; + Radius = radius; + } + + /// + /// Gets the area of the circle. + /// + /// The area of the circle, calculated as πr². + public float Area + { + get => MathF.PI * Radius * Radius; + } + + /// + /// Gets the center point of the circle. + /// + /// The center point. + public Point Center { get; } + + /// + /// Gets the circumference of the circle. + /// + /// The circumference of the circle, calculated as 2πr. + public float Circumference + { + get => 2 * MathF.PI * Radius; + } + + /// + /// Gets the diameter of the circle. + /// + /// The diameter. This is always twice the . + public int Diameter + { + get => Radius * 2; + } + + /// + /// Gets the radius of the circle. + /// + /// The radius. + public int Radius { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(Circle left, Circle right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Circle left, Circle right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the radius of one circle is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(Circle left, Circle right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(Circle left, Circle right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(Circle left, Circle right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(Circle left, Circle right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Explicitly converts a to a . + /// + /// The circle to convert. + /// The converted circle. + public static explicit operator Circle(CircleF circle) + { + return Circle.FromCircleF(circle); + } + + /// + /// Converts a to a . + /// + /// The circle to convert. + /// The converted circle. + public static Circle FromCircleF(CircleF circle) + { + PointF center = circle.Center; + return new Circle(new Point((int)center.X, (int)center.Y), (int)circle.Radius); + } + + /// + /// Compares this instance to another . + /// + /// The other object. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of , or + /// is . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + /// is not an instance of . + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not Circle other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// The other circle. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + public int CompareTo(Circle other) + { + return Radius.CompareTo(other.Radius); + } + + /// + public override bool Equals(object? obj) + { + return obj is Circle circle && Equals(circle); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Circle other) + { + return Center.Equals(other.Center) && Radius.Equals(other.Radius); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Center, Radius); + } +} diff --git a/X10D/src/Drawing/CircleF.cs b/X10D/src/Drawing/CircleF.cs new file mode 100644 index 0000000..7832eb3 --- /dev/null +++ b/X10D/src/Drawing/CircleF.cs @@ -0,0 +1,315 @@ +using System.Drawing; +using System.Numerics; +using X10D.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a circle that is composed of a single-precision floating-point center point and radius. +/// +public readonly struct CircleF : IEquatable, IComparable, IComparable +{ + /// + /// The empty circle. That is, a circle whose center point is (0, 0) and whose radius is 0. + /// + public static readonly CircleF Empty = new(); + + /// + /// The unit circle. That is, a circle whose center point is (0, 0) and whose radius is 1. + /// + public static readonly CircleF Unit = new(0f, 0f, 1.0f); + + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate of the center point. + /// The Y coordinate of the center point. + /// The radius of the circle. + public CircleF(float centerX, float centerY, float radius) + : this(new Vector2(centerX, centerY), radius) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the circle. + /// The radius of the circle. + public CircleF(Vector2 center, float radius) + : this(center.ToPointF(), radius) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the circle. + /// The radius of the circle. + public CircleF(PointF center, float radius) + { + Center = center; + Radius = radius; + } + + /// + /// Gets the area of the circle. + /// + /// The area of the circle, calculated as πr². + public float Area + { + get => MathF.PI * Radius * Radius; + } + + /// + /// Gets the center point of the circle. + /// + /// The center point. + public PointF Center { get; } + + /// + /// Gets the circumference of the circle. + /// + /// The circumference of the circle, calculated as 2πr. + public float Circumference + { + get => 2 * MathF.PI * Radius; + } + + /// + /// Gets the diameter of the circle. + /// + /// The diameter. This is always twice the . + public float Diameter + { + get => Radius * 2; + } + + /// + /// Gets the radius of the circle. + /// + /// The radius. + public float Radius { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(CircleF left, CircleF right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(CircleF left, CircleF right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the radius of one circle is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(CircleF left, CircleF right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(CircleF left, CircleF right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(CircleF left, CircleF right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(CircleF left, CircleF right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Implicitly converts a to a . + /// + /// The circle to convert. + /// The converted circle. + public static implicit operator CircleF(Circle circle) + { + return FromCircle(circle); + } + + /// + /// Converts a to a . + /// + /// The circle to convert. + /// The converted circle. + public static CircleF FromCircle(Circle circle) + { + return new CircleF(circle.Center, circle.Radius); + } + + /// + /// Compares this instance to another . + /// + /// The other object. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of , or + /// is . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + /// is not an instance of . + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not CircleF other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// The other circle. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + public int CompareTo(CircleF other) + { + return Radius.CompareTo(other.Radius); + } + + /// + public override bool Equals(object? obj) + { + return obj is CircleF circle && Equals(circle); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(CircleF other) + { + return Center.Equals(other.Center) && Radius.Equals(other.Radius); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Center, Radius); + } +} diff --git a/X10D/src/Drawing/ColorExtensions.cs b/X10D/src/Drawing/ColorExtensions.cs index e21cf9e..4542264 100644 --- a/X10D/src/Drawing/ColorExtensions.cs +++ b/X10D/src/Drawing/ColorExtensions.cs @@ -9,6 +9,112 @@ namespace X10D.Drawing; /// public static class ColorExtensions { + /// + /// Deconstructs the current color into its ARGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color color, out byte a, out byte r, out byte g, out byte b) + { + a = color.A; + (r, g, b) = color; + } + + /// + /// Deconstructs the current color into its RGB components. + /// + /// The source color. + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + /// + /// When this method returns, contains the component of . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static void Deconstruct(this Color color, out byte r, out byte g, out byte b) + { + r = color.R; + g = color.G; + b = color.B; + } + + /// + /// Returns a which most closely resembles the current color. + /// + /// The source color. + /// The closest . + /// Glenn Slayden, https://stackoverflow.com/a/12340136/1467293 + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ConsoleColor GetClosestConsoleColor(this Color color) + { + ConsoleColor result = 0; + double red = color.R; + double green = color.G; + double blue = color.B; + var delta = double.MaxValue; + +#if NET5_0_OR_GREATER + foreach (ConsoleColor consoleColor in Enum.GetValues()) +#else + foreach (ConsoleColor consoleColor in Enum.GetValues(typeof(ConsoleColor))) +#endif + { +#if NET5_0_OR_GREATER + string name = Enum.GetName(consoleColor)!; +#else + string name = Enum.GetName(typeof(ConsoleColor), consoleColor)!; +#endif + Color currentColor = Color.FromName(name == "DarkYellow" ? "Orange" : name); // bug fix + double r = currentColor.R - red; + double g = currentColor.G - green; + double b = currentColor.B - blue; + double t = r * r + g * g + b * b; + + if (t == 0.0) + { + return consoleColor; + } + + if (t < delta) + { + delta = t; + result = consoleColor; + } + } + + return result; + } + /// /// Returns a new with the red, green, and blue components inverted. Alpha is not affected. /// diff --git a/X10D/src/Drawing/Cuboid.cs b/X10D/src/Drawing/Cuboid.cs new file mode 100644 index 0000000..cc2ebae --- /dev/null +++ b/X10D/src/Drawing/Cuboid.cs @@ -0,0 +1,345 @@ +using System.Numerics; +using X10D.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a cuboid in 3D space, which uses single-precision floating-point numbers for its coordinates. +/// +public readonly struct Cuboid : IEquatable +{ + /// + /// The empty cuboid. That is, a cuboid whose size is zero. + /// + public static readonly Cuboid Empty = new(); + + /// + /// A cube. That is, a cuboid whose size is the same in all three dimensions. + /// + /// A cube with the size (1, 1, 1). + public static readonly Cuboid Cube = new(0, 0, 0, 1, 1, 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The center X coordinate. + /// The center Y coordinate. + /// The center Z coordinate. + /// The width. + /// The height. + /// The depth. + public Cuboid(float centerX, float centerY, float centerZ, float width, float height, float depth) + : this(centerX, centerY, centerZ, width, height, depth, 0, 0, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center X coordinate. + /// The center Y coordinate. + /// The center Z coordinate. + /// The width. + /// The height. + /// The depth. + /// The yaw. + /// The pitch. + /// The roll. + public Cuboid(float centerX, float centerY, float centerZ, float width, float height, float depth, float yaw, float pitch, + float roll) + : this(new Vector3(centerX, centerY, centerZ), new Vector3(width, height, depth), new Vector3(pitch, yaw, roll)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point. + /// The size. + public Cuboid(in Vector3 center, in Vector3 size) + { + Center = center; + Size = size; + Orientation = Quaternion.Identity; + + Vector3 halfExtents = Size / 2.0f; + LocalFrontTopLeft = new Vector3(-halfExtents.X, halfExtents.Y, -halfExtents.Z); + LocalFrontTopRight = new Vector3(halfExtents.X, halfExtents.Y, -halfExtents.Z); + LocalFrontBottomLeft = new Vector3(-halfExtents.X, -halfExtents.Y, -halfExtents.Z); + LocalFrontBottomRight = new Vector3(halfExtents.X, -halfExtents.Y, -halfExtents.Z); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point. + /// The size. + /// The orientation of the cuboid. + public Cuboid(in Vector3 center, in Vector3 size, in Vector3 orientation) + : this(center, size, Quaternion.CreateFromYawPitchRoll(orientation.Y, orientation.X, orientation.Z)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point. + /// The size. + /// The orientation of the cuboid. + public Cuboid(in Vector3 center, in Vector3 size, in Quaternion orientation) + : this(center, size) + { + Orientation = orientation; + + Vector3 halfExtents = Size / 2.0f; + var localFrontTopLeft = new Vector3(-halfExtents.X, halfExtents.Y, -halfExtents.Z); + var localFrontTopRight = new Vector3(halfExtents.X, halfExtents.Y, -halfExtents.Z); + var localFrontBottomLeft = new Vector3(-halfExtents.X, -halfExtents.Y, -halfExtents.Z); + var localFrontBottomRight = new Vector3(halfExtents.X, -halfExtents.Y, -halfExtents.Z); + + Rotate( + orientation, + ref localFrontTopLeft, + ref localFrontTopRight, + ref localFrontBottomLeft, + ref localFrontBottomRight); + + LocalFrontTopLeft = localFrontTopLeft; + } + + /// + /// Gets the center point of the cuboid. + /// + /// The center point. + public Vector3 Center { get; } + + /// + /// Gets the orientation of this cuboid. + /// + /// The orientation. + public Quaternion Orientation { get; } + + /// + /// Gets the size of the cuboid. + /// + /// The size. + public Vector3 Size { get; } + + /// + /// Gets the front-top-left corner of the box, in local space. + /// + /// The front-top-left corner. + public Vector3 LocalFrontTopLeft { get; } + + /// + /// Gets the front-top-right corner of the box, in local space. + /// + /// The front-top-right corner. + public Vector3 LocalFrontTopRight { get; } + + /// + /// Gets the front-bottom-left corner of the box, in local space. + /// + /// The front-bottom-left corner. + public Vector3 LocalFrontBottomLeft { get; } + + /// + /// Gets the front-bottom-right corner of the box, in local space. + /// + /// The front-bottom-right corner. + public Vector3 LocalFrontBottomRight { get; } + + /// + /// Gets the back-top-left corner of the box, in local space. + /// + /// The back-top-left corner. + public Vector3 LocalBackTopLeft + { + get => -LocalFrontBottomRight; + } + + /// + /// Gets the back-top-right corner of the box, in local space. + /// + /// The back-top-right corner. + public Vector3 LocalBackTopRight + { + get => -LocalFrontBottomLeft; + } + + /// + /// Gets the back-bottom-left corner of the box, in local space. + /// + /// The back-bottom-left corner. + public Vector3 LocalBackBottomLeft + { + get => -LocalFrontTopRight; + } + + /// + /// Gets the back-bottom-right corner of the box, in local space. + /// + /// The back-bottom-right corner. + public Vector3 LocalBackBottomRight + { + get => -LocalFrontTopLeft; + } + + /// + /// Gets the front-top-left corner of the box, in world space. + /// + /// The front-top-left corner. + public Vector3 FrontTopLeft + { + get => LocalFrontTopLeft + Center; + } + + /// + /// Gets the front-top-right corner of the box, in world space. + /// + /// The front-top-right corner. + public Vector3 FrontTopRight + { + get => LocalFrontTopRight + Center; + } + + /// + /// Gets the front-bottom-left corner of the box, in world space. + /// + /// The front-bottom-left corner. + public Vector3 FrontBottomLeft + { + get => LocalFrontBottomLeft + Center; + } + + /// + /// Gets the front-bottom-right corner of the box, in world space. + /// + /// The front-bottom-right corner. + public Vector3 FrontBottomRight + { + get => LocalFrontBottomRight + Center; + } + + /// + /// Gets the back-bottom-left corner of the box, in world space. + /// + /// The back-bottom-left corner. + public Vector3 BackTopLeft + { + get => LocalBackTopLeft + Center; + } + + /// + /// Gets the back-bottom-right corner of the box, in world space. + /// + /// The back-bottom-right corner. + public Vector3 BackTopRight + { + get => LocalBackTopRight + Center; + } + + /// + /// Gets the back-bottom-right corner of the box, in world space. + /// + /// The back-bottom-right corner. + public Vector3 BackBottomLeft + { + get => LocalBackBottomLeft + Center; + } + + /// + /// Gets the back-bottom-right corner of the box, in world space. + /// + /// The back-bottom-right corner. + public Vector3 BackBottomRight + { + get => LocalBackBottomRight + Center; + } + + /// + /// Gets the volume of this cuboid. + /// + /// The volume. + public float Volume + { + get + { + Vector3 size = Size; + return size.X * size.Y * size.Z; + } + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator ==(Cuboid left, Cuboid right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Cuboid left, Cuboid right) + { + return !left.Equals(right); + } + + private static Vector3 RotatePointAroundPivot(in Vector3 point, in Vector3 pivot, in Quaternion rotation) + { + Vector3 direction = point - pivot; + return pivot + rotation.Multiply(direction); + } + + private static void Rotate( + in Quaternion orientation, + ref Vector3 localFrontTopLeft, + ref Vector3 localFrontTopRight, + ref Vector3 localFrontBottomLeft, + ref Vector3 localFrontBottomRight + ) + { + localFrontTopLeft = RotatePointAroundPivot(localFrontTopLeft, Vector3.Zero, orientation); + localFrontTopRight = RotatePointAroundPivot(localFrontTopRight, Vector3.Zero, orientation); + localFrontBottomLeft = RotatePointAroundPivot(localFrontBottomLeft, Vector3.Zero, orientation); + localFrontBottomRight = RotatePointAroundPivot(localFrontBottomRight, Vector3.Zero, orientation); + } + + /// + public override bool Equals(object? obj) + { + return obj is Cuboid other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Cuboid other) + { + return Center.Equals(other.Center) && Size.Equals(other.Size) && Orientation.Equals(other.Orientation); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Center, Size, LocalFrontTopLeft); + } +} diff --git a/X10D/src/Drawing/Ellipse.cs b/X10D/src/Drawing/Ellipse.cs new file mode 100644 index 0000000..736b3aa --- /dev/null +++ b/X10D/src/Drawing/Ellipse.cs @@ -0,0 +1,194 @@ +using System.Drawing; + +namespace X10D.Drawing; + +/// +/// Represents an ellipse that is composed of a 32-bit signed integer center point and radius. +/// +public readonly struct Ellipse : IEquatable +{ + /// + /// The empty ellipse. That is, an ellipse whose center point is (0, 0) and whose two radii are 0. + /// + public static readonly Ellipse Empty = new(); + + /// + /// The unit ellipse. That is, an ellipse whose center point is (0, 0) and whose two radii are 1. + /// + public static readonly Ellipse Unit = new(0, 0, 1, 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate of the center point. + /// The Y coordinate of the center point. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + public Ellipse(int centerX, int centerY, int horizontalRadius, int verticalRadius) + : this(new Point(centerX, centerY), new Size(horizontalRadius, verticalRadius)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + public Ellipse(Point center, Size radius) + { + Center = center; + HorizontalRadius = radius.Width; + VerticalRadius = radius.Height; + } + + /// + /// Gets the area of the ellipse. + /// + /// The area of the ellipse, calculated as πab. + public float Area + { + get => MathF.PI * HorizontalRadius * VerticalRadius; + } + + /// + /// Gets the center point of the ellipse. + /// + /// The center point. + public Point Center { get; } + + /// + /// Gets the approximate circumference of the ellipse. + /// + /// + /// The approximate circumference of the ellipse, calculated as + /// π(a+b)(3([(a-b)²]/(a+b)²(sqrt(-3(((a-b)²)/(a+b)²)+4+10))+1). + /// + public float ApproximateCircumference + { + get + { + float aMinusB = HorizontalRadius - VerticalRadius; + float aPlusB = HorizontalRadius + VerticalRadius; + + float aMinusB2 = aMinusB * aMinusB; + float aPlusB2 = aPlusB * aPlusB; + + return MathF.PI * (aPlusB * (3 * (aMinusB2 / (aPlusB2 * MathF.Sqrt(-3 * (aMinusB2 / aPlusB2) + 4 + 10))) + 1)); + } + } + + /// + /// Gets the horizontal radius of the ellipse. + /// + /// The horizontal radius. + public int HorizontalRadius { get; } + + /// + /// Gets the radius of the ellipse. + /// + /// The radius. + public Size Radius + { + get => new(HorizontalRadius, VerticalRadius); + } + + /// + /// Gets the vertical radius of the ellipse. + /// + /// The vertical radius. + public int VerticalRadius { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(in Ellipse left, in Ellipse right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(in Ellipse left, in Ellipse right) + { + return !left.Equals(right); + } + + /// + /// Implicitly converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static implicit operator Ellipse(in Circle circle) + { + return FromCircle(circle); + } + + /// + /// Explicitly converts an to an . + /// + /// The ellipse to convert. + /// The converted ellipse. + public static explicit operator Ellipse(in EllipseF ellipse) + { + return FromEllipseF(ellipse); + } + + /// + /// Converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static Ellipse FromCircle(in Circle circle) + { + return new Ellipse(circle.Center, new Size(circle.Radius, circle.Radius)); + } + + /// + /// Converts an to an . + /// + /// The ellipse to convert. + /// The converted ellipse. + public static Ellipse FromEllipseF(in EllipseF ellipse) + { + PointF center = ellipse.Center; + return new Ellipse((int)center.X, (int)center.Y, (int)ellipse.HorizontalRadius, (int)ellipse.VerticalRadius); + } + + /// + public override bool Equals(object? obj) + { + return obj is Ellipse ellipse && Equals(ellipse); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Ellipse other) + { + return HorizontalRadius.Equals(other.HorizontalRadius) && VerticalRadius.Equals(other.VerticalRadius); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(HorizontalRadius, VerticalRadius); + } +} diff --git a/X10D/src/Drawing/EllipseF.cs b/X10D/src/Drawing/EllipseF.cs new file mode 100644 index 0000000..310f17f --- /dev/null +++ b/X10D/src/Drawing/EllipseF.cs @@ -0,0 +1,241 @@ +using System.Drawing; +using System.Numerics; +using X10D.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents an ellipse that is composed of a single-precision floating-point center point and radius. +/// +public readonly struct EllipseF : IEquatable +{ + /// + /// The empty ellipse. That is, an ellipse whose center point is (0, 0) and whose two radii are 0. + /// + public static readonly EllipseF Empty = new(); + + /// + /// The unit ellipse. That is, an ellipse whose center point is (0, 0) and whose two radii are 1. + /// + public static readonly EllipseF Unit = new(0.0f, 0.0f, 1.0f, 1.0f); + + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate of the center point. + /// The Y coordinate of the center point. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + public EllipseF(float centerX, float centerY, float horizontalRadius, float verticalRadius) + { + Center = new PointF(centerX, centerY); + HorizontalRadius = horizontalRadius; + VerticalRadius = verticalRadius; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + public EllipseF(PointF center, SizeF radius) + { + Center = center; + HorizontalRadius = radius.Width; + VerticalRadius = radius.Height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + public EllipseF(PointF center, Vector2 radius) + { + Center = center; + HorizontalRadius = radius.X; + VerticalRadius = radius.Y; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point of the ellipse. + /// The radius of the ellipse. + public EllipseF(Vector2 center, Vector2 radius) + { + Center = center.ToPointF(); + HorizontalRadius = radius.X; + VerticalRadius = radius.Y; + } + + /// + /// Gets the area of the ellipse. + /// + /// The area of the ellipse, calculated as πab. + public float Area + { + get => MathF.PI * HorizontalRadius * VerticalRadius; + } + + /// + /// Gets the center point of the ellipse. + /// + /// The center point. + public PointF Center { get; } + + /// + /// Gets the approximate circumference of the ellipse. + /// + /// + /// The approximate circumference of the ellipse, calculated as + /// π(a+b)(3([(a-b)²]/(a+b)²(sqrt(-3(((a-b)²)/(a+b)²)+4+10))+1). + /// + public float ApproximateCircumference + { + get + { + float aMinusB = HorizontalRadius - VerticalRadius; + float aPlusB = HorizontalRadius + VerticalRadius; + + float aMinusB2 = aMinusB * aMinusB; + float aPlusB2 = aPlusB * aPlusB; + + return MathF.PI * (aPlusB * (3 * (aMinusB2 / (aPlusB2 * MathF.Sqrt(-3 * (aMinusB2 / aPlusB2) + 4 + 10))) + 1)); + } + } + + /// + /// Gets the horizontal radius of the ellipse. + /// + /// The horizontal radius. + public float HorizontalRadius { get; } + + /// + /// Gets the radius of the ellipse. + /// + /// The radius. + public SizeF Radius + { + get => new(HorizontalRadius, VerticalRadius); + } + + /// + /// Gets the vertical radius of the ellipse. + /// + /// The vertical radius. + public float VerticalRadius { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(in EllipseF left, in EllipseF right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(in EllipseF left, in EllipseF right) + { + return !left.Equals(right); + } + + /// + /// Implicitly converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static implicit operator EllipseF(in Circle circle) + { + return FromCircle(circle); + } + + /// + /// Implicitly converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static implicit operator EllipseF(in CircleF circle) + { + return FromCircleF(circle); + } + + /// + /// Implicitly converts an to an . + /// + /// The ellipse to convert. + /// The converted ellipse. + public static implicit operator EllipseF(in Ellipse ellipse) + { + return FromEllipse(ellipse); + } + + /// + /// Converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static EllipseF FromCircle(in Circle circle) + { + return new EllipseF(circle.Center, new SizeF(circle.Radius, circle.Radius)); + } + + /// + /// Converts a to an . + /// + /// The circle to convert. + /// The converted ellipse. + public static EllipseF FromCircleF(in CircleF circle) + { + return new EllipseF(circle.Center, new SizeF(circle.Radius, circle.Radius)); + } + + /// + /// Converts an to an . + /// + /// The ellipse to convert. + /// The converted ellipse. + public static EllipseF FromEllipse(in Ellipse ellipse) + { + return new EllipseF(ellipse.Center, ellipse.Radius); + } + + /// + public override bool Equals(object? obj) + { + return obj is EllipseF ellipse && Equals(ellipse); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(EllipseF other) + { + return HorizontalRadius.Equals(other.HorizontalRadius) && VerticalRadius.Equals(other.VerticalRadius); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(HorizontalRadius, VerticalRadius); + } +} diff --git a/X10D/src/Drawing/Line.cs b/X10D/src/Drawing/Line.cs new file mode 100644 index 0000000..4c24060 --- /dev/null +++ b/X10D/src/Drawing/Line.cs @@ -0,0 +1,321 @@ +using System.Drawing; +using System.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a line in 2D space that is composed of 32-bit signed integer X and Y coordinates. +/// +public readonly struct Line : IEquatable, IComparable, IComparable +{ + /// + /// The empty line. That is, a line whose start and end points are at (0, 0). + /// + public static readonly Line Empty = new(); + + /// + /// The line whose start point is at (0, 0) and end point is at (1, 1). + /// + public static readonly Line One = new(Point.Empty, new Point(1, 1)); + + /// + /// The line whose start point is at (0, 0) and end point is at (1, 0). + /// + public static readonly Line UnitX = new(Point.Empty, new Point(1, 0)); + + /// + /// The line whose start point is at (0, 0) and end point is at (0, 1). + /// + public static readonly Line UnitY = new(Point.Empty, new Point(0, 1)); + + /// + /// Initializes a new instance of the struct by taking the start and end points. + /// + /// The start point. + /// The end point. + public Line(Point start, Point end) + { + End = end; + Start = start; + } + + /// + /// Gets the end point of the line. + /// + /// The end point. + public Point End { get; } + + /// + /// Gets the length of this line. + /// + /// The length. + public float Length + { + get => MathF.Sqrt(LengthSquared); + } + + /// + /// Gets the length of this line, squared. + /// + /// The squared length. + public float LengthSquared + { + get => MathF.Pow(End.X - Start.X, 2.0f) + MathF.Pow(End.Y - Start.Y, 2.0f); + } + + /// + /// Gets the start point of the line. + /// + /// The start point. + public Point Start { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(in Line left, in Line right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(in Line left, in Line right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the length of one line is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(in Line left, in Line right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(in Line left, in Line right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the length of one line is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(in Line left, in Line right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(in Line left, in Line right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Explicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static explicit operator Line(in LineF line) + { + return FromLineF(line); + } + + /// + /// Explicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static explicit operator Line(in Line3D line) + { + return FromLine3D(line); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static Line FromLine3D(in Line3D line) + { + Vector3 start = line.Start; + Vector3 end = line.End; + return new Line(new Point((int)start.X, (int)start.Y), new Point((int)end.X, (int)end.Y)); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static Line FromLineF(in LineF line) + { + PointF start = line.Start; + PointF end = line.End; + return new Line(new Point((int)start.X, (int)start.Y), new Point((int)end.X, (int)end.Y)); + } + + /// + /// Compares this instance to another object. + /// + /// The object with with which to compare + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// is not an instance of . + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not Line other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// + public int CompareTo(Line other) + { + return LengthSquared.CompareTo(other.LengthSquared); + } + + /// + public override bool Equals(object? obj) + { + return obj is Line other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Line other) + { + return End.Equals(other.End) && Start.Equals(other.Start); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(End, Start); + } +} diff --git a/X10D/src/Drawing/Line3D.cs b/X10D/src/Drawing/Line3D.cs new file mode 100644 index 0000000..d2e4177 --- /dev/null +++ b/X10D/src/Drawing/Line3D.cs @@ -0,0 +1,326 @@ +using System.Drawing; +using System.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a line in 3D space that is composed of 32-bit signed integer X, Y and Z coordinates. +/// +public readonly struct Line3D : IEquatable, IComparable, IComparable +{ + /// + /// The empty line. That is, a line whose start and end points are at (0, 0). + /// + public static readonly Line3D Empty = new(); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (1, 1, 1). + /// + public static readonly Line3D One = new(Vector3.Zero, Vector3.One); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (1, 0, 0). + /// + public static readonly Line3D UnitX = new(Vector3.Zero, Vector3.UnitX); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (0, 1, 0). + /// + public static readonly Line3D UnitY = new(Vector3.Zero, Vector3.UnitY); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (0, 0, 1). + /// + public static readonly Line3D UnitZ = new(Vector3.Zero, Vector3.UnitZ); + + /// + /// Initializes a new instance of the struct by taking the start and end points. + /// + /// The start point. + /// The end point. + public Line3D(in Vector3 start, in Vector3 end) + { + End = end; + Start = start; + } + + /// + /// Gets the end point of the line. + /// + /// The end point. + public Vector3 End { get; } + + /// + /// Gets the length of this line. + /// + /// The length. + public float Length + { + get => (End - Start).Length(); + } + + /// + /// Gets the length of this line, squared. + /// + /// The squared length. + public float LengthSquared + { + get => (End - Start).LengthSquared(); + } + + /// + /// Gets the start point of the line. + /// + /// The start point. + public Vector3 Start { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(Line3D left, Line3D right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Line3D left, Line3D right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the length of one line is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(in Line3D left, in Line3D right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(in Line3D left, in Line3D right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the length of one line is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(in Line3D left, in Line3D right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(in Line3D left, in Line3D right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Implicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static implicit operator Line3D(in Line line) + { + return FromLine(line); + } + + /// + /// Implicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static implicit operator Line3D(in LineF line) + { + return FromLineF(line); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static Line3D FromLine(in Line line) + { + Point start = line.Start; + Point end = line.End; + return new Line3D(new Vector3(start.X, start.Y, 0), new Vector3(end.X, end.Y, 0)); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static Line3D FromLineF(in LineF line) + { + PointF start = line.Start; + PointF end = line.End; + return new Line3D(new Vector3(start.X, start.Y, 0), new Vector3(end.X, end.Y, 0)); + } + + /// + /// Compares this instance to another object. + /// + /// The object with with which to compare + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// is not an instance of . + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not Line3D other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// + public int CompareTo(Line3D other) + { + return LengthSquared.CompareTo(other.LengthSquared); + } + + /// + public override bool Equals(object? obj) + { + return obj is Line3D other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Line3D other) + { + return End.Equals(other.End) && Start.Equals(other.Start); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(End, Start); + } +} diff --git a/X10D/src/Drawing/LineF.cs b/X10D/src/Drawing/LineF.cs new file mode 100644 index 0000000..c4b31b2 --- /dev/null +++ b/X10D/src/Drawing/LineF.cs @@ -0,0 +1,330 @@ +using System.Drawing; +using System.Numerics; +using X10D.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a line in 2D space that is composed of single-precision floating-point X and Y coordinates. +/// +public readonly struct LineF : IEquatable, IComparable, IComparable +{ + /// + /// The empty line. That is, a line whose start and end points are at (0, 0). + /// + public static readonly LineF Empty = new(); + + /// + /// The line whose start point is at (0, 0) and end point is at (1, 1). + /// + public static readonly LineF One = new(Vector2.Zero, new Vector2(1, 1)); + + /// + /// The line whose start point is at (0, 0) and end point is at (1, 0). + /// + public static readonly LineF UnitX = new(Vector2.Zero, new Vector2(1, 0)); + + /// + /// The line whose start point is at (0, 0) and end point is at (0, 1). + /// + public static readonly LineF UnitY = new(Vector2.Zero, new Vector2(0, 1)); + + /// + /// Initializes a new instance of the struct by taking the start and end points. + /// + /// The start point. + /// The end point. + public LineF(Vector2 start, Vector2 end) + : this(start.ToPointF(), end.ToPointF()) + { + } + + /// + /// Initializes a new instance of the struct by taking the start and end points. + /// + /// The start point. + /// The end point. + public LineF(PointF start, PointF end) + { + End = end; + Start = start; + } + + /// + /// Gets the end point of the line. + /// + /// The end point. + public PointF End { get; } + + /// + /// Gets the length of this line. + /// + /// The length. + public float Length + { + get => MathF.Sqrt(LengthSquared); + } + + /// + /// Gets the length of this line, squared. + /// + /// The squared length. + public float LengthSquared + { + get => MathF.Pow(End.X - Start.X, 2.0f) + MathF.Pow(End.Y - Start.Y, 2.0f); + } + + /// + /// Gets the start point of the line. + /// + /// The start point. + public PointF Start { get; } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(in LineF left, in LineF right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(in LineF left, in LineF right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the length of one line is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(in LineF left, in LineF right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(in LineF left, in LineF right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the length of one line is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(in LineF left, in LineF right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(in LineF left, in LineF right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Implicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static implicit operator LineF(in Line line) + { + return FromLine(line); + } + + /// + /// Explicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static explicit operator LineF(in Line3D line) + { + return FromLine3D(line); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static LineF FromLine(in Line line) + { + return new LineF(line.Start, line.End); + } + + /// + /// Converts a to a . + /// + /// The line to convert. + /// The converted line. + public static LineF FromLine3D(in Line3D line) + { + Vector3 start = line.Start; + Vector3 end = line.End; + return new LineF(new PointF(start.X, start.Y), new PointF(end.X, end.Y)); + } + + /// + /// Compares this instance to another object. + /// + /// The object with with which to compare + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// is not an instance of . + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not LineF other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// + public int CompareTo(LineF other) + { + return LengthSquared.CompareTo(other.LengthSquared); + } + + /// + public override bool Equals(object? obj) + { + return obj is LineF other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(LineF other) + { + return End.Equals(other.End) && Start.Equals(other.Start); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(End, Start); + } +} diff --git a/X10D/src/Drawing/PointExtensions.cs b/X10D/src/Drawing/PointExtensions.cs new file mode 100644 index 0000000..f81f0e1 --- /dev/null +++ b/X10D/src/Drawing/PointExtensions.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace X10D.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PointExtensions +{ + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The line on which the point may lie. + /// + /// if lies on the line defined by ; otherwise + /// . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Point point, LineF line) + { + return ((PointF)point).IsOnLine(line); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Point point, PointF start, PointF end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Point point, Vector2 start, Vector2 end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Size ToSize(this Point point) + { + return new Size(point.X, point.Y); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF ToSizeF(this Point point) + { + return new SizeF(point.X, point.Y); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToVector2(this Point point) + { + return new Vector2(point.X, point.Y); + } +} diff --git a/X10D/src/Drawing/PointFExtensions.cs b/X10D/src/Drawing/PointFExtensions.cs new file mode 100644 index 0000000..a464400 --- /dev/null +++ b/X10D/src/Drawing/PointFExtensions.cs @@ -0,0 +1,143 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Numerics; +using System.Runtime.CompilerServices; +using X10D.Math; + +namespace X10D.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class PointFExtensions +{ + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The line on which the point may lie. + /// + /// if lies on the line defined by ; otherwise + /// . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this PointF point, LineF line) + { + (float x1, float x2) = (line.Start.X, line.End.X); + (float y1, float y2) = (line.Start.Y, line.End.Y); + (float x, float y) = (point.X, point.Y); + return System.Math.Abs((y2 - y1) * (x - x2) - (y - y2) * (x2 - x1)) < float.Epsilon; + } + + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this PointF point, PointF start, PointF end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Determines if the current lies on the specified . + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this PointF point, Vector2 start, Vector2 end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The point whose components to round. + /// The rounded point. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static PointF Round(this PointF point) + { + return point.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The point whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded point. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static PointF Round(this PointF point, float nearest) + { + float x = point.X.Round(nearest); + float y = point.Y.Round(nearest); + return new PointF(x, y); + } + + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static SizeF ToSizeF(this PointF point) + { + return new SizeF(point.X, point.Y); + } + +#if !NET6_0_OR_GREATER + /// + /// Converts the current to a . + /// + /// The point to convert. + /// The resulting . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ToVector2(this PointF point) + { + return new Vector2(point.X, point.Y); + } +#endif +} diff --git a/X10D/src/Drawing/Polygon.cs b/X10D/src/Drawing/Polygon.cs new file mode 100644 index 0000000..63abae8 --- /dev/null +++ b/X10D/src/Drawing/Polygon.cs @@ -0,0 +1,261 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; + +namespace X10D.Drawing; + +/// +/// Represents a 2D polygon composed of 32-bit signed integer vertices. +/// +public class Polygon : IEquatable +{ + private readonly List _vertices = new(); + + /// + /// Initializes a new instance of the class. + /// + public Polygon() + { + } + + /// + /// Initializes a new instance of the class by copying the specified polygon. + /// + public Polygon(Polygon polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + + _vertices = new List(); + for (var index = 0; index < polygon._vertices.Count; index++) + { + Point vertex = polygon._vertices[index]; + _vertices.Add(vertex); + } + } + + /// + /// Initializes a new instance of the class by constructing it from the specified vertices. + /// + /// An enumerable collection of vertices from which the polygon should be constructed. + public Polygon(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + _vertices = new List(vertices); + } + + /// + /// Gets an empty polygon. That is, a polygon with no vertices. + /// + /// An empty polygon. + public static Polygon Empty + { + get => new(); + } + + /// + /// Returns a value indicating whether this polygon is convex. + /// + /// if this polygon is convex; otherwise, . + public bool IsConvex + { + get + { + if (_vertices.Count < 3) + { + return false; + } + + var positive = false; + var negative = false; + Point p0 = _vertices[0]; + + for (var index = 1; index < _vertices.Count; index++) + { + Point p1 = _vertices[index]; + int d = (p1.X - p0.X) * (p1.Y + p0.Y); + + if (d > 0) + { + positive = true; + } + else if (d < 0) + { + negative = true; + } + + if (positive && negative) + { + return false; + } + + p0 = p1; + } + + return true; + } + } + + /// + /// Gets the number of vertices in this polygon. + /// + /// An value, representing the number of vertices in this polygon. + public int VertexCount + { + get => _vertices.Count; + } + + /// + /// Gets a read-only view of the vertices in this polygon. + /// + /// + /// A of values, representing the vertices of this polygon. + /// + public IReadOnlyList Vertices + { + get => _vertices.AsReadOnly(); + } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(Polygon? left, Polygon? right) + { + return Equals(left, right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Polygon? left, Polygon? right) + { + return !(left == right); + } + + /// + /// Explicitly converts a to a . + /// + /// The polygon to convert. + /// The converted polygon. + [return: NotNullIfNotNull("polygon")] + public static explicit operator Polygon?(PolygonF? polygon) + { + return polygon is null ? null : FromPolygonF(polygon); + } + + /// + /// Explicitly converts a to a . + /// + /// The polygon to convert. + /// The converted polygon. + /// is . + public static Polygon FromPolygonF(PolygonF polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + + var vertices = new List(); + + foreach (PointF vertex in polygon.Vertices) + { + vertices.Add(new Point((int)vertex.X, (int)vertex.Y)); + } + + return new Polygon(vertices); + } + + /// + /// Adds a vertex to this polygon. + /// + /// The vertex to add. + public void AddVertex(Point vertex) + { + _vertices.Add(vertex); + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// An enumerable collection of vertices to add. + /// is . + public void AddVertices(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + foreach (Point vertex in vertices) + { + AddVertex(vertex); + } + } + + /// + /// Clears all vertices from this polygon. + /// + public void ClearVertices() + { + _vertices.Clear(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Polygon polygon && Equals(polygon); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Polygon? other) + { + return other is not null && _vertices.SequenceEqual(other._vertices); + } + + /// + public override int GetHashCode() + { + return _vertices.Aggregate(0, HashCode.Combine); + } +} diff --git a/X10D/src/Drawing/PolygonF.cs b/X10D/src/Drawing/PolygonF.cs new file mode 100644 index 0000000..51d9eeb --- /dev/null +++ b/X10D/src/Drawing/PolygonF.cs @@ -0,0 +1,318 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Numerics; +using X10D.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a 2D polygon composed of single-precision floating-vertex vertices. +/// +public class PolygonF +{ + private readonly List _vertices = new(); + + /// + /// Initializes a new instance of the class. + /// + public PolygonF() + { + } + + /// + /// Initializes a new instance of the class by copying the specified polygon. + /// + /// is . + public PolygonF(PolygonF polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + _vertices = new List(); + for (var index = 0; index < polygon._vertices.Count; index++) + { + PointF vertex = polygon._vertices[index]; + _vertices.Add(vertex); + } + } + + /// + /// Initializes a new instance of the class by constructing it from the specified vertices. + /// + /// An enumerable collection of vertices from which the polygon should be constructed. + /// is . + public PolygonF(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + _vertices = new List(); + foreach (Vector2 vertex in vertices) + { + _vertices.Add(vertex.ToPointF()); + } + } + + /// + /// Initializes a new instance of the class by constructing it from the specified vertices. + /// + /// An enumerable collection of vertices from which the polygon should be constructed. + /// is . + public PolygonF(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + _vertices = new List(vertices); + } + + /// + /// Gets an empty polygon. That is, a polygon with no vertices. + /// + /// An empty polygon. + public static PolygonF Empty + { + get => new(); + } + + /// + /// Returns a value indicating whether this polygon is convex. + /// + /// if this polygon is convex; otherwise, . + public bool IsConvex + { + get + { + if (_vertices.Count < 3) + { + return false; + } + + var positive = false; + var negative = false; + PointF p0 = _vertices[0]; + + for (var index = 1; index < _vertices.Count; index++) + { + PointF p1 = _vertices[index]; + float d = (p1.X - p0.X) * (p1.Y + p0.Y); + + if (d > 0) + { + positive = true; + } + else if (d < 0) + { + negative = true; + } + + if (positive && negative) + { + return false; + } + + p0 = p1; + } + + return true; + } + } + + /// + /// Gets the number of vertices in this polygon. + /// + /// An value, representing the number of vertices in this polygon. + public int VertexCount + { + get => _vertices.Count; + } + + /// + /// Gets a read-only view of the vertices in this polygon. + /// + /// + /// A of values, representing the vertices of this polygon. + /// + public IReadOnlyList Vertices + { + get => _vertices.AsReadOnly(); + } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(PolygonF? left, PolygonF? right) + { + return Equals(left, right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(PolygonF? left, PolygonF? right) + { + return !(left == right); + } + + /// + /// Implicitly converts a to a . + /// + /// The polygon to convert. + /// The converted polygon. + [return: NotNullIfNotNull("polygon")] + public static implicit operator PolygonF?(Polygon? polygon) + { + return polygon is null ? null : FromPolygon(polygon); + } + + /// + /// Implicitly converts a to a . + /// + /// The polygon to convert. + /// The converted polygon. + /// is . + public static PolygonF FromPolygon(Polygon polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + + var vertices = new List(); + + foreach (Point vertex in polygon.Vertices) + { + vertices.Add(vertex); + } + + return new PolygonF(vertices); + } + + /// + /// Adds a vertex to this polygon. + /// + /// The vertex to add. + public void AddVertex(PointF vertex) + { + _vertices.Add(vertex); + } + + /// + /// Adds a vertex to this polygon. + /// + /// The vertex to add. + public void AddVertex(Vector2 vertex) + { + AddVertex(vertex.ToPointF()); + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// An enumerable collection of vertices to add. + /// is . + public void AddVertices(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + foreach (PointF vertex in vertices) + { + AddVertex(vertex); + } + } + + /// + /// Adds a collection of vertices to this polygon. + /// + /// An enumerable collection of vertices to add. + /// is . + public void AddVertices(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + foreach (Vector2 vertex in vertices) + { + AddVertex(vertex); + } + } + + /// + /// Clears all vertices from this polygon. + /// + public void ClearVertices() + { + _vertices.Clear(); + } + + /// + public override bool Equals(object? obj) + { + return obj is PolygonF polygon && Equals(polygon); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(PolygonF? other) + { + return other is not null && _vertices.SequenceEqual(other._vertices); + } + + /// + public override int GetHashCode() + { + return _vertices.Aggregate(0, HashCode.Combine); + } +} diff --git a/X10D/src/Drawing/Polyhedron.cs b/X10D/src/Drawing/Polyhedron.cs new file mode 100644 index 0000000..2f46ca0 --- /dev/null +++ b/X10D/src/Drawing/Polyhedron.cs @@ -0,0 +1,249 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a 3D polyhedron composed of single-precision floating-point points. +/// +public class Polyhedron : IEquatable +{ + private readonly List _vertices = new(); + + /// + /// Initializes a new instance of the class. + /// + public Polyhedron() + { + } + + /// + /// Initializes a new instance of the class by copying the specified polyhedron. + /// + /// is . + public Polyhedron(Polyhedron polyhedron) + : this(polyhedron?._vertices ?? throw new ArgumentNullException(nameof(polyhedron))) + { + } + + /// + /// Initializes a new instance of the class by constructing it from the specified vertices. + /// + /// An enumerable collection of vertices from which the polyhedron should be constructed. + /// is . + public Polyhedron(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + _vertices = new List(vertices); + } + + /// + /// Gets an empty polyhedron. That is, a polygon with no vertices. + /// + /// An empty polyhedron. + public static Polyhedron Empty + { + get => new(); + } + + /// + /// Gets the number of vertices in this polyhedron. + /// + /// An value, representing the number of vertices in this polyhedron. + public int VertexCount + { + get => _vertices.Count; + } + + /// + /// Gets a read-only view of the vertices in this polyhedron. + /// + /// + /// A of values, representing the vertices of this polyhedron. + /// + public IReadOnlyList Vertices + { + get => _vertices.AsReadOnly(); + } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(Polyhedron? left, Polyhedron? right) + { + return Equals(left, right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Polyhedron? left, Polyhedron? right) + { + return !(left == right); + } + + /// + /// Implicitly converts a to a . + /// + /// The polyhedron to convert. + /// + /// The converted polyhedron, or if is . + /// + [return: NotNullIfNotNull("polygon")] + public static implicit operator Polyhedron?(Polygon? polygon) + { + return polygon is null ? null : FromPolygon(polygon); + } + + /// + /// Implicitly converts a to a . + /// + /// The polyhedron to convert. + /// + /// The converted polyhedron, or if is . + /// + [return: NotNullIfNotNull("polygon")] + public static implicit operator Polyhedron?(PolygonF? polygon) + { + return polygon is null ? null : FromPolygonF(polygon); + } + + /// + /// Converts a to a . + /// + /// The polyhedron to convert. + /// The converted polyhedron. + /// is . + public static Polyhedron FromPolygon(Polygon polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + + var vertices = new List(); + + foreach (Point vertex in polygon.Vertices) + { + vertices.Add(new Vector3(vertex.X, vertex.Y, 0)); + } + + return new Polyhedron(vertices); + } + + /// + /// Converts a to a . + /// + /// The polyhedron to convert. + /// The converted polyhedron. + /// is . + public static Polyhedron FromPolygonF(PolygonF polygon) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(polygon); +#else + if (polygon is null) + { + throw new ArgumentNullException(nameof(polygon)); + } +#endif + + var vertices = new List(); + + foreach (PointF vertex in polygon.Vertices) + { + vertices.Add(new Vector3(vertex.X, vertex.Y, 0)); + } + + return new Polyhedron(vertices); + } + + /// + /// Adds a vertex to this polyhedron. + /// + /// The vertex to add. + public void AddVertex(Vector3 vertex) + { + _vertices.Add(vertex); + } + + /// + /// Adds a collection of vertices to this polyhedron. + /// + /// An enumerable collection of vertices to add. + /// is . + public void AddVertices(IEnumerable vertices) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(vertices); +#else + if (vertices is null) + { + throw new ArgumentNullException(nameof(vertices)); + } +#endif + + foreach (Vector3 vertex in vertices) + { + AddVertex(vertex); + } + } + + /// + /// Clears all vertices from this polyhedron. + /// + public void ClearVertices() + { + _vertices.Clear(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Polyhedron polyhedron && Equals(polyhedron); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Polyhedron? other) + { + return other is not null && _vertices.SequenceEqual(other._vertices); + } + + /// + public override int GetHashCode() + { + return _vertices.Aggregate(0, HashCode.Combine); + } +} diff --git a/X10D/src/Drawing/RandomExtensions.cs b/X10D/src/Drawing/RandomExtensions.cs index edfecd3..24c7419 100644 --- a/X10D/src/Drawing/RandomExtensions.cs +++ b/X10D/src/Drawing/RandomExtensions.cs @@ -1,5 +1,7 @@ using System.Drawing; +#pragma warning disable CA5394 + namespace X10D.Drawing; /// diff --git a/X10D/src/Drawing/SizeExtensions.cs b/X10D/src/Drawing/SizeExtensions.cs new file mode 100644 index 0000000..54c3884 --- /dev/null +++ b/X10D/src/Drawing/SizeExtensions.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace X10D.Drawing; + +/// +/// Drawing-related extension methods for . +/// +public static class SizeExtensions +{ + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Point ToPoint(this Size size) + { + return new Point(size.Width, size.Height); + } + + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static PointF ToPointF(this Size size) + { + return new PointF(size.Width, size.Height); + } + + /// + /// Converts the current to a . + /// + /// The size to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector2 ToVector2(this Size size) + { + return new Vector2(size.Width, size.Height); + } +} diff --git a/X10D/src/Drawing/Sphere.cs b/X10D/src/Drawing/Sphere.cs new file mode 100644 index 0000000..26d91d6 --- /dev/null +++ b/X10D/src/Drawing/Sphere.cs @@ -0,0 +1,284 @@ +using System.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a sphere in 3D space, which uses single-precision floating-point numbers for its coordinates. +/// +public readonly struct Sphere : IEquatable, IComparable, IComparable +{ + /// + /// The empty sphere. That is, a sphere with a radius of zero. + /// + public static readonly Sphere Empty = new(); + + /// + /// The unit sphere. That is, a sphere with a radius of 1. + /// + public static readonly Sphere Unit = new(0, 0, 0, 1f); + + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate of the center point. + /// The Y coordinate of the center point. + /// The Z coordinate of the center point. + /// The radius. + public Sphere(float centerX, float centerY, float centerZ, float radius) + : this(new Vector3(centerX, centerY, centerZ), radius) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The center point. + /// The radius. + public Sphere(Vector3 center, float radius) + { + Center = center; + Radius = radius; + } + + /// + /// Gets the center-point of the sphere. + /// + /// The center point. + public Vector3 Center { get; } + + /// + /// Gets the circumference of the sphere. + /// + /// The circumference of the sphere, calculated as 2πr. + public float Circumference + { + get => 2 * MathF.PI * Radius; + } + + /// + /// Gets the diameter of the sphere. + /// + /// The diameter. + public float Diameter + { + get => Radius * 2; + } + + /// + /// Gets the radius of the sphere. + /// + /// The radius. + public float Radius { get; } + + /// + /// Gets the volume of this sphere. + /// + /// The volume. + public float Volume + { + get => (4f / 3f) * MathF.PI * Radius * Radius * Radius; + } + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered equal; otherwise, + /// . + /// + public static bool operator ==(Sphere left, Sphere right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Sphere left, Sphere right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the radius of one circle is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(Sphere left, Sphere right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(Sphere left, Sphere right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(Sphere left, Sphere right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the radius of one circle is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(Sphere left, Sphere right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Compares this instance to another . + /// + /// The other object. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of , or + /// is . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + /// is not an instance of . + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not Sphere other) + { + throw new ArgumentException(ExceptionMessages.ObjectIsNotAValidType); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// The other sphere. + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// Comparison only takes into consideration the . + public int CompareTo(Sphere other) + { + return Radius.CompareTo(other.Radius); + } + + /// + public override bool Equals(object? obj) + { + return obj is Sphere other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Sphere other) + { + return Center.Equals(other.Center) && Radius.Equals(other.Radius); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Center, Radius); + } +} diff --git a/X10D/src/ExceptionMessages.Designer.cs b/X10D/src/ExceptionMessages.Designer.cs index 7a62adb..ebabf39 100644 --- a/X10D/src/ExceptionMessages.Designer.cs +++ b/X10D/src/ExceptionMessages.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +18,7 @@ namespace X10D { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ExceptionMessages { @@ -69,6 +68,24 @@ namespace X10D { } } + /// + /// Looks up a localized string similar to Collection is read-only. Try using DisposeAll instead.. + /// + internal static string CollectionIsReadOnly_DisposeAll { + get { + return ResourceManager.GetString("CollectionIsReadOnly_DisposeAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Collection is read-only. Try using DisposeAllAsync instead.. + /// + internal static string CollectionIsReadOnly_DisposeAllAsync { + get { + return ResourceManager.GetString("CollectionIsReadOnly_DisposeAllAsync", resourceCulture); + } + } + /// /// Looks up a localized string similar to count must be greater than or equal to 0.. /// @@ -78,6 +95,69 @@ namespace X10D { } } + /// + /// Looks up a localized string similar to Count must be positive and count must refer to a location within the string/array/collection.. + /// + internal static string CountMustBeInRange { + get { + return ResourceManager.GetString("CountMustBeInRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The destination span is too short to contain the data.. + /// + internal static string DestinationSpanLengthTooShort { + get { + return ResourceManager.GetString("DestinationSpanLengthTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The end index must be less than the list count.. + /// + internal static string EndIndexGreaterThanCount { + get { + return ResourceManager.GetString("EndIndexGreaterThanCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The end index must be greater than or equal to the start index.. + /// + internal static string EndIndexLessThanStartIndex { + get { + return ResourceManager.GetString("EndIndexLessThanStartIndex", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Must specify valid information for parsing in the string.. + /// + internal static string EnumParseEmptyStringException { + get { + return ResourceManager.GetString("EnumParseEmptyStringException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type provided must be an Enum.. + /// + internal static string EnumParseNotEnumException { + get { + return ResourceManager.GetString("EnumParseNotEnumException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The enum has a size that is not supported.. + /// + internal static string EnumSizeIsUnexpected { + get { + return ResourceManager.GetString("EnumSizeIsUnexpected", resourceCulture); + } + } + /// /// Looks up a localized string similar to HashAlgorithm's Create method returned null reference.. /// @@ -96,6 +176,15 @@ namespace X10D { } } + /// + /// Looks up a localized string similar to Index was out of range. Must be non-negative and less than or equal to the size of the collection.. + /// + internal static string IndexOutOfRange { + get { + return ResourceManager.GetString("IndexOutOfRange", resourceCulture); + } + } + /// /// Looks up a localized string similar to Length must be greater than or equal to 0.. /// @@ -132,6 +221,33 @@ namespace X10D { } } + /// + /// Looks up a localized string similar to The specified object is not a valid type.. + /// + internal static string ObjectIsNotAValidType { + get { + return ResourceManager.GetString("ObjectIsNotAValidType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The source contains no elements.. + /// + internal static string SourceContainsNoElements { + get { + return ResourceManager.GetString("SourceContainsNoElements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The source contains too many elements.. + /// + internal static string SourceSpanIsTooLarge { + get { + return ResourceManager.GetString("SourceSpanIsTooLarge", resourceCulture); + } + } + /// /// Looks up a localized string similar to The stream does not support reading.. /// @@ -186,6 +302,24 @@ namespace X10D { } } + /// + /// Looks up a localized string similar to Rune.Utf8SequenceLength returns value {0} which is outside range 1 to 4 (inclusive), which is unexpected according to the official documentation.. + /// + internal static string UnexpectedRuneUtf8SequenceLength { + get { + return ResourceManager.GetString("UnexpectedRuneUtf8SequenceLength", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value cannot be negative.. + /// + internal static string ValueCannotBeNegative { + get { + return ResourceManager.GetString("ValueCannotBeNegative", resourceCulture); + } + } + /// /// Looks up a localized string similar to Year cannot be zero.. /// diff --git a/X10D/src/ExceptionMessages.resx b/X10D/src/ExceptionMessages.resx index e5beb21..9bc8b28 100644 --- a/X10D/src/ExceptionMessages.resx +++ b/X10D/src/ExceptionMessages.resx @@ -1,71 +1,210 @@  - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + The buffer is too small to contain the data. + + + Count must be positive and count must refer to a location within the string/array/collection. + + + Collection is read-only. Try using DisposeAll instead. + + + Collection is read-only. Try using DisposeAllAsync instead. + + + The destination span is too short to contain the data. + + + The end index must be greater than or equal to the start index. + + + The end index must be less than the list count. + + + Must specify valid information for parsing in the string. + + + Type provided must be an Enum. + + + The enum has a size that is not supported. + + + The specified object is not a valid type. + + + {0} is not a class. + + + {0} is not an interface. + + + {0} does not inherit {1} + + + HashAlgorithm does not offer Create method. + + + HashAlgorithm's Create method returned null reference. + + + Index was out of range. Must be non-negative and less than or equal to the size of the collection. + + + Length must be greater than or equal to 0. + + + The source contains no elements. + + + The source contains too many elements. + + + The stream does not support reading. + + + The stream does not support writing. + + + The length of the stream is too large. + + + maxValue must be greater than or equal to 0 + + + maxValue must be greater than or equal to minValue + + + {0} cannot be greater than {1} + + + count must be greater than or equal to 0. + + + Year cannot be zero. + + + Rune.Utf8SequenceLength returns value {0} which is outside range 1 to 4 (inclusive), which is unexpected according to the official documentation. + + + Value cannot be negative. + \ No newline at end of file diff --git a/X10D/src/IO/DirectoryInfoExtensions.cs b/X10D/src/IO/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..5eb0a92 --- /dev/null +++ b/X10D/src/IO/DirectoryInfoExtensions.cs @@ -0,0 +1,60 @@ +using System.Security; + +namespace X10D.IO; + +/// +/// IO-related extension methods for . +/// +public static class DirectoryInfoExtensions +{ + /// + /// Removes all files and subdirectories in this directory, recursively, without deleting this directory. + /// + /// The directory to clear. + /// + /// The directory described by this object does not exist or could not be found. + /// + /// + /// A target file is open or memory-mapped on a computer running Microsoft Windows NT. + /// -or- + /// There is an open handle on one of the files, and the operating system is Windows XP or earlier. This open handle can + /// result from enumerating directories and files. + /// -or- + /// The directory is read-only. + /// -or- + /// The directory contains one or more files or subdirectories and recursive is false. + /// -or- + /// The directory is the application's current working directory. + /// -or- + /// There is an open handle on the directory or on one of its files, and the operating system is Windows XP or earlier. + /// This open handle can result from enumerating directories and files. + /// + /// The caller does not have the required permission. + /// This directory or one of its children contain a read-only file. + public static void Clear(this DirectoryInfo directory) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(directory); +#else + if (directory is null) + { + throw new ArgumentNullException(nameof(directory)); + } +#endif + + if (!directory.Exists) + { + throw new DirectoryNotFoundException(); + } + + foreach (FileInfo file in directory.EnumerateFiles()) + { + file.Delete(); + } + + foreach (DirectoryInfo childDirectory in directory.EnumerateDirectories()) + { + childDirectory.Delete(true); + } + } +} diff --git a/X10D/src/IO/DoubleExtensions.cs b/X10D/src/IO/DoubleExtensions.cs index a981d23..f71e4be 100644 --- a/X10D/src/IO/DoubleExtensions.cs +++ b/X10D/src/IO/DoubleExtensions.cs @@ -17,9 +17,9 @@ public static class DoubleExtensions [Pure] public static byte[] GetBytes(this double value) { - Span buffer = stackalloc byte[8]; + byte[] buffer = new byte[8]; value.TryWriteBytes(buffer); - return buffer.ToArray(); + return buffer; } /// @@ -31,9 +31,9 @@ public static class DoubleExtensions [Pure] public static byte[] GetBytes(this double value, Endianness endianness) { - Span buffer = stackalloc byte[8]; + byte[] buffer = new byte[8]; value.TryWriteBytes(buffer, endianness); - return buffer.ToArray(); + return buffer; } /// diff --git a/X10D/src/IO/Int16Extensions.cs b/X10D/src/IO/Int16Extensions.cs index 61be94a..60b0b87 100644 --- a/X10D/src/IO/Int16Extensions.cs +++ b/X10D/src/IO/Int16Extensions.cs @@ -16,9 +16,9 @@ public static class Int16Extensions [Pure] public static byte[] GetBytes(this short value) { - Span buffer = stackalloc byte[2]; + byte[] buffer = new byte[2]; value.TryWriteBytes(buffer); - return buffer.ToArray(); + return buffer; } /// @@ -30,9 +30,9 @@ public static class Int16Extensions [Pure] public static byte[] GetBytes(this short value, Endianness endianness) { - Span buffer = stackalloc byte[2]; + byte[] buffer = new byte[2]; value.TryWriteBytes(buffer, endianness); - return buffer.ToArray(); + return buffer; } /// diff --git a/X10D/src/IO/Int32Extensions.cs b/X10D/src/IO/Int32Extensions.cs index 57668fb..a349dec 100644 --- a/X10D/src/IO/Int32Extensions.cs +++ b/X10D/src/IO/Int32Extensions.cs @@ -16,9 +16,9 @@ public static class Int32Extensions [Pure] public static byte[] GetBytes(this int value) { - Span buffer = stackalloc byte[4]; + byte[] buffer = new byte[4]; value.TryWriteBytes(buffer); - return buffer.ToArray(); + return buffer; } /// @@ -30,7 +30,7 @@ public static class Int32Extensions [Pure] public static byte[] GetBytes(this int value, Endianness endianness) { - Span buffer = stackalloc byte[4]; + byte[] buffer = new byte[4]; value.TryWriteBytes(buffer, endianness); return buffer.ToArray(); } diff --git a/X10D/src/IO/ListOfByteExtensions.cs b/X10D/src/IO/ListOfByteExtensions.cs index d9fd480..3289fef 100644 --- a/X10D/src/IO/ListOfByteExtensions.cs +++ b/X10D/src/IO/ListOfByteExtensions.cs @@ -158,7 +158,6 @@ public static class ListOfByteExtensions throw new ArgumentNullException(nameof(source)); } #endif - return BitConverter.ToInt64(source.ToArray(), startIndex); } @@ -211,15 +210,13 @@ public static class ListOfByteExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(encoding); #else if (source is null) { throw new ArgumentNullException(nameof(source)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(encoding); -#else + if (encoding is null) { throw new ArgumentNullException(nameof(encoding)); diff --git a/X10D/src/IO/StreamExtensions.cs b/X10D/src/IO/StreamExtensions.cs index edabeec..c55b784 100644 --- a/X10D/src/IO/StreamExtensions.cs +++ b/X10D/src/IO/StreamExtensions.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -107,6 +108,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + const int decimalSize = sizeof(decimal); const int int32Size = sizeof(int); const int partitionSize = decimalSize / int32Size; @@ -166,6 +172,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(double)]; stream.Read(buffer); @@ -221,6 +232,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(short)]; stream.Read(buffer); @@ -270,6 +286,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(int)]; stream.Read(buffer); @@ -319,6 +340,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(long)]; stream.Read(buffer); @@ -345,7 +371,7 @@ public static class StreamExtensions /// The stream from which the value should be read. /// The endian encoding to use. /// A single-precision floating point value read from the stream. - public static double ReadSingle(this Stream stream, Endianness endianness) + public static float ReadSingle(this Stream stream, Endianness endianness) { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(stream); @@ -368,6 +394,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(float)]; stream.Read(buffer); @@ -425,6 +456,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(ushort)]; stream.Read(buffer); @@ -476,6 +512,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(uint)]; stream.Read(buffer); @@ -527,6 +568,11 @@ public static class StreamExtensions } #endif + if (!stream.CanRead) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportReading); + } + Span buffer = stackalloc byte[sizeof(ulong)]; stream.Read(buffer); @@ -609,6 +655,7 @@ public static class StreamExtensions /// The stream to which the value should be written. /// The two-byte signed integer to write. /// The number of bytes written to the stream. + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, short value) { return stream.Write(value, DefaultEndianness); @@ -645,6 +692,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(short)]; if (endianness == Endianness.LittleEndian) @@ -666,6 +718,7 @@ public static class StreamExtensions /// The stream to which the value should be written. /// The four-byte signed integer to write. /// The number of bytes written to the stream. + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, int value) { return stream.Write(value, DefaultEndianness); @@ -703,6 +756,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(int)]; if (endianness == Endianness.LittleEndian) @@ -725,6 +783,7 @@ public static class StreamExtensions /// The eight-byte signed integer to write. /// The number of bytes written to the stream. /// is . + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, long value) { return stream.Write(value, DefaultEndianness); @@ -762,6 +821,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(long)]; if (endianness == Endianness.LittleEndian) @@ -785,6 +849,7 @@ public static class StreamExtensions /// The number of bytes written to the stream. /// is . [CLSCompliant(false)] + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, ushort value) { return stream.Write(value, DefaultEndianness); @@ -823,6 +888,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(ushort)]; if (endianness == Endianness.LittleEndian) @@ -846,6 +916,7 @@ public static class StreamExtensions /// The number of bytes written to the stream. /// is . [CLSCompliant(false)] + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, uint value) { return stream.Write(value, DefaultEndianness); @@ -884,6 +955,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(uint)]; if (endianness == Endianness.LittleEndian) @@ -907,6 +983,7 @@ public static class StreamExtensions /// The number of bytes written to the stream. /// is . [CLSCompliant(false)] + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, ulong value) { return stream.Write(value, DefaultEndianness); @@ -945,6 +1022,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(ulong)]; if (endianness == Endianness.LittleEndian) @@ -991,6 +1073,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(float)]; if (endianness == Endianness.LittleEndian) @@ -1000,10 +1087,17 @@ public static class StreamExtensions #else if (BitConverter.IsLittleEndian) { - value = BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)); + MemoryMarshal.Write(buffer, ref value); + } + else + { + // dotcover disable + //NOSONAR + int temp = BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)); + MemoryMarshal.Write(buffer, ref temp); + //NOSONAR + // dotcover enable } - - System.Runtime.InteropServices.MemoryMarshal.Write(buffer, ref value); #endif } else @@ -1013,10 +1107,17 @@ public static class StreamExtensions #else if (BitConverter.IsLittleEndian) { - value = BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)); + int temp = BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)); + MemoryMarshal.Write(buffer, ref temp); + } + else + { + // dotcover disable + //NOSONAR + MemoryMarshal.Write(buffer, ref value); + //NOSONAR + // dotcover enable } - - System.Runtime.InteropServices.MemoryMarshal.Write(buffer, ref value); #endif } @@ -1055,6 +1156,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + Span buffer = stackalloc byte[sizeof(double)]; if (endianness == Endianness.LittleEndian) @@ -1064,10 +1170,17 @@ public static class StreamExtensions #else if (BitConverter.IsLittleEndian) { - value = BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)); + MemoryMarshal.Write(buffer, ref value); + } + else + { + // dotcover disable + //NOSONAR + long temp = BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)); + MemoryMarshal.Write(buffer, ref temp); + //NOSONAR + // dotcover enable } - - System.Runtime.InteropServices.MemoryMarshal.Write(buffer, ref value); #endif } else @@ -1077,10 +1190,17 @@ public static class StreamExtensions #else if (BitConverter.IsLittleEndian) { - value = BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)); + long temp = BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)); + MemoryMarshal.Write(buffer, ref temp); + } + else + { + // dotcover disable + //NOSONAR + MemoryMarshal.Write(buffer, ref value); + //NOSONAR + // dotcover enable } - - System.Runtime.InteropServices.MemoryMarshal.Write(buffer, ref value); #endif } @@ -1096,6 +1216,7 @@ public static class StreamExtensions /// The endian encoding to use. /// The number of bytes written to the stream. /// is . + [ExcludeFromCodeCoverage] public static int Write(this Stream stream, decimal value, Endianness endianness) { #if NET6_0_OR_GREATER @@ -1119,6 +1240,11 @@ public static class StreamExtensions } #endif + if (!stream.CanWrite) + { + throw new ArgumentException(ExceptionMessages.StreamDoesNotSupportWriting); + } + int[] bits = decimal.GetBits(value); long preWritePosition = stream.Position; @@ -1135,7 +1261,7 @@ public static class StreamExtensions return (int)(stream.Position - preWritePosition); } - private static int WriteInternal(this Stream stream, Span value) + private static int WriteInternal(this Stream stream, ReadOnlySpan value) { long preWritePosition = stream.Position; stream.Write(value); diff --git a/X10D/src/IO/TextReaderExtensions.cs b/X10D/src/IO/TextReaderExtensions.cs new file mode 100644 index 0000000..4ecb213 --- /dev/null +++ b/X10D/src/IO/TextReaderExtensions.cs @@ -0,0 +1,53 @@ +namespace X10D.IO; + +/// +/// IO-related extension methods for . +/// +public static class TextReaderExtensions +{ + /// + /// Enumerates the lines provided by the current text reader. + /// + /// The reader whose lines to enumerate. + /// An enumerable collection of lines as read from . + /// is . + public static IEnumerable EnumerateLines(this TextReader reader) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(reader); +#else + if (reader is null) + { + throw new ArgumentNullException(nameof(reader)); + } +#endif + + while (reader.ReadLine() is { } line) + { + yield return line; + } + } + + /// + /// Asynchronously enumerates the lines provided by the current text reader. + /// + /// The reader whose lines to enumerate. + /// An asynchronous enumerable collection of lines as read from . + /// is . + public static async IAsyncEnumerable EnumerateLinesAsync(this TextReader reader) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(reader); +#else + if (reader is null) + { + throw new ArgumentNullException(nameof(reader)); + } +#endif + + while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) + { + yield return line; + } + } +} diff --git a/X10D/src/Linq/EnumerableExtensions.cs b/X10D/src/Linq/EnumerableExtensions.cs new file mode 100644 index 0000000..4362dd2 --- /dev/null +++ b/X10D/src/Linq/EnumerableExtensions.cs @@ -0,0 +1,443 @@ +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace X10D.Linq; + +/// +/// LINQ-inspired extension methods for . +/// +public static class EnumerableExtensions +{ + /// + /// Concatenates a single value to the end of a sequence. + /// + /// The source sequence. + /// The value to concatenate to the end of the source sequence. + /// The type of the elements in . + /// + /// An that contains the concatenated elements of the input sequence, and the specified + /// value. + /// + /// is . + public static IEnumerable ConcatOne(this IEnumerable source, TSource value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + foreach (TSource item in source) + { + yield return item; + } + + yield return value; + } + + /// + /// Returns the minimum and maximum values in a sequence of values. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// The type of the elements in . + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (T? Minimum, T? Maximum) MinMax(this IEnumerable source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return MinMax(source, Comparer.Default); + } + + /// + /// Returns the minimum and maximum values in a sequence of values, using a specified comparer. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// The comparer which shall be used to compare each element in the sequence. + /// The type of the elements in . + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (T? Minimum, T? Maximum) MinMax(this IEnumerable source, IComparer? comparer) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + comparer ??= Comparer.Default; + + // ReSharper disable once PossibleMultipleEnumeration + if (source.TryGetSpan(out ReadOnlySpan span)) + { + return MinMaxSpan(comparer, span); + } + + T? minValue; + T? maxValue; + + // ReSharper disable once PossibleMultipleEnumeration + using (IEnumerator enumerator = source.GetEnumerator()) + { + if (!enumerator.MoveNext()) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + minValue = enumerator.Current; + maxValue = minValue; + + while (enumerator.MoveNext()) + { + T current = enumerator.Current; + + if (minValue is null || comparer.Compare(current, minValue) < 0) + { + minValue = current; + } + + if (maxValue is null || comparer.Compare(current, maxValue) > 0) + { + maxValue = current; + } + } + } + + return (minValue, maxValue); + } + + /// + /// Invokes a transform function on each element of a sequence of elements and returns the minimum and maximum values. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// A transform function to apply to each element. + /// The type of the elements in . + /// The type of the elements to compare. + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (TResult? Minimum, TResult? Maximum) MinMax(this IEnumerable source, + Func selector) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } +#endif + + return MinMax(source, selector, Comparer.Default); + } + + /// + /// Invokes a transform function on each element of a sequence of elements and returns the minimum and maximum values, + /// using a specified comparer. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// A transform function to apply to each element. + /// The comparer which shall be used to compare each element in the sequence. + /// The type of the elements in . + /// The type of the elements to compare. + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (TResult? Minimum, TResult? Maximum) MinMax(this IEnumerable source, + Func selector, + IComparer? comparer) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } +#endif + + comparer ??= Comparer.Default; + + // ReSharper disable once PossibleMultipleEnumeration + if (source.TryGetSpan(out ReadOnlySpan span)) + { + return MinMaxSpan(selector, comparer, span); + } + + TResult? minValue; + TResult? maxValue; + + // ReSharper disable once PossibleMultipleEnumeration + using (IEnumerator enumerator = source.GetEnumerator()) + { + if (!enumerator.MoveNext()) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + minValue = selector(enumerator.Current); + maxValue = minValue; + + while (enumerator.MoveNext()) + { + TResult current = selector(enumerator.Current); + + if (minValue is null || comparer.Compare(current, minValue) < 0) + { + minValue = current; + } + + if (maxValue is null || comparer.Compare(current, maxValue) > 0) + { + maxValue = current; + } + } + } + + return (minValue, maxValue); + } + + /// + /// Returns the minimum and maximum values in a sequence according to a specified key selector function. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// A function to extract the key for each element. + /// The type of the elements in . + /// The type of the elements to compare. + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (TSource? Minimum, TSource? Maximum) MinMaxBy(this IEnumerable source, + Func keySelector) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } +#endif + + return MinMaxBy(source, keySelector, Comparer.Default); + } + + /// + /// Returns the minimum and maximum values in a sequence according to a specified key selector function. + /// + /// A sequence of values to determine the minimum and maximum values of. + /// A function to extract the key for each element. + /// The comparer which shall be used to compare each element in the sequence. + /// The type of the elements in . + /// The type of the elements to compare. + /// A tuple containing the minimum and maximum values in . + /// is . + /// contains no elements. + public static (TSource? Minimum, TSource? Maximum) MinMaxBy(this IEnumerable source, + Func keySelector, + IComparer? comparer) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } +#endif + + comparer ??= Comparer.Default; + TSource? minValue; + TSource? maxValue; + + // ReSharper disable once PossibleMultipleEnumeration + if (source.TryGetSpan(out ReadOnlySpan span)) + { + return MinMaxSelectedSpan(keySelector, comparer, span); + } + + // ReSharper disable once PossibleMultipleEnumeration + using (IEnumerator enumerator = source.GetEnumerator()) + { + if (!enumerator.MoveNext()) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + minValue = enumerator.Current; + maxValue = minValue; + + while (enumerator.MoveNext()) + { + TSource current = enumerator.Current; + TResult transformedCurrent = keySelector(current); + + if (minValue is null || comparer.Compare(transformedCurrent, keySelector(minValue)) < 0) + { + minValue = current; + } + + if (maxValue is null || comparer.Compare(transformedCurrent, keySelector(maxValue)) > 0) + { + maxValue = current; + } + } + } + + return (minValue, maxValue); + } + + private static (T? Minimum, T? Maximum) MinMaxSpan(IComparer comparer, ReadOnlySpan span) + { + if (span.IsEmpty) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + T minValue = span[0]; + T maxValue = minValue; + + for (var index = 1; (uint)index < (uint)span.Length; index++) + { + T current = span[index]; + + if (comparer.Compare(current, minValue) < 0) + { + minValue = current; + } + + if (comparer.Compare(current, maxValue) > 0) + { + maxValue = current; + } + } + + return (minValue, maxValue); + } + + private static (TSource? Minimum, TSource? Maximum) MinMaxSelectedSpan(Func keySelector, + IComparer comparer, + ReadOnlySpan span) + { + if (span.IsEmpty) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + TSource minValue = span[0]; + TSource maxValue = minValue; + + for (var index = 1; (uint)index < (uint)span.Length; index++) + { + TSource current = span[index]; + TResult transformedCurrent = keySelector(current); + + if (minValue is null || comparer.Compare(transformedCurrent, keySelector(minValue)) < 0) + { + minValue = current; + } + + if (maxValue is null || comparer.Compare(transformedCurrent, keySelector(maxValue)) > 0) + { + maxValue = current; + } + } + + return (minValue, maxValue); + } + + private static (TResult?, TResult?) MinMaxSpan(Func selector, + IComparer comparer, + ReadOnlySpan span) + { + if (span.IsEmpty) + { + throw new InvalidOperationException(ExceptionMessages.SourceContainsNoElements); + } + + TResult minValue = selector(span[0]); + TResult maxValue = minValue; + + for (var index = 1; (uint)index < (uint)span.Length; index++) + { + TResult current = selector(span[index]); + + if (minValue is null || comparer.Compare(current, minValue) < 0) + { + minValue = current; + } + + if (maxValue is null || comparer.Compare(current, maxValue) > 0) + { + maxValue = current; + } + } + + return (minValue, maxValue); + } + + private static bool TryGetSpan(this IEnumerable source, out ReadOnlySpan span) + { + var result = true; + + switch (source) + { + case TSource[] array: + span = array; + break; + +#if NET5_0_OR_GREATER + case List list: + span = CollectionsMarshal.AsSpan(list); + break; +#endif + + default: + span = default; + result = false; + break; + } + + return result; + } +} diff --git a/X10D/src/Math/ByteExtensions.cs b/X10D/src/Math/ByteExtensions.cs index 310c4e4..016ffbb 100644 --- a/X10D/src/Math/ByteExtensions.cs +++ b/X10D/src/Math/ByteExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; namespace X10D.Math; @@ -9,7 +9,7 @@ namespace X10D.Math; public static class ByteExtensions { /// - /// Computes the digital root of this 16-bit integer. + /// Computes the digital root of this 8-bit integer. /// /// The value whose digital root to compute. /// The digital root of . @@ -57,6 +57,23 @@ public static class ByteExtensions return result; } + /// + /// Calculates the greatest common factor between the current 8-bit unsigned integer, and another 8-bit unsigned integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static byte GreatestCommonFactor(this byte value, byte other) + { + return (byte)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -73,7 +90,7 @@ public static class ByteExtensions #endif public static bool IsEven(this byte value) { - return value % 2 == 0; + return (value & 1) == 0; } /// @@ -108,6 +125,23 @@ public static class ByteExtensions return ((long)value).IsPrime(); } + /// + /// Calculates the lowest common multiple between the current 8-bit signed integer, and another 8-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static byte LowestCommonMultiple(this byte value, byte other) + { + return (byte)((long)value).LowestCommonMultiple(other); + } + /// /// Returns the multiplicative persistence of a specified value. /// @@ -126,4 +160,39 @@ public static class ByteExtensions { return ((long)value).MultiplicativePersistence(); } + + /// + /// Wraps the current 8-bit unsigned integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static byte Wrap(this byte value, byte low, byte high) + { + return (byte)((ulong)value).Wrap(low, high); + } + + /// + /// Wraps the current 8-bit unsigned integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static byte Wrap(this byte value, byte length) + { + return (byte)((ulong)value).Wrap(length); + } } diff --git a/X10D/src/Math/ComparableExtensions.cs b/X10D/src/Math/ComparableExtensions.cs index 06cd86a..58fae9a 100644 --- a/X10D/src/Math/ComparableExtensions.cs +++ b/X10D/src/Math/ComparableExtensions.cs @@ -133,7 +133,7 @@ public static class ComparableExtensions if (lower.GreaterThan(upper)) { throw new ArgumentException( - string.Format(ExceptionMessages.LowerCannotBeGreaterThanUpper, lower, upper), + string.Format(CultureInfo.CurrentCulture, ExceptionMessages.LowerCannotBeGreaterThanUpper, lower, upper), nameof(lower)); } diff --git a/X10D/src/Math/DecimalExtensions.cs b/X10D/src/Math/DecimalExtensions.cs index 418909c..280c81d 100644 --- a/X10D/src/Math/DecimalExtensions.cs +++ b/X10D/src/Math/DecimalExtensions.cs @@ -9,19 +9,21 @@ namespace X10D.Math; /// public static class DecimalExtensions { -#if NETCOREAPP3_0_OR_GREATER /// /// Returns the complex square root of this decimal number. /// /// The number whose square root is to be found. /// The square root of . [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif public static Complex ComplexSqrt(this decimal value) { return Complex.Sqrt((double)value); } -#endif /// /// Returns a value indicating whether the current value is evenly divisible by 2. @@ -94,6 +96,23 @@ public static class DecimalExtensions return System.Math.Round(value / nearest) * nearest; } + /// + /// Saturates this decimal number. + /// + /// The value to saturate. + /// The saturated value. + /// This method clamps between 0 and 1. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static decimal Saturate(this decimal value) + { + return System.Math.Clamp(value, 0.0m, 1.0m); + } + /// /// Returns an integer that indicates the sign of this decimal number. /// @@ -133,7 +152,7 @@ public static class DecimalExtensions } /// - /// Returns the square root of this double-precision floating-point number. + /// Returns the square root of this decimal number. /// /// The number whose square root is to be found. /// @@ -173,7 +192,7 @@ public static class DecimalExtensions case 0: return 0; case < 0: - throw new ArgumentException("value cannot be negative", nameof(value)); + throw new ArgumentException(ExceptionMessages.ValueCannotBeNegative, nameof(value)); } decimal previous; @@ -181,14 +200,45 @@ public static class DecimalExtensions do { previous = current; - if (previous == 0.0m) - { - return 0; - } - current = (previous + value / previous) / 2; } while (System.Math.Abs(previous - current) > 0.0m); return current; } + + /// + /// Wraps the current decimal number between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static decimal Wrap(this decimal value, decimal low, decimal high) + { + decimal difference = high - low; + return low + (((value - low) % difference) + difference) % difference; + } + + /// + /// Wraps the current decimal number between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static decimal Wrap(this decimal value, decimal length) + { + return ((value % length) + length) % length; + } } diff --git a/X10D/src/Math/DoubleExtensions.cs b/X10D/src/Math/DoubleExtensions.cs index 203eb4a..232df3f 100644 --- a/X10D/src/Math/DoubleExtensions.cs +++ b/X10D/src/Math/DoubleExtensions.cs @@ -140,23 +140,26 @@ public static class DoubleExtensions return System.Math.Atanh(value); } -#if NETCOREAPP3_0_OR_GREATER /// /// Returns the complex square root of this double-precision floating-point number. /// /// The number whose square root is to be found. /// The square root of . [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif public static Complex ComplexSqrt(this double value) { switch (value) { case double.PositiveInfinity: case double.NegativeInfinity: - return Complex.Infinity; + return new Complex(double.PositiveInfinity, double.PositiveInfinity); case double.NaN: - return Complex.NaN; + return new Complex(double.NaN, double.NaN); case 0: return Complex.Zero; @@ -166,7 +169,6 @@ public static class DoubleExtensions return new Complex(0, System.Math.Sqrt(-value)); } } -#endif /// /// Returns the cosine of the specified angle. @@ -312,6 +314,23 @@ public static class DoubleExtensions return System.Math.Round(value / nearest) * nearest; } + /// + /// Saturates this double-precision floating-point number. + /// + /// The value to saturate. + /// The saturated value. + /// This method clamps between 0 and 1. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double Saturate(this double value) + { + return System.Math.Clamp(value, 0.0, 1.0); + } + /// /// Returns the sine of the specified angle. /// @@ -443,11 +462,6 @@ public static class DoubleExtensions do { previous = current; - if (previous == 0.0) - { - return 0; - } - current = (previous + value / previous) / 2; } while (System.Math.Abs(previous - current) > double.Epsilon); @@ -494,4 +508,40 @@ public static class DoubleExtensions { return System.Math.Tanh(value); } + + /// + /// Wraps the current double-precision floating-point number between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double Wrap(this double value, double low, double high) + { + double difference = high - low; + return low + (((value - low) % difference) + difference) % difference; + } + + /// + /// Wraps the current double-precision floating-point number between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double Wrap(this double value, double length) + { + return ((value % length) + length) % length; + } } diff --git a/X10D/src/Math/InclusiveOptions.cs b/X10D/src/Math/InclusiveOptions.cs index 1c78197..edb7030 100644 --- a/X10D/src/Math/InclusiveOptions.cs +++ b/X10D/src/Math/InclusiveOptions.cs @@ -4,7 +4,7 @@ /// Provides options for clusivity. /// [Flags] -public enum InclusiveOptions : byte +public enum InclusiveOptions { /// /// Indicates that the comparison will be exclusive. diff --git a/X10D/src/Math/Int16Extensions.cs b/X10D/src/Math/Int16Extensions.cs index 50dd890..3a72b9a 100644 --- a/X10D/src/Math/Int16Extensions.cs +++ b/X10D/src/Math/Int16Extensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; namespace X10D.Math; @@ -62,6 +62,23 @@ public static class Int16Extensions return result; } + /// + /// Calculates the greatest common factor between the current 16-bit signed integer, and another 16-bit signed integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static short GreatestCommonFactor(this short value, short other) + { + return (short)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -118,6 +135,23 @@ public static class Int16Extensions return ((long)value).IsPrime(); } + /// + /// Calculates the lowest common multiple between the current 16-bit signed integer, and another 16-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static short LowestCommonMultiple(this short value, short other) + { + return (short)((long)value).LowestCommonMultiple(other); + } + /// /// Performs a modulo operation which supports a negative dividend. /// @@ -200,4 +234,39 @@ public static class Int16Extensions { return System.Math.Sign(value); } + + /// + /// Wraps the current 16-bit signed integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static short Wrap(this short value, short low, short high) + { + return (short)((long)value).Wrap(low, high); + } + + /// + /// Wraps the current 16-bit signed integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static short Wrap(this short value, short length) + { + return (short)((long)value).Wrap(length); + } } diff --git a/X10D/src/Math/Int32Extensions.cs b/X10D/src/Math/Int32Extensions.cs index 86d3c2f..d654014 100644 --- a/X10D/src/Math/Int32Extensions.cs +++ b/X10D/src/Math/Int32Extensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; namespace X10D.Math; @@ -62,6 +62,23 @@ public static class Int32Extensions return result; } + /// + /// Calculates the greatest common factor between the current 32-bit signed integer, and another 32-bit signed integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int GreatestCommonFactor(this int value, int other) + { + return (int)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -118,6 +135,23 @@ public static class Int32Extensions return ((long)value).IsPrime(); } + /// + /// Calculates the lowest common multiple between the current 32-bit signed integer, and another 32-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int LowestCommonMultiple(this int value, int other) + { + return (int)((long)value).LowestCommonMultiple(other); + } + /// /// Performs a modulo operation which supports a negative dividend. /// @@ -200,4 +234,39 @@ public static class Int32Extensions { return System.Math.Sign(value); } + + /// + /// Wraps the current 32-bit signed integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int Wrap(this int value, int low, int high) + { + return (int)((long)value).Wrap(low, high); + } + + /// + /// Wraps the current 32-bit signed integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int Wrap(this int value, int length) + { + return (int)((long)value).Wrap(length); + } } diff --git a/X10D/src/Math/Int64Extensions.cs b/X10D/src/Math/Int64Extensions.cs index 51dfc75..8d76498 100644 --- a/X10D/src/Math/Int64Extensions.cs +++ b/X10D/src/Math/Int64Extensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; namespace X10D.Math; @@ -62,6 +62,28 @@ public static class Int64Extensions return result; } + /// + /// Calculates the greatest common factor between the current 64-bit signed integer, and another 64-bit unsigned integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long GreatestCommonFactor(this long value, long other) + { + while (other != 0) + { + (value, other) = (other, value % other); + } + + return value; + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -117,17 +139,11 @@ public static class Int64Extensions { switch (value) { - case < 2: return false; - case 2: - case 3: return true; + case <= 1: return false; + case <= 3: return true; } - if (value % 2 == 0 || value % 3 == 0) - { - return false; - } - - if ((value + 1) % 6 != 0 && (value - 1) % 6 != 0) + if ((value & 1) == 0 || value % 3 == 0) { return false; } @@ -143,6 +159,38 @@ public static class Int64Extensions return true; } + /// + /// Calculates the lowest common multiple between the current 64-bit signed integer, and another 64-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long LowestCommonMultiple(this long value, long other) + { + if (value == 0 || other == 0) + { + return 0; + } + + if (value == 1) + { + return other; + } + + if (other == 1) + { + return value; + } + + return value * other / value.GreatestCommonFactor(other); + } + /// /// Performs a modulo operation which supports a negative dividend. /// @@ -256,4 +304,40 @@ public static class Int64Extensions { return System.Math.Sign(value); } + + /// + /// Wraps the current 64-bit signed integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long Wrap(this long value, long low, long high) + { + long difference = high - low; + return low + (((value - low) % difference) + difference) % difference; + } + + /// + /// Wraps the current 64-bit signed integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long Wrap(this long value, long length) + { + return ((value % length) + length) % length; + } } diff --git a/X10D/src/Math/MathUtility.cs b/X10D/src/Math/MathUtility.cs index 8613219..890acac 100644 --- a/X10D/src/Math/MathUtility.cs +++ b/X10D/src/Math/MathUtility.cs @@ -8,6 +8,175 @@ namespace X10D.Math; /// public static class MathUtility { + private const double DefaultGamma = 2.2; + private const float DefaultGammaF = 2.2f; + + /// + /// Applies a simple bias function to value. + /// + /// The value to which the bias function will be applied. + /// The bias value. Valid values range from 0-1. + /// The biased result. + /// + /// If is less than 0.5, will be shifted downward; otherwise, upward. + /// + public static float Bias(float value, float bias) + { + return value / ((1.0f / bias - 2.0f) * (1.0f - value) + 1.0f); + } + + /// + /// Applies a simple bias function to value. + /// + /// The value to which the bias function will be applied. + /// The bias value. Valid values range from 0-1. + /// The biased result. + /// + /// If is less than 0.5, will be shifted downward; otherwise, upward. + /// + public static double Bias(double value, double bias) + { + return value / ((1.0 / bias - 2.0) * (1.0 - value) + 1.0); + } + + /// + /// Calculates exponential decay for a value. + /// + /// The value to decay. + /// A factor by which to scale the decay. + /// The decay amount. + /// The exponentially decayed value. + public static float ExponentialDecay(float value, float alpha, float decay) + { + return value * MathF.Exp(-decay * alpha); + } + + /// + /// Calculates exponential decay for a value. + /// + /// The value to decay. + /// A factor by which to scale the decay. + /// The decay amount. + /// The exponentially decayed value. + public static double ExponentialDecay(double value, double alpha, double decay) + { + return value * System.Math.Exp(-decay * alpha); + } + + /// + /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The linear value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float GammaToLinear(float value) + { + return GammaToLinear(value, DefaultGammaF); + } + + /// + /// Converts a gamma-encoded value to a linear value using the specified gamma value. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The gamma value to use for decoding. + /// The linear value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float GammaToLinear(float value, float gamma) + { + return MathF.Pow(value, 1.0f / gamma); + } + + /// + /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The linear value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double GammaToLinear(double value) + { + return GammaToLinear(value, DefaultGamma); + } + + /// + /// Converts a gamma-encoded value to a linear value using the specified gamma value. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The gamma value to use for decoding. + /// The linear value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double GammaToLinear(double value, double gamma) + { + return System.Math.Pow(value, 1.0 / gamma); + } + + /// + /// Returns the linear interpolation inverse of a value, such that it determines where a value lies between two other + /// values. + /// + /// The value whose lerp inverse is to be found. + /// The start of the range. + /// The end of the range. + /// A value determined by (alpha - start) / (end - start). + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float InverseLerp(float alpha, float start, float end) + { + if (MathF.Abs(start - end) < float.Epsilon) + { + return 0f; + } + + return (alpha - start) / (end - start); + } + + /// + /// Returns the linear interpolation inverse of a value, such that it determines where a value lies between two other + /// values. + /// + /// The value whose lerp inverse is to be found. + /// The start of the range. + /// The end of the range. + /// A value determined by (alpha - start) / (end - start). + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double InverseLerp(double alpha, double start, double end) + { + if (System.Math.Abs(start - end) < double.Epsilon) + { + return 0.0; + } + + return (alpha - start) / (end - start); + } + /// /// Linearly interpolates from one value to a target using a specified alpha. /// @@ -51,4 +220,240 @@ public static class MathUtility // "precise" method: (1 - t) * a + t * b return ((1.0 - alpha) * value) + (alpha * target); } + + /// + /// Converts a linear value to a gamma-encoded value using a gamma value of 2.2. + /// + /// The linear value to convert. Expected range is [0, 1]. + /// The gamma-encoded value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float LinearToGamma(float value) + { + return LinearToGamma(value, DefaultGammaF); + } + + /// + /// Converts a linear value to a gamma-encoded value using the specified gamma value. + /// + /// The linear value to convert. Expected range is [0, 1]. + /// The gamma value to use for encoding. + /// The gamma-encoded value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float LinearToGamma(float value, float gamma) + { + return MathF.Pow(value, 1.0f / gamma); + } + + /// + /// Converts a linear value to a gamma-encoded value using a gamma value of 2.2. + /// + /// The linear value to convert. Expected range is [0, 1]. + /// The gamma-encoded value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double LinearToGamma(double value) + { + return LinearToGamma(value, DefaultGamma); + } + + /// + /// Converts a linear value to a gamma-encoded value using the specified gamma value. + /// + /// The linear value to convert. Expected range is [0, 1]. + /// The gamma value to use for encoding. + /// The gamma-encoded value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double LinearToGamma(double value, double gamma) + { + return System.Math.Pow(value, 1.0 / gamma); + } + + /// + /// Returns the pulse wave for a given value. + /// + /// The value to calculate. + /// The inclusive lower bound of the pulse. + /// The inclusive upper bound of the pulse. + /// + /// 1 if lies between and ; + /// otherwise, 0. + /// + public static float Pulse(float value, float lowerBound, float upperBound) + { + bool result = lowerBound <= value && value <= upperBound; +#if NET6_0_OR_GREATER + return Unsafe.As(ref result); +#else + unsafe + { + var pResult = (int*)&result; + return *pResult; + } +#endif + } + + /// + /// Returns the pulse wave for a given value. + /// + /// The value to calculate. + /// The inclusive lower bound of the pulse. + /// The inclusive upper bound of the pulse. + /// + /// 1 if lies between and ; + /// otherwise, 0. + /// + public static double Pulse(double value, double lowerBound, double upperBound) + { + bool result = lowerBound <= value && value <= upperBound; +#if NET6_0_OR_GREATER + return Unsafe.As(ref result); +#else + unsafe + { + var pResult = (int*)&result; + return *pResult; + } +#endif + } + + /// + /// Returns the incremental sawtooth wave of a given value. + /// + /// The value to calculate. + /// The sawtooth wave of the given value. + public static float Sawtooth(float value) + { + return (value - MathF.Floor(value)); + } + + /// + /// Returns the incremental sawtooth wave of a given value. + /// + /// The value to calculate. + /// The sawtooth wave of the given value. + public static double Sawtooth(double value) + { + return (value - System.Math.Floor(value)); + } + + /// + /// Converts a value from being a percentage of one range, to being the same percentage in a new range. + /// + /// The value to convert. + /// The old minimum value. + /// The old maximum value. + /// The new minimum value. + /// The new maximum value. + /// The scaled value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float ScaleRange(float value, float oldMin, float oldMax, float newMin, float newMax) + { + float oldRange = oldMax - oldMin; + float newRange = newMax - newMin; + float alpha = (value - oldMin) / oldRange; + return (alpha * newRange) + newMin; + } + + /// + /// Converts a value from being a percentage of one range, to being the same percentage in a new range. + /// + /// The value to convert. + /// The old minimum value. + /// The old maximum value. + /// The new minimum value. + /// The new maximum value. + /// The scaled value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static double ScaleRange(double value, double oldMin, double oldMax, double newMin, double newMax) + { + double oldRange = oldMax - oldMin; + double newRange = newMax - newMin; + double alpha = (value - oldMin) / oldRange; + return (alpha * newRange) + newMin; + } + + /// + /// Calculates the sigmoid function for the given input value. + /// + /// The input value for which to calculate the sigmoid function. + /// The result of applying the sigmoid function to the input value. + /// + /// The sigmoid function is a commonly used activation function in artificial neural networks and logistic regression. It + /// maps any real-valued number to a value between 0 and 1. + /// + public static float Sigmoid(float value) + { + return 1.0f / (1.0f + MathF.Exp(-value)); + } + + /// + /// Calculates the sigmoid function for the given input value. + /// + /// The input value for which to calculate the sigmoid function. + /// The result of applying the sigmoid function to the input value. + /// + /// The sigmoid function is a commonly used activation function in artificial neural networks and logistic regression. It + /// maps any real-valued number to a value between 0 and 1. + /// + public static double Sigmoid(double value) + { + return 1.0f / (1.0f + System.Math.Exp(-value)); + } + + /// + /// Performs smooth Hermite interpolation from one value to a target using a specified alpha. + /// + /// The interpolation source. + /// The interpolation target. + /// The interpolation alpha. + /// The interpolation result. + public static float SmoothStep(float value, float target, float alpha) + { + alpha = System.Math.Clamp(alpha, 0.0f, 1.0f); + alpha = -2.0f * alpha * alpha * alpha + 3.0f * alpha * alpha; + return target * alpha + value * (1.0f - alpha); + } + + /// + /// Performs smooth Hermite interpolation from one value to a target using a specified alpha. + /// + /// The interpolation source. + /// The interpolation target. + /// The interpolation alpha. + /// The interpolation result. + public static double SmoothStep(double value, double target, double alpha) + { + alpha = System.Math.Clamp(alpha, 0.0, 1.0); + alpha = -2.0 * alpha * alpha * alpha + 3.0 * alpha * alpha; + return target * alpha + value * (1.0 - alpha); + } } diff --git a/X10D/src/Math/SByteExtensions.cs b/X10D/src/Math/SByteExtensions.cs index 07252ef..5bf96c5 100644 --- a/X10D/src/Math/SByteExtensions.cs +++ b/X10D/src/Math/SByteExtensions.cs @@ -63,6 +63,23 @@ public static class SByteExtensions return result; } + /// + /// Calculates the greatest common factor between the current 8-bit signed integer, and another 8-bit signed integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static sbyte GreatestCommonFactor(this sbyte value, sbyte other) + { + return (sbyte)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -79,7 +96,7 @@ public static class SByteExtensions #endif public static bool IsEven(this sbyte value) { - return value % 2 == 0; + return (value & 1) == 0; } /// @@ -119,6 +136,23 @@ public static class SByteExtensions return ((long)value).IsPrime(); } + /// + /// Calculates the lowest common multiple between the current 8-bit signed integer, and another 8-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static sbyte LowestCommonMultiple(this sbyte value, sbyte other) + { + return (sbyte)((long)value).LowestCommonMultiple(other); + } + /// /// Performs a modulo operation which supports a negative dividend. /// @@ -201,4 +235,39 @@ public static class SByteExtensions { return System.Math.Sign(value); } + + /// + /// Wraps the current 8-bit signed integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static sbyte Wrap(this sbyte value, sbyte low, sbyte high) + { + return (sbyte)((long)value).Wrap(low, high); + } + + /// + /// Wraps the current 8-bit signed integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static sbyte Wrap(this sbyte value, sbyte length) + { + return (sbyte)((long)value).Wrap(length); + } } diff --git a/X10D/src/Math/SingleExtensions.cs b/X10D/src/Math/SingleExtensions.cs index d72360a..faa5bcd 100644 --- a/X10D/src/Math/SingleExtensions.cs +++ b/X10D/src/Math/SingleExtensions.cs @@ -140,23 +140,26 @@ public static class SingleExtensions return MathF.Atanh(value); } -#if NETCOREAPP3_0_OR_GREATER /// /// Returns the complex square root of this single-precision floating-point number. /// /// The number whose square root is to be found. /// The square root of . [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif public static Complex ComplexSqrt(this float value) { switch (value) { case float.PositiveInfinity: case float.NegativeInfinity: - return Complex.Infinity; + return new Complex(double.PositiveInfinity, double.PositiveInfinity); case float.NaN: - return Complex.NaN; + return new Complex(double.NaN, double.NaN); case 0: return Complex.Zero; @@ -166,7 +169,6 @@ public static class SingleExtensions return new Complex(0, MathF.Sqrt(-value)); } } -#endif /// /// Returns the cosine of the specified angle. @@ -312,6 +314,23 @@ public static class SingleExtensions return MathF.Round(value / nearest) * nearest; } + /// + /// Saturates this single-precision floating-point number. + /// + /// The value to saturate. + /// The saturated value. + /// This method clamps between 0 and 1. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float Saturate(this float value) + { + return System.Math.Clamp(value, 0.0f, 1.0f); + } + /// /// Returns an integer that indicates the sign of this single-precision floating-point number. /// @@ -402,11 +421,6 @@ public static class SingleExtensions do { previous = current; - if (previous == 0.0f) - { - return 0; - } - current = (previous + value / previous) / 2; } while (MathF.Abs(previous - current) > float.Epsilon); @@ -493,4 +507,39 @@ public static class SingleExtensions { return MathF.Tanh(value); } + + /// + /// Wraps the current single-precision floating-point number between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float Wrap(this float value, float low, float high) + { + return (float)((double)value).Wrap(low, high); + } + + /// + /// Wraps the current single-precision floating-point number between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static float Wrap(this float value, float length) + { + return (float)((double)value).Wrap(length); + } } diff --git a/X10D/src/Math/UInt16Extensions.cs b/X10D/src/Math/UInt16Extensions.cs index dca43d9..065afcf 100644 --- a/X10D/src/Math/UInt16Extensions.cs +++ b/X10D/src/Math/UInt16Extensions.cs @@ -57,6 +57,24 @@ public static class UInt16Extensions return result; } + /// + /// Calculates the greatest common factor between the current 16-bit unsigned integer, and another 16-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ushort GreatestCommonFactor(this ushort value, ushort other) + { + return (ushort)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -113,6 +131,24 @@ public static class UInt16Extensions return !value.IsEven(); } + /// + /// Calculates the lowest common multiple between the current 16-bit unsigned integer, and another 16-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ushort LowestCommonMultiple(this ushort value, ushort other) + { + return (ushort)((ulong)value).LowestCommonMultiple(other); + } + /// /// Returns the multiplicative persistence of a specified value. /// @@ -131,4 +167,39 @@ public static class UInt16Extensions { return ((ulong)value).MultiplicativePersistence(); } + + /// + /// Wraps the current 16-bit unsigned integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ushort Wrap(this ushort value, ushort low, ushort high) + { + return (ushort)((ulong)value).Wrap(low, high); + } + + /// + /// Wraps the current 16-bit unsigned integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ushort Wrap(this ushort value, ushort length) + { + return (ushort)((ulong)value).Wrap(length); + } } diff --git a/X10D/src/Math/UInt32Extensions.cs b/X10D/src/Math/UInt32Extensions.cs index 8b1ffa5..df2d291 100644 --- a/X10D/src/Math/UInt32Extensions.cs +++ b/X10D/src/Math/UInt32Extensions.cs @@ -57,6 +57,24 @@ public static class UInt32Extensions return result; } + /// + /// Calculates the greatest common factor between the current 32-bit unsigned integer, and another 32-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static uint GreatestCommonFactor(this uint value, uint other) + { + return (uint)((long)value).GreatestCommonFactor(other); + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -113,6 +131,24 @@ public static class UInt32Extensions return !value.IsEven(); } + /// + /// Calculates the lowest common multiple between the current 32-bit unsigned integer, and another 32-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static uint LowestCommonMultiple(this uint value, uint other) + { + return (uint)((ulong)value).LowestCommonMultiple(other); + } + /// /// Returns the multiplicative persistence of a specified value. /// @@ -131,4 +167,39 @@ public static class UInt32Extensions { return ((ulong)value).MultiplicativePersistence(); } + + /// + /// Wraps the current 32-bit unsigned integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static uint Wrap(this uint value, uint low, uint high) + { + return (uint)((ulong)value).Wrap(low, high); + } + + /// + /// Wraps the current 32-bit unsigned integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static uint Wrap(this uint value, uint length) + { + return (uint)((ulong)value).Wrap(length); + } } diff --git a/X10D/src/Math/UInt64Extensions.cs b/X10D/src/Math/UInt64Extensions.cs index d0f78bd..818a975 100644 --- a/X10D/src/Math/UInt64Extensions.cs +++ b/X10D/src/Math/UInt64Extensions.cs @@ -57,6 +57,29 @@ public static class UInt64Extensions return result; } + /// + /// Calculates the greatest common factor between the current 64-bit unsigned integer, and another 64-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ulong GreatestCommonFactor(this ulong value, ulong other) + { + while (other != 0) + { + (value, other) = (other, value % other); + } + + return value; + } + /// /// Returns a value indicating whether the current value is evenly divisible by 2. /// @@ -93,17 +116,11 @@ public static class UInt64Extensions { switch (value) { - case < 2: return false; - case 2: - case 3: return true; + case <= 1: return false; + case <= 3: return true; } - if (value % 2 == 0 || value % 3 == 0) - { - return false; - } - - if ((value + 1) % 6 != 0 && (value - 1) % 6 != 0) + if ((value & 1) == 0 || value % 3 == 0) { return false; } @@ -138,6 +155,39 @@ public static class UInt64Extensions return !value.IsEven(); } + /// + /// Calculates the lowest common multiple between the current 64-bit unsigned integer, and another 64-bit unsigned + /// integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ulong LowestCommonMultiple(this ulong value, ulong other) + { + if (value == 0 || other == 0) + { + return 0; + } + + if (value == 1) + { + return other; + } + + if (other == 1) + { + return value; + } + + return value * other / value.GreatestCommonFactor(other); + } + /// /// Returns the multiplicative persistence of a specified value. /// @@ -187,4 +237,40 @@ public static class UInt64Extensions return persistence; } + + /// + /// Wraps the current 64-bit unsigned integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ulong Wrap(this ulong value, ulong low, ulong high) + { + ulong difference = high - low; + return low + (((value - low) % difference) + difference) % difference; + } + + /// + /// Wraps the current 64-bit unsigned integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ulong Wrap(this ulong value, ulong length) + { + return ((value % length) + length) % length; + } } diff --git a/X10D/src/Numerics/ByteExtensions.cs b/X10D/src/Numerics/ByteExtensions.cs index 02c6e74..c6eba5b 100644 --- a/X10D/src/Numerics/ByteExtensions.cs +++ b/X10D/src/Numerics/ByteExtensions.cs @@ -9,6 +9,26 @@ namespace X10D.Numerics; /// public static class ByteExtensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this byte value) + { + return ((uint)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -48,4 +68,23 @@ public static class ByteExtensions count = count.Mod(8); return (byte)((value >> count) | (value << (8 - count))); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static byte RoundUpToPowerOf2(this byte value) + { + return (byte)((uint)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/Int16Extensions.cs b/X10D/src/Numerics/Int16Extensions.cs index 1a3656b..aaa1005 100644 --- a/X10D/src/Numerics/Int16Extensions.cs +++ b/X10D/src/Numerics/Int16Extensions.cs @@ -8,6 +8,26 @@ namespace X10D.Numerics; /// public static class Int16Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this short value) + { + return ((uint)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -47,4 +67,23 @@ public static class Int16Extensions var unsigned = unchecked((ushort)value); return unchecked((short)unsigned.RotateRight(count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static short RoundUpToPowerOf2(this short value) + { + return (short)((uint)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/Int32Extensions.cs b/X10D/src/Numerics/Int32Extensions.cs index 3bf74b9..8fc4937 100644 --- a/X10D/src/Numerics/Int32Extensions.cs +++ b/X10D/src/Numerics/Int32Extensions.cs @@ -8,6 +8,26 @@ namespace X10D.Numerics; /// public static class Int32Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this int value) + { + return ((uint)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -47,4 +67,23 @@ public static class Int32Extensions var unsigned = unchecked((uint)value); return unchecked((int)unsigned.RotateRight(count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int RoundUpToPowerOf2(this int value) + { + return (int)((uint)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/Int64Extensions.cs b/X10D/src/Numerics/Int64Extensions.cs index 695ed4b..f115e08 100644 --- a/X10D/src/Numerics/Int64Extensions.cs +++ b/X10D/src/Numerics/Int64Extensions.cs @@ -8,6 +8,26 @@ namespace X10D.Numerics; /// public static class Int64Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this long value) + { + return ((ulong)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -47,4 +67,23 @@ public static class Int64Extensions var unsigned = unchecked((ulong)value); return unchecked((long)unsigned.RotateRight(count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long RoundUpToPowerOf2(this long value) + { + return (long)((ulong)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/QuaternionExtensions.cs b/X10D/src/Numerics/QuaternionExtensions.cs new file mode 100644 index 0000000..7852a67 --- /dev/null +++ b/X10D/src/Numerics/QuaternionExtensions.cs @@ -0,0 +1,78 @@ +using System.Numerics; + +namespace X10D.Numerics; + +/// +/// Numeric-related extension methods for . +/// +public static class QuaternionExtensions +{ + /// + /// Rotates the specified point with the specified rotation. + /// + /// The rotation. + /// The point. + /// The rotated point. + public static Vector3 Multiply(this in Quaternion rotation, in Vector3 point) + { + // the internet wrote it, I just handed it in. + // https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Quaternion.cs + float x = rotation.X * 2.0f; + float y = rotation.Y * 2.0f; + float z = rotation.Z * 2.0f; + float xx = rotation.X * x; + float yy = rotation.Y * y; + float zz = rotation.Z * z; + float xy = rotation.X * y; + float xz = rotation.X * z; + float yz = rotation.Y * z; + float wx = rotation.W * x; + float wy = rotation.W * y; + float wz = rotation.W * z; + + (float px, float py, float pz) = point; + + return new Vector3( + (1.0f - (yy + zz)) * px + (xy - wz) * py + (xz + wy) * pz, + (xy + wz) * px + (1.0f - (xx + zz)) * py + (yz - wx) * pz, + (xz - wy) * px + (yz + wx) * py + (1.0f - (xx + yy)) * pz + ); + } + + /// + /// Converts this quaternion to an axis/angle pair. + /// + /// The quaternion to convert. + /// A tuple containing the converted axis, and the angle in radians. + public static (Vector3 Axis, float Angle) ToAxisAngle(this in Quaternion value) + { + float angle = 2.0f * MathF.Acos(value.W); + Vector3 axis = Vector3.Normalize(new Vector3(value.X, value.Y, value.Z)); + return (axis, angle); + } + + /// + /// Converts this quaternion to a containing an Euler representation of the rotation. + /// + /// The quaternion to convert. + /// The Euler representation of , in radians. + public static Vector3 ToVector3(this in Quaternion value) + { + Quaternion normalized = Quaternion.Normalize(value); + + if (normalized == Quaternion.Identity) + { + return Vector3.Zero; + } + + float qx = normalized.X; + float qy = normalized.Y; + float qz = normalized.Z; + float qw = normalized.W; + + float x = MathF.Asin(-2 * (qx * qy - qz * qw)); + float y = MathF.Atan2(2 * (qy * qw + qx * qz), 1 - 2 * (qy * qy + qx * qx)); + float z = MathF.Atan2(2 * (qx * qw + qy * qz), 1 - 2 * (qz * qz + qw * qw)); + return new Vector3(-x, -y, -z); + } +} diff --git a/X10D/src/Numerics/RandomExtensions.cs b/X10D/src/Numerics/RandomExtensions.cs index cdf464f..5a89e94 100644 --- a/X10D/src/Numerics/RandomExtensions.cs +++ b/X10D/src/Numerics/RandomExtensions.cs @@ -1,6 +1,8 @@ using System.Numerics; using X10D.Core; +#pragma warning disable CA5394 + namespace X10D.Numerics; /// diff --git a/X10D/src/Numerics/SByteExtensions.cs b/X10D/src/Numerics/SByteExtensions.cs index eb6ce07..bdc1d71 100644 --- a/X10D/src/Numerics/SByteExtensions.cs +++ b/X10D/src/Numerics/SByteExtensions.cs @@ -9,6 +9,26 @@ namespace X10D.Numerics; [CLSCompliant(false)] public static class SByteExtensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this sbyte value) + { + return ((uint)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -48,4 +68,23 @@ public static class SByteExtensions var signed = unchecked((byte)value); return unchecked((sbyte)signed.RotateRight(count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static sbyte RoundUpToPowerOf2(this sbyte value) + { + return (sbyte)((uint)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/UInt16Extensions.cs b/X10D/src/Numerics/UInt16Extensions.cs index 54403c9..f28e318 100644 --- a/X10D/src/Numerics/UInt16Extensions.cs +++ b/X10D/src/Numerics/UInt16Extensions.cs @@ -9,6 +9,26 @@ namespace X10D.Numerics; [CLSCompliant(false)] public static class UInt16Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this ushort value) + { + return ((uint)value).PopCount(); + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -46,4 +66,23 @@ public static class UInt16Extensions { return (ushort)((ushort)(value >> count) | (ushort)(value << (16 - count))); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ushort RoundUpToPowerOf2(this ushort value) + { + return (ushort)((uint)value).RoundUpToPowerOf2(); + } } diff --git a/X10D/src/Numerics/UInt32Extensions.cs b/X10D/src/Numerics/UInt32Extensions.cs index 7381b1b..b02a205 100644 --- a/X10D/src/Numerics/UInt32Extensions.cs +++ b/X10D/src/Numerics/UInt32Extensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Contracts; +using System.Numerics; using System.Runtime.CompilerServices; namespace X10D.Numerics; @@ -9,6 +10,39 @@ namespace X10D.Numerics; [CLSCompliant(false)] public static class UInt32Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this uint value) + { +#if NETCOREAPP3_1_OR_GREATER + return BitOperations.PopCount(value); +#else + const uint c1 = 0x_55555555u; + const uint c2 = 0x_33333333u; + const uint c3 = 0x_0F0F0F0Fu; + const uint c4 = 0x_01010101u; + + value -= (value >> 1) & c1; + value = (value & c2) + ((value >> 2) & c2); + value = (((value + (value >> 4)) & c3) * c4) >> 24; + + return (int)value; +#endif + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -46,4 +80,34 @@ public static class UInt32Extensions { return (value >> count) | (value << (32 - count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static uint RoundUpToPowerOf2(this uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } } diff --git a/X10D/src/Numerics/UInt64Extensions.cs b/X10D/src/Numerics/UInt64Extensions.cs index 0f10aba..a103483 100644 --- a/X10D/src/Numerics/UInt64Extensions.cs +++ b/X10D/src/Numerics/UInt64Extensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Contracts; +using System.Numerics; using System.Runtime.CompilerServices; namespace X10D.Numerics; @@ -9,6 +10,39 @@ namespace X10D.Numerics; [CLSCompliant(false)] public static class UInt64Extensions { + /// + /// Returns the population count (number of bits set) of a mask. + /// + /// The mask. + /// The population count of . + /// + /// This method is similar in behavior to the x86 instruction + /// POPCNT + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static int PopCount(this ulong value) + { +#if NETCOREAPP3_1_OR_GREATER + return BitOperations.PopCount(value); +#else + const ulong c1 = 0x_55555555_55555555ul; + const ulong c2 = 0x_33333333_33333333ul; + const ulong c3 = 0x_0F0F0F0F_0F0F0F0Ful; + const ulong c4 = 0x_01010101_01010101ul; + + value -= (value >> 1) & c1; + value = (value & c2) + ((value >> 2) & c2); + value = (((value + (value >> 4)) & c3) * c4) >> 56; + + return (int)value; +#endif + } + /// /// Rotates the current value left by the specified number of bits. /// @@ -46,4 +80,35 @@ public static class UInt64Extensions { return (value >> count) | (value << (64 - count)); } + + /// + /// Rounds the current value up to a power of two. + /// + /// The value to round. + /// + /// The smallest power of two that's greater than or equal to , or 0 if + /// is 0 or the result overflows. + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static ulong RoundUpToPowerOf2(this ulong value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + return value + 1; +#endif + } } diff --git a/X10D/src/Numerics/Vector2Extensions.cs b/X10D/src/Numerics/Vector2Extensions.cs index 8831e2c..ff51e75 100644 --- a/X10D/src/Numerics/Vector2Extensions.cs +++ b/X10D/src/Numerics/Vector2Extensions.cs @@ -1,6 +1,9 @@ using System.Diagnostics.Contracts; +using System.Drawing; using System.Numerics; using System.Runtime.CompilerServices; +using X10D.Drawing; +using X10D.Math; namespace X10D.Numerics; @@ -9,6 +12,150 @@ namespace X10D.Numerics; /// public static class Vector2Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + public static void Deconstruct(this Vector2 vector, out float x, out float y) + { + x = vector.X; + y = vector.Y; + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The line on which the point may lie. + /// + /// if lies on the line defined by ; otherwise + /// . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Vector2 point, LineF line) + { + (float x1, float x2) = (line.Start.X, line.End.X); + (float y1, float y2) = (line.Start.Y, line.End.Y); + (float x, float y) = (point.X, point.Y); + return System.Math.Abs((y2 - y1) * (x - x2) - (y - y2) * (x2 - x1)) < float.Epsilon; + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Vector2 point, PointF start, PointF end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Determines if the current lies on the specified line. + /// + /// The point to check. + /// The starting point of the line. + /// The ending point of the line. + /// + /// if lies on the line defined by and + /// ; otherwise . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsOnLine(this Vector2 point, Vector2 start, Vector2 end) + { + return point.IsOnLine(new LineF(start, end)); + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector2 Round(this Vector2 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector2 Round(this Vector2 vector, float nearest) + { + float x = vector.X.Round(nearest); + float y = vector.Y.Round(nearest); + return new Vector2(x, y); + } + + /// + /// Converts the current to a . + /// + /// The vector to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static PointF ToPointF(this Vector2 vector) + { + return new PointF(vector.X, vector.Y); + } + + /// + /// Converts the current to a . + /// + /// The vector to convert. + /// The resulting . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static SizeF ToSizeF(this Vector2 vector) + { + return new SizeF(vector.X, vector.Y); + } + /// /// Returns a vector whose Y component is the same as the specified vector, and whose X component is a new value. /// diff --git a/X10D/src/Numerics/Vector3Extensions.cs b/X10D/src/Numerics/Vector3Extensions.cs index 0f906a9..fd81345 100644 --- a/X10D/src/Numerics/Vector3Extensions.cs +++ b/X10D/src/Numerics/Vector3Extensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; +using X10D.Math; namespace X10D.Numerics; @@ -9,6 +10,56 @@ namespace X10D.Numerics; /// public static class Vector3Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + /// The Z component value. + public static void Deconstruct(this Vector3 vector, out float x, out float y, out float z) + { + x = vector.X; + y = vector.Y; + z = vector.Z; + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector3 Round(this Vector3 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector3 Round(this Vector3 vector, float nearest) + { + float x = vector.X.Round(nearest); + float y = vector.Y.Round(nearest); + float z = vector.Z.Round(nearest); + return new Vector3(x, y, z); + } + /// /// Returns a vector whose Y and Z components are the same as the specified vector, and whose X component is a new value. /// diff --git a/X10D/src/Numerics/Vector4Extensions.cs b/X10D/src/Numerics/Vector4Extensions.cs index 20390bd..81e3e81 100644 --- a/X10D/src/Numerics/Vector4Extensions.cs +++ b/X10D/src/Numerics/Vector4Extensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; +using X10D.Math; namespace X10D.Numerics; @@ -9,6 +10,59 @@ namespace X10D.Numerics; /// public static class Vector4Extensions { + /// + /// Deconstructs the current into its components. + /// + /// The vector to deconstruct. + /// The X component value. + /// The Y component value. + /// The Z component value. + /// The W component value. + public static void Deconstruct(this Vector4 vector, out float x, out float y, out float z, out float w) + { + x = vector.X; + y = vector.Y; + z = vector.Z; + w = vector.W; + } + + /// + /// Rounds the components in the current to the nearest integer. + /// + /// The vector whose components to round. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector4 Round(this Vector4 vector) + { + return vector.Round(1.0f); + } + + /// + /// Rounds the components in the current to the nearest multiple of a specified number. + /// + /// The vector whose components to round. + /// The nearest multiple to which the components should be rounded. + /// The rounded vector. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static Vector4 Round(this Vector4 vector, float nearest) + { + float x = vector.X.Round(nearest); + float y = vector.Y.Round(nearest); + float z = vector.Z.Round(nearest); + float w = vector.W.Round(nearest); + return new Vector4(x, y, z, w); + } + /// /// Returns a vector whose Y, Z, and W components are the same as the specified vector, and whose X component is a new /// value. diff --git a/X10D/src/Reflection/MemberInfoExtensions.cs b/X10D/src/Reflection/MemberInfoExtensions.cs index 5c9bc7e..24039e0 100644 --- a/X10D/src/Reflection/MemberInfoExtensions.cs +++ b/X10D/src/Reflection/MemberInfoExtensions.cs @@ -37,8 +37,7 @@ public static class MemberInfoExtensions throw new ArgumentNullException(nameof(member)); } #endif - - return member.HasCustomAttribute(typeof(T)); + return Attribute.IsDefined(member, typeof(T)); } /// @@ -61,15 +60,13 @@ public static class MemberInfoExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(member); + ArgumentNullException.ThrowIfNull(attribute); #else if (member is null) { throw new ArgumentNullException(nameof(member)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(attribute); -#else + if (attribute is null) { throw new ArgumentNullException(nameof(attribute)); @@ -82,7 +79,7 @@ public static class MemberInfoExtensions attribute, typeof(Attribute)), nameof(attribute)); } - return member.GetCustomAttribute(attribute) is not null; + return Attribute.IsDefined(member, attribute); } /// @@ -106,15 +103,13 @@ public static class MemberInfoExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(member); + ArgumentNullException.ThrowIfNull(selector); #else if (member is null) { throw new ArgumentNullException(nameof(member)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(selector); -#else + if (selector is null) { throw new ArgumentNullException(nameof(selector)); @@ -146,15 +141,13 @@ public static class MemberInfoExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(member); + ArgumentNullException.ThrowIfNull(selector); #else if (member is null) { throw new ArgumentNullException(nameof(member)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(selector); -#else + if (selector is null) { throw new ArgumentNullException(nameof(selector)); diff --git a/X10D/src/Reflection/TypeExtensions.cs b/X10D/src/Reflection/TypeExtensions.cs index 9d53c39..6f56d62 100644 --- a/X10D/src/Reflection/TypeExtensions.cs +++ b/X10D/src/Reflection/TypeExtensions.cs @@ -57,15 +57,13 @@ public static class TypeExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(interfaceType); #else if (value is null) { throw new ArgumentNullException(nameof(value)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(interfaceType); -#else + if (interfaceType is null) { throw new ArgumentNullException(nameof(interfaceType)); @@ -144,15 +142,13 @@ public static class TypeExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(type); #else if (value is null) { throw new ArgumentNullException(nameof(value)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(type); -#else + if (type is null) { throw new ArgumentNullException(nameof(type)); diff --git a/X10D/src/Text/CharSpanExtensions.cs b/X10D/src/Text/CharSpanExtensions.cs new file mode 100644 index 0000000..7450c9a --- /dev/null +++ b/X10D/src/Text/CharSpanExtensions.cs @@ -0,0 +1,70 @@ +namespace X10D.Text; + +/// +/// Extension methods for and of . +/// +public static class CharSpanExtensions +{ + /// + /// Counts the occurrences of a substring within the current character span. + /// + /// The haystack search space. + /// The character span to count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, ReadOnlySpan needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. + /// + /// The haystack search space. + /// The character span to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, ReadOnlySpan needle, StringComparison comparison) + { + return CountSubstring((ReadOnlySpan)haystack, needle, comparison); + } + + /// + /// Counts the occurrences of a substring within the current character span. + /// + /// The haystack search space. + /// The character span to count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current character span, using a specified string comparison method. + /// + /// The haystack search space. + /// The character span to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, ReadOnlySpan needle, StringComparison comparison) + { + if (haystack.IsEmpty || needle.IsEmpty) + { + return 0; + } + + int haystackLength = haystack.Length; + int needleLength = needle.Length; + var count = 0; + + for (var index = 0; index < haystackLength - needleLength - 1; index++) + { + if (haystack[index..(index + needleLength)].Equals(needle, comparison)) + { + count++; + } + } + + return count; + } +} diff --git a/X10D/src/Text/EnumerableExtensions.cs b/X10D/src/Text/EnumerableExtensions.cs new file mode 100644 index 0000000..c5552b3 --- /dev/null +++ b/X10D/src/Text/EnumerableExtensions.cs @@ -0,0 +1,86 @@ +using System.Text.RegularExpressions; + +namespace X10D.Text; + +/// +/// Text-related extension methods for . +/// +public static class EnumerableExtensions +{ + /// + /// Filters a sequence of strings by regular expression. + /// + /// The sequence of strings to filter. + /// The regular expression pattern to use for matching. + /// The filtered sequence. + /// + /// or is . + /// + public static IEnumerable Grep(this IEnumerable source, string pattern) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(pattern); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (pattern is null) + { + throw new ArgumentNullException(nameof(pattern)); + } +#endif + + return Grep(source, pattern, false); + } + + /// + /// Filters a sequence of strings by regular expression, optionally allowing to ignore casing. + /// + /// The sequence of strings to filter. + /// The regular expression pattern to use for matching. + /// + /// to ignore casing when matching; otherwise, . + /// + /// The filtered sequence. + /// + /// or is . + /// + public static IEnumerable Grep(this IEnumerable source, string pattern, bool ignoreCase) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(pattern); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (pattern is null) + { + throw new ArgumentNullException(nameof(pattern)); + } +#endif + +#if NET6_0_OR_GREATER + if (source.TryGetNonEnumeratedCount(out int count) && count == 0) + { + yield break; + } +#endif + + var options = RegexOptions.Compiled | (ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None); + var regex = new Regex(pattern, options, Regex.InfiniteMatchTimeout); + + foreach (string item in source) + { + if (regex.IsMatch(item)) + { + yield return item; + } + } + } +} diff --git a/X10D/src/Text/RuneExtensions.cs b/X10D/src/Text/RuneExtensions.cs index 0249fa3..629d0e1 100644 --- a/X10D/src/Text/RuneExtensions.cs +++ b/X10D/src/Text/RuneExtensions.cs @@ -1,6 +1,9 @@ #if NETCOREAPP3_0_OR_GREATER +using System.Diagnostics; using System.Diagnostics.Contracts; +using System.Globalization; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace X10D.Text; @@ -44,17 +47,69 @@ public static class RuneExtensions return value.ToString(); } - int utf8SequenceLength = value.Utf8SequenceLength; - Span utf8 = stackalloc byte[utf8SequenceLength]; - value.EncodeToUtf8(utf8); + int length = value.Utf8SequenceLength; - Span buffer = stackalloc byte[utf8.Length * count]; - for (var index = 0; index < count; index++) + // Helpful documentation: https://en.wikipedia.org/wiki/UTF-8 + // This probably gonna break interning but whatever. + switch (length) { - utf8.CopyTo(buffer.Slice(index * utf8.Length, utf8.Length)); - } + case 1: + { + // Codepoint 0 to 0x00FF can be directly turn into char value without any conversion. + return new string((char)value.Value, count); + } - return Encoding.UTF8.GetString(buffer); + // Codepoint 0x0080 to 0x07FF takes 2 UTF-8 bytes, and it can be represented by 1 UTF-16 character (.NET runtime use + // UTF-16 encoding). + // Source: https://stackoverflow.com/questions/63905684 + case 2: + // Codepoint 0x0800 to 0xFFFF takes 3 UTF-8 bytes, and can also be represented by 1 UTF-16 character. + case 3: + { + // Codepoint 0x0080 to 0x07FF convert into 1 .NET character string, directly use string constructor. + unsafe + { + Span bytes = stackalloc byte[length]; + value.EncodeToUtf8(bytes); + + char character; + Encoding.UTF8.GetChars(bytes, new Span(&character, 1)); + + return new string(character, count); + } + } + + // Codepoint 0x10000 and beyond will takes **only** 2 UTF-16 character. + case 4: + { + return string.Create(count * 2, value, (span, _) => + { + unsafe + { + Span bytes = stackalloc byte[4]; + value.EncodeToUtf8(bytes); + + int characters; // 2 characters, fit inside 1 32-bit integer. + Encoding.UTF8.GetChars(bytes, new Span(&characters, 2)); + + MemoryMarshal.Cast(span).Fill(characters); + } + }); + } + + // dotcover disable + //NOSONAR + default: + string exceptionFormat = ExceptionMessages.UnexpectedRuneUtf8SequenceLength; + string message = string.Format(CultureInfo.CurrentCulture, exceptionFormat, length); +#if NET7_0_OR_GREATER + throw new UnreachableException(message); +#else + throw new InvalidOperationException(message); +#endif + //NOSONAR + // dotcover enable + } } } #endif diff --git a/X10D/src/Text/StringBuilderReader.cs b/X10D/src/Text/StringBuilderReader.cs index fe6e969..337a22c 100644 --- a/X10D/src/Text/StringBuilderReader.cs +++ b/X10D/src/Text/StringBuilderReader.cs @@ -55,10 +55,14 @@ public class StringBuilderReader : TextReader /// public override int Read(char[] buffer, int index, int count) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(buffer); +#else if (buffer is null) { throw new ArgumentNullException(nameof(buffer)); } +#endif if (index < 0) { diff --git a/X10D/src/Text/StringExtensions.cs b/X10D/src/Text/StringExtensions.cs index b7ffa57..b697c6e 100644 --- a/X10D/src/Text/StringExtensions.cs +++ b/X10D/src/Text/StringExtensions.cs @@ -136,23 +136,19 @@ public static class StringExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(sourceEncoding); + ArgumentNullException.ThrowIfNull(destinationEncoding); #else if (value is null) { throw new ArgumentNullException(nameof(value)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(sourceEncoding); -#else + if (sourceEncoding is null) { throw new ArgumentNullException(nameof(sourceEncoding)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(destinationEncoding); -#else + if (destinationEncoding is null) { throw new ArgumentNullException(nameof(destinationEncoding)); @@ -162,6 +158,207 @@ public static class StringExtensions return value.GetBytes(sourceEncoding).ToString(destinationEncoding); } + /// + /// Counts the occurrences of a character within the current character span. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this Span haystack, char needle) + { + return CountSubstring((ReadOnlySpan)haystack, needle); + } + + /// + /// Counts the occurrences of a character within the current character span. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this ReadOnlySpan haystack, char needle) + { + var count = 0; + + for (var index = 0; index < haystack.Length; index++) + { + if (haystack[index] == needle) + { + count++; + } + } + + return count; + } + + /// + /// Counts the occurrences of a character within the current string. + /// + /// The haystack search space. + /// The character to count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, char needle) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(haystack); +#else + if (haystack is null) + { + throw new ArgumentNullException(nameof(haystack)); + } +#endif + + return haystack.AsSpan().CountSubstring(needle); + } + + /// + /// Counts the occurrences of a substring within the current string. + /// + /// The haystack search space. + /// The substring to count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, string? needle) + { + return CountSubstring(haystack, needle, StringComparison.Ordinal); + } + + /// + /// Counts the occurrences of a substring within the current string, using a specified string comparison method. + /// + /// The haystack search space. + /// The substring to count. + /// The string comparison method used for determining substring count. + /// An integer representing the count of inside . + public static int CountSubstring(this string haystack, string? needle, StringComparison comparison) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(haystack); +#else + if (haystack is null) + { + throw new ArgumentNullException(nameof(haystack)); + } +#endif + + if (string.IsNullOrWhiteSpace(needle)) + { + return 0; + } + + return haystack.AsSpan().CountSubstring(needle, comparison); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// The combined string. + public static string EnsureEndsWith(this string? value, char substring) + { + return EnsureEndsWith(value, substring, StringComparison.Ordinal); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// One of the enumeration values that determines how the substring is compared. + /// The combined string. + public static string EnsureEndsWith(this string? value, char substring, StringComparison comparisonType) + { + return EnsureEndsWith(value, substring.ToString(), comparisonType); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// The combined string. + public static string EnsureEndsWith(this string? value, string substring) + { + return EnsureEndsWith(value, substring, StringComparison.Ordinal); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// One of the enumeration values that determines how the substring is compared. + /// The combined string. + public static string EnsureEndsWith(this string? value, string substring, StringComparison comparisonType) + { + if (string.IsNullOrEmpty(value)) + { + return substring; + } + + if (value.EndsWith(substring, comparisonType)) + { + return value; + } + + return value + substring; + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// The combined string. + public static string EnsureStartsWith(this string? value, char substring) + { + return EnsureStartsWith(value, substring, StringComparison.Ordinal); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// One of the enumeration values that determines how the substring is compared. + /// The combined string. + public static string EnsureStartsWith(this string? value, char substring, StringComparison comparisonType) + { + return EnsureStartsWith(value, substring.ToString(), comparisonType); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// The combined string. + public static string EnsureStartsWith(this string? value, string substring) + { + return EnsureStartsWith(value, substring, StringComparison.Ordinal); + } + + /// + /// Ensures that the current string starts with a specified substring. + /// + /// The string to check. + /// The substring to prepend, if the current string does not already start with it. + /// One of the enumeration values that determines how the substring is compared. + /// The combined string. + public static string EnsureStartsWith(this string? value, string substring, StringComparison comparisonType) + { + if (string.IsNullOrEmpty(value)) + { + return substring; + } + + if (value.StartsWith(substring, comparisonType)) + { + return value; + } + + return substring + value; + } + /// /// Parses a into an . /// @@ -217,7 +414,7 @@ public static class StringExtensions if (string.IsNullOrWhiteSpace(value)) { - throw new ArgumentException(Resource.EnumParseEmptyStringException, nameof(value)); + throw new ArgumentException(ExceptionMessages.EnumParseEmptyStringException, nameof(value)); } return Enum.Parse(value, ignoreCase); @@ -277,15 +474,13 @@ public static class StringExtensions { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(encoding); #else if (value is null) { throw new ArgumentNullException(nameof(value)); } -#endif -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(encoding); -#else + if (encoding is null) { throw new ArgumentNullException(nameof(encoding)); @@ -320,6 +515,34 @@ public static class StringExtensions return EmojiRegex.Value.IsMatch(value); } + /// + /// Returns a value indicating whether the current string represents an empty string. + /// + /// The value to check. + /// + /// if is empty; otherwise, . + /// + /// is . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsEmpty(this string value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value); +#else + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } +#endif + + return value.Length == 0; + } + /// /// Determines if all alpha characters in this string are considered lowercase. /// @@ -327,6 +550,7 @@ public static class StringExtensions /// /// if all alpha characters in this string are lowercase; otherwise, . /// + /// is . [Pure] #if NETSTANDARD2_1 [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -376,6 +600,46 @@ public static class StringExtensions return true; } + /// + /// Returns a value indicating whether the current string is ( in Visual + /// Basic), or represents an empty string. + /// + /// The value to check. + /// + /// if is or empty; otherwise, + /// . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) + { + return string.IsNullOrEmpty(value); + } + + /// + /// Returns a value indicating whether the current string is ( in Visual + /// Basic), represents an empty string, or consists of only whitespace characters. + /// + /// The value to check. + /// + /// if is , empty, or consists of only + /// whitespace; otherwise, . + /// + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) + { + return string.IsNullOrWhiteSpace(value); + } + /// /// Determines whether the current string is considered palindromic; that is, the letters within the string are the /// same when reversed. @@ -513,6 +777,49 @@ public static class StringExtensions return true; } + /// + /// Returns a value indicating whether the current string represents an empty string, or consists of only whitespace + /// characters. + /// + /// The value to check. + /// + /// if is empty or consists of only whitespace; otherwise, + /// . + /// + /// is . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static bool IsWhiteSpace(this string value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value); +#else + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } +#endif + + if (value.Length == 0) + { + return true; + } + + for (var index = 0; index < value.Length; index++) + { + if (!char.IsWhiteSpace(value[index])) + { + return false; + } + } + + return true; + } + /// /// Repeats a string a specified number of times. /// @@ -721,6 +1028,84 @@ public static class StringExtensions } } + /// + /// Determines whether the beginning of this string instance matches any of the specified strings using the current + /// culture for comparison. + /// + /// The value to compare. + /// An array of string to compare. + /// + /// if starts with any of the ; + /// otherwise, . + /// + /// + /// , or at least one of its elements, is . + /// + public static bool StartsWithAny(this string? value, params string[] startValues) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(startValues); +#else + if (startValues is null) + { + throw new ArgumentNullException(nameof(startValues)); + } +#endif + + if (startValues.Length == 0 || string.IsNullOrWhiteSpace(value)) + { + return false; + } + + return value.StartsWithAny(StringComparison.CurrentCulture, startValues); + } + + /// + /// Determines whether the beginning of this string instance matches any of the specified strings when compared using the + /// specified comparison option. + /// + /// The value to compare. + /// One of the enumeration values that determines how this string and value are compared. + /// An array of string to compare. + /// + /// if starts with any of the ; + /// otherwise, . + /// + /// + /// , or at least one of its elements, is . + /// + public static bool StartsWithAny(this string? value, StringComparison comparison, params string[] startValues) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(startValues); +#else + if (startValues is null) + { + throw new ArgumentNullException(nameof(startValues)); + } +#endif + + if (startValues.Length == 0 || string.IsNullOrWhiteSpace(value)) + { + return false; + } + + for (var index = 0; index < startValues.Length; index++) + { + if (startValues[index] is null) + { + throw new ArgumentNullException(nameof(startValues)); + } + + if (value.StartsWith(startValues[index], comparison)) + { + return true; + } + } + + return false; + } + /// /// Normalizes a string which may be either or empty to a specified alternative. /// diff --git a/X10D/src/Time/CharSpanExtensions.cs b/X10D/src/Time/CharSpanExtensions.cs new file mode 100644 index 0000000..a238503 --- /dev/null +++ b/X10D/src/Time/CharSpanExtensions.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace X10D.Time; + +/// +/// Time-related extension methods for of . +/// +public static class CharSpanExtensions +{ + /// + /// Parses this span of characters as a shorthand time span (e.g. 3w 2d 1h) and converts it to an instance of + /// . + /// + /// + /// The input span of characters. Floating point is not supported, but integers with the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// A new instance of . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static TimeSpan ToTimeSpan(this ReadOnlySpan input) + { + return TimeSpanParser.TryParse(input, out TimeSpan result) ? result : default; + } +} diff --git a/X10D/src/Time/DateTimeExtensions.cs b/X10D/src/Time/DateTimeExtensions.cs index f06a4f0..2993b03 100644 --- a/X10D/src/Time/DateTimeExtensions.cs +++ b/X10D/src/Time/DateTimeExtensions.cs @@ -1,4 +1,6 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; using System.Runtime.CompilerServices; namespace X10D.Time; @@ -15,6 +17,7 @@ public static class DateTimeExtensions #else [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] #endif + [ExcludeFromCodeCoverage] public static int Age(this DateTime value) { return value.Age(DateTime.Today); @@ -58,6 +61,32 @@ public static class DateTimeExtensions return ((DateTimeOffset)value).FirstDayOfMonth().DateTime; } + /// + /// Gets the ISO-8601 week number of the year for the current date. + /// + /// The date whose week number to return. + /// The ISO-8601 week number of the year. + /// Shawn Steele, Microsoft + /// + /// This implementation is directly inspired from a + /// + /// blog post + /// . + /// about this subject. + /// + [Pure] + public static int GetIso8601WeekOfYear(this DateTime value) + { + var calendar = CultureInfo.InvariantCulture.Calendar; + DayOfWeek day = calendar.GetDayOfWeek(value); + if (day is >= DayOfWeek.Monday and <= DayOfWeek.Wednesday) + { + value = value.AddDays(3); + } + + return calendar.GetWeekOfYear(value, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + } + /// /// Returns a value indicating whether the year represented by the current is a leap year. /// diff --git a/X10D/src/Time/DateTimeOffsetExtensions.cs b/X10D/src/Time/DateTimeOffsetExtensions.cs index e30560b..977cc60 100644 --- a/X10D/src/Time/DateTimeOffsetExtensions.cs +++ b/X10D/src/Time/DateTimeOffsetExtensions.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; namespace X10D.Time; @@ -19,6 +20,7 @@ public static class DateTimeOffsetExtensions #else [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] #endif + [ExcludeFromCodeCoverage] public static int Age(this DateTimeOffset value) { return value.Age(DateTime.Today); @@ -84,6 +86,25 @@ public static class DateTimeOffsetExtensions return value.AddDays(1 - value.Day); } + /// + /// Gets the ISO-8601 week number of the year for the current date. + /// + /// The date whose week number to return. + /// The ISO-8601 week number of the year. + /// Shawn Steele, Microsoft + /// + /// This implementation is directly inspired from a + /// + /// blog post + /// . + /// about this subject. + /// + [Pure] + public static int GetIso8601WeekOfYear(this DateTimeOffset value) + { + return value.DateTime.GetIso8601WeekOfYear(); + } + /// /// Returns a value indicating whether the year represented by the current is a leap year. /// diff --git a/X10D/src/Time/StringExtensions.cs b/X10D/src/Time/StringExtensions.cs index ea3eb77..dbb154b 100644 --- a/X10D/src/Time/StringExtensions.cs +++ b/X10D/src/Time/StringExtensions.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; namespace X10D.Time; /// -/// Extension methods for . +/// Time-related extension methods for . /// public static class StringExtensions { @@ -12,7 +12,7 @@ public static class StringExtensions /// Parses a shorthand time span string (e.g. 3w 2d 1h) and converts it to an instance of . /// /// - /// The input string. Floating point is not supported, but range the following units are supported: + /// The input string. Floating point is not supported, but integers with the following units are supported: /// /// /// @@ -64,10 +64,14 @@ public static class StringExtensions #endif public static TimeSpan ToTimeSpan(this string input) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(input); +#else if (input is null) { throw new ArgumentNullException(nameof(input)); } +#endif return TimeSpanParser.TryParse(input, out TimeSpan result) ? result diff --git a/X10D/src/Time/TimeSpanParser.cs b/X10D/src/Time/TimeSpanParser.cs index 2839f22..b320566 100644 --- a/X10D/src/Time/TimeSpanParser.cs +++ b/X10D/src/Time/TimeSpanParser.cs @@ -1,10 +1,84 @@ -namespace X10D.Time; +using System.Diagnostics.CodeAnalysis; + +namespace X10D.Time; /// /// Represents a class which contains a parser which converts into . /// public static class TimeSpanParser { + /// + /// Attempts to parses a shorthand time span (e.g. 3w 2d 1h) as a span of characters, converting it to an instance of + /// which represents that duration of time. + /// + /// + /// The input span of characters. Floating point is not supported, but range the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// When this method returns, contains the parsed result. + /// if the parse was successful, otherwise. + public static bool TryParse(ReadOnlySpan value, out TimeSpan result) + { + result = TimeSpan.Zero; + + if (value.Length == 0 || value.IsWhiteSpace()) + { + return false; + } + + var unitValue = 0; + + for (var index = 0; index < value.Length; index++) + { + char current = value[index]; + if (!HandleCharacter(value, ref result, current, ref unitValue, ref index)) + { + return false; + } + } + + return true; + } + /// /// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of /// which represents that duration of time. @@ -54,80 +128,98 @@ public static class TimeSpanParser /// /// When this method returns, contains the parsed result. /// if the parse was successful, otherwise. - /// is . - public static bool TryParse(string value, out TimeSpan result) + public static bool TryParse([NotNullWhen(true)] string? value, out TimeSpan result) { - if (value is null) + result = TimeSpan.Zero; + return !string.IsNullOrWhiteSpace(value) && TryParse(value.AsSpan(), out result); + } + + private static bool HandleCharacter( + ReadOnlySpan value, + ref TimeSpan result, + char current, + ref int unitValue, + ref int index + ) + { + char next = index < value.Length - 1 ? value[index + 1] : '\0'; + if (HandleSpecial(ref unitValue, current)) { - throw new ArgumentNullException(nameof(value)); + return true; + } + + if (HandleSuffix(ref index, ref result, ref unitValue, current, next)) + { + return true; } result = TimeSpan.Zero; - var unitValue = 0; + return false; + } - for (var index = 0; index < value.Length; index++) + private static bool HandleSuffix(ref int index, ref TimeSpan result, ref int unitValue, char current, char next) + { + switch (current) { - char current = value[index]; - switch (current) - { - case var digitChar when char.IsDigit(digitChar): - var digit = (int)char.GetNumericValue(digitChar); - unitValue = unitValue * 10 + digit; - break; + case 'm' when next == 'o': + index++; + result += TimeSpan.FromDays(unitValue * 30); + unitValue = 0; + return true; - case 'y': - result += TimeSpan.FromDays(unitValue * 365); - unitValue = 0; - break; + case 'm' when next == 's': + index++; + result += TimeSpan.FromMilliseconds(unitValue); + unitValue = 0; + return true; - case 'm': - if (index < value.Length - 1 && value[index + 1] == 'o') - { - index++; - result += TimeSpan.FromDays(unitValue * 30); - } - else if (index < value.Length - 1 && value[index + 1] == 's') - { - index++; - result += TimeSpan.FromMilliseconds(unitValue); - } - else - { - result += TimeSpan.FromMinutes(unitValue); - } + case 'm': + result += TimeSpan.FromMinutes(unitValue); + unitValue = 0; + return true; - unitValue = 0; - break; + case 'y': + result += TimeSpan.FromDays(unitValue * 365); + unitValue = 0; + return true; - case 'w': - result += TimeSpan.FromDays(unitValue * 7); - unitValue = 0; - break; + case 'w': + result += TimeSpan.FromDays(unitValue * 7); + unitValue = 0; + return true; - case 'd': - result += TimeSpan.FromDays(unitValue); - unitValue = 0; - break; + case 'd': + result += TimeSpan.FromDays(unitValue); + unitValue = 0; + return true; - case 'h': - result += TimeSpan.FromHours(unitValue); - unitValue = 0; - break; + case 'h': + result += TimeSpan.FromHours(unitValue); + unitValue = 0; + return true; - case 's': - result += TimeSpan.FromSeconds(unitValue); - unitValue = 0; - break; - - case var space when char.IsWhiteSpace(space): - break; - - default: - result = TimeSpan.Zero; - return false; - } + case 's': + result += TimeSpan.FromSeconds(unitValue); + unitValue = 0; + return true; } - return true; + return false; + } + + private static bool HandleSpecial(ref int unitValue, char current) + { + switch (current) + { + case var _ when char.IsDigit(current): + var digit = (int)char.GetNumericValue(current); + unitValue = unitValue * 10 + digit; + return true; + + case var _ when char.IsWhiteSpace(current): + return true; + } + + return false; } } diff --git a/banner.png b/banner.png deleted file mode 100644 index d9aa957..0000000 Binary files a/banner.png and /dev/null differ diff --git a/branding_Banner.png b/branding_Banner.png new file mode 100644 index 0000000..5a4101a Binary files /dev/null and b/branding_Banner.png differ diff --git a/branding_Icon.png b/branding_Icon.png new file mode 100644 index 0000000..7226e0a Binary files /dev/null and b/branding_Icon.png differ diff --git a/branding_Social.png b/branding_Social.png new file mode 100644 index 0000000..76c7cc7 Binary files /dev/null and b/branding_Social.png differ diff --git a/icon.png b/icon.png deleted file mode 100644 index 77ff5ba..0000000 Binary files a/icon.png and /dev/null differ