diff --git a/MyNumberNET/MyNumber.cs b/MyNumberNET/MyNumber.cs index 6e83afb..1283359 100644 --- a/MyNumberNET/MyNumber.cs +++ b/MyNumberNET/MyNumber.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; namespace MyNumberNET { @@ -103,4 +106,299 @@ public MyNumberMalformedException(string message) #endregion } + + /// + /// Represents a valid My Number that enforces correct format and provides type safety. + /// This is an immutable value type that ensures the My Number is always in a valid state. + /// + public readonly struct MyNumberValue : IEquatable, IFormattable + { + private readonly int[] _digits; + + /// + /// Gets the 12-digit array representation of this My Number. + /// + public int[] Digits => (int[])_digits?.Clone() ?? throw new InvalidOperationException("MyNumberValue is not initialized."); + + /// + /// Gets whether this MyNumberValue instance has been properly initialized. + /// + public bool IsInitialized => _digits != null; + + /// + /// Initializes a new instance of MyNumberValue from a 12-digit array. + /// + /// A 12-digit array representing a valid My Number. + /// Thrown when the digits don't represent a valid My Number. + public MyNumberValue(int[] digits) + { + if (!MyNumber.VerifyNumber(digits)) + { + throw new MyNumber.MyNumberMalformedException("The provided digits do not represent a valid My Number."); + } + _digits = (int[])digits.Clone(); + } + + /// + /// Initializes a new instance of MyNumberValue from individual digit parameters. + /// + /// First digit + /// Second digit + /// Third digit + /// Fourth digit + /// Fifth digit + /// Sixth digit + /// Seventh digit + /// Eighth digit + /// Ninth digit + /// Tenth digit + /// Eleventh digit + /// Twelfth digit (check digit) + /// Thrown when the digits don't represent a valid My Number. + public MyNumberValue(int d1, int d2, int d3, int d4, int d5, int d6, int d7, int d8, int d9, int d10, int d11, int d12) + : this(new[] { d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12 }) + { + } + + /// + /// Creates a MyNumberValue from the first 11 digits, automatically calculating the check digit. + /// + /// The first 11 digits of the My Number. + /// A valid MyNumberValue with the calculated check digit. + /// Thrown when the input is invalid. + public static MyNumberValue FromFirstElevenDigits(int[] firstElevenDigits) + { + if (firstElevenDigits == null || firstElevenDigits.Length != 11) + { + throw new MyNumber.MyNumberMalformedException("Must provide exactly 11 digits."); + } + + var checkDigit = MyNumber.CalculateCheckDigits(firstElevenDigits); + var allDigits = new int[12]; + Array.Copy(firstElevenDigits, allDigits, 11); + allDigits[11] = checkDigit; + + return new MyNumberValue(allDigits); + } + + /// + /// Attempts to parse a string representation of a My Number. + /// + /// String containing 12 digits (may include separators like spaces or hyphens). + /// The parsed MyNumberValue if successful. + /// True if parsing was successful, false otherwise. + public static bool TryParse(string value, out MyNumberValue result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + // Remove common separators + var cleanValue = value.Replace(" ", "").Replace("-", "").Replace("_", ""); + + if (cleanValue.Length != 12) + return false; + + var digits = new int[12]; + for (int i = 0; i < 12; i++) + { + if (!char.IsDigit(cleanValue[i])) + return false; + digits[i] = cleanValue[i] - '0'; + } + + try + { + result = new MyNumberValue(digits); + return true; + } + catch (MyNumber.MyNumberMalformedException) + { + return false; + } + } + + /// + /// Parses a string representation of a My Number. + /// + /// String containing 12 digits (may include separators like spaces or hyphens). + /// A valid MyNumberValue. + /// Thrown when the string cannot be parsed as a valid My Number. + public static MyNumberValue Parse(string value) + { + if (TryParse(value, out var result)) + return result; + + throw new ArgumentException($"Unable to parse '{value}' as a valid My Number.", nameof(value)); + } + + /// + /// Generates a random valid My Number. + /// + /// A randomly generated valid MyNumberValue. + public static MyNumberValue GenerateRandom() + { + var generator = new MyNumber(); + var digits = generator.GenerateRandomNumber(); + return new MyNumberValue(digits); + } + + /// + /// Returns the string representation of this My Number. + /// + /// A 12-digit string representation. + public override string ToString() + { + return ToString("N", CultureInfo.InvariantCulture); + } + + /// + /// Returns the string representation of this My Number with the specified format. + /// + /// + /// Format string: + /// "N" or null = no separators (default): "123456789012" + /// "S" = with spaces: "1234 5678 9012" + /// "H" = with hyphens: "1234-5678-9012" + /// "G" = grouped format: "1234-5678-901-2" + /// + /// Formatted string representation. + public string ToString(string format) + { + return ToString(format, CultureInfo.InvariantCulture); + } + + /// + /// Returns the string representation of this My Number with the specified format and format provider. + /// + /// Format string (see ToString(string) for options). + /// Format provider (currently not used). + /// Formatted string representation. + public string ToString(string format, IFormatProvider formatProvider) + { + if (!IsInitialized) + throw new InvalidOperationException("MyNumberValue is not initialized."); + + var digitString = string.Join("", _digits); + + return format?.ToUpperInvariant() switch + { + null or "N" => digitString, + "S" => $"{digitString.Substring(0, 4)} {digitString.Substring(4, 4)} {digitString.Substring(8, 4)}", + "H" => $"{digitString.Substring(0, 4)}-{digitString.Substring(4, 4)}-{digitString.Substring(8, 4)}", + "G" => $"{digitString.Substring(0, 4)}-{digitString.Substring(4, 4)}-{digitString.Substring(8, 3)}-{digitString.Substring(11, 1)}", + _ => throw new FormatException($"Format string '{format}' is not supported.") + }; + } + + /// + /// Determines whether this instance is equal to another MyNumberValue. + /// + /// The other MyNumberValue to compare. + /// True if equal, false otherwise. + public bool Equals(MyNumberValue other) + { + if (!IsInitialized && !other.IsInitialized) + return true; + if (!IsInitialized || !other.IsInitialized) + return false; + + return _digits.SequenceEqual(other._digits); + } + + /// + /// Determines whether this instance is equal to the specified object. + /// + /// The object to compare. + /// True if equal, false otherwise. + public override bool Equals(object obj) + { + return obj is MyNumberValue other && Equals(other); + } + + /// + /// Gets the hash code for this MyNumberValue. + /// + /// A hash code for this instance. + public override int GetHashCode() + { + if (!IsInitialized) + return 0; + + unchecked + { + int hash = 17; + foreach (var digit in _digits) + { + hash = hash * 31 + digit; + } + return hash; + } + } + + /// + /// Determines whether two MyNumberValue instances are equal. + /// + /// The first MyNumberValue. + /// The second MyNumberValue. + /// True if equal, false otherwise. + public static bool operator ==(MyNumberValue left, MyNumberValue right) + { + return left.Equals(right); + } + + /// + /// Determines whether two MyNumberValue instances are not equal. + /// + /// The first MyNumberValue. + /// The second MyNumberValue. + /// True if not equal, false otherwise. + public static bool operator !=(MyNumberValue left, MyNumberValue right) + { + return !left.Equals(right); + } + + /// + /// Implicitly converts a MyNumberValue to an int array. + /// + /// The MyNumberValue to convert. + /// A 12-digit int array. + public static implicit operator int[](MyNumberValue myNumber) + { + return myNumber.Digits; + } + + /// + /// Implicitly converts a MyNumberValue to a string. + /// + /// The MyNumberValue to convert. + /// A 12-digit string representation. + public static implicit operator string(MyNumberValue myNumber) + { + return myNumber.ToString(); + } + + /// + /// Explicitly converts an int array to a MyNumberValue. + /// + /// The 12-digit array to convert. + /// A MyNumberValue instance. + /// Thrown when the array is not a valid My Number. + public static explicit operator MyNumberValue(int[] digits) + { + return new MyNumberValue(digits); + } + + /// + /// Explicitly converts a string to a MyNumberValue. + /// + /// The string to convert. + /// A MyNumberValue instance. + /// Thrown when the string is not a valid My Number. + public static explicit operator MyNumberValue(string value) + { + return Parse(value); + } + } } \ No newline at end of file diff --git a/MyNumberNET_ApiServer/Controllers/MyNumberController.cs b/MyNumberNET_ApiServer/Controllers/MyNumberController.cs index 1f88367..ab3f3de 100644 --- a/MyNumberNET_ApiServer/Controllers/MyNumberController.cs +++ b/MyNumberNET_ApiServer/Controllers/MyNumberController.cs @@ -29,6 +29,47 @@ public ActionResult Verify([FromBody] int[] number) } } + /// + /// Verifies and creates a MyNumberValue from the provided input string. + /// + /// A string representation of the My Number (may include separators). + /// A MyNumberValue object if valid, or BadRequest if invalid. + [HttpPost("verify-string")] + public ActionResult VerifyString([FromBody] string numberString) + { + try + { + if (MyNumberValue.TryParse(numberString, out var myNumber)) + { + return Ok(myNumber); + } + return BadRequest($"Invalid My Number format: {numberString}"); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Creates a MyNumberValue from the provided digits array. + /// + /// An array of 12 integers representing the My Number digits. + /// A MyNumberValue object if valid, or BadRequest if invalid. + [HttpPost("create")] + public ActionResult Create([FromBody] int[] number) + { + try + { + var myNumber = new MyNumberValue(number); + return Ok(myNumber); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + /// /// Calculates the check digit for the provided 11-digit array. /// @@ -47,5 +88,81 @@ public ActionResult CheckDigit([FromBody] int[] number) return BadRequest(ex.Message); } } + + /// + /// Creates a complete MyNumberValue from the first 11 digits by calculating the check digit. + /// + /// An array of 11 integers representing the first 11 digits of My Number. + /// A complete MyNumberValue with calculated check digit, or BadRequest if invalid. + [HttpPost("complete")] + public ActionResult Complete([FromBody] int[] number) + { + try + { + var myNumber = MyNumberValue.FromFirstElevenDigits(number); + return Ok(myNumber); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Generates a random valid My Number. + /// + /// A randomly generated MyNumberValue. + [HttpGet("generate")] + public ActionResult Generate() + { + try + { + var myNumber = MyNumberValue.GenerateRandom(); + return Ok(myNumber); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Formats a MyNumberValue with the specified format. + /// + /// Request containing the My Number string and desired format. + /// The formatted My Number string, or BadRequest if invalid. + [HttpPost("format")] + public ActionResult Format([FromBody] FormatRequest request) + { + try + { + if (MyNumberValue.TryParse(request.Number, out var myNumber)) + { + var formatted = myNumber.ToString(request.Format ?? "N"); + return Ok(formatted); + } + return BadRequest($"Invalid My Number format: {request.Number}"); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } + + /// + /// Request model for formatting operations. + /// + public class FormatRequest + { + /// + /// The My Number to format. + /// + public string Number { get; set; } = ""; + + /// + /// The format to apply ("N", "S", "H", "G"). + /// + public string? Format { get; set; } } } diff --git a/MyNumberNET_Test/MyNumberControllerTests.cs b/MyNumberNET_Test/MyNumberControllerTests.cs index 57372ff..ab21183 100644 --- a/MyNumberNET_Test/MyNumberControllerTests.cs +++ b/MyNumberNET_Test/MyNumberControllerTests.cs @@ -2,11 +2,15 @@ using Xunit; using MyNumberNET_ApiServer.Controllers; using Microsoft.AspNetCore.Mvc; +using MyNumberNET; namespace MyNumberNET_Test { public class MyNumberControllerTests { + private readonly int[] _validDigits = { 6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0, 0 }; + private readonly int[] _validFirst11Digits = { 6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0 }; + [Fact] public void Verify_ValidNumber_ReturnsTrue() { @@ -68,5 +72,173 @@ public void CheckDigit_MalformedInput_ReturnsBadRequest() var badRequestResult = result.Result as BadRequestObjectResult; Assert.NotNull(badRequestResult); } + + [Fact] + public void VerifyString_ValidString_ReturnsMyNumberValue() + { + var controller = new MyNumberController(); + var result = controller.VerifyString("614106526000"); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + var myNumberValue = okResult.Value as MyNumberValue?; + Assert.True(myNumberValue.HasValue); + Assert.True(myNumberValue.Value.IsInitialized); + } + + [Fact] + public void VerifyString_ValidStringWithSeparators_ReturnsMyNumberValue() + { + var controller = new MyNumberController(); + var result = controller.VerifyString("6141-0652-6000"); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + var myNumberValue = okResult.Value as MyNumberValue?; + Assert.True(myNumberValue.HasValue); + Assert.True(myNumberValue.Value.IsInitialized); + } + + [Fact] + public void VerifyString_InvalidString_ReturnsBadRequest() + { + var controller = new MyNumberController(); + var result = controller.VerifyString("invalid"); + Assert.NotNull(result.Result); + var badRequestResult = result.Result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + } + + [Fact] + public void Create_ValidDigits_ReturnsMyNumberValue() + { + var controller = new MyNumberController(); + var result = controller.Create(_validDigits); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + var myNumberValue = okResult.Value as MyNumberValue?; + Assert.True(myNumberValue.HasValue); + Assert.True(myNumberValue.Value.IsInitialized); + } + + [Fact] + public void Create_InvalidDigits_ReturnsBadRequest() + { + var controller = new MyNumberController(); + var invalidDigits = new[] { 1,2,3,4,5,6,7,8,9,0,1,9 }; // Wrong check digit + var result = controller.Create(invalidDigits); + Assert.NotNull(result.Result); + var badRequestResult = result.Result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + } + + [Fact] + public void Complete_ValidFirst11Digits_ReturnsCompleteMyNumberValue() + { + var controller = new MyNumberController(); + var result = controller.Complete(_validFirst11Digits); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + var myNumberValue = okResult.Value as MyNumberValue?; + Assert.True(myNumberValue.HasValue); + Assert.True(myNumberValue.Value.IsInitialized); + + // Verify the check digit is correct + var digits = myNumberValue.Value.Digits; + Assert.Equal(0, digits[11]); // Expected check digit for our test data + } + + [Fact] + public void Complete_InvalidFirst11Digits_ReturnsBadRequest() + { + var controller = new MyNumberController(); + var invalidDigits = new[] { 1,2,3 }; // Too short + var result = controller.Complete(invalidDigits); + Assert.NotNull(result.Result); + var badRequestResult = result.Result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + } + + [Fact] + public void Generate_Always_ReturnsValidMyNumberValue() + { + var controller = new MyNumberController(); + + // Test multiple generations to ensure consistency + for (int i = 0; i < 10; i++) + { + var result = controller.Generate(); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + var myNumberValue = okResult.Value as MyNumberValue?; + Assert.True(myNumberValue.HasValue); + Assert.True(myNumberValue.Value.IsInitialized); + + // Verify it's actually valid + Assert.True(MyNumber.VerifyNumber(myNumberValue.Value.Digits)); + } + } + + [Fact] + public void Format_ValidNumberWithDefaultFormat_ReturnsFormattedString() + { + var controller = new MyNumberController(); + var request = new FormatRequest { Number = "614106526000" }; + var result = controller.Format(request); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + Assert.Equal("614106526000", okResult.Value); + } + + [Fact] + public void Format_ValidNumberWithSpaceFormat_ReturnsFormattedString() + { + var controller = new MyNumberController(); + var request = new FormatRequest { Number = "614106526000", Format = "S" }; + var result = controller.Format(request); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + Assert.Equal("6141 0652 6000", okResult.Value); + } + + [Fact] + public void Format_ValidNumberWithHyphenFormat_ReturnsFormattedString() + { + var controller = new MyNumberController(); + var request = new FormatRequest { Number = "614106526000", Format = "H" }; + var result = controller.Format(request); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + Assert.Equal("6141-0652-6000", okResult.Value); + } + + [Fact] + public void Format_ValidNumberWithGroupedFormat_ReturnsFormattedString() + { + var controller = new MyNumberController(); + var request = new FormatRequest { Number = "614106526000", Format = "G" }; + var result = controller.Format(request); + Assert.NotNull(result.Result); + var okResult = result.Result as OkObjectResult; + Assert.NotNull(okResult); + Assert.Equal("6141-0652-600-0", okResult.Value); + } + + [Fact] + public void Format_InvalidNumber_ReturnsBadRequest() + { + var controller = new MyNumberController(); + var request = new FormatRequest { Number = "invalid" }; + var result = controller.Format(request); + Assert.NotNull(result.Result); + var badRequestResult = result.Result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + } } } diff --git a/MyNumberNET_Test/MyNumberValueTests.cs b/MyNumberNET_Test/MyNumberValueTests.cs new file mode 100644 index 0000000..9f64947 --- /dev/null +++ b/MyNumberNET_Test/MyNumberValueTests.cs @@ -0,0 +1,359 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MyNumberNET; + +namespace MyNumberNET_Test +{ + [TestClass] + public class MyNumberValueTests + { + private readonly int[] _validMyNumber = { 6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0, 0 }; + private readonly int[] _invalidMyNumber = { 6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0, 1 }; + + [TestMethod] + public void Constructor_ValidDigits_CreatesMyNumberValue() + { + // Act + var myNumber = new MyNumberValue(_validMyNumber); + + // Assert + Assert.IsTrue(myNumber.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, myNumber.Digits); + } + + [TestMethod] + public void Constructor_InvalidDigits_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => new MyNumberValue(_invalidMyNumber)); + } + + [TestMethod] + public void Constructor_NullDigits_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => new MyNumberValue(null)); + } + + [TestMethod] + public void Constructor_WrongLength_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => new MyNumberValue(new int[10])); + Assert.ThrowsException(() => new MyNumberValue(new int[13])); + } + + [TestMethod] + public void Constructor_IndividualDigits_CreatesMyNumberValue() + { + // Act + var myNumber = new MyNumberValue(6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0, 0); + + // Assert + Assert.IsTrue(myNumber.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, myNumber.Digits); + } + + [TestMethod] + public void FromFirstElevenDigits_ValidDigits_CreatesMyNumberValue() + { + // Arrange + var firstEleven = new int[] { 6, 1, 4, 1, 0, 6, 5, 2, 6, 0, 0 }; + + // Act + var myNumber = MyNumberValue.FromFirstElevenDigits(firstEleven); + + // Assert + Assert.IsTrue(myNumber.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, myNumber.Digits); + } + + [TestMethod] + public void FromFirstElevenDigits_InvalidLength_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => + MyNumberValue.FromFirstElevenDigits(new int[10])); + Assert.ThrowsException(() => + MyNumberValue.FromFirstElevenDigits(null)); + } + + [TestMethod] + public void TryParse_ValidString_ReturnsTrue() + { + // Act + var success = MyNumberValue.TryParse("614106526000", out var result); + + // Assert + Assert.IsTrue(success); + Assert.IsTrue(result.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, result.Digits); + } + + [TestMethod] + public void TryParse_ValidStringWithSeparators_ReturnsTrue() + { + // Arrange + var testCases = new[] + { + "6141-0652-6000", + "6141 0652 6000", + "6141_0652_6000" + }; + + foreach (var testCase in testCases) + { + // Act + var success = MyNumberValue.TryParse(testCase, out var result); + + // Assert + Assert.IsTrue(success, $"Failed to parse: {testCase}"); + Assert.IsTrue(result.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, result.Digits); + } + } + + [TestMethod] + public void TryParse_InvalidString_ReturnsFalse() + { + // Arrange + var invalidCases = new[] + { + null, + "", + "abc", + "12345678901", // too short + "1234567890123", // too long + "614106526001", // invalid check digit + "61a106526000" // non-digit character + }; + + foreach (var invalidCase in invalidCases) + { + // Act + var success = MyNumberValue.TryParse(invalidCase, out var result); + + // Assert + Assert.IsFalse(success, $"Should have failed to parse: {invalidCase}"); + Assert.IsFalse(result.IsInitialized); + } + } + + [TestMethod] + public void Parse_ValidString_ReturnsMyNumberValue() + { + // Act + var result = MyNumberValue.Parse("614106526000"); + + // Assert + Assert.IsTrue(result.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, result.Digits); + } + + [TestMethod] + public void Parse_InvalidString_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => MyNumberValue.Parse("invalid")); + } + + [TestMethod] + public void GenerateRandom_Always_ReturnsValidMyNumberValue() + { + // Act + for (int i = 0; i < 100; i++) + { + var random = MyNumberValue.GenerateRandom(); + + // Assert + Assert.IsTrue(random.IsInitialized); + Assert.IsTrue(MyNumber.VerifyNumber(random.Digits)); + } + } + + [TestMethod] + public void ToString_DefaultFormat_ReturnsPlainDigits() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act + var result = myNumber.ToString(); + + // Assert + Assert.AreEqual("614106526000", result); + } + + [TestMethod] + public void ToString_VariousFormats_ReturnsCorrectFormat() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act & Assert + Assert.AreEqual("614106526000", myNumber.ToString("N")); + Assert.AreEqual("6141 0652 6000", myNumber.ToString("S")); + Assert.AreEqual("6141-0652-6000", myNumber.ToString("H")); + Assert.AreEqual("6141-0652-600-0", myNumber.ToString("G")); + } + + [TestMethod] + public void ToString_InvalidFormat_ThrowsException() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act & Assert + Assert.ThrowsException(() => myNumber.ToString("X")); + } + + [TestMethod] + public void Equals_SameValues_ReturnsTrue() + { + // Arrange + var myNumber1 = new MyNumberValue(_validMyNumber); + var myNumber2 = new MyNumberValue(_validMyNumber); + + // Act & Assert + Assert.IsTrue(myNumber1.Equals(myNumber2)); + Assert.IsTrue(myNumber1 == myNumber2); + Assert.IsFalse(myNumber1 != myNumber2); + } + + [TestMethod] + public void Equals_DifferentValues_ReturnsFalse() + { + // Arrange + var myNumber1 = new MyNumberValue(_validMyNumber); + var myNumber2 = MyNumberValue.GenerateRandom(); + + // Act & Assert + if (!myNumber1.Equals(myNumber2)) // They might be equal by chance + { + Assert.IsFalse(myNumber1.Equals(myNumber2)); + Assert.IsFalse(myNumber1 == myNumber2); + Assert.IsTrue(myNumber1 != myNumber2); + } + } + + [TestMethod] + public void Equals_UninitializedValues_ReturnsTrue() + { + // Arrange + var myNumber1 = new MyNumberValue(); + var myNumber2 = new MyNumberValue(); + + // Act & Assert + Assert.IsTrue(myNumber1.Equals(myNumber2)); + Assert.IsTrue(myNumber1 == myNumber2); + } + + [TestMethod] + public void GetHashCode_SameValues_ReturnsSameHashCode() + { + // Arrange + var myNumber1 = new MyNumberValue(_validMyNumber); + var myNumber2 = new MyNumberValue(_validMyNumber); + + // Act & Assert + Assert.AreEqual(myNumber1.GetHashCode(), myNumber2.GetHashCode()); + } + + [TestMethod] + public void ImplicitConversion_ToIntArray_ReturnsDigits() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act + int[] digits = myNumber; + + // Assert + CollectionAssert.AreEqual(_validMyNumber, digits); + } + + [TestMethod] + public void ImplicitConversion_ToString_ReturnsString() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act + string str = myNumber; + + // Assert + Assert.AreEqual("614106526000", str); + } + + [TestMethod] + public void ExplicitConversion_FromIntArray_CreatesMyNumberValue() + { + // Act + var myNumber = (MyNumberValue)_validMyNumber; + + // Assert + Assert.IsTrue(myNumber.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, myNumber.Digits); + } + + [TestMethod] + public void ExplicitConversion_FromString_CreatesMyNumberValue() + { + // Act + var myNumber = (MyNumberValue)"614106526000"; + + // Assert + Assert.IsTrue(myNumber.IsInitialized); + CollectionAssert.AreEqual(_validMyNumber, myNumber.Digits); + } + + [TestMethod] + public void ExplicitConversion_FromInvalidIntArray_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => (MyNumberValue)_invalidMyNumber); + } + + [TestMethod] + public void ExplicitConversion_FromInvalidString_ThrowsException() + { + // Act & Assert + Assert.ThrowsException(() => (MyNumberValue)"invalid"); + } + + [TestMethod] + public void Digits_PropertyAccess_ReturnsCopy() + { + // Arrange + var myNumber = new MyNumberValue(_validMyNumber); + + // Act + var digits1 = myNumber.Digits; + var digits2 = myNumber.Digits; + + // Assert + CollectionAssert.AreEqual(digits1, digits2); + Assert.AreNotSame(digits1, digits2); // Should be different instances + } + + [TestMethod] + public void Digits_UninitializedValue_ThrowsException() + { + // Arrange + var myNumber = new MyNumberValue(); + + // Act & Assert + Assert.ThrowsException(() => myNumber.Digits); + } + + [TestMethod] + public void ToString_UninitializedValue_ThrowsException() + { + // Arrange + var myNumber = new MyNumberValue(); + + // Act & Assert + Assert.ThrowsException(() => myNumber.ToString()); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 909ca1c..42bda77 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,40 @@ int[] number = {6,1,4,1,0,6,5,2,6,0,0,0}; bool isValid = MyNumber.VerifyNumber(number); ``` +### MyNumberValue (new) + +An immutable, strongly-typed value object that encapsulates a validated My Number. Constructing a `MyNumberValue` will validate format and check digit, making it safer to pass My Number values in code and APIs. + +Key points: +- Immutable and validated on creation. +- Construct from `int[]`, `string`, or individual digits. +- `TryParse`/`Parse` available for safe parsing. +- Provides `ToString(format)` with formats: `N` (plain), `S` (spaces), `H` (hyphens), `G` (grouped). + +Example usage: +```csharp +using MyNumberNET; + +// Parse from string +var value = MyNumberValue.Parse("614106526000"); + +// Safe parse +if (MyNumberValue.TryParse("6141-0652-6000", out var parsed)) +{ + Console.WriteLine(parsed.ToString("H")); // 6141-0652-6000 +} + +// Create from first 11 digits (check digit calculated) +var complete = MyNumberValue.FromFirstElevenDigits(new int[] {6,1,4,1,0,6,5,2,6,0,0}); + +// Generate random valid My Number +var random = MyNumberValue.GenerateRandom(); + +// Implicit conversions to `string` and `int[]` +string s = random; // "614106526000" +int[] digits = random; +``` + ### MyNumberNET_ApiServer ASP.NET Core Web API for validating and generating My Numbers.