Skip to content

Commit ee99a56

Browse files
committed
Refactor exceptions to use InvalidCharacterException.
Replaced `ArgumentException` with the new `InvalidCharacterException` for better semantic clarity when handling invalid input. Added `InvalidCharacterException` class and updated related tests accordingly to align with these changes. Updated the changelog to document the addition. Resolves: No entry
1 parent eafd58f commit ee99a56

File tree

4 files changed

+133
-30
lines changed

4 files changed

+133
-30
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Add `InvalidCharacterException` to throw an exception if the input string contains invalid characters.
10+
811
### Changed
912
- Renamed `Luhn.ConvertAlphaNumericToNumeric` to `Luhn.AlphaNumericToNumeric`
1013

src/InvalidCharacterException.cs

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// ----------------------------------------------------------------------------
2+
// <copyright file="InvalidCharacterException.cs" company="Private">
3+
// Copyright (c) 2025 All Rights Reserved
4+
// </copyright>
5+
// <author>Sebastian Walther</author>
6+
// <date>01/01/2025 09:00:26 PM</date>
7+
// ----------------------------------------------------------------------------
8+
9+
#region License
10+
11+
// ----------------------------------------------------------------------------
12+
// Copyright 2025 Sebastian Walther
13+
//
14+
// Permission is hereby granted, free of charge, to any person obtaining a copy
15+
// of this software and associated documentation files (the "Software"), to deal
16+
// in the Software without restriction, including without limitation the rights
17+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18+
// copies of the Software, and to permit persons to whom the Software is
19+
// furnished to do so, subject to the following conditions:
20+
//
21+
// The above copyright notice and this permission notice shall be included in
22+
// all copies or substantial portions of the Software.
23+
//
24+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30+
// THE SOFTWARE.
31+
32+
#endregion
33+
34+
using System;
35+
using System.Runtime.Serialization;
36+
37+
namespace LuhnDotNet
38+
{
39+
/// <summary>
40+
/// The exception that is thrown when an invalid character is encountered within an argument.
41+
/// </summary>
42+
[Serializable]
43+
public class InvalidCharacterException : ArgumentException
44+
{
45+
/// <summary>
46+
/// Initializes a new instance of the <see cref="InvalidCharacterException"/> class.
47+
/// </summary>
48+
public InvalidCharacterException() : base("Invalid character encountered in argument.")
49+
{
50+
}
51+
52+
/// <summary>
53+
/// Initializes a new instance of the <see cref="InvalidCharacterException"/> class with a specified error message.
54+
/// </summary>
55+
/// <param name="message">The message that describes the error.</param>
56+
public InvalidCharacterException(string message) : base(message)
57+
{
58+
}
59+
60+
/// <summary>
61+
/// Initializes a new instance of the <see cref="InvalidCharacterException"/> class with a specified error message
62+
/// and the name of the parameter that caused this exception.
63+
/// </summary>
64+
/// <param name="message">The error message that explains the reason for the exception.</param>
65+
/// <param name="paramName">The name of the parameter that caused the current exception.</param>
66+
public InvalidCharacterException(string message, string paramName) : base(message, paramName)
67+
{
68+
}
69+
70+
/// <summary>
71+
/// Initializes a new instance of the <see cref="InvalidCharacterException"/> class with a specified error message,
72+
/// the parameter name, and a reference to the inner exception that is the cause of this exception.
73+
/// </summary>
74+
/// <param name="message">The error message that explains the reason for the exception.</param>
75+
/// <param name="paramName">The name of the parameter that caused the current exception.</param>
76+
/// <param name="innerException">The exception that is the cause of the current exception
77+
/// or a null reference if no inner exception is specified.</param>
78+
public InvalidCharacterException(string message, string paramName, Exception innerException) : base(message, paramName, innerException)
79+
{
80+
}
81+
82+
/// <summary>
83+
/// Initializes a new instance of the <see cref="InvalidCharacterException"/> class with serialized data.
84+
/// </summary>
85+
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
86+
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
87+
[Obsolete(
88+
"This constructor is obsolete and will be removed in a future version. Use InvalidCharacterException(string message, string paramName, Exception innerException) instead.",
89+
false)]
90+
protected InvalidCharacterException(SerializationInfo info, StreamingContext context) : base(info, context)
91+
{
92+
}
93+
}
94+
}

