@@ -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 } [ \d X ] $ /
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+
173211type DayDelimiter = "." | "/" | "-"
174212
175213const 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