Найти путь к объекту во вложенном массиве объектов - PullRequest
1 голос
/ 09 мая 2019

У меня есть объект, параметры которого содержат и массив объекта. Я получаю 1 идентификатор объекта, и мне нужно найти его положение в этом беспорядке. С процедурным программированием у меня получилось работать с:

const opportunitiesById =  {
  1: [
    { id: 1, name: 'offer 1' },
    { id: 2, name: 'offer 1' }
  ],
  2: [
    { id: 3, name: 'offer 1' },
    { id: 4, name: 'offer 1' }
  ],
  3: [
    { id: 5, name: 'offer 1' },
    { id: 6, name: 'offer 1' }
  ]
};

const findObjectIdByOfferId = (offerId) => {
  let opportunityId;
  let offerPosition;
  const opportunities = Object.keys(opportunitiesById);

  opportunities.forEach(opportunity => {
    const offers = opportunitiesById[opportunity];

    offers.forEach((offer, index) => {
      if (offer.id === offerId) {
        opportunityId = Number(opportunity);
        offerPosition = index;
      }
    })
  });

return { offerPosition, opportunityId };
}

console.log(findObjectIdByOfferId(6)); // returns { offerPosition: 1, opportunityId: 3 }

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

R.findIndex(R.propEq('id', offerId))(opportunitiesById[1]);

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

Спасибо за любую помощь

Ответы [ 3 ]

2 голосов
/ 10 мая 2019

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

Вот способ, которым вы можете сделать это с помощью взаимной рекурсии.Сначала мы пишем findPath -

const identity = x =>
  x

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

Если вход является объектом, мы передаем его в функцию поиска пользователя f.Если функция поиска пользователя возвращает true, совпадение найдено, и мы возвращаем path.Если совпадений нет, мы ищем каждую пару ключ / значение объекта, используя вспомогательную функцию.В противном случае, если входные данные не объект, совпадения нет и ничего не осталось искать, поэтому верните undefined.Мы пишем помощник, findPath1 -

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

Если пары ключ / значение были исчерпаны, искать нечего, поэтому верните undefined.В противном случае у нас есть ключ k и значение v;добавьте k к пути и рекурсивно найдите v для соответствия.Если совпадения нет, рекурсивно ищите оставшиеся ключи / значения, more, используя тот же path.

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

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

findPath (offer => offer.id === 6, opportunitiesById)
// [ '3', '1' ]

Возвращенный путь ведет нас к объекту, который мы хотели найти -

opportunitiesById['3']['1']
// { id: 6, name: 'offer 1' }

Мы можем специализировать findPath, чтобы сделать интуитивно понятным findByOfferId function -

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

opportunitiesById['2']['0']
// { id: 3, name: 'offer 1' }

Как и Array.prototype.find, возвращается undefined, если совпадение не найдено -

findByOfferId (99, opportunitiesById)
// undefined

Разверните фрагмент ниже, чтобы проверить результаты в своем собственном браузере-

const identity = x =>
  x

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (opportunitiesById['2']['0'])
// { id: 3, name: 'offer 1' }

console .log (findByOfferId (99, opportunitiesById))
// undefined

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


Ответ Скотта вдохновил меня на попытку реализации с использованием генераторов.Мы начинаем с findPathGen -

const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

И, используя взаимную рекурсию, как мы делали в прошлый раз, мы вызываем помощник findPathGen1 -

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

Наконец, мы можем реализовать findPath и специализация findByOfferId -

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

Работает так же -

findPath (offer => offer.id === 3, opportunitiesById)
// [ '2', '0' ]

findPath (offer => offer.id === 99, opportunitiesById)
// undefined

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

findByOfferId (99, opportunitiesById)
// undefined

И в качестве бонуса, мы можем легко реализовать findAllPaths, используя Array.from -

const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById)
// [ [ '2', '0' ], [ '3', '1' ] ]

Проверьте результаты, развернув фрагмент ниже

const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))


const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (findByOfferId (99, opportunitiesById))
// undefined

// --------------------------------------------------
const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

console .log (findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById))
// [ [ '2', '0' ], [ '3', '1' ] ]
2 голосов
/ 10 мая 2019

Я бы преобразовал ваш объект в пары.

Так, например, преобразовав это:

