Как отфильтровать массив объектов для нечувствительных к регистру совпадений из любого ключа объекта

У меня есть этот пример кода, и я пытаюсь отфильтровать совпадающие объекты, не увеличивая сложность или производительность кода:

Этот код здесь фильтрует совпадения на основе одного явно определенного ключа, и он не чувствителен к регистру.

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
    rogueBonusKey: 'bob likes salmon' },
]

const searchString = 'Bob'

const found = people.filter((person) => {
  if (person.firstName === searchString) return true
})

console.log(found)

ЦЕЛЬ:

  1. Я хочу, чтобы он соответствовал регистру без учета регистра
  2. Я хочу, чтобы он возвращал совпадения с любого ключа
  3. Я хочу, чтобы он нашел, используя contains не точное совпадение

Что-то вроде этого:

// const people = [
//   { firstName: 'Bob', lastName: 'Smith', status: 'single' },
//   { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
//   { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
//   { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
//   { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
//   { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
//   { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
//     rogueBonusKey: 'bob likes salmon' },
// ]

// const searchString = 'bob'

// ... magic

// console.log(found)

// { firstName: 'Bob', lastName: 'Smith', status: 'single' },
// { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
// { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
// { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
//   rogueBonusKey: 'bob likes salmon' },

Я изучил документацию, связанную с Array.filter(), и определенно могу найти решения, включающие Array.reduce() и перебирающие вещи с помощью Object.keys(obj).forEach(), но я хочу знать, есть ли краткий и эффективный способ справиться с таким нечетким поиском.

Что-то вроде этого:

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship' },
    rogueBonusKey: 'bob likes salmon' },
]

const searchString = 'Bob'

const found = people.filter((person) => {
  if (person.toString().indexOf(searchString).toLowerCase !== -1) return true
})

console.log(found)

[edit] Это определенно работает, но приемлемо ли это?

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
    rogueBonusKey: 'bob likes salmon' },
]

const searchString = 'Bob'

const found = people.filter((person) => {
  const savageMatch = JSON.stringify(person)
    .toLowerCase()
    .indexOf(searchString.toLowerCase()) !== -1

  console.log(savageMatch)
  if (savageMatch) return true
})

console.log(found)

Оптимизирован объем памяти:

const found = people.filter((person) => JSON.stringify(person)
    .toLowerCase()
    .indexOf(searchString.toLowerCase()) !== -1
)

Преобразовано в функцию:

const fuzzyMatch = (collection, searchTerm) =>
  collection.filter((obj) => JSON.stringify(obj)
    .toLowerCase()
    .indexOf(searchTerm.toLowerCase()) !== -1
)

console.log(fuzzyMatch(people, 'bob'))

Здесь есть несколько отличных ответов; до сих пор я выбрал это для своих нужд фильтрации:

const people = [
  { imageURL: 'http://www.alice.com/goat.jpeg', firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  {
    firstName: 'Ronald', lastName: 'McDonlad', status: 'relationship',
    rogueBonusKey: 'bob likes salmon'
  },
  {
    imageURL: 'http://www.bob.com/cats.jpeg', firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
    rogueBonusKey: 'bob hates salmon'
  },
]

const searchString = 'bob'

const options = {
  caseSensitive: false,
  excludedKeys: ['imageURL', 'firstName'],
}

const customFind = (collection, term, opts) => {
  const filterBy = () => {
    const searchTerms = (!opts.caseSensitive) ? new RegExp(term, 'i') : new RegExp(term)
    return (obj) => {
      for (const key of Object.keys(obj)) {
        if (searchTerms.test(obj[key]) &&
          !opts.excludedKeys.includes(key)) return true
      }
      return false
    }
  }
  return collection.filter(filterBy(term))
}

const found = customFind(people, searchString, options)

console.log(found)

Я сделал так, чтобы он поддерживал чувствительность к регистру и исключал определенные клавиши.


person agm1984    schedule 05.11.2017    source источник
comment
Ожидается ли результат для всего сопоставленного объекта или только для определенного свойства, пары значений?   -  person guest271314    schedule 05.11.2017
comment
Соответствие всему объекту   -  person agm1984    schedule 05.11.2017


Ответы (6)


Если мы предположим, что все свойства являются строками, вы можете поступить следующим образом.

const people = [
  // ...
]

const searchString = 'Bob'

const filterBy = (term) => {
  const termLowerCase = term.toLowerCase()
  return (person) =>
    Object.keys(person)
      .some(prop => person[prop].toLowerCase().indexOf(termLowerCase) !== -1)
}

const found = people.filter(filterBy(searchString))

console.log(found)

Обновление: альтернативное решение с RegExp и более олдскульным :), но в 2 раза быстрее

