refspy.models.range

Data object for verse ranges.

  1"""Data object for verse ranges."""
  2
  3from typing import Self
  4from pydantic import BaseModel, model_validator
  5
  6from refspy.types.number import Number
  7from refspy.models.verse import verse, Verse
  8
  9
 10class Range(BaseModel):
 11    start: Verse
 12    end: Verse
 13
 14    def tuple(self) -> tuple[Verse, Verse]:
 15        return (self.start, self.end)
 16
 17    def __hash__(self) -> int:
 18        """Unique ID for key values."""
 19        return hash(self.tuple())
 20
 21    def __eq__(self, other) -> bool:  # <-- Should be Self; TypeError requires object
 22        return self.tuple() == other.tuple()
 23
 24    @model_validator(mode="after")
 25    def check_verse_order(self) -> Self:
 26        """Enforce verse order within the Range.
 27
 28        Raises:
 29            ValueError: If the start verse is greater than the end verse.
 30        """
 31        assert tuple(self.start) <= tuple(self.end)
 32        return self
 33
 34    def __lt__(self, other: Self) -> bool:
 35        return tuple(self) < tuple(other)
 36
 37    def overlaps(self, other: Self) -> bool:
 38        """Determine whether this reference overlaps the other.
 39
 40        Overlap is when either's start or end falls within the other's range.
 41
 42        Used in `refspy.reference.reference.overlaps()`
 43        """
 44        return any(
 45            [
 46                other.start <= self.start <= other.end,
 47                other.start <= self.end <= other.end,
 48                self.start <= other.start <= self.end,
 49                self.start <= other.end <= self.end,
 50            ]
 51        )
 52
 53    def contains(self, other: Self) -> bool:
 54        """Determine whether this reference contains the other.
 55
 56        Containment is when this reference's range includes the other's start
 57        and end.
 58
 59        Used in `refspy.reference.reference.contains()`
 60        """
 61        return self.start <= other.start and self.end >= other.end
 62
 63    def adjoins(self, other: Self) -> bool:
 64        """Determine whether this reference is adjacent to the other.
 65
 66        Adjacency determines whether two ranges can be joined by
 67        `refspy.ranges.join_ranges()`.
 68
 69        To keep things simple while covering most normal cases, adjacency is
 70        when:
 71
 72        * Two verse ranges are adjacent.
 73        * Two inter-chapter verse ranges are adjacent.
 74        * Two chapter_range references are adjacent.
 75        * Two book_range references are adjacent.
 76
 77        Note:
 78            We do not calculate adjacency for inter-book or inter-chapter
 79            references.
 80        """
 81        if any(
 82            [self.is_verse(), self.is_verse_range(), self.is_inter_chapter_range()]
 83        ) and any(
 84            [other.is_verse(), other.is_verse_range(), other.is_inter_chapter_range()]
 85        ):
 86            return any(
 87                [
 88                    all(
 89                        [
 90                            other.start > self.start,
 91                            other.start.verse == self.end.verse + 1,
 92                        ]
 93                    ),
 94                    all(
 95                        [
 96                            self.start > other.start,
 97                            self.start.verse == other.end.verse + 1,
 98                        ]
 99                    ),
100                ]
101            )
102        elif (
103            self.same_book_as(other)
104            and (self.is_chapter() or self.is_chapter_range())
105            and (other.is_chapter() or other.is_chapter_range())
106        ):
107            return any(
108                [
109                    all(
110                        [
111                            other.start > self.start,
112                            other.start.chapter == self.end.chapter + 1,
113                        ]
114                    ),
115                    all(
116                        [
117                            self.start > other.start,
118                            self.start.chapter == other.end.chapter + 1,
119                        ]
120                    ),
121                ]
122            )
123        elif (
124            self.same_library_as(other)
125            and (self.is_book() or self.is_book_range())
126            and (other.is_book() or other.is_book_range())
127        ):
128            return any(
129                [
130                    all(
131                        [
132                            other.start > self.start,
133                            other.start.book == self.end.book + 1,
134                        ]
135                    ),
136                    all(
137                        [
138                            self.start > other.start,
139                            self.start.book == other.end.book + 1,
140                        ]
141                    ),
142                ]
143            )
144        else:
145            return False
146
147    def merge(self, other: Self) -> Self:
148        """Combine two overlapping ranges."""
149        min_start_c, min_start_v = min(
150            (self.start.chapter, self.start.verse),
151            (other.start.chapter, other.start.verse),
152        )
153        max_end_c, max_end_v = max(
154            (self.end.chapter, self.end.verse),
155            (other.end.chapter, other.end.verse),
156        )
157        return self.__class__(
158            start=verse(
159                min(self.start.library, other.start.library),
160                min(self.start.book, other.start.book),
161                min_start_c,
162                min_start_v,
163            ),
164            end=verse(
165                max(self.end.library, other.end.library),
166                max(self.end.book, other.end.book),
167                max_end_c,
168                max_end_v,
169            ),
170        )
171
172    def join(self, other: Self):
173        """Combine two adjacent ranges."""
174        if self.start < other.start:
175            return self.__class__(start=self.start, end=other.end)
176        else:
177            return self.__class__(start=other.start, end=self.end)
178
179    def is_same_library(self) -> bool:
180        return self.start.library == self.end.library
181
182    def same_library_as(self, other: Self) -> bool:
183        """Determine if two ranges are in the same library."""
184        ids = set(
185            [
186                self.start.library,
187                self.end.library,
188                other.start.library,
189                other.end.library,
190            ]
191        )
192        return len(ids) == 1
193
194    def is_same_book(self) -> bool:
195        return self.is_same_library() and self.start.book == self.end.book
196
197    def same_book_as(self, other: Self) -> bool:
198        """Determine if two references are in the same book.
199
200        To be in the same book they must also be in the same library.
201        """
202        ids = set([self.start.book, self.end.book, other.start.book, other.end.book])
203        return self.same_library_as(other) and len(ids) == 1
204
205    def is_same_chapter(self) -> bool:
206        return (
207            self.is_same_library()
208            and self.is_same_book()
209            and self.start.chapter == self.end.chapter
210        )
211
212    def same_chapter_as(self, other: Self) -> bool:
213        """Determine if two references are in the same book.
214
215        To be in the same book they must also be in the same library.
216        """
217        ids = {
218            self.start.chapter,
219            self.end.chapter,
220            other.start.chapter,
221            other.end.chapter,
222        }
223        return self.same_book_as(other) and len(ids) == 1
224
225    def is_book(self) -> bool:
226        """Determine if this range covers one whole book.
227
228        Example:
229            `Matthew`
230        """
231        return all(
232            [
233                self.start.library == self.end.library,
234                self.start.book == self.end.book,
235                (self.start.chapter, self.end.chapter) == (1, 999),
236                (self.start.verse, self.end.verse) == (1, 999),
237            ]
238        )
239
240    def is_book_range(self) -> bool:
241        """Determine if this range goes from one whole book to another whole book.
242
243        Example:
244            `Matthew-John`
245        """
246        return all(
247            [
248                self.start.library == self.end.library,
249                self.start.book != self.end.book,
250                (self.start.chapter, self.end.chapter) == (1, 999),
251                (self.start.verse, self.end.verse) == (1, 999),
252            ]
253        )
254
255    def is_inter_book_range(self) -> bool:
256        """Determine if this range spans multiple books.
257
258        Example:
259            `Matt 1:2-Mark 3:4`
260        """
261        return all(
262            [
263                self.start.library == self.end.library,
264                self.start.book != self.end.book,
265                any(
266                    [
267                        self.start.chapter != 1,
268                        self.end.chapter != 999,
269                    ]
270                ),
271            ]
272        )
273
274    def is_chapter(self) -> bool:
275        """Determine if this range covers one whole chapter.
276
277        Example:
278            `Matthew 7`
279        """
280        return all(
281            [
282                self.start.library == self.end.library,
283                self.start.book == self.end.book,
284                self.start.chapter == self.end.chapter,
285                (self.start.verse, self.end.verse) == (1, 999),
286            ]
287        )
288
289    def is_chapter_range(self) -> bool:
290        """Determine if this range goes from one whole chapter to another whole
291        chapter.
292
293        Example:
294            `Matthew 5-8`
295        """
296        return all(
297            [
298                self.start.library == self.end.library,
299                self.start.book == self.end.book,
300                self.start.chapter != self.end.chapter,
301                (self.start.verse, self.end.verse) == (1, 999),
302            ]
303        )
304
305    def is_inter_chapter_range(self) -> bool:
306        """Determine if this range spans multiple chapters within the same book.
307
308        Example:
309            `Matthew 1:2-3:4`
310        """
311        return all(
312            [
313                self.start.library == self.end.library,
314                self.start.book == self.end.book,
315                self.start.chapter != self.end.chapter,
316                (self.start.verse, self.end.verse) != (1, 999),
317            ]
318        )
319
320    def is_verse(self) -> bool:
321        """Determine if this range covers one verse only.
322
323        Example:
324            `Matthew 7:8`
325        """
326        return all(
327            [
328                self.start.library == self.end.library,
329                self.start.book == self.end.book,
330                self.start.chapter == self.end.chapter,
331                self.start.verse == self.end.verse,
332            ]
333        )
334
335    def is_verse_range(self) -> bool:
336        """Determine if this range covers multiple verses within the same book
337        and chapter.
338
339        Example:
340            `Matthew 7:8-12`
341        """
342        return all(
343            [
344                self.start.library == self.end.library,
345                self.start.book == self.end.book,
346                self.start.chapter == self.end.chapter,
347                self.start.verse != self.end.verse,
348            ]
349        )
350
351
352# -----------------------------------
353# Shorthand constructor functions
354# -----------------------------------
355
356
357def range(start: Verse, end: Verse) -> Range:
358    """A shorthand contructor for Range objects.
359
360    Args:
361        start: the `refspy.verse.Verse` at the start of the range.
362        end: the `refspy.verse.Verse` at the end of the range.
363
364    Note:
365        To specify a single verse reference, supply a range with
366        the same verse as the start and the end.
367    """
368    return Range(start=start, end=end)
369
370
371def book_range(
372    library_id: Number, book_id: Number, book_end_id: Number | None = None
373) -> Range:
374    """
375    Shorthand function for creating book references from `refspy.number.Number`
376    values.
377    """
378    return range(
379        verse(library_id, book_id, 1, 1),
380        verse(library_id, book_end_id or book_id, 999, 999),
381    )
382
383
384def chapter_range(
385    library_id: Number,
386    book_id: Number,
387    chapter_id: Number,
388    chapter_end_id: Number | None = None,
389) -> Range:
390    """
391    Shorthand function for creating chapter references from
392    `refspy.number.Number` values.
393    """
394    return range(
395        verse(library_id, book_id, chapter_id, 1),
396        verse(library_id, book_id, chapter_end_id or chapter_id, 999),
397    )
398
399
400def verse_range(
401    library_id: Number,
402    book_id: Number,
403    chapter_id: Number,
404    verse_id: Number,
405    verse_end_id: Number | None = None,
406) -> Range:
407    """
408    Shorthand function for creating verse or range references from
409    `refspy.number.Number` values.
410
411    See `refspy.manager.Manager.bcv`
412    """
413    return range(
414        verse(library_id, book_id, chapter_id, verse_id),
415        verse(library_id, book_id, chapter_id, verse_end_id or verse_id),
416    )
417
418
419# -----------------------------------
420# Manipulation functions
421# -----------------------------------
422
423
424def merge_ranges(ranges: list[Range], skip_sort: bool = False) -> list[Range]:
425    """Merge overlapping ranges within a sorted list.
426
427    This performs a sort before merging unless skip_sort=True.
428    """
429    if not ranges:
430        return []
431    sorted_ranges = ranges if skip_sort else sorted(ranges)
432    new_ranges = []
433    last_range = sorted_ranges[0]
434    if sorted_ranges:
435        for this_range in sorted_ranges[1:]:
436            if last_range.overlaps(this_range):
437                last_range = last_range.merge(this_range)
438            else:
439                new_ranges.append(last_range)
440                last_range = this_range
441    new_ranges.append(last_range)
442    return new_ranges
443
444
445def combine_ranges(ranges: list[Range], skip_merge: bool = False) -> list[Range]:
446    """Join adjacent ranges within a sorted and merged list
447
448    This performs a sort and merge before combining, unless skip_merge=True.
449    """
450    if not ranges:
451        return []
452    merged_ranges = ranges if skip_merge else merge_ranges(ranges)
453    new_ranges = []
454    last_range = merged_ranges[0]
455    if merged_ranges:
456        for this_range in merged_ranges[1:]:
457            if last_range.adjoins(this_range):
458                last_range = last_range.join(this_range)
459            else:
460                new_ranges.append(last_range)
461                last_range = this_range
462    new_ranges.append(last_range)
463    return new_ranges
class Range(pydantic.main.BaseModel):
 11class Range(BaseModel):
 12    start: Verse
 13    end: Verse
 14
 15    def tuple(self) -> tuple[Verse, Verse]:
 16        return (self.start, self.end)
 17
 18    def __hash__(self) -> int:
 19        """Unique ID for key values."""
 20        return hash(self.tuple())
 21
 22    def __eq__(self, other) -> bool:  # <-- Should be Self; TypeError requires object
 23        return self.tuple() == other.tuple()
 24
 25    @model_validator(mode="after")
 26    def check_verse_order(self) -> Self:
 27        """Enforce verse order within the Range.
 28
 29        Raises:
 30            ValueError: If the start verse is greater than the end verse.
 31        """
 32        assert tuple(self.start) <= tuple(self.end)
 33        return self
 34
 35    def __lt__(self, other: Self) -> bool:
 36        return tuple(self) < tuple(other)
 37
 38    def overlaps(self, other: Self) -> bool:
 39        """Determine whether this reference overlaps the other.
 40
 41        Overlap is when either's start or end falls within the other's range.
 42
 43        Used in `refspy.reference.reference.overlaps()`
 44        """
 45        return any(
 46            [
 47                other.start <= self.start <= other.end,
 48                other.start <= self.end <= other.end,
 49                self.start <= other.start <= self.end,
 50                self.start <= other.end <= self.end,
 51            ]
 52        )
 53
 54    def contains(self, other: Self) -> bool:
 55        """Determine whether this reference contains the other.
 56
 57        Containment is when this reference's range includes the other's start
 58        and end.
 59
 60        Used in `refspy.reference.reference.contains()`
 61        """
 62        return self.start <= other.start and self.end >= other.end
 63
 64    def adjoins(self, other: Self) -> bool:
 65        """Determine whether this reference is adjacent to the other.
 66
 67        Adjacency determines whether two ranges can be joined by
 68        `refspy.ranges.join_ranges()`.
 69
 70        To keep things simple while covering most normal cases, adjacency is
 71        when:
 72
 73        * Two verse ranges are adjacent.
 74        * Two inter-chapter verse ranges are adjacent.
 75        * Two chapter_range references are adjacent.
 76        * Two book_range references are adjacent.
 77
 78        Note:
 79            We do not calculate adjacency for inter-book or inter-chapter
 80            references.
 81        """
 82        if any(
 83            [self.is_verse(), self.is_verse_range(), self.is_inter_chapter_range()]
 84        ) and any(
 85            [other.is_verse(), other.is_verse_range(), other.is_inter_chapter_range()]
 86        ):
 87            return any(
 88                [
 89                    all(
 90                        [
 91                            other.start > self.start,
 92                            other.start.verse == self.end.verse + 1,
 93                        ]
 94                    ),
 95                    all(
 96                        [
 97                            self.start > other.start,
 98                            self.start.verse == other.end.verse + 1,
 99                        ]
100                    ),
101                ]
102            )
103        elif (
104            self.same_book_as(other)
105            and (self.is_chapter() or self.is_chapter_range())
106            and (other.is_chapter() or other.is_chapter_range())
107        ):
108            return any(
109                [
110                    all(
111                        [
112                            other.start > self.start,
113                            other.start.chapter == self.end.chapter + 1,
114                        ]
115                    ),
116                    all(
117                        [
118                            self.start > other.start,
119                            self.start.chapter == other.end.chapter + 1,
120                        ]
121                    ),
122                ]
123            )
124        elif (
125            self.same_library_as(other)
126            and (self.is_book() or self.is_book_range())
127            and (other.is_book() or other.is_book_range())
128        ):
129            return any(
130                [
131                    all(
132                        [
133                            other.start > self.start,
134                            other.start.book == self.end.book + 1,
135                        ]
136                    ),
137                    all(
138                        [
139                            self.start > other.start,
140                            self.start.book == other.end.book + 1,
141                        ]
142                    ),
143                ]
144            )
145        else:
146            return False
147
148    def merge(self, other: Self) -> Self:
149        """Combine two overlapping ranges."""
150        min_start_c, min_start_v = min(
151            (self.start.chapter, self.start.verse),
152            (other.start.chapter, other.start.verse),
153        )
154        max_end_c, max_end_v = max(
155            (self.end.chapter, self.end.verse),
156            (other.end.chapter, other.end.verse),
157        )
158        return self.__class__(
159            start=verse(
160                min(self.start.library, other.start.library),
161                min(self.start.book, other.start.book),
162                min_start_c,
163                min_start_v,
164            ),
165            end=verse(
166                max(self.end.library, other.end.library),
167                max(self.end.book, other.end.book),
168                max_end_c,
169                max_end_v,
170            ),
171        )
172
173    def join(self, other: Self):
174        """Combine two adjacent ranges."""
175        if self.start < other.start:
176            return self.__class__(start=self.start, end=other.end)
177        else:
178            return self.__class__(start=other.start, end=self.end)
179
180    def is_same_library(self) -> bool:
181        return self.start.library == self.end.library
182
183    def same_library_as(self, other: Self) -> bool:
184        """Determine if two ranges are in the same library."""
185        ids = set(
186            [
187                self.start.library,
188                self.end.library,
189                other.start.library,
190                other.end.library,
191            ]
192        )
193        return len(ids) == 1
194
195    def is_same_book(self) -> bool:
196        return self.is_same_library() and self.start.book == self.end.book
197
198    def same_book_as(self, other: Self) -> bool:
199        """Determine if two references are in the same book.
200
201        To be in the same book they must also be in the same library.
202        """
203        ids = set([self.start.book, self.end.book, other.start.book, other.end.book])
204        return self.same_library_as(other) and len(ids) == 1
205
206    def is_same_chapter(self) -> bool:
207        return (
208            self.is_same_library()
209            and self.is_same_book()
210            and self.start.chapter == self.end.chapter
211        )
212
213    def same_chapter_as(self, other: Self) -> bool:
214        """Determine if two references are in the same book.
215
216        To be in the same book they must also be in the same library.
217        """
218        ids = {
219            self.start.chapter,
220            self.end.chapter,
221            other.start.chapter,
222            other.end.chapter,
223        }
224        return self.same_book_as(other) and len(ids) == 1
225
226    def is_book(self) -> bool:
227        """Determine if this range covers one whole book.
228
229        Example:
230            `Matthew`
231        """
232        return all(
233            [
234                self.start.library == self.end.library,
235                self.start.book == self.end.book,
236                (self.start.chapter, self.end.chapter) == (1, 999),
237                (self.start.verse, self.end.verse) == (1, 999),
238            ]
239        )
240
241    def is_book_range(self) -> bool:
242        """Determine if this range goes from one whole book to another whole book.
243
244        Example:
245            `Matthew-John`
246        """
247        return all(
248            [
249                self.start.library == self.end.library,
250                self.start.book != self.end.book,
251                (self.start.chapter, self.end.chapter) == (1, 999),
252                (self.start.verse, self.end.verse) == (1, 999),
253            ]
254        )
255
256    def is_inter_book_range(self) -> bool:
257        """Determine if this range spans multiple books.
258
259        Example:
260            `Matt 1:2-Mark 3:4`
261        """
262        return all(
263            [
264                self.start.library == self.end.library,
265                self.start.book != self.end.book,
266                any(
267                    [
268                        self.start.chapter != 1,
269                        self.end.chapter != 999,
270                    ]
271                ),
272            ]
273        )
274
275    def is_chapter(self) -> bool:
276        """Determine if this range covers one whole chapter.
277
278        Example:
279            `Matthew 7`
280        """
281        return all(
282            [
283                self.start.library == self.end.library,
284                self.start.book == self.end.book,
285                self.start.chapter == self.end.chapter,
286                (self.start.verse, self.end.verse) == (1, 999),
287            ]
288        )
289
290    def is_chapter_range(self) -> bool:
291        """Determine if this range goes from one whole chapter to another whole
292        chapter.
293
294        Example:
295            `Matthew 5-8`
296        """
297        return all(
298            [
299                self.start.library == self.end.library,
300                self.start.book == self.end.book,
301                self.start.chapter != self.end.chapter,
302                (self.start.verse, self.end.verse) == (1, 999),
303            ]
304        )
305
306    def is_inter_chapter_range(self) -> bool:
307        """Determine if this range spans multiple chapters within the same book.
308
309        Example:
310            `Matthew 1:2-3:4`
311        """
312        return all(
313            [
314                self.start.library == self.end.library,
315                self.start.book == self.end.book,
316                self.start.chapter != self.end.chapter,
317                (self.start.verse, self.end.verse) != (1, 999),
318            ]
319        )
320
321    def is_verse(self) -> bool:
322        """Determine if this range covers one verse only.
323
324        Example:
325            `Matthew 7:8`
326        """
327        return all(
328            [
329                self.start.library == self.end.library,
330                self.start.book == self.end.book,
331                self.start.chapter == self.end.chapter,
332                self.start.verse == self.end.verse,
333            ]
334        )
335
336    def is_verse_range(self) -> bool:
337        """Determine if this range covers multiple verses within the same book
338        and chapter.
339
340        Example:
341            `Matthew 7:8-12`
342        """
343        return all(
344            [
345                self.start.library == self.end.library,
346                self.start.book == self.end.book,
347                self.start.chapter == self.end.chapter,
348                self.start.verse != self.end.verse,
349            ]
350        )

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models. The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
def tuple(self) -> tuple[refspy.models.verse.Verse, refspy.models.verse.Verse]:
15    def tuple(self) -> tuple[Verse, Verse]:
16        return (self.start, self.end)
@model_validator(mode='after')
def check_verse_order(self) -> Self:
25    @model_validator(mode="after")
26    def check_verse_order(self) -> Self:
27        """Enforce verse order within the Range.
28
29        Raises:
30            ValueError: If the start verse is greater than the end verse.
31        """
32        assert tuple(self.start) <= tuple(self.end)
33        return self

