
Width, padding, truncation and wrapping
Youzhi
Yu
University of
Chicago
Source: vignettes/width-and-layout.Rmd
width-and-layout.RmdOne character is not one column
This is the whole vignette in one comparison:
labels <- c("中文", "abcd", "日本語")
nchar(labels) # characters
#> [1] 2 4 3
cjk_width(labels) # terminal columns
#> [1] 4 4 6中文 is two characters and four columns. Unicode Annex
#11 assigns every character an East Asian Width, and the values
Wide and Fullwidth mean two columns in a
monospaced terminal (Unicode Consortium
2026a). cjk_width() reports that number.
Everything that goes wrong with CJK in a console follows from code that used the first number where it needed the second.
pad_by_char <- function(x, n) paste0(x, strrep(" ", n - nchar(x)))
cat(paste0("|", pad_by_char(labels, 8), "|"), sep = "\n")
#> |中文 |
#> |abcd |
#> |日本語 |Three lines that should be the same width, and are not. Now with the column count:
cjk_pad()
Pads to a width in columns. side names the side the
padding goes on, and pad must itself be one column wide: a
two-column pad character cannot hit an odd target, so it is rejected
rather than allowed to overshoot.
cjk_pad("中文", 10)
#> [1] "中文 "
cjk_pad("中文", 10, side = "left")
#> [1] " 中文"
cjk_pad("中文", 10, side = "both")
#> [1] " 中文 "
cjk_pad("中文", 10, pad = ".")
#> [1] "中文......"A width already exceeded is left alone rather than truncated, because padding and truncation are separate verbs on purpose:
cjk_truncate()
Cuts to a width, again in columns, appending an ellipsis that is itself counted against the budget:
cjk_truncate("我今天很開心", 8)
#> [1] "我今..."
cjk_width(cjk_truncate("我今天很開心", 8))
#> [1] 7The cut never lands inside a character. Because a CJK character is two columns, an odd budget can leave one column unused, and leaving it empty is the only correct option:
cjk_truncate("中文中文", 5, ellipsis = "")
#> [1] "中文"
cjk_width(cjk_truncate("中文中文", 5, ellipsis = ""))
#> [1] 4
cjk_wrap()
Breaks a string into lines that each fit the budget. This is where counting columns stops being sufficient, because where you are allowed to break is its own question.
Lines are filled greedily, the way a terminal fills them: each takes as much text as fits. Every line is inside the budget:
Why this is not “split every N columns”
CJK text is mostly breakable between any two characters, since there are no spaces to break at, but not everywhere. Unicode Annex #14 defines the positions (Unicode Consortium 2026b), and several are forbidden:
- a closing bracket
)may not begin a line; - a full stop
。may not begin a line; - an opening bracket
(may not end one.
A naive chunker violates all three. cjk_wrap() uses
ICU’s implementation of the algorithm, so the break positions are the
ones a typesetter would choose:
Notice that no line begins with 。 or
).
One rule that is not universal
Those three hold whatever the locale. A fourth, whether a small kana
may be separated from the character it follows, does
not. Annex #14 defines strict, normal and loose
line-breaking styles and which applies is tailored per locale: under
"en" a small kana never begins a line, and under
"ja", which uses the looser style, it may. Name the style
when the output has to be reproducible:
kana <- "きょうはとてもいいてんきですっしゃちょ"
cat(cjk_wrap(kana, 6, locale = "ja@lb=strict"))
#> きょう
#> はとて
#> もいい
#> てんき
#> ですっ
#> しゃ
#> ちょLatin runs inside the same string still break on spaces, so mixed text behaves the way you would expect from both sides:
The shared contract
All three layout verbs behave identically at the edges, which is what makes them safe inside a pipeline:
cjk_pad(c("中", NA), 6)
#> [1] "中 " NA
cjk_truncate(character(0), 6)
#> character(0)
cjk_wrap(NA, 6)
#> [1] NAA width that is Inf or outside integer
range is an error rather than a silent NA.
cjk_pad(x, Inf) is a plausible way to write “do not
truncate”, and coercing it would return missing values with only a
coercion warning:
cjk_pad("中文", Inf)
#> Error:
#> ! `width` must be finite and within integer range.Ragged recycling is an error too, naming both lengths, because a plausible vector of the wrong length is worse for a layout function than a refusal:
Ambiguous width
Some characters (Greek, Cyrillic, a few punctuation marks) are
Ambiguous in Annex #11: two columns in a legacy East Asian
font, one otherwise. There is no context-free answer, and ICU has
changed its treatment more than once. cjk_width() reports
what ICU reports on your machine. If your output must align in a
specific terminal, measure there rather than assuming: