Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x

- name: Restore
run: dotnet restore

- name: Build (Release, both target frameworks)
run: dotnet build --configuration Release --no-restore

- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
48 changes: 48 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Publish NuGet

on:
push:
tags: [ 'v*.*.*' ]
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g. 10.0.1)'
required: true

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x

- name: Determine version
id: ver
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
V="${{ github.event.inputs.version }}"
else
V="${GITHUB_REF_NAME#v}"
fi
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "Publishing version $V"

- name: Restore
run: dotnet restore

- name: Build (Release)
run: dotnet build --configuration Release --no-restore

- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal

- name: Pack
run: dotnet pack Extensions.Standard/Extensions.Standard.csproj --configuration Release --output ./artifacts -p:Version=${{ steps.ver.outputs.version }}

- name: Push to NuGet
run: dotnet nuget push "artifacts/*.nupkg" --api-key "${{ secrets.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
19 changes: 8 additions & 11 deletions Extensions.Standard.Test/Extensions.Standard.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand All @@ -18,8 +19,4 @@
<ProjectReference Include="..\Extensions.Standard\Extensions.Standard.csproj" />
</ItemGroup>

<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>

</Project>
125 changes: 117 additions & 8 deletions Extensions.Standard.Test/ExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public class ExtensionsTest
[InlineData(123)]
[InlineData(99999)]
[InlineData(0)]
[InlineData(-0)]
[InlineData(-1)]
[InlineData(-2)]
[InlineData(-123)]
Expand Down Expand Up @@ -270,6 +269,9 @@ public void ScaleTestBorderCase2()
Assert.Equal(Math.Round(max, Precision), Math.Round(received, Precision));
}

// The following tests exercise the deprecated InClosedRange/InOpenRange shims
// to verify their (misleading but unchanged) behavior is preserved for consumers.
#pragma warning disable CS0618
[Fact]
public void InOpenRangeTest()
{
Expand Down Expand Up @@ -347,6 +349,7 @@ public void InClosedRangeTest2()
Assert.False(123123.InClosedRange(rangeMin, rangeMax));
Assert.False((-123).InClosedRange(rangeMin, rangeMax));
}
#pragma warning restore CS0618

[Fact]
public void MaxIndexFindsIndexOfBiggestElement()
Expand Down Expand Up @@ -614,12 +617,12 @@ public void RadiansToDegrees()
public void LineConstruction()
{
var start = new double[] { 0, 0 };
var testedLine = Utilities.ConstructLine(start, 10, 0);
var testedLine = Utilities.ConstructLineFromRadians(start, 10, 0);

Assert.True(testedLine[0] == 0 && testedLine[1] == 0 && testedLine[2] == 10 && testedLine[3] == 0,
$"wrong points: ({testedLine[0]},{testedLine[1]} {testedLine[2]},{testedLine[3]}), should be (0,0 10,0)");

testedLine = Utilities.ConstructLine(start, 10, 45);
testedLine = Utilities.ConstructLineFromRadians(start, 10, 45);

Assert.True(testedLine[0] == 0 && testedLine[1] == 0 && testedLine[2] > 5.253 && testedLine[2] < 5.254 && testedLine[3] > 8.509 && testedLine[3] < 8.51,
$"wrong points: ({testedLine[0]},{testedLine[1]} {testedLine[2]},{testedLine[3]}), should be (0,0 5.253,8.509)");
Expand Down Expand Up @@ -844,8 +847,8 @@ public void PartitionReturnsCorrectPartitions1()
var result = blahblah.Partition(ratio);
Assert.Single(result[0]);
Assert.Single(result[1]);
Assert.Equal(result[0].First(), item1);
Assert.Equal(result[1].First(), item2);
Assert.Equal(item1, result[0].First());
Assert.Equal(item2, result[1].First());
}

