13

I want to write a function that checks if an object has at least one value containing a substring. Something like this (pseudo-code):

const userMatchesText = (text, user) => user.includes(text);

The full structure of my objects (

So, for a user like the following:

const user = {
    id: '123abc',
    info: {
        age: 12,
        bio: 'This is my bio' 
    },
    social: {
        chatName: 'Chris',
        friends: ['friend1', 'other friend'],
        blocks: ['Creep']
    }
    //Etc. The objects I'm working with contain nested objects and arrays, etc.
}

, userMatches('bi', user) should return true because the substring 'bi' is found in the bio: 'this is my bio'. userMatches('324d, user) should likewise return false. usermatches('blocks', user) should, however, return false because the substring is only found in one of the keys, not one of the values.

The objects I'm working it look like this (the Mongoose Schema):

{
    account  : {
        dateOfCreation : Number
    },
    social   : {
        chatName         : String,
        friends          : [ { type: String } ],
        blocks           : [ { type: String } ],
        sentRequests     : [ { type: String } ],
        recievedRequests : [ { type: String } ],
        threads          : [ { type: String } ]
    },
    info     : {
        age            : Number,
        bio            : String,
        displayName    : String,
        profilePicture : String,
        subjects       : {
            tutor   : [
                {
                    subject : String,
                    level   : String
                }
            ],
            student : [
                {
                    subject : String,
                    level   : String
                }
            ]
        }
    },
    facebook : {
        id         : String,
        firstName  : String,
        middleName : String,
        fullName   : String,
        lastName   : String
    }
}

The best way of doing this I've found so far is destructuring all the keys that are strings off the object, and then using map and includes, like the function below.

const doesUserMatchText = (user, text) => {
    const { social: { chatName }, info: { displayName }, facebook: { firstName, middleName, lastName } } = user;
    const possibleMatches = [ chatName, displayName, firstName, middleName, lastName ];
    let match = false;
    possibleMatches.map(possibleMatch => {
        if (possibleMatch.includes(text)) {
            return (match = true);
        }
    });
};

This is, however, really annoying (and probably terribly inefficient, too), as the objects I'm working with are really large. It'd be really nice if i could just call userMatchesText(text, user) and get a Boolean value. Thanks a lot in advance!

Also, note I am not destructuring off all the keys that are Strings. The purpose of this function is to filter users based on a search query, and I figured it perhaps doesn't make too much sense to let users serch for other users by their bio, id etc. but rather, only by their various 'names'.

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 1
    Unless there is some structure of these objects that could be leveraged, its unavoidable to have to inspect each and every key to see if it is a match. (Excluding the short circuit `true` path, if we find a match early in the search) – Kallmanation Mar 02 '18 at 22:06
  • Thanks for the answer. I will update the anwer with the full structure of the objects. I did find a way of doing it, and it works very nicely (I think), but as you said, I have to access the pairs individually.. – Christoffer Corfield Aakre Mar 02 '18 at 22:17

3 Answers3

8

You can do this with a recursive function to traverse the entire object. Just make sure that the object doesn't have any circular references...

const user = {
    id: '123abc',
    info: {
        age: 12,
        bio: 'This is my bio' 
    },
    social: {
        chatName: 'Chris',
        friends: ['friend1', 'other friend'],
        blocks: ['Creep']
    }
    //Etc. The objects I'm working with contain nested objects and arrays, etc.
};

function userMatchesText(text, user) {
    if (typeof user === "string") return user.includes(text);
    return Object.values(user).some(val => userMatchesText(text, val));
}

console.log(userMatchesText("bi", user));
console.log(userMatchesText("other fri", user));
console.log(userMatchesText("zzz", user));
CRice
  • 29,968
  • 4
  • 57
  • 70
  • Great use of `some`! – Sidney Mar 02 '18 at 22:21
  • I know this is an older answer, but is there any way you can account for words AFTER the phase. For example, if userMatchesText("bi bro",user). It would come up false. – Swink Nov 29 '18 at 00:38
  • This function will error out on the `Object.values` call if `user` includes any `null` or `undefined` values. – Aaron Apr 17 '21 at 05:21
  • I was dealing with `null`s which was giving me errors, so I wrapped @CRice's interior in a `truthy` statement to filter out all the `null`s. https://stackoverflow.com/questions/5515310/is-there-a-standard-function-to-check-for-null-undefined-or-blank-variables-in – Pixelsmith Mar 23 '23 at 00:06
2

Pure JavaScript. This iterates over the object keys and as soon as it found one match it returns true.

The worst case is when the result is false, it iterates over all keys and subkeys.

(function() {
  var user = {
    id: '123abc',
    info: {
      age: 12,
      bio: 'This is my bio'
    },
    social: {
      chatName: 'Chris',
      friends: ['friend1', 'other friend'],
      blocks: ['Creep']
    }
    //Etc. The objects I'm working with contain nested objects and arrays, etc.
  };

  console.log('userMatches(\'bi\', user): ' + userMatches('bio', user));
  console.log('userMatches(\'324d\', user): ' + userMatches('324d', user));
  console.log('usermatches(\'blocks\', user) ' + userMatches('blocks', user));

  function userMatches(str, obj) {
    var queue = [];
    for (var k in obj) {
      if (obj.hasOwnProperty(k)) {
        if (typeof obj[k] === 'string') {
          if (obj[k].indexOf(str) !== -1) {
            return true;
          }
        } else {
          queue.push(obj[k]);
        }
      }
    }
    if (queue.length) {
      for (var i = 0; i < queue.length; i++) {
        if (userMatches(str, queue[i])) {
          return true;
        }
      }
    }
    return false;
  }
}());
lealceldeiro
  • 14,342
  • 6
  • 49
  • 80
1

This should do the trick:

(See explanation below the code)

const findInObject = (predicate, object) => {
  if (typeof object !== 'object') {
    throw new TypeError('Expected object but got ' + typeof object)
  }
  
  for (let key in object) {
    const value = object[key]
    switch (typeof value) {
      case 'object':
        if (findInObject(predicate, value))
          return true
      default:
        if (predicate(value))
          return true
    }
  }
  return false
}

const userContainsText = (text, user) => 
  findInObject(
    val => {
      if (typeof val !== 'string')
        return false
        
      return val.includes(text)
    },
    user
  )
  
const user = {
    id: '123abc',
    info: {
        age: 12,
        bio: 'This is my bio' 
    },
    social: {
        chatName: 'Chris',
        friends: ['friend1', 'other friend'],
        blocks: ['Creep']
    }
}

console.log(userContainsText('Chris', user))

The findInObject function does the heavy lifting. You supply a predicate (that's a function that returns true or false based on if the input "passes") and an object to search. It runs the predicate on every key in the object, recursively if the supplied object contains objects. It should stop searching if it gets a match. Otherwise, it travels the whole object.

The userContainsText function uses findInObject. It supplies a predicate that checks the content of any strings it gets. (Any other type fails the test). This function accepts the text to look for, and the user object to search (although technically this can by any object, not specifically a "user" object).

Sidney
  • 4,495
  • 2
  • 18
  • 30