bible_bot 2.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
+ module BibleBot
2
+ # Represents one of the 66 books in the bible (Genesis - Revelation).
3
+ # You should never need to initialize a Book, they are initialized in {Bible}.
4
+ class Book
5
+ attr_reader :id # @return [Integer]
6
+ attr_reader :name # @return [String]
7
+ attr_reader :abbreviation # @return [String]
8
+ attr_reader :regex # @return [String]
9
+ attr_reader :chapters # @return [Array<Integer>]
10
+ attr_reader :testament # @return [String]
11
+
12
+ # Uses the same Regex pattern to match as we use in {Reference.parse}.
13
+ # So this supports the same book name abbreviations.
14
+ #
15
+ # @param name [String]
16
+ # @return [Book]
17
+ # @example
18
+ # Book.find_by_name("Genesis")
19
+ def self.find_by_name(name)
20
+ return nil if name.nil? || name.strip == ""
21
+
22
+ Bible.books.find { |book| name.match(Regexp.new('\b'+book.regex+'\b', Regexp::IGNORECASE)) }
23
+ end
24
+
25
+ # Find by the Book ID defined in {Bible}.
26
+ #
27
+ # @param id [Integer]
28
+ # @return [Book]
29
+ def self.find_by_id(id)
30
+ Bible.books.find { |book| book.id == id }
31
+ end
32
+
33
+ def initialize(id:, name:, abbreviation:, regex:, chapters: [] , testament:)
34
+ @id = id
35
+ @name = name
36
+ @abbreviation = abbreviation
37
+ @regex = regex
38
+ @chapters = chapters
39
+ @testament = testament
40
+ end
41
+
42
+ # @return [String]
43
+ def formatted_name
44
+ case name
45
+ when 'Psalms' then 'Psalm'
46
+ else name
47
+ end
48
+ end
49
+
50
+ # Single chapter book like Jude
51
+ # @return [Boolean]
52
+ def single_chapter?
53
+ chapters.length == 1
54
+ end
55
+
56
+ # A reference containing the entire book
57
+ # @return [Reference]
58
+ def reference
59
+ @reference ||= Reference.new(start_verse: start_verse, end_verse: end_verse)
60
+ end
61
+
62
+ # @return [Verse]
63
+ def start_verse
64
+ @first_verse ||= Verse.from_id("#{id}001001".to_i)
65
+ end
66
+
67
+ # @return [Verse]
68
+ def end_verse
69
+ @last_verse ||= Verse.from_id(
70
+ "#{id}#{chapters.length.to_s.rjust(3, '0')}#{chapters.last.to_s.rjust(3, '0')}".to_i
71
+ )
72
+ end
73
+
74
+ # @return [Book, nil]
75
+ def next_book
76
+ return @next_book if defined? @next_book
77
+ @next_book = Book.find_by_id(id + 1)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,17 @@
1
+ module BibleBot
2
+ class BibleBotError < StandardError
3
+ end
4
+
5
+ # Raised if Reference is not valid.
6
+ # @example
7
+ # "Genesis 4-2"
8
+ class InvalidReferenceError < BibleBotError
9
+ end
10
+
11
+ # Raised if Verse is not valid.
12
+ # In other words, if a chapter or verse are referenced that don't actually exist.
13
+ # @example
14
+ # "Genesis 100:2"
15
+ class InvalidVerseError < BibleBotError
16
+ end
17
+ end
@@ -0,0 +1,148 @@
1
+ module BibleBot
2
+ # A Reference represents a range of verses.
3
+ class Reference
4
+ attr_reader :start_verse # @return [Verse]
5
+ attr_reader :end_verse # @return [Verse]
6
+
7
+ # Initialize a {Reference} from {Verse} IDs. If no end_verse_id is provided, it will
8
+ # set end_verse to equal start_verse.
9
+ #
10
+ # @param start_verse_id [Integer]
11
+ # @param end_verse_id [Integer]
12
+ # @return [Reference]
13
+ # @example
14
+ # BibleBot::Reference.from_verse_ids(1001001, 1001010) #=> (Gen 1:1-10)
15
+ def self.from_verse_ids(start_verse_id, end_verse_id=nil)
16
+ new(
17
+ start_verse: Verse.from_id(start_verse_id),
18
+ end_verse: Verse.from_id(end_verse_id || start_verse_id),
19
+ )
20
+ end
21
+
22
+ # Parse text into an array of scripture References.
23
+ #
24
+ # @param text [String] ex: "John 1:1 is the first but Romans 8:9-10 is another."
25
+ # @param validate [Boolean, :raise_errors]
26
+ # * true - Skip invalid references (default)
27
+ # * false - Include invalid references
28
+ # * :raise_errors - Raise error if any references are invalid
29
+ # @return [Array<Reference>]
30
+ def self.parse(text, validate: true)
31
+ return [] if text.nil? || text.strip == ""
32
+
33
+ ReferenceMatch.scan(text).map(&:reference).select do |ref|
34
+ ref.validate! if validate == :raise_errors
35
+
36
+ !validate || ref.valid?
37
+ end
38
+ end
39
+
40
+ # @param start_verse [Verse]
41
+ # @param end_verse [Verse] Defaults to start_verse if no end_verse is provided
42
+ def initialize(start_verse:, end_verse: nil)
43
+ @start_verse = start_verse
44
+ @end_verse = end_verse || start_verse
45
+ end
46
+
47
+ # Returns a formatted string of the {Reference}.
48
+ #
49
+ # @return [String]
50
+ # @example
51
+ # reference.formatted #=> "Genesis 2:4-5:9"
52
+ def formatted
53
+ formatted_verses = [start_verse.formatted(include_verse: !full_chapters?)]
54
+
55
+ if end_verse && end_verse > start_verse && !(same_start_and_end_chapter? && full_chapters?)
56
+ formatted_verses << end_verse.formatted(
57
+ include_book: !same_start_and_end_book?,
58
+ include_chapter: !same_start_and_end_chapter?,
59
+ include_verse: !full_chapters?,
60
+ )
61
+ end
62
+
63
+ formatted_verses.join('-')
64
+ end
65
+
66
+ # @return [Boolean]
67
+ def same_start_and_end_book?
68
+ start_verse.book == end_verse&.book
69
+ end
70
+
71
+ # @return [Boolean]
72
+ def same_start_and_end_chapter?
73
+ same_start_and_end_book? &&
74
+ start_verse.chapter_number == end_verse&.chapter_number
75
+ end
76
+
77
+ # One or multiple full chapters.
78
+ #
79
+ # @return [Boolean]
80
+ def full_chapters?
81
+ start_verse.verse_number == 1 && end_verse&.last_verse_in_chapter?
82
+ end
83
+
84
+ # @return [string]
85
+ def to_s
86
+ "BibleBot::Reference — #{formatted}"
87
+ end
88
+
89
+ # Returns true if the given verse is within the start and end verse of the Reference.
90
+ #
91
+ # @param verse [Verse]
92
+ # @return [Boolean]
93
+ def includes_verse?(verse)
94
+ return false unless verse.is_a?(Verse)
95
+
96
+ start_verse <= verse && verse <= end_verse
97
+ end
98
+
99
+ # Return true if the two references contain any of the same verses.
100
+ # @param other [Reference]
101
+ # @return [Boolean]
102
+ def intersects_reference?(other)
103
+ return false unless other.is_a?(Reference)
104
+
105
+ start_verse <= other.end_verse && end_verse >= other.start_verse
106
+ end
107
+
108
+ # Returns an array of all the verses contained in the Reference.
109
+ #
110
+ # @return [Array<Verse>]
111
+ def verses
112
+ return @verses if defined? @verses
113
+
114
+ @verses = []
115
+ return @verses unless valid?
116
+
117
+ verse = start_verse
118
+
119
+ loop do
120
+ @verses << verse
121
+ break if end_verse.nil? || verse == end_verse
122
+ verse = verse.next_verse
123
+ end
124
+
125
+ @verses
126
+ end
127
+
128
+ # @return [Hash]
129
+ def inspect
130
+ {
131
+ start_verse: start_verse&.formatted,
132
+ end_verse: end_verse&.formatted,
133
+ }
134
+ end
135
+
136
+ # @return [Boolean]
137
+ def valid?
138
+ start_verse&.valid? && end_verse&.valid? && end_verse >= start_verse
139
+ end
140
+
141
+ # Raises error if reference is invalid
142
+ def validate!
143
+ start_verse&.validate!
144
+ end_verse&.validate!
145
+ raise InvalidReferenceError.new "Reference is not vaild: #{inspect}" unless valid?
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,141 @@
1
+ module BibleBot
2
+ # This class contains all the logic for mapping the different parts of a scripture Match into an actual {Reference}.
3
+ # It wraps the Match returned from the regular expression defined in {Bible.scripture_re}.
4
+ #
5
+ # A scripture reference can take many forms, but the least abbreviated form is:
6
+ #
7
+ # Genesis 1:1 - Genesis 1:2
8
+ #
9
+ # Internally, this class represents this form using the following variables:
10
+ #
11
+ # b1 c1:v1 - b2 c2:v2
12
+ #
13
+ # See Readme for list of supported abbreviation rules.
14
+ #
15
+ # == Advanced Use Cases
16
+ #
17
+ # This is a low level class used internally by {Reference.parse}.
18
+ # There are however some advanced use cases which you might want to use it for.
19
+ # For example, if you want to know where in the parsed String certain matches occur.
20
+ #
21
+ # For this there are a few convenience attributes:
22
+ #
23
+ # * {#match}
24
+ # * {#length}
25
+ # * {#offset}
26
+ #
27
+ # @example
28
+ # matches = ReferenceMatch.scan("Mark 1:5 and another Romans 4:1")
29
+ # matches[0].match[0] #=> "Mark 1:5"
30
+ # matches[0].offset #=> 0
31
+ # matches[0].length #=> 8
32
+ # matches[0].match[0] #=> "Romans 4:1"
33
+ # matches[1].offset #=> 21
34
+ # matches[1].length #=> 10
35
+ #
36
+ # @note You shouldn't need to use this class directly. For the majority of use cases, just use {Reference.parse}.
37
+ class ReferenceMatch
38
+ attr_reader :match # @return [Match] The Match instance returned from the Regexp
39
+ attr_reader :length # @return [Integer] The length of the match in the text string
40
+ attr_reader :offset # @return [Integer] The starting position of the match in the text string
41
+
42
+ # Converts a string into an array of ReferenceMatches.
43
+ # Note: Does not validate References.
44
+ #
45
+ # @param text [String]
46
+ # @return [Array<ReferenceMatch>]
47
+ def self.scan(text)
48
+ scripture_reg = Bible.scripture_re
49
+ Array.new.tap do |matches|
50
+ text.scan(scripture_reg){ matches << self.new($~, $~.offset(0)[0]) }
51
+ end
52
+ end
53
+
54
+ # @return [Reference] Note: Reference is not yet validated
55
+ def reference
56
+ @reference ||= Reference.new(
57
+ start_verse: Verse.new(book: start_book, chapter_number: start_chapter.to_i, verse_number: start_verse.to_i),
58
+ end_verse: Verse.new(book: end_book, chapter_number: end_chapter.to_i, verse_number: end_verse.to_i),
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :b1 # @return [String]
65
+ attr_reader :c1 # @return [String] Represents the number after the start Book name, could be either chapter or verse number.
66
+ attr_reader :v1 # @return [String, nil] Represents the number after the colon, will always be start_verse if present.
67
+ attr_reader :b2 # @return [String, nil]
68
+ attr_reader :c2 # @return [String, nil] Represents the number after the end Book name, could be either chapter or verse number.
69
+ attr_reader :v2 # @return [String, nil] Represents the number after the colon, will always be end_verse if present.
70
+
71
+ # @param match [Match]
72
+ # @param offset [Integer]
73
+ def initialize(match, offset)
74
+ @match = match
75
+ @length = match.to_s.length
76
+ @offset = offset
77
+ @b1 = match[:BookTitle]
78
+ @c1 = match[:ChapterNumber]
79
+ @v1 = match[:VerseNumber]
80
+ @b2 = match[:EndBookTitle]
81
+ @c2 = match[:EndChapterNumber]
82
+ @v2 = match[:EndVerseNumber]
83
+ end
84
+
85
+ # @return [Book]
86
+ def start_book
87
+ # There will always be a starting book.
88
+ Book.find_by_name(@b1)
89
+ end
90
+
91
+ # @return [Book]
92
+ def end_book
93
+ # The end book is optional. If not provided, default to starting book.
94
+ Book.find_by_name(@b2) || start_book
95
+ end
96
+
97
+ # @return [Integer]
98
+ def start_chapter
99
+ # Start chapter should always be provided, except in the case of single chapter books.
100
+ # Jude 5 for example, c1==5 but the chapter should actually be 1.
101
+ return 1 if start_book.single_chapter?
102
+ c1
103
+ end
104
+
105
+ # @return [Integer]
106
+ def start_verse
107
+ # If there is a number in the v1 position, it will always represent the starting verse.
108
+ # There are a few cases where the start_verse will be in a different position or inferred.
109
+ # * Jude 4 (start_verse is in the c1 position)
110
+ # * Genesis 5 (start_verse is inferred to be 1, and end_verse is the last verse in Genesis 5)
111
+ v1 || (start_book.single_chapter? ? c1 : 1)
112
+ end
113
+
114
+ # @return [Integer]
115
+ def end_chapter
116
+ return start_chapter if single_verse_ref? # Ex: Genesis 1:3 => "1"
117
+ return 1 if end_book.single_chapter? # Ex: Jude 2-4 => "1"
118
+ return c1 if !b2 && !v2 && v1 # Ex: Genesis 1:2-3 => "1"
119
+ c2 || # Ex: Genesis 1:1 - 2:4 => "4"
120
+ c1 # Ex: Genesis 5 => "5"
121
+ end
122
+
123
+ # @return [Integer]
124
+ def end_verse
125
+ return start_verse if single_verse_ref? # Ex: Genesis 1:3 => "3"
126
+ v2 || # Ex: Genesis 1:4 - 2:5 => "5"
127
+ (
128
+ (v1 && !b2) ?
129
+ c2 : # Ex: Gen 1:4-8 => "8"
130
+ end_book.chapters[end_chapter.to_i - 1] # Genesis 1 => "31"
131
+ )
132
+ end
133
+
134
+ # @return [Boolean]
135
+ def single_verse_ref?
136
+ !b2 && !c2 && !v2 &&
137
+ (v1 || start_book.single_chapter?) # Ex: Genesis 5:1 || Jude 5
138
+ # Genesis 5 is not a single verse ref
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,156 @@
1
+ module BibleBot
2
+ # Verse represents a single verse in the bible.
3
+ class Verse
4
+ include Comparable
5
+
6
+ attr_reader :book # @return [Book]
7
+ attr_reader :chapter_number # @return [Integer]
8
+ attr_reader :verse_number # @return [Integer]
9
+
10
+ # Turns an Inteter into a Verse
11
+ # For more details, see note above the `id` method.
12
+ #
13
+ # @param id [Integer]
14
+ # @return [Verse]
15
+ # @example
16
+ # Verse.from_id(19_105_001) #=> <Verse book="Psalms" chapter_number=105 verse_number=1>
17
+ def self.from_id(id)
18
+ return from_string_id(id) if id.is_a?(String)
19
+ return nil if id.nil?
20
+ raise BibleBot::InvalidVerseError unless id.is_a?(Integer)
21
+
22
+ book_id = id / 1_000_000
23
+ chapter_number = id / 1_000 % 1_000
24
+ verse_number = id % 1_000
25
+ book = BibleBot::Book.find_by_id(book_id)
26
+
27
+ new(book: book, chapter_number: chapter_number, verse_number: verse_number)
28
+ end
29
+
30
+ # @param book [Book]
31
+ # @param chapter_number [Integer]
32
+ # @param verse_number [Integer]
33
+ def initialize(book:, chapter_number:, verse_number:)
34
+ @book = book
35
+ @chapter_number = chapter_number
36
+ @verse_number = verse_number
37
+ end
38
+
39
+ # Returns an Integer in the from of
40
+ #
41
+ # |- book.id
42
+ # | |- chapter_number
43
+ # | | |- verse_number
44
+ # XX_XXX_XXX
45
+ #
46
+ # Storing as an Integer makes it super convenient to store in a database
47
+ # and compare verses and verse ranges using simple database queries
48
+ #
49
+ # @return [Integer]
50
+ # @example
51
+ # verse.id #=> 19_105_001
52
+ # #-> this represents "Psalm 105:1"
53
+ def id
54
+ @id ||= "#{book.id}#{chapter_number.to_s.rjust(3, '0')}#{verse_number.to_s.rjust(3, '0')}".to_i
55
+ end
56
+
57
+ # @deprecated Use {id} instead
58
+ # @return [String] ex: "psalms-023-001"
59
+ def string_id
60
+ "#{book.name.downcase.gsub(' ', '_')}-#{chapter_number.to_s.rjust(3, '0')}-#{verse_number.to_s.rjust(3, '0')}"
61
+ end
62
+
63
+ # The Comparable mixin uses this to define all the other comparable methods
64
+ #
65
+ # @param other [Verse]
66
+ # @return [Integer] Either -1, 0, or 1
67
+ # * -1: this verse is less than the other verse
68
+ # * 0: this verse is equal to the other verse
69
+ # * 1: this verse is greater than the other verse
70
+ def <=>(other)
71
+ id <=> other.id
72
+ end
73
+
74
+ # @param include_book [Boolean]
75
+ # @param include_chapter [Boolean]
76
+ # @param include_verse [Boolean]
77
+ # @return [String]
78
+ # @example
79
+ # verse.formatted #=> "Genesis 5:23"
80
+ def formatted(include_book: true, include_chapter: true, include_verse: true)
81
+ str = String.new # Using String.new because string literals will be frozen in Ruby 3.0
82
+ str << "#{book.formatted_name} " if include_book
83
+
84
+ if book.single_chapter?
85
+ str << "#{verse_number}" if include_verse
86
+ else
87
+ str << "#{chapter_number}" if include_chapter
88
+ str << ":" if include_chapter && include_verse
89
+ str << "#{verse_number}" if include_verse
90
+ end
91
+
92
+ str.strip.freeze
93
+ end
94
+
95
+ # Returns next verse. It will reach into the next chapter or the next book
96
+ # until it gets to the last verse in the bible,
97
+ # at which point it will return nil.
98
+ #
99
+ # @return [Verse, nil]
100
+ def next_verse
101
+ return Verse.new(book: book, chapter_number: chapter_number, verse_number: verse_number + 1) unless last_verse_in_chapter?
102
+ return Verse.new(book: book, chapter_number: chapter_number + 1, verse_number: 1) unless last_chapter_in_book?
103
+ return Verse.new(book: book.next_book, chapter_number: 1, verse_number: 1) if book.next_book
104
+ nil
105
+ end
106
+
107
+ # @return [Boolean]
108
+ def last_verse_in_chapter?
109
+ verse_number == book.chapters[chapter_number - 1]
110
+ end
111
+
112
+ # @return [Boolean]
113
+ def last_chapter_in_book?
114
+ chapter_number == book.chapters.length
115
+ end
116
+
117
+ # @return [Hash]
118
+ def inspect
119
+ {
120
+ book: book&.name,
121
+ chapter_number: chapter_number,
122
+ verse_number: verse_number
123
+ }
124
+ end
125
+
126
+ # @return [Boolean]
127
+ def valid?
128
+ book.is_a?(BibleBot::Book) &&
129
+ chapter_number.is_a?(Integer) && chapter_number >= 1 && chapter_number <= book.chapters.length &&
130
+ verse_number.is_a?(Integer) && verse_number >= 1 && verse_number <= book.chapters[chapter_number-1]
131
+ end
132
+
133
+ # Raises error if reference is invalid
134
+ def validate!
135
+ raise InvalidVerseError.new "Verse is not valid: #{inspect}" unless valid?
136
+ end
137
+
138
+ private
139
+
140
+ # This gets called by {from_id} to allow it to be backwards compatible for a while.
141
+ # @deprecated Use {from_id} instead.
142
+ # @param verse_id [String] ex: "genesis-001-001"
143
+ # @return [Verse] ex: <Verse book="Genesis" chapter_number=1 verse_number=1>
144
+ def self.from_string_id(string_id)
145
+ parts = string_id.split( '-' )
146
+
147
+ book_name = parts[0].gsub( '_', ' ' )
148
+ chapter_number = parts[1].to_i
149
+ verse_number = parts[2].to_i
150
+
151
+ book = BibleBot::Bible.books.select{ |b| b.name.downcase == book_name }.first
152
+
153
+ new(book: book, chapter_number: chapter_number, verse_number: verse_number)
154
+ end
155
+ end
156
+ end