Group an array of object having nested level - javascript

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.

Related

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;
};

Getting syntax error when trying to create dictionary of object

I am getting an array of objects from API like below :
[
{
jobId:100,
jobName: "Java",
cities:[
{
cityId:10,
name : "chicago"
},
{
statusId:11,
name : "miami"
}]
},
{
jobId:101,
jobName: "Python",
cities:[
{
cityId:11,
name : "california"
},
{
cityId:12,
name : "texas"
}]
}
]
I want to access a city name of particular job like below :
var jobId = 101;
var cityId = 12;
var texas = jobDict[jobId].cityDict[cityId] // output: texas
Code:
angular.forEach(response.data, function (data) {
if (jobsDict[data.jobId] == undefined) {
jobsDict[data.jobId] = [];
}
angular.forEach(data.cities, function (city) {
jobsDict[data.jobId].push({
cityDic[city.cityId]: city.name // syntax error here saying unexpected token
});
});
});
I am trying to create a dictionary of jobs and inside a job dictionary, a dictionary of cities.
Can someone please help me fix the issue and achieve the desired output?
Your jobsDict is an array and therefore uses 0-based indexing, not jobID keys.
Likewise for the nested cities.
let jobDict = [{
jobId: 100,
jobName: "Java",
cities: [{
cityId: 10,
name: "chicago"
}, {
statusId: 11,
name: "miami"
}]
}, {
jobId: 101,
jobName: "Python",
cities: [{
cityId: 11,
name: "california"
}, {
cityId: 12,
name: "texas"
}]
}];
let jobId = 101;
let cityId = 12;
let texas =
jobDict.find(job => job.jobId === jobId)
.cities.find(city => city.cityId === cityId)
.name;
console.log(texas);
Following up on your comments, if you're really concerned about performance (which you shouldn't be unless you have 1000's of entries), here's how you can transform the original structure to a 'dictionary' structure that uses the job & city IDs as keys:
let jobs = [{
jobId: 100,
jobName: "Java",
cities: [{
cityId: 10,
name: "chicago"
}, {
cityId: 11,
name: "miami"
}]
}, {
jobId: 101,
jobName: "Python",
cities: [{
cityId: 11,
name: "california"
}, {
cityId: 12,
name: "texas"
}]
}];
let jobsById = Object.fromEntries(jobs.map(({jobId, jobName, cities}) =>
[jobId, {jobName, cities: Object.fromEntries(cities.map(({cityId, name}) => [cityId, name]))}]));
// Equivalent using traditional for loops.
let jobsById2 = {};
for (job of jobs) {
let cities = {};
for (city of job.cities)
cities[city.cityId] = city.name;
jobsById2[job.jobId] = {jobName: job.jobName, cities};
}
console.log(jobsById[101].cities[12]);
console.log(jobsById2[101].cities[12]);
Maybe something like:
const jobArray = [{
jobId: 100,
jobName: "Java",
cities: [{
cityId: 10,
name: "chicago"
},
{
cityId: 11,
name: "miami"
}
]
},
{
jobId: 101,
jobName: "Python",
cities: [{
cityId: 11,
name: "california"
},
{
cityId: 12,
name: "texas"
}
]
}
];
function index_job_array_A(jobArray) {
return jobArray.reduce(
(acc, item) => Object.assign(acc, {
[item.jobId]: Object.assign({}, item, {
cities: item.cities.reduce(
(acc, item) => Object.assign(acc, { [item.cityId]: item }),
{}
)
})
}),
{}
);
};
function index_job_array_B(jobArray) {
let indexed_jobs = {};
jobArray.forEach(function(job) {
let indexed_cities = {};
job.cities.forEach(function(city) {
indexed_cities[city.cityId] = city;
});
indexed_jobs[job.jobId] = { ...job, cities: indexed_cities };
});
return indexed_jobs;
};
const jobDictA = index_job_array_A(jobArray);
const jobDictB = index_job_array_B(jobArray);
console.log(jobDictA['100'].cities['10']);
console.log(jobDictB['100'].cities['10']);

