通过与另一个数组的值比较对数组进行排序

时间:2019-05-13 16:02:48

标签: javascript arrays sorting

我有两个数组:

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'] },
];

4 个答案:

答案 0 :(得分:1)

您可以获得索引,并且如果-1Infinity将该项目排序到数组的末尾。

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);

答案 1 :(得分:1)

这是一种实用的方法。首先,我们从一些函数开始,该函数可以告诉我们给定的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

但是,不能使用<>直接比较所讨论的数据。可以使用><比较数字和字符串,但我们都没有。我们的数据是一个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之前使用f对其输入进行转换。 compose接受两个一元函数fg,并返回一个依次调用fg的新函数。现在,我们已完成编写分拣器的所有工作-

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

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

const descending = (a, b) =>
  ascending (a, b) * -1

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

就是这样。展开下面的代码片段,以在您自己的浏览器中验证结果-

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 // => 2true - false // => 1。通常,隐式类型转换可能是您程序中令人痛苦的bug来源,因此上述解决方案需要特别注意以执行显式类型转换,并确保不会出现奇怪的行为。 / p>

是的,您可以将排序器写为-

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

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

答案 2 :(得分:0)

您可以使用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将返回零。因此,它们彼此相对不动

答案 3 :(得分:0)

您没有指定匹配项的排序方式。一种可能性是按比赛的次数排序。事实证明,这很简单。我的第一遍看起来像这样:

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的函数,实际上可以使用{{ 1}}。 (有时称为有序的-或"<"-类型)。该版本可能如下所示:

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的结果更具可重用性。在这里descendBy这个名字感觉更合适:

descend

我们已经将切片和排序移到了主函数中,使const descend = (fn) => (a, b) => fn(b) - fn(a) const sortByTagMatches = (target) => (xs) => xs .slice(0) .sort (descend (countMatches (target, 'tags') ) ) 非常简单。这就是我想离开的地方。现在的代码如下所示:

descend

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

为什么?

user633183提供了一个很好的答案,说明为什么我们应该将代码分解为小的可重用函数。这只是说明了相同的过程,但对问题可能如何解决的想法有所不同。