Groovy™ 俳句处理

作者: Paul King

发布时间:2023-03-25 07:22PM


这篇博客探讨了一些 Groovy 解决方案,用于 使用文本块的 Java 俳句 一文中的示例,该文章由 Donald Raab 撰写。在他的示例中,他使用了 Java 文本块,但 Groovy 已经通过其多行字符串支持类似的功能,因此我们不会就这方面做进一步阐述。

以下是 Donald 的一些创意作品

text of Donald Raab’s haikus

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

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

如果您想了解有关这些示例的更多背景信息,我们强烈建议您阅读 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 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 个值。