-
Notifications
You must be signed in to change notification settings - Fork 1
/
isbn.go
411 lines (347 loc) · 8.67 KB
/
isbn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
package isbn
import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
)
// Version specifies the ISBN version for a particular string.
type Version int
// ISBN versions.
const (
VersionUnknown Version = 0
Version10 Version = 10
Version13 Version = 13
)
// ISBN default prefix.
const (
DefaultPrefix = "978"
)
const (
version10Mod = 11
version13Mod = 10
)
const (
versionXParts = 1
version10Parts = 4
version13Parts = 5
)
const (
prefixLength = 3
groupLength = 5
registrantLength = 5
checkDigitLength = 1
)
const (
version10GroupIdx = iota
version10RegistrantIdx
version10PublicationIdx
version10CheckIdx
)
const (
version13PrefixIdx = iota
version13GroupIdx
version13RegistrantIdx
version13PublicationIdx
version13CheckIdx
)
const headerLength = 7
var (
isbnRegex = regexp.MustCompile(`(?i)(ISBN)?[\d\s\-]+X?`)
// only numbers or final X (for version 10 as a check digit number)
isbnParserRegex = regexp.MustCompile(`(?i)([\dX]+)`)
)
var (
ErrWrongISBN = errors.New("wrong input ISBN format")
)
// ISBN struct defines the core ISBN logic.
type ISBN struct {
originalISBN string
version Version
prefix string
registrationGroup string
registrant string
publication string
checkDigit string
err error
}
// NewISBN function creates ISBN instance based on the input string.
func NewISBN(isbnStr string) (isbn ISBN) {
// check if the string is basic ISBN string
match := isbnRegex.MatchString(isbnStr)
if !match {
isbn.err = ErrWrongISBN
return isbn
}
numbers := isbnParserRegex.FindAllString(isbnStr, -1)
// remove the ISBN version from numbers
if len(numbers) > 1 && (numbers[0] == "13" || numbers[0] == "10") {
numbers = numbers[1:]
}
switch len(numbers) {
case version10Parts:
isbn = ISBN{
version: Version10,
registrationGroup: numbers[version10GroupIdx],
registrant: numbers[version10RegistrantIdx],
publication: numbers[version10PublicationIdx],
checkDigit: numbers[version10CheckIdx],
}
case version13Parts:
isbn = ISBN{
version: Version13,
prefix: numbers[version13PrefixIdx],
registrationGroup: numbers[version13GroupIdx],
registrant: numbers[version13RegistrantIdx],
publication: numbers[version13PublicationIdx],
checkDigit: numbers[version13CheckIdx],
}
case versionXParts:
isbn = parseISBN(numbers[0])
default:
isbn.err = ErrWrongISBN
}
// set original ISBN
isbn.originalISBN = isbnStr
return isbn
}
// IsValid method checks the ISBN value(s) and returns true if the ISBN is valid, otherwise false.
func (isbn ISBN) IsValid() (valid bool) {
if isbn.err != nil || len(isbn.checkDigit) != 1 || len(isbn.originalISBN) < headerLength {
return false
}
originVersion := isbn.getVersionFromOriginal()
if originVersion != VersionUnknown && originVersion != isbn.version {
return false
}
switch isbn.version {
case Version10:
valid = isbn.calculateV10CheckDigit() == isbn.checkDigit
case Version13:
valid = isbn.calculateV13CheckDigit() == isbn.checkDigit
default:
valid = false
}
return valid
}
// Version method returns the current version of ISBN instance.
func (isbn ISBN) Version() Version {
return isbn.version
}
// Normalize method converts ISBN of version 10 into version 13 and/or recalculate the check digital
// which is located at the end of this ISBN.
func (isbn *ISBN) Normalize() {
if isbn.err != nil || isbn.version == Version13 && isbn.IsValid() {
return
}
isbn.prefix = DefaultPrefix
isbn.version = Version13
isbn.checkDigit = isbn.calculateV13CheckDigit()
}
// Error method returns status error.
func (isbn ISBN) Error() error {
return isbn.err
}
// String method creates a human readable format of ISBN.
func (isbn ISBN) String() string {
switch isbn.version {
case Version10:
return fmt.Sprintf("ISBN-%s %s-%s-%s-%s",
isbn.version.String(),
isbn.registrationGroup,
isbn.registrant,
isbn.publication,
isbn.checkDigit)
case Version13:
// we do not need to print version 13, it's implicit
return fmt.Sprintf("ISBN %s-%s-%s-%s-%s",
isbn.prefix,
isbn.registrationGroup,
isbn.registrant,
isbn.publication,
isbn.checkDigit)
default:
return ""
}
}
// BarCode method creates an ISBN code without hyphens between each ISBN part.
func (isbn ISBN) BarCode() string {
return fmt.Sprintf("%s%s%s%s%s",
isbn.prefix, // version 10 has this value empty
isbn.registrationGroup,
isbn.registrant,
isbn.publication,
isbn.checkDigit)
}
// ------------------------------------------------- PRIVATE METHODS -------------------------------------------------
func (isbn ISBN) calculateV13CheckDigit() string {
w := weightFn(isbn.version)
sum := weightSum(isbn.prefix, w)
sum += weightSum(isbn.registrationGroup, w)
sum += weightSum(isbn.registrant, w)
sum += weightSum(isbn.publication, w)
reminder := sum % version13Mod
if reminder == 0 {
reminder = version13Mod
}
return strconv.Itoa(version13Mod - reminder)
}
func (isbn ISBN) calculateV10CheckDigit() string {
w := weightFn(isbn.version)
sum := weightSum(isbn.registrationGroup, w)
sum += weightSum(isbn.registrant, w)
sum += weightSum(isbn.publication, w)
// reminder
digit := version10Mod - (sum % version10Mod)
if digit < 10 {
return strconv.Itoa(digit)
}
// special case when digit == 10
return "X"
}
func (isbn ISBN) getVersionFromOriginal() Version {
if isbn.originalISBN[0] != 'i' && isbn.originalISBN[0] != 'I' {
return VersionUnknown
}
// length of the original ISBN needs to be handled before this function is called
possibleVersion := isbn.originalISBN[:headerLength]
switch possibleVersion {
case "isbn-10", "ISBN-10":
return Version10
// when ISBN without number -> Version 13 expected
case "isbn", "ISBN", "isbn-13", "ISBN-13":
return Version13
default:
return VersionUnknown
}
}
// String method gets the string value of Version type
func (v Version) String() string {
return fmt.Sprintf("%d", v)
}
// ------------------------------------------------ PRIVATE FUNCTIONS-------------------------------------------------
func parseISBN(isbnStr string) (isbn ISBN) {
idx := 0
// load prefix
isbn.prefix, isbn.err = subString(isbnStr, idx, prefixLength)
if isbn.err != nil {
return isbn
}
// set versions and potentially correct prefix
if isbn.prefix != DefaultPrefix {
isbn.prefix = "" // version 10 doesn't have prefix
isbn.version = Version10
} else {
idx += prefixLength
isbn.version = Version13
}
groupLength := parseGroupLength(parseNumber(isbnStr, idx, groupLength))
if groupLength == 0 {
isbn.err = ErrWrongISBN
return isbn
}
isbn.registrationGroup, isbn.err = subString(isbnStr, idx, groupLength)
if isbn.err != nil {
return isbn
}
idx += groupLength
registrantLength := parseRegistrantLength(parseNumber(isbnStr, idx, registrantLength))
if registrantLength == 0 {
isbn.err = ErrWrongISBN
return isbn
}
isbn.registrant, isbn.err = subString(isbnStr, idx, registrantLength)
if isbn.err != nil {
return isbn
}
idx += registrantLength
lastIdx := len(isbnStr) - 1
isbn.publication, isbn.err = subString(isbnStr, idx, lastIdx-idx)
if isbn.err != nil {
return isbn
}
isbn.checkDigit, isbn.err = subString(isbnStr, lastIdx, checkDigitLength)
return isbn
}
func weightFn(version Version) func() int {
switch version {
case Version10:
value := 10
return func() int {
v := value
value--
return v
}
case Version13:
idx := -1
values := []int{1, 3}
return func() int {
idx++
return values[idx%2]
}
default:
return nil
}
}
func weightSum(number string, weight func() int) int {
sum := 0
for _, v := range number {
sum += int(v-'0') * weight()
}
return sum
}
func parseNumber(input string, start, length int) (sum int) {
if len(input) < start+length {
return sum
}
mul := 0
for i := start + length - 1; i >= start; i, mul = i-1, mul+1 {
sum += int(input[i]-'0') * int(math.Pow10(mul))
}
return sum
}
func subString(input string, start, length int) (string, error) {
if len(input) < start+length {
return "", ErrWrongISBN
}
return input[start : start+length], nil
}
func parseGroupLength(group int) int {
switch {
case group < 60000:
return 1
case group < 70000:
return 0
case group < 80000:
return 1
case group < 95000:
return 2
case group < 99000:
return 3
case group < 99900:
return 4
case group < 99999:
return 5
default:
return 0
}
}
func parseRegistrantLength(registrant int) int {
switch {
case registrant < 20000:
return 2
case registrant < 50000:
return 3
case registrant < 89000:
return 4
case registrant < 95000:
return 2
case registrant < 99000:
return 4
case registrant < 100000:
return 5
default:
return 0
}
}