refspy.models.reference
Data object for references.
References consist of lists of ranges and are entirely numerical objects.
They can be set to sort, merge, and join references when added together.
1"""Data object for references. 2 3References consist of lists of ranges and are entirely numerical objects. 4 5They can be set to sort, merge, and join references when added together. 6""" 7 8import collections 9from typing import Any, Self 10 11from pydantic import BaseModel, Field 12 13from refspy.types.number import Number 14from refspy.models.range import Range, combine_ranges, merge_ranges, range as _range 15from refspy.models.verse import Verse, verse 16 17 18class Reference(BaseModel): 19 """A reference object represents a list of verse ranges. 20 21 References are entirely numeric entities. Matchers are used to find them in 22 text, and formatters are used to turn them in to canonical links. 23 24 It is common to want references to be sorted and combined. Combining means 25 merging overlapping ranges and joining adjacent ones. This is done by creating 26 references with sorted(), and the `merge`, and `combine` functions in 27 `refspy.range.Range`, or calling the methods of the same name on references. 28 29 Example: 30 ``` 31 from reference import merge, combine 32 33 assert reference(*sorted(ranges)) == reference(*ranges).sort() 34 assert reference(*merge(ranges)) == reference(*ranges).merge() 35 assert reference(*combine(ranges)) == reference(*ranges).combine() 36 ``` 37 """ 38 39 ranges: list[Range] = Field(min_length=1) 40 """ 41 A reference must contain at least one `refspy.range.Range`. 42 43 Raises: 44 ValueError: If ranges is empty 45 """ 46 47 def tuple(self) -> tuple: 48 """For hashing and comparisons""" 49 return tuple([hash(_) for _ in self.ranges]) 50 51 def __hash__(self) -> int: 52 """Unique ID for key values.""" 53 return hash(self.tuple()) 54 55 def __eq__(self, other) -> bool: # <-- Should be Self; TypeError requires object 56 return self.tuple() == other.tuple() 57 58 def __add__(self, other: Self) -> Self: 59 """Overload the addition operator to combine reference ranges into a new object.""" 60 return self.__class__(ranges=[*self.ranges, *other.ranges]) 61 62 def __lt__(self, other: Self) -> bool: 63 """ 64 A simple implementation of '<' allows sorting and min/max. 65 """ 66 common = min(len(self.ranges), len(other.ranges)) 67 for i in range(0, common): 68 if self.ranges[i].start != other.ranges[i].start: 69 return self.ranges[i].start < other.ranges[i].start 70 if self.ranges[i].end != other.ranges[i].end: 71 return self.ranges[i].end < other.ranges[i].end 72 return False # <-- all equal 73 74 def equals(self, other: Self) -> bool: 75 """ 76 Reference equality means that two references have identical ranges. 77 78 Note: 79 We don't use __eq__, as that is already defined in BaseModel. 80 """ 81 return self.ranges == other.ranges 82 83 def overlaps(self, other: Self) -> bool: 84 """ 85 Two references overlap if ANY of their ranges overlap. 86 """ 87 return any( 88 [ 89 self_range.overlaps(other_range) 90 for other_range in other.ranges 91 for self_range in self.ranges 92 ] 93 ) 94 95 def contains(self, other: Self) -> bool: 96 """ 97 A reference contains another if ALL the other's ranges are contained by 98 ANY of it's own ranges. 99 """ 100 return all( 101 [ 102 any([self_range.contains(other_range) for self_range in self.ranges]) 103 for other_range in other.ranges 104 ] 105 ) 106 107 def adjoins(self, other: Self) -> bool: 108 """Determine adjacency for ranges. 109 110 A reference adjoins another if its maximum range adjoins the other's 111 minimum or vice-versa. This usually only makes sense for simple 112 references with a small number of ranges. 113 114 See `refspy.reference.Reference.adjoins`. 115 """ 116 return any( 117 [ 118 max(self.ranges).adjoins(min(other.ranges)), 119 min(self.ranges).adjoins(max(other.ranges)), 120 ] 121 ) 122 123 def count_books(self) -> int: 124 """Return the total unique books contained in this reference. 125 126 Allow that the same book ID could appear in multiple libraries. 127 """ 128 library_books = set() 129 for _ in self.ranges: 130 library_books.add(tuple([_.start.library, _.start.book])) 131 library_books.add(tuple([_.end.library, _.end.book])) 132 return len(library_books) 133 134 def is_book(self: Self) -> bool: 135 """Determine if reference is a whole book.""" 136 return self.ranges[0].is_book() 137 138 def is_chapter(self: Self) -> bool: 139 """Determine if reference is a whole chapter.""" 140 return self.ranges[0].is_chapter() 141 142 def first_verse(self) -> Verse: 143 """Find the first verse. 144 145 Only sensible if sorted=True 146 147 Note: 148 Consider sorting on the fly if sorted=False 149 """ 150 return self.ranges[0].start 151 152 def last_verse(self) -> Verse: 153 """Find the last verse. 154 155 Only sensible if sorted=True 156 157 Note: 158 Consider sorting on the fly if sorted=False 159 """ 160 return self.ranges[-1].end 161 162 def last_range(self) -> Range: 163 """Find the last verse. 164 165 Only sensible if sorted=True 166 167 Note: 168 Consider sorting on the fly if sorted=False 169 """ 170 return self.ranges[-1] 171 172 def sort(self) -> Self: 173 """Return a sorted reference.""" 174 return self.__class__(ranges=sorted(self.ranges)) 175 176 def merge(self) -> Self: 177 """Return a merged reference. 178 179 A merged reference is sorted, and has any overlapping ranges merged. 180 """ 181 return self.__class__(ranges=merge_ranges(self.ranges)) 182 183 def combine(self) -> Self: 184 """Return a combined reference. 185 186 A combined reference is sorted, merged, and has any adjacent ranges combined. 187 """ 188 return self.__class__(ranges=combine_ranges(self.ranges)) 189 190 191# ----------------------------------- 192# Shorthand constructor functions 193# ----------------------------------- 194 195 196def reference(*args: Range, **kwargs: Any) -> Reference: 197 """ 198 Construct a Reference object from `refspy.range.Range` arguments. 199 200 Example: 201 ``` 202 ref = reference( 203 _range(verse(1, 2, 3, 3), verse(1, 2, 3, 4)), 204 _range(verse(1, 2, 3, 6), verse(1, 2, 3, 7)) 205 ) 206 ``` 207 """ 208 return Reference(ranges=list(args), **kwargs) 209 210 211def book_reference(library_id: Number, book_id: Number) -> Reference: 212 """ 213 Shorthand function for creating book references from `refspy.number.Number` 214 values. 215 """ 216 return reference( 217 _range( 218 verse(library_id, book_id, 1, 1), 219 verse(library_id, book_id, 999, 999), 220 ) 221 ) 222 223 224def chapter_reference( 225 library_id: Number, book_id: Number, chapter_id: Number 226) -> Reference: 227 """ 228 Shorthand function for creating chapter references from 229 `refspy.number.Number` values. 230 """ 231 return reference( 232 _range( 233 verse(library_id, book_id, chapter_id, 1), 234 verse(library_id, book_id, chapter_id, 999), 235 ) 236 ) 237 238 239def verse_reference( 240 library_id: Number, 241 book_id: Number, 242 chapter_id: Number, 243 verse_id: Number, 244 verse_end_id: Number | None = None, 245) -> Reference: 246 """ 247 Shorthand function for creating verse or range references from 248 `refspy.number.Number` values. 249 250 See `refspy.manager.Manager.bcv` 251 """ 252 return reference( 253 _range( 254 verse(library_id, book_id, chapter_id, verse_id), 255 verse(library_id, book_id, chapter_id, verse_end_id or verse_id), 256 ) 257 ) 258 259 260# ----------------------------------- 261# Manipulation functions 262# ----------------------------------- 263 264 265def sort_references(references: list[Reference]) -> list[Reference]: 266 """ 267 Return the same references in sorted order based on their ranges. 268 269 References implement `__lt__()`, so are innately sortable. 270 271 Note: 272 - use `unique_references(sorted_references(references))` to make the 273 sorted list unique. 274 """ 275 return sorted(references) 276 277 278def unique_references(references: list[Reference]) -> list[Reference]: 279 """ 280 Return references in the same order, but without duplicates 281 282 References implement `__hash__()`, so can be used in sets. Sets retain 283 the order of inserted items. 284 """ 285 ordered = {hash(ref): ref for ref in references} 286 return list(ordered.values()) 287 288 289def split_reference(reference: Reference) -> list[Reference]: 290 """Split a single references into a list of references, one for each range it contains.""" 291 references = [] 292 for rng in reference.ranges: 293 references.append(Reference(ranges=[rng])) 294 return references 295 296 297def join_references(references: list[Reference]) -> Reference: 298 """Join a list of references into a single reference""" 299 ranges = [] 300 for ref in references: 301 for rng in ref.ranges: 302 ranges.append(rng) 303 return reference(*ranges) 304 305 306def count_references(references: list[Reference]) -> list[tuple[Reference, int]]: 307 """ 308 Return tuples [(ref, count)]. 309 310 Because references implement __hash__(), this can be done with the 311 `collections.Counter`. Transform the resulting dict_items iterator into regular 312 tuples for simple typing. 313 """ 314 return [(ref, i) for ref, i in collections.Counter(references).items()] 315 316 # reference_count = [] 317 # for ref in reference_list: 318 # found = False 319 # for key, (counted_ref, count) in enumerate(reference_count): 320 # if ref == counted_ref and not found: 321 # reference_count[key] = (counted_ref, count + 1) 322 # found = True 323 # if not found: 324 # reference_count.append((ref, 1)) 325 # return reference_count
19class Reference(BaseModel): 20 """A reference object represents a list of verse ranges. 21 22 References are entirely numeric entities. Matchers are used to find them in 23 text, and formatters are used to turn them in to canonical links. 24 25 It is common to want references to be sorted and combined. Combining means 26 merging overlapping ranges and joining adjacent ones. This is done by creating 27 references with sorted(), and the `merge`, and `combine` functions in 28 `refspy.range.Range`, or calling the methods of the same name on references. 29 30 Example: 31 ``` 32 from reference import merge, combine 33 34 assert reference(*sorted(ranges)) == reference(*ranges).sort() 35 assert reference(*merge(ranges)) == reference(*ranges).merge() 36 assert reference(*combine(ranges)) == reference(*ranges).combine() 37 ``` 38 """ 39 40 ranges: list[Range] = Field(min_length=1) 41 """ 42 A reference must contain at least one `refspy.range.Range`. 43 44 Raises: 45 ValueError: If ranges is empty 46 """ 47 48 def tuple(self) -> tuple: 49 """For hashing and comparisons""" 50 return tuple([hash(_) for _ in self.ranges]) 51 52 def __hash__(self) -> int: 53 """Unique ID for key values.""" 54 return hash(self.tuple()) 55 56 def __eq__(self, other) -> bool: # <-- Should be Self; TypeError requires object 57 return self.tuple() == other.tuple() 58 59 def __add__(self, other: Self) -> Self: 60 """Overload the addition operator to combine reference ranges into a new object.""" 61 return self.__class__(ranges=[*self.ranges, *other.ranges]) 62 63 def __lt__(self, other: Self) -> bool: 64 """ 65 A simple implementation of '<' allows sorting and min/max. 66 """ 67 common = min(len(self.ranges), len(other.ranges)) 68 for i in range(0, common): 69 if self.ranges[i].start != other.ranges[i].start: 70 return self.ranges[i].start < other.ranges[i].start 71 if self.ranges[i].end != other.ranges[i].end: 72 return self.ranges[i].end < other.ranges[i].end 73 return False # <-- all equal 74 75 def equals(self, other: Self) -> bool: 76 """ 77 Reference equality means that two references have identical ranges. 78 79 Note: 80 We don't use __eq__, as that is already defined in BaseModel. 81 """ 82 return self.ranges == other.ranges 83 84 def overlaps(self, other: Self) -> bool: 85 """ 86 Two references overlap if ANY of their ranges overlap. 87 """ 88 return any( 89 [ 90 self_range.overlaps(other_range) 91 for other_range in other.ranges 92 for self_range in self.ranges 93 ] 94 ) 95 96 def contains(self, other: Self) -> bool: 97 """ 98 A reference contains another if ALL the other's ranges are contained by 99 ANY of it's own ranges. 100 """ 101 return all( 102 [ 103 any([self_range.contains(other_range) for self_range in self.ranges]) 104 for other_range in other.ranges 105 ] 106 ) 107 108 def adjoins(self, other: Self) -> bool: 109 """Determine adjacency for ranges. 110 111 A reference adjoins another if its maximum range adjoins the other's 112 minimum or vice-versa. This usually only makes sense for simple 113 references with a small number of ranges. 114 115 See `refspy.reference.Reference.adjoins`. 116 """ 117 return any( 118 [ 119 max(self.ranges).adjoins(min(other.ranges)), 120 min(self.ranges).adjoins(max(other.ranges)), 121 ] 122 ) 123 124 def count_books(self) -> int: 125 """Return the total unique books contained in this reference. 126 127 Allow that the same book ID could appear in multiple libraries. 128 """ 129 library_books = set() 130 for _ in self.ranges: 131 library_books.add(tuple([_.start.library, _.start.book])) 132 library_books.add(tuple([_.end.library, _.end.book])) 133 return len(library_books) 134 135 def is_book(self: Self) -> bool: 136 """Determine if reference is a whole book.""" 137 return self.ranges[0].is_book() 138 139 def is_chapter(self: Self) -> bool: 140 """Determine if reference is a whole chapter.""" 141 return self.ranges[0].is_chapter() 142 143 def first_verse(self) -> Verse: 144 """Find the first verse. 145 146 Only sensible if sorted=True 147 148 Note: 149 Consider sorting on the fly if sorted=False 150 """ 151 return self.ranges[0].start 152 153 def last_verse(self) -> Verse: 154 """Find the last verse. 155 156 Only sensible if sorted=True 157 158 Note: 159 Consider sorting on the fly if sorted=False 160 """ 161 return self.ranges[-1].end 162 163 def last_range(self) -> Range: 164 """Find the last verse. 165 166 Only sensible if sorted=True 167 168 Note: 169 Consider sorting on the fly if sorted=False 170 """ 171 return self.ranges[-1] 172 173 def sort(self) -> Self: 174 """Return a sorted reference.""" 175 return self.__class__(ranges=sorted(self.ranges)) 176 177 def merge(self) -> Self: 178 """Return a merged reference. 179 180 A merged reference is sorted, and has any overlapping ranges merged. 181 """ 182 return self.__class__(ranges=merge_ranges(self.ranges)) 183 184 def combine(self) -> Self: 185 """Return a combined reference. 186 187 A combined reference is sorted, merged, and has any adjacent ranges combined. 188 """ 189 return self.__class__(ranges=combine_ranges(self.ranges))
A reference object represents a list of verse ranges.
References are entirely numeric entities. Matchers are used to find them in text, and formatters are used to turn them in to canonical links.
It is common to want references to be sorted and combined. Combining means
merging overlapping ranges and joining adjacent ones. This is done by creating
references with sorted(), and the merge, and combine functions in
refspy.range.Range, or calling the methods of the same name on references.
Example:
from reference import merge, combine assert reference(*sorted(ranges)) == reference(*ranges).sort() assert reference(*merge(ranges)) == reference(*ranges).merge() assert reference(*combine(ranges)) == reference(*ranges).combine()
A reference must contain at least one refspy.range.Range.
Raises:
- ValueError: If ranges is empty
48 def tuple(self) -> tuple: 49 """For hashing and comparisons""" 50 return tuple([hash(_) for _ in self.ranges])
For hashing and comparisons
75 def equals(self, other: Self) -> bool: 76 """ 77 Reference equality means that two references have identical ranges. 78 79 Note: 80 We don't use __eq__, as that is already defined in BaseModel. 81 """ 82 return self.ranges == other.ranges
Reference equality means that two references have identical ranges.
Note:
We don't use __eq__, as that is already defined in BaseModel.
84 def overlaps(self, other: Self) -> bool: 85 """ 86 Two references overlap if ANY of their ranges overlap. 87 """ 88 return any( 89 [ 90 self_range.overlaps(other_range) 91 for other_range in other.ranges 92 for self_range in self.ranges 93 ] 94 )
Two references overlap if ANY of their ranges overlap.
96 def contains(self, other: Self) -> bool: 97 """ 98 A reference contains another if ALL the other's ranges are contained by 99 ANY of it's own ranges. 100 """ 101 return all( 102 [ 103 any([self_range.contains(other_range) for self_range in self.ranges]) 104 for other_range in other.ranges 105 ] 106 )
A reference contains another if ALL the other's ranges are contained by ANY of it's own ranges.
108 def adjoins(self, other: Self) -> bool: 109 """Determine adjacency for ranges. 110 111 A reference adjoins another if its maximum range adjoins the other's 112 minimum or vice-versa. This usually only makes sense for simple 113 references with a small number of ranges. 114 115 See `refspy.reference.Reference.adjoins`. 116 """ 117 return any( 118 [ 119 max(self.ranges).adjoins(min(other.ranges)), 120 min(self.ranges).adjoins(max(other.ranges)), 121 ] 122 )
Determine adjacency for ranges.
A reference adjoins another if its maximum range adjoins the other's minimum or vice-versa. This usually only makes sense for simple references with a small number of ranges.
124 def count_books(self) -> int: 125 """Return the total unique books contained in this reference. 126 127 Allow that the same book ID could appear in multiple libraries. 128 """ 129 library_books = set() 130 for _ in self.ranges: 131 library_books.add(tuple([_.start.library, _.start.book])) 132 library_books.add(tuple([_.end.library, _.end.book])) 133 return len(library_books)
Return the total unique books contained in this reference.
Allow that the same book ID could appear in multiple libraries.
135 def is_book(self: Self) -> bool: 136 """Determine if reference is a whole book.""" 137 return self.ranges[0].is_book()
Determine if reference is a whole book.
139 def is_chapter(self: Self) -> bool: 140 """Determine if reference is a whole chapter.""" 141 return self.ranges[0].is_chapter()
Determine if reference is a whole chapter.
143 def first_verse(self) -> Verse: 144 """Find the first verse. 145 146 Only sensible if sorted=True 147 148 Note: 149 Consider sorting on the fly if sorted=False 150 """ 151 return self.ranges[0].start
Find the first verse.
Only sensible if sorted=True
Note:
Consider sorting on the fly if sorted=False
153 def last_verse(self) -> Verse: 154 """Find the last verse. 155 156 Only sensible if sorted=True 157 158 Note: 159 Consider sorting on the fly if sorted=False 160 """ 161 return self.ranges[-1].end
Find the last verse.
Only sensible if sorted=True
Note:
Consider sorting on the fly if sorted=False
163 def last_range(self) -> Range: 164 """Find the last verse. 165 166 Only sensible if sorted=True 167 168 Note: 169 Consider sorting on the fly if sorted=False 170 """ 171 return self.ranges[-1]
Find the last verse.
Only sensible if sorted=True
Note:
Consider sorting on the fly if sorted=False
173 def sort(self) -> Self: 174 """Return a sorted reference.""" 175 return self.__class__(ranges=sorted(self.ranges))
Return a sorted reference.
177 def merge(self) -> Self: 178 """Return a merged reference. 179 180 A merged reference is sorted, and has any overlapping ranges merged. 181 """ 182 return self.__class__(ranges=merge_ranges(self.ranges))
Return a merged reference.
A merged reference is sorted, and has any overlapping ranges merged.
184 def combine(self) -> Self: 185 """Return a combined reference. 186 187 A combined reference is sorted, merged, and has any adjacent ranges combined. 188 """ 189 return self.__class__(ranges=combine_ranges(self.ranges))
Return a combined reference.
A combined reference is sorted, merged, and has any adjacent ranges combined.
197def reference(*args: Range, **kwargs: Any) -> Reference: 198 """ 199 Construct a Reference object from `refspy.range.Range` arguments. 200 201 Example: 202 ``` 203 ref = reference( 204 _range(verse(1, 2, 3, 3), verse(1, 2, 3, 4)), 205 _range(verse(1, 2, 3, 6), verse(1, 2, 3, 7)) 206 ) 207 ``` 208 """ 209 return Reference(ranges=list(args), **kwargs)
Construct a Reference object from refspy.range.Range arguments.
Example:
ref = reference( _range(verse(1, 2, 3, 3), verse(1, 2, 3, 4)), _range(verse(1, 2, 3, 6), verse(1, 2, 3, 7)) )
212def book_reference(library_id: Number, book_id: Number) -> Reference: 213 """ 214 Shorthand function for creating book references from `refspy.number.Number` 215 values. 216 """ 217 return reference( 218 _range( 219 verse(library_id, book_id, 1, 1), 220 verse(library_id, book_id, 999, 999), 221 ) 222 )
Shorthand function for creating book references from refspy.number.Number
values.
225def chapter_reference( 226 library_id: Number, book_id: Number, chapter_id: Number 227) -> Reference: 228 """ 229 Shorthand function for creating chapter references from 230 `refspy.number.Number` values. 231 """ 232 return reference( 233 _range( 234 verse(library_id, book_id, chapter_id, 1), 235 verse(library_id, book_id, chapter_id, 999), 236 ) 237 )
Shorthand function for creating chapter references from
refspy.number.Number values.
240def verse_reference( 241 library_id: Number, 242 book_id: Number, 243 chapter_id: Number, 244 verse_id: Number, 245 verse_end_id: Number | None = None, 246) -> Reference: 247 """ 248 Shorthand function for creating verse or range references from 249 `refspy.number.Number` values. 250 251 See `refspy.manager.Manager.bcv` 252 """ 253 return reference( 254 _range( 255 verse(library_id, book_id, chapter_id, verse_id), 256 verse(library_id, book_id, chapter_id, verse_end_id or verse_id), 257 ) 258 )
Shorthand function for creating verse or range references from
refspy.number.Number values.
266def sort_references(references: list[Reference]) -> list[Reference]: 267 """ 268 Return the same references in sorted order based on their ranges. 269 270 References implement `__lt__()`, so are innately sortable. 271 272 Note: 273 - use `unique_references(sorted_references(references))` to make the 274 sorted list unique. 275 """ 276 return sorted(references)
Return the same references in sorted order based on their ranges.
References implement __lt__(), so are innately sortable.
Note:
- use
unique_references(sorted_references(references))to make the sorted list unique.
279def unique_references(references: list[Reference]) -> list[Reference]: 280 """ 281 Return references in the same order, but without duplicates 282 283 References implement `__hash__()`, so can be used in sets. Sets retain 284 the order of inserted items. 285 """ 286 ordered = {hash(ref): ref for ref in references} 287 return list(ordered.values())
Return references in the same order, but without duplicates
References implement __hash__(), so can be used in sets. Sets retain
the order of inserted items.
290def split_reference(reference: Reference) -> list[Reference]: 291 """Split a single references into a list of references, one for each range it contains.""" 292 references = [] 293 for rng in reference.ranges: 294 references.append(Reference(ranges=[rng])) 295 return references
Split a single references into a list of references, one for each range it contains.
298def join_references(references: list[Reference]) -> Reference: 299 """Join a list of references into a single reference""" 300 ranges = [] 301 for ref in references: 302 for rng in ref.ranges: 303 ranges.append(rng) 304 return reference(*ranges)
Join a list of references into a single reference
307def count_references(references: list[Reference]) -> list[tuple[Reference, int]]: 308 """ 309 Return tuples [(ref, count)]. 310 311 Because references implement __hash__(), this can be done with the 312 `collections.Counter`. Transform the resulting dict_items iterator into regular 313 tuples for simple typing. 314 """ 315 return [(ref, i) for ref, i in collections.Counter(references).items()] 316 317 # reference_count = [] 318 # for ref in reference_list: 319 # found = False 320 # for key, (counted_ref, count) in enumerate(reference_count): 321 # if ref == counted_ref and not found: 322 # reference_count[key] = (counted_ref, count + 1) 323 # found = True 324 # if not found: 325 # reference_count.append((ref, 1)) 326 # return reference_count
Return tuples [(ref, count)].
Because references implement __hash__(), this can be done with the
collections.Counter. Transform the resulting dict_items iterator into regular
tuples for simple typing.