6

Sort the objects by keys whose value is also an object and sort that internal object as well i.e sort the object recursively. Sorting should be as per key.

I looked into Stackoverflow's other questions but None of them is for Object Recursive Sorting.

Question I looked into:

Sorting JavaScript Object by property value

Example:

input = {
    "Memo": {
        "itemAmount1": "5",
        "taxName1": "TAX",
        "productPrice1": "10",
        "accountName1": "Account Receivable (Debtors)"
    },
    "Footer": {
        "productDescription2": "Maggie",
        "itemQuantity2": "49.5",
        "accountName2": "Account Receivable (Debtors)",
        "taxName2": "TAX"
    },
    "Header": {
        "itemDiscount3": "10",
        "accountName3": "Account Receivable (Debtors)",
        "productPrice3": "10",
        "taxName3": "TAX"
    }
}

Output

output = {
    "Footer": {
        "accountName2": "Account Receivable (Debtors)",
        "itemQuantity2": "49.5",
        "productDescription2": "Maggie",
        "taxName2": "TAX"
    },
    "Header": {
        "accountName3": "Account Receivable (Debtors)",
        "itemDiscount3": "10",
        "productPrice3": "10",
        "taxName3": "TAX"
    },
    "Memo": {
        "accountName1": "Account Receivable (Debtors)",
        "itemAmount1": "5",
        "productPrice1": "10",
        "taxName1": "TAX"
    }
}

It is not necessary that it is 2 level object hierarchy it may contain n level of object hierarchy which need to be sorted.

Community
  • 1
  • 1
ksr89
  • 249
  • 3
  • 9
  • 6
    Javascript objects have no order, they are a key-value map. You *can't* order them. – deceze Jul 08 '14 at 10:52
  • Fixed the title: it said "property" instead of key (which cannot be used to sort, as @deceze points) – Pablo Lozano Jul 08 '14 at 10:55
  • Why can't we sort the keys in alphabetic order? – Gaurav Ramanan Jul 08 '14 at 10:57
  • 1
    As pointed out in this answer: http://stackoverflow.com/a/5525812/2964675 it's not in the ECMAScript spec. If you want some sort of order, try Array Objects. – MarcoL Jul 08 '14 at 11:01
  • @MarcoCI @_deceze this link is pointing problem of Browsers, I checked that when I create an object in chrome's console, it gives me an already sorted object but I want to sort an object using **node.js**. – ksr89 Jul 08 '14 at 12:32
  • 1
    which comes first: your height, your age, the colour of your eyes, or your weight? *properties* of an object have no _order_, nor should they. Use another ordered structure (such as array) in tandem with the object. – Mulan Jul 08 '20 at 15:11

7 Answers7

7

I think what @ksr89 means is that when we apply a for - in loop, we get keys in sorted order. I think this is a valid use case especially in the development of Node.js based ORMs

The following function should work and is I think what you are looking for.

 input = {
    "Memo": {
        "itemAmount1": "5",
        "taxName1": "TAX",
        "productPrice1": "10",
        "accountName1": "Account Receivable (Debtors)"
    },
    "Footer": {
        "productDescription2": "Maggie",
        "itemQuantity2": "49.5",
        "accountName2": "Account Receivable (Debtors)",
        "taxName2": "TAX"
    },
    "Header": {
        "itemDiscount3": "10",
        "accountName3": "Account Receivable (Debtors)",
        "productPrice3": "10",
        "taxName3": "TAX"
    }
}
window.sortedObject = sort(input);

function sort(object){
    if (typeof object != "object" || object instanceof Array) // Not to sort the array
        return object;
    var keys = Object.keys(object);
    keys.sort();
    var newObject = {};
    for (var i = 0; i < keys.length; i++){
        newObject[keys[i]] = sort(object[keys[i]])
    }
    return newObject;
}
for (var key in sortedObject){
    console.log (key);
    //Prints keys in order
}
ksr89
  • 249
  • 3
  • 9
Gaurav Ramanan
  • 3,655
  • 2
  • 21
  • 29
  • Nothing guarantees the order of the keys when looping over them using `for..in`. It will *probably* keep the order that the object was created with, but again, there's no guarantee for that and may work completely differently in different Javascript implementations. – deceze Jul 08 '14 at 11:34
  • @deceze I have tested this with Chrome and therefore it will work in Node.js. I will try in other browsers & post here. I am aware this is not part of the ECMA standard but it is a good enough solution for Node.js applications, specifically here where ksr89 is trying to implement an ORM I think. – Gaurav Ramanan Jul 09 '14 at 05:30
  • It may happen that `object` where its keys needs ordering is inside of a list/array. This function won't sort it. – Tom Raganowicz Oct 22 '19 at 08:33
  • "Nothing guarantees the order of the keys when looping over them" - this is no longer true as this has recently changed – danday74 Aug 18 '22 at 09:44
4

I was on this page to write the following information. The code is based on Gaurav Ramanan's answer, but handles arrays and null differently.

Comparing JSON

To compare data from JSON files you may want to have them formatted the same way

  • from javascript: JSON.stringify(JSON.parse(jsonString), null, '\t') the last parameter could also be a number of spaces the last 2 parameters are optional (minified output if absent)
  • from Visual Studio Code: with the Prettify JSON extension

Verify indentation (i.e. TABs) and line endings (i.e. Unix). Also, keys may be recursively sorted during formatting.

Sorting keys with javascript:

const {isArray} = Array
const {keys} = Object

function sortKeysRec(obj) {
    if (isArray(obj)) {
        const newArray = []
        for (let i = 0, l = obj.length; i < l; i++)
            newArray[i] = sortKeysRec(obj[i])
        return newArray
    }
    if (typeof obj !== 'object' || obj === null)
        return obj
    const sortedKeys = keys(obj).sort()
    const newObject = {}
    for (let i = 0, l = sortedKeys.length; i < l; i++)
        newObject[sortedKeys[i]] = sortKeysRec(obj[sortedKeys[i]])
    return newObject
}

Ensure unix line endings with javascript: jsonString.replace(/\r\n/ug, '\n').

Rivenfall
  • 1,189
  • 10
  • 15
2

Following up on @Gaurav Ramanan 's answer here's a shorter ES6 approach:

function sort(obj) {
  if (typeof obj !== "object" || Array.isArray(obj))
    return obj;
  const sortedObject = {};
  const keys = Object.keys(obj).sort();
  keys.forEach(key => sortedObject[key] = sort(obj[key]));
  return sortedObject;
}

The first condition will simply ensure that you only parse a valid object. So if it's not, it will return immediately with the original value unchanged.

Then an empty object is assigned because it will be used in the forEach loop where it will be mutated with the final sorted result.

The output will be a recursively sorted object.

  • Beautiful! If you have arrays with objects inside them whose keys you want sorted, just replace the first condition with: `if (typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(sort);`. – Ahmed Fasih May 30 '23 at 21:35
1

The solution above works only for the current implementation detail of node.js.

The ECMAScript standard doesn't guarantee any order for the keys iteration.

That said, the only solution I can think of is to use an array as support to sort the properties of the object and iterate on it:

var keys = Object.keys(object);
keys.sort();

for (var i = 0; i < keys.length; i++){
// this won't break if someone change NodeJS or Chrome implementation
    console.log(keys[i]);
}
LStarky
  • 2,740
  • 1
  • 17
  • 47
MarcoL
  • 9,829
  • 3
  • 37
  • 50
1

As this has recently been revived, I think it's worth pointing out again that we should generally treat objects as unordered collections of properties. Although ES6 did specify key traversal order (mostly first-added to last-added properties, but with a twist for integer-like keys), depending on this feels as though you're misusing your type. If it's ordered, use an array.

That said, if you are determined to do this, then it's relatively simple with ES6:

const sortKeys = (o) =>
  Object (o) !== o || Array .isArray (o)
    ? o
    : Object .keys (o) .sort () .reduce ((a, k) => ({...a, [k]: sortKeys (o [k])}), {})

const input = {Memo: {itemAmount1: "5", taxName1: "TAX", productPrice1: "10", accountName1: "Account Receivable (Debtors)"}, Footer: {productDescription2: "Maggie", itemQuantity2: "49.5", accountName2: "Account Receivable (Debtors)", taxName2: "TAX"}, Header: {itemDiscount3: "10", accountName3: "Account Receivable (Debtors)", productPrice3: "10", taxName3: "TAX"}}

console .log (
  sortKeys(input)
)
.as-console-wrapper {min-height: 100% !important; top: 0}

Note that there is a potential performance issue here as described well by Rich Snapp. I would only spend time fixing it if it turned out to be a bottleneck in my application, but if we needed to we could fix that issue with a version more like this:

const sortKeys = (o) =>
  Object (o) !== o || Array .isArray (o)
    ? o
    : Object .keys (o) .sort () .reduce ((a, k) => ((a [k] = sortKeys (o [k]), a)), {})

While this works, the addition of the comma operator and the use of property assignment make it uglier to my mind. But either one should work.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
1

This tested answer provides a recursive solution to a recursive problem. Note, it DOES NOT sort arrays (this is often undesired) but DOES sort objects within arrays (even nested arrays).

