Reputation: 3082
So I have two arrays:
const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ]
What's the quickest way to produce a new array which is the difference between these two? In old school JavaScript, you'd have to do a for loop inside another for loop, I think...
Eg:
const availableLanguages = [ 'ES', 'DE' ]
Upvotes: 30
Views: 27373
Reputation:
Inspired by @Ori Drori's excellent answer, here is a pure set-based solution.
const all = new Set(allLanguages);
const used = new Set(usedLanguages.map(({lang}) => lang));
const availableLanguages = setDifference(all, used);
where
const setDifference = (a, b) => new Set([...a].filter(x => !b.has(x)));
availableLanguages
will be a set, so to work with it as an array you'll need to do Array.from
or [...map]
on it.
If one wanted to get all functional, then
const not = fn => x => !fn(x);
const isIn = set => x => set.has(x);
Now write
const setDifference = (a, b) => new Set([...a].filter(not(isIn(b))));
which some might consider more semantic or readable.
However, these solutions are somewhat unsatisfying and could be suboptimal. Even though Set#has
is O(1)
, compared to O(n)
for find
or some
, the overall performance is still O(n)
, since we have to iterate through all the elements of a
. It would be better to delete the elements from b
from a
, as was suggested in another answer. This would be
const setDifference = (a, b) => {
const result = new Set(a);
b.forEach(x => result.delete(x));
return result;
}
We can't use reduce
since that is not available on sets, and we don't want to have to convert the set into an array to use it. But we can use forEach
, which is available on sets. This alternative would be preferable if a
is larger and b
is smaller.
Most likely some future version of JS will have this built-in, allowing you to just say
const availableLanguages = all.difference(used)
Finally, if we are interested in exploring more ES6 features, we could write this as a generator which generates non-duplicate values, as in
function* difference(array, excludes) {
for (let x of array)
if (!excludes.includes(x)) yield x;
}
Now we can write
console.log([...difference(allLanguages, usedLanguages)]);
This solution might be recommended if there was a long list of languages, perhaps coming in one by one, and you wanted to get a stream of the non-used ones.
If one wanted O(1)
lookup into the list of exclusions without using sets, the classic approach is to pre-calculate a dictionary:
const dict = Object.assign({},
...usedLanguages.map(({lang}) => ({[lang]: true})));
const availableLanguages = allLanguages.filter(lang => lang in dict);
If that way of computing the dictionary is too arcane for you, then some people use reduce
:
const dict = usedLanguages.reduce((obj, {lang}) => {
obj[lang] = true;
return obj;
}, {});
Nina likes to write this using the comma operator as
const dict = usedLanguages.reduce((obj, {lang}) => (obj[lang] = true, obj), {});
which saves some curly braces.
Or, since JS still has for
loops :-):
const dict = {};
for (x of usedLanguages) {
dict[x.lang] = true;
}
Hey, you're the one who said you want to use ES6.
Upvotes: 15
Reputation: 192132
Iterate the usedLanguages
using Array#reduce
, with new Set(allLanguages)
as the start value. Delete the language from used
Set on every iteration. Spread the result into array. Complexity is O(n + m), where n is the length of the usedLanguages
array, and m the length of allLanguages
:
const allLanguages = [ 'ES', 'EN', 'DE' ];
const usedLanguages = [{ id: 1, lang: 'EN' }];
const result = [...usedLanguages.reduce((r, { lang }) => {
r.delete(lang);
return r;
}, new Set(allLanguages))];
console.log(result);
Upvotes: 4
Reputation: 122067
You can use filter()
and find()
to return filtered array.
const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ]
var result = allLanguages.filter(e => !usedLanguages.find(a => e == a.lang));
console.log(result)
You could also map()
second array and then use includes()
to filter out duplicates.
const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ].map(e => e.lang);
var result = allLanguages.filter(e => !usedLanguages.includes(e));
console.log(result)
Upvotes: 39
Reputation: 3872
You can use the following code:
availableLanguages = allLanguages.filter((lang1) => !usedLanguages.some((lang2) => lang2.lang === lang1))
The some
function is a lesser known relative to find
that is better suited for cases where you want to check if a condition is met by at least one element in the array.
Upvotes: 7