pericope 0.7.2 → 1.0.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: 0e48633c3d81555ea2a699d359437bcbebfae2c3
4
- data.tar.gz: b9017728cd671d4c9d24ed5612ffbf6459e38068
3
+ metadata.gz: 145908194097992ca18effe78d077c9ab4056e75
4
+ data.tar.gz: 88d93e6cb4e92b429ee3b0d3e559bd4c1f874aa1
5
5
  SHA512:
6
- metadata.gz: 280f67bd0faf5654e48a15971ef1874df15b3a5802ad146b27367acb3dc95b3758278c889bf77ab0708c51ecd42886c92340ab825dda1b8880d433478dd8e0fd
7
- data.tar.gz: e14e5dadefe61d965613f312d4b14e67566837ef248ae3b0d6bf2abb726f905397760d20b779d314e0b9b8e7327ac8e6fc7f8b2fdd17533fedea7c0ba1702ae8
6
+ metadata.gz: 72b6bc51a94abdbfbdcb842359d98ca9ca68c8c802c7e4c27c63237ddf66952fda859b64a9782c5b1f909658c4bbf2ac6d3ec073c9ea9f166885a10f8179725a
7
+ data.tar.gz: 109fe8d1a3eaffa62485d55db3febc29f68a5410592b8d1334610c6d7729575ea59cd2a9118e7e218e2311138356354023966501802f3730b222c5053721cf11
@@ -1,187 +1,171 @@
1
1
  require "pericope/version"
2
2
  require "pericope/data"
3
+ require "pericope/parsing"
3
4
 
4
5
  class Pericope
5
- attr_reader :book, :book_chapter_count, :book_name, :original_string, :ranges
6
+ extend Pericope::Parsing
7
+ include Enumerable
8
+
9
+ attr_reader :book, :original_string, :ranges
10
+
6
11
 
7
12
  def initialize(arg)
8
13
  case arg
9
14
  when String
10
15
  attributes = Pericope.match_one(arg)
11
- raise "no pericope found in #{arg} (#{arg.class})" if attributes.nil?
16
+ raise ArgumentError, "no pericope found in #{arg}" if attributes.nil?
12
17
 
13
18
  @original_string = attributes[:original_string]
14
- set_book attributes[:book]
19
+ @book = attributes[:book]
15
20
  @ranges = attributes[:ranges]
16
21
 
17
22
  when Array
18
- arg = arg.map(&:to_i)
19
- set_book Pericope.get_book(arg.first)
20
- @ranges = Pericope.group_array_into_ranges(arg)
21
-
22
- when Range
23
- STDERR.puts "DEPRECATION WARNING: instantiating a pericope with a single range is deprecated and will be removed in pericope 1.0.",
24
- "",
25
- " You can change `Pericope.new(range)` to `Pericope.new(range.to_a)`",
26
- ""
27
- set_book Pericope.get_book(arg.begin)
28
- @ranges = [arg]
23
+ @ranges = group_array_into_ranges(arg)
24
+ @book = @ranges.first.begin.book
29
25
 
30
26
  else
31
- attributes = arg
32
- @original_string = attributes[:original_string]
33
- set_book attributes[:book]
34
- @ranges = attributes[:ranges]
27
+ @original_string = arg[:original_string]
28
+ @book = arg[:book]
29
+ @ranges = arg[:ranges]
35
30
 
36
31
  end
32
+ raise ArgumentError, "must specify book" unless @book
37
33
  end
38
34
 
39
35
 
40
-
41
- def self.book_has_chapters?(book)
42
- BOOK_CHAPTER_COUNTS[book] > 1
43
- end
44
-
45
36
  def book_has_chapters?
46
37
  book_chapter_count > 1
47
38
  end
48
39
 
49
-
50
-
51
- # Differs from Pericope.new in that it won't raise an exception
52
- # if text does not contain a pericope but will return nil instead.
53
- def self.parse_one(text)
54
- parse(text) do |pericope|
55
- return pericope
56
- end
57
- nil
40
+ def book_name
41
+ @book_name ||= Pericope::BOOK_NAMES[@book]
58
42
  end
59
43
 
44
+ def book_chapter_count
45
+ @book_chapter_count ||= Pericope::BOOK_CHAPTER_COUNTS[@book]
46
+ end
60
47
 
61
48
 
62
- def self.parse(text)
63
- pericopes = []
64
- match_all(text) do |attributes|
65
- pericope = Pericope.new(attributes)
66
- if block_given?
67
- yield pericope
68
- else
69
- pericopes << pericope
70
- end
71
- end
72
- block_given? ? text : pericopes
49
+ def to_s(options={})
50
+ "#{book_name} #{well_formatted_reference(options)}"
73
51
  end
74
52
 
53
+ def inspect
54
+ "Pericope(#{to_s})"
55
+ end
75
56
 
76
57
 
77
- def self.split(text)
78
- segments = []
79
- start = 0
80
-
81
- match_all(text) do |attributes, match|
58
+ def ==(other)
59
+ other.is_a?(self.class) && [book, ranges] == [other.book, other.ranges]
60
+ end
82
61
 
83
- pretext = text.slice(start...match.begin(0))
84
- if pretext.length > 0
85
- segments << pretext
86
- yield pretext if block_given?
87
- end
62
+ def hash
63
+ [book, ranges].hash
64
+ end
88
65
 
89
- pericope = Pericope.new(attributes)
90
- segments << pericope
91
- yield pericope if block_given?
66
+ def <=>(other)
67
+ to_a <=> other.to_a
68
+ end
92
69
 
93
- start = match.end(0)
94
- end
70
+ def intersects?(other)
71
+ return false unless other.is_a?(Pericope)
72
+ return false unless book == other.book
95
73
 
96
- pretext = text.slice(start...text.length)
97
- if pretext.length > 0
98
- segments << pretext
99
- yield pretext if block_given?
74
+ ranges.each do |self_range|
75
+ other.ranges.each do |other_range|
76
+ return true if (self_range.end >= other_range.begin) and (self_range.begin <= other_range.end)
77
+ end
100
78
  end
101
79
 
102
- segments
80
+ false
103
81
  end
104
82
 
105
83
 
84
+ def each
85
+ return to_enum unless block_given?
106
86
 
107
- def self.sub(text)
108
- STDERR.puts "DEPRECATION WARNING: Pericope.sub is deprecated and will be removed in pericope 1.0.",
109
- "",
110
- " You can use `Pericope.split` with `inject` to replace pericopes in a block of text",
111
- ""
112
-
113
- split(text).inject("") do |text, segment|
114
- if segment.is_a?(String)
115
- text << segment
116
- else
117
- text << "{{#{segment.to_a.join(" ")}}}"
87
+ ranges.each do |range|
88
+ range.each do |verse|
89
+ yield verse
118
90
  end
119
91
  end
92
+
93
+ self
120
94
  end
121
95
 
122
96
 
97
+ class << self
98
+ attr_reader :max_letter
123
99
 
124
- def self.rsub(text)
125
- STDERR.puts "DEPRECATION WARNING: Pericope.rsub is deprecated and will be removed in pericope 1.0."
100
+ def max_letter=(value)
101
+ unless @max_letter == value
102
+ @max_letter = value.freeze
103
+ @_letters = nil
104
+ @_regexp = nil
105
+ @_normalizations = nil
106
+ @_letter_regexp = nil
107
+ @_fragment_regexp = nil
108
+ end
109
+ end
126
110
 
127
- text.gsub(/\{\{(\d{7,8} ?)+\}\}/) do |match|
128
- ids = match[2...-2].split.collect(&:to_i)
129
- Pericope.new(ids).to_s
111
+ def book_has_chapters?(book)
112
+ BOOK_CHAPTER_COUNTS[book] > 1
130
113
  end
131
- end
132
114
 
115
+ def get_max_verse(book, chapter)
116
+ id = (book * 1000000) + (chapter * 1000)
117
+ CHAPTER_VERSE_COUNTS[id]
118
+ end
133
119
 
120
+ def get_max_chapter(book)
121
+ BOOK_CHAPTER_COUNTS[book]
122
+ end
134
123
 
135
- def to_s(options={})
136
- "#{book_name} #{well_formatted_reference(options)}"
137
- end
124
+ def regexp
125
+ @_regexp ||= /#{book_pattern}\.?\s*(#{reference_pattern})/i
126
+ end
138
127
 