It uses lodash _.isPlainObject to simplify the logic of identifying an object but if you are not using lodash you can replace this with your own is it an object? logic.

const sortObjectProps = obj => {
  return Object.keys(obj).sort().reduce((ordered, key) => {
    let value = obj[key]

    if (_.isPlainObject(value)) {
      ordered[key] = sortObjectProps(value)
    } else {
      if (Array.isArray(value)) {
        value = value.map(v => {
          if (_.isPlainObject(v)) v = sortObjectProps(v)
          return v
        })
      }
      ordered[key] = value
    }
    return ordered
  }, {})
}

const input = {
  "Memo": {
    "itemAmount1": "5",
    "taxName1": "TAX",
    "productPrice1": "10",
    "accountName1": "Account Receivable (Debtors)"
  },
  "Footer": {
    "productDescription2": "Maggie",
    "itemQuantity2": "49.5",
    "accountName2": "Account Receivable (Debtors)",
    "taxName2": "TAX"
  },
  "Header": {
    "itemDiscount3": "10",
    "accountName3": "Account Receivable (Debtors)",
    "productPrice3": "10",
    "taxName3": "TAX"
  }
}

const expected = {
  "Footer": {
    "accountName2": "Account Receivable (Debtors)",
    "itemQuantity2": "49.5",
    "productDescription2": "Maggie",
    "taxName2": "TAX"
  },
  "Header": {
    "accountName3": "Account Receivable (Debtors)",
    "itemDiscount3": "10",
    "productPrice3": "10",
    "taxName3": "TAX"
  },
  "Memo": {
    "accountName1": "Account Receivable (Debtors)",
    "itemAmount1": "5",
    "productPrice1": "10",
    "taxName1": "TAX"
  }
}

const actual = sortObjectProps(input)

const success = JSON.stringify(actual) === JSON.stringify(expected)

console.log(JSON.stringify(actual))
console.log('success (actual is expected)', success)
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
danday74
  • 52,471
  • 49
  • 232
  • 283
  • For those of you who don't know what recursion is ... to understand recursion you must first of all understand recursion – danday74 Aug 18 '22 at 09:42
1

Sorting everything in an object

Yes, that included nested objects, arrays, arrays of objects (and sorting those objects, too!)

I took @danday74's solution as a starting point and made my version work with arrays, arrays nested in arrays, and objects nested in arrays.

That is to say, even something like this:

const beforeSort = {
  foo: {
    b: 2,
    a: [
      { b: 1, a: 10 },
      { y: 0, x: 5 },
    ],
  },
};

Becomes this:


const afterSort = {
  foo: {
    a: [
      { x: 5, y: 0 }, // See note
      { a: 10, b: 1 },
    ],
    b: 2,
  },
};

/**
 * Note:
 * This object goes first in this array because 5 < 10.
 * Unlike objects sorting by keys; arrays of objects sort 
 * by value of the first property, not by the key of the first property.
 * This was important for me because arrays of objects are typically
 * the same kinds of objects, so sorting alphabetically by key would be 
 * pretty pointless. Instead, it sorts by the value.
 */

My situation was I needed to compare an object (whose arrays' order didn't matter) to the JSON.stringify()'d object's string. Parsing the JSON into an object and performing a deep comparison between the objects wasn't an option as these strings were in a database.

And since the order of things could change randomly, I needed to make sure that the JSON generated was exactly the same every time. That meant sorting literally everything in the object, no matter how nested.

Using the above examples; the object beforeSort:

// After running through JSON.stringify()...
'{"foo":{"b":2,"a":[{"b":1,"a":10},{"y":0,"x":5}]}}'

Needs to match afterSort:

// After running through JSON.stringify()...
'{"foo":{"a":[{"x":5,"y":0},{"a":10,"b":1}],"b":2}}'

(Same object, different string.)

Obviously, if the order of an array is important to you, this won't be helpful.

Though... I'm not in the mood to look at it now, I imagined the idea that I could turn array-sorting on and off with a simple argument and a strategic if statement. Worth trying!

JavaScript version (with test objects)

// I use lodash's isEqual() is cloneDeep().
// Testing provided below.

