Skip to content

fix: implement natural sorting for routeIds in /stop endpoint#996

Open
ARCoder181105 wants to merge 8 commits into
OneBusAway:mainfrom
ARCoder181105:fix/stop-route-natural-sorting
Open

fix: implement natural sorting for routeIds in /stop endpoint#996
ARCoder181105 wants to merge 8 commits into
OneBusAway:mainfrom
ARCoder181105:fix/stop-route-natural-sorting

Conversation

@ARCoder181105
Copy link
Copy Markdown
Collaborator

@ARCoder181105 ARCoder181105 commented Jun 1, 2026

Description

fixes #995

This PR addresses a spec gap identified in the /stop endpoint. According to the wiki spec, routeIds must be sorted in natural string order by shortName (falling back to longName), meaning "14" < "101" < "B". Previously, the API returned routes in database fetch order.

Changes Made

  • Added a NaturalCompare utility function to internal/utils/ to handle string chunking and numeric comparisons.
  • Updated stopHandler to use slices.SortFunc combined with NaturalCompare immediately after fetching the stop's routes from the DB.
  • Added a targeted test TestStopHandler_NaturalSorting to explicitly verify the "2" < "14" < "101" < "B" ordering behavior.

Testing

  • Added automated unit tests verifying natural sort.
  • Existing tests pass successfully.
image

Summary by CodeRabbit

  • New Features

    • Stop endpoint now returns routes in natural string order (numeric-aware), using short names when present and falling back to long names, improving route list readability.
  • Tests

    • Added tests verifying natural sorting behavior, including numeric ordering, fallback name usage, and edge cases (leading zeros, large numbers).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds NaturalCompare and uses it (via slices.SortFunc) in the /stop handler to sort routes by ShortName (fallback LongName) before building route references; unit and integration tests validate chunk parsing, numeric comparison, and the endpoint's natural ordering.

Changes

Natural Route Sorting

Layer / File(s) Summary
Natural string comparison utility
internal/utils/string_utils.go, internal/utils/string_utils_test.go
Adds NaturalCompare(a,b string) int, extractChunk, compareNumericChunks, and unit tests covering chunk extraction, numeric comparisons (uint64 and big.Int fallback), leading-zero tie-breaks, overflow/large numbers, and various compare cases.
Stop handler route sorting integration
internal/restapi/stop_handler.go
Imports comparator helpers and slices, applies slices.SortFunc to sort routes by ShortName (fallback LongName) with AgencyID/ID tie-breakers immediately after fetching routes, before building combined route IDs/references.
Natural sorting integration test
internal/restapi/stop_handler_test.go
Adds TestStopHandler_NaturalSorting which creates out-of-order routes (e.g., "101", "B", "14", "2", plus a Fallback with empty ShortName), calls the stop endpoint, and asserts References.Routes and Entry.RouteIDs are returned in natural order ["2","14","101","B","Fallback"] and matching routeIDs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • burma-shave
  • fletcherw
  • aaronbrethorst
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: implementing natural sorting for routeIds in the /stop endpoint, which is the primary focus of this PR.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #995: implements NaturalCompare utility, updates stopHandler with slices.SortFunc sorting, adds comprehensive unit tests, and achieves the target ordering ('2' < '14' < '101' < 'B').
Out of Scope Changes check ✅ Passed All changes are directly related to implementing natural sorting for routes in the /stop endpoint; no unrelated modifications were introduced beyond the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/restapi/stop_handler_test.go (1)

261-269: ⚡ Quick win

Expand this test to assert the exact API contract (routeIds + fallback path).

This currently checks References.Routes order only. Please also assert model.Data.Entry.RouteIDs order and include at least one route with empty ShortName so the LongName fallback sorting path is exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/stop_handler_test.go` around lines 261 - 269, The test
currently only asserts ordering of model.Data.References.Routes; extend it to
also assert model.Data.Entry.RouteIDs preserves the exact API contract order
(i.e., the slice of route IDs in the same natural/fallback order), and add at
least one route in the test fixture with an empty ShortName so the LongName
fallback path is exercised; specifically update the test setup to include a
route whose ShortName == "" and LongName non-empty, then assert
returnedShortNames and model.Data.Entry.RouteIDs are each equal to the expected
order (where the empty ShortName uses LongName for ordering) to validate both
the public References.Routes ordering and the Entry.RouteIDs contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/restapi/stop_handler.go`:
- Around line 35-47: The comparator passed to slices.SortFunc for sorting a
[]gtfsdb.GetRoutesForStopRow currently only calls utils.NaturalCompare(nameA,
nameB) which can return 0 and produce nondeterministic relative ordering; update
the comparator used in slices.SortFunc to apply deterministic tie-breakers when
utils.NaturalCompare returns 0 by comparing row.AgencyID (string) and then
row.ID (or another stable unique field) in order to produce a consistent
ordering for equal natural names; locate the anonymous comparator function
around slices.SortFunc, call utils.NaturalCompare first, and if it returns 0
return comparison results for a.AgencyID vs b.AgencyID, and if still equal
return comparison of a.ID vs b.ID.

In `@internal/utils/string_utils.go`:
- Around line 33-34: NaturalCompare currently ignores strconv.ParseUint errors
when converting numeric chunks (lines using numA, _ := strconv.ParseUint(chunkA,
10, 64) and numB, _ := strconv.ParseUint(chunkB, 10, 64)), which collapses
overflowing chunks to math.MaxUint64 and can break numeric ordering; update
NaturalCompare to detect and handle ParseUint errors explicitly: when ParseUint
returns an error (ErrRange), fall back to comparing numeric chunks by length
(longer digit-run = larger) then by lexicographic order if lengths equal, or
alternatively parse using big.Int for arbitrarily large numbers; adjust the
logic around chunkA/chunkB parsing so you don’t ignore errors and ensure correct
ordering for large numeric chunks.
- Around line 14-17: extractChunk currently advances by byte indices and calls
unicode.IsDigit on s[i], which breaks on UTF-8 and non-ASCII digits; change
extractChunk to iterate runes (for idx, r := range s[startByteOffset:]) and use
unicode.IsDigit(r) to build the chunk and return proper byte offsets and the
isDigit flag (or explicitly document/enforce ASCII-only input). In
NaturalCompare, handle strconv.ParseUint errors: do not silently treat ErrRange
as math.MaxUint64 — either parse numeric chunks with math/big.Int for
arbitrary-size integers and compare via big.Int.Cmp, or on overflow fall back to
comparing numeric chunk lengths (longer numeric string = larger) and then
lexicographically if lengths equal; update the strconv.ParseUint usage in
NaturalCompare to check and act on the returned error instead of ignoring it.

---

Nitpick comments:
In `@internal/restapi/stop_handler_test.go`:
- Around line 261-269: The test currently only asserts ordering of
model.Data.References.Routes; extend it to also assert model.Data.Entry.RouteIDs
preserves the exact API contract order (i.e., the slice of route IDs in the same
natural/fallback order), and add at least one route in the test fixture with an
empty ShortName so the LongName fallback path is exercised; specifically update
the test setup to include a route whose ShortName == "" and LongName non-empty,
then assert returnedShortNames and model.Data.Entry.RouteIDs are each equal to
the expected order (where the empty ShortName uses LongName for ordering) to
validate both the public References.Routes ordering and the Entry.RouteIDs
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59852292-d3d9-4591-8068-238480e51dc8

📥 Commits

Reviewing files that changed from the base of the PR and between d7f9874 and ad1213b.

📒 Files selected for processing (3)
  • internal/restapi/stop_handler.go
  • internal/restapi/stop_handler_test.go
  • internal/utils/string_utils.go

Comment thread internal/restapi/stop_handler.go
Comment thread internal/utils/string_utils.go Outdated
Comment thread internal/utils/string_utils.go Outdated
@ARCoder181105
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Collaborator

@burma-shave burma-shave left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed findings from natural sort implementation.


