26

I need to find unique objects from array based on 2 properties as below. When "class" and "fare" match, I need to pull out unique values and get them in results array.

Source:

var arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]

Expected result:

var result = [{class:"second", fare: "a"},
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "c"}
]

I looked over in SO and was able to find answer which is filtered based on one property (Create array of unique objects by property), but couldn't find which could do based on 2 properties.

Community
  • 1
  • 1
whyAto8
  • 1,660
  • 4
  • 30
  • 56
  • 1
    I think there would be more willingness to help if you showed your own attempt at a solution. You don't *have* to take answers from online; even a long-winded solution based on mild knowledge of object key access might get you thinking about the important parts. – Katana314 Jul 27 '16 at 13:01
  • `class` is an ES6 reserved word. Use 'kind', 'type' or any other alternative. – alejandromav Jul 27 '16 at 13:02
  • @Katana314 - Well, I tried couple of things but then removed that code eventually. Anyway, will try to pos that. – whyAto8 Jul 27 '16 at 13:07
  • @alex030293 - yeah, i agree. i just used it for demo purpose, am actually not using class – whyAto8 Jul 27 '16 at 13:07
  • @alex030293 There is no problem whatsoever using `class` as a property name. –  Jul 27 '16 at 13:07

9 Answers9

31

You could build a combined key for the hash table and filter the given array.

var arr = [{ class: "second", fare: "a" }, { class: "second", fare: "b" }, { class: "first", fare: "a" }, { class: "first", fare: "a" }, { class: "second", fare: "a" }, { class: "first", fare: "c" }],
    result = arr.filter(function (a) {
        var key = a.class + '|' + a.fare;
        if (!this[key]) {
            this[key] = true;
            return true;
        }
    }, Object.create(null));

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

The same without (mis)using thisArg of Array#filter.

var array = [{ class: "second", fare: "a" }, { class: "second", fare: "b" }, { class: "first", fare: "a" }, { class: "first", fare: "a" }, { class: "second", fare: "a" }, { class: "first", fare: "c" }],
    seen = Object.create(null),
    result = array.filter(o => {
        var key = ['class', 'fare'].map(k => o[k]).join('|');
        if (!seen[key]) {
            seen[key] = true;
            return true;
        }
    });

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
19

Using a combined key and a Map

const array = [
 { class: "second", fare: "a" }, 
 { class: "second", fare: "b" }, 
 { class: "first", fare: "a" }, 
 { class: "first", fare: "a" }, 
 { class: "second", fare: "a" }, 
 { class: "first", fare: "c" }
];

console.log(unique(array, ['class', 'fare']));

function unique(arr, keyProps) {
 const kvArray = arr.map(entry => {
  const key = keyProps.map(k => entry[k]).join('|');
  return [key, entry];
 });
 const map = new Map(kvArray);
 return Array.from(map.values());
}
Tamo Maes
  • 355
  • 3
  • 6
  • Tamo, can you add explanation on how this is working? I am a little confused on the `.join` part. – Maddy Sep 24 '19 at 13:39
  • `join` converts an array to a string while inserting the separator between items `|`. It's creating a string based on the values of the keyProps. `second|a`, `second|b`, etc. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join – Jason Harwig Sep 24 '19 at 13:54
7

Hey you can use this method

const unique = (arr, props = []) => [...new Map(arr.map(entry => [props.map(k => entry[k]).join('|'), entry])).values()];

const array = [
    {
        class: 'second',
        fare: 'a', 
    },
    {
        class: 'second',
        fare: 'b', 
    },
    {
        class: 'first',
        fare: 'a', 
    },
    {
        class: 'first',
        fare: 'a', 
    },
    {
        class: 'second',
        fare: 'a', 
    },
    {
        class: 'first',
        fare: 'c',
    },
];

const newArr = unique(array, ['class', 'fare']);

console.log(newArr) // [{ class: 'second', fare: 'a' }, { class: 'second', fare: 'b' }, { class: 'first', fare: 'a' }, { class: 'first', fare: 'c' }]
Asaf
  • 949
  • 2
  • 13
  • 14
4

short way to solve the above problem though it is a very old thread and maybe it might help someone in the future.

