I have a nested array of objects which looks something like below.
[
{
"region": null,
"country": null,
"territory": "Worldwide",
"territoryCode": "ALL",
"t2": null,
"t3": null,
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 1,
"localLanguageName": "N/A",
"localLanguageCode": null
}
},
{
"region": "Africa",
"country": "Madagascar",
"territory": null,
"territoryCode": "MG",
"t2": "AFR",
"t3": "MG",
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 30,
"localLanguageName": "Malagasy, French",
"localLanguageCode": "MLG, FRE"
}
},
{
"region": "Africa",
"country": null,
"territory": null,
"territoryCode": "AFR",
"t2": "AFR",
"t3": null,
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 2,
"localLanguageName": "N/A",
"localLanguageCode": null
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": null,
"territoryCode": "MA",
"t2": "AFR",
"t3": "MA",
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 35,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": "Morocco (excl. Western Sahara)",
"territoryCode": "MAXEH",
"t2": "AFR",
"t3": "MA",
"t4": "MAXEH",
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 36,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": "Western Sahara",
"territoryCode": "EH",
"t2": "AFR",
"t3": "MA",
"t4": "EH",
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 37,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
}
]
I'm looking to group my entire data object on the basis of unique region, country, t2-t7 binations, and to have an output like this
[{
"region": "Africa",
"country": [{
"desc": "Madagascar",
"t2": [{
"id": "AFR",
"localLanguageName": "Malagasy, French",
"localLanguageCode": "MLG, FRE"
"t3": [{
"id": "MG"
}]
}]
},
{
"desc": "Morocco (incl. Western Sahara)",
"subTerritory": [{
"t2": "AFR",
"t3": [{
"id": "MA",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t4": [{
"id": "MAXEH",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t5": [{
"id": ""
.
.
.
}]
},
{
"id": "EH",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t5": [{
"id": ""
.
.
.
}]
}]
}]
}]
}]
}]
I'm looking at the most efficient way to group the data. Is it better to use a hashmap ? or the map/reduce methods in Javascript ?
I have tried the below. It's obviously inplete, but I'm stuck after a couple of iterations.
const result = Object.values(data.reduce((key, curr) => {
const { region, country, t2, t3, t4, t5, t6, t7 } = curr;
if (!key[country]) {
let obj = {};
obj.region = region;
obj.country = country;
obj.t2 = [{
id: t2,
t3: [{
id: t3,
t4: {
id: t4,
t5: t5
}
}]
}];
key[country] = obj;
} else {
key[country].t2 = key[country].t2 || [];
const foundCountry = key[country].t2.find(x => x.desc === t2);
if (!foundCountry) {
key[country].t2.push({
id: t2,
t3: [{
id: t3,
t4: {
id: t4,
t5: t5
}
}]
});
} else {
const tx = foundCountry.find(x => x.id === t3);
if (!tx) {
foundCountry.push({
id: t3,
t4: {
id: t4,
t5: t5
}
});
} else {
tx.id = t3;
tx.t4 = t4;
}
}
}
return key;
}, {}));
console.log(util.inspect(result, false, null, true))
return result;
I have a nested array of objects which looks something like below.
[
{
"region": null,
"country": null,
"territory": "Worldwide",
"territoryCode": "ALL",
"t2": null,
"t3": null,
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 1,
"localLanguageName": "N/A",
"localLanguageCode": null
}
},
{
"region": "Africa",
"country": "Madagascar",
"territory": null,
"territoryCode": "MG",
"t2": "AFR",
"t3": "MG",
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 30,
"localLanguageName": "Malagasy, French",
"localLanguageCode": "MLG, FRE"
}
},
{
"region": "Africa",
"country": null,
"territory": null,
"territoryCode": "AFR",
"t2": "AFR",
"t3": null,
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 2,
"localLanguageName": "N/A",
"localLanguageCode": null
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": null,
"territoryCode": "MA",
"t2": "AFR",
"t3": "MA",
"t4": null,
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 35,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": "Morocco (excl. Western Sahara)",
"territoryCode": "MAXEH",
"t2": "AFR",
"t3": "MA",
"t4": "MAXEH",
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 36,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
},
{
"region": "Africa",
"country": "Morocco (incl. Western Sahara)",
"territory": "Western Sahara",
"territoryCode": "EH",
"t2": "AFR",
"t3": "MA",
"t4": "EH",
"t5": null,
"t6": null,
"t7": null,
"localLanguage": {
"territoryId": 37,
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
}
}
]
I'm looking to group my entire data object on the basis of unique region, country, t2-t7 binations, and to have an output like this
[{
"region": "Africa",
"country": [{
"desc": "Madagascar",
"t2": [{
"id": "AFR",
"localLanguageName": "Malagasy, French",
"localLanguageCode": "MLG, FRE"
"t3": [{
"id": "MG"
}]
}]
},
{
"desc": "Morocco (incl. Western Sahara)",
"subTerritory": [{
"t2": "AFR",
"t3": [{
"id": "MA",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t4": [{
"id": "MAXEH",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t5": [{
"id": ""
.
.
.
}]
},
{
"id": "EH",
"localLanguageName": "Arabic, French",
"localLanguageCode": "ARA, FRE"
"t5": [{
"id": ""
.
.
.
}]
}]
}]
}]
}]
}]
I'm looking at the most efficient way to group the data. Is it better to use a hashmap ? or the map/reduce methods in Javascript ?
I have tried the below. It's obviously inplete, but I'm stuck after a couple of iterations.
const result = Object.values(data.reduce((key, curr) => {
const { region, country, t2, t3, t4, t5, t6, t7 } = curr;
if (!key[country]) {
let obj = {};
obj.region = region;
obj.country = country;
obj.t2 = [{
id: t2,
t3: [{
id: t3,
t4: {
id: t4,
t5: t5
}
}]
}];
key[country] = obj;
} else {
key[country].t2 = key[country].t2 || [];
const foundCountry = key[country].t2.find(x => x.desc === t2);
if (!foundCountry) {
key[country].t2.push({
id: t2,
t3: [{
id: t3,
t4: {
id: t4,
t5: t5
}
}]
});
} else {
const tx = foundCountry.find(x => x.id === t3);
if (!tx) {
foundCountry.push({
id: t3,
t4: {
id: t4,
t5: t5
}
});
} else {
tx.id = t3;
tx.t4 = t4;
}
}
}
return key;
}, {}));
console.log(util.inspect(result, false, null, true))
return result;
Share
Improve this question
edited Feb 3, 2023 at 8:35
Nina Scholz
387k26 gold badges364 silver badges414 bronze badges
asked Mar 31, 2020 at 3:59
stackUser67stackUser67
1284 silver badges13 bronze badges
5
- 1 In your original data, you're in great shape to connect to elasticsearch! About how to write the conversion, what have you tried that is tripping you up? – Doug Commented Mar 31, 2020 at 4:03
- @Doug, Tried the following var theatres = [], city = [];for (var key in data) { var output = {}, city_data = {}; city_data.desc= data[key].city; output.id = data[key].theatreId; output.desc = data[key].theatreDescription; theatres.push(output); city.push(city_data)} But looking to do the same using any default methods – stackUser67 Commented Mar 31, 2020 at 4:05
-
why do you have for
country
different object structure, one witht2
and another withsubTerritory
? what is the reason about? – Nina Scholz Commented Nov 14, 2020 at 13:52 - please add a realistic result from the data. – Nina Scholz Commented Nov 14, 2020 at 15:33
- @stackUser67 could you please specify the exact keys from the input json that you want to do a group by on? As per my understanding with the output json you have posted, you are grouping by 'region' at level 1 and all the countries under the same region sit inside the array being referenced by the key 'country'. And how exactly are you planning to populate 'subTerritory' in your final output json? – Soumojit Ghosh Commented Nov 17, 2020 at 5:37
6 Answers
Reset to default 2This answer offers two solutions:
- A level centristic approach with custom functions for processing every level.
- A fast approach with a given array of keys and a uniform result set.
Custom functions for every level with short circuit
This approach takes a different view to every level and allows to end the iteration for a certain node.
result = data.reduce((r, o) => {
let p = r;
groups.every(fn => p = fn(o, p));
return r;
}, []);
The above code iterates the given data set and uses an array of grouping function which returns either truthy or falsy value. In case of a falsy value the iteration stops at this level.
({ region }, target) => { // 1
if (!region) return; // 2
let temp = target.find(q => q.region === region); // 3
if (!temp) target.push(temp = { region, country: [] }); // 4
return temp.country; // 5
}
A grouping function has at least four or five parts.
The function signature contains the source object or a subset via destructuring and a target. The target could be an array or an object.
A check if the grouping value exists and if not it returns. This ends the iteration for all following groups as well. This point is optional.
A search for finding the right object with the wanted group.
A check if the group exist and if not it creates a new group with wanted properties
A return value of the function, either an array or an object, depending on the next grouping function.
The main advantage of this approach is the ability to handle different levels with custom structures and the possibility to omit empty/not given grouping values.
The main disadvantage is to have a function for every level.
const
groups = [
({ region }, target) => {
if (!region) return;
let temp = target.find(q => q.region === region);
if (!temp) target.push(temp = { region, country: [] });
return temp.country;
},
({ country: desc }, target) => {
if (!desc) return;
let temp = target.find(q => q.desc === desc);
if (!temp) target.push(temp = { desc, t2: [] });
return temp.t2;
},
({ t2: id, localLanguage: { localLanguageName, localLanguageCode } }, target) => {
if (!id) return;
let temp = target.find(q => q.id === id);
if (!temp) target.push(temp = { id, localLanguageName, localLanguageCode, t3: [] });
return temp.t3;
},
({ t3: id }, target) => {
if (!id) return;
let temp = target.find(q => q.id === id);
if (!temp) target.push(temp = { id, t4: [] });
return temp.t4;
},
({ t4: id }, target) => {
if (!id) return;
let temp = target.find(q => q.id === id);
if (!temp) target.push(temp = { id, t5: [] });
return temp.t5;
}
],
data = [{ region: null, country: null, territory: "Worldwide", territoryCode: "ALL", t2: null, t3: null, t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 1, localLanguageName: "N/A", localLanguageCode: null } }, { region: "Africa", country: "Madagascar", territory: null, territoryCode: "MG", t2: "AFR", t3: "MG", t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 30, localLanguageName: "Malagasy, French", localLanguageCode: "MLG, FRE" } }, { region: "Africa", country: null, territory: null, territoryCode: "AFR", t2: "AFR", t3: null, t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 2, localLanguageName: "N/A", localLanguageCode: null } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: null, territoryCode: "MA", t2: "AFR", t3: "MA", t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 35, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: "Morocco (excl. Western Sahara)", territoryCode: "MAXEH", t2: "AFR", t3: "MA", t4: "MAXEH", t5: null, t6: null, t7: null, localLanguage: { territoryId: 36, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: "Western Sahara", territoryCode: "EH", t2: "AFR", t3: "MA", t4: "EH", t5: null, t6: null, t7: null, localLanguage: { territoryId: 37, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }],
result = data.reduce((r, o) => {
let p = r;
groups.every(fn => p = fn(o, p));
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Fast uniform approach, every level has the same structure
{ id, children: [] }
This is the structure of the result for every level. id
refkects the actual group value and children contains other objects of the same structure or the final object.
This approach takes an object for all keys of the level and a result _
inside.
It start with filtering the top groups with a value and reduces the groups by pulling out the level property from the (final) object.
If the group at the actual level does not exist a new property with an array is created. This object contains a single property _
with an empty array. This array shares the same object reference as in the later visible object for this level's children.
At the end of the reducing the final object without visited properties is pushed.
Finally the underscore property is returned, because it contains all nested groups.
The main advantage of this approach is to add instantly the wanted groupings by the given keys.
The main disadvantage is to get a usniform result which offers no customisation for certain level. It does not filter unwanted objects.
const
keys = ['region', 'country', 't2', 't3', 't4', 't5', 't6', 't7'],
data = [{ region: null, country: null, territory: "Worldwide", territoryCode: "ALL", t2: null, t3: null, t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 1, localLanguageName: "N/A", localLanguageCode: null } }, { region: "Africa", country: "Madagascar", territory: null, territoryCode: "MG", t2: "AFR", t3: "MG", t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 30, localLanguageName: "Malagasy, French", localLanguageCode: "MLG, FRE" } }, { region: "Africa", country: null, territory: null, territoryCode: "AFR", t2: "AFR", t3: null, t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 2, localLanguageName: "N/A", localLanguageCode: null } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: null, territoryCode: "MA", t2: "AFR", t3: "MA", t4: null, t5: null, t6: null, t7: null, localLanguage: { territoryId: 35, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: "Morocco (excl. Western Sahara)", territoryCode: "MAXEH", t2: "AFR", t3: "MA", t4: "MAXEH", t5: null, t6: null, t7: null, localLanguage: { territoryId: 36, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }, { region: "Africa", country: "Morocco (incl. Western Sahara)", territory: "Western Sahara", territoryCode: "EH", t2: "AFR", t3: "MA", t4: "EH", t5: null, t6: null, t7: null, localLanguage: { territoryId: 37, localLanguageName: "Arabic, French", localLanguageCode: "ARA, FRE" } }],
result = data
.reduce((t, o) => {
const groups = keys.filter((flag => k => flag = flag && o[k])(true));
groups
.reduce(function (r, k) {
let id;
({ [k]: id, ...o } = o);
if (!r[id]) {
r[id] = { _: [] };
r._.push({ id, children: r[id]._ });
}
return r[id];
}, t)._.push(o);
return t;
}, { _: [] })
._;
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
It is hard to understand what part, if or any of the given data set should be covered by a canonical solution and what target op is looking for. Therefore this answer consist of two part but if a hint would be given, anybody would take a benefit.
You need to group the array object using Array.prototype.reduce based on the state
property then check if a city exists or not, if it exists overwrite with latest values, else push it into the city
array, likewise you need to check for the theater, at the end you need to return the accumulator for next iteration.
const data1 = [{
city: 'LAKE GENEVA',
state: 'WISCONSIN',
theatreId: '000080',
theatreDescription: 'GENEVA 4'
},
{
city: 'BURLINGTON',
state: 'WISCONSIN',
theatreId: 'c05364',
theatreDescription: 'PLAZA THEATRE 4'
}
];
const data2 = [{
city: 'MIAMI',
state: 'FLORIDA',
theatreId: 'c05170',
theatreDescription: 'DOLPHIN 24'
}, {
city: 'MIAMI',
state: 'FLORIDA',
theatreId: '000306',
theatreDescription: 'CMX BRICKELL CITY CENTRE 10'
}];
const reduceCityTheaters = (arr) => Object.values(arr.reduce((acc, curr) => {
// Deconstruct needed properties
const { state, city, theatreId, theatreDescription } = curr;
// Check if state not present
if (!acc[state]) {
let obj = {};
obj.state = state;
obj.city = [{
desc: city,
theatres: [{
id: theatreId,
desc: theatreDescription
}]
}];
acc[state] = obj;
} else { // Check if state is present
acc[state].city = acc[state].city || [];
const foundCity = acc[state].city.find(x => x.desc === city);
// Check if city exists or not if not push it
if (!foundCity) {
acc[state].city.push({
desc: city,
theatres: [{
id: theatreId,
desc: theatreDescription
}]
});
} else {
const foundTheater = foundCity.theatres.find(x => x.id === theatreId);
// Check if theatre exists or not if not push it
if (!foundTheater) {
foundCity.theatres.push({
id: theatreId,
desc: theatreDescription
});
} else {
foundTheater.id = theatreId;
foundTheater.desc = theatreDescription;
}
}
}
return acc;
}, {}));
const res1 = reduceCityTheaters(data1);
const res2 = reduceCityTheaters(data2);
console.log('1', res1);
console.log('2', res2);
I would start with a very generic groupBy
that supports nesting multiple groups based on an array of keys:
// Takes an array of keys and array of objects and returns a nested grouping
const groupByKeys = ([k, ...ks], xs) => k
? mapObj(
ys => groupByKeys(ks, ys),
groupByKey(k, xs)
)
: xs;
// Groups an array based by a key
const groupByKey = (k, xs) => xs
.reduce(
(gs, x) => Object.assign(gs, { [x[k]]: (gs[k] || []).concat([x]) }),
{}
);
// Utility to map a function over the values of an object
const mapObj = (f, obj) => Object.fromEntries(
Object.entries(obj).map(([k, v]) => [ k, f(v) ])
);
Note: You can probably find well maintained, better tested and more performant versions of these utility methods in front-end libraries like Ramda or lodash.
You can now group your data using:
const groups = groupByKeys(
["region", "country", "t2", "t3", "t4", "t5", "t6", "t7"],
your_data
);
Next step would be the harder part of transforming in to the desired format, and dealing with all the null
paths.
To make this a bit easier and more performant, I include an array of all elements on every grouping layer:
const groupByKeys = ([k, ...ks], xs) => k ? Object.assign(mapObj( ys => groupByKeys(ks, ys), groupByKey(k, xs) ), { [AllKey]: xs }) : xs;
We can now traverse our grouping using Object.values
, Array.prototype.map
, and some destructuring:
const transform = groups => Object
.values(groups)
.map(
({ [AllKey]: allElementsInThisLayer, ...childGrouping }) => { /* ... */ }
);
All that's left to do now is define the logic for transforming every layer. This is where, to be honest, I don't really understand your desired oute. I implemented the first few layers, but maybe you can do a better job yourself, now that you have structured data to work with:
// Transformers for all layers of grouping
const Region = ({ [AllKey]: [ { region } ], ...countries }) => ({
region,
country: Object.values(countries).map(Country).filter(Country.notEmpty)
});
Region.notEmpty = ({ region }) => region !== null;
const Country = ({ [AllKey]: [ { country } ], ...t2}) => ({
desc: country,
t2: Object.values(t2).map(T2)
});
Country.notEmpty = ({ desc }) => desc !== null;
const T2 = ({ [AllKey]: [ t2 ], ...t3 }) => ({
id: t2.t2,
localLanguageName: t2.localLanguage.localLanguageName,
localLanguageCode: t2.localLanguage.localLanguageCode,
t3: Object.values(t3).map(T3)
})
const T3 = ({ [AllKey]: [ { t3 } ], ...t4 }) => ({
id: t3 // Etc.
})
Here's a runnable snippet. Continue the work at the // Etc.
ment