0

I have objects that look like the below:

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"
     }     

As you can see in all of the objects are the same except three. I want the object 'three' to be saved in a variable

var uniqueObject = three;

How can I select the object that is not like the others?

5
  • Is it guaranteed that all the other objects are the same? Commented Jun 26, 2019 at 12:58
  • are these objects stored within one larger object? In an array or in separate variables? Commented Jun 26, 2019 at 12:59
  • @nickzoum yes almost 100% of the time the others will be the same Commented Jun 26, 2019 at 13:02
  • @NickParsons its seperate variables, but can be added to an array if that will work better Commented Jun 26, 2019 at 13:03
  • @jumpman8947 yes, if you put it into an array it will make this easier to work with. Commented Jun 26, 2019 at 13:15

5 Answers 5

3

If you put your objects into an array such that you can use array methods to help assist you in achieving your goal. Firstly, you could use JSON.stringify() with reduce to keep a record of the number of objects you find in your array. Then you can use Object.values with .find to find the object which has an occurrence of 1, which will be your odd object.

See example below:

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"}];

const new_obj = arr.reduce((acc, obj, i) => {
  const key = JSON.stringify(obj);
  if(key in acc) {
    acc[key].count += 1;
  } else {
    acc[key] = {count:1, i};
  }
  return acc;
}, {});

const values = Object.values(new_obj);

let res = -1;
if(values.length != 1) {
  let {i} = values.find(({count}) => count === 1);
  res = arr[i];
}
console.log(res);

Sign up to request clarification or add additional context in comments.

3 Comments

This is not returning the object, but a JSON-stringified version of it. (This is slightly disguised by console.log, but if you log typeof res you get "string". While you can do a JSON.parse on the results, you are then returning a clone, and not the original object.
@ScottSauyet good pickup Scott. Thanks for pointing that out. The issue should be solved now
Yes, that fixes it. See my answer for alternatives that return the original object rather than a clone, and especially the last one ("snowflake"), which takes a significantly different approach.
1

First we transform the object to an array via the Object.value function. Then we turn the containing objects into Strings to allow for String comparisons via filter. The filter checks whether the current entry is truly unique. Afterwards we parse the returning String back to Object.

As of right now we only return the very first unique value (in case there ever be more than one). But on the other hand, this would even work when the inner objects had a different structure or a deeply nested structure!

let obj = {
  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"
  }
};

let res = JSON.parse(Object.values(obj)
            .map(o => JSON.stringify(o))
            .filter((v,i,arr) => arr.indexOf(v) == arr.lastIndexOf(v))[0] || null);

console.log(res)

3 Comments

Would it make more sense to throw an error or return no value if the expectations of having only one different version are not met?
Technically one should iterate over the results from filter (without the [0]!), parsing each current value. In that situation you would not have to implement a dirty hack like seen above. If there is none or too many results (easily checkable via length) fare what fits your code idea the best.
That indexOf === lastIndexOf is a nice technique that I will have to remember!
1

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.

1 Comment

Some very interesting approaches here as well, well explained
0

This will work ,hope it helps.Using Simple filter with Object.values() and bringing out duplicates with Set (to check if its total yes or yes and no)

let js = {
  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"
  }
}
// if array length is 1 there are repeated values ,if its 2 then there are two distinct (yes and no both present)
let res = Object.values(js).filter(ele => {
  if ([...new Set(Object.values(ele))].length == 2) {
    return ele;
  }
})

console.log(res[0])

3 Comments

I'm guessing that the point is not to find the one with two separate values but the one which happens to be different from the others. For instance if object three was yes-yes-yes and the others were all no-no-yes, then it would still be object three that we should seek.
Umm the user who asked knows well.I just worked according to the output ,this still works if all are no ie no no no and even if two are no and one is yes and vice versa@ScottSauyet
I think you're missing the point of the question. We don't know the structure of the objects; they might have entirely different keys or different numbers of them. However, we know that all but one of them are the same. The goal is to find that one.
0

You can group your objects using a function like:

function groupObject(object, expression) {
  if (typeof expression !== "function") expression = function(x) {
    return JSON.stringify(x);
  };
  return Object.keys(object).reduce(function(result, key) {
    var groupKey = expression.call(object, object[key], key, object);
    if (!(groupKey in result)) result[groupKey] = {};
    result[groupKey][key] = object[key];
    return result;
  }, {});
}

Which will get an output of the form:

{
    "{\"team\":\"yes\",\"school\":\"yes\",\"work\":\"yes\"}": {
        "one": {
            "team": "yes",
            "school": "yes",
            "work": "yes"
        },
        "two": {
            "team": "yes",
            "school": "yes",
            "work": "yes"
        },
        "four": {
            "team": "yes",
            "school": "yes",
            "work": "yes"
        }
    },
    "{\"team\":\"no\",\"school\":\"no\",\"work\":\"yes\"}": {
        "three": {
            "team": "no",
            "school": "no",
            "work": "yes"
        }
    }
}

After that, you can just find the group that has only 1 item and the get name of that item.

const obj = {
  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"
  }
};

function groupObject(object, expression) {
  if (typeof expression !== "function") expression = function(x) {
    return JSON.stringify(x);
  };
  return Object.keys(object).reduce(function(result, key) {
    var groupKey = expression.call(object, object[key], key, object);
    if (!(groupKey in result)) result[groupKey] = {};
    result[groupKey][key] = object[key];
    return result;
  }, {});
}

var group = groupObject(obj);

var groupItem = Object.values(groupObject(obj)).find(function(group) {
  return Object.keys(group).length === 1;
});

var itemName = Object.keys(groupItem)[0];

console.log("Final result: " + itemName);
console.log("Result + object: ");
console.log(groupItem);
console.log("Grouped Object: ");
console.log(group);

For the record, the code shown above is es-5 compatible and will work in older browsers without the need of transpilers. Also the function groupObject allows you to be more specific (i.e. groupObject(obj, x => x.team) will only group the team property):

const obj = {
  one: {
    team: "yes",
    school: "no",
    work: "yes"
  },
  two: {
    team: "yes",
    school: "no",
    work: "yes"
  },
  three: {
    team: "no",
    school: "yes",
    work: "yes"
  },
  four: {
    team: "yes",
    school: "yes",
    work: "yes"
  }
};

function groupObject(object, expression) {
  if (typeof expression !== "function") expression = function(x) {
    return JSON.stringify(x);
  };
  return Object.keys(object).reduce(function(result, key) {
    var groupKey = expression.call(object, object[key], key, object);
    if (!(groupKey in result)) result[groupKey] = {};
    result[groupKey][key] = object[key];
    return result;
  }, {});
}

// `one` and `two` got matched in the first group although they are not identical to `four`, because only the `team` property is being checked
var group = groupObject(obj, x => x.team);

var groupItem = Object.values(groupObject(obj)).find(function(group) {
  return Object.keys(group).length === 1;
});

var itemName = Object.keys(groupItem)[0];

console.log("Final result: " + itemName);
console.log("Result + object: ");
console.log(groupItem);
console.log("Grouped Object: ");
console.log(group);

4 Comments

@Nick Parsons: It did not provide the requested return when I checked it ten minutes ago and it is just a chunk of code without explanation.
@JavaScript Now the answer is complete
If you are willing to forgot ES5 compatibility, you could write function groupObject(object, expression = JSON.stringify) {...} and skip your first block. (Then of course you can make it an arrow function, and use a single-expression body, and... ;-) )
@ScottSauyet Had it as an arrow function in the first edit, But I figured it would be better to provide an es5 answer, since its a more abstract/generic function

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.