src/Luhn.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ public static string AlphaNumericToNumeric(this string alphaNumeric)
268268
}
269269
else
270270
{
271-
throw new ArgumentException($"The character '{c}' is not a letter or a digit!", nameof(alphaNumeric));
271+
throw new InvalidCharacterException($"The character '{c}' is not a letter or a digit!", nameof(alphaNumeric));
272272
}
273273
}
274274

@@ -290,7 +290,7 @@ public static string AlphaNumericToNumeric(this string alphaNumeric)
290290
}
291291
else
292292
{
293-
throw new ArgumentException($"The character '{c}' is not a letter or a digit!", nameof(alphaNumeric));
293+
throw new InvalidCharacterException($"The character '{c}' is not a letter or a digit!", nameof(alphaNumeric));
294294
}
295295
}
296296

@@ -346,7 +346,7 @@ private static string ValidateAndTrimNumber(this string number)
346346
string trimmedNumber = number?.Trim();
347347
if (string.IsNullOrWhiteSpace(trimmedNumber) || !Regex.IsMatch(trimmedNumber, @"^\d+$"))
348348
{
349-
throw new ArgumentException($"The string '{number}' is not a number!", nameof(number));
349+
throw new InvalidCharacterException($"The string '{number}' is not a number!", nameof(number));
350350
}
351351