Enforce verse order within the Range.

Raises:
  • ValueError: If the start verse is greater than the end verse.
def overlaps(self, other: Self) -> bool:
38    def overlaps(self, other: Self) -> bool:
39        """Determine whether this reference overlaps the other.
40
41        Overlap is when either's start or end falls within the other's range.
42
43        Used in `refspy.reference.reference.overlaps()`
44        """
45        return any(
46            [
47                other.start <= self.start <= other.end,
48                other.start <= self.end <= other.end,
49                self.start <= other.start <= self.end,
50                self.start <= other.end <= self.end,
51            ]
52        )

Determine whether this reference overlaps the other.

Overlap is when either's start or end falls within the other's range.

Used in refspy.reference.reference.overlaps()

def contains(self, other: Self) -> bool:
54    def contains(self, other: Self) -> bool:
55        """Determine whether this reference contains the other.
56
57        Containment is when this reference's range includes the other's start
58        and end.
59
60        Used in `refspy.reference.reference.contains()`
61        """
62        return self.start <= other.start and self.end >= other.end

Determine whether this reference contains the other.

Containment is when this reference's range includes the other's start and end.

Used in refspy.reference.reference.contains()

def adjoins(self, other: Self) -> bool:
 64    def adjoins(self, other: Self) -> bool:
 65        """Determine whether this reference is adjacent to the other.
 66
 67        Adjacency determines whether two ranges can be joined by
 68        `refspy.ranges.join_ranges()`.
 69
 70        To keep things simple while covering most normal cases, adjacency is
 71        when:
 72
 73        * Two verse ranges are adjacent.
 74        * Two inter-chapter verse ranges are adjacent.
 75        * Two chapter_range references are adjacent.
 76        * Two book_range references are adjacent.
 77
 78        Note:
 79            We do not calculate adjacency for inter-book or inter-chapter
 80            references.
 81        """
 82        if any(
 83            [self.is_verse(), self.is_verse_range(), self.is_inter_chapter_range()]
 84        ) and any(
 85            [other.is_verse(), other.is_verse_range(), other.is_inter_chapter_range()]
 86        ):
 87            return any(
 88                [
 89                    all(
 90                        [
 91                            other.start > self.start,
 92                            other.start.verse == self.end.verse + 1,
 93                        ]
 94                    ),
 95                    all(
 96                        [
 97                            self.start > other.start,
 98                            self.start.verse == other.end.verse + 1,
 99                        ]
100                    ),
101                ]
102            )
103        elif (
104            self.same_book_as(other)
105            and (self.is_chapter() or self.is_chapter_range())
106            and (other.is_chapter() or other.is_chapter_range())
107        ):
108            return any(
109                [
110                    all(
111                        [
112                            other.start > self.start,
113                            other.start.chapter == self.end.chapter + 1,
114                        ]
115                    ),
116                    all(
117                        [
118                            self.start > other.start,
119                            self.start.chapter == other.end.chapter + 1,
120                        ]
121                    ),
122                ]
123            )
124        elif (
125            self.same_library_as(other)
126            and (self.is_book() or self.is_book_range())
127            and (other.is_book() or other.is_book_range())
128        ):
129            return any(
130                [
131                    all(
132                        [
133                            other.start > self.start,
134                            other.start.book == self.end.book + 1,
135                        ]
136                    ),
137                    all(
138                        [
139                            self.start > other.start,
140                            self.start.book == other.end.book + 1,
141                        ]
142                    ),
143                ]
144            )
145        else:
146            return False

