# 1.4.2 (31.07.26)
- **fix, severe**: `PyRanges.nearest_ranges(direction="upstream"/"downstream")` ignored strand on minus-strand intervals, returning the neighbour on the wrong side and so inverting the biological meaning of every directional nearest query on the reverse strand — nearest promoter, nearest TSS. For a minus-strand feature, upstream is the *higher* coordinate, as `PyRanges.upstream()` has always had it. `self` was split into its forward and reverse halves and both halves were then queried with the same coordinate direction. The strand-to-coordinate-direction mapping is now a single four-entry table rather than four hand-picked direction strings at four call sites, which is what let the bug through. Reported and fixed by Mike German (@steps-re) in gh-162
- **fix**: `RangeFrame.nearest_ranges` did not validate `direction`. Any value the coordinate space does not know — including the strand-aware `"upstream"`/`"downstream"`, which belong to `PyRanges` — reached `ruranges` and raised a `PanicException` reading `Invalid direction string`. That is not an `Exception`, so `except Exception` could not catch it and the interpreter was aborted. It now raises `ValueError` naming the valid options, as `multiple` has since 1.4.0
- **fix**: `PyRanges.nearest_ranges` with a directional `direction` on a frame without valid strand failed with `UndefinedVariableError: name 'Strand' is not defined`, leaked from a pandas query. It now raises `ValueError` explaining that upstream and downstream are strand-aware. `direction="any"` is unaffected
- the two direction vocabularies are named for what they are: `VALID_NEAREST_TYPE` is now `VALID_GENOMIC_DIRECTION_TYPE` (`any`/`upstream`/`downstream`, strand-aware) and `VALID_DIRECTION_TYPE` is now `VALID_COORDINATE_DIRECTION_TYPE` (`any`/`forward`/`backward`, where forward is always the higher coordinate). Both are internal to `pyranges1.core.names`. `VALID_NEAREST_OPTIONS` listed `upstream` twice and never `downstream`; it was unused, and the corrected list now backs the validation above
- **fix, gh-166**: `strand_behavior="opposite"` dropped every non-location column of `other`. To flip the strand, `prepare_by_binary` took `other.loc[:, [*RANGE_COLS, *by]].copy()` — a copy that was also a projection — so `join_overlaps` and `nearest_ranges` lost the columns they exist to report. `left.join_overlaps(right, strand_behavior="opposite")` returned no `ID_b`/`Score_b` at all, while the same call with `"same"` returned both, making the loss depend on the strand option rather than on the data. Downstream code indexing those columns raised `KeyError` for one strand setting and worked for the others. The copy is now of the whole frame; the projection was never needed
- **fix**: `complement_ranges` emitted intervals of non-positive length at the externally bounded edges — an interval no validator in the library accepts. With `chromsizes` and a last interval ending exactly on the chromosome size it emitted a terminal gap of zero length (`Start == End == size`); that gap is now dropped, as is any other gap the kernel returns with `End <= Start`
- **breaking**: `complement_ranges(include_first_interval=True)` now raises `ValueError` when any interval starts below zero. The first complement interval runs from coordinate 0 to the first interval, so a negative start produced a gap of *negative* length: one row `chr1:[-1, 0)+` came back as `Start=0, End=-1`. Negative coordinates remain legal everywhere else — `extend_ranges`, `upstream` and `five_end` all produce them by design — this is checked only where it would produce an invalid result
- **fix**: `join_overlaps` with `report_overlap_column` reported an overlap for rows that matched nothing. `DataFrame.min`/`.max` skip nulls, so an unmatched row of a `left`, `right` or `outer` join got the length of the one interval present — `Overlap = 1` on a row whose every `_b` column is null. Unmatched rows now report null
- **breaking**: `split_overlaps` no longer drops `Strand` when `use_strand=False`. It dropped the column merely because it was not a grouping key, so `gr.split_overlaps(use_strand=False)` lost strand information the input carried and no other method discards this way. Every output interval descends from an input interval and now keeps its metadata. `between=True` is unchanged: gap intervals descend from no input row, so they keep only the location columns, and `Strand` among them only when it was a grouping key
- **breaking**: `group_cumsum` accumulated across chromosomes. With no `group_by` it ran a single cumulative sum over the whole frame, so two rows of length 10 on `chr1` and `chr2` came back as `[0,10)` and `[10,20)` — one coordinate space spanning two chromosomes. `Chromosome`, and `Strand` when strand-aware, now always partition the cumulative space. The documented behaviour ("when *None* all intervals on the same chromosome are cumulated together") was already this; only the code disagreed. Naming a column in `group_by` no longer drops the chromosome partition either. The caller's own keys are placed *before* the implicit ones, because the key order decides how groups are numbered and therefore the order rows come back in when `keep_order=False`; only the key set decides which intervals share a cumulative space. `map_to_local`, `map_to_global` and `tile_ranges(add_window_id=True)` all call `group_cumsum` internally with a transcript-level key, and their output row order is unchanged by this fix
- **breaking**: `to_bed`, `to_gtf` and `to_gff3` always infer compression from the file extension. The default was `None`, which pandas reads as *no compression even for a .gz path*, while all three docstrings said "infer" — `to_bed("x.bed.gz")` silently wrote plain text under a `.gz` name. `None` is now treated as `"infer"`, so there is no longer a way to write uncompressed output to a `.gz` path; pass a different extension

# 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.
