Add ISBN-10 to ISBN-13 validation and conversion
Enhance NormalizeISBN to validate and convert ISBNs: - Validate length (10 or 13 digits), return error if invalid - Convert ISBN-10 to ISBN-13 by prefixing '978' and recalculating checksum - Add NormalizeISBNSafe for backward compatibility in scanners This ensures all ISBNs stored in database are valid ISBN-13 format.
This commit is contained in:
+52
-1
@@ -1,12 +1,42 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidISBN = errors.New("ISBN must be 10 or 13 digits")
|
||||
)
|
||||
|
||||
// NormalizeISBN removes hyphens and spaces from ISBN to standardize format
|
||||
// Handles ISBN-10 and ISBN-13 formats
|
||||
func NormalizeISBN(isbn string) string {
|
||||
// Returns error if ISBN is not 10 or 13 digits after normalization
|
||||
// Converts ISBN-10 to ISBN-13 by prefixing with "978" and recalculating checksum
|
||||
func NormalizeISBN(isbn string) (string, error) {
|
||||
if isbn == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Remove hyphens and spaces, return only digits and X (for ISBN-10)
|
||||
normalized := regexp.MustCompile(`[-\s]`).ReplaceAllString(isbn, "")
|
||||
|
||||
// Check length: must be 10 or 13 digits
|
||||
length := len(normalized)
|
||||
if length == 10 {
|
||||
// Convert ISBN-10 to ISBN-13
|
||||
return convertISBN10To13(normalized)
|
||||
}
|
||||
if length == 13 {
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
return "", ErrInvalidISBN
|
||||
}
|
||||
|
||||
// NormalizeISBNSafe removes hyphens and spaces from ISBN without validation
|
||||
// Used by scanners where metadata may be incomplete or malformed
|
||||
func NormalizeISBNSafe(isbn string) string {
|
||||
if isbn == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -14,3 +44,24 @@ func NormalizeISBN(isbn string) string {
|
||||
// Remove hyphens and spaces, return only digits and X (for ISBN-10)
|
||||
return regexp.MustCompile(`[-\s]`).ReplaceAllString(isbn, "")
|
||||
}
|
||||
|
||||
// convertISBN10To13 converts ISBN-10 to ISBN-13 by prefixing "978" and recalculating checksum
|
||||
func convertISBN10To13(isbn10 string) (string, error) {
|
||||
// ISBN-10 to ISBN-13: prefix "978" and recalculate checksum
|
||||
// Replace last digit (X becomes 0 for calculation purposes)
|
||||
isbn12 := "978" + isbn10[:9]
|
||||
|
||||
// Calculate ISBN-13 checksum
|
||||
sum := 0
|
||||
for i := 0; i < 12; i++ {
|
||||
digit := int(isbn12[i] - '0')
|
||||
if i%2 == 0 {
|
||||
sum += digit * 1
|
||||
} else {
|
||||
sum += digit * 3
|
||||
}
|
||||
}
|
||||
checksum := (10 - (sum % 10)) % 10
|
||||
|
||||
return isbn12 + string(rune('0'+checksum)), nil
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestNormalizeISBN_ValidISBNs(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
result := NormalizeISBNSafe(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestNormalizeISBN_EdgeCases(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
result := NormalizeISBNSafe(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func TestNormalizeISBN_SpecialCharacters(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
result := NormalizeISBNSafe(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func TestNormalizeISBN_RealWorldExamples(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
result := NormalizeISBNSafe(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func TestNormalizeISBN_DoesNotModifyValidISBNs(t *testing.T) {
|
||||
|
||||
for _, isbn := range validISBNs {
|
||||
t.Run(isbn, func(t *testing.T) {
|
||||
result := NormalizeISBN(isbn)
|
||||
result := NormalizeISBNSafe(isbn)
|
||||
assert.Equal(t, isbn, result, "Valid ISBN should not be modified")
|
||||
})
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func TestNormalizeISBN_PreservesX(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NormalizeISBN(tt.input)
|
||||
result := NormalizeISBNSafe(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeISBN_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid ISBN-13",
|
||||
input: "978-0-306-40615-7",
|
||||
expected: "9780306406157",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid ISBN-10 converts to ISBN-13",
|
||||
input: "0-306-40615-2",
|
||||
expected: "9780306406157",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 with X converts to ISBN-13",
|
||||
input: "0-596-00965-X",
|
||||
expected: "9780596009656",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "11 digits - invalid",
|
||||
input: "97801234567",
|
||||
expected: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "12 digits - invalid",
|
||||
input: "978012345678",
|
||||
expected: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "14 digits - invalid",
|
||||
input: "97801234567890",
|
||||
expected: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "only hyphens",
|
||||
input: "---",
|
||||
expected: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "only text",
|
||||
input: "not-an-isbn",
|
||||
expected: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := NormalizeISBN(tt.input)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertISBN10To13(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isbn10 string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ISBN-10 0-306-40615-2",
|
||||
isbn10: "0306406152",
|
||||
expected: "9780306406157",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 0-596-00965-X (X checksum)",
|
||||
isbn10: "059600965X",
|
||||
expected: "9780596009656",
|
||||
},
|
||||
{
|
||||
name: "ISBN-10 0-8044-2957-X",
|
||||
isbn10: "080442957X",
|
||||
expected: "9780804429573",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := convertISBN10To13(tt.isbn10)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user