Determine whether this reference is adjacent to the other.

Adjacency determines whether two ranges can be joined by refspy.ranges.join_ranges().

To keep things simple while covering most normal cases, adjacency is when:

  • Two verse ranges are adjacent.
  • Two inter-chapter verse ranges are adjacent.
  • Two chapter_range references are adjacent.
  • Two book_range references are adjacent.
Note:

We do not calculate adjacency for inter-book or inter-chapter references.

def merge(self, other: Self) -> Self:
148    def merge(self, other: Self) -> Self:
149        """Combine two overlapping ranges."""
150        min_start_c, min_start_v = min(
151            (self.start.chapter, self.start.verse),
152            (other.start.chapter, other.start.verse),
153        )
154        max_end_c, max_end_v = max(
155            (self.end.chapter, self.end.verse),
156            (other.end.chapter, other.end.verse),
157        )
158        return self.__class__(
159            start=verse(
160                min(self.start.library, other.start.library),
161                min(self.start.book, other.start.book),
162                min_start_c,
163                min_start_v,
164            ),
165            end=verse(
166                max(self.end.library, other.end.library),
167                max(self.end.book, other.end.book),
168                max_end_c,
169                max_end_v,
170            ),
171        )

Combine two overlapping ranges.

def join(self, other: Self):
173    def join(self, other: Self):
174        """Combine two adjacent ranges."""
175        if self.start < other.start:
176            return self.__class__(start=self.start, end=other.end)
177        else:
178            return self.__class__(start=other.start, end=self.end)

