ueeieiie
ueeieiie

Reputation: 1562

How to filter an array of objects by multiple identical properties

How do I filter this array as described in the question

Description

Notice that entry1 and entry4 share the same value for property: 'subject' and property: 'field'.

Question

Im looking for a performative and clean way to filter this array and get the entries that share both values for those propertyies.

UPDATE:

Returned value

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" },
]

P.S

  1. 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.

  1. My actual data is much bigger, and contains much more property types for each entry. Also its not ordered as nicely as I show here. I did simplify it, so please have in mind performance.

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

Answers (5)

ueeieiie
ueeieiie

Reputation: 1562

I first filtered all property.subjects and reduced them into a multidimensional array, where each array contains subject values that appeared more then once.

Then I filter all property.fields and check if their property.subjectare 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

trincot
trincot

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:

  1. 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.

  2. 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"]'
    
  3. 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"]
    }
    
  4. 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

Ori Drori
Ori Drori

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

nstanard
nstanard

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

JasonR
JasonR

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

Related Questions