bible 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+
2
+ module Bible
3
+
4
+ # Will iterate over verses in a given reference, using the lookup given.
5
+ class BibleIterator
6
+ include Enumerable
7
+ attr_reader :reference
8
+
9
+ # default lookup, if none selected
10
+ def self.default_lookup
11
+ @@default_lookup ||= nil
12
+ end
13
+
14
+ def self.default_lookup=(value)
15
+ @@default_lookup = value
16
+ end
17
+
18
+ def initialize(ref, lookup = nil)
19
+ raise "A lookup class must be provided if no default is set" if lookup.nil? && @@default_lookup.nil?
20
+ raise "A reference to look up must be provided" if ref.nil?
21
+ @reference = ref
22
+ @lookup = lookup || @@default_lookup
23
+ end
24
+
25
+ def method_missing(sym, *args)
26
+ @reference.__send__(sym, *args)
27
+ end
28
+
29
+ def iterate_chapter(chapter, &blk)
30
+ if chapter.single_verse?
31
+ blk.call(@lookup.get_ref(chapter.book.book_symbol, chapter.chapter_number, chapter.verse.verse_number), chapter.book.book_symbol, chapter.chapter_number, chapter.verse.verse_number)
32
+ else
33
+ chapter.verses.each { |v| blk.call(@lookup.get_ref(chapter.book.book_symbol, chapter.chapter_number, v.verse_number), chapter.book.book_symbol, chapter.chapter_number, v.verse_number) }
34
+ end
35
+ end
36
+
37
+ def iterate_book(book, &blk)
38
+ if book.single_chapter?
39
+ iterate_chapter(book.chapter, &blk)
40
+ else
41
+ book.chapters.each { |c|
42
+ iterate_chapter(c, &blk)
43
+ }
44
+ end
45
+ end
46
+
47
+ def each(&blk)
48
+ if @reference.single_book?
49
+ iterate_book(@reference.book, &blk)
50
+ else
51
+ @reference.books.each { |b| iterate_book(b, &blk) }
52
+ end
53
+ end
54
+
55
+ def to_s
56
+ s = ""
57
+ v, b, c = nil, nil, nil
58
+
59
+ self.each do |text, book_symbol, chapter_number, verse_number|
60
+ if book_symbol != b && chapter_number != c
61
+ s << "\n\n" unless s.nil? || s.strip == ""
62
+ s << book_symbol.to_s << ", Chapter " << chapter_number.to_s << "\n\n"
63
+ elsif chapter_number != c
64
+ s << "\n\n" unless s.nil? || s.strip == ""
65
+ s << "Chapter " << chapter_number.to_s << "\n\n"
66
+ elsif (! v.nil?) && v != verse_number - 1
67
+ s << "\n\n"
68
+ end
69
+ b, c, v = book_symbol, chapter_number, verse_number
70
+ s << text
71
+ end
72
+ s
73
+ end
74
+
75
+ def inspect
76
+ self.to_s
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,42 @@
1
+ require 'open-uri'
2
+
3
+ module Bible
4
+ class DRLookup
5
+
6
+ @@books = {}
7
+
8
+ def self.getURL(book, chapter)
9
+ raise "chapter cannot be nil" if chapter.nil?
10
+
11
+ offset = Bible::BookInfo.all_books.index(book) + 1
12
+ "http://www.biblegateway.com/passage/?book_id=#{offset}&chapter=#{chapter}&version=63"
13
+ end
14
+
15
+ def self.get_ref(book, chapter = nil, verse = nil)
16
+ # TODO: Handle nil chapter
17
+ text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
18
+ scanner = StringScanner.new(text)
19
+ if ! scanner.skip_until(/<span id="en-DRA-(.*?)".*?>.*?<\/span>/i)
20
+ scanner = StringScanner.new("")
21
+ else
22
+ unless verse.nil?
23
+ if verse > 1
24
+ id = scanner[1].to_i + (verse - 1)
25
+ if ! scanner.skip_until(/<span id="en-DRA-(#{id})".*?>.*?<\/span>/i)
26
+ scanner = StringScanner.new("")
27
+ end
28
+ end
29
+
30
+ # Extract verse and place in string
31
+ scanner = StringScanner.new(scanner.check_until(/<p \/>/)) unless scanner.empty?
32
+ end
33
+ end
34
+
35
+ if scanner.empty?
36
+ ""
37
+ else
38
+ scanner.rest.strip.gsub(/(\d+)<\/span>/, '\1 ').gsub(/<.*?>/im, "")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ require 'open-uri'
2
+
3
+ module Bible
4
+ class NABLookup
5
+
6
+ @@books = {}
7
+
8
+ def self.getURL(book, chapter = nil)
9
+ raise "chapter cannot be nil" if chapter.nil?
10
+ b = book.to_s.gsub(" ", "").downcase
11
+ case Bible::Books[book]
12
+ when Bible::Books["Song of Solomon".to_sym]
13
+ "http://www.usccb.org/nab/bible/songs/song#{chapter.to_s}.htm"
14
+ when Bible::Books[:Philemon], Bible::Books[:Obadiah], Bible::Books["2 John".to_sym], Bible::Books["3 John".to_sym], Bible::Books[:Jude]
15
+ "http://www.usccb.org/nab/bible/#{b}/#{b}.htm"
16
+ when Bible::Books[:Psalms]
17
+ "http://www.usccb.org/nab/bible/#{b}/psalm#{chapter.to_s}.htm"
18
+ else
19
+ "http://www.usccb.org/nab/bible/#{b}/#{b}#{chapter.to_s}.htm"
20
+ end
21
+ end
22
+
23
+ def self.get_ref(book, chapter = nil, verse = nil)
24
+ # TODO: Handle nil chapter
25
+ text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
26
+ scanner = StringScanner.new(text)
27
+ scanner.skip_until(/<DL>/)
28
+ scanner = StringScanner.new(scanner.check_until(/<\/DL>/))
29
+ unless verse.nil?
30
+ if scanner.skip_until(/<A.*?NAME="v#{verse}".*?>.*?#{verse}.*?<\/A>/)
31
+ scanner = StringScanner.new(scanner.check_until(/<\/DD>/))
32
+ else
33
+ return ""
34
+ end
35
+ end
36
+
37
+ scanner.rest.strip.gsub(/<SUP>.*?<\/SUP>/im, "").gsub(/<.*?>/im, "")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,48 @@
1
+ require 'open-uri'
2
+
3
+ module Bible
4
+ class RSVLookup
5
+ @@books = {}
6
+
7
+ def self.getURL(book, chapter)
8
+ raise "chapter cannot be nil" if chapter.nil?
9
+
10
+ case book
11
+ when %s(1 Kings)
12
+ b = "1Kgs"
13
+ when %s(2 Kings)
14
+ b = "2Kgs"
15
+ when :Job
16
+ b = "BJob"
17
+ when %s(Song of Solomon)
18
+ b = "Cant"
19
+ when :Philemon
20
+ b = "Phlm"
21
+ else
22
+ b = book.to_s.split(" ").join("")[0 ... 4]
23
+ end
24
+ "http://etext.lib.virginia.edu/etcbin/toccer-new2?id=Rsv#{b}.sgm&images=images/modeng&data=/texts/english/modeng/parsed&tag=public&part=#{chapter}&division=div1"
25
+ end
26
+
27
+ def self.get_ref(book, chapter , verse )
28
+ raise "Verse cannot be nil for RSV lookup." if verse.nil?
29
+ raise "Chapter cannot be nil for RSV lookup." if chapter.nil?
30
+
31
+ text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
32
+ scanner = StringScanner.new(text)
33
+ if ! verse.nil?
34
+ if ! scanner.skip_until(/<i>#{verse}:<\/i>/)
35
+ scanner = StringScanner.new("")
36
+ else
37
+ scanner = StringScanner.new(scanner.scan_until(/(<br>|<\/p>)/)[0 ... scanner[1].length * -1])
38
+ end
39
+ end
40
+
41
+ if scanner.empty?
42
+ ""
43
+ else
44
+ scanner.rest
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,905 @@
1
+ require 'rubygems'
2
+ require 'strscan'
3
+ require 'yaml'
4
+
5
+ # Contains classes to parse bible verses and
6
+ # an iterator to look them up given an appropriate bible lookup class.
7
+ module Bible
8
+
9
+ class ChapterInfo
10
+ attr_reader :book, :chapter
11
+
12
+ # Takes a BookInfo and count of verses in the chapter
13
+ def initialize(book, chapter, verse_count)
14
+ @book = book
15
+ @chapter = chapter
16
+ @verse_count = verse_count
17
+ @verse_range = Range.new(1, @verse_count)
18
+ end
19
+
20
+ # Returns range of verses in this chapter
21
+ def verses
22
+ @verse_range
23
+ end
24
+
25
+ end
26
+
27
+ class BookInfo
28
+
29
+ # Symbol naming book this information is about
30
+ attr_accessor :book, :abbreviations, :chapters
31
+
32
+ # Returns the BookInfo of the next book from this book
33
+ def succ
34
+ idx = BookInfo.all_books.index(@book)
35
+ BookInfo.all_books[idx + 1] unless idx.nil?
36
+ end
37
+
38
+ def <=>(value)
39
+ if value.respond_to?(:book)
40
+ oidx = BookInfo.all_books.index(value.book)
41
+ elsif value.is_a?(String)
42
+ oidx = BookInfo.all_books.index(value.to_sym)
43
+ elsif value.is_a?(Symbol)
44
+ oidx = BookInfo.all_books.index(value.to_s)
45
+ else
46
+ raise "Don't know how to compare BookInfo to #{value.inspect}"
47
+ end
48
+
49
+ return BookInfo.all_books.index(@book) <=> oidx
50
+ end
51
+
52
+ # Yields the BookInfo of each book, including this one, up to the next book.
53
+ def upto(b)
54
+ idx = BookInfo.all_books.index(@book)
55
+ endIdx = BookInfo.all_books.index(b)
56
+ while idx <= endIdx
57
+ yield BookInfo.all_books[idx]
58
+ idx += 1
59
+ end
60
+ end
61
+
62
+ # Get the nth chapter (0 based)
63
+ def [](value)
64
+ @chapter_info[value]
65
+ end
66
+
67
+ private
68
+ def initialize(book, num_chapters, verse_counts, abbreviations)
69
+ @book = book
70
+ # Create hash associating chapters to verses
71
+ @chapters = Range.new(1, num_chapters)
72
+ @chapter_info = ([*(@chapters)].zip(verse_counts).collect { |chapter_def| ChapterInfo.new(self, chapter_def[0], chapter_def[1]) })
73
+ @abbreviations = abbreviations.is_a?(Array) ? abbreviations : [abbreviations]
74
+ end
75
+
76
+ public
77
+
78
+ def ==(value)
79
+ if value.respond_to?(:book)
80
+ return value.book == @book
81
+ elsif value.respond_to?(:to_s)
82
+ return @book.to_s == value.to_s || Books[value.to_s].book == @book
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ # Array of all books defined
89
+ def self.all_books
90
+ @@all_books
91
+ end
92
+
93
+ # Get a specific book by passing a symbol representing its name
94
+ def self.[](book)
95
+ return all_books[all_books.index(book)]
96
+ end
97
+
98
+ YAML::load_file(File.dirname(__FILE__) + "/bible.yml").each { |b|
99
+ n = b[0].to_sym
100
+ abb = b[1]
101
+ ch = b[2]
102
+ v = b[3]
103
+
104
+ # Add name of book to abbreviations, unless it's already there.
105
+ if abb.nil?
106
+ abb = [n.to_s]
107
+ elsif abb.is_a?(Array)
108
+ abb << n.to_s unless abb.include?(n.to_s)
109
+ else
110
+ ((abb = [abb]) << n.to_s) unless abb == n.to_s
111
+ end
112
+
113
+ (@@all_books ||= []) << BookInfo.new(n, ch, v, abb)
114
+ }
115
+ end # end BookInfo
116
+
117
+ # Index all books by single 'canonical' symbol. Keys are symbols, values are BookInfo instances.
118
+ CanonicalBooks = Hash[*(BookInfo::all_books.collect do |b|
119
+ if b.book.is_a?(Array)
120
+ b.book.collect { |bn| [bn, b] }
121
+ else
122
+ [b.book, b]
123
+ end
124
+ end).flatten]
125
+
126
+ # Index all books by possible abbreviations. Keys are strings or symbols and values are BookInfo instances.
127
+ Books = Hash[*(BookInfo::all_books.collect { |b| b.abbreviations.collect { |a| [a.to_s.downcase, b] } }.flatten)]
128
+
129
+ class << Books
130
+ def[](value)
131
+ if value.is_a?(Symbol)
132
+ return CanonicalBooks[value]
133
+ else
134
+ super(value.to_s.downcase)
135
+ end
136
+ end
137
+ end
138
+
139
+ class BibleRefParser
140
+
141
+ # Helper module which can is used by Verses and Chapters objects to compare themselves
142
+ # against Arrays and Ranges.
143
+ module RangeComparisons
144
+ # Compares the given ranges to the given value. value can hold an Array or a Range. An array
145
+ # can contain fixnums or ranges.
146
+ def compare_ranges(ranges, value, which)
147
+ return (value.empty? && ranges.empty?) if value.is_a?(Array) && (value.empty? || ranges.empty?)
148
+ return (value.nil? && ranges.empty?) if value.nil? || ranges.empty?
149
+
150
+ if value.is_a?(Range)
151
+ # Continuous ranges mean there are no nil elements in the array, since those represent
152
+ # discontinuous verses or chapters. Therefore, first step is to make sure ranges holds all non-nil items.
153
+ raise "Can't compare to range because #{which} are not continuous." unless ranges.nitems == ranges.length
154
+ return Range.new(ranges.first, ranges.last) == value
155
+ elsif value.is_a?(Array)
156
+ # compare all non-nil elements, since nils are just internal markers for our use
157
+ c = ranges.compact
158
+ offset = 0
159
+ value.each_with_index do |val, idx|
160
+ if val.is_a?(Range)
161
+ val.each do |range_value|
162
+ return false if range_value != c[idx + offset]
163
+ offset += 1
164
+ end
165
+ offset -= 1
166
+ else
167
+ return false if val != c[idx + offset]
168
+ end
169
+ end
170
+ return true
171
+ elsif value.respond_to?(:to_i)
172
+ raise "Can't compare single fixnum to multiple #{which}." unless ranges.length == 1
173
+ return ranges[0].to_i == value.to_i
174
+ else
175
+ raise "Don't know how to compare #{which} to value #{value.inspect}"
176
+ end
177
+
178
+ end
179
+ end
180
+
181
+ class Book
182
+ class SingleBook
183
+ attr_accessor :book_symbol, :chapter
184
+
185
+ # Argument should be a symbol from BookInfo class representing the book.
186
+ def initialize(book_symbol)
187
+ @book_symbol = book_symbol
188
+ end
189
+
190
+ def book
191
+ self
192
+ end
193
+
194
+ # Fixes the book reference. If chapter is nil, assumed to refer to entire book. If
195
+ # chapters ends in -1, assumed to refere to chapters to end of book. If any chapters
196
+ # are contained in this book, they will all be fixed up.
197
+ def fixup
198
+ # No chapters at all means ALL chapters, so create the range for fixup.
199
+ if ! has_chapter?
200
+ book = Books[@book_symbol]
201
+ # By reference self.chapters, a refernce to Chapter 1 is created.
202
+ # More verbosely, we could assign self.chapter = Chapter 1 and then
203
+ # reference self.chapters, but there isn't much point.
204
+ self.chapters << Chapter.new(self, -1)
205
+ @chapters.fixup
206
+ elsif single_chapter?
207
+ @chapter.fixup
208
+ if @chapter.single_verse?
209
+ self.verse = @chapter.verse
210
+ end
211
+ else
212
+ @chapters.fixup
213
+ end
214
+ # Don't want reference to chapters method to create chapters after fixup
215
+ @fixedUp = true
216
+ end
217
+
218
+ # Assumes value is a symbol and compares it to the book represented
219
+ # here.
220
+ def ==(value)
221
+ return false if value.nil? || @book_symbol.nil?
222
+ if value.is_a?(Symbol)
223
+ return value == @book_symbol
224
+ end
225
+ end
226
+
227
+ def has_chapter?
228
+ (respond_to?(:chapter) && ! @chapter.nil?) || (respond_to?(:chapters) && ! @chapters.nil?)
229
+ end
230
+
231
+ def single_chapter?
232
+ respond_to?(:chapter)
233
+ end
234
+
235
+ def single_verse?
236
+ single_chapter? && @chapter.single_verse?
237
+ end
238
+
239
+ def method_missing(symbol, *args)
240
+ if ! @fixedUp
241
+ case symbol
242
+ # when chapters is referenced, remove the chapter method
243
+ # and add chapters method so this book has multiple chapters.
244
+ when :chapters
245
+ c = self.chapter
246
+ class << self
247
+ attr_reader :chapters
248
+ undef_method :chapter, :chapter=
249
+ end
250
+ remove_instance_variable :@chapter if defined?(@chapter)
251
+
252
+ @chapters = Chapters.new(self)
253
+ # If no initial chapter has been set, set it to 1
254
+ if c.nil?
255
+ @chapters << Chapter.new(self, 1)
256
+ else
257
+ @chapters << c
258
+ end
259
+
260
+ @chapters
261
+ when :verse, :verse=
262
+ class << self
263
+ define_method :verse do
264
+ @chapter.verse
265
+ end
266
+
267
+ define_method :verse= do |value|
268
+ @chapter.verse = value
269
+ end
270
+ end
271
+
272
+ self.__send__(symbol, *args)
273
+ else
274
+ super(symbol, *args)
275
+ end
276
+ else
277
+ super(symbol, *args)
278
+ end
279
+ end
280
+
281
+ def inspect
282
+ if single_chapter?
283
+ "#{@book_symbol} " + @chapter.inspect
284
+ else
285
+ "#{@book_symbol} " + @chapters.inspect
286
+ end
287
+ end
288
+
289
+ end
290
+
291
+ class MultipleBooks
292
+ def initialize(single)
293
+ @books = [single]
294
+ end
295
+
296
+ def fixup
297
+ if ! @fixedUp
298
+ @books.compact.each { |b| b.fixup }
299
+ @fixedUp = true
300
+ end
301
+ end
302
+
303
+ def <<(value)
304
+ unless value.nil? || @books.last.nil?
305
+ Books[@books.last.book_symbol].succ.upto(value) { |book_info|
306
+ @books << SingleBook.new(book_info.book)
307
+ }
308
+ else
309
+ if value.nil?
310
+ @books << value
311
+ else
312
+ @books << SingleBook.new(value)
313
+ end
314
+ end
315
+ end
316
+
317
+ def books
318
+ @books.compact
319
+ end
320
+
321
+ def ==(value)
322
+ raise "Can't compare multipe books to single book" unless value.is_a?(Array)
323
+ @books.compact.each_with_index { |b, idx|
324
+ raise "Don't know how to compare to book #{value[idx]}" unless value[idx].is_a?(String) || value[idx].is_a?(Symbol)
325
+ }
326
+ end
327
+
328
+ def method_missing(sym, *args)
329
+ # This forwarding is used only until fixup because the parse
330
+ # algorithm depends on it to query the "current reference" in a multiple
331
+ # book situation
332
+ if ! @fixedUp
333
+ @books.last.__send__(sym, *args)
334
+ else
335
+ super(sym, *args)
336
+ end
337
+ end
338
+
339
+ def inspect
340
+ @books.compact.inject("") { |val, b|
341
+ val << " " unless val.empty?
342
+ val << b.inspect
343
+ val
344
+ }
345
+ end
346
+
347
+ end
348
+
349
+ def has_book?
350
+ ! @proxy.nil?
351
+ end
352
+
353
+ def single_book?
354
+ self.has_book? && @proxy.is_a?(SingleBook)
355
+ end
356
+
357
+ def <<(value)
358
+ if @proxy.nil?
359
+ @proxy = SingleBook.new(value)
360
+ else
361
+ @proxy = MultipleBooks.new(@proxy) if @proxy.is_a?(SingleBook)
362
+ @proxy << value
363
+ end
364
+ end
365
+
366
+ def method_missing(symbol, *args)
367
+ if @proxy.nil?
368
+ super(symbol, *args)
369
+ else
370
+ @proxy.__send__(symbol, *args)
371
+ end
372
+ end
373
+
374
+ end
375
+
376
+ class Chapter
377
+ include Comparable
378
+ include Enumerable
379
+
380
+ attr_reader :book, :chapter_number
381
+ attr_accessor :verse
382
+
383
+ def initialize(book, chapter)
384
+ raise "Chapter must be an integer" unless chapter.is_a?(Fixnum)
385
+
386
+ @book = book
387
+ @chapter_number = chapter
388
+ end
389
+
390
+ def fixup
391
+ if ! has_verse?
392
+ self.verses << Verse.new(self, -1)
393
+ self.verses.fixup
394
+ elsif ! single_verse?
395
+ @verses.fixup
396
+ end
397
+ # Don't want reference to verses method to create chapters after fixup
398
+ @fixedUp = true
399
+ end
400
+
401
+ # Assumes value is a Fixnum and determines if it is equal
402
+ # to this chapter.
403
+ def ==(value)
404
+ return false if value.nil? || @chapter_number.nil?
405
+ raise "Don't know how to compare chapter to #{value.inspect}" unless value.is_a?(Fixnum)
406
+ return value == @chapter_number
407
+ end
408
+
409
+ def to_i
410
+ return @chapter_number.to_i
411
+ end
412
+
413
+ def to_s
414
+ return @chapter_number.to_s
415
+ end
416
+
417
+ def <=>(value)
418
+ return -1 if value.nil?
419
+ raise "Don't know how to compare Chapter and #{value.inspect} becuase it does not implement to_i." unless value.respond_to?(:to_i)
420
+ return @chapter_number.to_i <=> value.to_i
421
+ end
422
+
423
+ def succ
424
+ return Chapter.new(@book, @chapter_number + 1)
425
+ end
426
+
427
+ def upto(i)
428
+ raise "Cannot enumerate to value #{i.inspect} because it does not respond to to_i." unless i.respond_to?(:to_i)
429
+ yield self
430
+ (@chapter_number + 1).upto(i.to_i) { |x| yield(Chapter.new(@book, x)) }
431
+ end
432
+
433
+ def has_verse?
434
+ (respond_to?(:verse) && ! @verse.nil?) || (respond_to?(:verses) && ! @verses.nil?)
435
+ end
436
+
437
+ def single_verse?
438
+ respond_to?(:verse)
439
+ end
440
+
441
+ def method_missing(symbol, *args)
442
+ unless @fixedUp
443
+ case symbol
444
+ # when chapters is referenced, remove the chapter method
445
+ # and add chapters method so this book has multiple chapters.
446
+ when :verses
447
+ c = self.verse
448
+ class << self
449
+ attr_reader :verses
450
+ undef_method :verse, :verse=
451
+ end
452
+ remove_instance_variable :@verse if defined?(@verse)
453
+
454
+ @verses = Verses.new(self)
455
+ # If no initial verse, set it to 1
456
+ if c.nil?
457
+ @verses << Verse.new(self, 1)
458
+ else
459
+ @verses << c
460
+ end
461
+
462
+ @verses
463
+ else
464
+ super(symbol, *args)
465
+ end
466
+ else
467
+ super(symbol, *args)
468
+ end
469
+ end
470
+
471
+ def inspect
472
+ if single_verse?
473
+ "#{@chapter_number}:" + @verse.inspect
474
+ else
475
+ "#{@chapter_number}:" + @verses.inspect
476
+ end
477
+ end
478
+ end
479
+
480
+ class Verse
481
+ include Comparable
482
+ include Enumerable
483
+
484
+ attr_reader :chapter, :verse_number
485
+ attr_accessor :verse
486
+
487
+ def initialize(chapter, verse)
488
+ raise "Verse must be an integer" unless verse.is_a?(Fixnum)
489
+
490
+ @chapter = chapter
491
+ @verse_number = verse
492
+ end
493
+
494
+ # Assumes value is a Fixnum and determines if it is equal
495
+ # to this verse.
496
+ def ==(value)
497
+ return false if value.nil? || @verse_number.nil?
498
+ raise "Don't know how to compare verse to #{value.inspect}" unless value.is_a?(Fixnum)
499
+ return value == @verse_number
500
+ end
501
+
502
+ def to_i
503
+ return @verse_number.to_i
504
+ end
505
+
506
+ def to_s
507
+ return @verse_number.to_s
508
+ end
509
+
510
+ def <=>(value)
511
+ return -1 if value.nil?
512
+ raise "Don't know how to compare Verse and #{value.inspect} becuase it does not implement to_i." unless value.respond_to?(:to_i)
513
+ return @verse_number.to_i <=> value.to_i
514
+ end
515
+
516
+ def succ
517
+ return Verse.new(@chapter, @verse_number + 1)
518
+ end
519
+
520
+ def upto(i)
521
+ raise "Cannot enumerate to value #{i.inspect} because it does not respond to to_i." unless i.respond_to?(:to_i)
522
+ yield(self)
523
+ (@verse_number + 1).upto(i.to_i) { |x| yield(Verse.new(@chapter, x)) }
524
+ end
525
+
526
+ def inspect
527
+ self.to_s
528
+ end
529
+ end
530
+
531
+ # Represents range or discontinuous series of chaptesr
532
+ class Chapters
533
+ include RangeComparisons
534
+
535
+ def initialize(book)
536
+ @chapters = []
537
+ @book = book
538
+ end
539
+
540
+ def fixup
541
+ if @chapters.last == -1
542
+ book = Books[@book.book_symbol]
543
+ @chapters.pop
544
+ @chapters.last.succ.upto(book.chapters.last) do |c|
545
+ @chapters << c
546
+ end
547
+ end
548
+
549
+ @chapters.compact.each { |c| c.fixup }
550
+ end
551
+
552
+ def <<(value)
553
+ unless value.nil? || @chapters.empty? || @chapters.last.nil? || value == -1
554
+ raise "Cannot add a chapter reference in reverse order" if @chapters.last > value
555
+ unless @chapters.last.chapter_number == value
556
+ @chapters.last.succ.upto(value) { |c|
557
+ @chapters << c
558
+ }
559
+ end
560
+ else
561
+ @chapters << value
562
+ end
563
+ end
564
+
565
+ def length
566
+ @chapters.length
567
+ end
568
+
569
+ def last
570
+ @chapters.last
571
+ end
572
+
573
+ def [](index)
574
+ return @chapters.compact[index]
575
+ end
576
+
577
+ def ==(value)
578
+ compare_ranges(@chapters, value, "chapters")
579
+ end
580
+
581
+ def inspect
582
+ @chapters.inject("") { |val, c|
583
+ val << "," unless val.empty?
584
+ val << c.inspect unless c.nil?
585
+ val
586
+ }
587
+ end
588
+
589
+ # Returns Chapter objects for each chapter, or -1 if "end of chapters" is indicated.
590
+ # If chapters is empty, will be a no-op
591
+ def each
592
+ return if @chapters.empty?
593
+ @chapters.each { |chapter|
594
+ yield chapter unless chapter.nil?
595
+ }
596
+ end
597
+ end
598
+
599
+ # Represents range or discontinuous series of chaptesr
600
+ class Verses
601
+ include RangeComparisons
602
+
603
+ def initialize(chapter)
604
+ @verses = []
605
+ @chapter = chapter
606
+ end
607
+
608
+ def fixup
609
+ if @verses.last == -1
610
+ @verses.pop
611
+ chapter = Books[@chapter.book.book_symbol][@chapter.chapter_number - 1]
612
+ @verses.last.succ.upto(chapter.verses.last) do |v|
613
+ @verses << v
614
+ end
615
+ end
616
+ end
617
+
618
+ def <<(value)
619
+ unless value.nil? || @verses.empty? || @verses.last.nil? || value == -1
620
+ raise "Cannot add a verse reference in reverse order" if @verses.last > value
621
+ unless @verses.last.verse_number == value
622
+ @verses.last.succ.upto(value) { |v|
623
+ @verses << v
624
+ }
625
+ end
626
+ else
627
+ @verses << value
628
+ end
629
+ end
630
+
631
+ def length
632
+ @verses.length
633
+ end
634
+
635
+ def last
636
+ @verses.last
637
+ end
638
+
639
+ def [](index)
640
+ return @verses.compact[index]
641
+ end
642
+
643
+ def ==(value)
644
+ compare_ranges(@verses, value, "verses")
645
+ end
646
+
647
+ # All verses defined on the chapter. If none are defined, this is a no-op.
648
+ # Will yield either Verse objects or -1 (indicating go to end of verses).
649
+ def each
650
+ return if @verses.empty?
651
+ @verses.each { |verse|
652
+ yield verse unless verse.nil?
653
+ }
654
+ end
655
+
656
+ def inspect
657
+ last_verse = nil
658
+ s = @verses.inject("") { |val, v|
659
+ if val.empty? || last_verse.nil?
660
+ val << "," unless val.empty?
661
+ val << v.inspect
662
+ elsif v.nil?
663
+ val << "-#{last_verse.inspect}"
664
+ end
665
+
666
+ last_verse = v
667
+ val
668
+ }
669
+
670
+ s << "-#{last_verse.inspect}" unless last_verse.nil?
671
+ s
672
+ end
673
+ end
674
+
675
+ # determines if a book reference is coming up in the string. Does not
676
+ # determine if its a valid book - jsut the form of one.
677
+ def self.book_ahead?(str)
678
+ return ! get_book(str).nil?
679
+ end
680
+
681
+ # Returns the upcoming book, if any. If modify is true, the string passed in has the book referenced removed
682
+ def self.get_book(scanner, modify = false)
683
+ # Look for numbered book references (a digit, followed by whitespace, following by some number of word characters.)
684
+ if t = scanner.check(/\d\s+[A-Za-z]+(\.|)/)
685
+ if modify
686
+ return scanner.scan(/\d\s+[A-Za-z]+(\.|)/).gsub(/\./, "")
687
+ else
688
+ return t
689
+ end
690
+ # Look for one of the 'normal' books
691
+ elsif t = scanner.check(/[A-Za-z]+(\.|)/)
692
+ if modify
693
+ return scanner.scan(/[A-Za-z]+(\.|)/).gsub(/\./, "")
694
+ else
695
+ return t
696
+ end
697
+ end
698
+
699
+ return nil
700
+ end
701
+
702
+ def self.parse(reference)
703
+ return nil if reference.nil? || (reference = reference.strip) == ""
704
+
705
+ state = :book
706
+ # will use curr_ref to build up each reference
707
+ curr_ref = Book.new
708
+
709
+ ref = StringScanner.new(reference)
710
+ while ! ref.eos?
711
+ ref.skip(/\s*/)
712
+ if ! ref.eos?
713
+ token = ""
714
+ case state
715
+ when :book, :end_book
716
+ # Special case - one of the numbered books.
717
+ token = get_book(ref, true)
718
+
719
+ raise "Book #{token} not recognized in #{ref.rest}" if Books[token].nil?
720
+
721
+ if state == :end_book
722
+ curr_ref << Books[token].book
723
+ state = :end_chapter
724
+ elsif curr_ref.has_book?
725
+ # add discontinuity if there is a previous book defined.
726
+ curr_ref << nil if curr_ref.has_book?
727
+ curr_ref << Books[token].book
728
+ state = :start_chapter
729
+ else
730
+ # look for tokens indicating another book is ahead, rather than a chapter
731
+ curr_ref << Books[token].book
732
+ if ref.skip(/\s+(-|,|;|$)/)
733
+ state = :end_book
734
+ else
735
+ state = :start_chapter
736
+ end
737
+ end
738
+ when :start_chapter, :end_chapter
739
+
740
+ # Get all digits, assume they are a chapter
741
+ token << ref.scan(/\d+/)
742
+ chapter = token.to_i rescue (raise "Chapter not recognized as integer #{token}")
743
+
744
+ case state
745
+ when :start_chapter
746
+ if ! curr_ref.has_chapter?
747
+ curr_ref.chapter = Chapter.new(curr_ref.book, chapter)
748
+ else
749
+ curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
750
+ end
751
+ when :end_chapter
752
+ # Handles the case where this is a book-spanning reference.
753
+ if ! curr_ref.has_chapter?
754
+ if chapter > 1
755
+ curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
756
+ else
757
+ curr_ref.chapter = Chapter.new(curr_ref.book, chapter)
758
+ end
759
+ elsif (! curr_ref.single_chapter?) || curr_ref.chapter != chapter
760
+ curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
761
+ end
762
+ end
763
+ token = ""
764
+
765
+ # peek ahead to determine next place to go
766
+ ref.skip(/\s*/)
767
+ case state
768
+ when :start_chapter
769
+ case ref.peek(1)
770
+ when ":", "."
771
+ ref.getch
772
+ state = :start_verse
773
+ when "-"
774
+ ref.getch
775
+ ref.skip(/\s*/)
776
+ if book_ahead?(ref)
777
+ # indicate we need to go to end of chapters for this book
778
+ curr_ref.book.chapters << Chapter.new(curr_ref.book, -1)
779
+ state = :end_book
780
+ else
781
+ state = :end_chapter
782
+ end
783
+ when ",", ";"
784
+ ref.getch
785
+ ref.skip(/\s*/)
786
+ curr_ref.chapters << nil
787
+ # If a word character comes up next, we are looking at a book reference
788
+ if book_ahead?(ref)
789
+ state = :book
790
+ else
791
+ state = :start_chapter
792
+ end
793
+ when ""
794
+ ref.getch
795
+ break
796
+ else
797
+ raise "Unrecognized token after verse: #{ref.rest}"
798
+ end
799
+ when :end_chapter
800
+ case ref.peek(1)
801
+ when ",", ";"
802
+ ref.getch
803
+ ref.skip(/\s*/)
804
+ # add nil to indicate discontinuity
805
+ curr_ref.chapters << nil
806
+ if book_ahead?(ref)
807
+ state = :book
808
+ else
809
+ state = :start_chapter
810
+ end
811
+ when ":", "."
812
+ ref.getch
813
+ state = :end_verse
814
+ when ""
815
+ ref.getch
816
+ break
817
+ else
818
+ raise "Unrecognized token after end chapter: #{ref.ref}"
819
+ end
820
+ end
821
+ when :start_verse , :end_verse
822
+
823
+ token << ref.scan(/\d+/) if ! ref.eos?
824
+ verse = token.to_i rescue (raise "Verse not recognized as integer #{token}")
825
+
826
+ case state
827
+ when :start_verse
828
+ cp = curr_ref.single_chapter? ? curr_ref.chapter : curr_ref.chapters.last
829
+ if ! cp.has_verse?
830
+ cp.verse = Verse.new(cp, verse)
831
+ elsif (! cp.single_verse?) || cp.verse != verse
832
+ cp.verses << nil
833
+ cp.verses << Verse.new(cp, verse)
834
+ end
835
+ when :end_verse
836
+ cp = curr_ref.single_chapter? ? curr_ref.chapter : curr_ref.chapters.last
837
+ if ! cp.has_verse?
838
+ if verse > 1
839
+ cp.verses << Verse.new(cp, verse)
840
+ else
841
+ cp.verse = Verse.new(cp, verse)
842
+ end
843
+ elsif (! cp.single_verse?) || cp.verse != verse
844
+ cp.verses << Verse.new(cp, verse)
845
+ end
846
+ end
847
+
848
+ token = ""
849
+
850
+ # Skip whitespace and single character references (for references such as "Acts 1:1a")
851
+ ref.skip(/[a-zA-Z]?\s*/)
852
+ case ref.peek(1)
853
+ when "-"
854
+ raise "Unrecognized token after #{ref}" if state == :end_verse
855
+ ref.getch
856
+ ref.skip(/\s*/)
857
+ # Determine if we are actually looking at a chapter or book spanning reference.
858
+ # We start by assuming we'll see a verse refernce next, and first look for a book.
859
+ state = :end_verse
860
+ if book_ahead?(ref)
861
+ curr_ref.chapters.last.verses << Verse.new(curr_ref.chapters.last, -1)
862
+ curr_ref.chapters << Chapter.new(curr_ref.book, -1)
863
+ state = :end_book
864
+ else
865
+ # No book was found, so now look for a chapter spannign reference. We do this
866
+ # by seeing if a colon follows the expected numeric reference.
867
+ if ref.check(/\d+(:|\.)/)
868
+ # This is actually a chapter reference, not a verse reference. Indicate our current verse should
869
+ # go to the "end", and then go to chapter parsing
870
+ curr_ref.chapters.last.verses << Verse.new(curr_ref.chapters.last, -1)
871
+ state = :end_chapter
872
+ end
873
+ end
874
+ when ",", ";"
875
+ ref.getch
876
+ ref.skip(/\s*/)
877
+
878
+ # Need to cheat and determine if verse, chapter:verse, or book lie ahead
879
+ # assume we are going to see another verse
880
+ state = :start_verse
881
+
882
+ if book_ahead?(ref)
883
+ state = :book
884
+ else
885
+ if ref.check(/\d+(:|\.)/)
886
+ curr_ref.chapters << nil
887
+ state = :start_chapter
888
+ elsif ! ref.check(/\d+(,|-|;|$)/)
889
+ raise "Unrecognized token after end of verse: '#{ref}'"
890
+ end
891
+ end
892
+ when ""
893
+ ref.getch
894
+ else
895
+ raise "Unrecognized token after verse: #{ref.rest}"
896
+ end
897
+ end
898
+ end
899
+ end
900
+
901
+ curr_ref.fixup
902
+ return curr_ref
903
+ end
904
+ end
905
+ end