Combine two adjacent ranges.

def is_same_library(self) -> bool:
180    def is_same_library(self) -> bool:
181        return self.start.library == self.end.library
def same_library_as(self, other: Self) -> bool:
183    def same_library_as(self, other: Self) -> bool:
184        """Determine if two ranges are in the same library."""
185        ids = set(
186            [
187                self.start.library,
188                self.end.library,
189                other.start.library,
190                other.end.library,
191            ]
192        )
193        return len(ids) == 1

Determine if two ranges are in the same library.

def is_same_book(self) -> bool:
195    def is_same_book(self) -> bool:
196        return self.is_same_library() and self.start.book == self.end.book
def same_book_as(self, other: Self) -> bool:
198    def same_book_as(self, other: Self) -> bool:
199        """Determine if two references are in the same book.
200
201        To be in the same book they must also be in the same library.
202        """
203        ids = set([self.start.book, self.end.book, other.start.book, other.end.book])
204        return self.same_library_as(other) and len(ids) == 1

Determine if two references are in the same book.

To be in the same book they must also be in the same library.

def is_same_chapter(self) -> bool:
206    def is_same_chapter(self) -> bool:
207        return (
208            self.is_same_library()
209            and self.is_same_book()
210            and self.start.chapter == self.end.chapter
211        )
def same_chapter_as(self, other: Self) -> bool:
213    def same_chapter_as(self, other: Self) -> bool:
214        """Determine if two references are in the same book.
215
216        To be in the same book they must also be in the same library.
217        """
218        ids = {
219            self.start.chapter,
220            self.end.chapter,
221            other.start.chapter,
222            other.end.chapter,
223        }
224        return self.same_book_as(other) and len(ids) == 1

