Recursively group by key values (groups within groups) - javascript

I would like to group by multiple keys such that a call like this:
groupBy(winners, ['country', 'athlete'])
On the following data:
[
{ athlete: "Michael Phelps", age: 19, country: "United States" },
{ athlete: "Michael Phelps", age: 27, country: "United States" },
{ athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" },
{ athlete: "Allison Schmitt", age: 22, country: "United States" },
]
Would produce (nested in order of key):
{
'United States': {
'Michael Phelps': [
{ athlete: "Michael Phelps", age: 19, country: "United States" },
{ athlete: "Michael Phelps", age: 27, country: "United States" }
],
'Allison Schmitt': [
{ athlete: "Allison Schmitt", age: 22 country: "United States" }
]
},
'Zimbabwe': {
'Kirsty Coventry': [
{ athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" }
]
}
}
Grouping by one key is easy, but I'm stuck on getting it to recursively group each group with the next key. This just groups by each key all in one level:
const get = (obj: Record<string, any>, k: string) =>
k.split(".").reduce((o, i) => (o ? o[i] : o), obj);
type GetValue<Item> = (item: Item) => string;
function groupBy<Item>(items: Item[], keys: (string | GetValue<Item>)[]) {
return keys.reduce(
(acc, key) => {
return items.reduce((accc, item) => {
const value =
typeof key === "function" ? key(item) : get(item, key);
(accc[value] = accc[value] || []).push(item);
return accc;
}, acc);
},
{} as Record<string, any>
);
}
const r = groupBy<Athlete>(winners.slice(0, 50), [
athlete => athlete.country,
"athlete"
]);
Here is a runnable example: https://codesandbox.io/s/groupby-ebgly?file=/src/index.ts:244-929
Apologies the extra complexity is around making it easy to specify keys using dot notation or a function for something even more complex such as a value in an array.
Thanks

You could reduce the array and reduce the keys and take for the last key an array fro pushing the object.
const
groupBy = (array, keys) => array.reduce((r, o) => {
keys
.reduce((q, k, i, { length }) => q[o[k]] = q[o[k]] || (i + 1 === length ? [] : {}), r)
.push(o);
return r;
}, {}),
winners = [{ athlete: "Michael Phelps", age: 19, country: "United States" }, { athlete: "Michael Phelps", age: 27, country: "United States" }, { athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" }, { athlete: "Allison Schmitt", age: 22, country: "United States" }];
console.log(groupBy(winners, ['country', 'athlete']));
.as-console-wrapper { max-height: 100% !important; top: 0; }

You can do recursion on groupBy by reducing the keys at each step.
Something like this:
const winners = [{
athlete: "Michael Phelps",
age: 19,
country: "United States"
},
{
athlete: "Michael Phelps",
age: 27,
country: "United States"
},
{
athlete: "Kirsty Coventry",
age: 24,
country: "Zimbabwe"
},
{
athlete: "Allison Schmitt",
age: 22,
country: "United States"
},
]
const get = (obj, k) => k.split(".").reduce((o, i) => (o ? o[i] : o), obj);
function groupBy(items, keys) {
const key = keys[0]
const res = items.reduce((accc, item) => {
const k =
typeof key === "function" ? key(item) : get(item, key);
if (typeof k === "string") {
(accc[k] = accc[k] || []).push(item);
}
return accc;
}, {});
if (keys.length - 1 > 0)
return Object.fromEntries(Object.entries(res).map(([key, val]) => [key, groupBy(val, keys.slice(1))]))
else
return res // recursion base
}
const r = groupBy(winners, ['country', 'athlete'])
console.log(r)

Related

Find the difference between two complex objects containing array of objects

let baseObj = {
place: {city: 'Bangalore', pin: 123456},
office: [
{ name: 'Tom', age: 22, salutation: { title: 'Mr'}},
{ name: 'John', age: 31, salutation: { title: 'Mr'}}
]
}
let updatedObj = {
place: {city: 'Bangalore', pin: 99999},
office: [
{ name: 'Tom', age: 22, salutation: { title: 'Mr'}},
{ name: 'Peter', age: 16, salutation: { title: 'Mr'}},
{ name: 'John', age: 31, salutation: { title: 'Mr'}}
]
}
expected result = {
place: {city: 'Bangalore', pin: 99999},
office: [
{ name: 'Peter', age: 16, salutation: { title: 'Mr'}}
]
}
Note: comparison can be done by finding the properties of object and values but no comparison should be done hardcoding the properties
tried comparing the object but when we have an array of object i.e office, comparing with index(i.e 0,1) doesn't help as the array might not be sorted so couldn't proceed much
have tried the below code but it fails to get the desired output if the objects in an array are in different sequence as compared to the other array
ex. office1: [
{ name: 'Tom', age: 22, salutation: { title: 'Mr'}},
{ name: 'John', age: 31, salutation: { title: 'Mr'}}
]
office2: [
{ name: 'Tom', age: 22, salutation: { title: 'Mr'}},
{ name: 'Peter', age: 16, salutation: { title: 'Mr'}},
{ name: 'John', age: 31, salutation: { title: 'Mr'}}
]
function findDiff(obj1, obj2) {
var diffObj = Array.isArray(obj2) ? [] : {}
Object.getOwnPropertyNames(obj2).forEach(function(prop) {
if(prop !=='lenght' ){
if (typeof obj2[prop] === 'object') {
diffObj[prop] = obj1[prop]== undefined? obj2[prop]: findDiff(obj1[prop], obj2[prop])
if (Array.isArray(diffObj[prop]) && Object.getOwnPropertyNames(diffObj[prop]).length === 1 || Object.getOwnPropertyNames(diffObj[prop]).length === 0) {
delete diffObj[prop]
}
}} else if(prop !=='lenght') {
if(obj1[prop] !== obj2[prop]){
diffObj[prop] = obj2[prop]
}
}
});
return diffObj
}
This compare function seems to achieve exactly what you want :
const baseObj = {"grade":"A","queue":"1","myCollections":{"myCollection":[{"commonCollection":[{"winterCollection":[{"name":"ICE","isChilled":"true"}]}]}]},"remarks":{"remark":[{"name":"GOOD","category":"A","text":{"value":"Very Good"},"indicator":"good"}]}}
const updatedObj = {"grade":"A","queue":"1","myCollections":{"myCollection":[{"commonCollection":[{"winterCollection":[{"name":"ICE","isChilled":"true"},{"code":"SNOW","isChilled":"true"}]}]}]},"remarks":{"remark":[{"name":"GOOD","category":"A","text":{"value":"Very Good"},"indicator":"good"},{"name":"BEST","text":{"value":"Test"},"category":"O","indicator":"outstanding"}]}}
const compare = (a, b, allObj = true) => {
if (typeof a !== 'object') return a === b ? null : b
if (Array.isArray(a)) {
const arr = []
b.forEach(e => {
if (a.every(el => compare(el, e, true) !== null))
arr.push(e)
});
return arr.length === 0 ? null : arr
} else {
const keys = Object.keys(b) // assuming a and b have the same properties
const obj = allObj ? b : {}
let changed = false
keys.map(key => {
const compared = compare(a[key], b[key], true)
if (compared) {
obj[key] = compared
changed = true
}
})
return changed ? obj : null
}
}
const expectedResult = {"grade":"A","queue":"1","myCollections":{"myCollection":[{"commonCollection":[{"winterCollection":[{"code":"SNOW","isChilled":"true"}]}]}]},"remarks":{"remark":[{"name":"BEST","text":{"value":"Test"},"category":"O","indicator":"outstanding"}]}}
const result = compare(baseObj, updatedObj)
console.log(result)
console.log(expectedResult)
console.log(JSON.stringify(result) === JSON.stringify(expectedResult))
PS: I compared each pair but it is O(n^2). The best way is if you had an id property on every of array children.

Filter duplicate records and push into array with additional condition using js

I wan to filter the list of objects where city and state are not same, and then create a list of cities out of the filtered objects where temperature is less than 40.
but condition is both sate and city should not be same
let arr = [
{
city:"chennai",
state: "tamilnadu",
temp: 44
},
{
city:"coimbator",
state: "tamilnadu",
temp: 39
},
{
city:"mumbai",
state: "maharashtra",
temp: 32
},
{
city:"delhi",
state: "delhi",
temp: 24
},
{
city:"kolkata",
state: "west bengal",
temp: 28
}
];
Javascript code:
const uniqueStateCity = [];
const unique = arr.filter(element => {
const isDuplicate = uniqueStateCity.includes(element.city);
if (!isDuplicate) {
if(element.temp < 40)
{
uniqueStateCity.push(element.city );
return true;
}
}
return false;
});
console.log(unique );
You should try this in your code!
just put condition in the result statement with arrow function and you are good to go.
let arr = [
{
city: "chennai",
state: "tamilnadu",
temp: 44,
},
{
city: "coimbator",
state: "tamilnadu",
temp: 39,
},
{
city: "mumbai",
state: "maharashtra",
temp: 32,
},
{
city: "delhi",
state: "delhi",
temp: 24,
},
{
city: "kolkata",
state: "west bengal",
temp: 28,
},
];
const result = arr.filter((data) => data.temp < 40 && data.state !== data.city);
const data = result.map((x) => x.city);
console.log(data);
This checks for uniqueness of city and state values and also handles your additional conditions. It will not add values which have the same city and the same state value as any other object in the array.
const arr = [
{
city: "chennai",
state: "tamilnadu",
temp: 44,
},
{
city: "coimbator",
state: "tamilnadu",
temp: 39,
},
{
city: "mumbai",
state: "maharashtra",
temp: 32,
},
{
city: "mumbai",
state: "maharashtra",
temp: 32,
},
{
city: "delhi",
state: "delhi",
temp: 24,
},
{
city: "kolkata",
state: "west bengal",
temp: 28,
}
];
const result = arr.reduce((all, cur) => {
if (
all.findIndex((c) => c.city === cur.city && c.state === cur.state) < 0 &&
cur.state !== cur.city &&
cur.temp < 40
) {
all.push(cur);
}
return all;
}, []);
console.log(result);
let arr = [
{
city: "chennai",
state: "tamilnadu",
temp: 44,
},
{
city: "coimbator",
state: "tamilnadu",
temp: 39,
},
{
city: "mumbai",
state: "maharashtra",
temp: 32,
},
{
city: "mumbai",
state: "maharashtra",
temp: 32,
},
{
city: "delhi",
state: "delhi",
temp: 24,
},
{
city: "kolkata",
state: "west bengal",
temp: 28,
},
];
const result = [
...new Set(
arr.filter((data) => data.temp < 40 && data.state !== data.city).map((data) => data.city)
)
];
console.log(result);
Since you want to filter out the objects having same names for city and state and with temperature greater than 40.
Also if your original arr has duplicate objects, and you want an array of unique objects. You can use:
const result = arr.filter(data => data.temp < 40 && data.state !== data.city);
for (let i = 0; i < result.length; ++i) {
for (let j = 0; j < result.length; ++j) {
if ( i !== j &&
result[i].city === result[j].city &&
result[i].state === result[j].state &&
result[i].temp === result[j].temp
) {
result.splice(j, 1);
}
}
}

Format array of objects

I have this array of objects (data). These are the first to indexes:
0: Object
granularity_time: "total"
granularity_geo: "nation"
location_code: "norge"
border: "2020"
age: "90+"
sex: "female"
year: "1900"
week: "1"
yrwk: "1900-01"
season: "1899/1900"
x: "24"
date: "1900-01-01"
n: "219"
date_of_publishing: "2022-01-12"
tag_outcome: "death"
1: Object
granularity_time: "total"
granularity_geo: "nation"
location_code: "norge"
border: "2020"
age: "90+"
sex: "male"
year: "1900"
week: "1"
yrwk: "1900-01"
season: "1899/1900"
x: "24"
date: "1900-01-01"
n: "127"
date_of_publishing: "2022-01-12"
tag_outcome: "death"
Its statistics where men and woman in the same age has its own object. To index 0 is for woman age 90+, index 1 for men age 90+. Index 2 is for woman 80+, index 3 men 80+ etc.
I want to format the data so each index holds two objects, for men and woman in the same age.
Something like this:
const newData = [
{
woman: data[0],
men: data[1]
},
{
woman: data[2],
men: data[3]
},
{
woman: data[4],
men: data[5]
},
{
woman: data[6],
men: data[7]
},
{
woman: data[8],
men: data[9]
},
{
woman: data[10],
men: data[11]
},
{
woman: data[12],
men: data[13]
}
];
Ive tried to make a function that iterates over the data, but each entry ends up undefined:
const formatData = (data) => {
const newData = [];
data?.map((item, i) => {
i % 2 === 0 ? newData.push({ woman: item[i], men: item[i++] }) : null;
});
return newData;
};
What am I missing here?
Since the output is half of the input, it's really more of a reduce()...
const data = [{ gender: "m", name: "sam" }, { gender: "f", name: "sally" }, { gender: "m", name: "allen" }, { gender: "f", name: "abby" }, { gender: "m", name: "jim" }, { gender: "f", name: "jill" }];
const newData = data.reduce((acc, obj, i) => {
i % 2 ? acc[acc.length-1].woman = obj : acc.push({ man: obj })
return acc;
}, []);
console.log(newData)
You could use Array.from() and Array.map() to create the desired output from your original data array.
The length of the new array will be data.length / 2, and each alternate item will be assigned to either the men or women property of each element in the output array.
const template = { year: 1900, location_code: 'norge' };
// Some array of input data...
const data = Array.from({length: 14}, (v,k) => ({...template, id: k, gender: ( k % 2 === 0 ? 'female':'male' )}));
const result = Array.from( { length: data.length / 2 }, (v, idx) => {
return { women: data[2*idx], men: data[2*idx + 1], };
})
console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }
This actually works, but im not sure if its best practice since im just using the map to get i..
const formatData = (data) => {
const newData = [];
data?.map((item, i) => {
i % 2 === 0
? newData.push({ woman: data[i], men: data[i + 1] })
: null;
});
return newData;
};

Group an array of object having nested level

I have a tabular mode array object as shown below upto n level. it can be any parent children's records
var records = [
{ country: "USA", state: "FLORIDA", city: "city1" },
{ country: "USA", state: "FLORIDA", city: "city2" },
{ country: "USA", state: "FLORIDA", city:"city3" },
{ country: "USA", state: "ALASKA" },
{ country: "USA", state: "ALBAMA" },
]
var columns = ["country","state","city"] // upto n column
I need to group in below format for nth level as there can be n level of relations, group records in below format
{
sequencer: 1, value: 'USA', loop: [
{ sequencer: 1, value: 'FLORIDA', loop: [
{ sequencer: 1, value: 'city1' },
{ sequencer: 2, value: 'city2' },
{ sequencer: 3, value: 'city3' },
], },
{ sequencer: 2, value: 'ALASKA' },
{ sequencer: 3, value: 'ALBAMA' },
],
}
Can someone write an recursive function to group for n level of column object.
Thanks
You could group the data by avoiding unwanted loop properties.
const
data = [{ country: "USA", state: "FLORIDA", city: "city1" }, { country: "USA", state: "FLORIDA", city: "city2" }, { country: "USA", state: "FLORIDA", city: "city3" }, { country: "USA", state: "ALASKA" }, { country: "USA", state: "ALBAMA" }],
keys = ['country', 'state', 'city'],
result = data.reduce((loop, o) => {
keys
.map(k => o[k])
.filter(Boolean)
.reduce((r, value) => {
let temp = (r.loop ??= []).find(q => q.value === value);
if (!temp) r.loop.push(temp = { sequence: r.loop.length + 1, value });
return temp;
}, { loop });
return loop;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I answered a similar question recently. You could write your transform using flatMap and recursion -
const transform = ([ name, ...more ], { value, loop = [{}] }, r = {}) =>
name === undefined
? [ r ]
: loop.flatMap(t => transform(more, t, { [name]: value, ...r }))
const records =
{sequencer:1,value:'USA',loop:[{sequencer:1,value:'FLORIDA',loop:[{sequencer:1,value:'city1'},{sequencer:2,value:'city2'},{sequencer:3,value:'city3'}]},{sequencer:2,value:'ALASKA'},{sequencer:3,value:'ALBAMA'}]}
const columns =
['country', 'state', 'city']
const result =
transform(["country", "state", "city"], records)
console.log(JSON.stringify(result, null, 2))
A function statement equivalent of the => expression above -
function transform
( [ name, ...more ]
, { value, loop = [{}] }
, r = {}
)
{ if (name === undefined)
return [ r ]
else
return loop.flatMap
( t =>
transform
( more
, t
, { [name]: value, ...r }
)
)
}
Read the original Q&A for more insight on this technique.

How to find the value inside an array with objects with the most occuring value (deep mode)?

Let's say I have an array with objects like this:
const persons = [
{
name: "Erik",
age: 45
},
{
name: "Bob",
age: 37
},
{
name: "Erik",
age: 28
},
{
name: "Jasper",
age: 29
},
{
name: "Erik",
age: 34
}
];
How do I find the value based on a key with the most occuring value?
In this example, when passing name as key that would be Erik.
Something like this:
const deepMode = (array, key) => {
// Perhaps something with .reduce?
}
And when called, it should return:
deepMode(persons, "name"); // Returns "Erik"
You could take a Map, count the ocurrences and reduce the key/value pairs for getting the max valaue. Return the key without iterating again.
const
deepMode = (array, key) => Array
.from(array.reduce((m, o) => m.set(o[key], (m.get(o[key]) || 0) + 1), new Map))
.reduce((a, b) => a[1] > b[1] ? a : b)[0],
persons = [{ name: "Erik", age: 45 }, { name: "Bob", age: 37 }, { name: "Erik", age: 28 }, { name: "Jasper", age: 29 }, { name: "Erik", age: 34 }];
console.log(deepMode(persons, 'name'));
you can count keys by adding them in object and checking if key exists in object,then increment value, if not then add key into object, after that with Object.keys get keys of object sort them and get first element which is most occurring
const persons = [
{
name: "Erik",
age: 45
},
{
name: "Bob",
age: 37
},
{
name: "Erik",
age: 28
},
{
name: "Jasper",
age: 29
},
{
name: "Erik",
age: 34
}
];
const deepMode = (array, key) => {
const obj = {};
array.forEach(v => {
if (obj[v[key]]) {
obj[v[key]] += 1;
} else {
obj[v[key]] = 1;
}
});
return Object.keys(obj).sort((a,b) => obj[b]-obj[a])[0];
}
console.log(deepMode(persons, 'name'));
You could reduce into a Map, find the max, then find and return it.
function findMostOccuringKeyValue(arr, key) {
const grouped = arr.reduce((a, b) => a.set(b[key], (a.get(b[key]) || 0) + 1), new Map);
const max = Math.max(...grouped.values());
return [...grouped].find(([k, v]) => v === max)[0];
}
console.log(findMostOccuringKeyValue(persons, 'name'));
<script>
const persons = [
{
name: "Erik",
age: 45
},
{
name: "Bob",
age: 37
},
{
name: "Erik",
age: 28
},
{
name: "Jasper",
age: 29
},
{
name: "Erik",
age: 34
}
];
</script>

Categories

Resources