Skip to content

Commit fd6f0da

Browse files
committed
Fix string.drop_start on JavaScript with multi-byte characters
The JavaScript target reused unsafe_byte_slice with Erlang byte offsets, but JS strings are indexed by UTF-16 code units, so drop_start returned wrong results for multi-byte input. Add a target-specific implementation that slices off the grapheme prefix using code-unit lengths.
1 parent 64cbd6a commit fd6f0da

4 files changed

Lines changed: 26 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Fixed a bug where `string.drop_start` would return incorrect results on
6+
JavaScript when the string contained multi-byte characters.
7+
38
## v1.0.4 - 2026-05-30
49

510
- Fix a bug where dicts and sets with hash collisions but equal entries would

src/gleam/string.gleam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ pub fn slice(from string: String, at_index idx: Int, length len: Int) -> String
200200
fn grapheme_slice(string: String, index: Int, length: Int) -> String
201201

202202
@external(erlang, "binary", "part")
203-
@external(javascript, "../gleam_stdlib.mjs", "string_byte_slice")
204203
fn unsafe_byte_slice(string: String, index: Int, length: Int) -> String
205204

206205
/// Drops contents of the first `String` that occur before the second `String`.
@@ -227,6 +226,7 @@ pub fn crop(from string: String, before substring: String) -> String
227226
/// assert drop_start(from: "The Lone Gunmen", up_to: 2) == "e Lone Gunmen"
228227
/// ```
229228
///
229+
@external(javascript, "../gleam_stdlib.mjs", "string_drop_start")
230230
pub fn drop_start(from string: String, up_to num_graphemes: Int) -> String {
231231
case num_graphemes <= 0 {
232232
True -> string

src/gleam_stdlib.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,13 @@ export function length(data) {
199199
return data.length;
200200
}
201201

202-
export function string_byte_slice(string, index, length) {
203-
return string.slice(index, index + length);
202+
export function string_drop_start(string, num_graphemes) {
203+
if (num_graphemes <= 0) {
204+
return string;
205+
}
206+
207+
const prefix = string_grapheme_slice(string, 0, num_graphemes);
208+
return string.slice(prefix.length);
204209
}
205210

206211
export function string_grapheme_slice(string, idx, len) {

test/gleam/string_test.gleam

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,19 @@ pub fn drop_start_3499_test() {
471471
assert string.drop_start("\r]", 1) == "]"
472472
}
473473

474+
pub fn drop_start_multibyte_test() {
475+
// https://github.com/gleam-lang/stdlib/issues/924
476+
assert string.drop_start("广州abcdefghijklmn", 0) == "广州abcdefghijklmn"
477+
assert string.drop_start("广州abcdefghijklmn", 1) == "州abcdefghijklmn"
478+
assert string.drop_start("广州abcdefghijklmn", 2) == "abcdefghijklmn"
479+
assert string.drop_start("广州abcdefghijklmn", 3) == "bcdefghijklmn"
480+
}
481+
482+
pub fn drop_start_grapheme_cluster_test() {
483+
assert string.drop_start("👶🏿abc", 1) == "abc"
484+
assert string.drop_start("e\u{0301}abc", 1) == "abc"
485+
}
486+
474487
pub fn drop_end_basic_test() {
475488
assert string.drop_end("gleam", up_to: 2) == "gle"
476489
}

0 commit comments

Comments
 (0)