const people = [
  // ...
]

const searchString = 'Bob'

const escapeRegExp = (str) => // or better use 'escape-string-regexp' package
  str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")


const filterBy = (term) => {
  const re = new RegExp(escapeRegExp(term), 'i')
  return person => {
    for (let prop in person) {
      if (!person.hasOwnProperty(prop)) {
        continue;
      }
      if (re.test(person[prop])) {
        return true;
      }
    }
    return false;        
  }
}

const found = people.filter(filterBy(searchString))
person Dmitry Druganov    schedule 05.11.2017
comment
Это, безусловно, лучше, чем строгать его, уверен. - person agm1984; 05.11.2017
comment
Я только что понял, что должен также добавить исключенные ключи, чтобы это могло быть общего назначения. Ваш, похоже, должен поддерживать это довольно быстро. - person agm1984; 05.11.2017
comment
Мне нравится, как вы используете .some(). Я не знал, что такое существует. Я отмечу этот ответ как правильный после Array.some(more time has passed), предполагая, что никто больше не приходит с безумными жемчужинами данных. - person agm1984; 05.11.2017
comment
Обновлены ваши тесты последним решением: jsperf.com/fuzzy-match-objects-2 - person Dmitry Druganov; 05.11.2017
comment
Если вы удалите экранирование, это должно быть немного быстрее, чем решение @guest271314. На самом деле его решение тоже должно иметь побег. - person Dmitry Druganov; 05.11.2017
comment
Кроме того, в вашем случае проверка собственных свойств может не понадобиться, поэтому эта микрооптимизация также должна немного повысить производительность. :) - person Dmitry Druganov; 05.11.2017
comment
Ваш обновленный ответ довольно красив. У меня возникли проблемы с попыткой добавить excludedKeys к ответу guest271314. Я бы не назвал ваш более олдскульным. Циклы for in и for of поддерживают async/await. Ваш на самом деле выглядит так, как будто будет просто добавить дополнительные параметры конфигурации. Я думаю, что я могу закончить это в ближайшее время с вашими обновлениями. Я опубликую окончательный ответ, когда закончу. Ответ гостя отличный, но очень жесткий. Хотя очень поэтично. Ваш поставляет дополнительные крючки с for in. - person agm1984; 05.11.2017
comment
Я заменил ваш for in на for (const key of Object.keys(obj)) ;) - person agm1984; 05.11.2017
comment
@DmitryDruganov есть ли способ сделать то же самое, если мой массив объектов содержит также числа? - person legacy; 25.07.2021

Вы должны дать fujs шанс. http://fusejs.io/ У него есть несколько интересных настроек, таких как порог, который допускает некоторую ошибку опечатки (0,0 = идеально, 1.0 = соответствует чему угодно) и keys, чтобы указать любые ключи, по которым вы хотите выполнить поиск.

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
    rogueBonusKey: 'bob likes salmon' },
]

const fuseOptions = {
  caseSensitive: false,
  shouldSort: true,
  threshold: 0.2,
  location: 0,
  distance: 100,
  maxPatternLength: 32,
  minMatchCharLength: 1,
  keys: [
    "firstName",
    "lastName",
    "rogueBonusKey",
  ]
};


const search = (txt) => {
  const fuse = new Fuse(people, fuseOptions);
  const result = fuse.search(txt);
  return result;
}
person Doppio    schedule 06.11.2017

вам нужно отфильтровать массив, а затем отфильтровать каждый ключ в объектах, чтобы он соответствовал регулярному выражению. В этом примере проблема разбивается на отдельные функции ответственности и связывается с функциональными концепциями, например.

