我有两个数组:
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'] },
];
答案 0 :(得分:1)
您可以获得索引,并且如果-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);
答案 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
将值之前进行比较。输入场景contramap
和compose
-
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
接受两个一元函数f
和g
,并返回一个依次调用f
和g
的新函数。现在,我们已完成编写分拣器的所有工作-
posts .sort
( contramap
( ascending
, compose (Number, postHasAnyTags (tags))
)
)
需要一个小的更改。当帖子中有我们要查找的标签时,postHasAnyTags
返回true
,否则返回false
。转换为数字时,这些值分别为1
和0
。 ascending
比较器会将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 // => 2
或true - false // => 1
。通常,隐式类型转换可能是您程序中令人痛苦的bug来源,因此上述解决方案需要特别注意以执行显式类型转换,并确保不会出现奇怪的行为。 / p>
是的,您可以将排序器写为-
posts .sort
( (a, b) =>
Number (tags .some (t => b.tags .includes (t)))
- Number (tags .some (t => a.tags .includes (t)))
)
但这是一堆重复的事情,丝毫没有向读者传达其意图。在编写程序时,我们需要以允许我们通过名称隔离和捕获行为的方式来管理复杂性。通过定义诸如postHasAnyTags
和descending
之类的函数,我们能够将工作划分为合理的较小部分。这些较小的部分更易于编写,测试和维护。最重要的是,这些较小的部分可以在程序的其他区域中重复使用。与上面的超级复杂lambda相比,它很难编写,测试,维护并且绝对不能在程序的其他区域使用。
答案 2 :(得分:0)
您可以使用some
和includes
检查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
如果a
和b
都具有匹配的标签或没有标签,则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提供了一个很好的答案,说明为什么我们应该将代码分解为小的可重用函数。这只是说明了相同的过程,但对问题可能如何解决的想法有所不同。