Convert Array into Object with filtering and prioritizatoin and minimum loops

Max Zavodniuk :

Given an array

const array = [{
  typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, {
  typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
  typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}]

My ultimate goal is to get this:

{
  welcome: {
    landscape: ['Welcome'],
    portrait: ['Bonjour bonjour']
  }
}

To do that, I need to convert into object like {typeName: {orientation: value[]}}, like this:

// This is NOT what I want, it's just an intermediate form -- keep reading
{
  welcome: {
    landscape: ['Welcome', 'Bonjour'],
    portrait: ['Bonjour bonjour']
  }
}

But including prioritization: if languageId=1 present on array, then ignore rest values for specific typeName, orientation..In the sample above should be only ['Welcome'] since it's languageId=1, so 'Bonjour' can be ignored, though if languageId=1 is missing, then any value can be added (welcome.portrait).

With convering I haven't faced with any problems..Doing it thought .reduce() method

array.reduce((prev, current) => ({
    ...prev,
    [current.typeName]: {
        ...prev[current.typeName],
        [current.orientation]: [
            ...(((prev[current.typeName] || {})[current.orientation]) || []),
            current.value
        ]
    }
  }), {});

but prioritization I can do only with filtering that also does loop inside it..No problem so far, but if array will be pretty huge - performance will suffer

    const array = [{
      typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
    }, {
      typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
    }, {
      typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
    }]

    const result =  array
      .filter((item) => {
          return item.languageId === 1 ||
          !array.some((innerItem) => ( //Inner loop that I want to avoid
              innerItem.typeName === item.typeName &&
              innerItem.orientation === item.orientation &&
              innerItem.languageId === 1
          ))
      })
      .reduce((prev, current) => ({
        ...prev,
        [current.typeName]: {
            ...prev[current.typeName],
            [current.orientation]: [
                ...(((prev[current.typeName] || {})[current.orientation]) || []),
                current.value
            ]
        }
      }), {});
      
console.log(result)

So the question is what's the best approach to avoid inner loop?

T.J. Crowder :

You can use a Set to keep track of whether you've seen a languageId === 1 for a particular typeName and orientation. Set is required to have sublinear performance:

Set objects must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection.

So a Set will outperform an inner loop. (You could also use an object if your keys are strings, which they are in my example below. If you do, create it with Object.create(null) so it doesn't inherit from Object.prototype.)

Then just a single straightforward loop rather than reduce:

const array = [{
  typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Welcome'
}, { typeName: 'welcome', orientation: 'landscape', languageId: 1, value: 'Bon dia'
}, {
  typeName: 'welcome', orientation: 'landscape', languageId: 2, value: 'Bonjour'
}, {
  typeName: 'welcome', orientation: 'portrait', languageId: 2, value: 'Bonjour bonjour'
}]

const sawPriority = new Set();
const result = {};
for (const {typeName, orientation, languageId, value} of array) {
    const key = `${typeName}-${orientation}`;
    const isPriority = languageId === 1;
    let entry = result[typeName];
    if (!entry) {
        // No entry yet, easy peasy
        entry = result[typeName] = {[orientation]: [value]};
        if (isPriority) {
            sawPriority.add(key);
        }
    } else {
        const hasPriority = sawPriority.has(key);
        if (hasPriority === isPriority) {
            // Either the first for a new orientation, or a subsequent entry that
            // matches the priority
            const inner = entry[orientation];
            if (inner) {
                // Subsequent, add
                inner.push(value);
            } else {
                // First for orientation, create
                entry[orientation] = [value];
            }
        } else if (isPriority) {
            // It's a new priority entry, overwrite
            entry[orientation] = [value];
            sawPriority.add(key);
        }
    }
}

console.log(result)

(In a comment, you mentioned that if there are multiple languageId === 1 entries, they should be built up in an array, so I've included a second one in the above to show that working.)


In the above, I'm comparing two boolean values like this:

if (hasPriority === isPriority) {

That works because I know that they're both actually booleans, not just truthy or falsy values, because isPriority is the result of a === comparison (guaranteed to be a boolean) and hasPriority is the result of calling has on Set (guaranteed to be a boolean).

If there were any question about whether one of them might be just a truthy or falsy value rather than definitely true or false, I'd've used ! on them to ensure they were booleans:

if (!hasPriority === !isPriority) {

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=12375&siteId=1