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.
@@ -0,0 +1,3 @@
1
+ require 'nori'
2
+ require "lisbn/cache_method"
3
+ require "lisbn/lisbn"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'lisbn'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ end
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