在对象嵌套数组中查找对象的路径

时间:2019-05-09 19:08:18

标签: functional-programming ramda.js

我有一个对象,该对象的参数包含和对象数组。我收到1个对象ID,我需要在整个混乱中找到它的位置。通过过程编程,我可以使用它:

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 个答案:

答案 0 :(得分:2)

我会将您的对象转换成对。

例如,将其转换为

{ 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 :(得分:2)

可以使用许多小功能将它们拼凑在一起,但我想向您展示如何以更直接的方式编码您的意图。该程序还有一个好处,它将立即返回。也就是说,找到匹配项后,不会继续通过其他键/值对进行搜索。

这是使用相互递归的一种方法。首先,我们写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功能-

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


在此related Q&A中,我演示了一个递归搜索函数,该函数返回匹配的对象,而不是匹配的路径。还有其他有用的花絮,所以我建议您看一下。


Scott的回答激发了我尝试使用生成器的实现。我们从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 :(得分:1)

这是另一种方法:

我们从此生成器功能开始:

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


另一个简短说明:生成路径的顺序只是一种可能。如果要从pre-order更改为post-order,可以将yield p中的getPaths行从第一行移到最后一行。


最后,您询问了有关使用功能技术进行此操作的问题,并提到了Ramda。如customcommander的解决方案所示,您可以 使用Ramda做到这一点。并且user633183的(一如既往的出色)答案表明,可以通过主要的功能技术来做到这一点。

我仍然发现这是一种较为简单的方法。对于customcommander来说,要找到Ramda版本是很礼貌的,因为Ramda并不是特别适合于递归任务,但是对于必须访问诸如JS对象之类的递归结构的节点的明显方法仍然是使用递归算法。我是Ramda的作者之一,甚至没有尝试来了解该解决方案的工作原理。

更新

user633183指出这会更简单,而且仍然很懒:

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