【问题标题】:Find path to object in object nested array在对象嵌套数组中查找对象的路径
【发布时间】:2019-09-27 16:25:23
【问题描述】:

我有一个对象,其中的参数包含对象数组。我收到 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]);

我需要知道路径的原因是因为我需要使用新数据修改该报价并将其更新回原来的位置。

感谢您的帮助

【问题讨论】:

    标签: functional-programming ramda.js


    【解决方案1】:

    可以使用许多小函数将其拼凑在一起,但我想向您展示如何以更直接的方式编码您的意图。这个程序有一个额外的好处,它会立即返回。即,它不会在找到匹配项后继续搜索其他键/值对。

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

    展开下面的sn-p,在自己的浏览器中验证结果-

    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
    

    作为奖励,我们可以使用Array.from 轻松实现findAllPaths -

    const findAllPaths = (f = identity, o = {}) =>
      Array .from (findPathGen (f, o))
    
    findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById)
    // [ [ '2', '0' ], [ '3', '1' ] ]
    

    通过展开下面的sn-p来验证结果

    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' ] ]

    【讨论】:

    • 感谢您的回答。即使你比我目前对函数式编程的理解高出几个层次,这也很有启发性。
    • @GotTheFeverMedia 如果你了解函数组成、换能器和镜头,我认为这里提供的findPath 应该没有问题。没有隐藏的依赖关系,您可以使用简单的铅笔和纸方法进行评估。如果您有任何具体问题,请询问:)
    • @GotTheFeverMedia,强烈建议:如果你想学习函数式技术,请仔细研究这个答案。可以从中学到很多东西。然后去看看user633183的其他一些答案。首先尝试超越不寻常的代码布局来了解它的含义。但是,也分析一下该布局背后的逻辑;这是一种非常强大的风格。我从她的帖子中学到的东西比 StackOverflow 上的任何人都多。
    • 谢谢你的好话,斯科特。 @GotTheFeverMedia 如果有任何额外的解释会有所帮助,我很乐意提供。
    • 感谢你们的帮助。我正在研究它
    【解决方案2】:

    这是另一种方法:

    我们从这个生成器函数开始:

    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,可以将getPaths中的yield p行从第一行移到最后一行。


    最后,您询问了如何使用函数式技术进行此操作,并提到了 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
    }
    

    【讨论】:

    • 感谢您的精彩解释,该过程很容易理解。受您工作的启发,我在答案中添加了一个生成器实现:D
    • [...getPaths(o)].find 有一个弱点,因为在find 开始之前,所有路径都被急切地计算出来。与程序的其余部分兼容的解决方案可能是创建一个在生成器上运行的 find 函数,而不是使用 Array 的 find
    • @user633183:有一个版本的getPaths 可以解决这个问题,但你说得对,调整后的find 会是更好的选择。
    • 很好的更新,斯科特。您手动处理迭代器而不是使用for (const p of getPaths(o)) ... 是否有特殊原因?
    • 更新后的实现看起来超级干净。我还了解到,只要您不使用休息模式,就可以使用解构赋值从生成器中懒惰地获取值,这会导致迭代器单步执行直到耗尽。我在答案中的first 中使用了这种技术,它从可迭代对象中获取第一个值,但忽略其余部分。这在第一个值出现后有效地暂停了迭代器。与您合作总是很愉快。干杯。
    【解决方案3】:

    我会把你的对象变成成对的。

    所以例如转换这个:

    { 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>

    【讨论】:

    • 谢谢。我真的把它插了进去,它工作了。迷人的。我现在将尝试了解它是如何工作的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 2015-02-12
    • 2017-12-24
    • 1970-01-01
    • 2021-07-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多