【问题标题】:Sort an array by comparison with values of another array通过与另一个数组的值进行比较来对数组进行排序
【发布时间】:2019-05-14 04:11:55
【问题描述】:

我有两个数组:

const tags = ['one', 'two', 'three'];
let posts = [
  { tags: ['four', 'five'] },
  { tags: ['one', 'six'] },
  { tags: ['seven'] },
  { tags: ['nine', 'two'] },
];

我需要以这种方式对posts 数组进行排序:tags 数组中至少有一个标签的元素必须位于数组的开头。其余元素的顺序(没有匹配的标签)并不重要。

预期结果:

posts = [
  { tags: ['one', 'six'] },
  { tags: ['nine', 'two'] },
  { tags: ['four', 'five'] },
  { tags: ['seven'] },
];

【问题讨论】:

标签: javascript arrays sorting


【解决方案1】:

您可以使用someincludes 检查每个对象的任何标签是否存在于tags 数组中。然后减去正在比较的 2 个对象的值。

const tags = ['one', 'two', 'three'];
let posts = [
  { tags: ['four', 'five'] },
  { tags: ['one', 'six'] },
  { tags: ['seven'] },
  { tags: ['nine', 'two'] },
];

posts.sort((a, b) => 
  tags.some(t => b.tags.includes(t)) - tags.some(t => a.tags.includes(t))
)

console.log(posts)

如果a 有匹配的标签而b 没有,则compareFunction 返回-1 (false - true) 并且a 的优先级相对于b

反之则返回1

如果ab 都有匹配的标签或没有标签,compareFunction 将返回零。所以,它们相对于彼此是不动的