function deepSortObject(object) {
  const deepSort = (object) => {
    // Null or undefined objects return immediately.
    if (object == null) {
      return object;
    }

    // Handle arrays.
    if (Array.isArray(object)) {
      return (
        _.cloneDeep(object)
          // Recursively sort each item in the array.
          .map((item) => deepSort(item))
          // Sort array itself.
          .sort((a, b) => {
            let workingA = a;
            let workingB = b;

            // Object or Array, we need to look at its first value...
            if (typeof a === "object") {
              workingA = a[Object.keys(a)[0]];
            }
            if (typeof b === "object") {
              workingB = b[Object.keys(b)[0]];
            }
            if (Array.isArray(a)) {
              workingA = a[0];
            }
            if (Array.isArray(b)) {
              workingB = b[0];
            }

            // If either a or b was an object/array, we deep sort...
            if (workingA !== a || workingB !== b) {
              const sortedOrder = deepSort([workingA, workingB]);

              if (_.isEqual(sortedOrder[0], workingA)) {
                return -1;
              } else {
                return 1;
              }
            }

            // If both were scalars, sort the normal way!
            return a < b ? -1 : a > b ? 1 : 0;
          })
      );
    }

    // Anything other than Objects or Arrays just send it back.
    if (typeof object != "object") {
      return object;
    }

    // Handle objects.
    const keys = Object.keys(object);
    keys.sort();

    const newObject = {};

    for (let i = 0; i < keys.length; ++i) {
      newObject[keys[i]] = deepSort(object[keys[i]]);
    }
    return newObject;
  };

  return deepSort(object);
}


// TESTING
const unsortedInput = {
  ObjectC: {
    propertyG_C: [[8, 7, 6], [5, 4, 3], [], [2, 1, 0]], // Array of arrays
    propertyF_C: [
      // This should result in sorting like: [2]'s a:0, [1]'s a:1, [0]'s a.x:5
      {
        b: 2,
        a: [
          { b: 1, a: 10 }, // Sort array y by property a...
          { y: 0, x: 5 }, // vs property x
          // Hot testing tip: change x to -1 and propertyF_C will sort it to the top!
        ],
      },
      { c: 1, b: [1, 2, 0], a: 1 },
      { c: 0, b: [1, 2, 0], a: 0 },
    ],
    propertyE_C: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_C: true,
    propertyC_C: 1,
    propertyD_C: [2, 0, 1],
    propertyA_C: "Blah",
  },
  ObjectA: {
    propertyE_A: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_A: true,
    propertyC_A: 1,
    propertyD_A: [2, 0, 1],
    propertyA_A: "Blah",
  },
  ObjectB: {
    propertyE_B: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_B: true,
    propertyC_B: 1,
    propertyD_B: [2, 0, 1],
    propertyA_B: "Blah",
  },
};

const sortedOutput = {
  ObjectA: {
    100: true,
    200: false,
    propertyA_A: "Blah",
    propertyB_A: true,
    propertyC_A: 1,
    propertyD_A: [0, 1, 2],
    propertyE_A: {
      a: 1,
      b: 2,
    },
  },
  ObjectB: {
    100: true,
    200: false,
    propertyA_B: "Blah",
    propertyB_B: true,
    propertyC_B: 1,
    propertyD_B: [0, 1, 2],
    propertyE_B: {
      a: 1,
      b: 2,
    },
  },
  ObjectC: {
    100: true,
    200: false,
    propertyA_C: "Blah",
    propertyB_C: true,
    propertyC_C: 1,
    propertyD_C: [0, 1, 2],
    propertyE_C: {
      a: 1,
      b: 2,
    },
    propertyF_C: [
      { a: 0, b: [0, 1, 2], c: 0 },
      { a: 1, b: [0, 1, 2], c: 1 },
      {
        a: [
          { x: 5, y: 0 },
          { a: 10, b: 1 },
        ],
        b: 2,
      },
    ],
    propertyG_C: [[0, 1, 2], [3, 4, 5], [6, 7, 8], []],
  },
};

// Some basic testing...
console.log("Before sort, are the JSON strings the same?", JSON.stringify(unsortedInput) === JSON.stringify(sortedOutput));
console.log("After sort, are the JSON stirngs the same?", JSON.stringify(deepSortObject(unsortedInput)) === JSON.stringify(sortedOutput));
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

TypeScript version

/* eslint-disable @typescript-eslint/no-explicit-any */
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";

/**
 * Takes an object that may have nested properties and returns a new shallow
 * copy of the object with the keys sorted. It also sorts arrays, and arrays of
 * objects.
 *
 * IF THERE IS ANY IMPORTANCE IN THE ORDER OF YOUR ARRAYS DO NOT USE THIS.
 *
 * Use this in conjunction with JSON.strigify() to create consistent string
 * representations of the same object, even if the order of properties or arrays
 * might be different.
 *
 * And if you're wondering. Yes, modern JS does maintain order in objects:
 * https://exploringjs.com/es6/ch_oop-besides-classes.html#_traversal-order-of-properties
 *
 * @param object
 * @returns object
 */
