Home > database >  Finding objects in array by multiple (and unknown in advance) properties
Finding objects in array by multiple (and unknown in advance) properties

Time:06-15

I have an array of objects. I need to write a function that will search the array by properties/values passed as param object. So for example I have this array of users:

const items = [{
  firstName:"John",
  lastName: 'Doe',
  password: '*******',
  email: '[email protected]',
  role: 'ROLE_ADMIN'
},
{
  firstName:"Jane",
  lastName: 'Doe',
  password: '********',
  email: '[email protected]',
  role: 'ROLE_USER'
},
{
  firstName:"John",
  lastName: 'Roe',
  password: '**********',
  email: '[email protected]',
  role: 'ROLE_USER'
}]

Given I pass to the function the following param

{firstName: 'John'}

I should return an array of 2 objects for both users with the name John. And if I pass this param:

{firstName: 'John', role: 'ROLE_ADMIN'}

then I should return an array with only 1 user (in this case) which matches both params.

If no matching users found - return an empty array. If params is an empty object - return the entire array.

So I wrote this function:

findMany(params) {
  return new Promise((resolve, reject) => {
    const keys = Object.keys(params)
    if (!keys.length || !this.items.length) return resolve(this.items)
    let result = []
    let isMatch = false
    for (const item of this.items) {
      for (const key of keys) {
        if(item[key] === params[key]) {
          isMatch = true
        } else {
          isMatch = false
        }
        if (!isMatch) break
      }
      if (isMatch) result.push(item.toJSON())
    }
    resolve(result)
  })
}

Though it works just fine and I do get the desired result, I was wondering is there a more elegant way to write this? It kinda bugs me to have a nested loop, which increases the time complexity to O(n2). So is there a more elegant solution?

And I still didn't care of a case when someone may pass a property that doesn't exist, i.e.

{firstName: 'John', role: 'ROLE_ADMIN', someProperty: 'some value'}

Not sure how to tackle it...

CodePudding user response:

The implementation of a possible solution is pretty straight forward. One basically has to write a filter function.

Such a function receives a single item and upon some comparison logic returns either of the boolean values.

In addition one can take advantage of the filter method's 2nd thisArg argument which will hold the to be matched key-values pairs, provided as object reference.

Thus one just needs to iterate over the provided object's entries and has to return whether every looked for key-value pair has a matching counterpart in the currently processed item.

function doesItemMatchEveryBoundEntry(item) {
  return Object
    // `this` refers to the bound and to matched entries.
    .entries(this)
    .every(([key, value]) => item[key] === value);
}

const items = [{
  firstName: 'John',
  lastName: 'Doe',
  password: '*******',
  email: '[email protected]',
  role: 'ROLE_ADMIN',
}, {
  firstName: 'Jane',
  lastName: 'Doe',
  password: '********',
  email: '[email protected]',
  role: 'ROLE_USER',
}, {
  firstName: 'John',
  lastName: 'Roe',
  password: '**********',
  email: '[email protected]',
  role: 'ROLE_USER',
}];

console.log(
  "{ firstName: 'John' } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { firstName: 'John' },
    ),
);
console.log(
  "{ firstName: 'John', role: 'ROLE_ADMIN' } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { firstName: 'John', role: 'ROLE_ADMIN' },
    ),
);

// OP ... If no matching users found - return an empty array.
console.log(
  "{ firstName: 'John', role: '' } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { firstName: 'John', role: '' },
    ),
);

// OP ... If params is an empty object - return the entire array.
console.log(
  "{ /* empty */ } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { /* empty */ },
    ),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

OP

And I still didn't care of a case when someone may pass a property that doesn't exist, i.e.

{firstName: 'John', role: 'ROLE_ADMIN', someProperty: 'some value'}

Not sure how to tackle it...

One easily could refactor the above approach into a function which receives the additional information by the filter function's this context as well. The changed implementation takes the OP's last requirement/wish into account and implements it with the desired default behavior of skipping (over) provided property names which do not exist in the processed item.

function doesItemMatchEveryBoundEntry(item) {
  const { params, skipNonExisting = true } = this;

  return Object
    .entries(params)
    .every(([key, value]) =>
      // 2nd next line translates to ... `skipProperty`.
      // (and please do not apply "De Morgan's laws".)
      (!(key in item) && !!skipNonExisting) ||
      (item[key] === value)
    );
}

const items = [{
  firstName: 'John',
  lastName: 'Doe',
  password: '*******',
  email: '[email protected]',
  role: 'ROLE_ADMIN',
}, {
  firstName: 'Jane',
  lastName: 'Doe',
  password: '********',
  email: '[email protected]',
  role: 'ROLE_USER',
}, {
  firstName: 'John',
  lastName: 'Roe',
  password: '**********',
  email: '[email protected]',
  role: 'ROLE_USER',
}];

console.log(
  'DEFAULT ... skip non existing properties',
  "\n ... { params: { firstName: 'John', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { params: { firstName: 'John', foo: 'foo' } },
    ),
);
console.log(
  'DEFAULT ... skip non existing properties',
  "\n ... { params: { firstName: 'John', role: 'ROLE_ADMIN', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { params: { firstName: 'John', role: 'ROLE_ADMIN', foo: 'foo' } },
    ),
);
// OP ... If no matching users found - return an empty array.
console.log(
  'DEFAULT ... skip non existing properties',
  "\n... { params: { role: '', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { params: { role: '', foo: 'foo' } },
    ),
);
// OP ... If params is an empty object - return the entire array.
console.log(
  'DEFAULT ... skip non existing properties',
  "\n... { params: { /* empty */ } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { params: { /* empty */ } },
    ),
);

console.log(
  'explixitly take non existing properties into account',
  "\n ... { skipNonExisting: false, params: { firstName: 'John', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { skipNonExisting: false, params: { firstName: 'John', foo: 'foo' } },
    ),
);
console.log(
  'explixitly take non existing properties into account',
  "\n ... { skipNonExisting: false, params: { firstName: 'John', role: 'ROLE_ADMIN', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { skipNonExisting: false, params: { firstName: 'John', role: 'ROLE_ADMIN', foo: 'foo' } },
    ),
);
// OP ... If no matching users found - return an empty array.
console.log(
  'explixitly take non existing properties into account',
  "\n... { skipNonExisting: false, params: { role: '', foo: 'foo' } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { skipNonExisting: false, params: { role: '', foo: 'foo' } },
    ),
);
// OP ... If params is an empty object - return the entire array.
console.log(
  'explixitly take non existing properties into account',
  "\n... { skipNonExisting: false, params: { /* empty */ } } ...",
  items
    .filter(
      doesItemMatchEveryBoundEntry,
      { skipNonExisting: false, params: { /* empty */ } },
    ),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

CodePudding user response:

You can use filter to filter out the array.

const items = [
  {
    firstName: "John",
    lastName: "Doe",
    password: "*******",
    email: "[email protected]",
    role: "ROLE_ADMIN"
  },
  {
    firstName: "Jane",
    lastName: "Doe",
    password: "********",
    email: "[email protected]",
    role: "ROLE_USER"
  },
  {
    firstName: "John",
    lastName: "Roe",
    password: "**********",
    email: "[email protected]",
    role: "ROLE_USER"
  }
];

function match(params) {
  const keys = Object.keys(params);
  return items.filter((item) => {
    let flag = true;
    keys.every((key) => {
      if ((item[key] && item[key] !== params[key]) || !item[key]) {
        flag = false;
        return false;
      }
      return true;
    });
    return flag;
  });
}

console.log(match({ firstName: "John",role: "ROLE_USER" }));

  • Related