lisbn 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|