How to filter deeply nested json by multiple attributes with vue/javascript

I have some JSON with the following structure:
{
"root": {
"Europe": {
"children": [
{
"name": "Germany"
},
{
"name": "England",
"children": [
{
"name": "London",
"search_words": ["city", "capital"],
"children": [
{
"name": "Westminster",
"search_words": ["borough"]
}
]
},
{
"name": "Manchester",
"search_words": ["city"]
}
]
},
{
"name": "France",
"children": [
{
"name": "Paris",
"search_words": ["city", "capital"]
}
]
}
]
},
"North America": {
"children": [
{
"name": "Canada",
"children": [
{
"name": "Toronto"
},
{
"name": "Ottawa"
}
]
},
{
"name": "United States"
}
]
}
}
}
I want to filter the JSON based on a text search. I should be able to search by both the name and any search_words. The final problem is that the JSON can be arbitrarily deep so it needs to be able to search through all levels.
I also need to loop through and print out the JSON in HTML (using Vue) and for it to update based on the search. Currently unclear how I can do this without knowing how many levels deep the JSON will go?
Any help will be greatly appreciated!
I answered a similar question recently. I'm sharing it here because I think it provides a relevant foundation for this post. Before we begin though, we must first address the irregular shape of your input data -
const data2 =
{ name:"root"
, children:
Array.from
( Object.entries(data.root)
, ([ country, _ ]) =>
Object.assign({ name:country }, _)
)
}
console.log(JSON.stringify(data2, null, 2))
Now we can see data2 is a uniform { name, children: [ ... ]} shape -
{
"name": "root",
"children": [
{
"name": "Europe",
"children": [
{ "name": "Germany" },
{
"name": "England",
"children": [
{
"name": "London",
"search_words": [ "city", "capital" ],
"children": [
{
"name": "Westminster",
"search_words": [ "borough" ]
}
]
},
{
"name": "Manchester",
"search_words": [ "city" ]
}
]
},
{
"name": "France",
"children": [
{
"name": "Paris",
"search_words": [ "city", "capital" ]
}
]
}
]
},
{
"name": "North America",
"children": [
{
"name": "Canada",
"children": [
{ "name": "Toronto" },
{ "name": "Ottawa" }
]
},
{ "name": "United States" }
]
}
]
}
Now we write a generic depth-first traversal function, dft -
function* dft (t, path = [])
{ for (const _ of t.children ?? [])
yield* dft(_, [...path, t.name ])
yield [path, t]
}
Our dft function gives us a path to each element, e, in our input tree, t -
["root","Europe"]
{"name":"Germany"}
["root","Europe","England","London"]
{name:"Westminster", search_words:["borough"]}
["root","Europe","England"]
{name:"London", search_words:["city","capital"], children:[...]}
["root","Europe","England"]
{name:"Manchester", search_words:["city"]}
["root","Europe"]
{name:"England", children:[...]}
["root","Europe","France"]
{name:"Paris", search_words:["city","capital"]}
["root","Europe"]
{name:"France", children:[...]}
["root"]
{name:"Europe", children:[...]}
["root","North America","Canada"]
{name:"Toronto"}
Now that we know that path to each of the nodes, we can create an index which uses the path and any search_words to link back to the node -
const index = t =>
Array.from
( dft(t)
, ([path, e]) =>
[ [...path, e.name, ...e.search_words ?? [] ] // all words to link to e
, e // e
]
)
.reduce
( (m, [ words, e ]) =>
insertAll(m, words, e) // update the index using generic helper
, new Map
)
This depends on a generic helper insertAll -
const insertAll = (m, keys, value) =>
keys.reduce
( (m, k) =>
m.set(k, [ ...m.get(k) ?? [], value ])
, m
)
With index finished, we have a way to create a fast lookup for any search term -
const myIndex =
index(data2)
console.log(myIndex)
Map
{ "Europe" =>
[{"name":"Germany"},{"name":"Westminster",...},{"name":"London",...},{"name":"Manchester",...},{"name":"England"...},{"name":"Manchester",...}]},{"name":"Paris",...},{"name":"France"...},{"name":"Europe"...},{"name":"Manchester",...}]},{"name":"France"...}]}]
, "Germany" =>
[{"name":"Germany"}]
, "England" =>
[{"name":"Westminster",...},{"name":"London",...},{"name":"Manchester",...},{"name":"England"...},{"name":"Manchester",...}]}]
, "London" =>
[{"name":"Westminster",...},{"name":"London",...}]
, "Westminster" =>
[{"name":"Westminster",...}]
, "borough" =>
[{"name":"Westminster",...}]
, "city" =>
[{"name":"London",...},{"name":"Manchester",...},{"name":"Paris",...}]
, "capital" =>
[{"name":"London",...},{"name":"Paris",...}]
, "Manchester" =>
[{"name":"Manchester",...}]
, "France" =>
[{"name":"Paris",...},{"name":"France"...}]
, "Paris" =>
[{"name":"Paris",...}]
, "North America" =>
[{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...},{"name":"United States"},{"name":"North America"...},
{"name":"United States"}]}]
, "Canada" =>
[{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...}]
, "Toronto" =>
[{"name":"Toronto"}]
, "Ottawa" =>
[{"name":"Ottawa"}]
, "United States" =>
[{"name":"United States"}]
}
This should highlight the remaining inconsistencies in your data. For example, you have some nodes nested under city, capital, or borough. Also worth noting that we should probably use s.toLowerCase() on all of the index keys so that lookups can be case-insensitive. This is an exercise left for the reader.
Creating the index is easy and you only need to do it once -
const myIndex =
index(data2)
Your index can be reused for as many lookups as you need -
console.log(myIndex.get("Toronto") ?? [])
console.log(myIndex.get("France") ?? [])
console.log(myIndex.get("Paris") ?? [])
console.log(myIndex.get("Canada") ?? [])
console.log(myIndex.get("Zorp") ?? [])
[{"name":"Toronto"}]
[{"name":"Paris",...},{"name":"France"...}]
[{"name":"Paris",...}]
[{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...}]
[]
Inserting the results in you Vue application is left for you.
As Thankyou points out, your inconsistent data format makes it harder to write nice code for this. My approach is slightly different. Instead of transforming your data, I write a wrapper to my generic function to handle this output in a more useful manner.
We start with a function collect, that will work recursively with {name?, search_words?, children?, ...rest} objects, returning the nodes that match a given predicate and recurring on the children. We call this with a function, search, that takes a search term and makes a predicate from it. (Here we test if name or any search_term matches the term; this would be easy to modify for partial matches, for case insensitivity, and so on.)
Then we write the wrapper I mentioned, searchLocations. It descends into the .root node and then maps and combines the results of calling search on each of the root's values.
const collect = (pred) => ({children = [], ...rest}) => [
... (pred (rest) ? [rest] : []),
... children .flatMap (collect (pred))
]
const search = (term) =>
collect (({name = '', search_words = []}) => name == term || search_words .includes (term))
const searchLocations = (locations, term) =>
Object.values (locations .root) .flatMap (search (term))
const locations = {root: {Europe: {children: [{name: "Germany"}, {name: "England", children: [{name: "London", search_words: ["city", "capital"], children: [{name: "Westminster", search_words: ["borough"]}]}, {name: "Manchester", search_words: ["city"]}]}, {name: "France", children: [{name: "Paris", search_words: ["city", "capital"]}]}]}, "North America": {children: [{name: "Canada", children: [{name: "Toronto"}, {name: "Ottawa"}]}, {name: "United States"}]}}}
console .log ('Toronto', searchLocations (locations, 'Toronto'))
console .log ('borough', searchLocations (locations, 'borough'))
console .log ('capital', searchLocations (locations, 'capital'))
.as-console-wrapper {max-height: 100% !important; top: 0}
If what you want, as it sounds like you might, is the same structure as the input, keeping only the nodes necessary to include the matches, then we should be able to do something similar starting with a tree filtering function. I will try to look at that after the holidays.
Update
I did look at this again, looking to filter the tree as a tree. The code wasn't much harder. But this time, I did use a convert function to turn your data into a more consistent recursive structure. Thus the whole object becomes an array with two elements at the root, one with name "Europe" and the other with name "North America", each with their existing children nodes. This makes all further processing easier.
There are two key functions here:
The first is a general-purpose deepFilter function, which takes a predicate and an array of items that may have children nodes structured like their parents, and returns a new version containing anything which matches the predicate and their entirely ancestry. It looks like this:
const deepFilter = (pred) => (xs) =>
xs .flatMap (({children = [], ...rest}, _, __, kids = deepFilter (pred) (children)) =>
pred (rest) || kids.length
? [{...rest, ...(kids.length ? {children: kids} : {})}]
: []
)
The second is specific to this problem: searchLocation. It calls deepFilter using a predicate constructed from a search term and the converted structure already discussed. It uses a convert helper for the structure, and a search helper to turn the search term into a predicate that looks for (case-insensitive) partial matches on the name and all search terms.
const searchLocations = (loc, locations = convert(loc)) => (term) =>
term.length ? deepFilter (search (term)) (locations) : locations
This is demonstrated by a user interface which shows the locations in nested <UL>s, with a search box that filters the locations in real time.
If, for instance, you enter just "w" in the search box, you will get
Europe
England
London (city, capital)
Westminster (borough)
North America
Canada
Ottawa
as "Westminster" and "Ottawa" are the only matches.
If you enter "city" you will get
Europe
England
London (city, capital)
Manchester (city)
France
Paris (city, capital)
You can see this in action in this snippet:
// utility function
const deepFilter = (pred) => (xs) =>
xs .flatMap (({children = [], ...rest}, _, __, kids = deepFilter (pred) (children)) =>
pred (rest) || kids.length
? [{...rest, ...(kids.length ? {children: kids} : {})}]
: []
)
// helper functions
const search = (t = '', term = t.toLowerCase()) => ({name = '', search_words = []}) =>
term.length && (
name .toLowerCase () .includes (term) ||
search_words .some (word => word .toLowerCase() .includes (term))
)
const convert = ({root}) =>
Object.entries (root) .map (([name, v]) => ({name, ...v}))
// main function
const searchLocations = (loc, locations = convert(loc)) => (term) =>
term.length ? deepFilter (search (term)) (locations) : locations
// sample data
const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };
// main function specialized to given data
const mySearch = searchLocations(myData)
// UI demo
const format = (locations, searchTerm) => `<ul>${
locations.map(loc => `<li>${
loc.name +
(loc.search_words ? ` (${loc.search_words. join(', ')})` : ``) +
(loc.children ? format(loc.children, searchTerm) : '')
}</li>`)
.join('')
}</ul>`
const render = (locations, searchTerm) =>
document .getElementById ('main') .innerHTML = format (locations, searchTerm)
document .getElementById ('search') .addEventListener (
'keyup',
(e) => render (mySearch (e.target.value))
)
// show demo
render (mySearch (''))
<div style="float: right" id="control">
<label>Search: <input type="text" id="search"/></label>
</div>
<div style="margin-top: -1em" id="main"></div>
Obviously, this does not use Vue to generate the tree, only some string manipulation and innerHTML. I leave that part to you. But it should show another way to filter a nested structure.
Not entirely clear what you are looking for from your questions, but I'm guessing you need to modify the data to ensure that when it gets rendered, the matched data is correctly highlighted?
Here is a solution to find the matched objects using object-scan
// const objectScan = require('object-scan');
const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };
// eslint-disable-next-line camelcase
const mySearchFn = (term) => ({ name, search_words = [] }) => name === term || search_words.includes(term);
const search = (input, searchFn) => objectScan(['**[*]'], {
filterFn: ({ value, context }) => {
if (searchFn(value)) {
const { children, ...match } = value;
context.push(match);
}
}
})(input, []);
console.log(search(myData, mySearchFn('Toronto')));
// => [ { name: 'Toronto' } ]
console.log(search(myData, mySearchFn('borough')));
// => [ { name: 'Westminster', search_words: [ 'borough' ] } ]
console.log(search(myData, mySearchFn('capital')));
// => [ { name: 'Paris', search_words: [ 'city', 'capital' ] }, { name: 'London', search_words: [ 'city', 'capital' ] } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
And this is how you could inject information that can then be picked up by your rendering pipeline
// const objectScan = require('object-scan');
const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };
// eslint-disable-next-line camelcase
const mySearchFn = (term) => ({ name, search_words = [] }) => name === term || search_words.includes(term);
const search = (input, searchFn) => objectScan(['**[*]'], {
filterFn: ({ value }) => {
if (searchFn(value)) {
value.css = { highlight: true };
return true;
} else {
delete value.css;
return false;
}
},
rtn: 'count' // return number of matches
})(input);
console.log(search(myData, mySearchFn('Toronto')));
// => 1
console.log(myData);
// => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ] } ] }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ] } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto', css: { highlight: true } }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
console.log(search(myData, mySearchFn('borough')));
// => 1
console.log(myData);
// => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ], css: { highlight: true } } ] }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ] } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto' }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
console.log(search(myData, mySearchFn('capital')));
// => 2
console.log(myData);
// => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ] } ], css: { highlight: true } }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ], css: { highlight: true } } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto' }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
Using a library might not be worth it for you, it's a trade-off. Let me know if you have questions / thoughts on this answer.