Determine if two references are in the same book.

To be in the same book they must also be in the same library.

def is_book(self) -> bool:
226    def is_book(self) -> bool:
227        """Determine if this range covers one whole book.
228
229        Example:
230            `Matthew`
231        """
232        return all(
233            [
234                self.start.library == self.end.library,
235                self.start.book == self.end.book,
236                (self.start.chapter, self.end.chapter) == (1, 999),
237                (self.start.verse, self.end.verse) == (1, 999),
238            ]
239        )

Determine if this range covers one whole book.

Example:

Matthew

def is_book_range(self) -> bool:
241    def is_book_range(self) -> bool:
242        """Determine if this range goes from one whole book to another whole book.
243
244        Example:
245            `Matthew-John`
246        """
247        return all(
248            [
249                self.start.library == self.end.library,
250                self.start.book != self.end.book,
251                (self.start.chapter, self.end.chapter) == (1, 999),
252                (self.start.verse, self.end.verse) == (1, 999),
253            ]
254        )

Determine if this range goes from one whole book to another whole book.

Example:

Matthew-John

def is_inter_book_range(self) -> bool:
256    def is_inter_book_range(self) -> bool:
257        """Determine if this range spans multiple books.
258
259        Example:
260            `Matt 1:2-Mark 3:4`
261        """
262        return all(
263            [
264                self.start.library == self.end.library,
265                self.start.book != self.end.book,
266                any(
267                    [
268                        self.start.chapter != 1,
269                        self.end.chapter != 999,
270                    ]
271                ),
272            ]
273        )

