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?
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) {