带有非英文符号的 sprintf 填充

sprintf padding with non English symbols

我遇到了非英语符号的奇怪 sprintf() 行为。我尝试填充字符串,但得到了意想不到的结果:

lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
       function(x) sprintf("%-20s: %s", x, "VALUE"))
#> [[1]]
#> [1] "ZZZ                 : VALUE"
#> 
#> [[2]]
#> [1] "ZZZZZZ              : VALUE"
#> 
#> [[3]]
#> [1] "ЯЯЯ              : VALUE"
#> 
#> [[4]]
#> [1] "ЯЯЯЯЯЯ        : VALUE"
#> 

任何人都可以解释为什么会发生这种情况以及如何解决它?

会话信息可能有用:

R version 3.2.2 (2015-08-14)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Arch Linux

locale:
[1] LC_CTYPE=ru_RU.UTF-8       LC_NUMERIC=C               LC_TIME=ru_RU.UTF-8        LC_COLLATE=C              
[5] LC_MONETARY=ru_RU.UTF-8    LC_MESSAGES=ru_RU.UTF-8    LC_PAPER=ru_RU.UTF-8       LC_NAME=C                 
[9] LC_ADDRESS=C               LC_TELEPHONE=C             LC_MEASUREMENT=ru_RU.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
[1] shiny_0.12.2      R6_2.1.1          rsconnect_0.4.1.4 htmltools_0.2.6   tools_3.2.2       Rcpp_0.12.2       digest_0.6.8     
[8] xtable_1.8-0      httpuv_1.3.3      mime_0.4 

我可以告诉你为什么会这样,但不能告诉你如何解决。来自 docs for sprintf:

Field widths and precisions of %s conversions are interpreted as bytes, not characters, as described in the C standard.

在 UTF-8 中,字符 Я 是两个字节 (0xD0 0xAF),因此 "ЯЯЯ" 是 6 个字节,而 "ZZZ" 是 3 个字节,并且 sprintf 呈现他们相应地。

编辑

一种解决方法是使用 sprintf 的星号功能,它可以让您声明字段的宽度(以字节为单位),以及 nchar 函数,它可以让您计算显示宽度和字符串中的字节数。

因此,例如,nchar("ЯЯЯ", "width")nchar("ЯЯЯ", "bytes") return 分别为 3 和 6。如果我们想将它的宽度填充到 20 个显示字符,那么我们必须给 sprintf 一个 23 字节的宽度:20 加上字节数减去显示宽度。

sprintf("%-*s", 23, "ЯЯЯ")
#> [1] "ЯЯЯ                 "

或:

str <- "ЯЯЯ"
pad.len <- 20 + nchar(str, "bytes") + nchar(str, "width")
sprintf("%-*s", pad.len, str)
#> [1] "ЯЯЯ                 "

这也适用于 "ZZZ",因为字节数和显示宽度相等,所以结果为 20:

pad <- function(str) {
  pad.len <- 20 + nchar(str, "bytes") - nchar(str, "width")
  return(sprintf("%-*s: %s", pad.len, str, "VALUE"))
}

print(lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"), pad))
#> [[1]]
#> [1] "ZZZ                 : VALUE"
#> 
#> [[2]]
#> [1] "ZZZZZZ              : VALUE"
#> 
#> [[3]]
#> [1] "ЯЯЯ                 : VALUE"
#> 
#> [[4]]
#> [1] "ЯЯЯЯЯЯ              : VALUE"

P.S。这是我编写的第一个 R 代码,所以如果您发现任何改进方法,请随时发表评论。

我从 stringi 包中找到了带有 stri_pad_right() 函数的解决方案:

lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
       function(x) paste0(stringi::stri_pad_right(x, 20), ": VALUE"))
#> [[1]]
#> [1] "ZZZ                 : VALUE"
#> 
#> [[2]]
#> [1] "ZZZZZZ              : VALUE"
#> 
#> [[3]]
#> [1] "ЯЯЯ                 : VALUE"
#> 
#> [[4]]
#> [1] "ЯЯЯЯЯЯ              : VALUE"
#> 

更新

基于@Jordan 答案的另一种解决方案仅使用基本 R 函数:

str_pad <- function(str, width = floor(0.9 * getOption("width")),
                    side = c("left", "both", "right")) {
    side <- match.arg(side)
    asc <- iconv(str, "latin1", "ASCII")
    ind <- is.na(asc) | asc != str
    if (any(ind)) 
        width <- width + nchar(str, "bytes") - nchar(str, "width")
    switch(side, left = sprintf("%-*s", width, str),
           right = sprintf("%*s", width, str),
           both = sprintf("%-*s", width, sprintf("%*s", floor(width/2), str)))
}
lapply(c("ZZZ", "ZZZZZZ", "ЯЯЯ", "ЯЯЯЯЯЯ"),
       function(x) paste0(str_pad(x, 20), ": VALUE"))
#> [[1]]
#> [1] "ZZZ                 : VALUE"
#> 
#> [[2]]
#> [1] "ZZZZZZ              : VALUE"
#> 
#> [[3]]
#> [1] "ЯЯЯ                 : VALUE"
#> 
#> [[4]]
#> [1] "ЯЯЯЯЯЯ              : VALUE"
#>