fix: implement natural sorting for routeIds in /stop endpoint#996
fix: implement natural sorting for routeIds in /stop endpoint#996ARCoder181105 wants to merge 8 commits into
Conversation
📝 WalkthroughWalkthroughAdds 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. ChangesNatural Route Sorting
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
internal/restapi/stop_handler_test.go (1)
261-269: ⚡ Quick winExpand this test to assert the exact API contract (
routeIds+ fallback path).This currently checks
References.Routesorder only. Please also assertmodel.Data.Entry.RouteIDsorder and include at least one route with emptyShortNameso theLongNamefallback 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
📒 Files selected for processing (3)
internal/restapi/stop_handler.gointernal/restapi/stop_handler_test.gointernal/utils/string_utils.go
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
burma-shave
left a comment
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/utils/string_utils.go (1)
16-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep digit chunking aligned with numeric parsing.
extractChunktreats any Unicode decimal digit as numeric, butcompareNumericChunksonly parses ASCII decimal strings. That means inputs like"٢"vs"1"enter the numeric path, fail parsing on the first chunk, and then fall through withnumA == 0, which incorrectly orders٢before1. Either normalize Unicode digits before comparison or restrict the chunker to ASCII0-9so 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
📒 Files selected for processing (4)
internal/restapi/stop_handler.gointernal/restapi/stop_handler_test.gointernal/utils/string_utils.gointernal/utils/string_utils_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/restapi/stop_handler.go



Description
fixes #995
This PR addresses a spec gap identified in the
/stopendpoint. According to the wiki spec,routeIdsmust be sorted in natural string order byshortName(falling back tolongName), meaning"14" < "101" < "B". Previously, the API returned routes in database fetch order.Changes Made
NaturalCompareutility function tointernal/utils/to handle string chunking and numeric comparisons.stopHandlerto useslices.SortFunccombined withNaturalCompareimmediately after fetching the stop's routes from the DB.TestStopHandler_NaturalSortingto explicitly verify the"2" < "14" < "101" < "B"ordering behavior.Testing
Summary by CodeRabbit
New Features
Tests