# 1.4.1 (31.07.26)
- **fix, severe**: `RangeFrame.sort_ranges` passed the kernel's arguments in the wrong order — group ids landed in `starts`, `Start` in `ends` and `End` in `groups` — so the primary sort key was `End`, not `Start`. On a 100,000-row frame with realistic coordinates, 97,964 rows came back in the wrong position. `PyRanges.sort_ranges` was unaffected. The `groups` argument of `ruranges.numpy.sort_intervals` is keyword-only from `ruranges>=0.1.7`, so the transposition can no longer happen silently
- **fix, severe**: `RangeFrame.sort_ranges()` with no `by` ran a full Python-level pass over every row to build an array that is provably all zeros — 97% of the total cost of an unkeyed sort (5.77 s of 5.95 s at 5,000,000 rows). The replacement ranker is never invoked for zero columns
- **breaking**: `RangeFrame.sort_ranges` no longer takes `natsort`, and orders string keys lexically. Natural ordering exists for chromosome names, and a `RangeFrame` has no `Chromosome` column; `PyRanges.sort_ranges` keeps `natsort` unchanged. `rf.sort_ranges(by="transcript_id")` therefore now puts `t10` before `t9`
- **breaking**: `RangeFrame.sort_ranges` no longer takes `sort_rows_reverse_order`. It was declared and never called, and it was the one parameter in the family requiring a per-row Python sequence. Callers wanting 5'→3' ordering use `PyRanges.sort_ranges`
- both `sort_ranges` methods take `by` keys that *relocate*: the full key list is `Chromosome, Strand, *by, Start, End`, and any column named in `by` is taken out of its implicit position and used where you put it. `by=["Strand", "Chromosome"]` sorts by strand first; `by=["Start", "End", "score"]` sorts by a key *after* the coordinates. Both halves of gh-94. The rule applies per column, so `by=["score", "Chromosome"]` gives `Strand, score, Chromosome, Start, End` — name every key you care about
- both `sort_ranges` methods take `sort_descending`, naming keys to reverse. Any key qualifies, the implicit `Chromosome`, `Strand`, `Start` and `End` included. A name that is not a sort key raises `ValueError` rather than being ignored. On the coordinate keys it composes with `use_strand` by XOR, so a reversed row and a reversed key cancel out
- **deprecated**: `RangeFrame.sort_by_position` now warns and will be removed next release. `sort_ranges()` with no arguments sorts by the same columns, and on `PyRanges` `sort_by_position` is a trap: it sorts globally by position and ignores `Chromosome`
- sorting is faster and the ordering rule is now shared with `polaranges`. Key columns are coded one at a time rather than as key tuples — the cost is the number of distinct values per column, not the number of distinct combinations, which approaches the row count — and the natural ordering of a column's distinct string values happens in Rust, in `ruranges_core::ranks`, which `polaranges` also calls. Doing it in Python cost 24.9 s for 10 million distinct values against 0.7 s in Rust and was the largest line item in every large sort. `natsort` is no longer a runtime dependency of the sort path
- document that `use_strand=False` does not remove `Strand` from the sort keys: it only stops negative-strand rows being ordered 3'→5'. To sort without grouping by strand, drop or rename the column

# 1.4.0 (30.07.26)
- **breaking**: `RangeFrame.overlap` now defaults to `multiple=False`, matching `PyRanges.overlap`. It previously defaulted to `"all"`, so `rf.overlap(other)` and `gr.overlap(other)` — the same bare call on a class and its subclass — meant different things: one reported every overlapping pair, the other filtered. A bare `overlap()` is now a filter everywhere. Pass `multiple=True` for the old generic behaviour
- `RangeFrame.overlap` also takes `multiple` as a bool now, like `PyRanges.overlap`. `overlap` returns rows of self and nothing from other, so the only thing this option can change is how many times a row appears: `"first"` and `"last"` would select the same rows and differ in output order alone. A string now raises `TypeError` instead of being silently truthy, and the option is keyword-only on both
- `PyRanges.overlap` used to derive the string the kernel wants with a bare truthiness test, so `multiple="first"` and `multiple="last"` silently meant `"all"` — the opposite of what was asked, with no error. On a 200k x 500k overlap that is a 47% row-count difference
- `intersect_overlaps`, `join_overlaps` and `set_intersect_overlaps` keep the `{"all", "first", "last"}` vocabulary: they return information taken from other, so `"first"` and `"last"` genuinely select different output there
- reject an unknown `multiple` with a `ValueError` naming the valid options; it used to reach `ruranges` and abort the interpreter with a Rust panic reading `invalid overlap_type string: "Invalid direction string"`, which `except Exception` cannot catch
- correct the `multiple` docstrings that described `overlap` and `join_overlaps` as reporting "subintervals". Only `intersect_overlaps`, `set_intersect_overlaps` and `subtract_overlaps` return subintervals; `overlap` returns whole intervals of self and `join_overlaps` returns them alongside other's columns
- remove `"contained"` from `VALID_OVERLAP_TYPE`. It was declared but `ruranges>=0.1.5` rejects the overlap type, so it only ever panicked, and it duplicated `contained_intervals_only=True`, which is now the single way to express containment. `multiple="contained"` raises a `ValueError` naming that argument
- annotate `tile_ranges`'s `use_strand` as `VALID_USE_STRAND_TYPE`, like every other method taking it. It was annotated `bool` while already accepting and handling `"auto"` at runtime, so type checkers rejected a valid call

