diff --git a/src/StrongTypes.Tests/Collections/ExceptNullsTests.cs b/src/StrongTypes.Tests/Collections/ExceptNullsTests.cs new file mode 100644 index 00000000..380da5ac --- /dev/null +++ b/src/StrongTypes.Tests/Collections/ExceptNullsTests.cs @@ -0,0 +1,55 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace StrongTypes.Tests; + +public class ExceptNullsTests +{ + [Fact] + public void ExceptNulls_NullableStruct() + { + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + var list = new List { null, guid1, null, guid2, null }; + + Guid[] result = list.ExceptNulls().ToArray(); + + Assert.Equal(new[] { guid1, guid2 }, result); + } + + [Fact] + public void ExceptNulls_NullableReference() + { + var list = new List { null, "1 potato", null, "2 potatoes", null }; + + string[] result = list.ExceptNulls().ToArray(); + + Assert.Equal(new[] { "1 potato", "2 potatoes" }, result); + } + + [Fact] + public void ExceptNulls_AllNull_ReturnsEmpty() + { + Assert.Empty(new int?[] { null, null, null }.ExceptNulls()); + Assert.Empty(new string?[] { null, null, null }.ExceptNulls()); + } + + [Fact] + public void ExceptNulls_NoNulls_ReturnsAll() + { + Assert.Equal(new[] { 1, 2, 3 }, new int?[] { 1, 2, 3 }.ExceptNulls()); + Assert.Equal(new[] { "a", "b" }, new string?[] { "a", "b" }.ExceptNulls()); + } + + [Fact] + public void ExceptNulls_ReferenceResult_IsStronglyTyped() + { + IEnumerable source = new[] { "x", null, "y" }; + IEnumerable result = source.ExceptNulls(); + Assert.Equal(new[] { "x", "y" }, result); + } +} diff --git a/src/StrongTypes.Tests/Collections_Old/ExceptNullsTests_Old.cs b/src/StrongTypes.Tests/Collections_Old/ExceptNullsTests_Old.cs deleted file mode 100644 index 4aed0286..00000000 --- a/src/StrongTypes.Tests/Collections_Old/ExceptNullsTests_Old.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace StrongTypes.Tests.Collections; - -public class ExceptNullsTests -{ - [Fact] - public void ExceptNulls() - { - var list = new List { null, "1 potato", null, "2 potatoes", null }; - var result = list.ExceptNulls().ToArray(); - - Assert.Equal(2, result.Length); - Assert.Equal(new [] { "1 potato", "2 potatoes" }, result); - } - - [Fact] - public void ExceptNulls_Nullable() - { - var guid1 = Guid.NewGuid(); - var guid2 = Guid.NewGuid(); - var list = new List { null, guid1, null, guid2, null }; - Guid[] result = list.ExceptNulls().ToArray(); - - Assert.Equal(2, result.Length); - Assert.Equal(new [] { guid1, guid2 }, result); - } -} \ No newline at end of file diff --git a/src/StrongTypes.Tests/Digits/DigitExtensionsTests.cs b/src/StrongTypes.Tests/Digits/DigitExtensionsTests.cs new file mode 100644 index 00000000..b58b3269 --- /dev/null +++ b/src/StrongTypes.Tests/Digits/DigitExtensionsTests.cs @@ -0,0 +1,44 @@ +#nullable enable + +using System.Linq; +using FsCheck.Xunit; +using Xunit; + +namespace StrongTypes.Tests; + +public class DigitExtensionsTests +{ + [Property] + public void AsDigit_DelegatesToTryCreate(char c) + { + Assert.Equal(Digit.TryCreate(c), c.AsDigit()); + } + + [Fact] + public void FilterDigits_ExtractsDigitsInOrder() + { + Assert.Equal( + new byte[] { 1, 2, 3, 8, 7, 6, 5, 9 }, + "ASD 1 some spaces 2 with numbers 38 7 in between .6 ?:`'!@(#*&$%&^!@)$_ them59" + .FilterDigits() + .Select(d => d.Value)); + } + + [Fact] + public void FilterDigits_ReturnsEmpty_ForNull() + { + Assert.Empty(((string?)null).FilterDigits()); + } + + [Fact] + public void FilterDigits_ReturnsEmpty_ForEmptyString() + { + Assert.Empty("".FilterDigits()); + } + + [Fact] + public void FilterDigits_ReturnsEmpty_ForNonDigitsOnly() + { + Assert.Empty("abc XYZ !@#".FilterDigits()); + } +} diff --git a/src/StrongTypes.Tests/Digits/DigitTests.cs b/src/StrongTypes.Tests/Digits/DigitTests.cs new file mode 100644 index 00000000..5421283e --- /dev/null +++ b/src/StrongTypes.Tests/Digits/DigitTests.cs @@ -0,0 +1,207 @@ +#nullable enable + +using System; +using FsCheck.Xunit; +using Xunit; + +namespace StrongTypes.Tests; + +public class DigitTests +{ + [Theory] + [InlineData('0', (byte)0)] + [InlineData('1', (byte)1)] + [InlineData('2', (byte)2)] + [InlineData('3', (byte)3)] + [InlineData('4', (byte)4)] + [InlineData('5', (byte)5)] + [InlineData('6', (byte)6)] + [InlineData('7', (byte)7)] + [InlineData('8', (byte)8)] + [InlineData('9', (byte)9)] + public void TryCreate_Succeeds_ForAsciiDigits(char input, byte expected) + { + var digit = Digit.TryCreate(input); + Assert.NotNull(digit); + Assert.Equal(expected, digit!.Value.Value); + } + + [Theory] + [InlineData('a')] + [InlineData('z')] + [InlineData('B')] + [InlineData(' ')] + [InlineData('.')] + [InlineData(char.MinValue)] + [InlineData(char.MaxValue)] + public void TryCreate_ReturnsNull_ForNonDigits(char input) + { + Assert.Null(Digit.TryCreate(input)); + } + + [Fact] + public void Create_Throws_ForNonDigit() + { + Assert.Throws(() => Digit.Create('a')); + } + + [Fact] + public void Create_Succeeds_ForDigit() + { + Assert.Equal((byte)7, Digit.Create('7').Value); + } + + [Fact] + public void Default_IsZero() + { + Assert.Equal((byte)0, default(Digit).Value); + } + + [Property] + public void TryCreate_AgreesWith_CharIsDigit(char c) + { + var result = Digit.TryCreate(c); + Assert.Equal(char.IsDigit(c), result.HasValue); + } + + [Property] + public void AllIntegerFirstDigits_Succeed(int number) + { + var firstDigit = Math.Abs(number).ToString()[0]; + Assert.NotNull(Digit.TryCreate(firstDigit)); + } + + [Fact] + public void Equality_Digit() + { + Assert.Equal(Digit.Create('5'), Digit.Create('5')); + Assert.NotEqual(Digit.Create('5'), Digit.Create('6')); + Assert.True(Digit.Create('5') == Digit.Create('5')); + Assert.True(Digit.Create('5') != Digit.Create('6')); + } + + [Fact] + public void Equality_Byte() + { + var digit = Digit.Create('5'); + Assert.True(digit.Equals((byte)5)); + Assert.False(digit.Equals((byte)6)); + Assert.True(digit.Equals((object)(byte)5)); + + Assert.True(digit == (byte)5); + Assert.True((byte)5 == digit); + Assert.True(digit != (byte)6); + Assert.True((byte)6 != digit); + } + + [Fact] + public void Equality_Int() + { + var digit = Digit.Create('5'); + Assert.True(digit.Equals(5)); + Assert.False(digit.Equals(6)); + Assert.False(digit.Equals(500)); + Assert.True(digit.Equals((object)5)); + + Assert.True(digit == 5); + Assert.True(5 == digit); + Assert.True(digit != 500); + Assert.True(500 != digit); + } + + [Property] + public void GetHashCode_MatchesByteHashCode(char c) + { + var digit = Digit.TryCreate(c); + if (digit is null) + { + return; + } + Assert.Equal(digit.Value.Value.GetHashCode(), digit.Value.GetHashCode()); + } + + [Fact] + public void Comparison_Digit() + { + var three = Digit.Create('3'); + var seven = Digit.Create('7'); + + Assert.True(three.CompareTo(seven) < 0); + Assert.True(seven.CompareTo(three) > 0); + Assert.Equal(0, three.CompareTo(three)); + + Assert.True(three < seven); + Assert.True(three <= seven); + Assert.True(seven > three); + Assert.True(seven >= three); + var threeAgain = Digit.Create('3'); + Assert.True(three <= threeAgain); + Assert.True(three >= threeAgain); + } + + [Fact] + public void Comparison_Byte() + { + var digit = Digit.Create('5'); + Assert.True(digit.CompareTo((byte)4) > 0); + Assert.True(digit.CompareTo((byte)6) < 0); + Assert.Equal(0, digit.CompareTo((byte)5)); + + Assert.True(digit > (byte)4); + Assert.True(digit >= (byte)5); + Assert.True(digit < (byte)6); + Assert.True(digit <= (byte)5); + + Assert.True((byte)4 < digit); + Assert.True((byte)5 <= digit); + Assert.True((byte)6 > digit); + Assert.True((byte)5 >= digit); + } + + [Fact] + public void Comparison_Int() + { + var digit = Digit.Create('5'); + Assert.True(digit.CompareTo(-1) > 0); + Assert.True(digit.CompareTo(1000) < 0); + Assert.Equal(0, digit.CompareTo(5)); + + Assert.True(digit > -1); + Assert.True(digit >= 5); + Assert.True(digit < 1000); + Assert.True(digit <= 5); + + Assert.True(-1 < digit); + Assert.True(5 <= digit); + Assert.True(1000 > digit); + Assert.True(5 >= digit); + } + + [Fact] + public void Comparison_NonGeneric() + { + IComparable digit = Digit.Create('5'); + Assert.True(digit.CompareTo(null) > 0); + Assert.True(digit.CompareTo(Digit.Create('3')) > 0); + Assert.True(digit.CompareTo((byte)3) > 0); + Assert.True(digit.CompareTo(3) > 0); + Assert.Throws(() => digit.CompareTo("not a digit")); + } + + [Fact] + public void ImplicitConversions() + { + byte b = Digit.Create('5'); + int i = Digit.Create('9'); + + Assert.Equal((byte)5, b); + Assert.Equal(9, i); + } + + [Fact] + public void ToString_IsDecimalValue() + { + Assert.Equal("0", Digit.Create('0').ToString()); + Assert.Equal("9", Digit.Create('9').ToString()); + } +} diff --git a/src/StrongTypes.Tests/Numbers/NumberExtensionsTests.cs b/src/StrongTypes.Tests/Numbers/NumberExtensionsTests.cs new file mode 100644 index 00000000..acb7d984 --- /dev/null +++ b/src/StrongTypes.Tests/Numbers/NumberExtensionsTests.cs @@ -0,0 +1,99 @@ +#nullable enable + +using FsCheck.Xunit; +using Xunit; + +namespace StrongTypes.Tests; + +public class NumberExtensionsTests +{ + [Fact] + public void Divide_Int_ReturnsQuotient() + { + Assert.Equal(0.5m, 1.Divide(2)); + Assert.Equal(1.5m, 3.Divide(2)); + } + + [Fact] + public void Divide_Int_ReturnsNull_WhenDivisorIsZero() + { + Assert.Null(1.Divide(0)); + Assert.Null(3489.Divide(0)); + } + + [Fact] + public void Divide_Decimal_ReturnsQuotient() + { + Assert.Equal(0.5m, 1m.Divide(2)); + Assert.Equal(1.5m, 3m.Divide(2)); + } + + [Fact] + public void Divide_Decimal_ReturnsNull_WhenDivisorIsZero() + { + Assert.Null(1m.Divide(0)); + Assert.Null(3489m.Divide(0)); + } + + [Property] + public void Divide_Int_MatchesRawDivision_WhenNonZero(int a, decimal b) + { + if (b == 0) + { + return; + } + Assert.Equal(a / b, a.Divide(b)); + } + + [Property] + public void Divide_Decimal_MatchesRawDivision_WhenNonZero(decimal a, decimal b) + { + if (b == 0) + { + return; + } + Assert.Equal(a / b, a.Divide(b)); + } + + [Fact] + public void SafeDivide_Int_ReturnsQuotient() + { + Assert.Equal(0.5m, 1.SafeDivide(2)); + Assert.Equal(1.5m, 3.SafeDivide(2)); + } + + [Fact] + public void SafeDivide_Int_ReturnsFallback_WhenDivisorIsZero() + { + Assert.Equal(14.33m, 1.SafeDivide(0, 14.33m)); + Assert.Equal(12.12m, 3489.SafeDivide(0, 12.12m)); + Assert.Equal(0m, 1.SafeDivide(0)); + } + + [Fact] + public void SafeDivide_Decimal_ReturnsQuotient() + { + Assert.Equal(0.5m, 1m.SafeDivide(2)); + Assert.Equal(1.5m, 3m.SafeDivide(2)); + } + + [Fact] + public void SafeDivide_Decimal_ReturnsFallback_WhenDivisorIsZero() + { + Assert.Equal(14.33m, 1m.SafeDivide(0, 14.33m)); + Assert.Equal(12.12m, 3489m.SafeDivide(0, 12.12m)); + Assert.Equal(0m, 1m.SafeDivide(0)); + } + + [Property] + public void SafeDivide_Int_EqualsDivideOrFallback(int a, decimal b, decimal otherwise) + { + Assert.Equal(a.Divide(b) ?? otherwise, a.SafeDivide(b, otherwise)); + } + + [Property] + public void SafeDivide_Decimal_EqualsDivideOrFallback(decimal a, decimal b, decimal otherwise) + { + Assert.Equal(a.Divide(b) ?? otherwise, a.SafeDivide(b, otherwise)); + } +} diff --git a/src/StrongTypes.Tests/Numeric_Old/DigitTests_Old.cs b/src/StrongTypes.Tests/Numeric_Old/DigitTests_Old.cs deleted file mode 100644 index 75987a17..00000000 --- a/src/StrongTypes.Tests/Numeric_Old/DigitTests_Old.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Linq; -using FsCheck.Xunit; -using Xunit; - -namespace StrongTypes.Tests.Numeric; - -public class DigitTests -{ - [Fact] - internal void AsDigit() - { - AssertDigitOption(0, '0'.AsDigit()); - AssertDigitOption(1, '1'.AsDigit()); - AssertDigitOption(2, '2'.AsDigit()); - AssertDigitOption(3, '3'.AsDigit()); - AssertDigitOption(4, '4'.AsDigit()); - AssertDigitOption(5, '5'.AsDigit()); - AssertDigitOption(6, '6'.AsDigit()); - AssertDigitOption(7, '7'.AsDigit()); - AssertDigitOption(8, '8'.AsDigit()); - AssertDigitOption(9, '9'.AsDigit()); - - OptionAssert.IsEmpty('a'.AsDigit()); - OptionAssert.IsEmpty('z'.AsDigit()); - OptionAssert.IsEmpty('B'.AsDigit()); - OptionAssert.IsEmpty(char.MinValue.AsDigit()); - OptionAssert.IsEmpty(char.MaxValue.AsDigit()); - } - - [Fact] - internal void Equality() - { - Assert.Equal('0'.AsDigit(), '0'.AsDigit()); - Assert.Equal('1'.AsDigit(), '1'.AsDigit()); - Assert.Equal('2'.AsDigit(), '2'.AsDigit()); - Assert.Equal('3'.AsDigit(), '3'.AsDigit()); - Assert.Equal('4'.AsDigit(), '4'.AsDigit()); - - Assert.NotEqual('0'.AsDigit(), '9'.AsDigit()); - Assert.NotEqual('1'.AsDigit(), '8'.AsDigit()); - Assert.NotEqual('2'.AsDigit(), '7'.AsDigit()); - Assert.NotEqual('3'.AsDigit(), '6'.AsDigit()); - Assert.NotEqual('4'.AsDigit(), '5'.AsDigit()); - } - - [Fact] - internal void FilterDigits() - { - Assert.Equal( - new byte[] { 1, 2, 3, 8, 7, 6, 5, 9, }, - "ASD 1 some spaces 2 with numbers 38 7 in between .6 ?:`'!@(#*&$%&^!@)$_ them59".FilterDigits().Select(d => d.Value) - ); - } - - [Property] - internal void AllNumbersSucceed(int number) - { - var firstDigit = Math.Abs(number).ToString()[0]; - OptionAssert.NonEmpty(firstDigit.AsDigit()); - } - - [Property] - internal void AllCharsSucceed(char c) - { - var result = c.AsDigit(); - Assert.Equal(char.IsDigit(c), result.NonEmpty); - } - - private void AssertDigitOption(byte value, Option digit) - { - Assert.True(digit.NonEmpty, "Option was expected to have a value, but was empty."); - Assert.Equal(value, digit.Get()); - Assert.Equal(value, digit.Get().Value); - } -} \ No newline at end of file diff --git a/src/StrongTypes.Tests/Numeric_Old/DivideTests_Old.cs b/src/StrongTypes.Tests/Numeric_Old/DivideTests_Old.cs deleted file mode 100644 index bb3bf71b..00000000 --- a/src/StrongTypes.Tests/Numeric_Old/DivideTests_Old.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Xunit; - -namespace StrongTypes.Tests.Numeric; - -public class DivideTests -{ - [Fact] - internal void SafeDivide_int() - { - Assert.Equal(0.5m, 1.SafeDivide(2)); - Assert.Equal(1.5m, 3.SafeDivide(2)); - Assert.Equal(14.33m, 1.SafeDivide(0, 14.33m)); - Assert.Equal(12.12m, 3489.SafeDivide(0, 12.12m)); - } - - [Fact] - internal void SafeDivide_decimal() - { - Assert.Equal(0.5m, 1m.SafeDivide(2)); - Assert.Equal(1.5m, 3m.SafeDivide(2)); - Assert.Equal(14.33m, 1m.SafeDivide(0, 14.33m)); - Assert.Equal(12.12m, 3489m.SafeDivide(0, 12.12m)); - } - - [Fact] - internal void Divide_int() - { - Assert.Equal(0.5m.ToOption(), 1.Divide(2)); - Assert.Equal(1.5m.ToOption(), 3.Divide(2)); - Assert.Equal(Option.Empty(), 1.Divide(0)); - Assert.Equal(Option.Empty(), 3489.Divide(0)); - } - - [Fact] - internal void Divide_decimal() - { - Assert.Equal(0.5m.ToOption(), 1m.Divide(2)); - Assert.Equal(1.5m.ToOption(), 3m.Divide(2)); - Assert.Equal(Option.Empty(), 1m.Divide(0)); - Assert.Equal(Option.Empty(), 3489m.Divide(0)); - } -} \ No newline at end of file diff --git a/src/StrongTypes/Collections/IEnumerableExtensions.cs b/src/StrongTypes/Collections/IEnumerableExtensions.cs new file mode 100644 index 00000000..fb006b42 --- /dev/null +++ b/src/StrongTypes/Collections/IEnumerableExtensions.cs @@ -0,0 +1,28 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace StrongTypes; + +public static partial class IEnumerableExtensions +{ + /// + /// Returns the non-null values from , unwrapping each + /// nullable into its underlying value. + /// + public static IEnumerable ExceptNulls(this IEnumerable source) + where T : struct + { + return source.Where(item => item.HasValue).Select(item => item!.Value); + } + + /// + /// Returns the non-null references from . + /// + public static IEnumerable ExceptNulls(this IEnumerable source) + where T : class + { + return source.Where(item => item is not null)!; + } +} diff --git a/src/StrongTypes/Collections_Old/IEnumerableExtensions_Generic_Old.cs b/src/StrongTypes/Collections_Old/IEnumerableExtensions_Generic_Old.cs index 1af4fc7a..1cc748dc 100644 --- a/src/StrongTypes/Collections_Old/IEnumerableExtensions_Generic_Old.cs +++ b/src/StrongTypes/Collections_Old/IEnumerableExtensions_Generic_Old.cs @@ -89,18 +89,6 @@ public static IEnumerable Except(this IEnumerable e, params IEnumerable return Enumerable.Except(e, others.Flatten()); } - public static IEnumerable ExceptNulls(this IEnumerable e) - where T : struct - { - return e.Where(item => item.HasValue).Select(item => item.Value); - } - - public static IEnumerable ExceptNulls(this IEnumerable e) - where T : class - { - return e.Where(v => v is not null); - } - public static bool IsMultiple(this IEnumerable e) { switch (e) diff --git a/src/StrongTypes/Digits/Digit.cs b/src/StrongTypes/Digits/Digit.cs new file mode 100644 index 00000000..0423bb44 --- /dev/null +++ b/src/StrongTypes/Digits/Digit.cs @@ -0,0 +1,141 @@ +#nullable enable + +using System; + +namespace StrongTypes; + +/// +/// A decimal digit in the range 09, parsed from a single character. +/// +/// +/// +/// Construct via or . A character is accepted when +/// returns true — this includes non-ASCII Unicode decimal digits, +/// whose numeric value is folded into a byte in the 0–9 range. +/// +/// +/// default(Digit) represents the digit 0, which satisfies the invariant. +/// +/// +public readonly struct Digit : + IEquatable, + IEquatable, + IEquatable, + IComparable, + IComparable, + IComparable, + IComparable +{ + private Digit(byte value) + { + Value = value; + } + + public byte Value { get; } + + public static implicit operator byte(Digit d) => d.Value; + public static implicit operator int(Digit d) => d.Value; + + /// + /// Returns a wrapping the decimal value of , + /// or null if is not a decimal digit character. + /// + public static Digit? TryCreate(char value) + { + if (!char.IsDigit(value)) + { + return null; + } + + return new Digit((byte)char.GetNumericValue(value)); + } + + /// + /// Returns a wrapping the decimal value of . + /// Throws if is not a decimal digit character. + /// + public static Digit Create(char value) + { + return TryCreate(value) + ?? throw new ArgumentException($"Value must be a decimal digit character, but was '{value}'.", nameof(value)); + } + + #region Equality + + public override int GetHashCode() => Value.GetHashCode(); + + public override bool Equals(object? obj) => + obj switch + { + Digit other => Equals(other), + byte otherByte => Equals(otherByte), + int otherInt => Equals(otherInt), + _ => false + }; + + public bool Equals(Digit other) => Value == other.Value; + + public bool Equals(byte other) => Value == other; + + public bool Equals(int other) => Value == other; + + public static bool operator ==(Digit left, Digit right) => left.Equals(right); + public static bool operator !=(Digit left, Digit right) => !left.Equals(right); + + public static bool operator ==(Digit left, byte right) => left.Equals(right); + public static bool operator !=(Digit left, byte right) => !left.Equals(right); + public static bool operator ==(byte left, Digit right) => right.Equals(left); + public static bool operator !=(byte left, Digit right) => !right.Equals(left); + + public static bool operator ==(Digit left, int right) => left.Equals(right); + public static bool operator !=(Digit left, int right) => !left.Equals(right); + public static bool operator ==(int left, Digit right) => right.Equals(left); + public static bool operator !=(int left, Digit right) => !right.Equals(left); + + #endregion Equality + + #region Comparison + + public int CompareTo(Digit other) => Value.CompareTo(other.Value); + + public int CompareTo(byte other) => Value.CompareTo(other); + + public int CompareTo(int other) => ((int)Value).CompareTo(other); + + int IComparable.CompareTo(object? obj) => + obj switch + { + null => 1, + Digit other => CompareTo(other), + byte otherByte => CompareTo(otherByte), + int otherInt => CompareTo(otherInt), + _ => throw new ArgumentException($"Object must be of type {nameof(Digit)}, {nameof(Byte)}, or {nameof(Int32)}.", nameof(obj)) + }; + + public static bool operator <(Digit left, Digit right) => left.CompareTo(right) < 0; + public static bool operator <=(Digit left, Digit right) => left.CompareTo(right) <= 0; + public static bool operator >(Digit left, Digit right) => left.CompareTo(right) > 0; + public static bool operator >=(Digit left, Digit right) => left.CompareTo(right) >= 0; + + public static bool operator <(Digit left, byte right) => left.CompareTo(right) < 0; + public static bool operator <=(Digit left, byte right) => left.CompareTo(right) <= 0; + public static bool operator >(Digit left, byte right) => left.CompareTo(right) > 0; + public static bool operator >=(Digit left, byte right) => left.CompareTo(right) >= 0; + public static bool operator <(byte left, Digit right) => right.CompareTo(left) > 0; + public static bool operator <=(byte left, Digit right) => right.CompareTo(left) >= 0; + public static bool operator >(byte left, Digit right) => right.CompareTo(left) < 0; + public static bool operator >=(byte left, Digit right) => right.CompareTo(left) <= 0; + + public static bool operator <(Digit left, int right) => left.CompareTo(right) < 0; + public static bool operator <=(Digit left, int right) => left.CompareTo(right) <= 0; + public static bool operator >(Digit left, int right) => left.CompareTo(right) > 0; + public static bool operator >=(Digit left, int right) => left.CompareTo(right) >= 0; + public static bool operator <(int left, Digit right) => right.CompareTo(left) > 0; + public static bool operator <=(int left, Digit right) => right.CompareTo(left) >= 0; + public static bool operator >(int left, Digit right) => right.CompareTo(left) < 0; + public static bool operator >=(int left, Digit right) => right.CompareTo(left) <= 0; + + #endregion Comparison + + public override string ToString() => Value.ToString(); +} diff --git a/src/StrongTypes/Digits/DigitExtensions.cs b/src/StrongTypes/Digits/DigitExtensions.cs new file mode 100644 index 00000000..f62d7977 --- /dev/null +++ b/src/StrongTypes/Digits/DigitExtensions.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace StrongTypes; + +public static class DigitExtensions +{ + /// + /// Returns a wrapping , or + /// null if is not a decimal digit character. + /// + public static Digit? AsDigit(this char value) => Digit.TryCreate(value); + + /// + /// Returns the decimal digits in , in order. + /// A null input yields an empty sequence. + /// + public static IEnumerable FilterDigits(this string? value) + { + if (value is null) + { + return Enumerable.Empty(); + } + + return value.Select(Digit.TryCreate).ExceptNulls(); + } +} diff --git a/src/StrongTypes/Numbers/NumberExtensions.cs b/src/StrongTypes/Numbers/NumberExtensions.cs new file mode 100644 index 00000000..351220ef --- /dev/null +++ b/src/StrongTypes/Numbers/NumberExtensions.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace StrongTypes; + +public static class NumberExtensions +{ + /// + /// Returns divided by , or null + /// when is zero. + /// + public static decimal? Divide(this int a, decimal b) + => b == 0 ? null : a / b; + + /// + /// Returns divided by , or null + /// when is zero. + /// + public static decimal? Divide(this decimal a, decimal b) + => b == 0 ? null : a / b; + + /// + /// Returns divided by , or + /// when is zero. + /// + public static decimal SafeDivide(this int a, decimal b, decimal otherwise = 0) + => a.Divide(b) ?? otherwise; + + /// + /// Returns divided by , or + /// when is zero. + /// + public static decimal SafeDivide(this decimal a, decimal b, decimal otherwise = 0) + => a.Divide(b) ?? otherwise; +} diff --git a/src/StrongTypes/Numeric_Old/DigitExtensions_Old.cs b/src/StrongTypes/Numeric_Old/DigitExtensions_Old.cs deleted file mode 100644 index e33eff02..00000000 --- a/src/StrongTypes/Numeric_Old/DigitExtensions_Old.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StrongTypes; - -public static class DigitExtensions -{ - public static Option AsDigit(this char value) - { - return Digit.Create(value); - } - - public static IEnumerable FilterDigits(this string value) - { - if (value is null) - { - return Enumerable.Empty(); - } - return value.Select(Digit.Create).Flatten(); - } -} \ No newline at end of file diff --git a/src/StrongTypes/Numeric_Old/Digit_Old.cs b/src/StrongTypes/Numeric_Old/Digit_Old.cs deleted file mode 100644 index dfc30273..00000000 --- a/src/StrongTypes/Numeric_Old/Digit_Old.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; - -namespace StrongTypes; - -public readonly struct Digit : IEquatable -{ - private Digit(byte value) - { - Value = value; - } - - public byte Value { get; } - public static implicit operator byte(Digit i) => i.Value; - public static implicit operator int(Digit i) => i.Value; - - public static Option Create(char value) - { - return CreateNullable(value).ToOption(); - } - - public static Digit CreateUnsafe(char value) - { - var result = CreateNullable(value); - ArgumentNullException.ThrowIfNull(result, nameof(Digit)); - return result.Value; - } - - public static Digit? CreateNullable(char value) - { - return char.IsDigit(value) - ? new Digit(byte.Parse(value.ToString())) - : null; - } - - public static bool operator ==(Digit left, Digit right) => left.Equals(right); - - public static bool operator !=(Digit left, Digit right) => !left.Equals(right); - - public override bool Equals(object obj) - { - return obj is Digit other && Equals(other); - } - - public bool Equals(Digit other) - { - return Value == other.Value; - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public override string ToString() - { - return Value.ToString(); - } -} diff --git a/src/StrongTypes/Numeric_Old/NumberExtensions_Old.cs b/src/StrongTypes/Numeric_Old/NumberExtensions_Old.cs deleted file mode 100644 index b6d609d0..00000000 --- a/src/StrongTypes/Numeric_Old/NumberExtensions_Old.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace StrongTypes; - -public static class NumberExtensions -{ - public static decimal SafeDivide(this int a, decimal b, decimal otherwise = 0) - { - return b == 0 - ? otherwise - : a / b; - } - - public static decimal SafeDivide(this decimal a, decimal b, decimal otherwise = 0) - { - return b == 0 - ? otherwise - : a / b; - } - - public static Option Divide(this int a, decimal b) - { - return b == 0 - ? Option.Empty() - : Option.Valued(a / b); - } - - public static Option Divide(this decimal a, decimal b) - { - return b == 0 - ? Option.Empty() - : Option.Valued(a / b); - } -} \ No newline at end of file