export function deepSortObject(object: any) {
  const deepSort = (object: any): any => {
    // Null or undefined objects return immediately.
    if (object == null) {
      return object;
    }

    // Handle arrays.
    if (Array.isArray(object)) {
      return (
        cloneDeep(object)
          // Recursively sort each item in the array.
          .map((item) => deepSort(item))
          // Sort array itself.
          .sort((a, b) => {
            let workingA = a;
            let workingB = b;

            // Object or Array, we need to look at its first value...
            if (typeof a === "object") {
              workingA = a[Object.keys(a)[0]];
            }
            if (typeof b === "object") {
              workingB = b[Object.keys(b)[0]];
            }
            if (Array.isArray(a)) {
              workingA = a[0];
            }
            if (Array.isArray(b)) {
              workingB = b[0];
            }

            // If either a or b was an object/array, we deep sort...
            if (workingA !== a || workingB !== b) {
              const sortedOrder = deepSort([workingA, workingB]);

              if (isEqual(sortedOrder[0], workingA)) {
                return -1;
              } else {
                return 1;
              }
            }

            // If both were scalars, sort the normal way!
            return a < b ? -1 : a > b ? 1 : 0;
          })
      );
    }

    // Anything other than Objects or Arrays just send it back.
    if (typeof object != "object") {
      return object;
    }

    // Handle objects.
    const keys = Object.keys(object);
    keys.sort();

    const newObject: Record<string, unknown> = {};

    for (let i = 0; i < keys.length; ++i) {
      newObject[keys[i]] = deepSort(object[keys[i]]);
    }
    return newObject;
  };

  return deepSort(object);
}

Unit tests

import { deepSortObject } from "@utils/object";

const unsortedInput = {
  ObjectC: {
    propertyG_C: [[8, 7, 6], [5, 4, 3], [], [2, 1, 0]], // Array of arrays
    propertyF_C: [
      // This should result in sorting like: [2]'s a:0, [1]'s a:1, [0]'s a.x:5
      {
        b: 2,
        a: [
          { b: 1, a: 10 }, // Sort array y by property a...
          { y: 0, x: 5 }, // vs property x
          // Hot testing tip: change x to -1 and propertyF_C will sort it to the top!
        ],
      },
      { c: 1, b: [1, 2, 0], a: 1 },
      { c: 0, b: [1, 2, 0], a: 0 },
    ],
    propertyE_C: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_C: true,
    propertyC_C: 1,
    propertyD_C: [2, 0, 1],
    propertyA_C: "Blah",
  },
  ObjectA: {
    propertyE_A: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_A: true,
    propertyC_A: 1,
    propertyD_A: [2, 0, 1],
    propertyA_A: "Blah",
  },
  ObjectB: {
    propertyE_B: {
      b: 2,
      a: 1,
    },
    200: false,
    100: true,
    propertyB_B: true,
    propertyC_B: 1,
    propertyD_B: [2, 0, 1],
    propertyA_B: "Blah",
  },
};

const sortedOutput = {
  ObjectA: {
    100: true,
    200: false,
    propertyA_A: "Blah",
    propertyB_A: true,
    propertyC_A: 1,
    propertyD_A: [0, 1, 2],
    propertyE_A: {
      a: 1,
      b: 2,
    },
  },
  ObjectB: {
    100: true,
    200: false,
    propertyA_B: "Blah",
    propertyB_B: true,
    propertyC_B: 1,
    propertyD_B: [0, 1, 2],
    propertyE_B: {
      a: 1,
      b: 2,
    },
  },
  ObjectC: {
    100: true,
    200: false,
    propertyA_C: "Blah",
    propertyB_C: true,
    propertyC_C: 1,
    propertyD_C: [0, 1, 2],
    propertyE_C: {
      a: 1,
      b: 2,
    },
    propertyF_C: [
      { a: 0, b: [0, 1, 2], c: 0 },
      { a: 1, b: [0, 1, 2], c: 1 },
      {
        a: [
          { x: 5, y: 0 },
          { a: 10, b: 1 },
        ],
        b: 2,
      },
    ],
    propertyG_C: [[0, 1, 2], [3, 4, 5], [6, 7, 8], []],
  },
};

describe("object utils", () => {
  describe("sortObjectByKeys()", () => {
    test("should sort correctly", () => {
      expect(JSON.stringify(deepSortObject(unsortedInput))).toEqual(
        JSON.stringify(sortedOutput)
      );
    });
  });
});

My lead told me this might be worthy of cleaning up and hosting on NPM, I dunno. Too lazy. So I posted it here instead.

KG23
  • 105
  • 7