Skip to content

Implementation of a SAMRecord comparator that matches samtools' queryname sort order.#1600

Merged
tfenne merged 3 commits into
masterfrom
tf_sam_sortorder_natural
Jul 15, 2026
Merged

Implementation of a SAMRecord comparator that matches samtools' queryname sort order.#1600
tfenne merged 3 commits into
masterfrom
tf_sam_sortorder_natural

Conversation

@tfenne

@tfenne tfenne commented Mar 30, 2022

Copy link
Copy Markdown
Member

@nh13, @lbergelson I put this together mostly for fun - it's a comparator that will sort read names the same way samtools does. I'm not really sure what the best way to integrate this is in HTSJDK though. Thoughts?

Summary by CodeRabbit

  • New Features

    • Added natural sorting for SAM record read names.
    • Numeric portions of read names are compared by numeric value, including very large numbers.
    • Leading zeros do not affect numeric ordering.
    • Existing tie-breaking behavior is preserved when read names match.
  • Tests

    • Added comprehensive coverage for mixed names, numeric ordering, prefixes, empty names, large values, and record-level comparisons.

@codecov-commenter

codecov-commenter commented Mar 30, 2022

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.852%. Comparing base (a38c78d) to head (2260167).
⚠️ Report is 74 commits behind head on master.

Files with missing lines Patch % Lines
.../samtools/SAMRecordQueryNameNaturalComparator.java 94.118% 1 Missing and 1 partial ⚠️

❌ Your changes status has failed because you have indirect coverage changes. Learn more about Unexpected Coverage Changes and reasons for indirect coverage changes.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@               Coverage Diff               @@
##              master     #1600       +/-   ##
===============================================
+ Coverage     69.838%   69.852%   +0.014%     
- Complexity      9653      9664       +11     
===============================================
  Files            703       704        +1     
  Lines          37647     37681       +34     
  Branches        6114      6121        +7     
===============================================
+ Hits           26292     26321       +29     
- Misses          8904      8906        +2     
- Partials        2451      2454        +3     
Files with missing lines Coverage Δ
.../samtools/SAMRecordQueryNameNaturalComparator.java 94.118% <94.118%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nh13

nh13 commented Mar 30, 2022

Copy link
Copy Markdown
Member

Always best to be I second place: #1601

@nh13

nh13 commented Mar 30, 2022

Copy link
Copy Markdown
Member

Do you need to fall back on the first and second of pair flags as samtools does? https://github.com/samtools/samtools/blob/bdc5bb81c11f2ab25ea97351213cb87b33857c4d/bam_sort.c#L1798

@tfenne

tfenne commented Mar 30, 2022

Copy link
Copy Markdown
Member Author

@nh13 I guess it depends on whether you want to match samtools 100% or implement the "natural" sort order which doesn't prescribe how ties are broken. I don't think I have a strong opinion so long as e.g. first of pair sorts before second of pair, and secondary/supplementary records sort after primary

@tfenne

tfenne commented Mar 30, 2022

Copy link
Copy Markdown
Member Author

I think the ideal way to make this work, but which would be a PITA to retrofit would be something akin to fgbio's SamOrder, which defines more than the SO tag, and has capabilities for setting SO, SS etc. and for detecting the ordering based on more than the SO tag. But we had the benefit of writing that many years later, and after the sub-sort stuff landed in the spec.

@lbergelson

lbergelson commented Mar 30, 2022

Copy link
Copy Markdown
Member

Ah, the eternal problem #766. I was assuming that awkwardly working around this was the impetus for the last pr?

I think we have a few options none of which are trivial.
I like what you describe. The subsort field was invented to fix this problem so it makes sense that we would use it. Do you know if samtools is outputting the appropriate SS field in new versions? We never started doing so, but maybe they're more responsible... That would solve the problem going forward, and we could allow users to pick which variant order they want to use when writing new files. It wouldn't solve the problem of reading older sam files.

An alternative would be to try to make some sort of meta queryname sorter which tries to figure out the subsort when it's reading a file. That shouldn't be hard I would think, just try both sort orders and if one passes it's valid. I can't remember if there's any issue with stateful comparators but I think it wouldn't be a problem to have one that figures out which it is and then remembers it.

We should write the SS field in either case...

@tfenne

tfenne commented Mar 30, 2022

Copy link
Copy Markdown
Member Author

@lbergelson I don't think samtools writes the SS field either for queryname sorting. Though we might be able to get @jmarshall or @nh13 to help with that.

I think the easiest first step would be to have HTSJDK accept either as being queryname sorted - HTSJDK already has that SAMSortOrderChecker which is stateful. I don't think it would be hard to extend that to allow for either/or checking for queryname sorting. And that way we could at least get to the point where HTSJDK (and picard etc.) would accept either samtools or HTSJDK queryname sorting (or, "natural" and "lexicographic") as valid. It's possible that would just fix ValidateSamFile in Picard, or it might require a little more tweaking.

