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
class Reference(pydantic.main.BaseModel):
 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
def tuple(self) -> tuple:
48    def tuple(self) -> tuple:
49        """For hashing and comparisons"""
50        return tuple([hash(_) for _ in self.ranges])

For hashing and comparisons

def equals(self, other: Self) -> bool:
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.

def overlaps(self, other: Self) -> bool:
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.

def contains(self, other: Self) -> bool:
 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.

def adjoins(self, other: Self) -> bool:
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.

See refspy.reference.Reference.adjoins.

def count_books(self) -> int:
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.

def is_book(self: Self) -> bool:
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.

def is_chapter(self: Self) -> bool:
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.

def first_verse(self) -> refspy.models.verse.Verse:
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

def last_verse(self) -> refspy.models.verse.Verse:
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

def last_range(self) -> refspy.models.range.Range:
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

def sort(self) -> Self:
173    def sort(self) -> Self:
174        """Return a sorted reference."""
175        return self.__class__(ranges=sorted(self.ranges))

Return a sorted reference.

def merge(self) -> Self:
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.

def combine(self) -> Self:
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.

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

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

def reference( *args: refspy.models.range.Range, **kwargs: Any) -> Reference:
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))
)
def book_reference( library_id: Annotated[int, Ge(ge=1), Le(le=999)], book_id: Annotated[int, Ge(ge=1), Le(le=999)]) -> Reference:
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.

def chapter_reference( 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)]) -> Reference:
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.

def verse_reference( 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) -> Reference:
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.

See refspy.manager.Manager.bcv

def sort_references( references: list[Reference]) -> list[Reference]:
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.
def unique_references( references: list[Reference]) -> list[Reference]:
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.

def split_reference( reference: Reference) -> list[Reference]:
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.

def join_references( references: list[Reference]) -> Reference:
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

def count_references( references: list[Reference]) -> list[tuple[Reference, int]]:
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.