You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Encountered an error when attempting to pad a string based on the number of visual characters. Currently, we rely on String#length to determine the number of characters to pad; however, this returns the number of code points, which may not be same as the number of graphemes.
We should consider how to properly handle Unicode characters comprised of multiple code points.
Related Issues
Does this issue have any related issues?
No.
Questions
Any questions for reviewers?
No.
Other
Any other information relevant to this issue? This may include screenshots, references, stack traces, sample output, and/or implementation notes.
What steps are required to reproduce the unexpected output?
In order to reproduce this bug, do the following:
attempt to pad using an input string containing surrogate pairs or by providing a string with which to pad containing surrogate pairs.
Expected Results
What are the expected results?
I might expect that I would see padding based on the number of visual characters. Currently, the implementation runs the risk of unpaired surrogates.
Actual Results
What are the actual results?
N/A
Environments
What environments are affected (e.g., Node v0.4.x, Chrome, IE 11)? If Node.js, include the npm version, operating system, and any other potentially relevant platform information.
I reproduced this with the published @stdlib/string-left-pad, @stdlib/string-right-pad, and @stdlib/string-pad packages at 0.2.3; the current develop implementations still use the same code-unit length/substrings:
I would like to address all three APIs, including truncation and the centerRight tie rule, using the existing grapheme-boundary utilities rather than a Symbol/Intl-only implementation. Before preparing the RFC required for a breaking change, should this change the existing APIs' length semantics or introduce separate grapheme-aware variants?
One edge case the contract should cover is padding that merges across a boundary: appending any number of \u0308 characters to a still produces one grapheme cluster. A loop trying to reach the requested visual length would never finish in that case. I propose explicitly documenting/rejecting padding that cannot increase the resulting grapheme count, rather than silently returning a shorter result or retrying without a bound.
Investigation and this comment were prepared with Codex/GPT-6 assistance. No PR has been opened.
I looked into this. left-pad, right-pad (public + base/), and pad all use String#length/String#substring, so length and truncation are in UTF-16 code units, not user-perceived characters. pad has the sharper version of this: its truncation can land a substring cut inside a surrogate pair and emit an unpaired surrogate, e.g. pad('a', 2, {rpad: '😀'}) → 'a\ud83d'.
@Shubham-Padkonde's comment above reproduced this precisely (thanks for the repro cases — reused them below to check my fix). I've opened #15319, which switches all five implementations to measure and truncate by grapheme cluster using this package's own num-grapheme-clusters/base/slice-grapheme-clusters, so pad('a', 2, {rpad: '😀'}) now returns 'a😀' and truncation never splits a cluster.
On the combining-mark non-termination case you raised (lpad('a', N, '̈') never actually reaching N clusters since consecutive Extend characters merge under UAX #29 GB9) — confirmed it reproduces, and I've left it as a documented, tested limitation in the PR rather than rolled a fix in, since it's a distinct question from what's reported here. I asked in the PR whether a RangeError for a non-progressing pad string is the right follow-up, and whether this should go through the RFC process as a breaking change or land as a bug fix — your call to make, not mine. Happy to take this if it's unassigned.
Activity
unworld11 commentedon Feb 28, 2025
is this still open or fixed?
Shubham-Padkonde commentedon Sep 17, 2026
I reproduced this with the published
@stdlib/string-left-pad,@stdlib/string-right-pad, and@stdlib/string-padpackages at 0.2.3; the currentdevelopimplementations still use the same code-unit length/substrings:I would like to address all three APIs, including truncation and the
centerRighttie rule, using the existing grapheme-boundary utilities rather than a Symbol/Intl-only implementation. Before preparing the RFC required for a breaking change, should this change the existing APIs' length semantics or introduce separate grapheme-aware variants?One edge case the contract should cover is padding that merges across a boundary: appending any number of
\u0308characters toastill produces one grapheme cluster. A loop trying to reach the requested visual length would never finish in that case. I propose explicitly documenting/rejecting padding that cannot increase the resulting grapheme count, rather than silently returning a shorter result or retrying without a bound.Investigation and this comment were prepared with Codex/GPT-6 assistance. No PR has been opened.
Abhist17 commentedon Sep 18, 2026
I looked into this.
left-pad,right-pad(public +base/), andpadall useString#length/String#substring, so length and truncation are in UTF-16 code units, not user-perceived characters.padhas the sharper version of this: its truncation can land asubstringcut inside a surrogate pair and emit an unpaired surrogate, e.g.pad('a', 2, {rpad: '😀'})→'a\ud83d'.@Shubham-Padkonde's comment above reproduced this precisely (thanks for the repro cases — reused them below to check my fix). I've opened #15319, which switches all five implementations to measure and truncate by grapheme cluster using this package's own
num-grapheme-clusters/base/slice-grapheme-clusters, sopad('a', 2, {rpad: '😀'})now returns'a😀'and truncation never splits a cluster.On the combining-mark non-termination case you raised (
lpad('a', N, '̈')never actually reachingNclusters since consecutiveExtendcharacters merge under UAX #29 GB9) — confirmed it reproduces, and I've left it as a documented, tested limitation in the PR rather than rolled a fix in, since it's a distinct question from what's reported here. I asked in the PR whether aRangeErrorfor a non-progressingpadstring is the right follow-up, and whether this should go through the RFC process as a breaking change or land as a bug fix — your call to make, not mine. Happy to take this if it's unassigned.