diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index 07325159a..ae8ba0ed0 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -1870,14 +1870,14 @@ impl SliceableSequenceOp for PyStr { .collect::() .into(), PyKindStr::Utf8(s) => { - let char_len = (range.len() / step) + 1; + let char_len = range.len().div_ceil(step); let mut out = String::with_capacity(2 * char_len); out.extend(s.chars().skip(range.start).take(range.len()).step_by(step)); // SAFETY: char_len is accurate unsafe { Self::new_with_char_len(out, char_len) } } PyKindStr::Wtf8(w) => { - let char_len = (range.len() / step) + 1; + let char_len = range.len().div_ceil(step); let mut out = Wtf8Buf::with_capacity(2 * char_len); out.extend( w.code_points() @@ -1900,7 +1900,7 @@ impl SliceableSequenceOp for PyStr { .collect::() .into(), PyKindStr::Utf8(s) => { - let char_len = (range.len() / step) + 1; + let char_len = range.len().div_ceil(step); // not ascii, so the codepoints have to be at least 2 bytes each let mut out = String::with_capacity(2 * char_len); out.extend( @@ -1914,7 +1914,7 @@ impl SliceableSequenceOp for PyStr { unsafe { Self::new_with_char_len(out, char_len) } } PyKindStr::Wtf8(w) => { - let char_len = (range.len() / step) + 1; + let char_len = range.len().div_ceil(step); // not ascii, so the codepoints have to be at least 2 bytes each let mut out = Wtf8Buf::with_capacity(2 * char_len); out.extend( diff --git a/extra_tests/snippets/builtin_str_unicode_slice.py b/extra_tests/snippets/builtin_str_unicode_slice.py index 252f84b1c..1d35c6c48 100644 --- a/extra_tests/snippets/builtin_str_unicode_slice.py +++ b/extra_tests/snippets/builtin_str_unicode_slice.py @@ -59,3 +59,32 @@ assert hebrew_text[30:10:-3] == "אםהֱאּ " assert len(hebrew_text[30:10:-3]) == 7 assert hebrew_text[30:10:-1] == "א ,םיִהֹלֱא אָרָּב ," assert len(hebrew_text[30:10:-1]) == 20 + + +# A stepped slice whose span is an exact multiple of the step ends on the last +# character it collects rather than one past it, so the character count is the +# span divided by the step and not one more. The subject goes through a +# variable because a constant subscript is folded at compile time and would +# never reach the runtime slice at all. +def stepped(s, step): + return s[::step] + + +for subject, step, expected in [ + ("a\u00e9c", 3, "a"), + ("가나다라", 2, "가다"), + ("가나다라마바", 3, "가라"), + ("가나다라", -2, "라나"), + ("가나다라마바", -3, "바다"), + ("\U0001f600\U0001f601\U0001f602\U0001f603", 2, "\U0001f600\U0001f602"), +]: + sliced = stepped(subject, step) + assert sliced == expected, (subject, step, sliced) + assert len(sliced) == len(expected), (subject, step, len(sliced)) + # An overstated count makes the string claim characters its buffer does not + # hold, which reversed() then reads past. + assert list(reversed(sliced)) == list(expected)[::-1] + +assert len(stepped(hebrew_text, 2)) == 30 +assert len(stepped(hebrew_text, 4)) == 15 +assert len(stepped(hebrew_text, -2)) == 30