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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 080e05e9e7c6084e500e3fb0ca2d827c4dc52474
4
- data.tar.gz: 222d0cce1e42df11b29496b191da1ed60ae37e42
3
+ metadata.gz: d173a4ec0a1b4ba26c0beec642a3f66b845c67d4
4
+ data.tar.gz: 41a42a08153cb57f4f0c775794cbf72c56890369
5
5
  SHA512:
6
- metadata.gz: 3291b552acdb8d6339d0043ac0cbe66e0e906f4b007c3e04a4dcc972a4947e299930bb1c66bbb999a6bb8510d0b55ec6b2611e59dc9aafe7f76077c70bcd76c5
7
- data.tar.gz: 8d5840f2a39cbcee9aedb56135b7b556c2eb046615224331c4c40ff97bdbb59f08602154f2e5d9ff22831c03620d2ef95511d0cd6d182ec04eb81fd7229e425c
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.
@@ -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
- attr_accessor :letters, :digits, :doon1, :firstcutter, :doon2,
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
-
@@ -1,3 +1,3 @@
1
1
  module LCCallNumber
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -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.1
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: 2014-01-30 00:00:00.000000000 Z
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.2.0
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: