Lodash sortedLastIndex: Purpose and Key Use Cases

Lodash's _.sortedLastIndex method is a binary search utility designed to find the highest index at which a value can be inserted into an already sorted array while maintaining its sort order. This article covers how the method operates, how it differs from _.sortedIndex, and the specific production use cases where choosing the highest insertion index is critical, such as maintaining First-In-First-Out (FIFO) ordering among duplicate keys and performing binary-search-based range queries.

What is _.sortedLastIndex?

The _.sortedLastIndex function uses a binary search algorithm to evaluate a sorted array and determine where a given value should be inserted.

_.sortedLastIndex(array, value)

The key behavior is how it handles duplicate values:

const numbers = [10, 20, 20, 20, 30];

_.sortedIndex(numbers, 20);     // Output: 1 (before existing 20s)
_.sortedLastIndex(numbers, 20); // Output: 4 (after existing 20s)

Specific Use Cases

1. Maintaining FIFO Order for Equivalent Keys

The most common use case for _.sortedLastIndex is maintaining a stable, chronologically ordered list of items that share identical primary sort values.

If you maintain an array of tasks sorted by priority (e.g., Priority 1 to 5), new tasks with the same priority should logically be queued behind previously added tasks of that same priority. Using _.sortedIndex would place the new task before existing identical priorities (LIFO). _.sortedLastIndex places the new entry directly after existing duplicates (FIFO), ensuring fair, stable queue management:

const priorityQueue = [1, 2, 2, 2, 3];
const newPriority = 2;

const insertAt = _.sortedLastIndex(priorityQueue, newPriority);
priorityQueue.splice(insertAt, 0, newPriority);
// Result: [1, 2, 2, 2, 2, 3] (appended to the end of the 2s)

2. Determining Value Multiplicity and Ranges in \(O(\log n)\) Time

By pairing _.sortedIndex and _.sortedLastIndex, you can determine the exact count and index span of duplicate items in a massive sorted array without iterating linearly.

const data = [5, 10, 15, 15, 15, 15, 20, 25];
const target = 15;

const startIndex = _.sortedIndex(data, target);     // 2
const endIndex = _.sortedLastIndex(data, target);   // 6

const count = endIndex - startIndex;                 // 4
const occurrences = data.slice(startIndex, endIndex); // [15, 15, 15, 15]

This pattern provides a pure \(O(\log n)\) search time, which is significantly faster than using filter or indexOf/lastIndexOf combinations on large datasets.

3. Binning and Range Boundary Routing

When mapping continuous data into discrete bins (such as age brackets, tax brackets, or latency percentiles), the boundary conditions dictate whether edge values are included in the lower or upper bucket:

This ensures that values equal to the bin boundary fall into the intended side of the interval without requiring manual comparison operators.