Skip to content

Commit 64add57

Browse files
authored
Merge pull request #106 from hsaito/mynumber-object
Add MyNumberValue type: enforce My Number format, API improvements, tests
2 parents 54f93bb + 1d55789 commit 64add57

5 files changed

Lines changed: 980 additions & 0 deletions

File tree

MyNumberNET/MyNumber.cs

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Text;
36

47
namespace MyNumberNET
58
{
@@ -103,4 +106,299 @@ public MyNumberMalformedException(string message)
103106

104107
#endregion
105108
}
109+
110+
/// <summary>
111+
/// Represents a valid My Number that enforces correct format and provides type safety.
112+
/// This is an immutable value type that ensures the My Number is always in a valid state.
113+
/// </summary>
114+
public readonly struct MyNumberValue : IEquatable<MyNumberValue>, IFormattable
115+
{
116+
private readonly int[] _digits;
117+
118+
/// <summary>
119+
/// Gets the 12-digit array representation of this My Number.
120+
/// </summary>
121+
public int[] Digits => (int[])_digits?.Clone() ?? throw new InvalidOperationException("MyNumberValue is not initialized.");
122+
123+
/// <summary>
124+
/// Gets whether this MyNumberValue instance has been properly initialized.
125+
/// </summary>
126+
public bool IsInitialized => _digits != null;
127+
128+
/// <summary>
129+
/// Initializes a new instance of MyNumberValue from a 12-digit array.
130+
/// </summary>
131+
/// <param name="digits">A 12-digit array representing a valid My Number.</param>
132+
/// <exception cref="MyNumber.MyNumberMalformedException">Thrown when the digits don't represent a valid My Number.</exception>
133+
public MyNumberValue(int[] digits)
134+
{
135+
if (!MyNumber.VerifyNumber(digits))
136+
{
137+
throw new MyNumber.MyNumberMalformedException("The provided digits do not represent a valid My Number.");
138+
}
139+
_digits = (int[])digits.Clone();
140+
}
141+
142+
/// <summary>
143+
/// Initializes a new instance of MyNumberValue from individual digit parameters.
144+
/// </summary>
145+
/// <param name="d1">First digit</param>
146+
/// <param name="d2">Second digit</param>
147+
/// <param name="d3">Third digit</param>
148+
/// <param name="d4">Fourth digit</param>
149+
/// <param name="d5">Fifth digit</param>
150+
/// <param name="d6">Sixth digit</param>
151+
/// <param name="d7">Seventh digit</param>
152+
/// <param name="d8">Eighth digit</param>
153+
/// <param name="d9">Ninth digit</param>
154+
/// <param name="d10">Tenth digit</param>
155+
/// <param name="d11">Eleventh digit</param>
156+
/// <param name="d12">Twelfth digit (check digit)</param>
157+
/// <exception cref="MyNumber.MyNumberMalformedException">Thrown when the digits don't represent a valid My Number.</exception>
158+
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)
159+
: this(new[] { d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12 })
160+
{
161+
}
162+
163+
/// <summary>
164+
/// Creates a MyNumberValue from the first 11 digits, automatically calculating the check digit.
165+
/// </summary>
166+
/// <param name="firstElevenDigits">The first 11 digits of the My Number.</param>
167+
/// <returns>A valid MyNumberValue with the calculated check digit.</returns>
168+
/// <exception cref="MyNumber.MyNumberMalformedException">Thrown when the input is invalid.</exception>
169+
public static MyNumberValue FromFirstElevenDigits(int[] firstElevenDigits)
170+
{
171+
if (firstElevenDigits == null || firstElevenDigits.Length != 11)
172+
{
173+
throw new MyNumber.MyNumberMalformedException("Must provide exactly 11 digits.");
174+
}
175+
176+
var checkDigit = MyNumber.CalculateCheckDigits(firstElevenDigits);
177+
var allDigits = new int[12];
178+
Array.Copy(firstElevenDigits, allDigits, 11);
179+
allDigits[11] = checkDigit;
180+
181+
return new MyNumberValue(allDigits);
182+
}
183+
184+
/// <summary>
185+
/// Attempts to parse a string representation of a My Number.
186+
/// </summary>
187+
/// <param name="value">String containing 12 digits (may include separators like spaces or hyphens).</param>
188+
/// <param name="result">The parsed MyNumberValue if successful.</param>
189+
/// <returns>True if parsing was successful, false otherwise.</returns>
190+
public static bool TryParse(string value, out MyNumberValue result)
191+
{
192+
result = default;
193+
194+
if (string.IsNullOrWhiteSpace(value))
195+
return false;
196+
197+
// Remove common separators
198+
var cleanValue = value.Replace(" ", "").Replace("-", "").Replace("_", "");
199+
200+
if (cleanValue.Length != 12)
201+
return false;
202+
203+
var digits = new int[12];
204+
for (int i = 0; i < 12; i++)
205+
{
206+
if (!char.IsDigit(cleanValue[i]))
207+
return false;
208+
digits[i] = cleanValue[i] - '0';
209+
}
210+
211+
try
212+
{
213+
result = new MyNumberValue(digits);
214+
return true;
215+
}
216+
catch (MyNumber.MyNumberMalformedException)
217+
{
218+
return false;
219+
}
220+
}
221+
222+
/// <summary>
223+
/// Parses a string representation of a My Number.
224+
/// </summary>
225+
/// <param name="value">String containing 12 digits (may include separators like spaces or hyphens).</param>
226+
/// <returns>A valid MyNumberValue.</returns>
227+
/// <exception cref="ArgumentException">Thrown when the string cannot be parsed as a valid My Number.</exception>
228+
public static MyNumberValue Parse(string value)
229+
{
230+
if (TryParse(value, out var result))
231+
return result;
232+
233+
throw new ArgumentException($"Unable to parse '{value}' as a valid My Number.", nameof(value));
234+
}
235+
236+
/// <summary>
237+
/// Generates a random valid My Number.
238+
/// </summary>
239+
/// <returns>A randomly generated valid MyNumberValue.</returns>
240+
public static MyNumberValue GenerateRandom()
241+
{
242+
var generator = new MyNumber();
243+
var digits = generator.GenerateRandomNumber();
244+
return new MyNumberValue(digits);
245+
}
246+
247+
/// <summary>
248+
/// Returns the string representation of this My Number.
249+
/// </summary>
250+
/// <returns>A 12-digit string representation.</returns>
251+
public override string ToString()
252+
{
253+
return ToString("N", CultureInfo.InvariantCulture);
254+
}
255+
256+
/// <summary>
257+
/// Returns the string representation of this My Number with the specified format.
258+
/// </summary>
259+
/// <param name="format">
260+
/// Format string:
261+
/// "N" or null = no separators (default): "123456789012"
262+
/// "S" = with spaces: "1234 5678 9012"
263+
/// "H" = with hyphens: "1234-5678-9012"
264+
/// "G" = grouped format: "1234-5678-901-2"
265+
/// </param>
266+
/// <returns>Formatted string representation.</returns>
267+
public string ToString(string format)
268+
{
269+
return ToString(format, CultureInfo.InvariantCulture);
270+
}
271+
272+
/// <summary>
273+
/// Returns the string representation of this My Number with the specified format and format provider.
274+
/// </summary>
275+
/// <param name="format">Format string (see ToString(string) for options).</param>
276+
/// <param name="formatProvider">Format provider (currently not used).</param>
277+
/// <returns>Formatted string representation.</returns>
278+
public string ToString(string format, IFormatProvider formatProvider)
279+
{
280+
if (!IsInitialized)
281+
throw new InvalidOperationException("MyNumberValue is not initialized.");
282+
283+
var digitString = string.Join("", _digits);
284+
285+
return format?.ToUpperInvariant() switch
286+
{
287+
null or "N" => digitString,
288+
"S" => $"{digitString.Substring(0, 4)} {digitString.Substring(4, 4)} {digitString.Substring(8, 4)}",
289+
"H" => $"{digitString.Substring(0, 4)}-{digitString.Substring(4, 4)}-{digitString.Substring(8, 4)}",
290+
"G" => $"{digitString.Substring(0, 4)}-{digitString.Substring(4, 4)}-{digitString.Substring(8, 3)}-{digitString.Substring(11, 1)}",
291+
_ => throw new FormatException($"Format string '{format}' is not supported.")
292+
};
293+
}
294+
295+
/// <summary>
296+
/// Determines whether this instance is equal to another MyNumberValue.
297+
/// </summary>
298+
/// <param name="other">The other MyNumberValue to compare.</param>
299+
/// <returns>True if equal, false otherwise.</returns>
300+
public bool Equals(MyNumberValue other)
301+
{
302+
if (!IsInitialized && !other.IsInitialized)
303+
return true;
304+
if (!IsInitialized || !other.IsInitialized)
305+
return false;
306+
307+
return _digits.SequenceEqual(other._digits);
308+
}
309+
310+
/// <summary>
311+
/// Determines whether this instance is equal to the specified object.
312+
/// </summary>
313+
/// <param name="obj">The object to compare.</param>
314+
/// <returns>True if equal, false otherwise.</returns>
315+
public override bool Equals(object obj)
316+
{
317+
return obj is MyNumberValue other && Equals(other);
318+
}
319+
320+
/// <summary>
321+
/// Gets the hash code for this MyNumberValue.
322+
/// </summary>
323+
/// <returns>A hash code for this instance.</returns>
324+
public override int GetHashCode()
325+
{
326+
if (!IsInitialized)
327+
return 0;
328+
329+
unchecked
330+
{
331+
int hash = 17;
332+
foreach (var digit in _digits)
333+
{
334+
hash = hash * 31 + digit;
335+
}
336+
return hash;
337+
}
338+
}
339+
340+
/// <summary>
341+
/// Determines whether two MyNumberValue instances are equal.
342+
/// </summary>
343+
/// <param name="left">The first MyNumberValue.</param>
344+
/// <param name="right">The second MyNumberValue.</param>
345+
/// <returns>True if equal, false otherwise.</returns>
346+
public static bool operator ==(MyNumberValue left, MyNumberValue right)
347+
{
348+
return left.Equals(right);
349+
}
350+
351+
/// <summary>
352+
/// Determines whether two MyNumberValue instances are not equal.
353+
/// </summary>
354+
/// <param name="left">The first MyNumberValue.</param>
355+
/// <param name="right">The second MyNumberValue.</param>
356+
/// <returns>True if not equal, false otherwise.</returns>
357+
public static bool operator !=(MyNumberValue left, MyNumberValue right)
358+
{
359+
return !left.Equals(right);
360+
}
361+
362+
/// <summary>
363+
/// Implicitly converts a MyNumberValue to an int array.
364+
/// </summary>
365+
/// <param name="myNumber">The MyNumberValue to convert.</param>
366+
/// <returns>A 12-digit int array.</returns>
367+
public static implicit operator int[](MyNumberValue myNumber)
368+
{
369+
return myNumber.Digits;
370+
}
371+
372+
/// <summary>
373+
/// Implicitly converts a MyNumberValue to a string.
374+
/// </summary>
375+
/// <param name="myNumber">The MyNumberValue to convert.</param>
376+
/// <returns>A 12-digit string representation.</returns>
377+
public static implicit operator string(MyNumberValue myNumber)
378+
{
379+
return myNumber.ToString();
380+
}
381+
382+
/// <summary>
383+
/// Explicitly converts an int array to a MyNumberValue.
384+
/// </summary>
385+
/// <param name="digits">The 12-digit array to convert.</param>
386+
/// <returns>A MyNumberValue instance.</returns>
387+
/// <exception cref="MyNumber.MyNumberMalformedException">Thrown when the array is not a valid My Number.</exception>
388+
public static explicit operator MyNumberValue(int[] digits)
389+
{
390+
return new MyNumberValue(digits);
391+
}
392+
393+
/// <summary>
394+
/// Explicitly converts a string to a MyNumberValue.
395+
/// </summary>
396+
/// <param name="value">The string to convert.</param>
397+
/// <returns>A MyNumberValue instance.</returns>
398+
/// <exception cref="ArgumentException">Thrown when the string is not a valid My Number.</exception>
399+
public static explicit operator MyNumberValue(string value)
400+
{
401+
return Parse(value);
402+
}
403+
}
106404
}

0 commit comments

Comments
 (0)