Skip to content

Commit f38c05a

Browse files
committed
feat: add string.isbn keyword for ISBN-10 / ISBN-13 validation
1 parent d075962 commit f38c05a

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

ark/docs/components/dts/type.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

ark/type/__tests__/integration/allConfig.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ configure({
116116
"string.creditCard": {
117117
description: "configured"
118118
},
119+
"string.isbn": {
120+
description: "configured"
121+
},
119122
"string.date.parse": {
120123
description: "configured"
121124
},

ark/type/__tests__/keywords/string.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,44 @@ contextualize(() => {
8888
).snap('must be a credit card number (was "5489582921773370")')
8989
})
9090

91+
it("isbn", () => {
92+
const Isbn = type("string.isbn")
93+
// ISBN-10
94+
attest(Isbn("4873118735")).equals("4873118735")
95+
attest(Isbn("4-87311-873-5")).equals("4-87311-873-5")
96+
attest(Isbn("4 87311 873 5")).equals("4 87311 873 5")
97+
attest(Isbn("020530902X")).equals("020530902X")
98+
attest(Isbn("0-205-30902-X")).equals("0-205-30902-X")
99+
// ISBN-13
100+
attest(Isbn("9784873118734")).equals("9784873118734")
101+
attest(Isbn("978-4-87311-873-4")).equals("978-4-87311-873-4")
102+
attest(Isbn("9790000000001")).equals("9790000000001")
103+
// Invalid checksum
104+
attest(Isbn("4873118736").toString()).equals(
105+
'must be an ISBN (was "4873118736")'
106+
)
107+
attest(Isbn("9784873118735").toString()).equals(
108+
'must be an ISBN (was "9784873118735")'
109+
)
110+
// Lowercase x is not accepted
111+
attest(Isbn("020530902x").toString()).equals(
112+
'must be an ISBN (was "020530902x")'
113+
)
114+
// X in wrong position
115+
attest(Isbn("X234567890").toString()).equals(
116+
'must be an ISBN (was "X234567890")'
117+
)
118+
// Wrong length
119+
attest(Isbn("978487311873").toString()).equals(
120+
'must be an ISBN (was "978487311873")'
121+
)
122+
// Non-digit characters
123+
attest(Isbn("abc1234567890").toString()).equals(
124+
'must be an ISBN (was "abc1234567890")'
125+
)
126+
attest(Isbn("").toString()).equals('must be an ISBN (was "")')
127+
})
128+
91129
it("semver", () => {
92130
attest(keywords.string.semver("1.0.0")).snap("1.0.0")
93131
attest(keywords.string.semver("-1.0.0").toString()).snap(

ark/type/keywords/string.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,44 @@ export const creditCard = rootSchema({
170170
}
171171
})
172172

173+
// https://en.wikipedia.org/wiki/ISBN#ISBN-10_check_digits
174+
export const isIsbn10 = (input: string): boolean => {
175+
let sum = 0
176+
for (let i = 0; i < 10; i++) {
177+
const char = input[i]
178+
const value = char === "X" ? 10 : Number.parseInt(char, 10)
179+
sum += value * (10 - i)
180+
}
181+
return sum % 11 === 0
182+
}
183+
184+
// https://en.wikipedia.org/wiki/ISBN#ISBN-13_check_digit_calculation
185+
export const isIsbn13 = (input: string): boolean => {
186+
let sum = 0
187+
for (let i = 0; i < 13; i++) {
188+
const value = Number.parseInt(input[i], 10)
189+
sum += value * (i % 2 === 0 ? 1 : 3)
190+
}
191+
return sum % 10 === 0
192+
}
193+
194+
const isbnSeparatorMatcher = /[ -]+/g
195+
const isbn10Matcher = /^\d{9}[\dX]$/
196+
const isbn13Matcher = /^\d{13}$/
197+
198+
export const isbn = rootSchema({
199+
domain: "string",
200+
predicate: {
201+
meta: "an ISBN",
202+
predicate: (input: string) => {
203+
const sanitized = input.replace(isbnSeparatorMatcher, "")
204+
if (isbn10Matcher.test(sanitized)) return isIsbn10(sanitized)
205+
if (isbn13Matcher.test(sanitized)) return isIsbn13(sanitized)
206+
return false
207+
}
208+
}
209+
})
210+
173211
type DayDelimiter = "." | "/" | "-"
174212

175213
const dayDelimiterMatcher = /^[./-]$/
@@ -919,6 +957,7 @@ export const string = Scope.module(
919957
email,
920958
integer: stringInteger,
921959
ip,
960+
isbn,
922961
json,
923962
lower,
924963
normalize,
@@ -953,6 +992,7 @@ export declare namespace string {
953992
email: string
954993
integer: stringInteger.submodule
955994
ip: ip.submodule
995+
isbn: string
956996
json: stringJson.submodule
957997
lower: lower.submodule
958998
normalize: normalize.submodule

0 commit comments

Comments
 (0)