lisbn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/Rakefile +5 -0
- data/data/RangeMessage.xml +5280 -0
- data/lib/lisbn.rb +3 -0
- data/lib/lisbn/cache_method.rb +15 -0
- data/lib/lisbn/lisbn.rb +135 -0
- data/lisbn.gemspec +19 -0
- data/spec/cache_method_spec.rb +27 -0
- data/spec/lisbn_spec.rb +135 -0
- data/spec/spec_helper.rb +7 -0
- metadata +108 -0
data/lib/lisbn.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class Lisbn < String
|
2
|
+
module CacheMethod
|
3
|
+
def cache_method(*methods)
|
4
|
+
methods.map(&:to_s).each do |method|
|
5
|
+
alias_method method + "_without_cache", method
|
6
|
+
define_method method do |*args, &blk|
|
7
|
+
@cache ||= {}
|
8
|
+
@cache[[method, self]] ||= send(method + "_without_cache", *args, &blk)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
extend CacheMethod
|
15
|
+
end
|
data/lib/lisbn/lisbn.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
class Lisbn < String
|
2
|
+
# Returns a normalized ISBN form
|
3
|
+
def isbn
|
4
|
+
upcase.gsub(/[^0-9X]/, '')
|
5
|
+
end
|
6
|
+
|
7
|
+
# Returns true if the ISBN is valid, false otherwise.
|
8
|
+
def valid?
|
9
|
+
case isbn.length
|
10
|
+
when 10
|
11
|
+
valid_isbn_10?
|
12
|
+
when 13
|
13
|
+
valid_isbn_13?
|
14
|
+
else
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a valid ISBN in ISBN-10 format.
|
20
|
+
# Returns nil if the ISBN is invalid or incapable of conversion to ISBN-10.
|
21
|
+
def isbn10
|
22
|
+
return unless valid?
|
23
|
+
return isbn if isbn.length == 10
|
24
|
+
return unless isbn[0..2] == "978"
|
25
|
+
|
26
|
+
isbn[3..-2] + isbn_10_checksum
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a valid ISBN in ISBN-13 format.
|
30
|
+
# Returns nil if the ISBN is invalid.
|
31
|
+
def isbn13
|
32
|
+
return unless valid?
|
33
|
+
return isbn if isbn.length == 13
|
34
|
+
|
35
|
+
'978' + isbn[0..-2] + isbn_13_checksum
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an Array with the 'parts' of the ISBN-13 in left-to-right order.
|
39
|
+
# The parts of an ISBN are as follows:
|
40
|
+
# - GS1 prefix
|
41
|
+
# - Group identifier
|
42
|
+
# - Prefix/publisher code
|
43
|
+
# - Item number
|
44
|
+
# - Check digit
|
45
|
+
#
|
46
|
+
# Returns nil if the ISBN is not valid.
|
47
|
+
# Returns nil if the group and prefix cannot be identified.
|
48
|
+
def parts
|
49
|
+
return unless isbn13
|
50
|
+
|
51
|
+
group = prefix = nil
|
52
|
+
|
53
|
+
RANGES.each_pair do |g, prefixes|
|
54
|
+
next unless isbn13.match("^#{g}")
|
55
|
+
group = g
|
56
|
+
|
57
|
+
pre_loc = group.length
|
58
|
+
prefixes.each do |p|
|
59
|
+
number = isbn13.slice(pre_loc, p[:length]).to_i
|
60
|
+
next unless p[:range].include?(number)
|
61
|
+
|
62
|
+
prefix = p.merge(:number => number)
|
63
|
+
break
|
64
|
+
end
|
65
|
+
|
66
|
+
break
|
67
|
+
end
|
68
|
+
|
69
|
+
# In the unlikely event we can't categorize it...
|
70
|
+
return unless group && prefix
|
71
|
+
|
72
|
+
prefix = sprintf("%0#{prefix[:length]}d", prefix[:number])
|
73
|
+
[group[0..2], group[3..-1], prefix, isbn13[(group.length + prefix.length)..-2], isbn13[-1..-1]]
|
74
|
+
end
|
75
|
+
|
76
|
+
cache_method :isbn, :valid?, :isbn10, :isbn13, :parts
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def isbn_10_checksum
|
81
|
+
base = isbn.length == 13 ? isbn[3..-2] : isbn[0..-2]
|
82
|
+
|
83
|
+
products = base.each_char.each_with_index.map do |chr, i|
|
84
|
+
chr.to_i * (10 - i)
|
85
|
+
end
|
86
|
+
|
87
|
+
remainder = products.inject(0) {|m, v| m + v} % 11
|
88
|
+
case remainder
|
89
|
+
when 0
|
90
|
+
0
|
91
|
+
when 1
|
92
|
+
'X'
|
93
|
+
else
|
94
|
+
11 - remainder
|
95
|
+
end.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
def isbn_13_checksum
|
99
|
+
base = (isbn.length == 13 ? '' : '978') + isbn[0..-2]
|
100
|
+
|
101
|
+
products = base.each_char.each_with_index.map do |chr, i|
|
102
|
+
chr.to_i * (i % 2 == 0 ? 1 : 3)
|
103
|
+
end
|
104
|
+
|
105
|
+
remainder = products.inject(0) {|m, v| m + v} % 10
|
106
|
+
(remainder == 0 ? 0 : 10 - remainder).to_s
|
107
|
+
end
|
108
|
+
|
109
|
+
def valid_isbn_10?
|
110
|
+
return false unless isbn.match(/^[0-9]{9}[0-9X]$/)
|
111
|
+
isbn[-1..-1] == isbn_10_checksum
|
112
|
+
end
|
113
|
+
|
114
|
+
def valid_isbn_13?
|
115
|
+
return false unless isbn.match(/^[0-9]{13}$/)
|
116
|
+
isbn[-1..-1] == isbn_13_checksum
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.ranges
|
120
|
+
rngs = Nori.parse(File.read(File.dirname(__FILE__) + '/../../data/RangeMessage.xml'))
|
121
|
+
Array(rngs["ISBNRangeMessage"]["RegistrationGroups"]["Group"]).flatten.inject({}) do |memo, group|
|
122
|
+
prefix = group["Prefix"].gsub(/-/, '')
|
123
|
+
ranges = Array(group["Rules"]["Rule"]).flatten.map do |rule|
|
124
|
+
length = rule["Length"].to_i
|
125
|
+
next unless length > 0
|
126
|
+
|
127
|
+
{:range => Range.new(*rule["Range"].split("-").map {|r| r[0..(length - 1)].to_i }), :length => length}
|
128
|
+
end.compact
|
129
|
+
|
130
|
+
memo.update(prefix => ranges)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
RANGES = ranges
|
135
|
+
end
|
data/lisbn.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |gem|
|
3
|
+
gem.authors = ["Mike Ragalie"]
|
4
|
+
gem.email = ["michael.ragalie@verbasoftware.com"]
|
5
|
+
gem.description = %q{ISBN manipulation helpers}
|
6
|
+
gem.summary = %q{Provides methods for converting between ISBN-10 and ISBN-13,
|
7
|
+
checking validity and splitting ISBNs into groups and prefixes}
|
8
|
+
gem.homepage = ""
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($\)
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "lisbn"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = "0.1.0"
|
16
|
+
|
17
|
+
gem.add_dependency "nori"
|
18
|
+
gem.add_development_dependency "rspec"
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class String
|
4
|
+
extend Lisbn::CacheMethod
|
5
|
+
|
6
|
+
def with_excitement!
|
7
|
+
self + "!"
|
8
|
+
end
|
9
|
+
|
10
|
+
cache_method :with_excitement!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "cache_method" do
|
14
|
+
subject { String.new("awesomeness") }
|
15
|
+
|
16
|
+
it "evaluates the method the first time but not subsequent times" do
|
17
|
+
subject.should_receive(:+).with("!").once.and_return("you got stubbed!")
|
18
|
+
subject.with_excitement!
|
19
|
+
subject.with_excitement!.should == "you got stubbed!"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "reevaluates the method if the object's hash changes" do
|
23
|
+
subject.with_excitement!.should == "awesomeness!"
|
24
|
+
subject.replace("more awesomeness")
|
25
|
+
subject.with_excitement!.should == "more awesomeness!"
|
26
|
+
end
|
27
|
+
end
|
data/spec/lisbn_spec.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Lisbn" do
|
4
|
+
describe "#isbn" do
|
5
|
+
it "converts the string to just digits and X" do
|
6
|
+
isbn = Lisbn.new("9487-028asdfasdf878X7")
|
7
|
+
isbn.isbn.should == "9487028878X7"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#valid?" do
|
12
|
+
it "recognizes a valid ISBN10" do
|
13
|
+
isbn = Lisbn.new("0123456789")
|
14
|
+
isbn.valid?.should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
it "recognizes a valid ISBN10 with X checksum" do
|
18
|
+
isbn = Lisbn.new("160459411X")
|
19
|
+
isbn.valid?.should be_true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "recognizes a valid ISBN10 with 0 checksum" do
|
23
|
+
isbn = Lisbn.new("0679405070")
|
24
|
+
isbn.valid?.should be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "recognizes an invalid ISBN10" do
|
28
|
+
isbn = Lisbn.new("0123546789")
|
29
|
+
isbn.valid?.should be_false
|
30
|
+
end
|
31
|
+
|
32
|
+
it "recognizes a valid ISBN13" do
|
33
|
+
isbn = Lisbn.new("9780000000002")
|
34
|
+
isbn.valid?.should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
it "recognizes a valid ISBN13 with 0 checksum" do
|
38
|
+
isbn = Lisbn.new("9780062870780")
|
39
|
+
isbn.valid?.should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "recognizes an invalid ISBN13" do
|
43
|
+
isbn = Lisbn.new("9780000000003")
|
44
|
+
isbn.valid?.should be_false
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns false for improperly-formatted ISBNs" do
|
48
|
+
isbn = Lisbn.new("97800000X0002")
|
49
|
+
isbn.valid?.should be_false
|
50
|
+
end
|
51
|
+
|
52
|
+
it "regards anything not 10 or 13 digits as invalid" do
|
53
|
+
isbn = Lisbn.new("")
|
54
|
+
isbn.valid?.should be_false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#isbn10" do
|
59
|
+
subject { Lisbn.new("9780000000002") }
|
60
|
+
|
61
|
+
it "returns nil if invalid" do
|
62
|
+
subject.stub(:valid? => false)
|
63
|
+
subject.isbn10.should be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns nil if the ISBN is 13-digits and isn't in the 978 GS1" do
|
67
|
+
lisbn = Lisbn.new("9790000000003")
|
68
|
+
lisbn.stub(:valid? => true)
|
69
|
+
lisbn.isbn10.should be_nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it "computes the ISBN10 checksum" do
|
73
|
+
subject.isbn10.should == "0000000000"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "returns the isbn if it's 10 digits" do
|
77
|
+
lisbn = Lisbn.new("0000000000")
|
78
|
+
lisbn.stub(:valid? => true)
|
79
|
+
lisbn.should_not_receive(:isbn_10_checksum)
|
80
|
+
lisbn.isbn10.should == "0000000000"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#isbn13" do
|
85
|
+
subject { Lisbn.new("0000000000") }
|
86
|
+
|
87
|
+
it "returns nil if invalid" do
|
88
|
+
subject.stub(:valid? => false)
|
89
|
+
subject.isbn13.should be_nil
|
90
|
+
end
|
91
|
+
|
92
|
+
it "computes the ISBN13 checksum" do
|
93
|
+
subject.isbn13.should == "9780000000002"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "returns the isbn if it's 13 digits" do
|
97
|
+
lisbn = Lisbn.new("9780000000002")
|
98
|
+
lisbn.stub(:valid? => true)
|
99
|
+
lisbn.should_not_receive(:isbn_13_checksum)
|
100
|
+
lisbn.isbn13.should == "9780000000002"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#parts" do
|
105
|
+
subject { Lisbn.new("9780000000002") }
|
106
|
+
|
107
|
+
it "splits into the right groups" do
|
108
|
+
subject.parts.should == ["978", "0", "00", "000000", "2"]
|
109
|
+
end
|
110
|
+
|
111
|
+
it "works with long groups" do
|
112
|
+
lisbn = Lisbn.new("9786017002015")
|
113
|
+
lisbn.parts.should == ["978", "601", "7002", "01", "5"]
|
114
|
+
end
|
115
|
+
|
116
|
+
it "returns nil if it can't find a valid group" do
|
117
|
+
lisbn = Lisbn.new("9780100000002")
|
118
|
+
lisbn.parts.should be_nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "ranges" do
|
123
|
+
it "skips over invalid '0-length' ranges" do
|
124
|
+
Lisbn::RANGES.values.flatten.map {|v| v[:length]}.should_not include(0)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "retains normal string methods" do
|
129
|
+
subject { Lisbn.new("9780000000002") }
|
130
|
+
|
131
|
+
it "#splits" do
|
132
|
+
subject.split("7").should == ["9", "80000000002"]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lisbn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Mike Ragalie
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-05-12 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: nori
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
description: ISBN manipulation helpers
|
49
|
+
email:
|
50
|
+
- michael.ragalie@verbasoftware.com
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- .gitignore
|
59
|
+
- .rspec
|
60
|
+
- Gemfile
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- data/RangeMessage.xml
|
65
|
+
- lib/lisbn.rb
|
66
|
+
- lib/lisbn/cache_method.rb
|
67
|
+
- lib/lisbn/lisbn.rb
|
68
|
+
- lisbn.gemspec
|
69
|
+
- spec/cache_method_spec.rb
|
70
|
+
- spec/lisbn_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
homepage: ""
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.24
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Provides methods for converting between ISBN-10 and ISBN-13, checking validity and splitting ISBNs into groups and prefixes
|
105
|
+
test_files:
|
106
|
+
- spec/cache_method_spec.rb
|
107
|
+
- spec/lisbn_spec.rb
|
108
|
+
- spec/spec_helper.rb
|