Recursively group by key values (groups within groups)

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)

How to merge Array of Objects based on the same value?

I have this array of Objects:
var array = [{
country: "Austria",
name: "2019-01-04T23:00:00.000Z",
value: "1"
},
{
country: "Austria",
name: "2019-01-11T23:00:00.000Z",
value: "3"
},
{
country: "Austria",
name: "2019-01-18T23:00:00.000Z",
value: "1"
}
]
I want manipulate this to achieve this result:
var array = [{
country: "Austria",
series: [{
name: "2019-01-04T23:00:00.000Z",
value: "1"
},
{
name: "2019-01-11T23:00:00.000Z",
value: "3"
},
{
name: "2019-01-18T23:00:00.000Z",
value: "1"
}
]
}]
I read many questions but none helped me.
You could loop thorugh the array. Use destructuring to get country and rest of the properties separately. Add each unique country to group object as key and push the rest object to the series array. Then use Object.values() to get the values as an array
const array=[{country:"Austria",name:"2019-01-04T23:00:00.000Z",value:"1"},{country:"Austria",name:"2019-01-11T23:00:00.000Z",value:"3"},{country:"Austria",name:"2019-01-18T23:00:00.000Z",value:"1"}];
const group = {};
array.forEach(({ country, ...rest }) => {
group[country] = group[country] || { country, series: [] };
group[country].series.push(rest)
})
console.log(Object.values(group))
This should do:
var map = {};
for(var entity of array) {
if(!map[entity.country]) {
map[entity.country] = {
country: entity.country,
series: [
{
name: entity.name,
value: entity.value
}
]
};
}
else {
map[entity.country].series.push({
name: entity.name,
value: entity.value
});
}
}
var mappedArray = Object.values(map);
Here is functional solution without for loops and mutable variables:
const result = array.reduce((carry, item) => {
if (!carry.includes(item.country)) {
carry.push(item.country);
}
return carry;
}, []).map(country => {
return {
country: country,
series: array.filter(item => item.country === country).map(item => {
return {
name: item.name,
value: item.value
};
})
};
You can do something like:
const result = array
.map(
c => ({
country: c.country,
series: array
.filter(d => d.country === c.country)
.map(
d => ({
name: d.name,
value: d.value
})
)
})
)

Categories

Resources