moraviec
moraviec

Reputation: 95

JS: Filter array of objects by max value per category

What is most efficient / elegant way to achieve sql-like filtering effect. I want to filter them and get only that objects which are max value in some group.

This is my code, it works but probably it's not best way:

uniqueValues = (arr) => [...new Set(arr)];
getMaxTimeOf = (arr) => Math.max(...arr.map(o => o.timeStamp), 0);
selectorName = (name) => (obj) => obj.name === name;
selectorTime = (time) => (obj) => obj.timeStamp === time;
getGroup = (obj, selector) => obj.filter(selector)

onlyLastChangedFrom = (history) => {
const uniqueNames = uniqueValues(history.map(o => o.name))
let filtered = []
 uniqueNames.forEach(name => {
  const group = getGroup(history, selectorName(name))
  const groupLastTime = getMaxTimeOf(group)
  const lastChange = getGroup(group, selectorTime(groupLastTime))
  filtered.push(lastChange[0])
 });
 return filtered
}   
onlyLastChangedFrom(history)
    // Input:
    [ { name: 'bathroom',
        value: 54,
        timeStamp: 1562318089713 },
      { name: 'bathroom',
        value: 55,
        timeStamp: 1562318090807 },
      { name: 'bedroom',
        value: 48,
        timeStamp: 1562318092084 },
      { name: 'bedroom',
        value: 49,
        timeStamp: 1562318092223 },
      { name: 'room',
        value: 41,
        timeStamp: 1562318093467 } ]

    // Output:
    [ { name: 'bathroom',
        value: 55,
        timeStamp: 1562318090807 },
      { name: 'bedroom',
        value: 49,
        timeStamp: 1562318092223 },
      { name: 'room',
        value: 41,
        timeStamp: 1562318093467 } ]

Upvotes: 8

Views: 6617

Answers (8)

Nina Scholz
Nina Scholz

Reputation: 386560

What is most efficient / elegant way to achieve sql-like filtering effect.

You could take functions for every step and pipe all functions for a single result.

For example in SQL, you would have the following query:

SELECT name, value, MAX(timeStamp) 
FROM data 
GROUP BY name;

With an SQL like approach, you could group first and take the max object out of the result sets.

result = pipe(
    groupBy('name'),
    select(max('timeStamp'))
)(data);

UPDATE 2024

Now with Object.groupBy

const
    // functions
    pipe = (...functions) => input => functions.reduce((acc, fn) => fn(acc), input),
    groupBy = key => array => Object.values(Object.groupBy(
        array, 
        object => object[key]
    )),
    max = key => array => array.reduce((a, b) => a[key] > b[key] ? a : b),
    select = fn => array => array.map(fn),

    // data and result
    data = [{ name: 'bathroom', value: 54, timeStamp: 1562318089713 }, { name: 'bathroom', value: 55, timeStamp: 1562318090807 }, { name: 'bedroom', value: 48, timeStamp: 1562318092084 }, { name: 'bedroom', value: 49, timeStamp: 1562318092223 }, { name: 'room', value: 41, timeStamp: 1562318093467 }],
    result = pipe(
        groupBy('name'),
        select(max('timeStamp'))
    )(data);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 2

Vladimir Salguero
Vladimir Salguero

Reputation: 5947

You can use sort and filter methods

const arr = [{"name":"bathroom","value":54,"timeStamp":1562318089713},{"name":"bathroom","value":55,"timeStamp":1562318090807},{"name":"bedroom","value":48,"timeStamp":1562318092084},{"name":"bedroom","value":49,"timeStamp":1562318092223},{"name":"room","value":41,"timeStamp":1562318093467}]
arr.sort(function (a, b) {
    return b.value - a.value;
}).filter((v, i, a) => a.findIndex((v2) => v2.name === v.name) === i);

console.log(arr);

Upvotes: 0

Ori Drori
Ori Drori

Reputation: 191976

Reduce the array to an object, using the name property as the key. For each item, check if the item that exists in the accumulator has a higher value than the current item, and if not replace it with the current item. Convert back to an array with Object.values():

const arr = [{"name":"bathroom","value":54,"timeStamp":1562318089713},{"name":"bathroom","value":55,"timeStamp":1562318090807},{"name":"bedroom","value":48,"timeStamp":1562318092084},{"name":"bedroom","value":49,"timeStamp":1562318092223},{"name":"room","value":41,"timeStamp":1562318093467}]

