lisbn 0.1.0

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