0

I have a several levels deep javscript object. All levels are objects except the final levels which are arrays I need to sort.

My code so far looks like this :

for (let group in objRes) {
    if (objRes.hasOwnProperty(group)) {
        for (let type in objRes[group]) {
            if (objRes[group].hasOwnProperty(type)) {
                for (let name in objRes[group][type]) {
                    if (objRes[group][type].hasOwnProperty(name)) {
                        for (let tenor in objRes[group][type][name]) {
                            if (objRes[group][type][name].hasOwnProperty(tenor)) {
                                objRes[group][type][name][tenor] = objRes[group][type][name][tenor].sort((x,y)=>x.date>y.date);
                            }
                        }
                    }
                }
            }
        }
    }
}

The levels (group,type,name,tenor) are all strings and the last level array members look like : {date:'2019-12-25',value:35}

So objRes looks like

{
group1:
    {type1:
        {name1:
            {tenor1:[{date:'2019-12-25',value:35},...],
         name2 :{...}
         }
    },
    {type2 :{...}},
group2:{...}
}

Is there a clever way to simplify this ?

You can assume that the number of levels is known or not.

Chapo
  • 2,563
  • 3
  • 30
  • 60
  • 2
    Please update your question with a representative example of the contents of `objRes`. – T.J. Crowder Nov 11 '19 at 10:05
  • If the number of levels is arbitrary, what condition tells you to stop and do the `sort`? What is it about the objects at that level? – T.J. Crowder Nov 11 '19 at 10:05
  • You can probably write a recursive function to check if it has lower levels – prgrm Nov 11 '19 at 10:06
  • The update doesn't provide a representative example of the contents of `objRes`, it's not at all clear where the ends of the objects are (your curly braces are unmatched). If you're not willing to put in effort on your question to ensure it's clear, with useful sample data, etc., why should people put in effort answering it? – T.J. Crowder Nov 11 '19 at 10:17

2 Answers2

4

You can use a recursive function for this. It's hard to give an exact example based on the information in the question, but for instance:

function process(obj) {
    // Loop through the values of the own properties of the object
    for (const value of Object.values(obj)) {
        // Is this the termination condition?
        if (Array.isArray(value)) { // <== A guess at the condition, adjust as needed
            // We've reached the bottom
            value.sort((x, y) => x.date.localeCompare(y.date)); // <== Note correction, you can't just return the result of `>`
        } else {
            // Not the termination, recurse
            process(value);
        }
    }
}

Live Example using a guess at your data:

function process(obj) {
    // Loop through the values of the own properties of the object
    for (const value of Object.values(obj)) {
        // Is this the termination condition?
        if (Array.isArray(value)) { // <== A guess at the condition, adjust as needed
            // We've reached the bottom
            value.sort((x, y) => x.date.localeCompare(y.date)); // <== Note correction, you can't just return the result of `>`
        } else {
            // Not the termination, recurse
            process(value);
        }
    }
}

const objRes = {
    group1: {
        type1: {
            name1: {
                tenor1: [
                    {date: '2019-12-23', value: 35},
                    {date: '2019-12-25', value: 32},
                    {date: '2019-12-24', value: 30},
                ]
            },
            name2 :[]
        },
        type2: {}
    },
    group2: {}
};
process(objRes);
console.log(JSON.stringify(objRes, null, 4));
.as-console-wrapper {
    max-height: 100% !important;
}

Some notes on that:

  1. You can avoid the for-in/hasOwnProperty combination by using Object.values.

  2. You can loop through those values with for-of.

  3. There has to be some termination condition telling the function when it's reached "the bottom." In that example I've used Array.isArray because you're sorting at the final level.

  4. Array.prototype.sort modifies the array directly, you don't need to use its return value.

  5. The function you pass to sort must return a negative number, 0, or a positive number, not a boolean (more here). Since your date values appear to be strings in yyyy-MM-dd form, you can use localeCompare to do that (since it happens that in that format, a lexicographic comparison is also a date comparison).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

This alternate to the answer from T.J. Crowder takes several of the decision points mentioned there and makes them explicit function calls.

const process = (test, transform, data) =>
  typeof data == "object"
    ? ( Array .isArray (data)
       ? (xs) => xs .map (([_, x]) => x)
       : Object .fromEntries
      ) ( Object .entries (data) .map (([k, v]) => 
        [k, test (k, v) ? transform (v) : process (test, transform, v)]
      ))
    : data  

It accepts two functions as well as your data object. The first one is to test whether you've hit a nested value you want to massage. So we could imagine something like (k, v) => k .startsWith ('tenor') or (k, v) => Array .isArray (v). The second function accepts the value at that entry and returns an updated value, perhaps (v) => v . sort((a, b) => a .date .localeCompare (b .date)).

(Note: Your sort call is problematic. Don't use .sort ((a, b) => a < b), which returns a boolean that is then coerced into a 0 or a 1, whereas a proper comparator should return -1 when a < b. If you can compare with <, then this should always work (a, b) => a < b ? -1 : a > b ? 1 : 0. I don't know if there's a more complete SO question on this than Sorting an array with .sort((a,b) => a>b) works. Why?.)

You can see it in action in this snippet:

const process = (test, transform, data) =>
  typeof data == "object"
    ? ( Array .isArray (data)
        ? (xs) => xs .map (([_, x]) => x)
        : Object .fromEntries
      ) ( Object .entries (data) .map (([k, v]) => 
        [k, test (k, v) ? transform (v) : process (test, transform, v)]
      ))
    : data    

const dateSort = (xs) => xs .slice (0) .sort ((a, b) => a .date .localeCompare (b .date))

const objRes = {
    group1: {
        type1: {
            name1: {
                tenor1: [
                    {date: '2019-12-23', value: 35},
                    {date: '2019-12-25', value: 32},
                    {date: '2019-12-24', value: 30},
                ]
            },
            name2: []
        },
        type2: {}
    },
    group2: {
        type3: [
            {date: '2020-01-03', value: 42},
            {date: '2019-01-01', value: 43},
            {date: '2019-01-02', value: 44},
        ]
    }
};

// This one only sorts the first group of dates
console .log (
  process (
    (k, v) => k .startsWith ('tenor'),
    dateSort,
    objRes
  )
)

// This one sorts both groups
console .log (
  process (
    (k, v) => Array .isArray (v),
    dateSort,
    objRes
  )
)

The code is a bit dense. The outermost condition applies our processing only if the data is an object, returning it intact if not, and doing the following processing if it is:

We first use Object.entries to turn our object into name-value pairs, then map over that object by testing each pair to see whether we've hit something we should transform and returning either the transformed value or the result of recursing on the value.

Perhaps the trickiest bit is this:

      ( Array .isArray (data)
        ? (xs) => xs .map (([_, x]) => x)
        : Object .fromEntries
      )

Here we just choose a function to apply to the converted pairs to turn it back into a whole. If it's an array, we skip the keys and return an array of the values. If it's not, we use Object.fromEntries to create an object of them. If Object.entries is not available in your environment, it's easy to shim.

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