Determine if this range spans multiple books.

Example:

Matt 1:2-Mark 3:4

def is_chapter(self) -> bool:
275    def is_chapter(self) -> bool:
276        """Determine if this range covers one whole chapter.
277
278        Example:
279            `Matthew 7`
280        """
281        return all(
282            [
283                self.start.library == self.end.library,
284                self.start.book == self.end.book,
285                self.start.chapter == self.end.chapter,
286                (self.start.verse, self.end.verse) == (1, 999),
287            ]
288        )

Determine if this range covers one whole chapter.

Example:

Matthew 7

def is_chapter_range(self) -> bool:
290    def is_chapter_range(self) -> bool:
291        """Determine if this range goes from one whole chapter to another whole
292        chapter.
293
294        Example:
295            `Matthew 5-8`
296        """
297        return all(
298            [
299                self.start.library == self.end.library,
300                self.start.book == self.end.book,
301                self.start.chapter != self.end.chapter,
302                (self.start.verse, self.end.verse) == (1, 999),
303            ]
304        )

Determine if this range goes from one whole chapter to another whole chapter.

Example:

Matthew 5-8

def is_inter_chapter_range(self) -> bool:
306    def is_inter_chapter_range(self) -> bool:
307        """Determine if this range spans multiple chapters within the same book.
308
309        Example:
310            `Matthew 1:2-3:4`
311        """
312        return all(
313            [
314                self.start.library == self.end.library,
315                self.start.book == self.end.book,
316                self.start.chapter != self.end.chapter,
317                (self.start.verse, self.end.verse) != (1, 999),
318            ]
319        )

