I find this a deceptively interesting question. My first approach was similar to the one from Nick Parsons, adding a JSON.parse to the results. But I didn't like that it returned a clone of the object. My second attempt looked like this:
const isNotLikeTheOthers = (makeKey) => (objs) =>
objs [Object .entries ( objs.reduce (
(a, obj, idx, __, key = makeKey(obj)) => ({...a, [key] : [...(a[key] || []), idx]}),
{}
)).find (([k, idxs]) => idxs.length == 1)[1][0]]
const oneOfTheseThings = isNotLikeTheOthers (JSON.stringify)
const arr = [{team: "yes", school: "yes", work: "yes"}, {team: "yes", school: "yes", work: "yes"}, {team: "no", school: "no", work: "yes"}, {team: "yes", school: "yes", work: "yes"}];
console .log (
oneOfTheseThings (arr)
)
And I wrote a minor variant of it in case the items are in an object rather than an array:
const isNotLikeTheOthers = (makeKey) => (objs, vals = Object .values (objs) ) =>
vals [ Object .entries ( vals .reduce (
(a, obj, idx, __, key = makeKey(obj)) => ({...a, [key] : [...(a[key] || []), idx]}),
{}
)).find (([k, idxs]) => idxs.length == 1)[1][0]]
const oneOfTheseThings = isNotLikeTheOthers (JSON.stringify)
const objs = {one: {team: "yes", school: "yes", work: "yes"}, two: {team: "yes", school: "yes", work: "yes"}, three: {team: "no", school: "no", work: "yes"}, four: {team: "yes", school: "yes", work: "yes"}};
console .log (
oneOfTheseThings (objs)
)
Both of these separate out makeKey and use JSON.stringify for it. It might be useful to be able to supply an alternative key-generation function. But it also might just make sense to inline JSON.stringify here.
Note that both of these return references to your original object, not clones of it, like we might get with a JSON.stringify/JSON.parse dance. This seems useful, although I don't know the OP's requirements.
However, there is something wrong with this approach. This is a find operation. It should be able to stop as soon as we know the result. But we have to process every record with this technique. We should be able to create a version which stops as soon as the result is known. That was the genesis of my third version:
const snowflake = (eq) => ([one, two, three, ...rest]) =>
eq(one, two)
? [three, ...rest] .find (x => ! eq (one, x))
: eq (one, three)
? two
: one
const uniqueItem = snowflake (equals)
const arr = [{team: "yes", school: "yes", work: "yes"}, {team: "yes", school: "yes", work: "yes"}, {team: "no", school: "no", work: "yes"}, {team: "yes", school: "yes", work: "yes"}];
console .log (
uniqueItem (arr)
)
<script src="https://bundle.run/[email protected]"></script>
<script> const {equals} = ramda </script>
Here we write a function that accepts a function that tests whether two values are equal and returns a function that finds the unique item in an array based on it. In this case, I use Ramda's equals function to test, but we could have just as easily used Underscore's or any home-grown version. In fact, we can also use a comparison of JSON.stringify's result, via
const uniqueItem = snowflake ( (a, b) => JSON.stringify (a) == JSON.stringify (b) )
Again we can inline the comparison operation, and I probably would unless I had a strong need for the more generic version.
This version seems the best all around. There must be at least three values for the question to even make sense, so we name them one, two, and three. We can start by comparing the first two items. If they are the same, then the answer is the first item among the remaining item not to equal that first value. If they are different, then one of them must be the oddball, and we can determine which simply by comparing the first with the third.
This version may not be any more time efficient than the others, especially because a comparison like the JSON.stringify will have to run multiple times for our baseline object. We could fix that, and curry one parameter into some version of equals, but that seems to lose a little of the elegance.