versiref 0.1.0__py3-none-any.whl

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.
versiref/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """versiref can parse, manipulate, and format references to Bible verses.
2
+
3
+ It is versatile in that it can handle complex references, different
4
+ versifications, and it is flexible about the format is parses and the output it
5
+ generates.
6
+ """
7
+
8
+ from versiref.bible_ref import BibleRef, SimpleBibleRef, VerseRange
9
+ from versiref.ref_parser import RefParser
10
+ from versiref.ref_style import RefStyle, standard_names
11
+ from versiref.versification import Versification
12
+
13
+ __all__ = [
14
+ "BibleRef",
15
+ "SimpleBibleRef",
16
+ "VerseRange",
17
+ "RefParser",
18
+ "RefStyle",
19
+ "Versification",
20
+ "standard_names",
21
+ ]
versiref/bible_ref.py ADDED
@@ -0,0 +1,427 @@
1
+ """Bible reference handling for versiref.
2
+
3
+ This module provides classes for representing and manipulating Bible references.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Generator, Optional
8
+
9
+ from versiref.ref_style import RefStyle
10
+ from versiref.versification import Versification
11
+
12
+
13
+ @dataclass
14
+ class VerseRange:
15
+ """Represents a range of verses within a single book of the Bible.
16
+
17
+ A verse range has a start and end point, each defined by chapter, verse, and
18
+ subverse. The original text from which this range was parsed can be stored.
19
+
20
+ A verse number less than 0 means "unspecified". When a verse number is less
21
+ than 0, the corresponding subverse should be "", but it is ignored
22
+ regardless of its value. If start_verse and end_verse are both less than 0,
23
+ the range is a whole chapter or chapters. If start_verse >= 0 and end_verse
24
+ < 0, the verses are f"{start_verse}ff". This is only allowed if
25
+ start_chapter == end_chapter. Nor is it allowed to have start_verse < 0 and
26
+ end_verse >= 0, start_chapter == end_chapter && start_verse > end_verse, or
27
+ start_chapter < end_chapter. The result of SimpleBibleRef.format() is
28
+ undefined if the class contains a VerseRange with disallowed values. Where a
29
+ definite end is needed, applications can interpret "ff"
30
+ (style.following_verses) as "until the end of the chapter."
31
+ """
32
+
33
+ start_chapter: int
34
+ start_verse: int
35
+ start_subverse: str
36
+ end_chapter: int
37
+ end_verse: int
38
+ end_subverse: str
39
+ original_text: Optional[str] = None
40
+
41
+ def is_whole_chapters(self) -> bool:
42
+ """Return True if this range does not specify verse limits."""
43
+ return self.start_verse < 0 and self.end_verse < 0
44
+
45
+ def is_valid(self) -> bool:
46
+ """Check if this verse range has valid values.
47
+
48
+ Returns False if any of these conditions are met:
49
+ - start_verse >= 0 and end_verse < 0 (ff notation) but start_chapter != end_chapter
50
+ - start_verse < 0 and end_verse >= 0
51
+ - start_chapter == end_chapter and start_verse > end_verse
52
+ - start_chapter > end_chapter
53
+
54
+ Returns:
55
+ bool: True if the verse range has valid values, False otherwise
56
+
57
+ """
58
+ # Check for invalid "ff" notation (must be in same chapter)
59
+ if (
60
+ self.start_verse >= 0
61
+ and self.end_verse < 0
62
+ and self.start_chapter != self.end_chapter
63
+ ):
64
+ return False
65
+
66
+ # Cannot have unspecified start verse but specified end verse
67
+ if self.start_verse < 0 and self.end_verse >= 0:
68
+ return False
69
+
70
+ # Cannot have start verse greater than end verse in same chapter
71
+ if (
72
+ self.start_chapter == self.end_chapter
73
+ and self.start_verse > self.end_verse
74
+ and self.end_verse >= 0
75
+ ):
76
+ return False
77
+
78
+ # Cannot have start chapter greater than end chapter
79
+ if self.start_chapter > self.end_chapter:
80
+ return False
81
+
82
+ return True
83
+
84
+
85
+ @dataclass
86
+ class SimpleBibleRef:
87
+ """Represents a sequence of verse ranges within a single book of the Bible.
88
+
89
+ A SimpleBibleRef consists of a book ID (using Paratext three-letter codes)
90
+ and a list of verse ranges. The ranges are not necessarily in numeric order.
91
+ A SimpleBibleRef with an empty list of ranges refers to the entire book.
92
+ It optionally stores the original text from which the book ID was parsed.
93
+
94
+ This class is "naive" in that it doesn't specify its versification system.
95
+ """
96
+
97
+ book_id: str
98
+ ranges: list[VerseRange] = field(default_factory=list)
99
+ original_text: Optional[str] = None
100
+
101
+ @classmethod
102
+ def for_range(
103
+ cls,
104
+ book_id: str,
105
+ chapter: int,
106
+ start_verse: int,
107
+ end_chapter: Optional[int] = None,
108
+ end_verse: Optional[int] = None,
109
+ start_subverse: str = "",
110
+ end_subverse: str = "",
111
+ original_text: Optional[str] = None,
112
+ ) -> "SimpleBibleRef":
113
+ """Create a SimpleBibleRef with a single VerseRange.
114
+
115
+ Args:
116
+ book_id: The book ID (e.g., "JHN" for John)
117
+ chapter: The chapter number
118
+ start_verse: The starting verse number
119
+ end_chapter: The ending chapter number (defaults to start chapter if None)
120
+ end_verse: The ending verse number (defaults to start verse if None)
121
+ start_subverse: The starting subverse (defaults to "")
122
+ end_subverse: The ending subverse (defaults to "")
123
+ original_text: The original text from which this reference was parsed (defaults to None)
124
+
125
+ Returns:
126
+ A SimpleBibleRef instance with a single VerseRange
127
+
128
+ """
129
+ # If end_chapter is not specified, use the start chapter
130
+ if end_chapter is None:
131
+ end_chapter = chapter
132
+
133
+ # If end_verse is not specified, use the start verse
134
+ if end_verse is None:
135
+ end_verse = start_verse
136
+
137
+ verse_range = VerseRange(
138
+ start_chapter=chapter,
139
+ start_verse=start_verse,
140
+ start_subverse=start_subverse,
141
+ end_chapter=end_chapter,
142
+ end_verse=end_verse,
143
+ end_subverse=end_subverse,
144
+ original_text=original_text,
145
+ )
146
+
147
+ return cls(book_id=book_id, ranges=[verse_range], original_text=original_text)
148
+
149
+ def is_whole_book(self) -> bool:
150
+ """Return True if this reference refers to the entire book.
151
+
152
+ Note that this regards the form of the reference rather than its
153
+ content. So it returns True for John but False for John 1–21.
154
+ """
155
+ return len(self.ranges) == 0
156
+
157
+ def is_whole_chapters(self) -> bool:
158
+ """Return True if this reference does not specify verse limits.
159
+
160
+ Note that this regards the form of the reference rather than its
161
+ content. So it returns true for John and John 6 but False for John
162
+ 1:1–51.
163
+ """
164
+ for range in self.ranges:
165
+ if not range.is_whole_chapters():
166
+ return False
167
+ return True
168
+
169
+ def is_valid(self, versification: Versification) -> bool:
170
+ """Check if this Bible reference is valid according to the given versification.
171
+
172
+ Args:
173
+ versification: The Versification to check against
174
+
175
+ Returns:
176
+ bool: True if the reference is valid, False otherwise
177
+
178
+ """
179
+ # Check if the book ID is included in the versification
180
+ if not versification.includes(self.book_id):
181
+ return False
182
+
183
+ # Check each verse range
184
+ for verse_range in self.ranges:
185
+ # Check if the verse range itself is valid.
186
+ # This will catch ranges that end before they start.
187
+ if not verse_range.is_valid():
188
+ return False
189
+
190
+ # Check if the chapters and verses are within the limits of the versification.
191
+ if versification.last_verse(self.book_id, verse_range.end_chapter) < 0:
192
+ return False
193
+
194
+ # We only need to check the start if it's in a different chapter or the end is indefinite.
195
+ if (
196
+ verse_range.start_chapter != verse_range.end_chapter
197
+ or verse_range.end_verse < 0
198
+ ) and verse_range.start_verse > versification.last_verse(
199
+ self.book_id, verse_range.start_chapter
200
+ ):
201
+ return False
202
+
203
+ # Check end. No special handling is needed if end_verse < 0.
204
+ if verse_range.end_verse > versification.last_verse(
205
+ self.book_id, verse_range.end_chapter
206
+ ):
207
+ return False
208
+
209
+ return True
210
+
211
+ def range_refs(self) -> Generator["SimpleBibleRef", None, None]:
212
+ """Yield a new SimpleBibleRef for each verse range.
213
+
214
+ Each yielded SimpleBibleRef contains only one verse range from this reference.
215
+ The book ID is preserved, and the original text for each new instance comes
216
+ from the verse range.
217
+
218
+ Yields:
219
+ SimpleBibleRef: A new reference containing a single verse range
220
+
221
+ """
222
+ for verse_range in self.ranges:
223
+ yield SimpleBibleRef(
224
+ book_id=self.book_id if self.book_id != "PSAS" else "PSA",
225
+ ranges=[verse_range],
226
+ original_text=verse_range.original_text,
227
+ )
228
+
229
+ def format(
230
+ self, style: RefStyle, versification: Optional[Versification] = None
231
+ ) -> str:
232
+ """Format this Bible reference as a string according to the given style.
233
+
234
+ Args:
235
+ style: The RefStyle to use for formatting
236
+ versification: Optional Versification to use for determining book structure.
237
+ If provided, chapter numbers will be omitted for
238
+ one-chapter books.
239
+
240
+ Returns:
241
+ A formatted string representation of this Bible reference
242
+
243
+ """
244
+ # Get the book name according to the style
245
+ if self.book_id not in style.names:
246
+ raise ValueError(f"Unknown book ID: {self.book_id}")
247
+
248
+ # We start with the book name and then add ranges incrementally.
249
+ result = style.names[self.book_id]
250
+ last_range = None
251
+ for range in self.ranges:
252
+ if last_range is None:
253
+ result += " "
254
+ if versification is not None and versification.is_single_chapter(
255
+ self.book_id
256
+ ):
257
+ states_chapter = False
258
+ else:
259
+ result += str(range.start_chapter)
260
+ states_chapter = True
261
+ elif last_range.end_chapter != range.start_chapter:
262
+ result += f"{style.chapter_separator}{range.start_chapter}"
263
+ states_chapter = True
264
+ else:
265
+ result += style.verse_range_separator
266
+ states_chapter = False
267
+ # Add start verse if specified
268
+ if range.start_verse >= 0:
269
+ if states_chapter:
270
+ result += style.chapter_verse_separator
271
+ result += f"{range.start_verse}{range.start_subverse}"
272
+ # Add range end if different
273
+ if range.end_verse < 0 and range.start_verse >= 0:
274
+ result += style.following_verses
275
+ elif (
276
+ range.end_chapter != range.start_chapter
277
+ or range.end_verse != range.start_verse
278
+ or range.end_subverse != range.start_subverse
279
+ ):
280
+ result += style.range_separator
281
+ if range.end_chapter != range.start_chapter:
282
+ result += str(range.end_chapter)
283
+ if range.end_verse >= 0:
284
+ result += f"{style.chapter_verse_separator}{range.end_verse}"
285
+ elif range.end_verse != range.start_verse:
286
+ result += f"{range.end_verse}"
287
+ if range.end_verse >= 0:
288
+ result += range.end_subverse
289
+ last_range = range
290
+ return result
291
+
292
+ def resolve_following_verses(self, versification: Versification) -> None:
293
+ """Resolve following verses in the verse ranges.
294
+
295
+ This gives a definite end to ranges that use "ff" notation, namely, the
296
+ last verse of the chapter.
297
+
298
+ Args:
299
+ versification: The Versification to use for resolving following
300
+ verses
301
+
302
+ """
303
+ for range in self.ranges:
304
+ if range.start_verse >= 0 and range.end_verse < 0:
305
+ range.end_verse = versification.last_verse(
306
+ self.book_id, range.end_chapter
307
+ )
308
+
309
+
310
+ @dataclass
311
+ class BibleRef:
312
+ """Represents a sequence of verse ranges within one or more books of the Bible.
313
+
314
+ A BibleRef consists of a list of SimpleBibleRef objects and the Versification
315
+ they use. The versification can be None, though this will not usually be the case.
316
+ It optionally stores the original text from which this reference was parsed.
317
+ """
318
+
319
+ simple_refs: list[SimpleBibleRef] = field(default_factory=list)
320
+ versification: Optional[Versification] = None
321
+ original_text: Optional[str] = None
322
+
323
+ @classmethod
324
+ def for_range(
325
+ cls,
326
+ book_id: str,
327
+ chapter: int,
328
+ start_verse: int,
329
+ end_chapter: Optional[int] = None,
330
+ end_verse: Optional[int] = None,
331
+ start_subverse: str = "",
332
+ end_subverse: str = "",
333
+ original_text: Optional[str] = None,
334
+ versification: Optional[Versification] = None,
335
+ ) -> "BibleRef":
336
+ """Create a BibleRef with a single SimpleBibleRef containing a single VerseRange.
337
+
338
+ Args:
339
+ book_id: The book ID (e.g., "JHN" for John)
340
+ chapter: The chapter number
341
+ start_verse: The starting verse number
342
+ end_chapter: The ending chapter number (defaults to start chapter if None)
343
+ end_verse: The ending verse number (defaults to start verse if None)
344
+ start_subverse: The starting subverse (defaults to "")
345
+ end_subverse: The ending subverse (defaults to "")
346
+ original_text: The original text from which this reference was parsed (defaults to None)
347
+ versification: The Versification to use (defaults to None)
348
+
349
+ Returns:
350
+ A BibleRef instance with a single SimpleBibleRef containing a single VerseRange
351
+
352
+ """
353
+ simple_ref = SimpleBibleRef.for_range(
354
+ book_id=book_id,
355
+ chapter=chapter,
356
+ start_verse=start_verse,
357
+ end_chapter=end_chapter,
358
+ end_verse=end_verse,
359
+ start_subverse=start_subverse,
360
+ end_subverse=end_subverse,
361
+ original_text=original_text,
362
+ )
363
+
364
+ return cls(
365
+ simple_refs=[simple_ref],
366
+ versification=versification,
367
+ original_text=original_text,
368
+ )
369
+
370
+ def is_whole_books(self) -> bool:
371
+ """Return True if this reference refers to entire books only.
372
+
373
+ Returns True iff all contained SimpleBibleRef instances refer to entire books.
374
+ """
375
+ return all(ref.is_whole_book() for ref in self.simple_refs)
376
+
377
+ def is_whole_chapters(self) -> bool:
378
+ """Return True if this reference does not specify verse limits.
379
+
380
+ Returns True iff all contained SimpleBibleRef instances refer to whole chapters.
381
+ """
382
+ return all(ref.is_whole_chapters() for ref in self.simple_refs)
383
+
384
+ def is_valid(self) -> bool:
385
+ """Check if this Bible reference is valid according to its versification.
386
+
387
+ An empty BibleRef is vacuously valid.
388
+
389
+ Returns:
390
+ bool: True if the reference is valid, False otherwise
391
+
392
+ """
393
+ if self.versification is None:
394
+ return False
395
+ return all(ref.is_valid(self.versification) for ref in self.simple_refs)
396
+
397
+ def range_refs(self) -> Generator["BibleRef", None, None]:
398
+ """Yield a new BibleRef for each verse range across all simple refs.
399
+
400
+ Each yielded BibleRef contains a single SimpleBibleRef with a single verse range.
401
+ The versification is preserved.
402
+
403
+ Yields:
404
+ BibleRef: A new reference containing a single verse range
405
+
406
+ """
407
+ for simple_ref in self.simple_refs:
408
+ for range_ref in simple_ref.range_refs():
409
+ yield BibleRef(
410
+ simple_refs=[range_ref],
411
+ versification=self.versification,
412
+ original_text=range_ref.original_text,
413
+ )
414
+
415
+ def format(self, style: RefStyle) -> str:
416
+ """Format this Bible reference as a string according to the given style.
417
+
418
+ Args:
419
+ style: The RefStyle to use for formatting
420
+
421
+ Returns:
422
+ A formatted string representation of this Bible reference
423
+
424
+ """
425
+ return style.chapter_separator.join(
426
+ [r.format(style, self.versification) for r in self.simple_refs]
427
+ )
@@ -0,0 +1,83 @@
1
+ {
2
+ "GEN": "Gen.",
3
+ "EXO": "Exod.",
4
+ "LEV": "Lev.",
5
+ "NUM": "Num.",
6
+ "DEU": "Deut.",
7
+ "JOS": "Josh.",
8
+ "JDG": "Judg.",
9
+ "RUT": "Ruth",
10
+ "1SA": "1 Sam.",
11
+ "2SA": "2 Sam.",
12
+ "1KI": "1 Kings",
13
+ "2KI": "2 Kings",
14
+ "1CH": "1 Chron.",
15
+ "2CH": "2 Chron.",
16
+ "EZR": "Ezra",
17
+ "NEH": "Neh.",
18
+ "EST": "Esther",
19
+ "JOB": "Job",
20
+ "PSA": "Ps.",
21
+ "PSAS": "Pss.",
22
+ "PRO": "Prov.",
23
+ "ECC": "Eccles.",
24
+ "SNG": "Song of Sol.",
25
+ "ISA": "Isa.",
26
+ "JER": "Jer.",
27
+ "LAM": "Lam.",
28
+ "EZK": "Ezek.",
29
+ "DAN": "Dan.",
30
+ "HOS": "Hos.",
31
+ "JOL": "Joel",
32
+ "AMO": "Amos",
33
+ "OBA": "Obad.",
34
+ "JON": "Jon.",
35
+ "MIC": "Mic.",
36
+ "NAM": "Nah.",
37
+ "HAB": "Hab.",
38
+ "ZEP": "Zeph.",
39
+ "HAG": "Hag.",
40
+ "ZEC": "Zech.",
41
+ "MAL": "Mal.",
42
+ "MAT": "Matt.",
43
+ "MRK": "Mark",
44
+ "LUK": "Luke",
45
+ "JHN": "John",
46
+ "ACT": "Acts",
47
+ "ROM": "Rom.",
48
+ "1CO": "1 Cor.",
49
+ "2CO": "2 Cor.",
50
+ "GAL": "Gal.",
51
+ "EPH": "Eph.",
52
+ "PHP": "Phil.",
53
+ "COL": "Col.",
54
+ "1TH": "1 Thess.",
55
+ "2TH": "2 Thess.",
56
+ "1TI": "1 Tim.",
57
+ "2TI": "2 Tim.",
58
+ "TIT": "Titus",
59
+ "PHM": "Philem.",
60
+ "HEB": "Heb.",
61
+ "JAS": "James",
62
+ "1PE": "1 Pet.",
63
+ "2PE": "2 Pet.",
64
+ "1JN": "1 John",
65
+ "2JN": "2 John",
66
+ "3JN": "3 John",
67
+ "JUD": "Jude",
68
+ "REV": "Rev.",
69
+ "ESG": "Additions to Esther",
70
+ "BAR": "Bar.",
71
+ "BEL": "Bel and Dragon",
72
+ "WIS": "Ws.",
73
+ "SIR": "Sir.",
74
+ "S3Y": "Song of Three Children",
75
+ "SUS": "Sus.",
76
+ "1MA": "1 Macc.",
77
+ "2MA": "2 Macc.",
78
+ "3MA": "3 Macc.",
79
+ "4MA": "4 Macc.",
80
+ "MAN": "Pr. of Man.",
81
+ "1ES": "1 Esd.",
82
+ "2ES": "2 Esd."
83
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "GEN": "Gn",
3
+ "EXO": "Ex",
4
+ "LEV": "Lv",
5
+ "NUM": "Nm",
6
+ "DEU": "Dt",
7
+ "JOS": "Jo",
8
+ "JDG": "Jgs",
9
+ "RUT": "Ru",
10
+ "1SA": "1 Sm",
11
+ "2SA": "2 Sm",
12
+ "1KI": "1 Kgs",
13
+ "2KI": "2 Kgs",
14
+ "1CH": "1 Chr",
15
+ "2CH": "2 Chr",
16
+ "EZR": "Ezr",
17
+ "NEH": "Neh",
18
+ "EST": "Est",
19
+ "JOB": "Jb",
20
+ "PSA": "Ps",
21
+ "PSAS": "Pss",
22
+ "PRO": "Prv",
23
+ "ECC": "Eccl",
24
+ "SNG": "Sg",
25
+ "ISA": "Is",
26
+ "JER": "Jer",
27
+ "LAM": "Lam",
28
+ "EZK": "Ez",
29
+ "DAN": "Dn",
30
+ "HOS": "Hos",
31
+ "JOL": "Jl",
32
+ "AMO": "Am",
33
+ "OBA": "Ob",
34
+ "JON": "Jon",
35
+ "MIC": "Mi",
36
+ "NAM": "Na",
37
+ "HAB": "Hb",
38
+ "ZEP": "Zeph",
39
+ "HAG": "Hg",
40
+ "ZEC": "Zec",
41
+ "MAL": "Mal",
42
+ "MAT": "Mt",
43
+ "MRK": "Mk",
44
+ "LUK": "Lk",
45
+ "JHN": "Jn",
46
+ "ACT": "Acts",
47
+ "ROM": "Rom",
48
+ "1CO": "1 Cor",
49
+ "2CO": "2 Cor",
50
+ "GAL": "Gal",
51
+ "EPH": "Eph",
52
+ "PHP": "Phil",
53
+ "COL": "Col",
54
+ "1TH": "1 Thes",
55
+ "2TH": "2 Thes",
56
+ "1TI": "1 Tim",
57
+ "2TI": "2 Tim",
58
+ "TIT": "Ti",
59
+ "PHM": "Phlm",
60
+ "HEB": "Heb",
61
+ "JAS": "Jas",
62
+ "1PE": "1 Pt",
63
+ "2PE": "2 Pt",
64
+ "1JN": "1 Jn",
65
+ "2JN": "2 Jn",
66
+ "3JN": "3 Jn",
67
+ "JUD": "Jude",
68
+ "REV": "Rv",
69
+ "TOB": "Tb",
70
+ "JDT": "Jdt",
71
+ "ESG": "Additions to Esther",
72
+ "WIS": "Ws",
73
+ "SIR": "Sir",
74
+ "BAR": "Bar",
75
+ "S3Y": "Song of Three Children",
76
+ "SUS": "Susanna",
77
+ "BEL": "Bel and Dragon",
78
+ "1MA": "1 Mc",
79
+ "2MA": "2 Mc",
80
+ "3MA": "3 Mc",
81
+ "4MA": "4 Mc",
82
+ "MAN": "Pr of Man",
83
+ "1ES": "1 Esd",
84
+ "2ES": "2 Esd"
85
+ }