@@ -15,12 +15,12 @@ var BibleBooks = map[string]string{
1515 "joshua" : "Joshua" , "josh" : "Joshua" , "jos" : "Joshua" ,
1616 "judges" : "Judges" , "judg" : "Judges" , "jdg" : "Judges" ,
1717 "ruth" : "Ruth" , "ru" : "Ruth" , "rth" : "Ruth" ,
18- "1 samuel" : "1 Samuel" , "1 sam" : "1 Samuel" , "1 sa" : "1 Samuel" , "1sam" : "1 Samuel" , "1sa" : "1 Samuel" , "i sam" : "1 Samuel" ,
19- "2 samuel" : "2 Samuel" , "2 sam" : "2 Samuel" , "2 sa" : "2 Samuel" , "2sam" : "2 Samuel" , "2sa" : "2 Samuel" , "ii sam" : "2 Samuel" ,
20- "1 kings" : "1 Kings" , "1 kgs" : "1 Kings" , "1 ki" : "1 Kings" , "1kings" : "1 Kings" , "i kgs" : "1 Kings" ,
21- "2 kings" : "2 Kings" , "2 kgs" : "2 Kings" , "2 ki" : "2 Kings" , "2kings" : "2 Kings" , "ii kgs" : "2 Kings" ,
22- "1 chronicles" : "1 Chronicles" , "1 chron" : "1 Chronicles" , "1 chr" : "1 Chronicles" , "1 ch" : "1 Chronicles" , "1chronicles" : "1 Chronicles" , "i chron" : "1 Chronicles" ,
23- "2 chronicles" : "2 Chronicles" , "2 chron" : "2 Chronicles" , "2 chr" : "2 Chronicles" , "2 ch" : "2 Chronicles" , "2chronicles" : "2 Chronicles" , "ii chron" : "2 Chronicles" ,
18+ "1 samuel" : "1 Samuel" , "1 sam" : "1 Samuel" , "1 sa" : "1 Samuel" , "1sam" : "1 Samuel" , "1sa" : "1 Samuel" , "i sam" : "1 Samuel" , "i samuel" : "1 Samuel" , "1st samuel" : "1 Samuel" ,
19+ "2 samuel" : "2 Samuel" , "2 sam" : "2 Samuel" , "2 sa" : "2 Samuel" , "2sam" : "2 Samuel" , "2sa" : "2 Samuel" , "ii sam" : "2 Samuel" , "ii samuel" : "2 Samuel" , "2nd samuel" : "2 Samuel" ,
20+ "1 kings" : "1 Kings" , "1 kgs" : "1 Kings" , "1 ki" : "1 Kings" , "1kings" : "1 Kings" , "i kgs" : "1 Kings" , "i kings" : "1 Kings" , "1st kings" : "1 Kings" ,
21+ "2 kings" : "2 Kings" , "2 kgs" : "2 Kings" , "2 ki" : "2 Kings" , "2kings" : "2 Kings" , "ii kgs" : "2 Kings" , "ii kings" : "2 Kings" , "2nd kings" : "2 Kings" ,
22+ "1 chronicles" : "1 Chronicles" , "1 chron" : "1 Chronicles" , "1 chr" : "1 Chronicles" , "1 ch" : "1 Chronicles" , "1chronicles" : "1 Chronicles" , "i chron" : "1 Chronicles" , "i chronicles" : "1 Chronicles" , "1st chronicles" : "1 Chronicles" ,
23+ "2 chronicles" : "2 Chronicles" , "2 chron" : "2 Chronicles" , "2 chr" : "2 Chronicles" , "2 ch" : "2 Chronicles" , "2chronicles" : "2 Chronicles" , "ii chron" : "2 Chronicles" , "ii chronicles" : "2 Chronicles" , "2nd chronicles" : "2 Chronicles" ,
2424 "ezra" : "Ezra" , "ezr" : "Ezra" ,
2525 "nehemiah" : "Nehemiah" , "neh" : "Nehemiah" , "ne" : "Nehemiah" ,
2626 "esther" : "Esther" , "est" : "Esther" , "esth" : "Esther" ,
@@ -54,25 +54,25 @@ var BibleBooks = map[string]string{
5454 "john" : "John" , "jn" : "John" , "jhn" : "John" , "joh" : "John" ,
5555 "acts" : "Acts" , "ac" : "Acts" , "act" : "Acts" ,
5656 "romans" : "Romans" , "rom" : "Romans" , "ro" : "Romans" , "rm" : "Romans" ,
57- "1 corinthians" : "1 Corinthians" , "1 cor" : "1 Corinthians" , "1 co" : "1 Corinthians" , "1cor" : "1 Corinthians" , "i cor" : "1 Corinthians" ,
58- "2 corinthians" : "2 Corinthians" , "2 cor" : "2 Corinthians" , "2 co" : "2 Corinthians" , "2cor" : "2 Corinthians" , "ii cor" : "2 Corinthians" ,
57+ "1 corinthians" : "1 Corinthians" , "1 cor" : "1 Corinthians" , "1 co" : "1 Corinthians" , "1cor" : "1 Corinthians" , "i cor" : "1 Corinthians" , "i corinthians" : "1 Corinthians" , "1st corinthians" : "1 Corinthians" ,
58+ "2 corinthians" : "2 Corinthians" , "2 cor" : "2 Corinthians" , "2 co" : "2 Corinthians" , "2cor" : "2 Corinthians" , "ii cor" : "2 Corinthians" , "ii corinthians" : "2 Corinthians" , "2nd corinthians" : "2 Corinthians" ,
5959 "galatians" : "Galatians" , "gal" : "Galatians" , "ga" : "Galatians" ,
6060 "ephesians" : "Ephesians" , "eph" : "Ephesians" , "ep" : "Ephesians" ,
6161 "philippians" : "Philippians" , "phil" : "Philippians" , "php" : "Philippians" ,
6262 "colossians" : "Colossians" , "col" : "Colossians" ,
63- "1 thessalonians" : "1 Thessalonians" , "1 thess" : "1 Thessalonians" , "1 th" : "1 Thessalonians" , "1thess" : "1 Thessalonians" , "i thess" : "1 Thessalonians" ,
64- "2 thessalonians" : "2 Thessalonians" , "2 thess" : "2 Thessalonians" , "2 th" : "2 Thessalonians" , "2thess" : "2 Thessalonians" , "ii thess" : "2 Thessalonians" ,
65- "1 timothy" : "1 Timothy" , "1 tim" : "1 Timothy" , "1 ti" : "1 Timothy" , "1tim" : "1 Timothy" , "i tim" : "1 Timothy" ,
66- "2 timothy" : "2 Timothy" , "2 tim" : "2 Timothy" , "2 ti" : "2 Timothy" , "2tim" : "2 Timothy" , "ii tim" : "2 Timothy" ,
63+ "1 thessalonians" : "1 Thessalonians" , "1 thess" : "1 Thessalonians" , "1 th" : "1 Thessalonians" , "1thess" : "1 Thessalonians" , "i thess" : "1 Thessalonians" , "i thessalonians" : "1 Thessalonians" , "1st thessalonians" : "1 Thessalonians" ,
64+ "2 thessalonians" : "2 Thessalonians" , "2 thess" : "2 Thessalonians" , "2 th" : "2 Thessalonians" , "2thess" : "2 Thessalonians" , "ii thess" : "2 Thessalonians" , "ii thessalonians" : "2 Thessalonians" , "2nd thessalonians" : "2 Thessalonians" ,
65+ "1 timothy" : "1 Timothy" , "1 tim" : "1 Timothy" , "1 ti" : "1 Timothy" , "1tim" : "1 Timothy" , "i tim" : "1 Timothy" , "i timothy" : "1 Timothy" , "1st timothy" : "1 Timothy" ,
66+ "2 timothy" : "2 Timothy" , "2 tim" : "2 Timothy" , "2 ti" : "2 Timothy" , "2tim" : "2 Timothy" , "ii tim" : "2 Timothy" , "ii timothy" : "2 Timothy" , "2nd timothy" : "2 Timothy" ,
6767 "titus" : "Titus" , "tit" : "Titus" , "ti" : "Titus" ,
6868 "philemon" : "Philemon" , "philem" : "Philemon" , "phlm" : "Philemon" , "phm" : "Philemon" ,
6969 "hebrews" : "Hebrews" , "heb" : "Hebrews" , "he" : "Hebrews" ,
7070 "james" : "James" , "jas" : "James" , "jm" : "James" ,
71- "1 peter" : "1 Peter" , "1 pet" : "1 Peter" , "1 pe" : "1 Peter" , "1 pt" : "1 Peter" , "1peter" : "1 Peter" , "i pet" : "1 Peter" ,
72- "2 peter" : "2 Peter" , "2 pet" : "2 Peter" , "2 pe" : "2 Peter" , "2 pt" : "2 Peter" , "2peter" : "2 Peter" , "ii pet" : "2 Peter" ,
73- "1 john" : "1 John" , "1 jn" : "1 John" , "1jn" : "1 John" , "1john" : "1 John" , "i jn" : "1 John" ,
74- "2 john" : "2 John" , "2 jn" : "2 John" , "2jn" : "2 John" , "2john" : "2 John" , "ii jn" : "2 John" ,
75- "3 john" : "3 John" , "3 jn" : "3 John" , "3jn" : "3 John" , "3john" : "3 John" , "iii jn" : "3 John" ,
71+ "1 peter" : "1 Peter" , "1 pet" : "1 Peter" , "1 pe" : "1 Peter" , "1 pt" : "1 Peter" , "1peter" : "1 Peter" , "i pet" : "1 Peter" , "i peter" : "1 Peter" , "1st peter" : "1 Peter" ,
72+ "2 peter" : "2 Peter" , "2 pet" : "2 Peter" , "2 pe" : "2 Peter" , "2 pt" : "2 Peter" , "2peter" : "2 Peter" , "ii pet" : "2 Peter" , "ii peter" : "2 Peter" , "2nd peter" : "2 Peter" ,
73+ "1 john" : "1 John" , "1 jn" : "1 John" , "1jn" : "1 John" , "1john" : "1 John" , "i jn" : "1 John" , "i john" : "1 John" , "1st john" : "1 John" ,
74+ "2 john" : "2 John" , "2 jn" : "2 John" , "2jn" : "2 John" , "2john" : "2 John" , "ii jn" : "2 John" , "ii john" : "2 John" , "2nd john" : "2 John" ,
75+ "3 john" : "3 John" , "3 jn" : "3 John" , "3jn" : "3 John" , "3john" : "3 John" , "iii jn" : "3 John" , "iii john" : "3 John" , "3rd john" : "3 John" ,
7676 "jude" : "Jude" , "jud" : "Jude" , "jd" : "Jude" ,
7777 "revelation" : "Revelation" , "rev" : "Revelation" , "re" : "Revelation" ,
7878}
@@ -110,63 +110,15 @@ func init() {
110110// ParseBibleReference parses a string to identify and normalize a Bible reference.
111111// It returns the normalized reference string and a boolean indicating validity.
112112func ParseBibleReference (input string ) (string , bool ) {
113- trimmedInput := strings . TrimSpace (input )
114- if trimmedInput == "" {
113+ ref , consumedLen , ok := ParseBibleReferenceFromStart (input )
114+ if ! ok {
115115 return "" , false
116116 }
117- lowerInput := strings .ToLower (trimmedInput )
118-
119- var foundBook string
120- var bookName string
121- var remainder string
122-
123- // 1. Try exact match (Greedy)
124- for _ , key := range sortedBookKeys {
125- if strings .HasPrefix (lowerInput , key ) {
126- matchLen := len (key )
127- if len (lowerInput ) > matchLen {
128- nextChar := lowerInput [matchLen ]
129- if isLetter (nextChar ) {
130- continue
131- }
132- }
133-
134- foundBook = key
135- bookName = BibleBooks [key ]
136- remainder = strings .TrimSpace (trimmedInput [matchLen :])
137- break
138- }
139- }
140-
141- // 2. If no exact match, try fuzzy matching
142- if foundBook == "" {
143- fBook , matchLen := findFuzzyMatch (trimmedInput ) // Pass original case for splitting, but logic handles case
144- if fBook != "" {
145- bookName = fBook
146- foundBook = fBook // Mark as found
147- remainder = strings .TrimSpace (trimmedInput [matchLen :])
148- }
149- }
150-
151- if foundBook == "" {
117+ // Verify that we consumed the entire string (ignoring whitespace)
118+ if len (strings .TrimSpace (input [consumedLen :])) > 0 {
152119 return "" , false
153120 }
154-
155- // Check remainder
156- if remainder == "" {
157- if SingleChapterBooks [bookName ] {
158- return bookName , true
159- }
160- // Multi-chapter book defaults to chapter 1
161- return bookName + " 1" , true
162- }
163-
164- // Remainder validation
165- if isValidReferenceSyntax (remainder ) {
166- return bookName + " " + remainder , true
167- }
168-
169- return "" , false
121+ return ref , true
170122}
171123
172124func findFuzzyMatch (input string ) (string , int ) {
@@ -246,19 +198,147 @@ func isLetter(c byte) bool {
246198 return (c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' )
247199}
248200
249- func isValidReferenceSyntax (s string ) bool {
250- hasDigit := false
251- for _ , r := range s {
252- switch {
253- case r >= '0' && r <= '9' :
254- hasDigit = true
255- case r == ' ' || r == ':' || r == '-' || r == '.' || r == ',' || r == '\t' :
201+ // ExtractBibleReferences extracts all valid Bible references from a text.
202+ func ExtractBibleReferences (input string ) []string {
203+ var refs []string
204+
205+ startIdx := 0
206+ length := len (input )
207+
208+ for startIdx < length {
209+ // If we are at a whitespace, advance.
210+ if input [startIdx ] == ' ' || input [startIdx ] == '\t' || input [startIdx ] == '\n' || input [startIdx ] == '\r' {
211+ startIdx ++
212+ continue
213+ }
214+
215+ ref , consumed , ok := ParseBibleReferenceFromStart (input [startIdx :])
216+ if ok {
217+ refs = append (refs , ref )
218+ startIdx += consumed
219+ } else {
220+ // Advance to next word
221+ nextSpace := strings .IndexAny (input [startIdx :], " \t \n \r " )
222+ if nextSpace == - 1 {
223+ break
224+ }
225+ startIdx += nextSpace
226+ }
227+ }
228+ return refs
229+ }
230+
231+ // ParseBibleReferenceFromStart attempts to parse a Bible reference at the beginning of the string.
232+ // Returns the normalized reference, the length of text consumed from input, and whether a match was found.
233+ func ParseBibleReferenceFromStart (input string ) (string , int , bool ) {
234+ // 1. Skip leading whitespace
235+ startOffset := 0
236+ for startOffset < len (input ) && (input [startOffset ] == ' ' || input [startOffset ] == '\t' || input [startOffset ] == '\n' || input [startOffset ] == '\r' ) {
237+ startOffset ++
238+ }
239+ if startOffset == len (input ) {
240+ return "" , 0 , false
241+ }
242+
243+ currentInput := input [startOffset :]
244+ lowerInput := strings .ToLower (currentInput )
245+
246+ var foundBook string
247+ var bookName string
248+ var matchLen int // Length in currentInput
249+
250+ // 1. Try exact match (Greedy)
251+ for _ , key := range sortedBookKeys {
252+ if strings .HasPrefix (lowerInput , key ) {
253+ mLen := len (key )
254+ // Ensure whole word match
255+ if len (lowerInput ) > mLen {
256+ nextChar := lowerInput [mLen ]
257+ if isLetter (nextChar ) {
258+ continue
259+ }
260+ }
261+
262+ foundBook = key
263+ bookName = BibleBooks [key ]
264+ matchLen = mLen
265+ break
266+ }
267+ }
268+
269+ // 2. If no exact match, try fuzzy matching
270+ if foundBook == "" {
271+ fBook , mLen := findFuzzyMatch (currentInput )
272+ if fBook != "" {
273+ bookName = fBook
274+ foundBook = fBook
275+ matchLen = mLen
276+ }
277+ }
278+
279+ if foundBook == "" {
280+ return "" , 0 , false
281+ }
282+
283+ // We found a book. Now parse the numbers (remainder).
284+ remainderStart := matchLen
285+ // Skip spaces after book
286+ for remainderStart < len (currentInput ) && (currentInput [remainderStart ] == ' ' || currentInput [remainderStart ] == '\t' ) {
287+ remainderStart ++
288+ }
289+
290+ remainder := currentInput [remainderStart :]
291+
292+ // Consume valid reference syntax
293+ syntax , syntaxLen := consumeReferenceSyntax (remainder )
294+
295+ totalConsumed := startOffset + remainderStart + syntaxLen
296+
297+ if syntax == "" {
298+ if SingleChapterBooks [bookName ] {
299+ return bookName , totalConsumed , true
300+ }
301+ // Multi-chapter book defaults to chapter 1
302+ return bookName + " 1" , totalConsumed , true
303+ }
304+
305+ if hasDigit (syntax ) {
306+ return bookName + " " + syntax , totalConsumed , true
307+ }
308+
309+ if SingleChapterBooks [bookName ] {
310+ return bookName , startOffset + matchLen , true // Don't consume the invalid syntax
311+ }
312+ return bookName + " 1" , startOffset + matchLen , true
313+ }
314+
315+ func consumeReferenceSyntax (s string ) (string , int ) {
316+ lastDigit := - 1
317+
318+ for i , r := range s {
319+ if r >= '0' && r <= '9' {
320+ lastDigit = i
321+ } else if r == ':' || r == '-' || r == '.' || r == ',' || r == ' ' || r == '\t' {
256322 continue
257- default :
258- return false // Invalid character
323+ } else {
324+ break
325+ }
326+ }
327+
328+ if lastDigit == - 1 {
329+ return "" , 0
330+ }
331+
332+ return s [:lastDigit + 1 ], lastDigit + 1
333+ }
334+
335+ func hasDigit (s string ) bool {
336+ for _ , r := range s {
337+ if r >= '0' && r <= '9' {
338+ return true
259339 }
260340 }
261- return hasDigit
341+ return false
262342}
263343
264344func levenshteinDistance (s1 , s2 string ) int {
0 commit comments