352352
return trimmedNumber;
@@ -365,7 +365,7 @@ private static ReadOnlySpan<char> ValidateAndTrimNumber(this ReadOnlySpan<char>
365365
var trimmedNumber = number.Trim();
366366
if (trimmedNumber.Length == 0 || !trimmedNumber.IsDigits())
367367
{
368-
throw new ArgumentException($"The string '{number}' is not a number!", nameof(number));
368+
throw new InvalidCharacterException($"The string '{number}' is not a number!", nameof(number));
369369
}
370370

371371
return trimmedNumber;

tests/LuhnTest.cs

+32-26
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ public void ComputeLuhnNumber_ValidRawNumber_ReturnsExpectedLuhnNumber(
183183
/// </summary>
184184
/// <param name="expectedResult">The expected validation result</param>
185185
/// <param name="luhnNumber">Test number inclusive check digit</param>
186-
[Theory(DisplayName = "Validates a number containing a check digit")]
186+
[Theory(DisplayName = "Validates a valid Luhn number")]
187187
[MemberData(nameof(LuhnNumberValidationSet), MemberType = typeof(LuhnTest))]
188-
public void LuhnNumberValidationTest(bool expectedResult, string luhnNumber)
188+
public void IsValidLuhnNumber_ValidLuhnNumber_ReturnsExpectedResult(bool expectedResult, string luhnNumber)
189189
{
190190
Assert.Equal(expectedResult, luhnNumber.IsValidLuhnNumber());
191191
Assert.Equal(expectedResult, luhnNumber.AsSpan().IsValidLuhnNumber());
@@ -197,9 +197,12 @@ public void LuhnNumberValidationTest(bool expectedResult, string luhnNumber)
197197
/// <param name="expectedResult">Expected validation result</param>
198198
/// <param name="number">Test number exclusive check digit</param>
199199
/// <param name="checkDigit">Test Luhn check digit</param>
200-
[Theory(DisplayName = "Validates a number with separate check digit between 0 and 9")]
200+
[Theory(DisplayName = "Validates a valid number with separate, valid check digit between 0 and 9")]
201201
[MemberData(nameof(LuhnCheckDigitValidationSet), MemberType = typeof(LuhnTest))]
202-
public void LuhnCheckDigitValidationTest(bool expectedResult, string number, byte checkDigit)
202+
public void IsValidLuhnCheckDigit_ValidNumberAndCheckDigit_ReturnsExpectedResult(
203+
bool expectedResult,
204+
string number,
205+
byte checkDigit)
203206
{
204207
Assert.Equal(expectedResult, checkDigit.IsValidLuhnCheckDigit(number));
205208
Assert.Equal(expectedResult, checkDigit.IsValidLuhnCheckDigit(number.AsSpan()));
@@ -212,10 +215,10 @@ public void LuhnCheckDigitValidationTest(bool expectedResult, string number, byt
212215
/// <param name="invalidNumber">Invalid raw number</param>
213216
[Theory(DisplayName = "Calculates the check digit for an invalid raw number to throw an exception")]
214217
[MemberData(nameof(InvalidNumbers), MemberType = typeof(LuhnTest))]
215-
public void ComputeLuhnCheckDigit_InvalidRawNumber_ThrowsArgumentException(string invalidNumber)
218+
public void ComputeLuhnCheckDigit_InvalidRawNumber_ThrowsInvalidCharacterException(string invalidNumber)
216219
{
217-
Assert.Throws<ArgumentException>(() => invalidNumber.ComputeLuhnCheckDigit());
218-
Assert.Throws<ArgumentException>(() => invalidNumber.AsSpan().ComputeLuhnCheckDigit());
220+
Assert.Throws<InvalidCharacterException>(() => invalidNumber.ComputeLuhnCheckDigit());
221+
Assert.Throws<InvalidCharacterException>(() => invalidNumber.AsSpan().ComputeLuhnCheckDigit());
219222
}
220223

221224
/// <summary>
@@ -225,10 +228,10 @@ public void ComputeLuhnCheckDigit_InvalidRawNumber_ThrowsArgumentException(strin
225228
/// <param name="invalidNumber">Invalid raw number</param>
226229
[Theory(DisplayName = "Calculates the Luhn number for an invalid raw number to throw an exception")]
227230
[MemberData(nameof(InvalidNumbers), MemberType = typeof(LuhnTest))]
228-
public void ComputeLuhnNumber_InvalidRawNumber_ThrowsArgumentException(string invalidNumber)
231+
public void ComputeLuhnNumber_InvalidRawNumber_ThrowsInvalidCharacterException(string invalidNumber)
229232
{
230-
Assert.Throws<ArgumentException>(invalidNumber.ComputeLuhnNumber);
231-
Assert.Throws<ArgumentException>(() => invalidNumber.AsSpan().ComputeLuhnNumber());
233+
Assert.Throws<InvalidCharacterException>(invalidNumber.ComputeLuhnNumber);
234+
Assert.Throws<InvalidCharacterException>(() => invalidNumber.AsSpan().ComputeLuhnNumber());
232235
}
233236

234237
/// <summary>
@@ -237,22 +240,23 @@ public void ComputeLuhnNumber_InvalidRawNumber_ThrowsArgumentException(string in
237240
/// </summary>
238241
[Theory(DisplayName = "Validates an invalid Luhn number (e.g. none-numeric characters) to throw an exception")]
239242
[MemberData(nameof(InvalidNumbers), MemberType = typeof(LuhnTest))]
240-
public void LuhnNumberValidationExceptionTest(string invalidNumber)
243+
public void IsValidLuhnNumber_InvalidInput_ThrowsInvalidCharacterException(string invalidNumber)
241244
{
242-
Assert.Throws<ArgumentException>(() => invalidNumber.IsValidLuhnNumber());
243-
Assert.Throws<ArgumentException>(() => invalidNumber.AsSpan().IsValidLuhnNumber());
245+
Assert.Throws<InvalidCharacterException>(() => invalidNumber.IsValidLuhnNumber());
246+
Assert.Throws<InvalidCharacterException>(() => invalidNumber.AsSpan().IsValidLuhnNumber());
244247
}
245248

246249
/// <summary>
247-
/// Tests whether an exception is thrown when an invalid number and a valid check digit between 0 and 9
248-
/// is passed to the Luhn validation algorithm.
250+
/// Tests whether an exception is thrown when an invalid number is passed to the Luhn validation algorithm.
249251
/// </summary>
250252
[Theory(DisplayName = "Validates an invalid number with any check digit between 0 and 9 to throw an exception")]
251253
[MemberData(nameof(InvalidNumbersAndCheckDigits), MemberType = typeof(LuhnTest))]
252-
public void NumberValidationExceptionTest(string invalidNumber, byte checkDigit)
254+
public void IsValidLuhnCheckDigit_InvalidInput_ThrowsInvalidCharacterException(
255+
string invalidNumber,
256+
byte checkDigit)
253257
{
254-
Assert.Throws<ArgumentException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber));
255-
Assert.Throws<ArgumentException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber.AsSpan()));
258+
Assert.Throws<InvalidCharacterException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber));
259+
Assert.Throws<InvalidCharacterException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber.AsSpan()));
256260
}
257261

