forEach method seems to skip an entry - javascript

Aim
I am trying to make a function which would take an array of "votes" and return an object containing the number of times each "candidate" received a score of 1.
For example an array such as:
[
{
"blue": 1,
"red": 3,
"purple": 2,
"yellow": 4
},
{
"blue": 2,
"red": 3,
"purple": 4,
"yellow": 1
},
{
"blue": 1,
"red": 2,
"purple": 4,
"yellow": 3
},
{
"blue": 3,
"red": 4,
"purple": 2,
"yellow": 1
}
];
should return an object
{
"blue": 2,
"red": 0,
"purple": 0,
"yellow": 2
}
Current Code
Currently I have written the function
// adds up the first choice results from raw data
// return object containing number of first choice votes each candidate received
const add_first_choices = function (raw_data) {
// create object to store number of first choices
const storage_obj = empty_candidate_obj(raw_data);
// loop through results, adding first choices to storage_obj
raw_data.forEach(function (single_voter_obj) {
Object.values(single_voter_obj).forEach(function (value) {
if (value === 1) {
storage_obj[getKeyByValue(single_voter_obj, value)] += 1;
}
});
});
return storage_obj;
};
which uses the following two other functions
// returns object of candidate names, each with value of 0
const empty_candidate_obj = function (raw_data) {
const input_data = raw_data;
let first_vote = input_data[0];
const keys = Object.keys(first_vote);
keys.forEach(function (key) {
first_vote[key] = 0;
});
return first_vote;
};
and
// returns key from object value
const getKeyByValue = function (object, value) {
return Object.keys(object).find((key) => object[key] === value);
};
Problem
When inputting the above array into my function it returns
{
blue: 1,
red: 0,
purple: 0,
yellow: 2
}
=> Not as expected!
Do you know what I have done wrong?
Thanks for any replies :)

The issue was with this line:
const input_data = raw_data;
Copying an array of objects is problematic because copying the array doesn't automatically make new copies of the objects. It's just a new array with references to the original objects, so if you change an object in the new array you're changing the original object.
So you need to make new copies of the objects too:
const input_data = raw_data.map(obj => ({...obj}));
You can solve your problem slightly simpler with reduce.
const data = [{"blue":1,"red":3,"purple":2,"yellow":4},{"blue":2,"red":3,"purple":4,"yellow":1},{"blue":1,"red":2,"purple":4,"yellow":3},{"blue":3,"red":4,"purple":2,"yellow":1}];
// We initialise the `reduce` with a new `{}` object,
// which is updated with each iteration.
// `acc` is the accumulator, `c` is the current element
const result = data.reduce((acc, c) => {
// For each object in the array we grab the key and value
Object.entries(c).forEach(([ key, value ]) => {
// If our initial object doesn't already have a key
// that matches, create one and set it to zero
acc[key] = acc[key] || 0;
// And if the value is one, increase the value of that property
if (value === 1) acc[key]++;
});
// Make sure to return the initial object
// for the next iteration
return acc;
}, {});
console.log(result);

With respect to your code the problem is in this line of empty_candidate_obj funcation
let first_vote = input_data[0];
Update you code to send a copy
let first_vote = {...input_data[0]};
Object works on reference. so it's like your first object of your dataset values are updated to 0 as well with your empty object

You probably want to use a reducer something like this:
const results = [
{
"blue": 1,
"red": 3,
"purple": 2,
"yellow": 4
},
{
"blue": 2,
"red": 3,
"purple": 4,
"yellow": 1
},
{
"blue": 1,
"red": 2,
"purple": 4,
"yellow": 3
},
{
"blue": 3,
"red": 4,
"purple": 2,
"yellow": 1
}
];
const empty_obj = {
"blue": 0,
"red": 0,
"purple": 0,
"yellow": 0
}
result = results.reduce( reducer , empty_obj)
console.log( result )
function reducer( acc, val ) {
for( key in val )
if( val[key] === 1 ) acc[key]++;
return acc;
}

Try this function:
function candidateScore1Times(array) {
const newObj = {}
for (const score of array) {
Object.keys(score).forEach(elem => {
if (newObj[elem] != null) {
if (score[elem] == 1) {
newObj[elem]++
}
} else {
score[elem] == 1 ? newObj[elem] = 1 : newObj[elem] = 0
}
})
}
return newObj
}

Related

How to recursively check property types in an object

