如何确定打印到控制台时一个字符串将占据多少列?

q3qa4bjr  于 2022-10-17  发布在  Unix
关注(0)|答案(1)|浏览(155)

这个问题仅限于没有空格的字符串,这些字符串是故意编写的,以供人类阅读。
我不关心NUL或其他字符,这些字符在一篇供人类阅读的文本中找不到。
此外,我也不关心“病态”案例,比如


# !/usr/bin/env perl

use strict; use warnings;
use feature 'say';
say 'dog', "\r", 'rat';
say 'a', "\b", 'z';

例如,当文本不全是ASCII时,这个问题对于生成居中的文本行很有用。
在下面的Perl脚本中,我们首先查看占据1列的字符串,然后是2列、3列等等。
正如我们从运行这段代码中看到的那样,无论是字节数,还是通过在B处拆分字符串而创建的数组的长度,都不能可靠地告诉我们打印时一个字符串或一个字符将占据多少列。有没有办法弄到这个号码?


# !/usr/bin/env perl

use strict; use warnings;
use feature 'say';

while(<DATA>)
{
    say '------------------------------------------';
    print;
    $_=~s/\s//g;
    my@array=split /\B/,$_;
    say length $_,' bytes, ',scalar@array,' components';
}

__DATA__
a
é
ø
ü
α 
ά
∩
⊃
≈
≠
好
üb
üü
dog
Voß
café
Schwiizertüütsch
mnowg1ta

mnowg1ta1#

终端用于打印文本的列数直接由打印的“字符”数确定,其中每个字符可能需要0、1或2列来打印。
这些是逻辑Unicode字符,扩展的字素簇。它们可以是字符序列,通常是基本字符及其combining diacritical marks(重音),或者可以具有单个码点,但表示来自特定书写系统/语言的一个字符。
然后需要的是以一种尊重Unicode的方式将输入分解成字符,并找出每个字符需要多少列。(好吧,或者使用一个可以做到这一点的库。)
查看宽度的一种方法是使用正则表达式测试East_Asian_Width属性,以及p{East_Asian_Width=Wide}p{EA=W}。请参见perluniprops(+perluniintroperlunicode)。
或者,转到核心模块Unicode::UCD,它连接Unicode字符数据库并具有all属性。
这些值可以是:中性(非东亚)、宽、模糊、窄、全宽和半宽--根据上下文的不同,这些值总是可以解析为两个、窄或宽。请参阅Unicode标准附件中关于东亚宽度的所有详细信息,UAX#11,或参见上面链接的perluniprops中的列表,该列表还显示了它们被发现的频率;列出的前三个列表比其他三个列表更频繁地出现。
这个列表中最特别的是Ambiguous,它可以是宽的也可以是窄的,这取决于它的使用环境(无论它是不是东亚人),它包括了所有的细节;请参见链接。鉴于这个问题似乎有必要,我现在就不提了,把它当作狭隘的问题来处理。则需要2列的唯一属性值将是Wide
一个例子

use warnings;
use strict; 
use feature 'say';
use List::Util qw(sum);

use utf8;
use open qw(:std :encoding(UTF-8));

my @w = qw(a é ø ü α ά ∩ ⊃ ≈ ≠ 好 üb üü dog Voß café Schwiizertüütsch);

foreach my $word (@w) {
    my $cols = sum map { /\p{EA=W}/ ? 2 : 1 } split '', $word;
    say "$word needs $cols";    
}

如果我们要使用字符数据库,那么我们还需要字符的代码点,例如

use Unicode::UCD;

for my $ucp (unpack 'W', $word) {
   my $eaw = charprop($ucp, "East_Asian_Width");
   say $eaw;
}

这将生成上面列出的名称。
我们还需要为程序启用Unicode支持(这是问题中的示例程序失败的地方):utf8 pragma在那里是因为源文件本身包含Unicode字符,而open pragma负责标准流。
记住,所有这些都忽略了Ambiguous,即。我们认为它们都是狭隘的,这通常是不正确的。
改进这一点的最简单方法是使用库Unicode::GCString,有了它,事情就变得几乎微不足道了

use Unicode::GCString;

foreach my $word (@w) {
    say "$word needs ", 
        Unicode::GCString->new($word)->columns, " columns";
}

虽然这个库显然是经过深思熟虑并享有盛誉的,但这确实伴随着一个警告。在几年前的最近一次有意义的更新中,该库使用Unicode标准8.0.0(如它使用的Unicode::LineBreak中所述),这严重过时了,这可能会导致错误(参见示例here)。
与手动方法甚至没有解决宽度不明确的上下文的事实相比,这是一个nit。但这个问题上下文中的一个重要问题是,这是一个外部模块,它需要一个C库(sombok),并且似乎不再更新。
感谢tchrist提出了一些这方面的问题,从而引发了更详细的讨论。
如果这里的预期用途不涉及宽字符,那么它非常简单

while (<DATA>)
{
    s/\s//g;

    # Either of
    my @chars = split '';     
    my @egc = /(\X)/g;

    say "$_\t", 0+@chars, " chars (split), ", 0+@egc, " chars (regex, \\X)";
}

(然后提供的输入中的中文字符将不正确,但在这个用例中可能实际上不需要。)
X是匹配逻辑字符的一种方法,请参见同一页上的b{gcb}。这里不需要捕获(X),因为我们希望所有这些都匹配,所以my @egc = /X/g;就可以了。但这不会有什么坏处,如果图案中有更多的东西,人们可能会需要它,所以我放了()
请原谅我使用0+@ary表示数组大小,因为我正在尝试在显示宽度中容纳一行代码以便于阅读;为此,请务必使用scalar @ary
通过添加问题代码上方的语用标记,以及来自length的语句,我觉得很有启发意义
返回expr值的长度,单位为个字符
..。
与所有Perl字符操作一样,length通常处理逻辑字符,而不是物理字节。
(原文强调)
感谢Thomas Dickey在最初的帖子中呼吁省略字符宽度的讨论。

相关问题