diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ae5bcb --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..717a805 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 diff --git a/Extensions.Standard.Test/Extensions.Standard.Test.csproj b/Extensions.Standard.Test/Extensions.Standard.Test.csproj index f9d2890..b638f21 100644 --- a/Extensions.Standard.Test/Extensions.Standard.Test.csproj +++ b/Extensions.Standard.Test/Extensions.Standard.Test.csproj @@ -1,14 +1,15 @@ - + - - net6.0 + + net8.0 + false - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -18,8 +19,4 @@ - - - - diff --git a/Extensions.Standard.Test/ExtensionsTest.cs b/Extensions.Standard.Test/ExtensionsTest.cs index 6d8ecfb..c496a76 100644 --- a/Extensions.Standard.Test/ExtensionsTest.cs +++ b/Extensions.Standard.Test/ExtensionsTest.cs @@ -17,7 +17,6 @@ public class ExtensionsTest [InlineData(123)] [InlineData(99999)] [InlineData(0)] - [InlineData(-0)] [InlineData(-1)] [InlineData(-2)] [InlineData(-123)] @@ -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() { @@ -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() @@ -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)"); @@ -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] @@ -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] @@ -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"; @@ -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 lhs, IEnumerable 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() 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 diff --git a/Extensions.Standard/Extensions.Standard.csproj b/Extensions.Standard/Extensions.Standard.csproj index d4530b0..f0ae76d 100644 --- a/Extensions.Standard/Extensions.Standard.csproj +++ b/Extensions.Standard/Extensions.Standard.csproj @@ -1,31 +1,35 @@ - + - net6.0 + netstandard2.0;net8.0 + latest + true + + $(NoWarn);CS1591 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. Piotr Falkowski Piotr Falkowski - Piotr Falkowski © 2024 - + Piotr Falkowski © 2026 True https://github.com/PFalkowski/Extensions.Standard https://github.com/PFalkowski/Extensions.Standard git extensions, extension-methods, netcore, netstandard, boilerplate - 9.1.0 - Add DeepCopy + README.md + 10.0.0 + 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. MIT - - TRACE;RELEASE;NETSTANDARD2_0 - + + + - + - \ No newline at end of file + diff --git a/Extensions.Standard/Utilities.cs b/Extensions.Standard/Utilities.cs index fc921fb..1c57ca4 100644 --- a/Extensions.Standard/Utilities.cs +++ b/Extensions.Standard/Utilities.cs @@ -48,17 +48,34 @@ public static double ToRadians(this double degrees) return degrees / (180.0 / Math.PI); } - public static double[] ConstructLine(double[] startingPointXy, double length, double angleDegrees) + /// + /// Constructs a line segment [x1, y1, x2, y2] from a starting point, length and angle in radians. + /// + public static double[] ConstructLineFromRadians(double[] startingPointXy, double length, double angleRadians) { return new[] { startingPointXy[0], startingPointXy[1], - startingPointXy[0] + length * Math.Cos(angleDegrees), - startingPointXy[1] + length * Math.Sin(angleDegrees) + startingPointXy[0] + length * Math.Cos(angleRadians), + startingPointXy[1] + length * Math.Sin(angleRadians) }; } + /// + /// Constructs a line segment [x1, y1, x2, y2] from a starting point, length and angle in degrees. + /// + public static double[] ConstructLineFromDegrees(double[] startingPointXy, double length, double angleDegrees) + { + return ConstructLineFromRadians(startingPointXy, length, angleDegrees.ToRadians()); + } + + [Obsolete("The angle is interpreted as radians despite the parameter name. Use ConstructLineFromRadians (identical behavior) or ConstructLineFromDegrees. Will be removed in a future major version.")] + public static double[] ConstructLine(double[] startingPointXy, double length, double angleDegrees) + { + return ConstructLineFromRadians(startingPointXy, length, angleDegrees); + } + public static double Interpolate(double[] p1, double[] p2, double x) { return p1[1] + ((x - p1[0]) * p2[1] - (x - p1[0]) * p1[1]) / (p2[0] - p1[0]); @@ -85,11 +102,29 @@ public static double Area(this IList polygon) #region Distance Measures + /// + /// Returns true when lies within the closed interval [from, to] (endpoints included). + /// + public static bool InRangeInclusive(this T value, T from, T to) where T : IComparable + { + return value.CompareTo(@from) >= 0 && value.CompareTo(to) <= 0; + } + + /// + /// Returns true when lies within the open interval (from, to) (endpoints excluded). + /// + public static bool InRangeExclusive(this T value, T from, T to) where T : IComparable + { + return value.CompareTo(@from) > 0 && value.CompareTo(to) < 0; + } + + [Obsolete("Misleading name: this excludes the endpoints (open interval). Use InRangeExclusive instead. Will be removed in a future major version.")] public static bool InClosedRange(this T value, T from, T to) where T : IComparable { return value.CompareTo(@from) > 0 && value.CompareTo(to) < 0; } + [Obsolete("Misleading name: this includes the endpoints (closed interval). Use InRangeInclusive instead. Will be removed in a future major version.")] public static bool InOpenRange(this T value, T from, T to) where T : IComparable { return value.CompareTo(@from) >= 0 && value.CompareTo(to) <= 0; @@ -210,6 +245,13 @@ public static TAccumulate InnerProduct(this IEnumerable Random.Shared; +#else + [ThreadStatic] private static Random _threadStaticRandom; + private static Random SharedRandom => _threadStaticRandom ??= new Random(); +#endif + /// /// Performs in-place Knuth Shuffle - reorder items randomly in-place, with Fisher-Yates algorithm. /// O(n) complexity. see: http://rosettacode.org/wiki/Knuth_shuffle#C.23 @@ -220,7 +262,7 @@ public static TAccumulate InnerProduct(this IEnumerable public static void Shuffle(this IList input, Random rand = null) { - rand ??= Random.Shared; + rand ??= SharedRandom; for (var i = 0; i < input.Count; ++i) { var j = rand.Next(i, input.Count); @@ -323,30 +365,36 @@ public static bool SequenceEquals(this IEnumerable seqenceA, IEnumerable /// public static bool IsEquivalent(this IEnumerable collectionLhs, IEnumerable collectionRhs) { - var result = true; - if (collectionLhs == null && collectionRhs == null) - { - return true; - } + if (collectionLhs == null && collectionRhs == null) return true; + if (collectionLhs == null || collectionRhs == null) return false; - if (collectionLhs == null || collectionRhs == null) + // Multiset (bag) equality: same elements with the same multiplicity, order independent. + var counts = new Dictionary(); + var nullCount = 0; + foreach (var item in collectionLhs) { - return false; + if (item == null) { ++nullCount; continue; } + counts.TryGetValue(item, out var current); + counts[item] = current + 1; } - var listA = collectionLhs as IList ?? collectionLhs.ToList(); - var listB = collectionRhs as HashSet ?? collectionRhs.ToHashSet(); - if (listA.Count != listB.Count) + foreach (var item in collectionRhs) { - return false; + if (item == null) + { + if (--nullCount < 0) return false; + continue; + } + if (!counts.TryGetValue(item, out var current) || current == 0) return false; + counts[item] = current - 1; } - foreach (var listAItem in listA) + if (nullCount != 0) return false; + foreach (var remaining in counts.Values) { - result = listB.Contains(listAItem); + if (remaining != 0) return false; } - - return result; + return true; } /// @@ -381,7 +429,7 @@ public static string Head(this IList input, int n) var stb = new StringBuilder(); stb.AppendLine(); - stb.AppendJoin(Environment.NewLine, input.Take(n)); + stb.Append(string.Join(Environment.NewLine, input.Take(n))); stb.AppendLine(); stb.AppendLine("..."); stb.AppendLine(); @@ -401,7 +449,7 @@ public static string Tail(this IList input, int n) var stb = new StringBuilder(); stb.AppendLine(); stb.AppendLine("..."); - stb.AppendJoin(Environment.NewLine, input.Skip(input.Count - n)); + stb.Append(string.Join(Environment.NewLine, input.Skip(input.Count - n))); stb.AppendLine(); return stb.ToString(); @@ -419,10 +467,10 @@ public static string HeadAndTail(this IList input, int n) var stb = new StringBuilder(); stb.AppendLine(); - stb.AppendJoin(Environment.NewLine, input.Take(n)); + stb.Append(string.Join(Environment.NewLine, input.Take(n))); stb.AppendLine(); stb.AppendLine("..."); - stb.AppendJoin(Environment.NewLine, input.Skip(input.Count - n)); + stb.Append(string.Join(Environment.NewLine, input.Skip(input.Count - n))); stb.AppendLine(); return stb.ToString(); @@ -449,7 +497,7 @@ public static string CenterText(this string text, int totalLengthOfLine, char fi /// public static int AsColor(byte red, byte green, byte blue, byte alpha = 255) { - return alpha + (red << 8) + (green << 16) + (blue << 24); + return (alpha << 24) | (red << 16) | (green << 8) | blue; } /// @@ -518,39 +566,7 @@ public static string AsMemory(this T bytes, byte decimals = 2, string numberS CultureInfo culture = null) where T : IConvertible { - var bytesConverted = Convert.ToDecimal(bytes); - var numberFormat = (NumberFormatInfo)(culture?.NumberFormat ?? CultureInfo.InvariantCulture.NumberFormat).Clone(); - if (numberSeparator != null) numberFormat.NumberGroupSeparator = numberSeparator; - - if (bytesConverted < Constants.KiB) - { - return $"{bytesConverted.ToString(numberFormat)} {"byte".PluralizeWhenNeeded(bytesConverted)}"; - } - if (bytesConverted < Constants.MiB) - { - return $"{Math.Round(bytesConverted / Constants.KiB, decimals).ToString(numberFormat)} {Constants.KibibyteSuffix}"; - } - if (bytesConverted < Constants.GiB) - { - return $"{Math.Round(bytesConverted / Constants.MiB, decimals).ToString(numberFormat)} {Constants.MebibyteSuffix}"; - } - if (bytesConverted < Constants.TiB) - { - return $"{Math.Round(bytesConverted / Constants.GiB, decimals).ToString(numberFormat)} {Constants.GibibyteSuffix}"; - } - if (bytesConverted < Constants.PiB) - { - return $"{Math.Round(bytesConverted / Constants.TiB, decimals).ToString(numberFormat)} {Constants.TebibyteSuffix}"; - } - if (bytesConverted < Constants.EiB) - { - return $"{Math.Round(bytesConverted / Constants.PiB, decimals).ToString(numberFormat)} {Constants.PebibyteSuffix}"; - } - if (bytesConverted < Constants.ZiB) - { - return $"{Math.Round(bytesConverted / Constants.YiB, decimals).ToString(numberFormat)} {Constants.ExbibyteSuffix}"; - } - return $"{Math.Round(bytesConverted / Constants.ZiB, decimals).ToString(numberFormat)} {Constants.YobibyteSuffix}"; + return FormatMagnitude(Convert.ToDecimal(bytes), Constants.BinaryOrders, decimals, numberSeparator, culture); } /// @@ -565,39 +581,33 @@ public static string AsMemoryDecimal(this T bytes, byte decimals = 2, string CultureInfo culture = null) where T : IConvertible { - var bytesConverted = Convert.ToDecimal(bytes); + return FormatMagnitude(Convert.ToDecimal(bytes), Constants.DecimalOrders, decimals, numberSeparator, culture); + } + + /// + /// Renders using the largest order of magnitude in + /// that does not exceed it. The first entry in is treated as the base unit + /// ("byte") and is pluralized rather than scaled. + /// + private static string FormatMagnitude(decimal value, Tuple[] orders, byte decimals, + string numberSeparator, CultureInfo culture) + { var numberFormat = (NumberFormatInfo)(culture?.NumberFormat ?? CultureInfo.InvariantCulture.NumberFormat).Clone(); if (numberSeparator != null) numberFormat.NumberGroupSeparator = numberSeparator; - if (bytesConverted < Constants.Kilo) - { - return $"{bytesConverted.ToString(numberFormat)} {"byte".PluralizeWhenNeeded(bytesConverted)}"; - } - if (bytesConverted < Constants.Mega) - { - return $"{Math.Round(bytesConverted / Constants.Kilo, decimals).ToString(numberFormat)} {Constants.KilobyteSuffix}"; - } - if (bytesConverted < Constants.Giga) - { - return $"{Math.Round(bytesConverted / Constants.Mega, decimals).ToString(numberFormat)} {Constants.MegabyteSuffix}"; - } - if (bytesConverted < Constants.Tera) - { - return $"{Math.Round(bytesConverted / Constants.Giga, decimals).ToString(numberFormat)} {Constants.GigabyteSuffix}"; - } - if (bytesConverted < Constants.Peta) - { - return $"{Math.Round(bytesConverted / Constants.Tera, decimals).ToString(numberFormat)} {Constants.TerabyteSuffix}"; - } - if (bytesConverted < Constants.Exa) + var orderIndex = 0; + for (var i = 1; i < orders.Length; ++i) { - return $"{Math.Round(bytesConverted / Constants.Peta, decimals).ToString(numberFormat)} {Constants.PetabyteSuffix}"; + if (value < orders[i].Item2) break; + orderIndex = i; } - if (bytesConverted < Constants.Zetta) + + var (suffix, multiplier) = (orders[orderIndex].Item1, orders[orderIndex].Item2); + if (orderIndex == 0) { - return $"{Math.Round(bytesConverted / Constants.Exa, decimals).ToString(numberFormat)} {Constants.ZettabyteSuffix}"; + return $"{value.ToString(numberFormat)} {suffix.PluralizeWhenNeeded(value)}"; } - return $"{Math.Round(bytesConverted / Constants.Zetta, decimals).ToString(numberFormat)} {Constants.YobibyteSuffix}"; + return $"{Math.Round(value / multiplier, decimals).ToString(numberFormat)} {suffix}"; } /// @@ -748,7 +758,7 @@ public static double Scale(this double value, (double Min, double Max) scale) { if (scale.Min > scale.Max) throw new ArgumentOutOfRangeException(nameof(scale.Min)); var tmp = value * (scale.Max - scale.Min); - if (double.IsInfinity(tmp) && value.InClosedRange(0.0, 1.0)) + if (double.IsInfinity(tmp) && value.InRangeExclusive(0.0, 1.0)) { return value.ScaleSafe(scale.Min, scale.Max); } @@ -777,7 +787,7 @@ public static (double Min, double Max) FindMinMaxInOn(this IEnumerable d public static IEnumerable Scale(this IEnumerable data, (double Min, double Max) scale) { var enumerated = data as double[] ?? data.ToArray(); - var (min, max) = data.FindMinMaxInOn(); + var (min, max) = enumerated.FindMinMaxInOn(); var m = (scale.Max - scale.Min) / (max - min); var c = scale.Min - min * m; var result = new double[enumerated.Length]; @@ -797,7 +807,7 @@ public static IEnumerable Scale(this IEnumerable data) public static IEnumerable Scale(this IEnumerable data, (double Min, double Max) scale) where T : IConvertible { var enumerated = data as T[] ?? data.ToArray(); - var (min, max) = enumerated.Cast().FindMinMaxInOn(); + var (min, max) = enumerated.Select(x => Convert.ToDouble(x)).FindMinMaxInOn(); var m = (scale.Max - scale.Min) / (max - min); var c = scale.Min - min * m; var result = new double[enumerated.Length]; diff --git a/README.md b/README.md index 4353e44..e1502fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Extensions.Standard +[![CI](https://github.com/PFalkowski/Extensions.Standard/actions/workflows/ci.yml/badge.svg)](https://github.com/PFalkowski/Extensions.Standard/actions/workflows/ci.yml) [![NuGet version (Extensions.Standard)](https://img.shields.io/nuget/v/Extensions.Standard.svg)](https://www.nuget.org/packages/Extensions.Standard/) [![Licence (Extensions.Serialization)](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) @@ -14,7 +15,7 @@ Lightweight extensions methods (and constants) for common programming tasks, lik - IsPrime - ManhattanDistance - EuclideanDistance -- InClosedRange +- InRangeInclusive / InRangeExclusive - MeanSquaredError - EqualsWithTolerance - HsVtoArgb