Line data Source code
1 : // Copyright 2026 The Authors
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'package:characters/characters.dart';
16 :
17 : import 'digit_string.dart' show DigitString;
18 :
19 : /// An International Standard Book Number (ISBN).
20 : ///
21 : /// See:
22 : ///
23 : /// - [ISBN Users' Manual](https://www.isbn-international.org/content/isbn-users-manual/29)
24 : /// - [OCLC International Standard Book Number](https://www.oclc.org/bibformats/en/0xx/020.html)
25 : /// - [Administration of the ISBN System](https://isbn-information.com/administration-of-the-isbn-system.html)
26 : class Isbn13 {
27 2 : static final gsiPrefixes = const ['978', '979'];
28 :
29 : final DigitString isbn;
30 :
31 : /// The GS1 (formerly EAN) Element (e.g., '978' or '979').
32 0 : DigitString get prefix => isbn.substring(0, 3);
33 :
34 : /// The ISBN 13 check digit.
35 0 : DigitString get checkDigit => isbn.valueAt(isbn.length - 1);
36 :
37 1 : Isbn13._(this.isbn);
38 :
39 1 : @override
40 2 : String toString() => isbn.toString();
41 :
42 1 : static DigitString? _extractISBNString(String input) {
43 3 : if (input.characters.length > 22) {
44 : return null;
45 : }
46 :
47 1 : var str = DigitString.extract(input);
48 :
49 2 : return str.length == 13 ? str : null;
50 : }
51 :
52 1 : static Isbn13? tryParse(String input) {
53 1 : var isbn = _extractISBNString(input);
54 :
55 : if (isbn == null) {
56 : return null;
57 : }
58 :
59 1 : if (!_validatePrefix(isbn)) {
60 : return null;
61 : }
62 :
63 1 : if (!validateChecksum(isbn)) {
64 : return null;
65 : }
66 :
67 1 : return Isbn13._(isbn);
68 : }
69 :
70 1 : static int? calculateIsbn13CheckDigit(DigitString input) {
71 : const weights = [1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3];
72 :
73 1 : var inputList = input.intList;
74 :
75 2 : if (input.length != 13) {
76 : return null;
77 : }
78 :
79 : var sumProd = 0;
80 2 : for (var i = 0; i < 12; i++) {
81 4 : sumProd += inputList[i] * weights[i];
82 : }
83 3 : return (10 - sumProd % 10) % 10;
84 : }
85 :
86 1 : static bool _validatePrefix(DigitString isbn) {
87 2 : final prefix = isbn.substring(0, 3).toString();
88 :
89 2 : if (!gsiPrefixes.contains(prefix)) {
90 : return false;
91 : }
92 : return true;
93 : }
94 :
95 : /// If [input] is a valid ISBN, returns `true`.
96 : ///
97 : /// Refer to
98 : /// [APPENDIX 1 Check digit calculation](https://www.isbn-international.org/content/isbn-users-manual/29)
99 1 : static bool validateChecksum(DigitString isbn) {
100 2 : if (isbn.length != 13) {
101 : return false;
102 : }
103 :
104 1 : var checkDigit = isbn.valueAt(12);
105 :
106 3 : return calculateIsbn13CheckDigit(isbn) == checkDigit.intValue;
107 : }
108 :
109 2 : static bool isValid(String value) => tryParse(value) != null;
110 : }
|