1

I got two arrays countries and networks but want to have another array which I called newState with this data structure:

[
 {
     id: null,
     name: "",
     networks: [
         {
            id: null,
            name: "",
         },
     ],
 },
 ....
]

I come up with this solution. It is working but maybe someone know more elegant way of doing it.

const networks = [
    {
        id: 1,
        name: "royal warmth",
        country: "Netherlands"
    },
    {
        id: 2,
        name: "interested power",
        country: "United Kingdom"
    },
    {
        id: 3,
        name: "drunk prejudice NL",
        country: "Netherlands"
    },
    {
        id: 10,
        name: "small Media promotion FR",
        country: "France"
    },
    {
        id: 11,
        name: "experimental vat FR",
        country: "France"
    },
]

const countries = [
    {
        id: 122,
        name: "Afghanistan"
    },
    {
        id: 190,
        name: "France"
    },
    {
        id: 210,
        name: "Netherlands"
    },
    {
        id: 226,
        name: "United Kingdom"
    }
]

let newState = [];
countries.forEach((country) => {
    let newNetworks = [];

    networks.forEach((network) => {
        if (network.country === country.name) {
            newNetworks.push({
                id: network.id,
                name: network.name,
            })
        }
    });

    if (newNetworks.length !== 0) {
        newState.push({
            id: country.id,
            name: country.name,
            networks: newNetworks,
        })
    }
});

console.log(newState);
Yana Trifonova
  • 586
  • 7
  • 28
  • Your code is valid, you can see different variations of this in https://stackoverflow.com/questions/46849286/merge-two-array-of-objects-based-on-a-key that use more Array methods and less `.push` – Roi Aug 24 '21 at 09:44

3 Answers3

3

Your solution is clean, but slightly inefficient since it has to iterate the entire networks array for each country. You can avoid this by first making a Map of the networks array grouped by country, and then iterating the country array and assigning these groups to each relevant country.

const networks = [{ id: 1, name: "royal warmth", country: "Netherlands" }, { id: 2, name: "interested power", country: "United Kingdom" }, { id: 3, name: "drunk prejudice NL", country: "Netherlands" }, { id: 10, name: "small Media promotion FR", country: "France" }, { id: 11, name: "experimental vat FR", country: "France" },];

const countries = [{ id: 122, name: "Afghanistan" }, { id: 190, name: "France" }, { id: 210, name: "Netherlands" }, { id: 226, name: "United Kingdom" }];

const networkMap = new Map;
for (const { country, ...network } of networks) {
  networkMap.set(country, [...(networkMap.get(country) ?? []), network]);
}

const newState = [];
for (const country of countries) {
  if (networkMap.has(country.name)) {
    newState.push({
      ...country,
      networks: networkMap.get(country.name)
    });
  }
}

console.log(newState)
.as-console-wrapper { max-height: 100% !important; top: 0; }

Here using the nullish coalescing operator (??) for checking missing networks, replace it with logical OR (||) for compatibility if needed

pilchard
  • 12,414
  • 5
  • 11
  • 23
1

Yes you can do it in single iteration such as,

const newState = countries.map(country => (networks.filter(e => e.country == country.name).length > 0 && {
    ...country,
    networks: networks.filter(e => e.country == country.name)
})).filter(e => e !== false)
console.log(newState)
1

(N.b. pilchard's implementation is very similar to mine, but I think that the performance caveat is a good complement to that answer — so I'll post anyway.)

I think that your implementation is both simple and elegant, but here is an alternative implementation that replaces the quadratic scanning with an intermediate accumulator:

let acc = {};
countries.forEach((({name, id}) => acc[name] = {id, name, networks: []}))
networks.forEach(({id, name, country}) => acc[country].networks.push({id, name}));
let newState = Object.values(acc).filter(x => x.networks.length);

Caveat:

Don't be afraid of expressing business logic using nested loops, it is usually fast enough given every day data – and often pretty easy to read. Out of our four implementations, your initial one runs — by far — the fastest on my machine*. (Things change with data set size and runtime environment, so take these figures with a grain of salt.)

*) Node v14.5.0 on a 13" MacBook Pro (2017)

Yana's dataset:

  • Yana's x 4,828,934 ops/sec ±0.91% (89 runs sampled)
  • Folkol's x 1,861,775 ops/sec ±0.46% (95 runs sampled)
  • Muljeeb's x 396,587 ops/sec ±1.32% (89 runs sampled)
  • Pilchard's x 195,126 ops/sec ±0.93% (91 runs sampled)