Determine if this range spans multiple chapters within the same book.

Example:

Matthew 1:2-3:4

def is_verse(self) -> bool:
321    def is_verse(self) -> bool:
322        """Determine if this range covers one verse only.
323
324        Example:
325            `Matthew 7:8`
326        """
327        return all(
328            [
329                self.start.library == self.end.library,
330                self.start.book == self.end.book,
331                self.start.chapter == self.end.chapter,
332                self.start.verse == self.end.verse,
333            ]
334        )

Determine if this range covers one verse only.

Example:

Matthew 7:8

def is_verse_range(self) -> bool:
336    def is_verse_range(self) -> bool:
337        """Determine if this range covers multiple verses within the same book
338        and chapter.
339
340        Example:
341            `Matthew 7:8-12`
342        """
343        return all(
344            [
345                self.start.library == self.end.library,
346                self.start.book == self.end.book,
347                self.start.chapter == self.end.chapter,
348                self.start.verse != self.end.verse,
349            ]
350        )

Determine if this range covers multiple verses within the same book and chapter.

Example:

Matthew 7:8-12

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

def range( start: refspy.models.verse.Verse, end: refspy.models.verse.Verse) -> Range:
358def range(start: Verse, end: Verse) -> Range:
359    """A shorthand contructor for Range objects.
360
361    Args:
362        start: the `refspy.verse.Verse` at the start of the range.
363        end: the `refspy.verse.Verse` at the end of the range.
364
365    Note:
366        To specify a single verse reference, supply a range with
367        the same verse as the start and the end.
368    """
369    return Range(start=start, end=end)

