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
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__andModel.__root_validators__from Pydantic V1. - __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models.
The
originandargsitems map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and theparameteritem 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-coreSchemaSerializerused to dump instances of the model. - __pydantic_validator__: The
pydantic-coreSchemaValidatorused 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.
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.
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()
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()
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.
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.
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.
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.
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.
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.
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
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
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
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
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
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
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
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
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.Verseat the start of the range. - end: the
refspy.verse.Verseat 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.
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.
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.
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.
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.
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.