I'm writing a function that takes in an object (A), compares it with another object (B) and creates a new object (C). Both A and B objects are symmetrical (same amount of properties, same keys) but the type of their values can differ. Eg: A.amount can be '11' while B.amount is a number. The end result should be an object C that has the values from A with the types from B.
In the function I came up with, I'm looping through all properties in object A, checking the type of the properties in object B with a switch/case statement, applying type conversion (like .toString()) and adding the corrected property to object C.
I would like the code to be recursive but as soon as there are nested objects involved I do not know how to reach the properties in B nor create the equivalent in C.
Example of an A object:
const data = {
a: "15",
b: "foo",
c: false,
d: {
da: 99,
db: [{
dba: "1.2",
dbb: true
}]
}
}
Example of a B object:
const targetType = {
a: 27,
b: "foofoo",
c: true,
d: {
da: "bar",
db: [{
dba: 4,
dbb: false
}]
}
}
Example of a C object -values from A, types from B:
const finalObject = {
a: 15,
b: "foo",
c: false,
d: {
da: "99",
db: [{
dba: 1.2,
dbb: true
}]
}
}
My function:
//data = object A
//targetType = object B
export function typeConversion(data: any, targetType: any) {
const resultObj = {};
Object.keys(data).forEach((key) => {
switch (typeof targetType[key]) {
case 'string':
resultObj[key] = data[key].toString();
break;
case 'boolean':
resultObj[key] = data[key] ? 'Yes' : 'No';
break;
case 'number':
resultObj[key] = Number(data[key]);
break;
//here is where it gets tricky:
case 'object':
if (Array.isArray(data[key])) {
const dataArray: any = [];
data[key].forEach((o) => {
//all objs in the array have the same shape, hence comparing to [0]
dataArray.push(typeConversion(o, targetType[key][0]));
});
resultObj[key] = dataArray;
} else {
const dataObj = {};
Object.keys(data[key]).forEach((subkey) => {
dataObj[subkey] = typeConversion(subkey, targetType[key][subkey]);
});
resultObj[key] = dataObj;
}
break;
}
});
return resultObj;
}
console.log(typeConversion(data, targetType))
The moment there is a nested object and typeConversion is called recursively, it will fail to find the path to the property in object B. I could add an optional parameter to the function for the 'parent property', but that would only work for one level of depth.
If there is a way to make this recursive, it can't be by coding the path to targetType[like][this].
TypeScript Playground example
All ideas welcome, perhaps I'm approaching the whole thing wrong.
I made some changes in your function to recursively create the final object. Although this is not Typescript specific problem, I wonder what is the use case for this :)
const transform = (data, targetType) => {
let finalObject = {};
const keys = Object.keys(data);
for (const key of keys) {
// If key is Array [], recursively add each element in Array
if (data[key] instanceof Array) {
finalObject[key] = [];
for (let i = 0; i < data[key].length; i++) {
const res = transform(data[key][i], targetType[key][i]);
finalObject[key].push(res);
}
}
// If key is Object {}, recursively add Object keys
else if (data[key] instanceof Object) {
finalObject[key] = transform(data[key], targetType[key]);
}
// If key is Primitive, we can directly add key
else {
switch (typeof targetType[key]) {
case 'string':
finalObject[key] = data[key].toString();
break;
case 'boolean':
finalObject[key] = data[key] ? 'Yes' : 'No';
break;
case 'number':
finalObject[key] = Number(data[key]);
break;
}
}
}
return finalObject;
};
const data = {
a: "15",
b: "foo",
c: false,
d: {
da: 99,
db: [{
dba: "1.2",
dbb: true
}]
}
}
const targetType = {
a: 27,
b: "foofoo",
c: true,
d: {
da: "bar",
db: [{
dba: 4,
dbb: false
}]
}
}
const finalObject = transform(data, targetType);
console.log('Final Object : ', finalObject);

How to split objects

How to split objects?
I want to split Object.
Such as one object per group or two object per group.
obj1: {
1: 1,
2: 2,
3: 3,
},
Split objects into groups(convert obj1 to obj2)
obj2: {
0: {
1: 1,
},
1: {
2: 2,
},
},
2: {
3: 3,
},
},
}
You could get the entries, chunk them by getting a new object and assign the array to an object.
function chunk(object, size) {
const
entries = Object.entries(object),
chunks = [];
let i = 0;
while (i < entries.length)
chunks.push(Object.fromEntries(entries.slice(i, i += size)));
return Object.assign({}, chunks);
}
console.log(chunk({ 1: 1, 2: 2, 3: 3 }, 1));
console.log(chunk({ 1: 1, 2: 2, 3: 3 }, 2));
.as-console-wrapper { max-height: 100% !important; top: 0; }
let obj1 = {
1 : 1 ,
2 : 2 ,
3 : 3
}
let index = 0 ;
let obj2 = {}
for(let key in obj1 ) {
let temp = {
}
temp[key] = obj1[key]
let prev = obj2 ;
obj2[index] = {
...temp
}
index++;
}
Try this code .
I hope this solves your issue :)