A shorthand contructor for Range objects.

Arguments:
  • start: the refspy.verse.Verse at the start of the range.
  • end: the refspy.verse.Verse at the end of the range.
Note:

To specify a single verse reference, supply a range with the same verse as the start and the end.

def book_range( library_id: Annotated[int, Ge(ge=1), Le(le=999)], book_id: Annotated[int, Ge(ge=1), Le(le=999)], book_end_id: Optional[Annotated[int, Ge(ge=1), Le(le=999)]] = None) -> Range:
372def book_range(
373    library_id: Number, book_id: Number, book_end_id: Number | None = None
374) -> Range:
375    """
376    Shorthand function for creating book references from `refspy.number.Number`
377    values.
378    """
379    return range(
380        verse(library_id, book_id, 1, 1),
381        verse(library_id, book_end_id or book_id, 999, 999),
382    )

Shorthand function for creating book references from refspy.number.Number values.

def chapter_range( library_id: Annotated[int, Ge(ge=1), Le(le=999)], book_id: Annotated[int, Ge(ge=1), Le(le=999)], chapter_id: Annotated[int, Ge(ge=1), Le(le=999)], chapter_end_id: Optional[Annotated[int, Ge(ge=1), Le(le=999)]] = None) -> Range:
385def chapter_range(
386    library_id: Number,
387    book_id: Number,
388    chapter_id: Number,
389    chapter_end_id: Number | None = None,
390) -> Range:
391    """
392    Shorthand function for creating chapter references from
393    `refspy.number.Number` values.
394    """
395    return range(
396        verse(library_id, book_id, chapter_id, 1),
397        verse(library_id, book_id, chapter_end_id or chapter_id, 999),
398    )

Shorthand function for creating chapter references from refspy.number.Number values.

def verse_range( library_id: Annotated[int, Ge(ge=1), Le(le=999)], book_id: Annotated[int, Ge(ge=1), Le(le=999)], chapter_id: Annotated[int, Ge(ge=1), Le(le=999)], verse_id: Annotated[int, Ge(ge=1), Le(le=999)], verse_end_id: Optional[Annotated[int, Ge(ge=1), Le(le=999)]] = None) -> Range:
401def verse_range(
402    library_id: Number,
403    book_id: Number,
404    chapter_id: Number,
405    verse_id: Number,
406    verse_end_id: Number | None = None,
407) -> Range:
408    """
409    Shorthand function for creating verse or range references from
410    `refspy.number.Number` values.
411
412    See `refspy.manager.Manager.bcv`
413    """
414    return range(
415        verse(library_id, book_id, chapter_id, verse_id),
416        verse(library_id, book_id, chapter_id, verse_end_id or verse_id),
417    )

Shorthand function for creating verse or range references from refspy.number.Number values.

See refspy.manager.Manager.bcv

def merge_ranges( ranges: list[Range], skip_sort: bool = False) -> list[Range]:
425def merge_ranges(ranges: list[Range], skip_sort: bool = False) -> list[Range]:
426    """Merge overlapping ranges within a sorted list.
427
428    This performs a sort before merging unless skip_sort=True.
429    """
430    if not ranges:
431        return []
432    sorted_ranges = ranges if skip_sort else sorted(ranges)
433    new_ranges = []
434    last_range = sorted_ranges[0]
435    if sorted_ranges:
436        for this_range in sorted_ranges[1:]:
437            if last_range.overlaps(this_range):
438                last_range = last_range.merge(this_range)
439            else:
440                new_ranges.append(last_range)
441                last_range = this_range
442    new_ranges.append(last_range)
443    return new_ranges

Merge overlapping ranges within a sorted list.

This performs a sort before merging unless skip_sort=True.

def combine_ranges( ranges: list[Range], skip_merge: bool = False) -> list[Range]:
446def combine_ranges(ranges: list[Range], skip_merge: bool = False) -> list[Range]:
447    """Join adjacent ranges within a sorted and merged list
448
449    This performs a sort and merge before combining, unless skip_merge=True.
450    """
451    if not ranges:
452        return []
453    merged_ranges = ranges if skip_merge else merge_ranges(ranges)
454    new_ranges = []
455    last_range = merged_ranges[0]
456    if merged_ranges:
457        for this_range in merged_ranges[1:]:
458            if last_range.adjoins(this_range):
459                last_range = last_range.join(this_range)
460            else:
461                new_ranges.append(last_range)
462                last_range = this_range
463    new_ranges.append(last_range)
464    return new_ranges

Join adjacent ranges within a sorted and merged list

This performs a sort and merge before combining, unless skip_merge=True.