Then the question becomes ... if it can accept either as valid, is there any reason not to switch to the samtools style as the default output for queryname?

@lbergelson

Copy link
Copy Markdown
Member

The main reason not to switch the default immediately is that a lot of people used mixed pipeline where some of the tools are on older versions. Switching would break them until they update all parts of the pipeline.

Does samtools have trouble reading htsjdk queryname bams? I'm not sure the problem is symmetrical.

@tfenne

tfenne commented Mar 31, 2022

Copy link
Copy Markdown
Member Author

I don't believe there are issues with samtools reading picard/htsjdk queryname sorted files, but it's not something I do regularly. I think samtools tends more towards trusting/assuming the user provided acceptable input and not validating.

tfenne added 2 commits July 15, 2026 10:15
Reworks the original draft (#1600) into a form ready to build on:

- Replace the long-based numeric comparison, which overflowed on numeric runs
  of ~19+ digits, with a digit-by-digit port of htslib's strnum_cmp. It is now
  a faithful match to samtools for numeric runs of any length.
- Expose the read-name comparison as a public static compareNatural(String,String),
  mirroring the parent's compareReadNames, so it is usable without instantiating
  the comparator.
- Use explicit ASCII '0'-'9' digit testing (read names are ASCII per the SAM
  spec) rather than the Unicode-aware Character.isDigit.
- Expand the single spec-example test into 12 focused tests: value-not-lexical
  ordering, leading-zero tie-breaks, the large-number (overflow) regression,
  empty/prefix/equal names, antisymmetry, and record-level tests that exercise
  the inherited pair/strand tie-breaking.

This is the standalone comparator only; wiring it to a SAM SS:queryname:natural
sub-sort tag is deliberately left to a follow-up, since htsjdk has no sub-sort
concept yet and that is a broader, cross-cutting design.
Research into the actual reference implementation (samtools bam_sort.c strnum_cmp)
showed current samtools ignores leading-zero counts entirely: numeric runs with
the same value compare equal regardless of leading zeros ("8" == "08" == "008"),
relying on a stable sort for their relative order. The earlier port ordered
"more leading zeros first", matching older htslib and a non-normative example
(the SAM spec itself defines no natural-order algorithm and contains no such
example). Since the goal is to match samtools, drop the leading-zero tiebreak.

Tests updated accordingly: leading-zero-only differences now assert equality,
and the ordered-list test uses names with a single unambiguous ordering.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94020b39-47a3-4c8c-bad8-a9e7e597bf26

📥 Commits

Reviewing files that changed from the base of the PR and between a38c78d and 7f70e4e.

📒 Files selected for processing (2)
  • src/main/java/htsjdk/samtools/SAMRecordQueryNameNaturalComparator.java
  • src/test/java/htsjdk/samtools/SAMRecordQueryNameNaturalComparatorTest.java

📝 Walkthrough

Walkthrough

Adds a public comparator for natural ordering of SAM query names, comparing digit runs by numeric value without overflow and preserving inherited SAM record tie-breaking. Tests cover string ordering, large values, comparator properties, and unmapped record integration.

Changes

Natural query-name ordering

Layer / File(s) Summary
Natural comparator implementation
src/main/java/htsjdk/samtools/SAMRecordQueryNameNaturalComparator.java
Adds digit-aware string comparison and overrides SAM record file-order comparison to use read names naturally.
Comparator behavior and integration tests
src/test/java/htsjdk/samtools/SAMRecordQueryNameNaturalComparatorTest.java
Tests numeric ordering, leading zeros, large digit runs, prefixes, antisymmetry, record comparison, and pair-order tie-breaking.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new SAMRecord comparator matching samtools queryname sort order.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tf_sam_sortorder_natural

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tfenne

tfenne commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Revisited the comparator and made a couple of changes to align it more precisely with samtools:

  • Replaced the long-based numeric comparison (which overflowed on very long numeric runs) with a digit-by-digit port of samtools' current strnum_cmp, so it matches for numeric runs of any length. The read-name comparison is also exposed as a public static compareNatural(String, String).
  • Aligned leading-zero handling with current samtools: equal-value numeric runs now compare equal regardless of leading zeros (8 == 08 == 008) rather than ordering by leading-zero count. (The SAM spec defines no formal natural-order algorithm, so samtools' strnum_cmp is the reference.)
  • Expanded the test coverage accordingly.

Fuller integration with the SAM SS (sub-sort) tag — modeling SS in the header and automatically selecting this comparator for SS:queryname:natural — is out of scope for this PR and can be tackled separately.

@tfenne
tfenne marked this pull request as ready for review July 15, 2026 16:16
@tfenne
tfenne merged commit 2ae4aaa into master Jul 15, 2026
5 checks passed
@tfenne
tfenne deleted the tf_sam_sortorder_natural branch July 15, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants