Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Semantics.Strings.Identifiers/IsIsbnAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace ktsu.Semantics.Strings.Identifiers;

/// <summary>
/// Validates that the string is an ISBN-10 (weighted mod-11, final digit may be X) or an ISBN-13
/// (mod-10), evaluated on the separator-stripped value. Registration-group and publisher ranges are
/// not validated.
/// (mod-10, with the <c>978</c> or <c>979</c> Bookland prefix), evaluated on the separator-stripped
/// value. Registration-group and publisher ranges are not validated.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class IsIsbnAttribute : NativeSemanticStringValidationAttribute
Expand Down Expand Up @@ -63,6 +63,12 @@ private static bool IsValidIsbn10(string s)

private static bool IsValidIsbn13(string s)
{
// An ISBN-13 is an EAN-13 in the Bookland range; any other prefix is a non-book barcode.
if (!s.StartsWith("978", StringComparison.Ordinal) && !s.StartsWith("979", StringComparison.Ordinal))
{
return false;
}

int sum = 0;
for (int i = 0; i < 13; i++)
{
Expand Down
5 changes: 3 additions & 2 deletions Semantics.Strings.Identifiers/Isbn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ namespace ktsu.Semantics.Strings.Identifiers;
/// <summary>
/// An International Standard Book Number in ISBN-10 or ISBN-13 form, e.g. <c>0306406152</c> or
/// <c>9780306406157</c>. Hyphens and spaces are stripped and the value is uppercased (for the ISBN-10
/// <c>X</c> check digit) on creation. The check digit is validated; registration-group and publisher
/// ranges are not.
/// <c>X</c> check digit) on creation. The check digit is validated, and an ISBN-13 must start with
/// the <c>978</c> or <c>979</c> Bookland prefix; registration-group and publisher ranges are not
/// validated.
/// </summary>
[IsIsbn]
public sealed record Isbn : SemanticString<Isbn>
Expand Down
2 changes: 1 addition & 1 deletion Semantics.Strings.Identifiers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Each type is a `sealed record` deriving from `SemanticString<T>`. None declare t
| `Uuid` | Trim, strip wrapping `{}` / `()`, lowercase | Canonical 8-4-4-4-12 hex (RFC 4122 layout) | Any variant/version accepted, including the nil UUID. Not version-checked. |
| `Ulid` | Trim, uppercase | 26 Crockford base32 chars, first char `0`-`7` | Timestamp is not otherwise decoded. |
| `Iban` | Strip spaces, uppercase | Length 15-34, country/check prefix, ISO 7064 mod-97-10 checksum | Country-specific BBAN structure is not enforced. |
| `Isbn` | Strip `-` and spaces, uppercase | ISBN-10 (weighted mod-11, `X` allowed) or ISBN-13 (mod-10) | Registration-group/publisher ranges are not validated. |
| `Isbn` | Strip `-` and spaces, uppercase | ISBN-10 (weighted mod-11, `X` allowed) or ISBN-13 (`978`/`979` prefix, mod-10) | Registration-group/publisher ranges are not validated. |
| `CreditCardNumber` | Strip spaces and hyphens | 13-19 digits, Luhn (mod-10) checksum | Luhn only. No issuer/network detection, no PCI guarantee. The value is sensitive, do not log it. |
| `JwtToken` | None (stored verbatim, case-sensitive) | Three `.`-separated segments, non-empty header and payload that base64url-decode to UTF-8 text delimited as `{ ... }` | Structural only: the header and payload bodies are not parsed, so malformed JSON between the braces is accepted. Signature is not decoded or verified. `alg`, claims, and expiry are not inspected. |

Expand Down
24 changes: 24 additions & 0 deletions Semantics.Test/Identifiers/IsbnTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ public void Create_BadIsbn13CheckDigit_Throws()
Assert.ThrowsExactly<ArgumentException>(() => Isbn.Create("978-0-306-40615-8"));
}

[TestMethod]
public void Create_ValidIsbn13With979Prefix_Succeeds()
{
Isbn isbn = Isbn.Create("979-10-90636-07-1");
Assert.AreEqual("9791090636071", isbn.WeakString);
}

[TestMethod]
[DataRow("4006381333931")]
[DataRow("5901234123457")]
[DataRow("9770317847001")]
public void Create_Ean13OutsideBooklandRange_Throws(string ean13)
{
Assert.ThrowsExactly<ArgumentException>(() => Isbn.Create(ean13));
}

[TestMethod]
public void TryCreate_Ean13OutsideBooklandRange_ReturnsFalse()
{
bool created = Isbn.TryCreate("4006381333931", out Isbn? result);
Assert.IsFalse(created);
Assert.IsNull(result);
}

[TestMethod]
public void Create_WrongLength_Throws()
{
Expand Down
Loading