const array = [
  { class: "second", fare: "a" }, 
  { class: "second", fare: "b" }, 
  { class: "first", fare: "a" }, 
  { class: "first", fare: "a" }, 
  { class: "second", fare: "a" }, 
  { class: "first", fare: "c" }
];

const uniqueObjects = new Set();

array.forEach(arr => uniqueObjects.add(JSON.stringify(arr)));

console.log(uniqueObjects.entries())
A-P
  • 198
  • 1
  • 12
  • `const array = [ { class: "second", fare: "a" }, { class: "second", fare: "b" }, { class: "first", fare: "a" }, { class: "first", fare: "a" }, { fare: "a", class: "second" }, { class: "first", fare: "c" } ];` your code doesn't work with this array – yury.hrynko Feb 21 '23 at 17:18
2

Like most answers, convert to a map with keys being a concatenation of the values, then back to an array. This one uses reduce.

const array = [
  { class: "second", fare: "a" }, 
  { class: "second", fare: "b" }, 
  { class: "first", fare: "a" }, 
  { class: "first", fare: "a" }, 
  { class: "second", fare: "a" }, 
  { class: "first", fare: "c" }
];

console.log(unique(array, ['class', 'fare']));

function unique(arr, keyProps) {
  return Object.values(arr.reduce((uniqueMap, entry) => {
    const key = keyProps.map(k => entry[k]).join('|');
    if (!(key in uniqueMap)) uniqueMap[key] = entry;
    return uniqueMap;
  }, {}));     
}  
Jason Harwig
  • 43,743
  • 5
  • 43
  • 44
2

Another several variations of extracting a unique array of objects based on arbitrary array of their fields, tested on performance:

Always the best by performance, uses recursive function

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]
 
const getUniqArrBy = (props = [], arrInp = [], objTmp={}, arrTmp=[]) => {
  if (arrInp.length > 0) {
    const lastItem = arrInp[arrInp.length -1]
    const propStr = props.reduce((res, item) => (`${res}${lastItem[item]}`), '')
    if (!objTmp[propStr]) {
      objTmp[propStr] = true
      arrTmp=[...arrTmp, lastItem]
    }
    arrInp.pop()
    return getUniqArrBy(props, arrInp, objTmp, arrTmp)
  }
  return arrTmp
}

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
    [
      {class: "first", fare: "c"},
      {class: "second", fare: "a"},
      {class: "first", fare: "a"},
      {class: "second", fare: "b"},
    ]
*/

1.5% lags behind the first one by performance

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]
 
const getUniqArrBy = (props = [], arrInp = []) => {
  const objKey = {}
  return arrInp.reduce((res, item) => {
    const valStr = props.reduce((res, prop) => `${res}${item[prop]}`, '')
    if(objKey[valStr]) return res
    objKey[valStr] = item
    return [...res, item]
  }, [])
}

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
    [
     {class:"second", fare: "a"},
     {class:"second", fare: "b"},
     {class:"first", fare: "a"},
     {class:"first", fare: "c"}
    ]
*/

Close to the fist two ones by performance

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]

const getUniqArrBy = (props = [], arr = []) => arr.filter((e, i) =>
   arr.findIndex(a => {
    let aKey = '', eKey = ''
    props.forEach(prop => (aKey = `${aKey}${a[prop]}`, eKey = `${eKey}${e[prop]}`))
    return aKey === eKey
}) === i)

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
[
 {class:"second", fare: "a"},
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "c"}
]
*/

In Firefox works fast, in Chrome and Edge - not, short by notation

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]
 
const getUniqArrBy = (props = [], arrInp = []) => {
  return Object.values(arrInp.reduce((res, item) => {
    const keyComb = props.reduce((res, prop) => `${res}${item[prop]}`, '')
    return { ...res, [keyComb]: item }
  }, []))
}

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
    [
     {class:"second", fare: "a"},
     {class:"second", fare: "b"},
     {class:"first", fare: "a"},
     {class:"first", fare: "c"}
    ]
*/

The short by notation, but usually a 5-8% slower then the first

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]

const getUniqArrBy = (props = [], arr = []) => arr.filter((e, i) =>
   arr.findIndex(a => props.reduce((res, prop) => `${res}${a[prop]}`, '') ===
   props.reduce((res, prop) => `${res}${e[prop]}`, '')) === i)

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
    [
     {class:"second", fare: "a"},
     {class:"second", fare: "b"},
     {class:"first", fare: "a"},
     {class:"first", fare: "c"}
    ]
