lc_callnumber 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/lc_callnumber/lcc.rb +137 -60
- data/lib/lc_callnumber/version.rb +1 -1
- data/test/test_lc_callnumber.rb +25 -5
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d173a4ec0a1b4ba26c0beec642a3f66b845c67d4
|
4
|
+
data.tar.gz: 41a42a08153cb57f4f0c775794cbf72c56890369
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3aa66113e6ac592735e4bfdba100b7b174635054dc26b0d136d749329b48df6957233bef6d426e50d07ab794a0e010b23cdcbd6f04f1ce52703d28ad9b53fe1e
|
7
|
+
data.tar.gz: 686dc9b45975782c5c426af6430e0bec8d08c21fa41a6aff5dd4b7b768a2871fd51d8bfb48f1a43bb8fe0b1c36e286db1319131c35f272f586d8b96c2899cf43
|
data/README.md
CHANGED
@@ -32,7 +32,7 @@ For purposes of this class, an LC Call Number consists of the following parts. O
|
|
32
32
|
|
33
33
|
* __Letter(s).__ One or more letters
|
34
34
|
* __Digit(s).__ One or more digits, optionally with a decimal point.
|
35
|
-
* __Doon1 (_Date Or Other Number_)__. Relatively rare as these things go, a DOON is used to represent the date the work is _about_ (as opposed to, say, the year it was published) or, in some cases, an identifier for an army division or group (say, "12th" for the US Army Twelfth Infantry).
|
35
|
+
* __Doon1 (_Date Or Other Number_)__. [Note: no one but me calls it a 'doon', but I needed a term and there wasn't another]. Relatively rare as these things go, a DOON is used to represent the date the work is _about_ (as opposed to, say, the year it was published) or, in some cases, an identifier for an army division or group (say, "12th" for the US Army Twelfth Infantry).
|
36
36
|
* __First cutter__. The first "Cutter Number" consisting of a letter followed by one or more digits. The first cutter is always supposed to be preceded by a dot, but, you know, isn't always.
|
37
37
|
* __Doon2__. Another date or other number
|
38
38
|
* __"Extra" Cutters__. The 2nd through Nth Cutter numbers, lumped together because we don't have to worry about them getting interspersed with doons.
|
data/lib/lc_callnumber/lcc.rb
CHANGED
@@ -4,7 +4,7 @@ module LCCallNumber
|
|
4
4
|
|
5
5
|
@parser = Parser.new
|
6
6
|
@transformer = Transform.new
|
7
|
-
|
7
|
+
|
8
8
|
def self.parse(i)
|
9
9
|
return UnparseableCallNumber.new(i) if i.nil?
|
10
10
|
i.chomp!
|
@@ -13,13 +13,13 @@ module LCCallNumber
|
|
13
13
|
i.gsub!(/\[(.+)\]/, "$1")
|
14
14
|
i.gsub!('\\', ' ')
|
15
15
|
|
16
|
-
# We also have a bunch that start with a slash and have slashes where you'd
|
16
|
+
# We also have a bunch that start with a slash and have slashes where you'd
|
17
17
|
# expect logical breaks. Don't know why.
|
18
|
-
|
18
|
+
|
19
19
|
if i =~ /\A\//
|
20
20
|
i.gsub!('/', ' ')
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
# Ditch leading + or *
|
24
24
|
i.gsub!(/\A[+*]/, '')
|
25
25
|
|
@@ -35,7 +35,7 @@ module LCCallNumber
|
|
35
35
|
!(/\d/.match i)
|
36
36
|
return UnparseableCallNumber.new(orig)
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
begin
|
40
40
|
p = @parser.parse(i)
|
41
41
|
# puts p
|
@@ -46,12 +46,12 @@ module LCCallNumber
|
|
46
46
|
return UnparseableCallNumber.new(orig)
|
47
47
|
end
|
48
48
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
|
55
55
|
|
56
56
|
# The "lead" -- initial letter(s) and digit(s)
|
57
57
|
class Lead
|
@@ -64,28 +64,28 @@ module LCCallNumber
|
|
64
64
|
"Lead<#{letters}, #{digits.inspect}>"
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
68
|
-
# A generic "Decimal" part, where we keep the
|
67
|
+
|
68
|
+
# A generic "Decimal" part, where we keep the
|
69
69
|
# integer and fractional parts separate so
|
70
70
|
# we can construct a string more easily
|
71
|
-
|
71
|
+
|
72
72
|
class Decimal
|
73
73
|
attr_accessor :intpart, :fractpart
|
74
74
|
def initialize(i, f=nil)
|
75
75
|
@intpart = i
|
76
76
|
@fractpart = f
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
def to_num
|
80
80
|
fractpart ? "#{intpart}.#{fractpart}".to_f : intpart.to_i
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
def to_s
|
84
84
|
fractpart ? "#{intpart}.#{fractpart}" : intpart
|
85
85
|
end
|
86
|
-
|
87
|
-
|
88
|
-
def inspect
|
86
|
+
|
87
|
+
|
88
|
+
def inspect
|
89
89
|
if fractpart
|
90
90
|
"#{intpart}:#{fractpart}"
|
91
91
|
else
|
@@ -93,36 +93,36 @@ module LCCallNumber
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
# A "Cutter" is a single letter followed by one or more digits
|
98
98
|
# It is sometimes preceded by a decimal point. We note whether or not
|
99
99
|
# we got a decimal point because it may be a hint as to whether or not
|
100
100
|
# we're actually looking at a Cutter.
|
101
|
-
|
101
|
+
|
102
102
|
class Cutter
|
103
103
|
attr_accessor :letter, :digits, :dot
|
104
104
|
def initialize(l,d, dot=nil)
|
105
|
-
@letter = l
|
106
|
-
@digits = d
|
105
|
+
@letter = l.to_s
|
106
|
+
@digits = d.to_i
|
107
107
|
@dot = !dot.nil?
|
108
108
|
end
|
109
|
-
|
109
|
+
|
110
110
|
def inspect
|
111
111
|
"Cut<#{letter},#{digits}>"
|
112
112
|
end
|
113
|
-
|
113
|
+
|
114
114
|
def to_s
|
115
115
|
"#{letter}#{digits}"
|
116
116
|
end
|
117
117
|
end
|
118
|
-
|
119
|
-
|
118
|
+
|
119
|
+
|
120
120
|
# Some call numbers have a year, infantry division, etc. preceeding
|
121
121
|
# and/or following the first cutter. We take all three as a unit so
|
122
122
|
# we can deal with edge cases more easily.
|
123
123
|
#
|
124
124
|
# "doon" in this case is "Date Or Other Number"
|
125
|
-
|
125
|
+
|
126
126
|
class FirstCutterSet
|
127
127
|
attr_accessor :doon1, :cutter, :doon2
|
128
128
|
def initialize(d1, cutter, d2)
|
@@ -131,84 +131,161 @@ module LCCallNumber
|
|
131
131
|
@cutter = cutter
|
132
132
|
end
|
133
133
|
end
|
134
|
-
|
134
|
+
|
135
135
|
# Bring it all together in the callnumber
|
136
|
-
# We feed it the parts we have, and then reassign fields based on
|
136
|
+
# We feed it the parts we have, and then reassign fields based on
|
137
137
|
# what we have.
|
138
138
|
#
|
139
139
|
# To wit:
|
140
140
|
# Q123 .C4 1990 --- published in 1990
|
141
141
|
# Q123 .C4 1990 .D5 2003 -- published in 2003 with a 2nd doon of 1990
|
142
|
-
|
143
|
-
|
142
|
+
|
143
|
+
|
144
144
|
class CallNumber
|
145
|
-
|
146
|
-
|
145
|
+
|
146
|
+
include Comparable
|
147
|
+
attr_accessor :letters, :digits, :doon1, :firstcutter, :doon2,
|
147
148
|
:extra_cutters, :year, :rest
|
148
149
|
attr_accessor :original_string
|
149
|
-
|
150
|
-
|
150
|
+
|
151
|
+
|
151
152
|
def initialize(lead, fcset=nil, ec=[], year=nil, rest=nil)
|
152
|
-
@letters = lead.letters
|
153
|
+
@letters = lead.letters.to_s.upcase
|
153
154
|
@digits = lead.digits.to_num
|
154
|
-
|
155
|
+
|
155
156
|
if fcset
|
156
157
|
@doon1 = fcset.doon1
|
157
158
|
@doon2 = fcset.doon2
|
158
159
|
@firstcutter = fcset.cutter
|
159
160
|
end
|
160
|
-
|
161
|
+
|
161
162
|
@extra_cutters = ec
|
162
163
|
@year = year
|
163
164
|
@rest = rest
|
164
|
-
|
165
|
+
|
165
166
|
@rest.strip! if @rest
|
166
|
-
|
167
|
+
|
167
168
|
self.reassign_fields
|
168
169
|
end
|
169
|
-
|
170
|
+
|
170
171
|
def reassign_fields
|
171
172
|
# If we've got a doon2, but no year and no extra cutters,
|
172
173
|
# put the doon2 in the year
|
173
|
-
|
174
|
+
|
174
175
|
if doon2 and (doon2=~ /\A\d+\Z/) and extra_cutters.empty? and year.nil?
|
175
176
|
@year = @doon2
|
176
177
|
@doon2 = nil
|
177
178
|
end
|
178
179
|
end
|
179
|
-
|
180
|
+
|
180
181
|
def inspect
|
181
182
|
header = %w(letters digits doon1 cut1 doon2 othercut)
|
182
183
|
actual = [letters,digits,doon1,firstcutter,doon2,extra_cutters.join(','),year,rest].map{|x| x.to_s}
|
183
184
|
fmt = '%-7s %-9s %-6s %-6s %-6s %-20s year=%-6s rest=%-s'
|
184
185
|
[('%-7s %-9s %-6s %-6s %-6s %-s' % header), (fmt % actual)].join("\n")
|
185
|
-
|
186
|
+
|
186
187
|
end
|
187
|
-
|
188
|
-
|
189
|
-
def valid?
|
188
|
+
|
189
|
+
|
190
|
+
def valid?
|
190
191
|
@letters && @digits
|
191
192
|
end
|
192
|
-
|
193
|
+
|
194
|
+
def <=>(other)
|
195
|
+
# Make call numbers sortable.
|
196
|
+
# Work through the various parts, from left to right.
|
197
|
+
# (They compare as strings or numbers as appropriate, thanks to the above.)
|
198
|
+
|
199
|
+
letters_cmp = letters <=> other.letters
|
200
|
+
return letters_cmp unless letters_cmp == 0
|
201
|
+
|
202
|
+
digits_cmp = digits <=> other.digits
|
203
|
+
return digits_cmp unless digits_cmp == 0
|
204
|
+
|
205
|
+
doon1_cmp = doon1 <=> other.doon1
|
206
|
+
return doon1_cmp unless doon1_cmp == 0
|
207
|
+
|
208
|
+
# Cutters have letters and numbers, which we need to compare separately.
|
209
|
+
if firstcutter
|
210
|
+
if other.firstcutter
|
211
|
+
# Both have firstcutters! Hurrah!
|
212
|
+
# Compare the letter
|
213
|
+
firstcutter_cmp = firstcutter.letter <=> other.firstcutter.letter
|
214
|
+
# If that didn't help, compare the digits
|
215
|
+
if firstcutter_cmp == 0
|
216
|
+
firstcutter_cmp = firstcutter.digits <=> other.firstcutter.digits
|
217
|
+
end
|
218
|
+
# If that didn't help, if other has an extra_cutter, it is last
|
219
|
+
if firstcutter_cmp == 0
|
220
|
+
firstcutter_cmp = -1 if (extra_cutters.empty? && ! other.extra_cutters.empty?)
|
221
|
+
end
|
222
|
+
else
|
223
|
+
# self has a firstcutter but other doesn't, so other is first
|
224
|
+
firstcutter_cmp = 1
|
225
|
+
end
|
226
|
+
else
|
227
|
+
# self has no firstcutter
|
228
|
+
if other.firstcutter
|
229
|
+
# other does, so it comes last
|
230
|
+
firstcutter_cmp = -1
|
231
|
+
else
|
232
|
+
# Neither has one, so carry on
|
233
|
+
firstcutter_cmp = 0
|
234
|
+
end
|
235
|
+
end
|
236
|
+
return firstcutter_cmp unless firstcutter_cmp == 0
|
237
|
+
|
238
|
+
doon2_cmp = doon2 <=> other.doon2
|
239
|
+
return doon2_cmp unless doon2_cmp == 0
|
240
|
+
|
241
|
+
# The extra_cutters are an array, so we need to compare each.
|
242
|
+
# Surely there is an easier way to do this?
|
243
|
+
extra_cutters_cmp = 0
|
244
|
+
# STDERR.puts extra_cutters
|
245
|
+
extra_cutters.each_index do |i|
|
246
|
+
if extra_cutters_cmp == 0 # Stop when we need go no further
|
247
|
+
# First, compare the letter
|
248
|
+
if other.extra_cutters[i]
|
249
|
+
extra_cutters_cmp = extra_cutters[i].letter <=> other.extra_cutters[i].letter
|
250
|
+
# If that didn't help, compare the digits
|
251
|
+
if extra_cutters_cmp == 0
|
252
|
+
extra_cutters_cmp = extra_cutters[i].digits <=> other.extra_cutters[i].digits
|
253
|
+
end
|
254
|
+
# If that didn't help, if self has no more extra_cutters but other does, self is first
|
255
|
+
if extra_cutters_cmp == 0
|
256
|
+
extra_cutters_cmp = -1 if (extra_cutters[i+1].nil? && other.extra_cutters[i+1])
|
257
|
+
end
|
258
|
+
else
|
259
|
+
# other has no extra_cutter, so it is first
|
260
|
+
extra_cutters_cmp = 1
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
return extra_cutters_cmp unless extra_cutters_cmp == 0
|
265
|
+
|
266
|
+
year_cmp = year <=> other.year
|
267
|
+
return year_cmp unless year_cmp == 0
|
268
|
+
|
269
|
+
rest_cmp = rest <=> other.rest
|
270
|
+
return rest_cmp
|
271
|
+
|
272
|
+
end
|
273
|
+
|
193
274
|
end
|
194
|
-
|
275
|
+
|
195
276
|
class UnparseableCallNumber < CallNumber
|
196
277
|
|
197
278
|
def initialize(str)
|
198
279
|
self.original_string = str
|
199
280
|
self.extra_cutters = []
|
200
281
|
end
|
201
|
-
|
202
|
-
def valid?
|
282
|
+
|
283
|
+
def valid?
|
203
284
|
false
|
204
285
|
end
|
205
|
-
|
286
|
+
|
206
287
|
end
|
207
|
-
|
208
|
-
|
209
|
-
|
288
|
+
|
289
|
+
|
290
|
+
|
210
291
|
end
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
data/test/test_lc_callnumber.rb
CHANGED
@@ -22,11 +22,31 @@ describe "Basics" do
|
|
22
22
|
assert_equal other_cutters.to_s.split(/\s*,\s*/), lc.extra_cutters.map(&:to_s), "#{orig} -> extra_cutters #{lc.inspect}"
|
23
23
|
assert_equal pubyear.to_s, lc.year.to_s, "#{orig} -> year #{lc.inspect}"
|
24
24
|
assert_equal rest.to_s, lc.rest.to_s, "#{orig} -> rest #{lc.inspect}"
|
25
|
-
|
25
|
+
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "Sorting" do
|
32
|
+
it "sorts call numbers properly" do
|
33
|
+
a1 = LCCallNumber.parse("A 50")
|
34
|
+
a2 = LCCallNumber.parse("A 7")
|
35
|
+
q1 = LCCallNumber.parse("QA 500")
|
36
|
+
q2 = LCCallNumber.parse("QA 500.M500")
|
37
|
+
q3 = LCCallNumber.parse("QA 500.M500 T59")
|
38
|
+
q4 = LCCallNumber.parse("QA 500.M500 T60")
|
39
|
+
q5 = LCCallNumber.parse("QA 500.M500 T60 A1")
|
40
|
+
q6 = LCCallNumber.parse("QA 500.M500 T60 Z54")
|
41
|
+
assert (a1 <=> a1) == 0 # a1 is a1
|
42
|
+
assert (a1 <=> a2) == 1 # a1 > a2
|
43
|
+
assert (a2 <=> q1) == -1 # a2 < q1
|
44
|
+
assert (q1 <=> q2) == -1 # q1 < q2
|
45
|
+
assert (q2 <=> q3) == -1 # q2 < q3
|
46
|
+
assert (q3 <=> q3) == 0 # q3 is q3
|
47
|
+
assert (q3 <=> q4) == -1 # q3 < q4
|
48
|
+
assert (q4 <=> q5) == -1 # q4 < q5
|
49
|
+
assert (q5 <=> q6) == -1 # q5 < q6
|
50
|
+
# Is there some way to put some test numbers into an array, sort it, and compare the original and sorted arrays?
|
28
51
|
end
|
29
52
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lc_callnumber
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bill Dueber
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parslet
|
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
107
|
version: '0'
|
108
108
|
requirements: []
|
109
109
|
rubyforge_project:
|
110
|
-
rubygems_version: 2.
|
110
|
+
rubygems_version: 2.4.5
|
111
111
|
signing_key:
|
112
112
|
specification_version: 4
|
113
113
|
summary: Work with LC Call (Classification) Numbers, including an attempt to parse
|
@@ -116,3 +116,4 @@ test_files:
|
|
116
116
|
- test/minitest_helper.rb
|
117
117
|
- test/test_data/basics.txt
|
118
118
|
- test/test_lc_callnumber.rb
|
119
|
+
has_rdoc:
|