google-sheetsgoogle-apps-scriptnatural-sort

Sort Sheet Range in Natural Order


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}
]);

Solution

  • 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.