Given a bunch of ( not sorted ) entities
const entities = [
{ id: "person-1", type: "person", fields: { age: 34 }},
{ id: "car-2", type: "car", fields: { manufacturer: "bar" }},
{ id: "house-2", type: "house", fields: { constructionYear: 2010 }},
{ id: "person-4", type: "person", fields: { age: 71 }},
{ id: "person-2", type: "person", fields: { age: 57 }},
{ id: "house-1", type: "house", fields: { constructionYear: 1968 }},
{ id: "car-1", type: "car", fields: { manufacturer: "foo" }},
{ id: "person-3", type: "person", fields: { age: 42 }},
];
and a bunch of "sources" with an optional sort object describing the sort index and a "isLessThan" compare function as a string
const sources = [
{ type: "person", sort: { index: 1, isLessThanFunctionAsString: "(left, right) => left.fields.age < right.fields.age" }},
{ type: "car" },
{ type: "house", sort: { index: 0, isLessThanFunctionAsString: "(left, right) => left.fields.constructionYear < right.fields.constructionYear" }},
];
Each source describes how to deal with entities of the given type. The source for "person" defines how entities of type "person" should be sorted.
I do not have any control over the configuration, the isLessThan function comes as a stringified function and its signature is (leftEntity: Entity, rightEntity: Entity) => boolean, so the logic inside the compare function could be anything
I want to sort the array entities by the information gathered from sources and started with
const entities = [{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}];
const sources = [{type:"person",sort:{index:1,isLessThanFunctionAsString:"(left, right) => left.fields.age < right.fields.age"}},{type:"car"},{type:"house",sort:{index:0,isLessThanFunctionAsString:"(left, right) => left.fields.constructionYear < right.fields.constructionYear"}}];
function sortEntities(unsortedEntities, allSources) {
// if there are no entities, there is nothing to do
if (unsortedEntities.length === 0) {
return unsortedEntities;
}
// only care for the sources with a sort function
const sourcesWithSort = allSources.filter(source => !!source.sort);
// if there are no sources with sort, there is nothing to do
if (sourcesWithSort.length === 0) {
return unsortedEntities;
}
// since we can only compare two entities of the same type we must sort the entities by type first
let sortedEntities = entities.sort((leftEntity, rightEntity) => {
// no need for sorting if both have the same type
if (leftEntity.type === rightEntity.type) {
return 0;
}
if (leftEntity.type < rightEntity.type) {
return -1;
}
return 1;
});
// we must sort the sources by sort index ( at this point we now that sort must exist )
const sortSources = sourcesWithSort.sort((leftSource, rightSource) => leftSource.sort.index - rightSource.sort.index);
// NOW we can start sorting the entities
for (const source of sortSources) {
sortedEntities = sortedEntities.sort((leftEntity, rightEntity) => {
const {
type
} = source;
// we can't compare entities if the types aren't equal to the source type
if (leftEntity.type !== type || rightEntity.type !== type) {
return 0;
}
const isLessThanFunction = (new Function("return " + source.sort.isLessThanFunctionAsString))();
const isLeftEntityLessThanRightEntity = isLessThanFunction(
leftEntity,
rightEntity
);
if (isLeftEntityLessThanRightEntity) {
return -1;
}
return 1;
});
}
return sortedEntities;
}
console.log(sortEntities([...entities], [...sources]));
My approach is getting really slow when dealing with many entities ( > 100 ) and many sources ( > 20 )
Do you have any ideas how to improve the code or maybe come up with faster alternatives?
You can change the columns array to use a comparator function which directly returns a sort order expected buy the sort comapreFn callback function. For the numerical fields it will be subtraction.
const columns = [
{ type: "person", sort: { index: 1, comparator: (left, right) => left.fields.age - right.fields.age }},
{ type: "car", sort: undefined },
{ type: "house", sort: { index: 0, comparator: (left, right) => left.fields.constructionYear - right.fields.constructionYear }},
];
If you want one for car, it would be
comparator: (a, b) => a.fields.manufacturer.localeCompare(b.fields.manufacturer)
Then you create a mapper object which maps the type to the index and the comparator function. If an index is not mentioned, then it is set to -Infinity (Because, in your case you have the car results on top)
const columnMap = columns.reduce((acc, o) => {
const { type, sort: { index = -Infinity, comparator } = {} } = o
acc[type] = { index, comparator };
return acc
}, {})
It creates this object:
{
"person": {
"index": 1,
"comparator": (a,b)=>a.fields.age-b.fields.age
},
"car": {
"index": -Infinity,
"comparator": undefined
},
"house": {
"index": 0,
"comparator": (a,b)=>a.fields.constructionYear-b.fields.constructionYear
}
}
Then sort the entities based on index value
if the index values are same (or both are -Infinity), then sort based on the type name
if both types are same, then sort based on the type specific comparator function.
Here's a working snippet:
const entities=[{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}],
columns=[{type:"person",sort:{index:1,comparator:(e,s)=>e.fields.age-s.fields.age}},{type:"car",sort:void 0},{type:"house",sort:{index:0,comparator:(e,s)=>e.fields.constructionYear-s.fields.constructionYear}}];
function sortEntities(array, columns) {
const columnMap = columns.reduce((acc, o) => {
const { type, sort: { index = -Infinity, comparator } = {} } = o
acc[type] = { index, comparator };
return acc
}, {})
return array.sort((a,b) =>
columnMap[a.type].index - columnMap[b.type].index
|| a.type.localeCompare(b.type)
|| columnMap[a.type].comparator?.(a,b)
)
}
console.log(sortEntities(entities, columns))
If you can't change the columns array, you can create your own comparator function by using eval to create a function using the string.
const entities=[{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}],
columns =[{type:"person",sort:{index:1,isLessThanFunctionAsString:"(left, right) => left.fields.age < right.fields.age"}},{type:"car"},{type:"house",sort:{index:0,isLessThanFunctionAsString:"(left, right) => left.fields.constructionYear < right.fields.constructionYear"}}];
function sortEntities(array, columns) {
const columnMap = columns.reduce((acc, { type, sort }) => {
let index = -Infinity, comparator;
if (sort) {
eval("var isLessThanFunction =" + sort.isLessThanFunctionAsString)
index = sort.index;
comparator = (a, b) => isLessThanFunction(a, b) ? -1 : 1
}
acc[type] = { index, comparator };
return acc
}, {})
return array.sort((a, b) =>
columnMap[a.type].index - columnMap[b.type].index
|| a.type.localeCompare(b.type)
|| columnMap[a.type].comparator?.(a, b)
)
}
console.log(sortEntities(entities, columns))
First of all, you will need to make sure your data is prepared to go into a table. You will need to process the raw data and convert it into an array of records. After that, you can sort your data much easier. Lastly, you can convert the sorted records into a table and add it to the document.
Also, you have a key on your sorter called isLessThan. You should change that to something more generic like fn or callback. You will want all sorters to be keyed similarly.
const main = () => {
document.body.append(table(sort(process(entities), columns), columns));
};
const sort = (data, columns) => {
const sorters = columns
.map(({ sort }) => sort)
.filter(s => s)
.sort(({ index: a }, { index: b }) => a - b)
.map(({ isLessThan }) => isLessThan); // This is not good...
return data.sort((a, b) => {
let sorted = 0, index = 0;
while (sorted === 0 && index < sorters.length) {
sorted = sorters[index](a, b);
index++;
}
return sorted;
});
};
const process = (rawData) =>
Object.entries(rawData.reduce((acc, { id, type, fields }) => {
const [, index] = id.match(/\w+-(\d+)/);
if (!acc[index]) acc[index] = { id: +index };
acc[index][type] = { fields };
return acc;
}, {})).sort(([k1], [k2]) => k1 - k2).map(([, v]) => v);
const table = (records, columns) =>
group('table', {},
group('thead', {},
group('tr', {},
...columns.map(col =>
leaf('th', { innerText: col.type })
)
)
),
group('tbody', {},
...records.map(record =>
group('tr', {},
...columns.map(col =>
leaf('td', {
innerText: render(record[col.type], record, col)
})
)
)
)
)
);
const render = (data, record, column) =>
data ? JSON.stringify(data) : '';
const leaf = (tagName, options) =>
Object.assign(document.createElement(tagName), options);
const group = (tagName, options, ...children) =>
appendAll(leaf(tagName, options), children);
const appendAll = (el, children = []) => {
children.forEach(child => el.appendChild(child));
return el;
};
const entities = [
{ id: "person-1", type: "person", fields: { age: 34 }},
{ id: "car-2", type: "car", fields: { manufacturer: "bar" }},
{ id: "house-2", type: "house", fields: { constructionYear: 2010 }},
{ id: "person-4", type: "person", fields: { age: 71 }},
{ id: "person-2", type: "person", fields: { age: 57 }},
{ id: "house-1", type: "house", fields: { constructionYear: 1968 }},
{ id: "car-1", type: "car", fields: { manufacturer: "foo" }},
{ id: "person-3", type: "person", fields: { age: 42 }},
];
const columns = [
{ type: "id" },
{ type: "person", sort: { index: 1, isLessThan: (left, right) => left?.fields?.age < right?.fields?.age }},
{ type: "car" },
{ type: "house", sort: { index: 0, isLessThan: (left, right) => left?.fields?.constructionYear < right?.fields?.constructionYear }},
];
main();
table { border-collapse: collapse; }
table, th, td { border: thin solid grey; }
th, td { padding: 0.5rem; }
th { background: #DDD; }
tbody tr:nth-child(even) { background: #EEE; }
td { font-family: monospace; font-size: smaller; }
I need to perform filter in the array of objects to get all the keys. Although, whenever there is a obj inside of that key, I would need to get the key name and concat with the key name from the obj, so for example:
const data = [ id: 5, name: "Something", obj: { lower: True, higher: False } ]
result = ["id", "name", "obj.lower", "obj.higher"]
I could manage to do the above code, but, if there is more objs inside the data, I would need to keep adding a if condition inside of my logic, I would like to know if there is any other way, so it doesn't matter how many objects I have inside the objects, It will concat always.
The code I used from the above mention:
const itemsArray = [
{ id: 1, item: "Item 001", obj: { name: 'Nilton001', message: "Free001", obj2: { test: "test001" } } },
{ id: 2, item: "Item 002", obj: { name: 'Nilton002', message: "Free002", obj2: { test: "test002" } } },
{ id: 3, item: "Item 003", obj: { name: 'Nilton003', message: "Free003", obj2: { test: "test003" } } },
];
const csvData = [
Object.keys(itemsArray[0]),
...itemsArray.map(item => Object.values(item))
].map(e => e.join(",")).join("\n")
// Separating keys
let keys = []
const allKeys = Object.entries(itemsArray[0]);
for (const data of allKeys) {
if (typeof data[1] === "object") {
const gettingObjKeys = Object.keys(data[1]);
const concatingKeys = gettingObjKeys.map((key) => data[0] + "." + key);
keys.push(concatingKeys);
} else {
keys.push(data[0])
}
}
//Flating
const flattingKeys = keys.reduce((acc, val: any) => acc.concat(val), []);
What I would like to achieve, lets suppose I have this array of object:
const data =
[
{ id: 10, obj: {name: "Name1", obj2: {name2: "Name2", test: "Test"}}}
...
]
Final result = ["id", "obj.name", "obj.obj2.name2", "obj.obj2.test"]
OBS: The first obj contains all the keys I need, no need to loop through other to get KEYS.
I would like to achieve, all the keys from the first object of the array, and if there is objects inside of objects, I would like to concat the obj names (obj.obj2key1)
You could map the key or the keys of the nested objects.
const
getKeys = object => Object
.entries(object)
.flatMap(([k, v]) => v && typeof v === 'object'
? getKeys(v).map(s => `${k}.${s}`)
: k
),
getValues = object => Object
.entries(object)
.flatMap(([k, v]) => v && typeof v === 'object'
? getValues(v)
: v
),
data = { id: 1, item: "Item 001", obj: { name: 'Nilton001', message: "Free001", obj2: { test: "test001" } } },
keys = getKeys(data),
values = getValues(data);
console.log(keys);
console.log(values);
.as-console-wrapper { max-height: 100% !important; top: 0; }
something like this
const itemsArray = [
{ id: 1, item: "Item 001", obj: { name: 'Nilton001', message: "Free001", obj2: { test: "test001" } } },
{ id: 2, item: "Item 002", obj: { name: 'Nilton002', message: "Free002", obj2: { test: "test002" } } },
{ id: 3, item: "Item 003", obj: { name: 'Nilton003', message: "Free003", obj2: { test: "test003" } } },
];
const item = itemsArray[0];
const getAllKeys = (obj, prefix=[]) => {
if(typeof obj !== 'object'){
return prefix.join('.')
}
return Object.entries(obj).flatMap(([k, v]) => getAllKeys(v, [...prefix, k]))
}
console.log(getAllKeys(item))
The OP solution can be simplified by accepting a prefix param (the parent key) and a results param (defaulted to [] and passed into the recursion) to do the flattening...
let obj = { key0: 'v0', key1: { innerKey0: 'innerV0', innerInner: { deeplyNested: 'v' } }, key2: { anotherInnerKey: 'innerV' } }
function recursiveKeys(prefix, obj, result=[]) {
let keys = Object.keys(obj);
keys.forEach(key => {
if (typeof obj[key] === 'object')
recursiveKeys(key, obj[key], result);
else
result.push(`${prefix}.${key}`)
});
return result;
}
console.log(recursiveKeys('', obj))
function getKeys(obj) {
return Object.keys((typeof obj === 'object' && obj) || {}).reduce((acc, key) => {
if (obj[key] && typeof obj[key] === 'object') {
const keys = getKeys(obj[key]);
keys.forEach((k) => acc.add(`${key}.${k}`));
} else {
acc.add(key);
}
return acc;
}, new Set());
}
// accumulate the keys in a set (the items of the array may
// have different shapes). All of the possible keys will be
// stored in a set
const s = itemsArray.reduce(
(acc, item) => new Set([...acc, ...getKeys(item)]),
new Set()
);
console.log('Keys => ', Array.from(s));
You can use recursion as follows. Since typeof([1,3,5]) is object, we also have to confirm that value is not an array, !Array.isArray(value):
const obj = { id: 10, obj: {name: "Name1", obj2: {name2: "Name2", test: "Test"}}};
const getKeys = (o,p) => Object.entries(o).flatMap(([key,value]) =>
typeof(value) === 'object' && !Array.isArray(value) ?
getKeys(value, (p?`${p}.`:"") + key) :
(p ? `${p}.`: "") + key
);
console.log( getKeys(obj) );
I have below array of objects
{
"roleid":[{"rolename":[1639]}],
"modnameid":[{"mname":[1516,1515,1514]}],
"accesstype":[{"accesstype":["VO","AA"]}]
}
and I want to convert it format below
[
{"modnameid":1516,"accesstype":"VO","roleid":1639},
{"modnameid":1516,"accesstype":"AA","roleid":1639},
{"modnameid":1515,"accesstype":"VO","roleid":1639},
{"modnameid":1515,"accesstype":"AA","roleid":1639},
{"modnameid":1514,"accesstype":"VO","roleid":1639},
{"modnameid":1514,"accesstype":"AA","roleid":1639}
]
How do we go about it, basic assumption what multi-tier map based operation, food for thoughts
Assistance is appreciated!
Beside the hardcoded answer, you could take a dynamic approach and get first an object with flat arrays of data and build an array of the cartesian product.
function getCartesian(object) {
return Object.entries(object).reduce((r, [k, v]) => {
var temp = [];
r.forEach(s =>
(Array.isArray(v) ? v : [v]).forEach(w =>
(w && typeof w === 'object' ? getCartesian(w) : [w]).forEach(x =>
temp.push(Object.assign({}, s, { [k]: x }))
)
)
);
return temp;
}, [{}]);
}
const
data = { roleid: [{ rolename: [1639] }], modnameid: [{ mname: [1516, 1515, 1514] }], accesstype: [{ accesstype: ["VO", "AA"] }] },
flat = v => v && typeof v === 'object'
? Object.values(v).flatMap(flat)
: v;
temp = Object.fromEntries(Object.entries(data).map(([k, v]) => [k, flat(v)]));
console.log(temp);
console.log(getCartesian(temp));
.as-console-wrapper { max-height: 100% !important; top: 0; }
can user
let data = {
"roleid": [{ "rolename": [1639] }],
"modnameid": [{ "mname": [1516, 1515, 1514] }],
"accesstype": [{ "accesstype": ["VO", "AA"] }]
}
let roles = []
data.roleid.forEach(element => {
roles = [...roles, ...element.rolename]
});
let names = []
data.modnameid.forEach(element => {
names = [...names, ...element.mname]
});
let types = []
data.accesstype.forEach(element => {
types = [...types, ...element.accesstype]
});
let informations = []
roles.forEach(role => {
names.forEach(name => {
types.forEach(type => {
informations.push({ "modnameid": role, "accesstype": type, "roleid": name })
});
});
});
console.log(informations);
I tried to print "person[0].name: a" and "person[1].name: b" and so on, based on this person array of object:
I used object entries twice, any other way I can make the loop more efficient?
const person = [{
name: 'a',
}, {
name: 'b'
}]
Object.entries(person).forEach(([key, value]) => {
Object.entries(value).forEach(([key2, value2]) => {
console.log(`person[${key}].${key2}`, ':', value2)
})
})
You could take a recursive approach and hand over the parent path.
const
show = (object, parent) => {
const wrap = Array.isArray(object) ? v => `[${v}]` : v => `.${v}`;
Object.entries(object).forEach(([k, v]) => {
if (v && typeof v === 'object') show(v, parent + wrap(k));
else console.log(parent + wrap(k), v);
});
},
person = [{ name: 'a' }, { name: 'b' }];
show(person, 'person');
You don't really need the first call. person is already an Array.
const person = [{
name: 'a',
}, {
name: 'b'
}]
person.forEach((value, key) => {
Object.entries(value).forEach(([key2, value2]) => {
console.log(`person[${key}].${key2}`, ':', value2)
})
})
Just in case forEach is for side effects. If you want to create another array with transformed values you better use map/flatMap instead.
const person = [{
name: 'a',
}, {
name: 'b'
}]
const transformed = person.flatMap((value, key) => {
return Object.entries(value).map(([key2, value2]) => `person[${key}].${key2}:${value2}`)
})
console.log(transformed)
You can also try something like this, with only one forEach() loop
const person = [{
name: 'a',
},{
name: 'b'
}];
person.forEach((el,i) => {
let prop = Object.keys(el).toString();
console.log(`person[${i}].${prop}`, ':', el[prop])
});
I would like to filter my data depending on a typed keyword.
https://jsfiddle.net/LeoCoco/e96L8akn/
let keyword = '-pre';
let data = {
'Basic': [{
name: 'a-basic'
}, {
name: 'b-basic'
}, {
name: 'c-basic'
}],
'Premium': [{
name: 'a-pre'
}, {
name: 'b-pre'
}, {
name: 'c-pre'
}],
'Extra': [{
name: 'a-ext'
}, {
name: 'b-ext'
}, {
name: 'c-ext'
}],
};
Output
'Premium': [{name: 'a-pre'}, { name: 'b-pre'}, { name: 'c-pre'}]
My try
lodash.forEach(data, (card) => {
card = card.filter(o => {
return Object.keys(o).some(k => {
return typeof o[k] === 'string' && o[k].toLowerCase().includes(keyword.toLowerCase());
});
});
})
But it does not work.The difficulty for me is that the filtering must happen on the nested object keys contained in each array.
var result={};
Object.keys(data).forEach(key => {
result[key] = data[key].filter(o => {
return Object.keys(o).some(k =>typeof o[k] === 'string' && o[k].toLowerCase().includes(keyword.toLowerCase()));
});
})
Because this is object you can use reduce() on Object.keys() instead and then inside use every() to check for keyword.
let keyword = '-pre';
let data = {"Basic":[{"name":"a-basic"},{"name":"b-basic"},{"name":"c-basic"}],"Premium":[{"name":"a-pre"},{"name":"b-pre"},{"name":"c-pre"}],"Extra":[{"name":"a-ext"},{"name":"b-ext"},{"name":"c-ext"}]}
var result = Object.keys(data).reduce(function(r, e) {
var check = data[e].every(o => o.name.indexOf(keyword) != -1);
if(check) r[e] = data[e]
return r;
}, {})
console.log(result)