Skip to content

fix: make string padding Unicode aware - #15319

Open
Abhist17 wants to merge 2 commits into
stdlib-js:developfrom
Abhist17:feat/unicode-aware-pad
Open

Abhist17 wants to merge 2 commits into
stdlib-js:developfrom
Abhist17:feat/unicode-aware-pad

Conversation

@Abhist17

Copy link
Copy Markdown

Resolves #298.

Description

This pull request:

  • Makes left-pad, right-pad (both the public packages and their base/ implementations), and pad measure and slice by grapheme cluster (user-perceived character) instead of String#length (UTF-16 code unit), using the num-grapheme-clusters and base/slice-grapheme-clusters primitives that already exist in this package.
  • Fixes a correctness bug beyond the reported one: pad's truncation step used String#substring at a code-unit offset, which could land inside a surrogate pair and emit an unpaired surrogate (invalid UTF-16) in the output — e.g. pad('a', 2, {rpad: '😀'}) returned 'a\ud83d'. Truncating by grapheme cluster instead means this can no longer happen.
  • Adds regression tests for surrogate pairs on all five touched implementations, plus tests that exercise the shrink/truncate and centering branches of pad with astral characters to confirm no lone surrogate is produced.
  • Documents (and pins with a test) a known limitation: if pad is a bare Unicode combining mark, repeating it does not grow the grapheme cluster count — per UAX #29 GB9, a run of Extend characters with no preceding base merges into a single cluster regardless of length — so the output can fall short of the requested length. Credit to @Shubham-Padkonde, who found this exact edge case in this comment while investigating the same issue; I've verified it reproduces (lpad('a', 5, '̈') currently returns a 2-cluster result, rpad('a', 5, '̈') a 1-cluster result) and left it as a documented limitation rather than folding a fix into this PR — see the Questions section below.

Related Issues

This pull request has the following related issues:

Questions

@Shubham-Padkonde raised two things worth a maintainer call before merge:

  1. Is this a bug fix or a breaking change requiring an RFC? I've treated it as a bug fix (the issue is filed as Bug, not RFC, and code-unit-based length was never a documented design choice) and kept the diff to swapping the length metric, not new options or a separate API. But it does change output for any existing caller padding non-ASCII text, so if the maintainers see it as a breaking change under CONTRIBUTING.md's RFC process, happy to split this into an RFC first.
  2. The combining-mark non-growth case above — worth a follow-up issue with a RangeError for a pad string that can't make progress, or is documenting the limitation (as this PR does) enough?

Other

No.

Checklist

AI Assistance

When authoring the changes proposed in this PR, did you use any kind of AI assistance?

  • Yes

If you answered "yes" above, how did you use AI assistance?

  • Code generation (e.g., when writing an implementation or fixing a bug)
  • Test/benchmark generation
  • Documentation (including examples)
  • Research and understanding

Disclosure

This PR was written primarily by Claude Code.


@stdlib-js/reviewers

`left-pad`, `right-pad`, and `pad` measured length with `String#length`
(UTF-16 code units), so a surrogate pair or other multi-code-unit
grapheme counted as more than one character. In `pad`, the truncation
step went further: it sliced with `String#substring` at a code-unit
offset that could fall inside a surrogate pair, producing an unpaired
surrogate in the output.

Switch all three to measure and slice by grapheme cluster (using the
`num-grapheme-clusters` and `base/slice-grapheme-clusters` primitives
already in this package), so length always means user-perceived
characters and truncation never splits one apart.

Fixes stdlib-js#298
If `pad` is a string whose grapheme cluster count doesn't grow when
repeated -- a bare Unicode combining mark, since per UAX stdlib-js#29 GB9 a run
of Extend characters merges into one cluster no matter how many times
it repeats -- left-pad/right-pad can return a string shorter than the
requested length. Pin the current behavior with a test and note it in
both READMEs rather than leave it undocumented.
@Abhist17
Abhist17 requested a review from a team September 18, 2026 08:21
@stdlib-bot stdlib-bot added Needs Review A pull request which needs code review. First-time Contributor A pull request from a contributor who has never previously committed to the project repository. labels Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor A pull request from a contributor who has never previously committed to the project repository. Needs Review A pull request which needs code review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Algorithm to pad string is not Unicode aware

2 participants