Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ISBN validation for ISBN-10 and ISBN-13 #323

Closed
Closed
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
1 change: 1 addition & 0 deletions library/src/validations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './integer/integer.ts';
export * from './ip/index.ts';
export * from './ipv4/index.ts';
export * from './ipv6/index.ts';
export * from './isbn/index.ts';
export * from './isoDate/index.ts';
export * from './isoDateTime/index.ts';
export * from './isoTime/index.ts';
Expand Down
1 change: 1 addition & 0 deletions library/src/validations/isbn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './isbn.ts';
93 changes: 93 additions & 0 deletions library/src/validations/isbn/isbn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, test } from 'vitest';
import { isbn } from './isbn.ts';

describe('isbn', () => {
test('should pass only valid ISBN-10', () => {
// ISBN-10
const validate = isbn(10);

const value1 = '0510652689';
expect(validate._parse(value1).output).toBe(value1);
const value2 = '1617290858';
expect(validate._parse(value2).output).toBe(value2);
const value3 = '0007269706';
expect(validate._parse(value3).output).toBe(value3);
const value4 = '3423214120';
expect(validate._parse(value4).output).toBe(value4);
const value5 = '340101319X';
expect(validate._parse(value5).output).toBe(value5);
const value6 = '3-8362-2119-5';
expect(validate._parse(value6).output).toBe(value6);
const value7 = '1-61729-085-8';
expect(validate._parse(value7).output).toBe(value7);
const value8 = '0-00-726970-6';
expect(validate._parse(value8).output).toBe(value8);
const value9 = '3-423-21412-0';
expect(validate._parse(value9).output).toBe(value9);
const value10 = '3-401-01319-X';
expect(validate._parse(value10).output).toBe(value10);
});

test('should reject invalid ISBN-10', () => {
const validate = isbn(10);

expect(validate._parse('').issues).toBeTruthy();
expect(validate._parse('3423214121').issues).toBeTruthy();
expect(validate._parse('978-3836221191').issues).toBeTruthy();
expect(validate._parse('123456789a').issues).toBeTruthy();
expect(validate._parse('978-3836221191').issues).toBeTruthy();
expect(validate._parse('3-423-21412-1').issues).toBeTruthy();
expect(validate._parse('9783836221191').issues).toBeTruthy();
expect(validate._parse('3 423 21412 1').issues).toBeTruthy();
expect(validate._parse('foo test').issues).toBeTruthy();
});

test('should pass only valid ISBN-13', () => {
const validate2 = isbn(13);

const value11 = '9783836221191';
expect(validate2._parse(value11).output).toBe(value11);
const value12 = '9783401013190';
expect(validate2._parse(value12).output).toBe(value12);
const value13 = '9784873113685';
expect(validate2._parse(value13).output).toBe(value13);
const value14 = '978-3-8362-2119-1';
expect(validate2._parse(value14).output).toBe(value14);
const value15 = '978-3401013190';
expect(validate2._parse(value15).output).toBe(value15);
const value16 = '978-4-87311-368-5';
expect(validate2._parse(value16).output).toBe(value16);
const value17 = '978 3 8362 2119 1';
expect(validate2._parse(value17).output).toBe(value17);
const value18 = '978 3401013190';
expect(validate2._parse(value18).output).toBe(value18);
const value19 = '978 4 87311 368 5';
expect(validate2._parse(value19).output).toBe(value19);
});

test('should reject invalid ISBN-13', () => {
const validate2 = isbn(13);

expect(validate2._parse('').issues).toBeTruthy();
expect(validate2._parse('9783836221190').issues).toBeTruthy();
expect(validate2._parse('3836221195').issues).toBeTruthy();
expect(validate2._parse('01234567890ab').issues).toBeTruthy();
expect(validate2._parse('978-3-8362-2119-0').issues).toBeTruthy();
expect(validate2._parse('3-8362-2119-5').issues).toBeTruthy();
expect(validate2._parse('978 3 8362 2119 0').issues).toBeTruthy();
expect(validate2._parse('3 8362 2119 5').issues).toBeTruthy();
expect(validate2._parse('foo test').issues).toBeTruthy();
});

test('should return custom error message for ISBN-10', () => {
const error = 'Value is not an ISBN-10!';
const validate = isbn(10, error);
expect(validate._parse('test').issues?.[0].message).toBe(error);
});

test('should return custom error message for ISBN-13', () => {
const error = 'Value is not an ISBN-13!';
const validate = isbn(13, error);
expect(validate._parse('test').issues?.[0].message).toBe(error);
});
});
75 changes: 75 additions & 0 deletions library/src/validations/isbn/isbn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { BaseValidation, ErrorMessage } from '../../types/index.ts';
import { actionIssue, actionOutput } from '../../utils/index.ts';

/**
* ISBN validation type.
*/
export type IsbnValidation<
TInput extends string,
TVersion extends 10 | 13
> = BaseValidation<TInput> & {
/**
* The validation type.
*/
type: 'isbn';
/**
* The ISBN validation function.
*/
requirement: (input: TInput, version: TVersion) => boolean;
};

/**
* Creates a pipeline validation action that validates an [ISBN](https://en.wikipedia.org/wiki/ISBN).
*
* @param version Version could be 10 for ISBN-10 and 13 for ISBN-13.
* @param message The error message
*
* @returns A validation action.
*/
export function isbn<TInput extends string, TVersion extends 10 | 13>(
version: TVersion,
message: ErrorMessage = 'Invalid ISBN'
): IsbnValidation<TInput, TVersion> {
return {
type: 'isbn',
async: false,
message,
requirement: isISBN,
_parse(input) {
return !this.requirement(input, version)
? actionIssue(this.type, this.message, input, this.requirement)
: actionOutput(input);
},
};
}

const ISBN10_REGEX = /^\d{9}[\dX]$/u;
const ISBN13_REGEX = /^\d{13}$/u;

function isISBN(isbn: string, version: 10 | 13): boolean {
const sanitizedIsbn = isbn.replace(/[\s-]+/gu, '');
if (version === 10 && ISBN10_REGEX.test(sanitizedIsbn)) {
return isISBN10(sanitizedIsbn);
}
if (version === 13 && ISBN13_REGEX.test(sanitizedIsbn)) {
return isISBN13(sanitizedIsbn);
}
return false;
}

function isISBN10(isbn: string): boolean {
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += (i + 1) * parseInt(isbn[i]);
}
sum += isbn[9] === 'X' ? 10 * 10 : 10 * parseInt(isbn[9]);
return sum % 11 === 0;
}

function isISBN13(isbn: string): boolean {
let sum = 0;
for (let i = 0; i < 13; i++) {
sum += parseInt(isbn[i]) * (i % 2 === 0 ? 1 : 3);
}
return sum % 10 === 0;
}
9 changes: 9 additions & 0 deletions website/src/routes/api/(validations)/isbn/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: isbn
contributors:
- ariskemper
---

# isbn

> The content of this page is not yet ready. Until then just use the [source code](https://github.com/fabian-hiller/valibot/blob/main/library/src/validations/isbn/isbn.ts).
1 change: 1 addition & 0 deletions website/src/routes/api/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
- [ip](/api/ip/)
- [ipv4](/api/ipv4/)
- [ipv6](/api/ipv6/)
- [isbn](/api/isbn/)
- [isoDate](/api/isoDate/)
- [isoDateTime](/api/isoDateTime/)
- [isoTime](/api/isoTime/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Pipeline validation actions examine the input and, if the input does not meet a
'ip',
'ipv4',
'ipv6',
'isbn',
'isoDate',
'isoDateTime',
'isoTime',
Expand Down