1

If I have an array like:

[
    {
        id: 1,
        title: 'foo'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    },
    {
        id: 4,
        title: 'bantz'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    }
]

And I want to return an array that contains any objects that appear only once. So for this example, the desired output would be:

[
    {
        id: 1,
        title: 'foo'
    },
    {
        id: 4,
        title: 'bantz'
    }
]

I have tried a few different approaches that I have found to solve this using reduce() and indexOf(), like this solution, but they do not work with objects for some reason.

Any assistance would be greatly appreciated.

Matt Gween
  • 177
  • 2
  • 12
  • All of those objects are distinct; from what you posted, *none* of them are really duplicates. You probably have your own criteria for what makes two objects the same; is it the "id" being equal? Both the "id" and "title"? – Pointy Jun 21 '17 at 20:54
  • They would be objects with all the same keys and values. I don't understand your comment. How is { id: 2, title: bar } not a duplicate of { id: 2, title: bar }? They have the exact same keys and values. EDIT: I just noticed what I listed as desired outcome is wrong. I will edit it now. – Matt Gween Jun 21 '17 at 20:57
  • @MattGween Pointy is explaining why the solutions using indexOf fail. `{} != {}` because, despite having the same keys and values as each other, they are two separate objects. – Paul Jun 21 '17 at 21:02
  • 1
    They would be unique objects because you are creating them directly. Each object you create is unique, even if the data is inside. What you want is to use your own version of equality and filter on that. – samanime Jun 21 '17 at 21:04
  • Possible duplicate of [Unique values in an array](https://stackoverflow.com/questions/1960473/unique-values-in-an-array) – zero298 Jun 21 '17 at 21:26

3 Answers3

3

Do something like this:

const data = [
    {
        id: 1,
        title: 'foo'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    },
    {
        id: 4,
        title: 'bantz'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    }
];

const isEqual = (a, b) => a.id === b.id;
const unique = (arr) => arr.reduce((result, a, index) =>
  result.concat(arr.some(b => a !== b && isEqual(a, b)) ? [] : a)
, []);

console.log(unique(data));

In this case, we loop through each element to reduce(), and before we add it, we see if another version of it exists in the array before adding it. We have to make sure that we are also not being equal without ourselves (otherwise we'd get an empty array).

isEqual() is a separate function to make it easy to customize what "equal" means.

As written, each element in data is unique, they're all separate objects. data[0] === data[4] is false, even though they have the same data. You must compare the data inside to determine if they're duplicates or not. As Paulpro mentioned earlier, {} === {} is also false, because they're two different objects, even though their values are the same.

console.log({} === {});
console.log({ a: 1 } === { a: 1 });

In the example version of isEqual(), I considered them equal if they had the same id.


Answer to previous version of the question

Do something like this:

const data = [
    {
        id: 1,
        title: 'foo'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    },
    {
        id: 4,
        title: 'bantz'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    }
];

const isEqual = (a, b) => a.id === b.id;
const unique = (arr) => arr.reduce((result, a) =>
  result.concat(result.some(b => isEqual(a, b)) ? [] : a)
, []);

console.log(unique(data));

I split isEqual() to it's own function so you could easily define what "equal" means. As someone pointed out, technically all of those are unique, even if the data is different. In my example, I defined equal ids to mean equal.

I then use reduce to go through each and build an object. Before I add it to the array (via concat()), I loop through all of them with some() and go until either I find one that is equal (which I wouldn't include) or none are equal and I add it.

Community
  • 1
  • 1
samanime
  • 25,408
  • 15
  • 90
  • 139
3

You could use a Map to avoid having to look through the array again and again, which would lead to inefficient O(n²) time-complexity. This is O(n):

function getUniquesOnly(data) {
    return Array.from(
        data.reduce( (acc, o) => acc.set(o.id, acc.has(o.id) ? 0 : o), new Map),
        (([k,v]) => v)
    ).filter( x => x );
}

var data = [
    {
        id: 1,
        title: 'foo'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    },
    {
        id: 4,
        title: 'bantz'
    },
    {
        id: 2,
        title: 'bar'
    },
    {
        id: 3,
        title: 'bat'
    }
];

console.log(getUniquesOnly(data));
trincot
  • 317,000
  • 35
  • 244
  • 286
0

A straightforward implementation would look something like this:

  • Create an empty set (in this case an array) to contain unique values by whatever metric you define (I.E. deep comparison or comparing by a unique value like the "id")
  • Loop over the list of values
  • Whenever you find a value that is not contained within the set of unique values, add it

That is essentially how the solution you posted works, except that all of the values in your array are -- in JavaScript's eyes -- unique. Because of this you need to define your own way to compare values.

The .reduce method can be used like so:

function areEqual(a, b) { /* define how you want the objects compared here */ }

function contains(a, lst) {
    return lst.reduce(function (acc, x) {
        return acc || areEqual(a, x);
    }, false);
}

function getUnique(lst) {
    return lst.reduce(function (acc, x) {
        if(!contains(x, acc))
        {
            acc.push(x);
        }

        return acc;
    }, []);
}

You may want to look at how JavaScript object comparison works. For deep comparison specifically (which it sounds like you want) I would look at existing answers.