const result = Object.values(arr.reduce((r, o) => {
  r[o.name] = (r[o.name] && r[o.name].value > o.value) ? r[o.name] : o

  return r
}, {}))

console.log(result)

Upvotes: 13

vishal patil
vishal patil

Reputation: 71

Use map() and foreach() to get desired output

const arr=[{name:"bathroom",value:54,timeStamp:1562318089713}, 
    {name:"bathroom",value:55,timeStamp:1562318090807}, 
    {name:"bedroom",value:48,timeStamp:1562318092084}, 
    {name:"bedroom",value:49,timeStamp:1562318092223}, 
    {name:"room",value:41,timeStamp:1562318093467}];

let res = new Map();
arr.forEach((obj) => {
    let values = res.get(obj.name);
    if(!(values && values.value > obj.value)){ 
        res.set(obj.name, obj) 
    }
})
console.log(res);
console.log([...res])

Upvotes: 0

Slai
Slai

Reputation: 22876

Here is another reduce alternative :

var arr = [{"name":"bathroom","value":54,"timeStamp":1562318089713},{"name":"bathroom","value":55,"timeStamp":1562318090807},{"name":"bedroom","value":48,"timeStamp":1562318092084},{"name":"bedroom","value":49,"timeStamp":1562318092223},{"name":"room","value":41,"timeStamp":1562318093467}];

var obj = arr.reduce((r, o) => (o.value < (r[o.name] || {}).value || (r[o.name] = o), r), {});

console.log( Object.values(obj) );

Upvotes: 1

Keith
Keith

Reputation: 24181

Doing this in stages.

  1. get a set of names
  2. create a sorted array in descending order
  3. then just map using find to get the first one

Below is an example.

const input = [{"name":"bathroom","value":54,"timeStamp":1562318089713},{"name":"bathroom","value":55,"timeStamp":1562318090807},{"name":"bedroom","value":48,"timeStamp":1562318092084},{"name":"bedroom","value":49,"timeStamp":1562318092223},{"name":"room","value":41,"timeStamp":1562318093467}];

const output = [...new Set(input.map(m => m.name))].
  map(m => [...input].sort(
    (a,b) => b.value - a.value).
    find(x => m === x.name));
  
console.log(output);

Upvotes: 0

StefanN
StefanN

Reputation: 911

I love to use lodash for stuff like this. It's very functional and therefore very clear and straightforward.

Take a look at the following code:

const DATA = [
  {
    name: "bathroom",
    value: 54,
    timeStamp: 1562318089713
  },
  {
    name: "bathroom",
    value: 55,
    timeStamp: 1562318090807
  },
  {
    name: "bedroom",
    value: 48,
    timeStamp: 1562318092084
  },
  {
    name: "bedroom",
    value: 49,
    timeStamp: 1562318092223
  },
  {
    name: "room",
    value: 41,
    timeStamp: 1562318093467
  }
];

let max = _
  .chain(DATA)
  .groupBy('name')
  .sortBy('value')
  .map(o => _(o).reverse().first())
  .flatten()
  .value();

console.log(max); // returns [{"name":"bathroom","value":55,"timeStamp":1562318090807},{"name":"bedroom","value":49,"timeStamp":1562318092223},{"name":"room","value":41,"timeStamp":1562318093467}]

Upvotes: 2

Nick Parsons
Nick Parsons

Reputation: 50684

You can use .reduce() by keeping an accumulated object which keeps the max group currently found and then use Object.values() to get an array of those objects (instead of an key-value pair object relationship).

See example below:

const arr=[{name:"bathroom",value:54,timeStamp:1562318089713},{name:"bathroom",value:55,timeStamp:1562318090807},{name:"bedroom",value:48,timeStamp:1562318092084},{name:"bedroom",value:49,timeStamp:1562318092223},{name:"room",value:41,timeStamp:1562318093467}];

const res = Object.values(arr.reduce((acc, o) => {
  acc[o.name] = acc[o.name] || o;
  if (o.value > acc[o.name].value)
    acc[o.name] = o;
  return acc;
}, {}));

console.log(res);

Upvotes: 0

Related Questions