Reputation: 1562
Notice that entry1 and entry4 share the same value for property: 'subject'
and property: 'field'
.
Im looking for a performative and clean way to filter this array and get the entries that share both value
s for those property
ies.
UPDATE:
I'm not trying to transform the data but analyze it. so the returned value from the analysis should look like this:
[['entry1', 'entry4'],...]
and with this analysis list I could easily transform my triples = [...]
into a list of triples where I remove one of entries(doesnt matter which, could be 'entry1' or 'entry4'), and update the other one
[
{ subject: "entry1", property: "subject", value: "sport" },
{ subject: "entry1", property: "field", value: "category" },
{ subject: "entry1", property: "content", value: "football" },
{ subject: "entry1", property: "content", value: "basketball" },
]
I'm not looking for a solution like:
array.filter(({property, value})=> property === 'sport' && value === 'category')
I dont know 'sport' or 'category'. Those are dynamic values.
code snippet:
const triples = [
{ subject: "entry1", property: "subject", value: "sport" },
{ subject: "entry1", property: "field", value: "category" },
{ subject: "entry1", property: "content", value: "football" },
{ subject: "entry4", property: "subject", value: "sport" },
{ subject: "entry4", property: "field", value: "category" },
{ subject: "entry4", property: "content", value: "basketball" },
{ subject: "entry2", property: "subject", value: "music" },
{ subject: "entry2", property: "field", value: "category" },
{ subject: "entry2", property: "content", value: "notes" },
{ subject: "entry3", property: "subject", value: "painting" },
{ subject: "entry3", property: "field", value: "category" },
{ subject: "entry3", property: "content", value: "drawings" }
];
Upvotes: 1
Views: 467
Reputation: 1562
I first filtered all property.subject
s and reduced them into a multidimensional array, where each array contains subject values that appeared more then once.
Then I filter all property.field
s and check if their property.subject
are equal as well.
Then I create a mapped object (mergeEntriesBysubjectIndex
) where I get {0: true, 1: false, 2: true}
where each key refer to subjects
indexed values.
In the end, I run on mergeEntriesBysubjectIndex
and each true index will trigger a new merged entry based on the indexed subjects
, and new updated array of all triples.
My implementation:
/*
* @description
* Get an mulitdimensional array, where each inner array represent a list
* of entries with similar value
*
* @ return [[], [], []]
*/
const subjects = Object.values(
triples
.filter(triple => triple.property === "subject")
.reduce((subjects, entry) => {
if (subjects[entry.value]) {
subjects[entry.value].push(entry.subject);
} else {
subjects[entry.value] = [];
subjects[entry.value].push(entry.subject);
}
return subjects;
}, {})
).filter(arr => arr.length > 1);
const fields = triples.filter(triple => triple.property === "field");
/*
* @description
* Create an object based on the "subjects" mulit-dimensional array from before
* Each key represent the index of "subjects", where the value is a boolean *
* representing a similar "property:field" value
*/
const mergeEntriesBysubjectIndex = subjects.reduce((filtered, chunk, index) => {
let values = [];
chunk.forEach(subject => {
const obj = fields.find(field => field.subject === subject).value;
values.push(obj);
});
filtered[index] = values.every((val, i, arr) => val === arr[0]);
return filtered;
}, {});
/*
* @description
* Get an array of subjects value (e.g. "entry1", "entry2")
* and return a new "merged" collection with uniqe objects
* and with the same name for a subject
*/
const mergeEntries = entries => {
const ent = triples.filter(triple => triple.subject === entries[0]);
const newContent = triples
.filter(
triple => triple.subject === entries[1] && triple.property === "content"
)
.map(triple => ({ ...triple, subject: entries[0] }));
return [...ent, ...newContent];
};
/*
* @description
* return a new updated list of triples without the specified entries
*/
const removeEntriesFromCurrentTriples = entries =>
triples.filter(triple => !entries.includes(triple.subject));
for (let index in mergeEntriesBysubjectIndex) {
if (mergeEntriesBysubjectIndex[index]) {
const mergeEntry = mergeEntries(subjects[index]);
const updateEntries = [
...removeEntriesFromCurrentTriples(subjects[index]),
...mergeEntry
];
// The new trasformed triples collection
console.log('transformed triples:', updateEntries)
}
}
Upvotes: 0
Reputation: 350831
I must say the input data structure is not optimal, and the use of "subject" as both a real object property and as a value for property
will make it all the more confusing. I will call the first notion (the real subject
) "entries", since the sample values are "entry1", "entry2", ....
Here is a way to extract ["entry1", "entry4"]
for your sample data:
Group the data by their entry into objects where "property" and "value" are translated into key/value pairs, so you would get something like this:
{
entry1: { subject: "sport", field: "category", content: "football" },
entry4: { subject: "sport", field: "category", content: "basketball" },
entry2: { subject: "music", field: "category", content: "notes" },
entry3: { subject: "painting", field: "category", content: "drawings" }
}
This will be easier to work with. The below code will in fact create a Map
instead of a plain object, but it is the same principle.
Define a new group
property for these objects, where the value is composed of subject and field, stringified as JSON. For example, the first object of the above result would be extended with:
group: '["sport","category"]'
Create a Map of entries, keyed by their group value. So that would give this result:
{
'["sport","category"]': ["entry1","entry4"],
'["music","category"]': ["entry2"],
'["painting","category"]': ["entry3"]
}
Now it is a simple step to only list the values (the subarrays) and only those that have more than one entry value.
Here is the implementation:
const triples = [{subject: "entry1", property: "subject", value: "sport"},{subject: "entry1", property: "field", value: "category"},{subject: "entry1", property: "content", value: "football"},{subject: "entry4", property: "subject", value: "sport"},{subject: "entry4", property: "field", value: "category"},{subject: "entry4", property: "content", value: "basketball"},{subject: "entry2", property: "subject", value: "music"},{subject: "entry2", property: "field", value: "category"},{subject: "entry2", property: "content", value: "notes"},{subject: "entry3", property: "subject", value: "painting"},{subject: "entry3", property: "field", value: "category"},{subject: "entry3", property: "content", value: "drawings"},];
// 1. Group the data by subject into objects where "property" and "value" are translated into key/value pairs:
const entries = new Map(triples.map(o => [o.subject, { entry: o.subject }]));
triples.forEach(o => entries.get(o.subject)[o.property] = o.value);
// 2. Define a group value for these objects (composed of subject and field)
entries.forEach(o => o.group = JSON.stringify([o.subject, o.field]));
// 3. Create Map of entries, keyed by their group value
const groups = new Map(Array.from(entries.values(), o => [o.group, []]));
entries.forEach(o => groups.get(o.group).push(o.entry));
// 4. Keep only the subarrays that have more than one value
const result = [...groups.values()].filter(group => group.length > 1);
console.log(result);
Be aware that the output is a nested array, because in theory there could be more combined entries, like [ ["entry1", "entry4"], ["entry123", "entry521", "entry951"] ]
The above can be modified/extended to get the final filtered result. In the third step you would still collect the objects (not just the entry value), and the filtered result is then mapped back to the original format:
const triples = [{subject: "entry1", property: "subject", value: "sport"},{subject: "entry1", property: "field", value: "category"},{subject: "entry1", property: "content", value: "football"},{subject: "entry4", property: "subject", value: "sport"},{subject: "entry4", property: "field", value: "category"},{subject: "entry4", property: "content", value: "basketball"},{subject: "entry2", property: "subject", value: "music"},{subject: "entry2", property: "field", value: "category"},{subject: "entry2", property: "content", value: "notes"},{subject: "entry3", property: "subject", value: "painting"},{subject: "entry3", property: "field", value: "category"},{subject: "entry3", property: "content", value: "drawings"},];
// 1. Group the data by subject into objects where "property" and "value" are translated into key/value pairs:
const entries = new Map(triples.map(o => [o.subject, { entry: o.subject }]));
triples.forEach(o => entries.get(o.subject)[o.property] = o.value);
// 2. Define a group value for these objects (composed of subject and field)
entries.forEach(o => o.group = JSON.stringify([o.subject, o.field]));
// 3. Create Map of objects(*), keyed by their group value
const groups = new Map(Array.from(entries.values(), o => [o.group, []]));
entries.forEach(o => groups.get(o.group).push(o));
// 4. Keep only the subarrays that have more than one value
const result = [...groups.values()].filter(group => group.length > 1)
// 5. ...and convert it back to the original format:
.flatMap(group => [
{ subject: group[0].entry, property: "subject", value: group[0].subject },
{ subject: group[0].entry, property: "field", value: group[0].field },
...group.map(o => ({ subject: group[0].entry, property: "content", value: o.content }))
]);
console.log(result);
Upvotes: 1
Reputation: 192477
Using lodash you can groupBy the subject
, convert to an object, groupBy objects by the new subject
property and the field
property, and convert back to an array of items:
const { flow, partialRight: pr, groupBy, map, set, head, flatMap, toPairs, isArray } = _;
const dontCollect = key => ['entry', 'subject', 'field'].includes(key);
const createPropery = (subject, property, value) => ({ subject, property, value });
const fn = flow(
pr(groupBy, 'subject'),
pr(map, (g, entry) => ({ // convert to object with the subject as entry
entry,
...g.reduce((r, o) => set(r, o.property, o.value), {}),
})),
pr(groupBy, o => `${o.subject}-${o.field}`),
pr(map, g => g.length > 1 ? _.mergeWith(...g, (a, b, k) => { // merge everything to an object
if(dontCollect(k)) return a;
return [].concat(a, b); // convert non entry, subject, or field properties to array if repeated
}) : head(g)),
pr(flatMap, ({ entry: subject, ...o }) => // convert back a series of rows
flow(
toPairs,
pr(flatMap, ([property, value]) => isArray(value) ?
map(value, v => createPropery(subject, property, v))
:
createPropery(subject, property, value)
)
)(o)
)
);
const triples = [{"subject":"entry1","property":"subject","value":"sport"},{"subject":"entry1","property":"field","value":"category"},{"subject":"entry1","property":"content","value":"football"},{"subject":"entry4","property":"subject","value":"sport"},{"subject":"entry4","property":"field","value":"category"},{"subject":"entry4","property":"content","value":"basketball"},{"subject":"entry2","property":"subject","value":"music"},{"subject":"entry2","property":"field","value":"category"},{"subject":"entry2","property":"content","value":"notes"},{"subject":"entry3","property":"subject","value":"painting"},{"subject":"entry3","property":"field","value":"category"},{"subject":"entry3","property":"content","value":"drawings"}];
const result = fn(triples);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
Upvotes: 0
Reputation: 823
I'll start answering the question but we will need to go back and forth so I can better understand what you are looking for.
let data = [
{subject: 'entry1', property: 'subject', value: 'sport'},
{subject: 'entry1', property: 'field', value: 'category'},
{subject: 'entry1', property: 'content', value: 'football'},
{ subject: 'entry4', property: 'subject', value: 'sport' },
{ subject: 'entry4', property: 'field', value: 'category' },
{ subject: 'entry4', property: 'content', value: 'basketball' },
{subject: 'entry2', property: 'subject', value: 'music'},
{subject: 'entry2', property: 'field', value: 'category'},
{subject: 'entry2', property: 'content', value: 'notes'},
{subject: 'entry3', property: 'subject', value: 'painting'},
{subject: 'entry3', property: 'field', value: 'category'},
{subject: 'entry3', property: 'content', value: 'drawing'}
]
let keys = data.map((item, inex) => { return item.subject })
let uniqueKeys = keys.filter((item, index) => { return keys.indexOf(item) >= index })
let propertiesWeCareAbout = ['subject', 'field']
let mappedValues = data.reduce((acc, item, index) => {
acc[item.subject] = {}
acc[item.subject].values = data.map((subItm, subIndx) => { if (item.subject === subItm.subject) { if (propertiesWeCareAbout.indexOf(subItm.property) > -1) {return subItm.value} }}).filter(Boolean)
return acc;
}, {})
// this is where I leave you... because I think you need to finish this up yourself.
// You have all the mapped data you need to solve your question.
// You now just need to map over the unique keys checking the `mappedValues` data structure for entries that have the same values in the values array.
// You can rename things if you want. But these are all the parts of the solution laid out.
// p.s. You can remove the 'category' string from the propertiesWeCareAbout array based on the example you provided... and you can simplify what I've provided in a number of ways.
// this is where you map to get just the strings of "entry1" and "entry4" based on the other mapped data provided. Then you can combine data as you said you need to.
let finalListOfEntriesThatNeedToBeMerged = uniqueKeys.map((item, index) => {return item})
console.log(mappedValues)
console.log(finalListOfEntriesThatNeedToBeMerged)
This is where you want to start. But the next steps depend on what you are looking to map the data to.
I'm going to focus on this comment next: "entries that share both values for those properties."
Upvotes: 0
Reputation: 401
You can reduce the array of triples to an object where result[propertyString][valueString]
is an array of triples with "property" equal to propertyString and "value" equal to valueString:
triples.reduce((acc, triple) => {
acc[triple.property] = acc[triple.property] || {};
acc[triple.property][triple.value] = acc[triple.property][triple.value] || [];
acc[triple.property][triple.value].push(triple);
return acc;
}, {})
You can then search that object for the properties and values you want, and check if there is more than one triple.
Upvotes: 0