Groovy™ 俳句处理
发布时间:2023-03-25 07:22PM
这篇博客探讨了一些 Groovy 解决方案,用于 使用文本块的 Java 俳句 一文中的示例,该文章由 Donald Raab 撰写。在他的示例中,他使用了 Java 文本块,但 Groovy 已经通过其多行字符串支持类似的功能,因此我们不会就这方面做进一步阐述。
以下是 Donald 的一些创意作品
在他的示例中,他以各种方式处理了这些示例。我们将使用 Groovy 来处理相同的示例。
José Paumard 最近在 JEP Café 视频 中对这些示例进行了精彩的后续讨论。
示例 1:查找不同的字母
在此示例中,我们希望查看俳句文本中使用的所有单个字母。我们将忽略任何标点符号,并将所有字母转换为小写,因为我们不关心大小写区别。
这是 Groovy 代码
assert haiku.codePoints().toArray()
.findAll(Character::isAlphabetic)
.collect(Character::toLowerCase)
.toUnique()
.collect(Character::toString)
.join() == 'breakingthoupvmwcdflsy'
与博客和视频中的内容相比,我们做了一个微小的改动。我们使用了 codePoints()
而不是 chars()
。虽然 Donald 当前的俳句文本不包含任何代理对,但我们最好还是做好准备,以便将来出现时能够处理。您可以看到,下面的笑脸表情符号是用两个字符编码的
assert "😃".codePoints().mapToObj(Character::toString).toList()[0].size() == 2
我们相信,这些符号在某人的俳句中出现得越来越频繁只是时间问题。
示例 2:将字母拆分为唯一和重复分区
在下一个示例中,我们希望计算每个字母的出现次数,并区分多次重复的字母和只出现一次的字母。
我们将使用一个映射来存储已见到的字母(键)及其出现次数(值)。我们将创建一个条件,对于只出现一次的映射条目,该条件为真。
var uniqueAndDuplicatePartitions = e -> e.value == 1
我们使用 Groovy 的 countBy
方法创建我们的映射,然后使用 split
方法和我们之前的条件。这将把映射分成唯一和重复的集合。
assert haiku.codePoints().toArray()
.findAll(Character::isAlphabetic)
.collect(Character::toLowerCase)
.collect(Character::toString)
.countBy{ it }
.split(uniqueAndDuplicatePartitions)
*.size() == [0, 22]
当我们检查这两个集合的大小后,我们发现没有字母只出现一次,所有字母都重复出现。
示例 3:查找使用最多的字母
我们的最后一个例子是前面例子的一个变体。我们不仅要找到唯一和重复的字符,还要找到出现频率最高的前三个字母。
和以前一样,我们需要一个条件。这一次,我们将用于排序(倒序)
var byCountDescending = e -> -e.value
现在,我们只需使用我们的条件进行排序,然后取前3个。
assert haiku.codePoints().toArray()
.findAll(Character::isAlphabetic)
.collect(Character::toLowerCase)
.collect(Character::toString)
.countBy{ it }
.sort(byCountDescending)
.take(3) == [e:94, t:65, i:62]
示例 3:其他变体
我们也可以为此使用 Eclipse Collections
var top3 = Strings.asCodePoints(haiku)
.select(Character::isAlphabetic)
.collectInt(Character::toLowerCase)
.collect(Character::toString)
.toBag()
.topOccurrences(3)
[e:94, t:65, i:62].eachWithIndex{ k, v, i ->
assert top3[i] == PrimitiveTuples.pair(k, v)
}
使用 Bag
及其 topOccurrences
方法为我们完成了大部分繁重的工作。事实上,这个解决方案在存在平局时也存在行为差异,我们稍后会再讨论这个问题。
我们当然可以使用 Stream API,就像博客和视频中那样。这是 Groovy 的等效代码
assert haiku.codePoints()
.filter(Character::isAlphabetic)
.map(Character::toLowerCase)
.mapToObj(Character::toString)
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
))
.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue().reversed())
.limit(3)
.toList()
.collectEntries() == [e:94, t:65, i:62]
视频指出,上述代码在本质上是相当技术性的,因为您需要跟踪我们如何使用映射来模拟我们的问题领域,才能理解每个处理步骤在做什么。
它建议使用记录来更好地捕获少量领域模型,并使我们的代码更直观。让我们看看在 Groovy 上做同样的事情。
这里有我们将使用的三条记录
record Letter(int codePoint) {
Letter(int codePoint) {
this.codePoint = Character.toLowerCase(codePoint)
}
}
record LetterCount(int count) implements Comparable<LetterCount> {
int compareTo(LetterCount other) {
Integer.compare(this.count, other.count)
}
}
record LetterByCount(Letter letter, LetterCount count) {
LetterByCount(Letter letter, Integer count) {
this(letter, new LetterCount(count))
}
static Comparator<? super LetterByCount> comparingByCount() {
Comparator.comparing(LetterByCount::count)
}
}
现在我们的“收集”和“排序”步骤都是基于我们的领域模型的,这样更容易理解了
assert haiku.codePoints().toArray()
.findAll(Character::isAlphabetic)
.collect(Letter::new)
.countBy{ it }
.collect(LetterByCount::new)
.toSorted(LetterByCount.comparingByCount().reversed())
.take(3)
*.letter
*.codePoint
.collect(Character::toString) == ['e', 't', 'i']
视频还深入探讨了与 Eclipse Collections 版本的有趣区别。Bag 类的 topOccurrences
方法处理平局,在平局的情况下返回两次出现。前 3 次出现中没有平局,事实上前 14 次也没有,但是如果你调用 topOccurrences(15)
,则会返回 16 次出现。我们可以遵循视频中的建议,这将给出以下 Groovy 代码
var byCountReversed = e -> -e.key
assert haiku.codePoints().toArray()
.findAll(Character::isAlphabetic)
.collect(Character::toLowerCase)
.collect(Character::toString)
.countBy{ it }
.groupBy{ k, v -> v }
.sort(byCountReversed)
.take(15)
*.value.sum()*.key == ['e', 't', 'i', 'a',
'o', 'n', 's', 'r',
'h', 'd', 'w', 'l',
'u', 'm', 'p', 'c']
我们实质上是在执行两个“分组”语句,第一个作为 countBy
的一部分,然后是对值进行后续的 groupBy
。正如我们所看到的,如果我们查看前 15 个出现次数,会返回 16 个值。