Groovy 海库处理

作者:Paul King
发布日期:2023-11-07 07:22PM


这篇博客介绍了一些用 Groovy 解决 使用文本块的 Java 海库 文章中示例的方法。在他的示例中,他使用的是 Java 文本块,但 Groovy 已经支持类似的功能,即多行字符串,因此我们不再赘述这方面的内容。

以下是 Donald 的一些创意写作

text of Donald Raab’s haikus

在他的示例中,他以各种方式处理这些示例。我们将看看如何使用 Groovy 做相同的示例。

在最近的 JEP Café 视频 中,José Paumard 对这些示例进行了很好的后续讨论。

如果你想了解更多关于这些示例的背景信息,我们强烈建议阅读 Donald 的 博客 或观看 José 的 视频

示例 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 集合来实现这一点

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 集合版本的一个有趣的差异。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 个值。