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.