bible 1.0.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,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