258262
/// <summary>
@@ -261,7 +265,9 @@ public void NumberValidationExceptionTest(string invalidNumber, byte checkDigit)
261265
/// </summary>
262266
[Theory(DisplayName = "Validates a number with separate check digit greater than 9 to throw an exception")]
263267
[MemberData(nameof(NumbersWithInvalidCheckDigits), MemberType = typeof(LuhnTest))]
264-
public void LuhnCheckDigitValidationExceptionTest(string invalidNumber, byte checkDigit)
268+
public void IsValidLuhnCheckDigit_InvalidInput_ThrowsArgumentOutOfRangeException(
269+
string invalidNumber,
270+
byte checkDigit)
265271
{
266272
Assert.Throws<ArgumentOutOfRangeException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber));
267273
Assert.Throws<ArgumentOutOfRangeException>(() => checkDigit.IsValidLuhnCheckDigit(invalidNumber.AsSpan()));
@@ -270,7 +276,7 @@ public void LuhnCheckDigitValidationExceptionTest(string invalidNumber, byte che
270276
/// <summary>
271277
/// Test data for AlphaNumericToNumeric method.
272278
/// </summary>
273-
public static IEnumerable<object[]> ConvertAlphaNumericToNumericData =>
279+
public static IEnumerable<object[]> AlphaNumericToNumericData =>
274280
new List<object[]>
275281
{
276282
new object[] { "A1B2C3", "101112123" },
@@ -287,8 +293,8 @@ public void LuhnCheckDigitValidationExceptionTest(string invalidNumber, byte che
287293
/// <param name="input">Input string</param>
288294
/// <param name="expected">Expected output</param>
289295
[Theory(DisplayName = "Converts an alphanumeric string to a numeric string")]
290-
[MemberData(nameof(ConvertAlphaNumericToNumericData), MemberType = typeof(LuhnTest))]
291-
public void ConvertAlphaNumericToNumeric_ShouldReturnExpectedResult(string input, string expected)
296+
[MemberData(nameof(AlphaNumericToNumericData), MemberType = typeof(LuhnTest))]
297+
public void AlphaNumericToNumeric_ValidInput_ShouldReturnExpectedResult(string input, string expected)
292298
{
293299
Assert.Equal(expected, input.AlphaNumericToNumeric());
294300
}
@@ -302,9 +308,9 @@ public void ConvertAlphaNumericToNumeric_ShouldReturnExpectedResult(string input
302308
/// xUnit to check if the expected exception is thrown.
303309
/// </remarks>
304310
[Fact(DisplayName = "Converts an invalid alphanumeric string to a numeric string to throw an exception")]
305-
public void ConvertAlphaNumericToNumeric_InvalidInput_ThrowsArgumentException()
311+
public void AlphaNumericToNumeric_InvalidInput_ThrowsInvalidCharacterException()
306312
{
307-
Assert.Throws<ArgumentException>(()=> "!@#$%^&*()".AlphaNumericToNumeric());
313+
Assert.Throws<InvalidCharacterException>(()=> "!@#$%^&*()".AlphaNumericToNumeric());
308314
}
309315

310316
/// <summary>
@@ -328,7 +334,7 @@ public void ConvertAlphaNumericToNumeric_InvalidInput_ThrowsArgumentException()
328334
/// <param name="expected">Expected output</param>
329335
[Theory(DisplayName = "Validates a numeric string converted from an alphanumeric string")]
330336
[MemberData(nameof(IsValidWithConvertData), MemberType = typeof(LuhnTest))]
331-
public void IsValidWithConvertTest(string input, bool expected)
337+
public void IsValidLuhnNumber_WithAlphaNumericToNumeric_ReturnsExpectedCheckDigit(string input, bool expected)
332338
{
333339
Assert.Equal(expected, input.AlphaNumericToNumeric().IsValidLuhnNumber());
334340
Assert.Equal(expected, input.AlphaNumericToNumeric().AsSpan().IsValidLuhnNumber());
@@ -366,7 +372,7 @@ public void IsValidWithConvertTest(string input, bool expected)
366372
/// </remarks>
367373
[Theory(DisplayName = "Calculates the check digit for a valid alphanumeric string")]
368374
[MemberData(nameof(ComputeLuhnCheckDigitWithConvertData), MemberType = typeof(LuhnTest))]
369-
public void ComputeLuhnCheckDigit_WithConvertAlphaNumericToNumeric_ReturnsExpectedCheckDigit(
375+
public void ComputeLuhnCheckDigit_WithAlphaNumericToNumeric_ReturnsExpectedCheckDigit(
370376
string input,
371377
byte expected)
372378
{

0 commit comments

Comments
 (0)