Slightly larger dataset:

  • Folkol's x 1,920 ops/sec ±0.95% (93 runs sampled)
  • Pilchard's x 53.12 ops/sec ±1.38% (68 runs sampled)
  • Yana's x 21.48 ops/sec ±12.75% (46 runs sampled)
  • Muljeeb's x 16.06 ops/sec ±5.68% (44 runs sampled)

Here is the test code (using benchmark.js), if you want to reproduce the test results on your machine:

let {Suite} = require('benchmark')

const networks = [
    {
        id: 1,
        name: "royal warmth",
        country: "Netherlands"
    },
    {
        id: 2,
        name: "interested power",
        country: "United Kingdom"
    },
    {
        id: 3,
        name: "drunk prejudice NL",
        country: "Netherlands"
    },
    {
        id: 10,
        name: "small Media promotion FR",
        country: "France"
    },
    {
        id: 11,
        name: "experimental vat FR",
        country: "France"
    },
]

const countries = [
    {
        id: 122,
        name: "Afghanistan"
    },
    {
        id: 190,
        name: "France"
    },
    {
        id: 210,
        name: "Netherlands"
    },
    {
        id: 226,
        name: "United Kingdom"
    }
]

const num_countries = 195;
const networks_per_country = 100;
const many_countries = [...Array(num_countries).keys()].map(n => ({
    id: n,
    name: `country_${n}`
}))
const many_networks = [...Array(num_countries * networks_per_country).keys()].map(n => ({
    id: n,
    name: `network_${n}`,
    country: `country_${n % networks_per_country}`
}));

function merge_yana(countries, networks) {
    let newState = [];
    countries.forEach((country) => {
        let newNetworks = [];

        networks.forEach((network) => {
            if (network.country === country.name) {
                newNetworks.push({
                    id: network.id,
                    name: network.name,
                })
            }
        });

        if (newNetworks.length !== 0) {
            newState.push({
                id: country.id,
                name: country.name,
                networks: newNetworks,
            })
        }
    });
    return newState;
}

function merge_pilchard(countries, networks) {
    const networkMap = new Map;
    for (const {country, ...network} of networks) {
        networkMap.set(country, [...(networkMap.get(country) ?? []), network]);
    }

    const newState = [];
    for (const country of countries) {
        if (networkMap.has(country.name)) {
            newState.push({
                ...country,
                networks: networkMap.get(country.name)
            });
        }
    }
    return newState;
}

function merge_muljeeb(countries, networks) {
    const newState = countries.map(country => (networks.filter(e => e.country == country.name).length > 0 && {
        ...country,
        networks: networks.filter(e => e.country == country.name)
    })).filter(e => e !== false)
    return newState;
}

function merge_folkol(countries, networks) {
    let acc = {};
    countries.forEach((({name, id}) => acc[name] = {id, name, networks: []}))
    networks.forEach(({id, name, country}) => acc[country].networks.push({id, name}));
    return Object.values(acc).filter(x => x.networks.length);
}

function benchmark(name, countries, networks) {
    console.log(name);
    Suite(name)
        .add("Yana's", () => merge_yana(countries, networks))
        .add("Pilchard's", () => merge_pilchard(countries, networks))
        .add("Muljeeb's", () => merge_muljeeb(countries, networks))
        .add("Folkol's", () => merge_folkol(countries, networks))
        .on('cycle', ({target}) => console.log(` - ${target}`))
        .run();
}

benchmark("Yana's dataset:", countries, networks);
benchmark("Slightly larger dataset:", many_countries, many_networks);
folkol
  • 4,752
  • 22
  • 25
  • 1
    Wow! That's a great job! I upload real data for "countries" and "networks" and here is the result I get: _______ - Yana's x 15,077 ops/sec ±2.68% (83 runs sampled) _________________________________ _______ - Pilchard's x 13,595 ops/sec ±1.40% (87 runs sampled)_________________________________ _______ - Muljeeb's x 11,841 ops/sec ±1.13% (91 runs sampled)________________________________ _______ - Folkol's x 18,587 ops/sec ±0.87% (92 runs sampled) – Yana Trifonova Aug 24 '21 at 15:41
  • So, they are all equally fast(/slow) — just pick whichever you believe will be easiest to maintain/most clearly expresses what you want to say :) Just out of curiosity, what kind of networks are these? Social networks? – folkol Aug 25 '21 at 09:41
  • 1
    For the data set words were taken from this [website](http://creativityforyou.com/combomaker.html). Original data is digital agencies names, do not ask why they called networks in db. – Yana Trifonova Aug 25 '21 at 15:04