*/

A slower by 9-18% than the first in average in different browsers

const arr = [{class:"second", fare: "a"}, 
 {class:"second", fare: "b"},
 {class:"first", fare: "a"},
 {class:"first", fare: "a"},
 {class:"second", fare: "a"},
 {class:"first", fare: "c"}
]
 
const getUniqArrBy = (props = [], arrInp = []) => {
  const objKey = arrInp.reduce((res, item) => {
    const valStr = props.reduce((res, prop) => `${res}${item[prop]}`, '')
    return {...res, [valStr]: item }
  }, {})
  return Object.keys(objKey).map(item => objKey[item])
}

const uniq = getUniqArrBy(['class', 'fare'], arr)

console.info({ uniq })
/*
    [
     {class:"second", fare: "a"},
     {class:"second", fare: "b"},
     {class:"first", fare: "a"},
     {class:"first", fare: "c"}
    ]
*/
Roman
  • 19,236
  • 15
  • 93
  • 97
1
function unique(arr, keyProps) {
 const kvArray = arr.map(entry => { // entry = {class: "second", fare: "a"}

  // keyProps = ["class", "fare"]
  // k="class"; entry[k]="second";
  // k="fare"; entry[k]="a"
  // keyProps.map(k => entry[k])=["second","a"];
  // .join("|")="second|a";
  const key = keyProps.map(k => entry[k]).join('|'); // key = "second|a"
  return [key, entry]; // ["second|a",{"class":"second","fare":"a"}]
 });

 // kvArray = [["second|a",{"class":"second","fare":"a"}],["second|b", 
 //           {"class":"second","fare":"b"}],["first|a",{"class":"first","fare":"a"}], 
 //           ["first|a",{"class":"first","fare":"a"}],["second|a", 
 //           {"class":"second","fare":"a"}],["first|c",{"class":"first","fare":"c"}]]
 const map = new Map(kvArray); // this will remove duplicate entry with same key. 
 return Array.from(map.values()); // convert back to array from all entry's value
}
Nam Tang
  • 100
  • 1
  • 10
1

Another variation of extracting a unique array of objects based on arbitrary array of their fields:

    const arr = [
      { class: 'second', fare: 'a', weight: 12 },
      { class: 'second', fare: 'b', weight: 10 },
      { class: 'first', fare: 'a', weight: 15 },
      { class: 'first', fare: 'a', weight: 17 },
      { class: 'second', fare: 'a', weight: 12 },
      { class: 'first', fare: 'c', weight: 30 },
      { class: 'second', fare: 'b', weight: 22 },
    ]

    const getUniqArrBy = (props = [], arrInp = [{}]) => {
      const obj = {}
      let result = []
      arrInp.forEach((item, index) => {
            let key = ''
            props.forEach(prop => (key += item[prop]))
            obj[key] = index
      })
      const keys = Object.keys(obj)
      keys.forEach(key => (result = [...result, arrInp[obj[key]]]))
      return result
    }

    const uniq = getUniqArrBy(['class', 'fare'], arr)

    console.info({ uniq })
Ann
  • 47
  • 5
0

You can use forEach to loop and filter or find array property to find if the element exist in array

var arr = [{
      class: "second",
      fare: "a"
    }, {
      class: "second",
      fare: "b"
    }, {
      class: "first",
      fare: "a"
    }, {
      class: "first",
      fare: "a"
    }, {
      class: "second",
      fare: "a"
    }, {
      class: "first",
      fare: "c"
    }]
    var _unArray = []; // new array without duplicate
    arr.forEach(function(item) { // loop through array which contain duplicate
      // if item is not found in _unArray it will return empty array
       var isPresent = _unArray.filter(function(elem) {
        return elem.class === item.class && elem.fare === item.fare
      })
      if (isPresent.length == 0) { 
        _unArray.push(item)
      }
    })
    console.log(_unArray)

JSFIDDLE

brk
  • 48,835
  • 10
  • 56
  • 78
  • It is unsatisfying for the solution to have the properties `class` and `fare` baked into it. –  Jul 27 '16 at 16:16