Using Google Apps Script (not using functions), in Google Sheets, how can I sort a range in natural order (so that "4. This thing" comes before "10. That Thing"). Here is what I've been using, but it doesn't work in natural order:
SpreadsheetApp.getActiveSpreadsheet().getRangeByName('Active!Tickets').sort([
{column: 5, ascending: true},
{column: 4, ascending: true},
{column: 6, ascending: true}
]);
The following function sorts a range much like Range.sort()
, except that it observes numeric values within strings.
When a key column looks like ['10.c', '2.b', '1.a', 'd.2', 'd.10',]
,
Range.sort()
orders it as ['1.a', '10.c', '2.b', 'd.10', 'd.2',]
while this function gets it as ['1.a', '2.b', '10.c', 'd.2', 'd.10',]
.
Blank values are sorted to the end of the list, regardless of whether you're sorting ascending or descending. Also sorts diacritics such as é
, à
and ä
according to the rules relevant in the locale of the spreadsheet. Formulas in cells are preserved.
/** @OnlyCurrentDoc */
function test() {
const range = SpreadsheetApp.getActive().getRange('Active!Tickets'); // use a named range or an A1 type reference like 'Sheet1!A2:F'
sortRangeWithPartlyNumericKeys_(range, [
{ column: 5, ascending: true },
{ column: 4, ascending: true },
{ column: 6, ascending: true },
]);
}
/**
* Sorts a range the same way as Range.sort() but observes numeric values within strings.
*
* When a key column looks like ['10.c', '2.b', '1.a', 'd.2', 'd.10',],
* Range.sort() sets the range as ['1.a', '10.c', '2.b', 'd.10', 'd.2',]
* while this function sets it as ['1.a', '2.b', '10.c', 'd.2', 'd.10',].
* Sorts blanks to the end regardless of sort order. Preserves formulas.
*
* @param {SpreadsheetApp.Range} range A spreadsheet range.
* @param {Object[]} sortSpecObj An array of objects with these fields:
* {Number} column The one-based index of a column within the range.
* {Boolean} ascending Optional. When false, sorts in descending order.
* @param {String} optLanguage Optional. The BCP 47 primary language subtag whose sort order to use such as 'en', 'fr', 'es' or 'ja'. Defaults to the locale set in File > Settings.
* @return {SpreadsheetApp.Range} The sorted range, for chaining.
*/
function sortRangeWithPartlyNumericKeys_(range, sortSpecObj, optLanguage) {
// version 1.2, written by --Hyde, 6 May 2025
// - improve handling of strings that start with a number
// - correctly sort Date objects
// - see https://stackoverflow.com/a/79586996/13045193
'use strict';
const spec = sortSpecObj ? [sortSpecObj].flat(Infinity) : [{ column: 1, ascending: true },];
const columnIndices = spec.map(s => (s?.column || Number(s) || 1) - 1).reverse();
const sortOrders = spec.map(s => s?.ascending ?? true).reverse();
if (!optLanguage) optLanguage = range.getSheet().getParent().getSpreadsheetLocale().match(/[a-z]+/)?.[0] || 'en';
let values = range.getValues();
let formulas = range.getFormulasR1C1().map((row, i) => row.map((f, j) => f || values[i][j]));
columnIndices.forEach((columnIndex, i) => {
const order = 1 - (sortOrders[i] ? 0 : 2);
const keys = values
.map(row => row[columnIndex])
.map((v, rowIndex) => ({ rowIndex, v: v && v.toISOString ? v.toISOString() : v }))
.sort((a, b) => String(a.v).localeCompare(b.v, optLanguage, { numeric: true }))
.sort((a, b) => [a.v, b.v].join('') === '' ? 0 : a.v === '' ? order : b.v === '' ? -order : 0);
const _order = (a) => order === 1 ? a : a.reverse();
values = _order(keys.map(k => values[k.rowIndex]));
formulas = _order(keys.map(k => formulas[k.rowIndex]));
});
return range.setValues(formulas);
}
See Array.sort() and String.localeCompare().
To enable transformations of sort keys like SED14726
to 14726SED
so that the range sorts by the five digits as the primary key, try sortRangeWithPartlyNumericKeys_()
version 1.3.