【讨论】:

    【解决方案2】:

    您可以获得索引,如果-1 使用Infinity 将此项排序到数组的末尾。

    const
        getIndex = array => tags.findIndex(v => array.includes(v)),
        tags = ['one', 'two', 'three'];
    
    let posts = [{ tags: ['four', 'five'] }, { tags: ['one', 'six'] }, { tags: ['seven'] }, { tags: ['nine', 'two'] }]
    
    posts.sort((a, b) => ((getIndex(a.tags) + 1) || Infinity) - ((getIndex(b.tags) + 1) || Infinity))
    
    console.log(posts);

    【讨论】:

    • 看到Infinity 以巧妙/意想不到的方式使用总是很有趣。干得好尼娜。
    【解决方案3】:

    这是一种功能性方法。首先我们从一些函数开始,它可以告诉我们给定的post 是否包含任何给定的tags -

    const postHasAnyTags = (tags = []) => (post = {}) =>
      tags .some (t => post.tags .includes (t))
    

    接下来,在插入sort 之前,我们需要一个基础比较器。这是ascending -

    const ascending = (a, b) =>
      a < b
        ? -1
        : a > b
          ? 1
          : 0
    

    但是,无法使用&lt;&gt; 直接比较有问题的数据。数字和字符串可以使用&gt;&lt; 进行比较,但我们都没有。我们的数据是一个 post 对象和一个字符串数组,我们的辅助函数 postHasAnyTags 返回一个布尔值。我们必须map 比较这些值之前。进入场景,contramapcompose -

    const contramap = (f, g) =>
      (a, b) =>
        f (g (a), g (b))
    
    const compose = (f, g) =>
      x => f (g (x))
    

    可重用实用程序允许我们以有意义的方式转换函数。 contramap 采用二元函数 f 和一元函数 g 并返回一个新的二元函数,该函数使用 g 转换其输入,然后将其传递给 fcompose 采用两个一元函数fg,并返回一个新函数,依次调用fg。我们现在已经拥有了编写分拣机的所有部分 -

    posts .sort
      ( contramap
          ( ascending 
          , compose (Number, postHasAnyTags (tags))
          )
      )
    

    需要做一点小改动。当帖子具有我们要查找的标签时,postHasAnyTags 返回true,否则返回false。转换为数字时,这些值分别为 10ascending 比较器会将1-values 放在 0-values 之后,因为1 大于0。我们实际上需要一个descending 比较器来按您想要的顺序返回数据

    const descending = (a, b) =>
      ascending (a, b) * -1
    
    posts .sort
      ( contramap
          ( descending
          , compose (Number, postHasAnyTags (tags))
          )
      )
    

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

    const postHasAnyTags = tags => post =>
      tags .some (t => post.tags .includes (t))
    
    const contramap = (f, g) =>
      (a, b) =>
        f (g (a), g (b))
        
    const compose = (f, g) =>
      x => f (g (x))
      
    const ascending = (a, b) =>
      a < b
        ? -1
        : a > b
          ? 1
          : 0
    
    const descending = (a, b) =>
      ascending (a, b) * -1
    
    const tags =
      ['one', 'two', 'three']
    
    const posts = 
      [ { tags: ['four', 'five'] }
      , { tags: ['one', 'six'] }
      , { tags: ['seven'] }
      , { tags: ['nine', 'two'] }
      ]
    
    posts .sort
      ( contramap
          ( descending
          , compose (Number, postHasAnyTags (tags))
          )
      )
      
    console .log (posts)
      

    JavaScript 是松散类型的,允许您执行诸如加减布尔值之类的操作,例如 true + true // =&gt; 2true - false // =&gt; 1。一般来说,隐式类型转换可能是您程序中的一个痛苦来源,因此上述解决方案特别注意进行显式类型转换并确保不会出现奇怪的行为。

    所以是的,你可以把分拣机写成 -

    posts .sort
      ( (a, b) =>
          Number (tags .some (t => b.tags .includes (t)))
           - Number (tags .some (t => a.tags .includes (t)))
      )
    

    但这是一团糟,并没有以任何方式将其意图传达给读者。在编写程序时,我们需要以一种允许我们通过名称隔离和捕获行为的方式来管理复杂性。通过定义postHasAnyTagsdescending 之类的函数,我们能够将工作分成合理的更小部分。这些较小的部分更容易编写、测试和维护。最重要的是,这些较小的部分可以在程序的其他领域重复使用。与上面的超复杂 lambda 相比,它很难编写、测试、维护,并且绝对不能用于程序的其他领域。

    【讨论】:

      【解决方案4】:

      您没有指定匹配项的排序方式。一种可能性是按匹配数排序。事实证明,这相当简单。我的第一关是这样的:

      const countMatches = (target) => ({tags}) =>
        tags .reduce ( (n, tag) => target .includes (tag) ? n + 1 : n, 0)
      
      const descendBy = (fn) => (xs) => 
        xs .slice (0)
           .sort( (a, b) => fn (b) - fn (a) )
      
      const sortByTagMatches = (target) => descendBy ( countMatches (target) )
      
      sortByTagMatches (tags) (posts) //=> sorted results
      
      

      countMatches

      函数countMatches 只计算目标(tags)与帖子的tags 属性之间的匹配数。在使用完成工作的最具体的代码和更通用、可重用的版本之间总是存在矛盾。但是这里比较通用函数的区别:

      const countMatches = (target, name) => (o) =>
        o[name] .reduce ( (n, x) => target .includes (x) ? n + 1 : n, 0)
      

      以及具体的:

      const countMatches = (target) => ({tags}) =>
        tags .reduce ( (n, tag) => target .includes (tag) ? n + 1 : n, 0)
      

      是如此之小——它们之间的使用差异几乎一样小——如果有任何机会我可能想在我的应用程序的其他地方重用这个功能,我会选择这个通用的.

      我们可以合并另一个简化:

      const countMatches = (target, name) => (o) =>
        o[name] .filter ( (x) => target .includes (x) ) .length
      

      这有稍微简单的代码。传递给filter 的函数无疑比传递给reduce 的函数更干净。但是有一个权衡。 filter 创建一个新的帖子数组,仅使用它来获取其length,然后将其丢弃。在热代码中,这可能是一个性能问题,但大多数情况下,当reduce 调用并不比filter 调用复杂得多时,这样做是错误的。

      descendBy

      descendBy 很简单。您将一个整数返回函数传递给它,它返回一个函数,该函数本身接受一个值数组并返回该数组的排序版本。它不会就地修改数组。如果你真的想要它,你可以删除slice 调用。这个函数是基于我经常使用的一个叫做sortBy的函数,只有减法反转才能下降。我很可能将两者都包含在一个项目中,但如果我这样做了,我可能会将 sortBy 重命名为 ascendBy 以使平行清晰。

      如果我们愿意,制作这个函数的更通用版本并不难。除了接受一个返回数字的函数,我们可以有一个接受返回任何有序值、日期、字符串、数字和任何实现valueOf 的函数的函数,本质上是任何可以使用"&lt;" 进行比较的有用的函数。 (这有时称为有序 -- 或 Ord -- 类型)。该版本可能如下所示:

      const descendBy = (fn) => (xs) => 
        xs .slice (0)
           .sort( (a, b, x = fn (a), y = fn (b) ) => x < y ? 1 : x > y ? -1 : 0 )
      

      在这里,我正在考虑是否在第一次通过时更喜欢通用版本。上面的具体一个确实更简单。如果我需要更换它,我可能会使用特定的,因为知道它与通用的兼容。

      进一步简化

      descendBy 似乎做了太多的工作。它将Ord-returning 函数转换为比较器,然后使用该比较器对列表进行排序。最好将这两个步骤分开,使descendBy 的结果更可重用。这里descend这个名字感觉更合适:

      const descend = (fn) => 
        (a, b) => fn(b) - fn(a)
      
      const sortByTagMatches = (target) => (xs) =>
        xs .slice(0) .sort (descend (countMatches (target, 'tags') ) )
      

      我们将切片和排序转移到主函数中,让descend 变得非常简单。这就是我想我会离开的地方。现在的代码如下所示:

      const countMatches = (target, name) => (o) =>
        o[name] .filter ( (x) => target .includes (x) ) .length
      
      const descend = (fn) => 
        (a, b) => fn(b) - fn(a)
      
      const sortByTagMatches = (target) => (xs) =>
        xs .slice(0) .sort (descend (countMatches (target, 'tags') ) )
      
      const tags = ['one', 'two', 'three']
      const posts = [{tags: ['four', 'five']}, {tags: ['one', 'six']}, {tags: ['seven']}, {tags: ['one', 'three', 'five']}, {tags: ['nine', 'two']}]
      
      console .log (
        sortByTagMatches (tags) (posts)
      )

      (请注意,我添加了一个带有两个匹配标签的附加帖子来演示附加的排序功能。)

      为什么?

      user633183 给出了一个很好的答案,解释了为什么我们应该将代码分解成小的可重用函数。这只是演示了相同的过程,但对问题可能如何分解的想法略有不同。

      【讨论】:

        猜你喜欢
        • 2015-10-07
        • 2017-04-27
        • 2017-08-20
        • 2018-12-12
        • 1970-01-01
        • 2021-08-28
        • 1970-01-01
        • 1970-01-01
        • 2020-07-30
        相关资源
        最近更新 更多