// Assert the array matches the spec's natural order
expectedOrder := []string{"2", "14", "101", "B", "Fallback"}
assert.Equal(t, expectedOrder, returnedNames, "Routes should be sorted in natural string order")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion checks sort order on References.Routes, but the spec doesn't require references arrays to be sorted — only Entry.RouteIDs and Entry.StaticRouteIDs have that requirement.

The assertion passes today because the handler appends routes directly from the sorted routes slice (not via a map), so order is preserved as a side effect. But if the route-reference builder is ever refactored to use a deduplication map (the canonical pattern elsewhere in this codebase), this test will fail intermittently even though the spec-required behavior is intact.

The Entry.RouteIDs assertion below already covers the spec requirement. Consider removing the returnedNames loop and this assertion.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 4, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/utils/string_utils.go (1)

16-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep digit chunking aligned with numeric parsing.

extractChunk treats any Unicode decimal digit as numeric, but compareNumericChunks only parses ASCII decimal strings. That means inputs like "٢" vs "1" enter the numeric path, fail parsing on the first chunk, and then fall through with numA == 0, which incorrectly orders ٢ before 1. Either normalize Unicode digits before comparison or restrict the chunker to ASCII 0-9 so every “numeric” chunk is actually parseable.

Suggested minimal fix
+func isASCIIDigit(r rune) bool {
+	return r >= '0' && r <= '9'
+}
+
 // extractChunk grabs either a contiguous sequence of digits or non-digits.
 func extractChunk(s string, start int) (string, bool, int) {
 	if start >= len(s) {
 		return "", false, start
 	}
 
 	var isDigit bool
 	for idx, r := range s[start:] {
 		if idx == 0 {
-			isDigit = unicode.IsDigit(r)
+			isDigit = isASCIIDigit(r)
 			continue
 		}
-		if unicode.IsDigit(r) != isDigit {
+		if isASCIIDigit(r) != isDigit {
 			return s[start : start+idx], isDigit, start + idx
 		}
 	}
 	return s[start:], isDigit, len(s)
 }
In Go, does unicode.IsDigit return true for non-ASCII decimal digits such as Arabic-Indic "٢", and do strconv.ParseUint(base 10) or math/big.Int.SetString(base 10) accept those non-ASCII digits?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/utils/string_utils.go` around lines 16 - 49, extractChunk currently
treats any unicode.IsDigit as numeric but compareNumericChunks (and
strconv.ParseUint / big.Int.SetString) only accept ASCII 0-9, causing
mis-ordering for non-ASCII digits; fix by restricting the chunker to ASCII
digits: in the chunk extraction loop (the code block that sets isDigit and
iterates r over s[start:]) replace unicode.IsDigit(r) with a check for ASCII
digits (r >= '0' && r <= '9') or introduce an isASCIIDigit helper and use it
there so every "numeric" chunk is guaranteed parseable by compareNumericChunks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/utils/string_utils.go`:
- Around line 16-49: extractChunk currently treats any unicode.IsDigit as
numeric but compareNumericChunks (and strconv.ParseUint / big.Int.SetString)
only accept ASCII 0-9, causing mis-ordering for non-ASCII digits; fix by
restricting the chunker to ASCII digits: in the chunk extraction loop (the code
block that sets isDigit and iterates r over s[start:]) replace
unicode.IsDigit(r) with a check for ASCII digits (r >= '0' && r <= '9') or
introduce an isASCIIDigit helper and use it there so every "numeric" chunk is
guaranteed parseable by compareNumericChunks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a3aaba7-0599-4327-9192-5a9da9379af7

📥 Commits

Reviewing files that changed from the base of the PR and between ad1213b and fc0204b.

📒 Files selected for processing (4)
  • internal/restapi/stop_handler.go
  • internal/restapi/stop_handler_test.go
  • internal/utils/string_utils.go
  • internal/utils/string_utils_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/restapi/stop_handler.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec-gap: Implement natural sorting for routeIds in the /stop endpoint

2 participants