[Fact]
Expand All @@ -860,9 +863,9 @@ public void PartitionReturnsCorrectPartitions2()
var result = blahblah.Partition(ratio);
Assert.Equal(2, result[0].Count);
Assert.Single(result[1]);
Assert.Equal(result[0][0], item1);
Assert.Equal(result[0][1], item2);
Assert.Equal(result[1][0], item3);
Assert.Equal(item1, result[0][0]);
Assert.Equal(item2, result[0][1]);
Assert.Equal(item3, result[1][0]);
}

[Fact]
Expand Down Expand Up @@ -965,7 +968,9 @@ public void TestPartitionT()

private class PoCo
{
#pragma warning disable CS0414 // present only to assert private fields are excluded from the result
private string test = "DoNotShowThis";
#pragma warning restore CS0414
private string test2 { get; set; } = "DoNotShowThis";
public int TestInt { get; } = 10;
public string TestString { get; set; } = "20";
Expand Down Expand Up @@ -1090,6 +1095,110 @@ public void DeepCopy_NullInput_ReturnsNull()
// Assert
Assert.Null(copy);
}

[Fact]
public void IsEquivalentDetectsMismatchNotOnlyInLastElement()
{
// Regression: a non-final differing element used to be ignored.
Assert.False(new[] { "3", "1", "2" }.IsEquivalent(new[] { "4", "1", "2" }));
}

[Theory]
[InlineData(new[] { "a", "a", "b" }, new[] { "b", "a", "a" }, true)]
[InlineData(new[] { "a", "a", "b" }, new[] { "a", "b", "b" }, false)]
[InlineData(new[] { "1", "2", "3" }, new[] { "1", "2", "3", "3" }, false)]
public void IsEquivalentRespectsMultiplicity(IEnumerable<string> lhs, IEnumerable<string> rhs, bool expected)
{
Assert.Equal(expected, lhs.IsEquivalent(rhs));
}

[Fact]
public void AsMemoryFormatsHighBinaryOrders()
{
Assert.Equal("1 " + Constants.TebibyteSuffix, Constants.TiB.AsMemory());
Assert.Equal("1 " + Constants.PebibyteSuffix, Constants.PiB.AsMemory());
Assert.Equal("1 " + Constants.ExbibyteSuffix, Constants.EiB.AsMemory());
Assert.Equal("1 " + Constants.ZebibyteSuffix, Constants.ZiB.AsMemory());
Assert.Equal("1 " + Constants.YobibyteSuffix, Constants.YiB.AsMemory());
}

[Fact]
public void AsMemoryDecimalFormatsHighDecimalOrders()
{
Assert.Equal("1 " + Constants.TerabyteSuffix, Constants.Tera.AsMemoryDecimal());
Assert.Equal("1 " + Constants.PetabyteSuffix, Constants.Peta.AsMemoryDecimal());
Assert.Equal("1 " + Constants.ExabyteSuffix, Constants.Exa.AsMemoryDecimal());
Assert.Equal("1 " + Constants.ZettabyteSuffix, Constants.Zetta.AsMemoryDecimal());
Assert.Equal("1 " + Constants.YottabyteSuffix, Constants.Yotta.AsMemoryDecimal());
}

[Fact]
public void ScaleGenericNonDoubleSequenceScalesWithoutThrowing()
{
// Regression: Cast<double>() on boxed ints threw InvalidCastException.
var received = new[] { 0, 5, 10 }.Scale((0.0, 1.0)).ToList();
Assert.Equal(0.0, received[0], Precision);
Assert.Equal(0.5, received[1], Precision);
Assert.Equal(1.0, received[2], Precision);
}

[Theory]
[InlineData(10, 20, 30, 255)]
[InlineData(0, 0, 0, 0)]
[InlineData(255, 255, 255, 255)]
[InlineData(1, 2, 3, 4)]
public void AsColorRoundTripsThroughArgb(byte red, byte green, byte blue, byte alpha)
{
var unpacked = Utilities.AsColor(red, green, blue, alpha).AsColor();
Assert.Equal(alpha, unpacked[0]);
Assert.Equal(red, unpacked[1]);
Assert.Equal(green, unpacked[2]);
Assert.Equal(blue, unpacked[3]);
}

