我知道你说你不允许使用正则表达式,但是对于出现在这里的其他所有人,我将展示如何使用适当的工具。但是,然后我会用tr/// 做一些同样有用的事情。
编程(和数学)将看似困难的问题分解为更简单的问题的技巧之一,尤其是在您已经为简单问题找到解决方案的情况下。 (例如阅读Parnas decomposition)。
所以,问题是“我怎样才能删除所有的元音,除非它们在单词开头?” (在我把你的标题缩短一点之后)。这导致答案考虑单词,因此他们拆分输入,做了一些工作以确保他们没有处理第一个字符,然后重新组合结果。
但是,解决问题的另一种方法是“如何删除所有跟在另一个字母后面的元音?”。唯一不在另一个字母之后的字母是单词的第一个字母。
在另一个字母之后的元音的正则表达式很简单(但我会在这里坚持使用 ASCII,尽管它对于任何 Unicode 字母都一样简单):
[a-z][aeiou]
只有在第一个字母后有元音时才匹配。现在你想用任何东西替换所有这些。使用替换运算符s///。 /g 标志进行所有全局替换,/i 使其不区分大小写:
s/[a-z][aeiou]//gi;
但是,有一个问题。它还替换了那个前导字母。这很容易修复。替换中的\K 表示忽略替换中模式之前的部分。 \K 之前的任何内容都不会被替换。所以,这只会替换元音:
s/[a-z]\K[aeiou]//gi;
但是,可能有元音相邻,所以在前面的“一个或多个”项中加上 + 量词:
s/[a-z]\K[aeiou]+//gi;
你根本不需要关心文字。
其他一些方法
说一个字母必须跟在另一个字母后面有一个特殊的零宽度断言:非单词边界,\B(尽管它也将数字和下划线算作“字母”):
s/\B[aeiou]+//gi;
\K 是在 v5.10 中引入的,它确实是一个具有可变宽度后视功能的绝妙技巧。但是,这里的后视是固定宽度:它是一个字符:
s/(?<=[a-z])[aeiou]+//gi;
但是,在意文字
假设您需要分别处理每个单词,以满足其他要求。看起来您已经混合了一些类似于 Python 的代码,如果 Perl 能够做到这一点,那就太好了 :)。问题并没有太大变化,因为您可以对每个单词做同样的事情。
foreach my $word ( split /\s+/, $x ) {
.... # same thing for each word
}
但是,这里有一个有趣的转折点?你如何将它们重新组合在一起?假设这是分隔符,其他解决方案只使用一个空格。也许应该有两个空格,或制表符,或其他什么。 split 具有特殊的“分隔符保留模式”,可以保留碎片之间的任何内容。当您在 split 模式中捕获时,这些捕获值是输出列表的一部分:
my @words_and_separators = split /(\s+)/, $x;
由于您知道分隔符都不会有元音,因此您可以对它们进行替换,因为它们不会改变。这意味着您可以像对待单词一样对待它们(也就是说,没有特殊情况,这是您分解问题时要考虑的另一件事)。要获得具有原始间距的最终字符串,请在空字符串上使用 join:
my $ending_string = join '', @words_and_separators;
所以,这就是所有这些看起来如何组合在一起的方式。我将添加/r flag on the substitution so it returns the modified copy 而不是处理原始的(不要修改控制变量!):
my @words;
foreach my $word ( split /(\s+)/, $x ) {
push @words, $word =~ s/\B[aeiou]+//gr;
}
my $ending_string = join '', @words;
但是,foreach 有点烦人。这个列表管道是相同的,从下到上阅读这些更容易。每个事物都会产生一个列表,该列表流入它上面的事物。这就是我可能会用实际代码表达它的方式:
my $ending_string =
join '',
map { s/\B[aeiou]+//gr } # each item is in $_
split /(\s+)/, $x;
现在,这是大结局。如果我们不是在空格上而是在空格和每个单词的第一个字母上进行拆分呢?使用分隔符保留模式,我们知道我们只需要影响其他所有项目,所以我们像 map 一样计算它们:
my $n = 0;
my $ending_string =
join '',
map { ++$n % 2 ? tr/aeiouAEIOU//dr : $_ }
split /((?:^|\s+)[a-z])/i, $x;
但是,我不会以这种方式编写这种技术,因为最终有人会找到我并进行报复。相反,我之前觉得很烦人的 foreach 可能会安抚愤怒的群众:
my $n = 0;
foreach ( split /((?:^|\s+)[a-z])/i, $x ) {
print ++$n % 2 ? tr/aeiouAEIOU//dr : $_;
}
现在这会记住原始字符串中的实际分隔符,并留下“单词”的第一个字符,因为它不在我们要修改的元素中。
foreach 中的代码不需要使用条件运算符?: 或其他一些功能。重要的部分是跳过所有其他元素。如果您以前没有见过split 模式,那它有点令人费解,但这就是您在满足这些要求时所得到的。我认为修改子字符串的一部分很可能会在第一次阅读时绊倒人们。
我的意思是,如果他们要让你在作业中以错误的方式做事,那就用一些会占用他们一些时间的东西来反击。 :)
哦,这很有趣
我有另一个想法,因为tr/// 除了音译还有另一个任务。它也很重要。因为它返回替换的数量,如果你用它自己替换任何东西,你会得到那个东西出现的计数。你可以数元音,例如:
my $has_vowels = $string =~ tr/aeiou/aeiou/; # counts vowels
但是,对于一个字母的字符串,这意味着你有办法判断它是否是元音:
my $is_vowel = substr( $string, $i, 1 ) =~ tr/aeiou/aeiou/;
你也可以知道关于前一个字符的事情:
my $is_letter = substr( $string, $i - 1, 1 ) =~ tr/a-zA-Z/a-zA-Z/;
将它们放在一起,您可以查看任何位置并知道它是否是字母后面的元音。如果是这样,你跳过那封信。否则,您将该字母添加到输出中:
use v5.10;
$x = "I like apples more than oranges oooooranges\n";
my $output = substr $x, 0, 1; # avoid the -1 trap (end of string!)
for( my $i = 1; $i < length $x; $i++ ) {
if( substr( $x, $i, 1 ) =~ tr/aeiou/aeiou/ ) { # is a vowel
next if substr( $x, $i - 1, 1 ) =~ tr/a-zA-Z/a-zA-Z/;
}
$output .= substr $x, $i, 1;
}
say $output;
这具有使用推荐的运算符但完全绕过意图的有趣结果。但是,这是 tr/// 的正确使用方式。