bible_bot 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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