Тесты производительности включены, в хроме это стабильно быстрее, чем пример Дмитрия. Другие браузеры я не тестировал. Это может быть связано с оптимизацией, которую использует Chrome, чтобы позволить jit быстрее обрабатывать скрипт, когда код выражается в виде небольших функций с одной ответственностью, которые принимают только один тип данных в качестве входных данных и один тип данных в качестве выходных данных.

Из-за тестов загрузка занимает около 4 секунд.

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship', rogueBonusKey: 'bob likes salmon' },
]

// run a predicate function over each key of an object
// const hasValue = f => o => 
//  Object.keys(o).some(x => f(o[x]))
const hasValue = f => o => {
  let key
  for (key in o) {
    if (f(o[key])) return true
  }
  return false
}

// convert string to regular expression
const toReg = str => 
  new RegExp(str.replace(/\//g, '//'), 'gi')

// test a string with a regular expression
const match = reg => x => 
  reg.test(x)

// filter an array by a predicate
// const filter = f => a => a.filter(a)
const filter = f => a => {
  const ret = []
  let ii = 0
  let ll = a.length
  for (;ii < ll; ii++) {
    if (f(a[ii])) ret.push(a[ii])
  }
  return ret
}

// **magic**
const filterArrByValue = value => {
  // create a regular expression based on your search value
  // cache it for all filter iterations
  const reg = toReg(value)
  // filter your array of people
  return filter(
    // only return the results that match the regex
    hasValue(match(reg))
  )
}

// create a function to filter by the value 'bob'
const filterBob = filterArrByValue('Bob')

// ########################## UNIT TESTS ########################## //

console.assert('hasValue finds a matching value', !!hasValue(x => x === 'one')({ one: 'one' }))
console.assert('toReg is a regular expression', toReg('reg') instanceof RegExp)
console.assert('match finds a regular expression in a string', !!match(/test/)('this is a test'))
console.assert('filter filters an array', filter(x => x === true)([true, false]).length === 1)

// ##########################   RESULTS   ########################## //

console.log(
  // run your function passing in your people array
  'find bob',
  filterBob(people)
)

console.log(
  // or you could call both of them at the same time
  'find salmon',
  filterArrByValue('salmon')(people)
)

// ########################## BENCHMARKS ########################## //

// dmitry's first function
const filterBy = (term) => {
  const termLowerCase = term.toLowerCase()
  return (person) =>
    Object.keys(person)
      .some(prop => person[prop].toLowerCase().indexOf(termLowerCase) !== -1)
}

// dmitry's updated function
const escapeRegExp = (str) => // or better use 'escape-string-regexp' package
  str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")


const filterBy2 = (term) => {
  const re = new RegExp(escapeRegExp(term), 'i')
  return person => {
    for (let prop in person) {
      if (!person.hasOwnProperty(prop)) {
        continue;
      }
      if (re.test(person[prop])) {
        return true;
      }
    }
    return false;        
  }
}

// test stringify - incredibly slow
const fuzzyMatch = (collection, searchTerm) =>
  collection.filter((obj) => JSON.stringify(obj)
    .toLowerCase()
    .indexOf(searchTerm.toLowerCase()) !== -1
)

new Benchmark({ iterations: 1000000 })
  // test my function - fastest
  .add('synthet1c', function() {
    filterBob(people)
  })
  .add('dmitry', function() {
    people.filter(filterBy('Bob'))
  })
  .add('dmitry2', function() {
    people.filter(filterBy2('Bob'))
  })
  .add('guest', function() {
    fuzzyMatch(people, 'Bob')
  })
  .run()
<link rel="stylesheet" type="text/css" href="https://codepen.io/synthet1c/pen/WrQapG.css">
<script src="https://codepen.io/synthet1c/pen/WrQapG.js"></script>

person synthet1c    schedule 05.11.2017
comment
Это на самом деле очень здорово. Спасибо. Надо будет посмотреть на это поближе завтра. Мне любопытно, как это с точки зрения производительности по сравнению с обновленными решениями Гостя и Дмитрия. Я был слишком ленив сегодня вечером, чтобы попытаться сделать гигантский массив. - person agm1984; 05.11.2017
comment
Запустите фрагмент кода, чтобы увидеть разницу в производительности между примерами. - person synthet1c; 05.11.2017

Вы также можете использовать регулярные выражения с модификатором i для выполнять сопоставление без учета регистра и метод RegExp. прототип.тест()

  • Это очень удобно, когда вы хотите оценить несколько свойств объекта, например:

    new RegExp(searchString, 'i').test( 
      person.email || person.firstName || person.lastName
    )
    

Код:

const people = [{ firstName: 'Bob', lastName: 'Smith', status: 'single' }, { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' }, { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' }, { firstName: 'Sally', lastName: 'Fields', status: 'relationship' }, { firstName: 'Robert', lastName: 'Bobler', status: 'single' }, { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' }, { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship', rogueBonusKey: 'bob likes salmon' }]

const searchString = 'Bob'

const found = people.filter(({ firstName }) => 
  new RegExp(searchString, 'i').test(firstName))

console.log(found)

person Yosvel Quintero Arguelles    schedule 10.08.2020

Если весь совпадающий объект является ожидаемым результатом, вы можете использовать цикл for..of, Object.values(), Array.prototype.find()

consy searchString = "Bob";
const re = new RegExp(searchString, "i");
let res = [];
for (const props of Object.values(people))
  Object.values(props).find(prop => re.test(prop)) && res.push(props);
person guest271314    schedule 05.11.2017
comment
Спасибо, это круто. К сожалению, по какой-то причине это занимает в 2-3 раза больше времени, чем решение Дмитрия. - person agm1984; 05.11.2017
comment
@agm1984 agm1984 Можете ли вы создать jsperf для демонстрации? - person guest271314; 05.11.2017
comment
Лучшим тестом будет массив из тысяч объектов. В моем браузере первый в 2-3 раза быстрее, используя performance.now() - person agm1984; 05.11.2017
comment
@ agm1984 agm1984 В каком браузере вы пробовали код? В Chromium 61 for..of быстрее всего Array.some const filterBy = (term) =› { const termLowerCase = term.toLowerCase() return (person) => Object.keys(person) .some(prop =› person[prop ].toLowerCase().indexOf(termLowerCase) !== -1) } const found = people.filter(filterBy(searchString)) 115 822 ±3,10% 6% медленнее For of loop + Regex const re = new RegExp(searchString, i ) let res = [] for (const props of Object.values(people)) Object.values(props).find(prop =› re.test(prop)) && res.push(props) 122 775 ±3,10% самый быстрый< /я> - person guest271314; 05.11.2017
comment
Тестирование в Chrome 61.0.3163/Windows 10 0.0.0 - person agm1984; 05.11.2017
comment
В Firefox 57 Array.some был быстрее при первом запуске, медленнее при втором запуске в *nix Array.some const filterBy = (term) => { const termLowerCase = term.toLowerCase() return (person) => Object. keys(person) .some(prop =› person[prop].toLowerCase().indexOf(termLowerCase) !== -1) } const found = people.filter(filterBy(searchString)) 99 771 ±4,53% 5% медленнее Для of loop + Regex const re = new RegExp(searchString, i) let res = [] for (const props of Object.values(people)) Object.values(props).find(prop => re.test(prop)) && res.push(props) 101 249 ±0,82% быстрее - person guest271314; 05.11.2017
comment
Я наблюдаю примерно то же самое на своей машине. Кажется, что разница между обоими +/- 20% - person agm1984; 05.11.2017
comment
@agm1984 См. stackoverflow.com/questions/46516234/ - person guest271314; 05.11.2017
comment
Спасибо. По моему опыту, циклы for обычно всегда быстрее. Я работаю с вашим кодом в настоящее время. Довольно легко сделать его чувствительным к регистру или нет. - person agm1984; 05.11.2017

Вы можете использовать Array.prototype.find().

const people = [
  { firstName: 'Bob', lastName: 'Smith', status: 'single' },
  { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
  { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
  { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
  { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
  { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
  { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
    'rogueBonusKey': 'bob likes salmon' },
]

const searchString = 'Bob';

const person = people.find(({ firstName }) => firstName.toLowerCase() === searchString.toLowerCase());

console.log(person);

person xinthose    schedule 14.01.2021