[Fact]
public void InRangeInclusiveIncludesEndpoints()
{
Assert.True(0.InRangeInclusive(0, 10));
Assert.True(10.InRangeInclusive(0, 10));
Assert.True(5.InRangeInclusive(0, 10));
Assert.False((-1).InRangeInclusive(0, 10));
Assert.False(11.InRangeInclusive(0, 10));
}

[Fact]
public void InRangeExclusiveExcludesEndpoints()
{
Assert.False(0.InRangeExclusive(0, 10));
Assert.False(10.InRangeExclusive(0, 10));
Assert.True(5.InRangeExclusive(0, 10));
Assert.False((-1).InRangeExclusive(0, 10));
Assert.False(11.InRangeExclusive(0, 10));
}

[Fact]
public void ConstructLineFromDegreesConvertsAngle()
{
var start = new double[] { 0, 0 };
var line = Utilities.ConstructLineFromDegrees(start, 10, 45);

Assert.Equal(0, line[0]);
Assert.Equal(0, line[1]);
Assert.True(line[2] > 7.07 && line[2] < 7.072, $"x2 was {line[2]}, expected ~7.071");
Assert.True(line[3] > 7.07 && line[3] < 7.072, $"y2 was {line[3]}, expected ~7.071");
}

[Fact]
public void ObsoleteConstructLineStillBehavesLikeRadiansVariant()
{
var start = new double[] { 0, 0 };
#pragma warning disable CS0618
var legacy = Utilities.ConstructLine(start, 10, 1.2);
#pragma warning restore CS0618
var current = Utilities.ConstructLineFromRadians(start, 10, 1.2);
Assert.Equal(current, legacy);
}

#region Unit test related

private class TestClass
Expand Down
26 changes: 15 additions & 11 deletions Extensions.Standard/Extensions.Standard.csproj
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Ship XML docs for documented members without requiring every public member to be documented. -->
<NoWarn>$(NoWarn);CS1591</NoWarn>
<Description>Lightweight extensions methods (and constants) for common programming tasks:

Suffixes (AsMemory, AsTime), Interpolate, Partition, Shuffle (O(n)), Softmax, InnerProduct, IsPrime, ManhattanDistance, EuclideanDistance, InClosedRange, MeanSquaredError, EqualsWithTolerance, HsVtoArgb, Scale, Fit and many more.</Description>
<Company>Piotr Falkowski</Company>
<Authors>Piotr Falkowski</Authors>
<Copyright>Piotr Falkowski © 2024</Copyright>
<PackageLicenseUrl></PackageLicenseUrl>
<Copyright>Piotr Falkowski © 2026</Copyright>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageProjectUrl>https://github.com/PFalkowski/Extensions.Standard</PackageProjectUrl>
<RepositoryUrl>https://github.com/PFalkowski/Extensions.Standard</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>extensions, extension-methods, netcore, netstandard, boilerplate</PackageTags>
<Version>9.1.0</Version>
<PackageReleaseNotes>Add DeepCopy</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Version>10.0.0</Version>
<PackageReleaseNotes>Multi-target netstandard2.0 and net8.0. Fixed IsEquivalent, AsMemory/AsMemoryDecimal high orders, generic Scale, and AsColor packing. Deprecated InClosedRange/InOpenRange (misleading names) in favor of InRangeInclusive/InRangeExclusive, and ConstructLine in favor of ConstructLineFromRadians/ConstructLineFromDegrees.</PackageReleaseNotes>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>TRACE;RELEASE;NETSTANDARD2_0</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="StandardInterfaces" Version="3.1.0" />
</ItemGroup>

</Project>
</Project>
Loading