1
\$\begingroup\$

The data describes cycling activities (key1) and chunks of them (key2). In total there are around 1,000 key1 objects and arrays under key1 are 1-100 long.

  • Is there a simpler, more semantic, faster, or otherwise better way than to use two nested map in step 1?
  • I realise that step 3 is longer than it should be and there has to be a better way to do it. How can step 3 be improved?

// Original array
const arr = [
    {key1: [{key2: {id: 1, name: 'a'}}]},
    {key1: [{key2: {id: 2, name: 'b'}}]},
    {key1: [{key2: {id: 2, name: 'b'}}, {key2: {id: 3, name: 'c'}}]}
];
console.log(arr);

// Step 1: Extract meaningful information from original array
const arrOfArrOfObj = arr
    .map(category => category.key1
        .map(subCategory => (
                {
                    id: subCategory.key2.id,
                    name: subCategory.key2.name
                }
            )
        )
    );
console.log(arrOfArrOfObj);

// Step 2: Make the array one dimensional
const arrOfObj = [].concat(...arrOfArrOfObj);
console.log(arrOfObj);

// Step 3: Remove duplicates and count object occurrences.
let dedupedArrWithCount = [];
l = arrOfObj.length;
for (let i = 0; i < l; i++) {
    let objExists = false;
    for (let j = 0; j < dedupedArrWithCount.length; j++) {
        // Two objects are identical if their ids are identical.
        if (arrOfObj[i].id === dedupedArrWithCount[j].id) {
            objExists = true;
            dedupedArrWithCount[j].count += 1;
        }
    }
    if (!objExists) {
        dedupedArrWithCount.push({
            id: arrOfObj[i].id,
            name: arrOfObj[i].name,
            count: 1
        })
    }
}

console.log(dedupedArrWithCount);

\$\endgroup\$
  • 1
    \$\begingroup\$ what does this data describe? will there always be two levels or might there ever be 3 or more? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Oct 24 '18 at 21:42
  • \$\begingroup\$ The data describes cycling activities (key1) and chunks of them (key2). In total there are around 1,000 key1 objects and arrays under key1 are 1-100 long. \$\endgroup\$ – Jelefra Oct 24 '18 at 21:48
  • \$\begingroup\$ so does that mean that there will only ever be two levels of keys? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Oct 26 '18 at 20:11
  • \$\begingroup\$ Yes that's the case. \$\endgroup\$ – Jelefra Oct 27 '18 at 14:10
  • \$\begingroup\$ What do you meant by ` cycling activities (key1) and chunks of them (key2).` - Do you mean like 'push bike cycling'? Could you link to the terminology you are using? \$\endgroup\$ – dwjohnston May 22 at 7:17
0
\$\begingroup\$

Responding to your questions

“Is there a simpler, more semantic, faster, or otherwise better way than to use two nested map in step 1?”

While I like the benefits of functional approaches, they are often slower because of the extra function calls.

Steps 1 and 2 could be simplified by using two for...of loops (since features like const are also used). Also, instead of constructing a new object to return in the nested map callback, you can just return subCategory.key2 (though perhaps you simplified the data and the original data contains more properties that aren't needed in the end).

const arrOfObj = [];
for (let category of arr) {
    for (let subCategory of category.key1) {
        arrOfObj.push(subCategory.key2);
    }
}

This generally works faster, at least for the small dataset supplied - see this jsPerf test for comparison

“I realise that step 3 is longer than it should be and there has to be a better way to do it. How can step 3 be improved?”

I was thinking of using JSON.stringify() to make an array of counts in step 1 and then use JSON.parse() in step 3 for lookups in constant time, but apparently that was slower, possibly because of the original data set which only has 1 collision. Maybe for a larger dataset that would be faster. See this jsPerf for a comparison.

const arr = [
    {key1: [{key2: {id: 1, name: 'a'}}]},
    {key1: [{key2: {id: 2, name: 'b'}}]},
    {key1: [{key2: {id: 2, name: 'b'}}, {key2: {id: 3, name: 'c'}}]}
];
const counts = {};
for (let category of arr) {
  for (let subCategory of category.key1) {
    const countKey = JSON.stringify(subCategory.key2)
    counts[countKey] = (counts[countKey] || 0) + 1;
  }
}
const dedupedArrWithCount = [];
for (let key in counts) {
  const obj = JSON.parse(key);
  dedupedArrWithCount.push(Object.assign(obj, {
    count: counts[key]
  }));
}
console.log(dedupedArrWithCount);


Other feedback

The variable l is declared without any keyword:

let dedupedArrWithCount = [];
l = arrOfObj.length;

Unless this code is wrapped in an IIFE, l becomes a global variable, which can lead to unintentional consequences if that name is used later. Read more about this topic in this post about global variables. It is advisable to use const (or let if there was a need to re-assign it).

In that same vein, dedupedArrWithCount could be declared with the const keyword, since it is never re-assigned, just mutated using the push method.


One other thing I noticed is that

const arrOfObj = [].concat(...arrOfArrOfObj);

Is equivalent to

const flatArr = arrOfArrOfObj.flat();

Using .flat() appears to run faster in Firefox and Safari but not Chrome, at least when I ran this jsPerf.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.