Search code examples
javascriptreactjsarraysjsonloops

filter array of objects and get properties into a new array dynamically


I have input as follows

[
    {
        "metadata": { "id": 1071, "name": "First" },
        "languages": [
            { "name": "Alpha", "details": [ { "city": "usa", "count": 33 } ] },
            { "name": "Beta", "details": [ { "city": "japan", "count": 3 } ] },
        ]
    },
    {
        "metadata": { "id": 1068, "name": "Second" },
        "languages": [
            { "name": "Alpha", "details": [ { "city": "japan", "count": 10 }, { "city": "usa", "count": 20 } ] },
            { "name": "Beta", "details": [ { "city": "japan", "count": 15 }, { "city": "usa", "count": 25 } ] },
        ]
    },
]

So here is my scenario where i have to filter by language Alpha. So i have to go through first object and filter languages by Alpha and then get usa and japan count into a new array. Similarly loop through 2nd object and extract that count.

At the end, i should get below sequence of counts in the new array

output = { japan: [0, 10], usa: [33, 20] } //for filter by Alpha
output = { japan: [3, 15], usa: [0, 25] } //for filter by Beta

I am able to achieve the above output by using the below code.

const filterBy = (data, name) => {
    return data.reduce((p, c) => {
        for (let langObj of c.languages) {
            if (langObj.name === name) {
                const groupedOnCityCount = langObj.details.reduce((x, {city,count}) => ({ ...x, [city]: count }), {});
                p.usa.push(groupedOnCityCount?.usa || 0);
                p.japan.push(groupedOnCityCount?.japan || 0);
            }
        }
        return p;
    }, { japan: [], usa: [] });
}

const A = filterBy(data, 'Alpha');
const B = filterBy(data, 'Beta');

Everything uptil here works fine. The problem arises if the input is dynamic. i.e. we dont know if the output will only include japan and usa (its not static)

Here is the dynamic input i am expecting now instead of earlier static input.

[
    {
        "metadata": { "id": 1071, "name": "First" },
        "languages": [
            { "name": "Alpha", "details": [ { "city": "usa", "count": 33 }, { "city": "korea", "count": 20 } ] },
            { "name": "Beta", "details": [ { "city": "japan", "count": 3 } ] },
        ]
    },
    {
        "metadata": { "id": 1068, "name": "Second" },
        "languages": [
            { "name": "Alpha", "details": [] },
            { "name": "Beta", "details": [ { "city": "japan", "count": 15 }, { "city": "russia", "count": 77 } ] },
        ]
    },
]

As per above input, some languages may contain additional cities which doesnot exist in other objects. Also some may have empty details:[]

For above input, i want desired output

output = { usa: [33, 0] korea: [20, 0 ] } //for filter by Alpha
output = { japan: [3, 15], russia: [0, 77] } //for filter by Beta

Can someone let me know how to dynamically get this.


Solution

  • You could reduce the array and get the information from nested items.

    const
        filter = (array, value) => array.reduce((r, { languages }, i, a) => {
            languages.some(({ name, details }) => {
                if (name === value) {
                    details.forEach(({ city, count }) => {
                        r[city] ??= Array(a.length).fill(0);
                        r[city][i] = count;
                    })
                    return true;
                }
            });    
            return r;
        }, {}),
        data = [{ metadata: { id: 1071, name: "First" }, languages: [{ name: "Alpha", details: [{ city: "usa", count: 33 }, { city: "korea", count: 20 }] }, { name: "Beta", details: [{ city: "japan", count: 3 }] }] }, { metadata: { id: 1068, name: "Second" }, languages: [{ name: "Alpha", details: [] }, { name: "Beta", details: [{ city: "japan", count: 15 }, { city: "russia", count: 77 }] }] }],
        result1 = filter(data, 'Alpha'),
        result2 = filter(data, 'Beta');
    
    console.log(result1);
    console.log(result2);
    .as-console-wrapper { max-height: 100% !important; top: 0; }