{ 1: [{id:10}, {id:20}],
  2: [{id:11}, {id:21}] }

в это:

[ [1, [{id:10}, {id:20}]],
  [2, [{id:11}, {id:21}]] ]

Затем вы можете итерировать по этомумассив и уменьшить каждый массив предложений до индекса предложения, которое вы ищете.Скажем, вы ищете предложение № 21, приведенный выше массив будет выглядеть так:

[ [1, -1],
  [2,  1] ]

Затем вы возвращаете первый кортеж, второй элемент которого не равен -1:

[2, 1]

Вот как я бы предложил это сделать:

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const findOfferPath = (id, offers) =>
  pipe(
    toPairs,
    transduce(
      compose(
        map(over(lensIndex(1), findIndex(propEq('id', id)))),
        reject(pathEq([1], -1)),
        take(1)),
      concat,
      []))
    (offers);


console.log(findOfferPath(21, opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {pipe, transduce, compose, map, over, lensIndex, findIndex, propEq, reject, pathEq, take, concat, toPairs} = R;</script>

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

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const updateOffer = (path, update, offers) =>
  over(lensPath(path), assoc('name', update), offers);

console.log(updateOffer(["2", 1], '?', opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {over, lensPath, assoc} = R;</script>
1 голос
/ 11 мая 2019

Вот еще один подход:

Начнем с этой функции генератора:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
} 

, который можно использовать для поиска всех путей в объекте:

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

;[...getPaths(obj)]
//~> [[], ["a"], ["a", "x"], ["a", "y"], ["b"], ["b", "c"], ["b", "d"], 
//    ["b", "d", "x"], ["b", "e"], ["b", "e", "f"], ["b", "e", "f", "x"], 
//    ["b", "e", "f", "g"], ["b", "e", "f", "g", "x"]]

и затем, с помощью этой маленькой вспомогательной функции:

const path = (ps, o) => ps.reduce((o, p) => o[p] || {}, o)

мы можем написать

const findPath = (predicate, o) =>
  [...getPaths(o)] .find (p => predicate (path (p, o) ) )

который мы можем назвать как

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

Затем мы можем использовать эти функции для написания простой версии вашей функции:

const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined

Тривиально расширить это, чтобы получить все пути, для которых значение удовлетворяет предикату, просто заменив find на filter:

const findAllPaths = (predicate, o) =>
  [...getPaths(o)] .filter (p => predicate (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //=> [["b","d"],["b","e","f","g"]]

Хотя все это вызывает озабоченность. Даже если findPath нужно только найти первое совпадение, и хотя getPaths является генератором и, следовательно, ленивым, мы запускаем его полный цикл с [...getPaths(o)]. Так что, возможно, стоит использовать эту более уродливую, более императивную версию:

const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

Вот как все это выглядит вместе:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
}

const path = (ps, o) => ps.reduce ((o, p) => o[p] || {}, o)

   
// const findPath = (pred, o) =>
//   [...getPaths(o)] .find (p => pred (path (p, o) ) )


const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

const findAllPaths = (pred, o) =>
  [...getPaths(o)] .filter (p => pred (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //~> [["b","d"],["b","e","f","g"]]


const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined

Еще одно краткое замечание: порядок, в котором сгенерированы пути, является единственной возможностью. Если вы хотите перейти с предзаказ на пост-заказ , вы можете переместить строку yield p в getPaths с первой строки на последнюю.


Наконец, вы спросили об этом с помощью функциональных приемов и упомянули Рамду. Как показывает решение customcommander, вы можете сделать это с Рамдой. И (как всегда отлично) ответ пользователя 633183 демонстрирует, что это можно сделать в основном функциональными методами.

Я все еще нахожу это несколько более простым подходом. Престижность customcommander для поиска версии Ramda, потому что Ramda не особенно подходит для рекурсивных задач, но все же очевидный подход к чему-то, что должно посещать узлы рекурсивной структуры, такой как объект JS, заключается в использовании рекурсивного алгоритма. Я один из авторов Ramda, и я даже не пытался 1057 * понять, как работает это решение.

Обновление

user633183 указал, что это будет проще и лениво:

const findPath = (predicate, o) => {
  for (const p of getPaths(o)) 
    if (predicate (path (p, o)) ) 
      return p
}
...