# 1.3.11 (30.07.26)
- fix `to_bigwig(divide=True)` silently dropping any chromosome whose divided track reduces to a single run; `pyrle`'s `defragment` zeroes the value in that case and `to_ranges` then discards it as uncovered, so e.g. a one-interval frame with `Value=2` over coverage 1 returned no rows instead of log2(2) (gh-160)
- remove the dead `new_pyrles` accumulator in the same branch, which was built and discarded, and the redundant per-track `defragment()` call whose return value was thrown away
- removed the only doctest example showcasing a negative slack value, which isn't supported

# 1.3.10 (27.07.26)
- require `ruranges>=0.1.5`, which fixes `complement_ranges` attributing each gap to an arbitrary row's grouping columns (gh-157)
- `loci` now leaves an omitted slice bound genuinely open; the lower bound was clamped to a finite value, so `gr.loci[:n]` silently dropped intervals lying entirely below it
- remove `copy=False` from an `astype` call that failed pyright CI; add tox `[gh]` section

# 1.3.9 (30.05.26)
- fix `join_overlaps` duplicating rows in `left`/`outer` joins when the input index did not start at 0
- preserve the input index in `join_overlaps` output (self for inner/left/outer, other for right), consistent with `overlap`/`nearest_ranges`

# 1.3.8 (21.04.26)
- repo name changed from pyranges_1.x to pyranges1
- updated references to it
- fix issue 151 (Proper use of args and kwargs in concat)

# 1.3.7 (16.04.26)
- require `ruranges>=0.1.4`
- add `preserve_input_order` to Rust-backed overlap-style operations so large results can skip the extra output reordering step
- document the new output-order option with updated docstrings and doctest examples

# 1.3.6 (27.03.26)
- require `ruranges>=0.1.3`
- pick up the `ruranges` fix for `contained_intervals_only=True` overlaps when intervals share the same start coordinate
- fix `merge_overlaps` docs to refer to `count_col` instead of a nonexistent `count` parameter

# 1.3.5 (18.03.26)
- move GTF reading and attribute parsing onto the new `gtfreader>=0.1.1` dependency
- preserve semicolons inside quoted GTF attributes and keep duplicate quoted attributes in `to_rows_keep_duplicates`
- read GTF `Source` and `Frame` columns as categorical dtypes
- update GTF docs and doctests to reflect the new categorical display and supported duplicate-attribute format
- speed up GTF parsing

# 1.3.4 (14.03.26)
- accept ; in quotes in gtf attrs
- also speed up gtf parsing

# 1.3.3 (13.03.26)
- nearest_ranges: treat touching intervals as nearest matches with distance 1 instead of overlapping matches
- document touching-interval nearest behavior in doctests

# 1.3.2 (27.02.26)
- pandas 3 compatibility: removed pandas<3 constraint and aligned test matrix/dependencies
- update doctests/unit test expectations to pandas 3 native formatting (including `str` dtype display)
- remove dtype display normalization workaround in table rendering
- fix pandas 3 copy-on-write/read-only array issue in coverage path used by bigwig/rle conversion
- improve groupby `prod` compatibility across pandas 2/3 edge cases

# 1.3.1 (15.02.26)
- require `ruranges>=0.1.1`
- remove fallback compatibility hack for old `ruranges` installs that lacked `ruranges.numpy`
- add runtime guard with clear error for incompatible/too-old `ruranges`
- improve coverage with tests for unsigned/integer dtype paths used by Rust-backed overlap kernels

# 1.1.9 (26.01.26)
- pandas dependency bound to v2. This is in response to pandas 3.0.0 being released, breaking our doctests.

# 1.1.8 (30.12.25)
- to_gtf and to_gff3: fix bug where 'phase' (gtf) and 'frame' (gff3) are erroneously added to attributes field

# 1.1.7 (16.12.25)
- window_ranges: fix sort order issue in  when using by (#98 and #105)
- window_ranges: added argument add_window_id, updated documentation

# 0.1.17 (21.06.25)
- various fixes

# 0.1.14 (09.06.25)
- keep original row sort order

# 0.1.12 (10.05.25)
- keep original row sort order

# 0.1.9 (05.05.25)
- Function to compute interval metrics

# 0.1.8 (05.05.25)
- Dispatch to rust functions on type
- Add spliced subsequence multiple, cumsum group, and map to global

# 0.1.6 (20.03.25)
- Make sequence name a variable in get_sequence and get_transcript_sequence

# 0.1.5 (20.03.25)
- Fix bug where count_overlaps returned series not pyranges
- Add rust backend as non-optional dependency

# 0.1.4 (17-18.03.25)
Rust backend

# 0.1.0
New pyranges version, redesigned from scratch.