I want to set rank to array value?

I tried to create new array object from array , set rank according to its value . If value is the same, set the same rank and if next value is different set rank by skipping same rank length .
Expected result is
[
{
"rank": 1,
"data": 45
},
{
"rank": 2,
"data": 33
},
{
"rank": 3,
"data": 8
},
{
"rank": 4,
"data": 5
},
{
"rank": 4,
"data": 5
},
{
"rank": 6,
"data": 2
}
]
var data = [8,5,2,33,5,45];
var rankList = [];
var uniqueList = [];
var rank = 0;
var sameRank = 0;
data.sort(function(a,b) { return b - a; });
for(var i in data) {
if(uniqueList.includes(data[i])) {
rank++;
rankList.push({rank: sameRank, data: data[i]});
continue;
}
rank++;
sameRank++;
rankList.push({rank: rank, data: data[i]});
}
console.log(rankList);
Once you've sorted the array, create another array of objects with .map, keeping track of the last rank and data used. If the new data is the same, use the same rank (the one taken from a prior iteration) - otherwise, use the current iteration index plus 1:
const data = [8, 5, 2, 33, 5, 45];
data.sort((a, b) => b - a);
let lastRank = 0;
let lastData;
const output = data.map((data, i) => {
const objToReturn = { data };
objToReturn.rank = data === lastData ? lastRank : i + 1;
lastData = data;
lastRank = objToReturn.rank;
return objToReturn;
});
console.log(output);

How to display nested key of object from left to right?

I have an object
var tree = {
17:{
1:{
3:{},
4:{}
},
2:{
5:{},
6:{}
}
}
};
How to display the keys in this order 17, 1, 2, 3,4, 5,6 ?
i have tried this function:
var arr = [] ;
var arrObject = [] ;
function printValues(obj) {
for (var key in obj) {
arr.push(key);
if (typeof obj[key] === "object") {
arrObject.push(obj[key]);
printValues(obj[key]);
}
}
}
printValues(tree);
the result is 17,1,3,42,5,6 .
and i need 17, 1, 2, 3,4, 5,6
The following uses a recursive function to extract all the nested keys associated with their depth in the object. It then sorts the results by depth.
Note that the approach may change the order of the keys at a given depth. To demonstrate this, I change the 3 in your example to 99. The returned results include "4", "99", not "99", "4". However, as pointed out in the comment by #deceze, the order of properties in an object is undefined anyway, so changing the order within a given object depth shouldn't (hopefully) be a problem. (If it is a problem then you need a different approach, e.g. using an array instead of an object.)
Note also that the code below returns an array of strings. This would be especially appropriate if some of your keys are explicitly strings, e.g. using letters. If you really want the results to be numerical, just add a + to the map command, i.e. ... .map(elmt => +elmt[1]).
const getKeysOrderedByDepth = obj => {
let arr = [], level = 0;
const getKeys = obj => {
level += 1;
for (let key in obj) {
arr.push([level, key]);
const val = obj[key];
getKeys(obj[key]);
}
level -= 1;
};
getKeys(obj);
return arr.sort((a, b) => a[0] - b[0]).map(elmt => elmt[1]);
};
let tree = {
17: {
1: {
99: {},
4: {}
},
2: {
5: {},
6: {}
}
}
};
console.log(getKeysOrderedByDepth(tree));
This is works for me(changed):
const tree = {
first:{
1:{
3:{
7: 7,
8: 8,
},
4:{
9: 9,
10: 10,
}
},
2:{
5:{
20: {
30: 30,
},
21: {
31: 31,
}
},
6:{
22: 22,
}
},
},
};
const keys = [];
/**
* Handle merged values
* #param values
*/
function handleData(values) {
let subValues = {};
for (let key in values) {
keys.push(key);
Object.assign(subValues, handle(values[key]));
}
// If have values, handle them
if (Object.keys(subValues).length) handleData(subValues);
}
/**
* Handle one value
* #param value
* #returns {{}}
*/
function handle(value) {
if (Object.keys(value).length) {
return Object.assign({}, value);
}
}
// Handle our data
handleData(tree);
console.log(keys); // ['first',1,2,3,4,5,6,7,8,9,10,20,21,22,30,31]

Sorting JavaScript Object by key value Recursively

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

Categories

Resources