139
- def inspect
140
- "Pericope(#{to_s})"
141
- end
128
+ def normalizations
129
+ @_normalizations ||= [
130
+ [/(\d+)[".](\d+)/, '\1:\2'], # 12"5 and 12.5 -> 12:5
131
+ [/[–—]/, '-'], # convert em dash and en dash to -
132
+ [/[^0-9,:;\-–—#{letters}]/, ''] ] # remove everything but recognized symbols
133
+ end
142
134
 
143
- def ==(other)
144
- other.is_a?(self.class) && [book, ranges] == [other.book, other.ranges]
145
- end
135
+ def letter_regexp
136
+ @_letter_regexp ||= /[#{letters}]$/
137
+ end
146
138
 
147
- def hash
148
- [book, ranges].hash
149
- end
139
+ def fragment_regexp
140
+ @_fragment_regexp ||= /^(?:(?<chapter>\d{1,3}):)?(?<verse>\d{1,3})?(?<letter>[#{letters}])?$/
141
+ end
150
142
 
151
- def <=>(other)
152
- to_a <=> other.to_a
153
- end
143
+ private
154
144
 
145
+ def book_pattern
146
+ BOOK_PATTERN.source.gsub(/[ \n]/, "")
147
+ end
155
148
 
149
+ def reference_pattern
150
+ number = '\d{1,3}'
151
+ verse = "#{number}[#{letters}]?"
152
+ chapter_verse_separator = '\s*[:"\.]\s*'
153
+ list_or_range_separator = '\s*[\-–—,;]\s*'
154
+ chapter_and_verse = "(?:#{number + chapter_verse_separator})?" + verse
155
+ chapter_and_verse_or_letter = "(?:#{chapter_and_verse}|[#{letters}])"
156
+ chapter_and_verse + "(?:#{list_or_range_separator + chapter_and_verse_or_letter})*"
157
+ end
156
158
 
157
- def to_a
158
- # one range per chapter
159
- chapter_ranges = []
160
- ranges.each do |range|
161
- min_chapter = Pericope.get_chapter(range.begin)
162
- max_chapter = Pericope.get_chapter(range.end)
163
- if min_chapter == max_chapter
164
- chapter_ranges << range
165
- else
166
- chapter_ranges << Range.new(range.begin, Pericope.get_last_verse(book, min_chapter))
167
- for chapter in (min_chapter+1)...max_chapter
168
- chapter_ranges << Range.new(
169
- Pericope.get_first_verse(book, chapter),
170
- Pericope.get_last_verse(book, chapter))
171
- end
172
- chapter_ranges << Range.new(Pericope.get_first_verse(book, max_chapter), range.end)
173
- end
159
+ def letters
160
+ @_letters ||= ("a"..max_letter).to_a.join
174
161
  end
175
162
 
176
- chapter_ranges.inject([]) {|array, range| array.concat(range.to_a)}
177
163
  end
178
164
 
179
165
 
166
+ private
180
167
 
181
168
  def well_formatted_reference(options={})
182
- recent_chapter = nil # e.g. in 12:1-8, remember that 12 is the chapter when we parse the 8
183
- recent_chapter = 1 unless book_has_chapters?
184
-
185
169
  verse_range_separator = options.fetch(:verse_range_separator, "–") # en-dash
186
170
  chapter_range_separator = options.fetch(:chapter_range_separator, "—") # em-dash
187
171
  verse_list_separator = options.fetch(:verse_list_separator, ", ")
@@ -189,353 +173,64 @@ class Pericope
189
173
  always_print_verse_range = options.fetch(:always_print_verse_range, false)
190
174
  always_print_verse_range = true unless book_has_chapters?
191
175
 
192
- s = ""
193
- ranges.each_with_index do |range, i|
194
- min_chapter = Pericope.get_chapter(range.begin)
195
- min_verse = Pericope.get_verse(range.begin)
196
- max_chapter = Pericope.get_chapter(range.end)
197
- max_verse = Pericope.get_verse(range.end)
176
+ recent_chapter = nil # e.g. in 12:1-8, remember that 12 is the chapter when we parse the 8
177
+ recent_chapter = 1 unless book_has_chapters?
178
+ recent_verse = nil
198
179
 
180
+ ranges.each_with_index.each_with_object("") do |(range, i), s|
199
181
  if i > 0
200
- if recent_chapter == min_chapter
182
+ if recent_chapter == range.begin.chapter
201
183
  s << verse_list_separator
202
184
  else
203
185
  s << chapter_list_separator
204
186
  end
205
187
  end
206
188
 
207
- if min_verse == 1 && max_verse >= Pericope.get_max_verse(book, max_chapter) && !always_print_verse_range
208
- s << min_chapter.to_s
209
- s << "#{chapter_range_separator}#{max_chapter}" if max_chapter > min_chapter
189
+ last_verse = Pericope.get_max_verse(book, range.end.chapter)
190
+ if !always_print_verse_range && range.begin.verse == 1 && range.begin.whole? && (range.end.verse > last_verse || range.end.whole? && range.end.verse == last_verse)
191
+ s << range.begin.chapter.to_s
192
+ s << "#{chapter_range_separator}#{range.end.chapter}" if range.end.chapter > range.begin.chapter
210
193
  else
211
- if recent_chapter == min_chapter
212
- s << min_verse.to_s
194
+ if range.begin.partial? && range.begin.verse == recent_verse
195
+ s << range.begin.letter
213
196
  else
214
- recent_chapter = min_chapter
215
- s << "#{min_chapter}:#{min_verse}"
197
+ s << range.begin.to_s(with_chapter: recent_chapter != range.begin.chapter)
216
198
  end
217
199
 
218
- if range.count > 1
219
-
220
- if min_chapter == max_chapter
221
- s << "#{verse_range_separator}#{max_verse}"
200
+ if range.begin != range.end
201
+ if range.begin.chapter == range.end.chapter
202
+ s << "#{verse_range_separator}#{range.end}"
222
203
  else
223
- recent_chapter = max_chapter
224
- s << "#{chapter_range_separator}#{max_chapter}:#{max_verse}"
204
+ s << "#{chapter_range_separator}#{range.end.to_s(with_chapter: true)}"
225
205
  end
226
206
  end
227
- end
228
- end
229
-
230
- s
231
- end
232
-
233
-
234
207
 
235
- def intersects?(pericope)
236
- return false unless pericope.is_a?(Pericope)
237
- return false unless book == pericope.book
238
-
239
- ranges.each do |self_range|
240
- pericope.ranges.each do |other_range|
241
- return true if (self_range.end >= other_range.begin) and (self_range.begin <= other_range.end)
208
+ recent_chapter = range.end.chapter
209
+ recent_verse = range.end.verse if range.end.partial?
242
210
  end
243
211
  end
244
-
245
- false
246
- end
247
-
248
-
249
-
250
- def self.get_max_verse(book, chapter)
251
- id = (book * 1000000) + (chapter * 1000)
252
- CHAPTER_VERSE_COUNTS[id]
253
- end
254
-
255
- def self.get_max_chapter(book)
256
- BOOK_CHAPTER_COUNTS[book]
257
- end
258
-
259
-
260
-
261
- private
262
-
263
-
264
-
265
- def set_book(value)
266
- @book = value || raise(ArgumentError, "must specify book")
267
- @book_name = Pericope::BOOK_NAMES[@book]
268
- @book_chapter_count = Pericope::BOOK_CHAPTER_COUNTS[@book]
269
- end
270
-
271
-
272
-
273
- def self.get_first_verse(book, chapter)
274
- get_id(book, chapter, 1)
275
212
  end
276
213
 
277
- def self.get_last_verse(book, chapter)
278
- get_id(book, chapter, get_max_verse(book, chapter))
279
- end
280
-
281
- def self.get_next_verse(id)
282
- id + 1
283
- end
284
-
285
- def self.get_start_of_next_chapter(id)
286
- book = get_book(id)
287
- chapter = get_chapter(id) + 1
288
- verse = 1
289
- get_id(book, chapter, verse)
290
- end
291
-
292
- def self.to_valid_book(book)
293
- coerce_to_range(book, 1..66)
294
- end
295
-
296
- def self.to_valid_chapter(book, chapter)
297
- coerce_to_range(chapter, 1..get_max_chapter(book))
298
- end
299
-
300
- def self.to_valid_verse(book, chapter, verse)
301
- coerce_to_range(verse, 1..get_max_verse(book, chapter))
302
- end
214
+ def group_array_into_ranges(verses)
215
+ return [] if verses.nil? or verses.empty?
303
216
 
304
- def self.coerce_to_range(number, range)
305
- return range.begin if number < range.begin
306
- return range.end if number > range.end
307
- number
308
- end
309
-
310
- def self.get_id(book, chapter, verse)
311
- book = to_valid_book(book)
312
- chapter = to_valid_chapter(book, chapter)
313
- verse = to_valid_verse(book, chapter, verse)
314
-
315
- (book * 1000000) + (chapter * 1000) + verse
316
- end
317
-
318
- def self.get_book(id)
319
- id / 1000000 # the book is everything left of the least significant 6 digits
320
- end
321
-
322
- def self.get_chapter(id)
323
- (id % 1000000) / 1000 # the chapter is the 3rd through 6th most significant digits
324
- end
325
-
326
- def self.get_verse(id)
327
- id % 1000 # the verse is the 3 least significant digits
328
- end
329
-
330
-
331
-
332
- def self.group_array_into_ranges(array)
333
- return [] if array.nil? or array.empty?
334
-
335
- array.flatten!
336
- array.compact!
337
- array.sort!
217
+ verses = verses.flatten.compact.sort.map { |verse| Verse.parse(verse) }
338
218
 
339
219
  ranges = []
340
- range_start = array.shift
341
- range_end = range_start
342
- while true
343
- next_value = array.shift
344
- break if next_value.nil?
345
-
346
- if (next_value == get_next_verse(range_end)) ||
347
- (next_value == get_start_of_next_chapter(range_end))
348
- range_end = next_value
220
+ range_begin = verses.shift
221
+ range_end = range_begin
222
+ while verse = verses.shift
223
+ if verse > range_end.next
224
+ ranges << Range.new(range_begin, range_end)
225
+ range_begin = range_end = verse
349
226
  else
350
- ranges << (range_start..range_end)
351
- range_start = range_end = next_value
227
+ range_end = verse
352
228
  end
353
229
  end
354
- ranges << (range_start..range_end)
355
-
356
- ranges
357
- end
358
-
359
-
360
-
361
- # matches the first valid Bible reference in the supplied string
362
- def self.match_one(text)
363
- match_all(text) do |attributes|
364
- return attributes
365
- end
366
- nil
367
- end
368
-
369
-
370
-
371
- # matches all valid Bible references in the supplied string
372
- def self.match_all(text)
373
- text.scan(Pericope::PERICOPE_PATTERN) do
374
- match = Regexp.last_match
375
- book = BOOK_IDS[match.captures.find_index(&:itself)]
376
-
377
- ranges = parse_reference(book, match[67])
378
- next if ranges.empty?
379
-
380
- attributes = {
381
- :original_string => match.to_s,
382
- :book => book,
383
- :ranges => ranges
384
- }
385
-
386
- yield attributes, match
387
- end
388
- end
389
-
390
- def self.parse_reference(book, reference)
391
- parse_ranges(book, normalize_reference(reference).split(/[,;]/))
392
- end
393
230
 
394
- def self.normalize_reference(reference)
395
- NORMALIZATIONS.reduce(reference.to_s) { |reference, (regex, replacement)| reference.gsub(regex, replacement) }
231
+ ranges << Range.new(range_begin, range_end)
396
232
  end
397
233
 
398
- def self.parse_ranges(book, ranges)
399
- recent_chapter = nil # e.g. in 12:1-8, remember that 12 is the chapter when we parse the 8
400
- recent_chapter = 1 if !book_has_chapters?(book)
401
- ranges.map do |range|
402
- range = range.split('-') # parse the low end of a verse range and the high end separately
403
- range << range[0] if (range.length < 2) # treat 12:4 as 12:4-12:4
404
- lower_chapter_verse = range[0].split(':').map(&:to_i) # parse "3:28" to [3,28]
405
- upper_chapter_verse = range[1].split(':').map(&:to_i) # parse "3:28" to [3,28]
406
-
407
- # treat Mark 3-1 as Mark 3-3 and, eventually, Mark 3:1-35
408
- if (lower_chapter_verse.length == 1) &&
409
- (upper_chapter_verse.length == 1) &&
410
- (upper_chapter_verse[0] < lower_chapter_verse[0])
411
- upper_chapter_verse = lower_chapter_verse.dup
412
- end
413
-
414
- # make sure the low end of the range and the high end of the range
415
- # are composed of arrays with two appropriate values: [chapter, verse]
416
- chapter_range = false
417
- if lower_chapter_verse.length < 2
418
- if recent_chapter
419
- lower_chapter_verse.unshift recent_chapter # e.g. parsing 11 in 12:1-8,11 => remember that 12 is the chapter
420
- else
421
- lower_chapter_verse[0] = Pericope.to_valid_chapter(book, lower_chapter_verse[0])
422
- lower_chapter_verse << 1 # no verse specified; this is a range of chapters, start with verse 1
423
- chapter_range = true
424
- end
425
- else
426
- lower_chapter_verse[0] = Pericope.to_valid_chapter(book, lower_chapter_verse[0])
427
- end
428
- lower_chapter_verse[1] = Pericope.to_valid_verse(book, *lower_chapter_verse)
429
-
430
- if upper_chapter_verse.length < 2
431
- if chapter_range
432
- upper_chapter_verse[0] = Pericope.to_valid_chapter(book, upper_chapter_verse[0])
433
- upper_chapter_verse << Pericope.get_max_verse(book, upper_chapter_verse[0]) # this is a range of chapters, end with the last verse
434
- else
435
- upper_chapter_verse.unshift lower_chapter_verse[0] # e.g. parsing 8 in 12:1-8 => remember that 12 is the chapter
436
- end
437
- else
438
- upper_chapter_verse[0] = Pericope.to_valid_chapter(book, upper_chapter_verse[0])
439
- end
440
- upper_chapter_verse[1] = Pericope.to_valid_verse(book, *upper_chapter_verse)
441
-
442
- recent_chapter = upper_chapter_verse[0] # remember the last chapter
443
-
444
- Range.new(
445
- Pericope.get_id(book, *lower_chapter_verse),
446
- Pericope.get_id(book, *upper_chapter_verse))
447
- end
448
- end
449
-
450
-
451
-
452
- BOOK_PATTERN = %r{\b(?:
453
- (?:(?:3|iii|third|3rd)\s*(?:
454
- (john|joh|jon|jhn|jh|jo|jn)
455
- ))|
456
- (?:(?:2|ii|second|2nd)\s*(?:
457
- (samuels|samuel|sam|sa|sm)|
458
- (kings|king|kngs|kgs|kg|k)|
459
- (chronicles|chronicle|chron|chrn|chr)|
460
- (john|joh|jon|jhn|jh|jo|jn)|
461
- (corinthians?|cor?|corint?h?|corth)|
462
- (thessalonians?|thes{1,}|the?s?)|
463
- (timothy|tim|tm|ti)|
464
- (peter|pete|pet|ptr|pe|pt|pr)
465
- ))|
466
- (?:(?:1|i|first|1st)\s*(?:
467
- (samuels|samuel|sam|sa|sm)|
468
- (kings|king|kngs|kgs|kg|k)|
469
- (chronicles|chronicle|chron|chrn|chr)|
470
- (john|joh|jon|jhn|jh|jo|jn)|
471
- (corinthians?|cor?|corint?h?|corth)|
472
- (thessalonians?|thes{1,}|the?s?)|
473
- (timothy|tim|tm|ti)|
474
- (peter|pete|pet|ptr|pe|pt|pr)
475
- ))|
476
- (genesis|gen|gn|ge)|
477
- (exodus|exod|exo|exd|ex)|
478
- (leviticus|lev|levi|le|lv)|
479
- (numbers|number|numb|num|nmb|nu|nm)|
480
- (deuteronomy|deut|deu|dt)|
481
- (joshua|josh|jsh|jos)|
482
- (judges|jdgs|judg|jdg)|
483
- (ruth|rut|rth|ru)|
484
- (isaiah|isa|is|ia|isai|isah)|
485
- (ezra|ezr)|
486
- (nehemiah|neh|ne)|
487
- (esther|esth|est|es)|
488
- (job|jb)|
489
- (psalms|psalm|pslms|pslm|psm|psa|ps)|
490
- (proverbs|proverb|prov|prv|prvb|prvbs|pv)|
491
- (ecclesiastes|eccles|eccl|ecc|ecl)|
492
- ((?:the\s?)?song\s?of\s?solomon|(?:the\s?)?song\s?of\s?songs|sn?gs?|songs?|so?s|sol?|son|s\s?of\s?\ss)|
493
- (jeremiah?|jer?|jr|jere)|
494
- (lamentations?|lam?|lm)|
495
- (ezekiel|ezek|eze|ezk)|
496
- (daniel|dan|dn|dl|da)|
497
- (hosea|hos|ho|hs)|
498
- (joel|jl)|
499
- (amos|amo|ams|am)|
500
- (obadiah|obadia|obad|oba|obd|ob)|
501
- (jonah|jon)|
502
- (micah|mica|mic|mi)|
503
- (nahum|nah|nahu|na)|
504
- (habakk?uk|habk?)|
505
- (zephaniah?|ze?ph?)|
506
- (haggai|ha?gg?)|
507
- (zechariah?|ze?ch?)|
508
- (malachi|mal)|
509
- (matthew|matt|mat|ma|mt)|
510
- (mark|mrk|mk)|
511
- (luke|luk|lk|lu)|
512
- (john|joh|jon|jhn|jh|jo|jn)|
513
- (acts|act|ac)|
514
- (romans|roman|roms|rom|rms|ro|rm)|
515
- (galatians|galatian|galat|gala|gal|ga)|
516
- (ephesians?|eph?|ephe?s?)|
517
- (philippians?|phi?l|php|phi|philipp?)|
518
- (colossi?ans?|col?)|
519
- (titus|tit|ti)|
520
- (philemon|phl?mn?|philem?)|
521
- (hebrews|hebrew|heb)|
522
- (james|jam|jas|jm|js|ja)|
523
- (jude)|
524
- (revelations|revelation|revel|rev|rv|re)
525
- )}ix.freeze
526
-
527
- # The order books of the Bible are matched
528
- BOOK_IDS = [ 64, 10, 12, 14, 63, 47, 53, 55, 61, 9, 11, 13, 62, 46, 52, 54, 60, 1, 2, 3, 4, 5, 6, 7, 8, 23, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 56, 57, 58, 59, 65, 66 ].freeze
529
-
530
- BOOK_NAMES = [nil, "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job", "Psalm", "Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah", "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation"].freeze
531
-
532
- REFERENCE_PATTERN = '(?:\s*\d{1,3})(?:\s*[:\"\.]\s*\d{1,3}[ab]?(?:\s*[,;]\s*(?:\d{1,3}[:\"\.])?\s*\d{1,3}[ab]?)*)?(?:\s*[-–—]\s*(?:\d{1,3}\s*[:\"\.])?(?:\d{1,3}[ab]?)(?:\s*[,;]\s*(?:\d{1,3}\s*[:\"\.])?\s*\d{1,3}[ab]?)*)*'
533
-
534
- PERICOPE_PATTERN = /#{BOOK_PATTERN.source.gsub(/[ \n]/, "")}\.?(#{REFERENCE_PATTERN})/i
535
-
536
- NORMALIZATIONS = [
537
- [/(\d+)[".](\d+)/, '\1:\2'], # 12"5 and 12.5 -> 12:5
538
- [/[–—]/, '-'], # convert em dash and en dash to -
539
- [/[^0-9,:;\-–—]/, ''] # remove everything but [0-9,;:-]
540
- ]
541
234
  end
235
+
236
+ Pericope.max_letter = "d"
@@ -4,11 +4,7 @@ require "cli/base"
4
4
  class Pericope
5
5
  class CLI
6
6
 
7
-
8
-
9
- ALLOWED_COMMANDS = %w{help normalize parse substitute reverse-substitute usage}
10
-
11
-
7
+ ALLOWED_COMMANDS = %w{help normalize parse usage}
12
8
 
13
9
  def self.run(command, *args)
14
10
  if ALLOWED_COMMANDS.member?(command)
@@ -56,26 +52,6 @@ Glossary
56
52
 
57
53
 
58
54
 
59
- def substitute
60
- begin
61
- print Pericope.sub(input)
62
- rescue
63
- print $!.to_s
64
- end
65
- end
66
-
67
-
68
-
69
- def reverse_substitute
70
- begin
71
- print Pericope.rsub(input)
72
- rescue
73
- print $!.to_s
74
- end
75
- end
76
-
77
-
78
-
79
55
  def usage
80
56
  print <<-USAGE
81
57
 
@@ -88,8 +64,6 @@ Commands
88
64
  help Prints more information about pericope
89
65
  normalize Accepts a pericope and returns a properly-formatted pericope
90
66
  parse Accepts a pericope and returns a list of verse IDs
91
- substitute Accepts a block of text and replaces all pericopes in the text with verse IDs
92
- reverse-substitute Accepts a block of text and replaces collections of verse IDs with pericopes
93
67
  usage Prints this message
94
68
 
95
69
  USAGE
@@ -0,0 +1,264 @@
1
+ require "pericope/range"
2
+ require "pericope/verse"
3
+
4
+ class Pericope
5
+ module Parsing
6
+
7
+ # Differs from Pericope.new in that it won't raise an exception
8
+ # if text does not contain a pericope but will return nil instead.
9
+ def parse_one(text)
10
+ parse(text) do |pericope|
11
+ return pericope
12
+ end
13
+ nil
14
+ end
15
+
16
+ def parse(text)
17
+ pericopes = []
18
+ match_all(text) do |attributes|
19
+ pericope = Pericope.new(attributes)
20
+ if block_given?
21
+ yield pericope
22
+ else
23
+ pericopes << pericope
24
+ end
25
+ end
26
+ block_given? ? text : pericopes
27
+ end
28
+
29
+ def split(text)
30
+ segments = []
31
+ start = 0
32
+
33
+ match_all(text) do |attributes, match|
34
+
35
+ pretext = text.slice(start...match.begin(0))
36
+ if pretext.length > 0
37
+ segments << pretext
38
+ yield pretext if block_given?
39
+ end
40
+
41
+ pericope = Pericope.new(attributes)
42
+ segments << pericope
43
+ yield pericope if block_given?
44
+
45
+ start = match.end(0)
46
+ end
47
+
48
+ pretext = text.slice(start...text.length)
49
+ if pretext.length > 0
50
+ segments << pretext
51
+ yield pretext if block_given?
52
+ end
53
+
54
+ segments
55
+ end
56
+
57
+ def match_one(text)
58
+ match_all(text) do |attributes|
59
+ return attributes
60
+ end
61
+ nil
62
+ end
63
+
64
+ def match_all(text)
65
+ text.scan(Pericope.regexp) do
66
+ match = Regexp.last_match
67
+ book = BOOK_IDS[match.captures.find_index(&:itself)]
68
+
69
+ ranges = parse_reference(book, match[67])
70
+ next if ranges.empty?
71
+
72
+ attributes = {
73
+ :original_string => match.to_s,
74
+ :book => book,
75
+ :ranges => ranges
76
+ }
77
+
78
+ yield attributes, match
79
+ end
80
+ end
81
+
82
+ def parse_reference(book, reference)
83
+ parse_ranges(book, normalize_reference(reference).split(/[,;]/))
84
+ end
85
+
86
+ def normalize_reference(reference)
87
+ normalizations.reduce(reference.to_s) { |reference, (regex, replacement)| reference.gsub(regex, replacement) }
88
+ end
89
+
90
+ def parse_ranges(book, ranges)
91
+ default_chapter = nil
92
+ default_chapter = 1 unless book_has_chapters?(book)
93
+ default_verse = nil
94
+
95
+ ranges.map do |range|
96
+ range_begin_string, range_end_string = range.split("-")
97
+
98
+ # treat 12:4 as 12:4-12:4
99
+ range_end_string ||= range_begin_string
100
+
101
+ range_begin = parse_reference_fragment(range_begin_string, default_chapter: default_chapter, default_verse: default_verse)
102
+
103
+ # no verse specified; this is a range of chapters, start with verse 1
104
+ chapter_range = false
105
+ if range_begin.needs_verse?
106
+ range_begin.verse = 1
107
+ chapter_range = true
108
+ end
109
+
110
+ range_begin.chapter = to_valid_chapter(book, range_begin.chapter)
111
+ range_begin.verse = to_valid_verse(book, range_begin.chapter, range_begin.verse)
112
+
113
+ if range_begin_string == range_end_string && !chapter_range
114
+ range_end = range_begin.dup
115
+ else
116
+ range_end = parse_reference_fragment(range_end_string, default_chapter: (range_begin.chapter unless chapter_range))
117
+ range_end.chapter = to_valid_chapter(book, range_end.chapter)
118
+
119
+ # treat Mark 3-1 as Mark 3-3 and, eventually, Mark 3:1-35
120
+ range_end.chapter = range_begin.chapter if range_end.chapter < range_begin.chapter
121
+
122
+ # this is a range of chapters, end with the last verse
123
+ if range_end.needs_verse?
124
+ range_end.verse = get_max_verse(book, range_end.chapter)
125
+ else
126
+ range_end.verse = to_valid_verse(book, range_end.chapter, range_end.verse)
127
+ end
128
+ end
129
+
130
+ # e.g. parsing 11 in 12:1-8,11 => remember that 12 is the chapter
131
+ default_chapter = range_end.chapter
132
+
133
+ # e.g. parsing c in 9:12a, c => remember that 12 is the verse
134
+ default_verse = range_end.verse
135
+
136
+ range = Range.new(range_begin.to_verse(book: book), range_end.to_verse(book: book))
137
+
138
+ # an 'a' at the beginning of a range is redundant
139
+ range.begin.letter = nil if range.begin.letter == "a" && range.end.to_i > range.begin.to_i
140
+
141
+ # a 'c' at the end of a range is redundant
142
+ range.end.letter = nil if range.end.letter == max_letter && range.end.to_i > range.begin.to_i
143
+
144
+ range
145
+ end
146
+ end
147
+
148
+ def parse_reference_fragment(input, default_chapter: nil, default_verse: nil)
149
+ chapter, verse, letter = input.match(Pericope.fragment_regexp).captures
150
+ chapter = default_chapter unless chapter
151
+ chapter, verse = [verse, nil] unless chapter
152
+ verse = default_verse unless verse
153
+ letter = nil unless verse
154
+ ReferenceFragment.new(chapter.to_i, verse&.to_i, letter)
155
+ end
156
+
157
+ def to_valid_chapter(book, chapter)
158
+ coerce_to_range(chapter, 1..get_max_chapter(book))
159
+ end
160
+
161
+ def to_valid_verse(book, chapter, verse)
162
+ coerce_to_range(verse, 1..get_max_verse(book, chapter))
163
+ end
164
+
165
+ def coerce_to_range(number, range)
166
+ return range.begin if number < range.begin
167
+ return range.end if number > range.end
168
+ number
169
+ end
170
+
171
+
172
+ ReferenceFragment = Struct.new(:chapter, :verse, :letter) do
173
+ def needs_verse?
174
+ verse.nil?
175
+ end
176
+
177
+ def to_verse(book:)
178
+ Verse.new(book, chapter, verse, letter)
179
+ end
180
+ end
181
+ end
182
+
183
+
184
+ BOOK_PATTERN = %r{\b(?:
185
+ (?:(?:3|iii|third|3rd)\s*(?:
186
+ (john|joh|jon|jhn|jh|jo|jn)
187
+ ))|
188
+ (?:(?:2|ii|second|2nd)\s*(?:
189
+ (samuels|samuel|sam|sa|sm)|
190
+ (kings|king|kngs|kgs|kg|k)|
191
+ (chronicles|chronicle|chron|chrn|chr)|
192
+ (john|joh|jon|jhn|jh|jo|jn)|
193
+ (corinthians?|cor?|corint?h?|corth)|
194
+ (thessalonians?|thes{1,}|the?s?)|
195
+ (timothy|tim|tm|ti)|
196
+ (peter|pete|pet|ptr|pe|pt|pr)
197
+ ))|
198
+ (?:(?:1|i|first|1st)\s*(?:
199
+ (samuels|samuel|sam|sa|sm)|
200
+ (kings|king|kngs|kgs|kg|k)|
201
+ (chronicles|chronicle|chron|chrn|chr)|
202
+ (john|joh|jon|jhn|jh|jo|jn)|
203
+ (corinthians?|cor?|corint?h?|corth)|
204
+ (thessalonians?|thes{1,}|the?s?)|
205
+ (timothy|tim|tm|ti)|
206
+ (peter|pete|pet|ptr|pe|pt|pr)
207
+ ))|
208
+ (genesis|gen|gn|ge)|
209
+ (exodus|exod|exo|exd|ex)|
210
+ (leviticus|lev|levi|le|lv)|
211
+ (numbers|number|numb|num|nmb|nu|nm)|
212
+ (deuteronomy|deut|deu|dt)|
213
+ (joshua|josh|jsh|jos)|
214
+ (judges|jdgs|judg|jdg)|
215
+ (ruth|rut|rth|ru)|
216
+ (isaiah|isa|is|ia|isai|isah)|
217
+ (ezra|ezr)|
218
+ (nehemiah|neh|ne)|
219
+ (esther|esth|est|es)|
220
+ (job|jb)|
221
+ (psalms|psalm|pslms|pslm|psm|psa|ps)|
222
+ (proverbs|proverb|prov|prv|prvb|prvbs|pv)|
223
+ (ecclesiastes|eccles|eccl|ecc|ecl)|
224
+ ((?:the\s?)?song\s?of\s?solomon|(?:the\s?)?song\s?of\s?songs|sn?gs?|songs?|so?s|sol?|son|s\s?of\s?\ss)|
225
+ (jeremiah?|jer?|jr|jere)|
226
+ (lamentations?|lam?|lm)|
227
+ (ezekiel|ezek|eze|ezk)|
228
+ (daniel|dan|dn|dl|da)|
229
+ (hosea|hos|ho|hs)|
230
+ (joel|jl)|
231
+ (amos|amo|ams|am)|
232
+ (obadiah|obadia|obad|oba|obd|ob)|
233
+ (jonah|jon)|
234
+ (micah|mica|mic|mi)|
235
+ (nahum|nah|nahu|na)|
236
+ (habakk?uk|habk?)|
237
+ (zephaniah?|ze?ph?)|
238
+ (haggai|ha?gg?)|
239
+ (zechariah?|ze?ch?)|
240
+ (malachi|mal)|
241
+ (matthew|matt|mat|ma|mt)|
242
+ (mark|mrk|mk)|
243
+ (luke|luk|lk|lu)|
244
+ (john|joh|jon|jhn|jh|jo|jn)|
245
+ (acts|act|ac)|
246
+ (romans|roman|roms|rom|rms|ro|rm)|
247
+ (galatians|galatian|galat|gala|gal|ga)|
248
+ (ephesians?|eph?|ephe?s?)|
249
+ (philippians?|phi?l|php|phi|philipp?)|
250
+ (colossi?ans?|col?)|
251
+ (titus|tit|ti)|
252
+ (philemon|phl?mn?|philem?)|
253
+ (hebrews|hebrew|heb)|
254
+ (james|jam|jas|jm|js|ja)|
255
+ (jude)|
256
+ (revelations|revelation|revel|rev|rv|re)
257
+ )}ix.freeze
258
+
259
+ # The order books of the Bible are matched
260
+ BOOK_IDS = [ 64, 10, 12, 14, 63, 47, 53, 55, 61, 9, 11, 13, 62, 46, 52, 54, 60, 1, 2, 3, 4, 5, 6, 7, 8, 23, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 56, 57, 58, 59, 65, 66 ].freeze
261
+
262
+ BOOK_NAMES = [nil, "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job", "Psalm", "Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah", "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation"].freeze
263
+
264
+ end
@@ -0,0 +1,58 @@
1
+ class Pericope
2
+ class Range
3
+ include Enumerable
4
+
5
+ def initialize(first, last)
6
+ @begin = first
7
+ @end = last
8
+ end
9
+
10
+ attr_reader :begin
11
+ attr_reader :end
12
+
13
+ def ==(other)
14
+ return true if equal? other
15
+
16
+ other.kind_of?(Pericope::Range) and
17
+ self.begin == other.begin and
18
+ self.end == other.end
19
+ end
20
+
21
+ alias_method :eql?, :==
22
+
23
+ def hash
24
+ self.begin.hash ^ self.end.hash
25
+ end
26
+
27
+ def each
28
+ return to_enum unless block_given?
29
+
30
+ if self.begin == self.end
31
+ yield self.begin
32
+ return self
33
+ end
34
+
35
+ current = self.begin
36
+ last_verse = self.end.whole
37
+ while current < last_verse
38
+ yield current
39
+ current = current.succ
40
+ end
41
+
42
+ if self.end.partial?
43
+ "a".upto(self.end.letter).each do |letter|
44
+ yield Verse.new(self.end.book, self.end.chapter, self.end.verse, letter)
45
+ end
46
+ else
47
+ yield self.end
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ def inspect
54
+ "#{self.begin.to_id}..#{self.end.to_id}"
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,74 @@
1
+ class Pericope
2
+ Verse = Struct.new(:book, :chapter, :verse, :letter) do
3
+ include Comparable
4
+
5
+ def initialize(book, chapter, verse, letter=nil)
6
+ super
7
+
8
+ raise ArgumentError, "#{book} is not a valid book" if book < 1 || book > 66
9
+ raise ArgumentError, "#{chapter} is not a valid chapter" if chapter < 1 || chapter > Pericope.get_max_chapter(book)
10
+ raise ArgumentError, "#{verse} is not a valid verse" if verse < 1 || verse > Pericope.get_max_verse(book, chapter)
11
+ end
12
+
13
+ def self.parse(input)
14
+ return nil unless input
15
+ id = input.to_i
16
+ book = id / 1000000 # the book is everything left of the least significant 6 digits
17
+ chapter = (id % 1000000) / 1000 # the chapter is the 3rd through 6th most significant digits
18
+ verse = id % 1000 # the verse is the 3 least significant digits
19
+ letter = input[Pericope.letter_regexp] if input.is_a?(String)
20
+ new(book, chapter, verse, letter)
21
+ end
22
+
23
+ def <=>(other)
24
+ [ book, chapter, verse, letter || "a" ] <=> [ other.book, other.chapter, other.verse, other.letter || "a" ]
25
+ end
26
+
27
+ def ==(other)
28
+ to_a == other.to_a
29
+ end
30
+
31
+ def to_i
32
+ book * 1000000 + chapter * 1000 + verse
33
+ end
34
+ alias :number :to_i
35
+
36
+ def to_id
37
+ "#{to_i}#{letter}"
38
+ end
39
+
40
+ def to_s(with_chapter: false)
41
+ with_chapter ? "#{chapter}:#{verse}#{letter}" : "#{verse}#{letter}"
42
+ end
43
+
44
+ def partial?
45
+ !letter.nil?
46
+ end
47
+
48
+ def whole?
49
+ letter.nil?
50
+ end
51
+
52
+ def whole
53
+ return self unless partial?
54
+ self.class.new(book, chapter, verse)
55
+ end
56
+
57
+ def next
58
+ if partial? && (next_letter = letter.succ) <= Pericope.max_letter
59
+ return self.class.new(book, chapter, verse, next_letter)
60
+ end
61
+
62
+ next_verse = verse + 1
63
+ if next_verse > Pericope.get_max_verse(book, chapter)
64
+ next_chapter = chapter + 1
65
+ return nil if next_chapter > Pericope.get_max_chapter(book)
66
+ self.class.new(book, next_chapter, 1)
67
+ else
68
+ self.class.new(book, chapter, next_verse)
69
+ end
70
+ end
71
+ alias :succ :next
72
+
73
+ end
74
+ end
@@ -1,3 +1,3 @@
1
1
  class Pericope
2
- VERSION = "0.7.2" unless defined?(::Pericope::Version)
2
+ VERSION = "1.0.0" unless defined?(::Pericope::Version)
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pericope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Lail
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-21 00:00:00.000000000 Z
11
+ date: 2018-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: shoulda-context
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -100,8 +114,10 @@ files:
100
114
  - lib/pericope.rb
101
115
  - lib/pericope/cli.rb
102
116
  - lib/pericope/data.rb
117
+ - lib/pericope/parsing.rb
118
+ - lib/pericope/range.rb
119
+ - lib/pericope/verse.rb
103
120
  - lib/pericope/version.rb
104
- - lib/tasks/pericope_tasks.rake
105
121
  homepage: http://github.com/boblail/pericope
106
122
  licenses: []
107
123
  metadata: {}
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :pericope do
3
- # # Task goes here
4
- # end