Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 28 – String patterns

Strings in five languages

What length and indexing count in Python, JavaScript, Java, C++ and Go, how to build strings in linear time, and how to reverse text with emoji safely.

  • Beginner
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 35 minutes

What you will learn

  • Predict what length and indexing count in each language, whether code points, UTF-16 code units or bytes
  • Build a string in linear time with join, StringBuilder, strings.Builder or an appending std::string
  • Handle accents, emoji and flags when you reverse or compare text, without splitting a character
  • Check which alphabet the input uses before choosing a unit, and state the unit your code counts
  • Rebuild a line from its words and gaps in linear time

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict what length and indexing count in each language, whether code points, UTF-16 code units or bytes
  • Build a string in linear time with join, StringBuilder, strings.Builder or an appending std::string
  • Handle accents, emoji and flags when you reverse or compare text, without splitting a character
  • Check which alphabet the input uses before choosing a unit, and state the unit your code counts
  • Rebuild a line from its words and gaps in linear time

Before you start

On this page

Python, JavaScript, Java, C++ and Go all store text, but each counts it in its own unit. A Python str is a sequence of Unicode code points. A JavaScript or Java string is a sequence of UTF-16 code units, where most characters take one unit and most emoji take two. A Go string and a C++ std::string are sequences of bytes, which usually hold UTF-8. So len, length and size can return four different numbers for the same name, and s[i] hands back a whole character in one language, half an emoji in another and a single byte in a third. Four of the five also make strings immutable, which turns s = s + piece inside a loop into a copy of everything built so far. This lesson runs the same strings through all five languages: it measures them, times slow and fast ways to build them, and reverses six names, most of which come out wrong until the last fix.

One text, four lengths

Unicode gives every character a number, its code point, written U+0041 for A and U+1F600 for 😀. A string stores its code points in an encoding. UTF-8 spends one to four bytes on each: one on an ASCII character, two on é (U+00E9), three on a Devanagari letter and four on 😀, following the ranges that RFC 3629 sets out. UTF-16 spends one 16-bit code unit on every code point up to U+FFFF and two, called a surrogate pair, on every code point above it, which is where most emoji are.

A third level sits on top of the code points. What a reader takes for one character can be several of them: a letter followed by a combining accent, a Devanagari consonant with its vowel sign, an emoji with a skin-tone modifier, or a flag spelled with two regional indicator letters. Unicode Standard Annex #29 calls these user-perceived characters grapheme clusters and gives the rules that find their boundaries. The diagram measures one short text all four ways.

The text é followed by a smiling emoji, counted four ways: 2 characters, 3 code points, 4 UTF-16 code units and 7 UTF-8 bytes.2 characters (grapheme clusters)3 code points: Python's len4 UTF-16 code units: Java, JavaScript7 UTF-8 bytes: Go len, C++ size()é😀U+0065eU+0301accentU+1F600emoji00650301D83DDE0065CC81F09F9880

One text, four lengths

Text description of the diagram

The diagram counts one short text four ways, in four rows from top to bottom. The text is the letter é followed by a smiling-face emoji, and the é is written as two code points: the letter e and a combining acute accent.

  1. A reader sees 2 characters, which Unicode calls grapheme clusters: é and the emoji.
  2. Underneath are 3 code points: U+0065 (the letter e), U+0301 (the combining accent) and U+1F600 (the emoji). Python's len counts these.
  3. In UTF-16 the text takes 4 code units: 0065, 0301, D83D and DE00. The emoji is above U+FFFF, so it needs a surrogate pair, D83D followed by DE00. Java's and JavaScript's length count these.
  4. In UTF-8 the text takes 7 bytes: 65 for the e, CC 81 for the accent and F0 9F 98 80 for the emoji. Go's len and C++'s size() count these.

The same text is 2, 3, 4 or 7 long depending on which unit a language counts.

Each language’s length counts one of these units, and its index operator returns one of them:

Language Length counts Index gives
Python code points code point
JavaScript UTF-16 units code unit
Java UTF-16 units code unit
C++ bytes byte
Go bytes byte

To walk the code points instead, write for ch in s in Python, for (const ch of s) in JavaScript, s.codePoints() in Java and for i, r := range s in Go. The C++ standard library has nothing for it, so you decode UTF-8 yourself, as the function by_code_point in the C++ reversal further down does.

Here are six strings measured in every language. Press Run on the Python or JavaScript tab to measure them yourself; the Java, C++ and Go programs come with the terminal session that built and ran them.

Six strings measured in Python and JavaScript

Python · py/lengths.py

"""What len() counts in Python (code points), next to the size of the same text in UTF-16 and in UTF-8."""

import unicodedata

SAMPLES = [
    ("Asha", "plain ASCII"),
    ("Jos\u00e9", "é as one code point, U+00E9"),
    ("Jose\u0301", "e followed by U+0301, a combining acute accent"),
    ("\u0928\u0947\u0939\u093e", "Neha in Devanagari: two letters, each with a vowel sign"),
    ("\U0001F600", "one emoji, U+1F600"),
    ("\U0001F1EE\U0001F1F3", "a flag: the regional indicator letters I and N"),
]

print("len  UTF-16  UTF-8  text")
for text, note in SAMPLES:
    utf16_units = len(text.encode("utf-16-le")) // 2  # two bytes per UTF-16 code unit
    utf8_bytes = len(text.encode("utf-8"))
    print(f"{len(text):3}  {utf16_units:6}  {utf8_bytes:5}  {text} ({note})")

composed, decomposed = SAMPLES[1][0], SAMPLES[2][0]
print("an index gives one code point:", ascii(composed[3]), ascii(SAMPLES[4][0][0]))
print("the two Josés are equal:", composed == decomposed)
nfc = unicodedata.normalize
print("equal after NFC normalization:", nfc("NFC", composed) == nfc("NFC", decomposed))

Output

len  UTF-16  UTF-8  text
  4       4      4  Asha (plain ASCII)
  4       4      5  José (é as one code point, U+00E9)
  5       5      6  José (e followed by U+0301, a combining acute accent)
  4       4     12  नेहा (Neha in Devanagari: two letters, each with a vowel sign)
  1       2      4  😀 (one emoji, U+1F600)
  2       4      8  🇮🇳 (a flag: the regional indicator letters I and N)
an index gives one code point: '\xe9' '\U0001f600'
the two Josés are equal: False
equal after NFC normalization: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lengths.py

JavaScript · js/lengths.mjs

// What .length counts in JavaScript (UTF-16 code units), next to the code points that the string iterator walks over.
const samples = [
  ['Asha', 'plain ASCII'],
  ['Jos\u00e9', 'é as one code point, U+00E9'],
  ['Jose\u0301', 'e followed by U+0301, a combining acute accent'],
  ['\u0928\u0947\u0939\u093e', 'Neha in Devanagari: two letters, each with a vowel sign'],
  ['\u{1F600}', 'one emoji, U+1F600'],
  ['\u{1F1EE}\u{1F1F3}', 'a flag: the regional indicator letters I and N'],
];

console.log('length  code points  text');
for (const [text, note] of samples) {
  console.log(`${String(text.length).padStart(6)}  ${String([...text].length).padStart(11)}  ${text} (${note})`);
}

const smile = samples[4][0];
console.log('an index gives one code unit:', JSON.stringify(smile[0]), 'codePointAt(0):', smile.codePointAt(0).toString(16));
const composed = samples[1][0];
const decomposed = samples[2][0];
console.log('the two Josés are equal:', composed === decomposed);
console.log('equal after NFC normalization:', composed.normalize('NFC') === decomposed.normalize('NFC'));

Output

length  code points  text
     4            4  Asha (plain ASCII)
     4            4  José (é as one code point, U+00E9)
     5            5  José (e followed by U+0301, a combining acute accent)
     4            4  नेहा (Neha in Devanagari: two letters, each with a vowel sign)
     2            1  😀 (one emoji, U+1F600)
     4            2  🇮🇳 (a flag: the regional indicator letters I and N)
an index gives one code unit: "\ud83d" codePointAt(0): 1f600
the two Josés are equal: false
equal after NFC normalization: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lengths.mjs

The same six strings in Java, C++ and Go

Java · compiled/Lengths.java

// What length() counts in Java (UTF-16 code units), next to codePointCount and the size of the text in UTF-8.
// A Unicode escape in Java names one UTF-16 code unit, so the emoji and the flag are written as surrogate pairs.
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;

public class Lengths {
    public static void main(String[] args) {
        String[][] samples = {
            {"Asha", "plain ASCII"},
            {"Jos\u00e9", "é as one code point, U+00E9"},
            {"Jose\u0301", "e followed by U+0301, a combining acute accent"},
            {"\u0928\u0947\u0939\u093e", "Neha in Devanagari: two letters, each with a vowel sign"},
            {"\uD83D\uDE00", "one emoji, U+1F600"},
            {"\uD83C\uDDEE\uD83C\uDDF3", "a flag: the regional indicator letters I and N"},
        };
        System.out.println("length  code points  UTF-8  text");
        for (String[] sample : samples) {
            String text = sample[0];
            System.out.printf("%6d  %11d  %5d  %s (%s)%n", text.length(), text.codePointCount(0, text.length()),
                    text.getBytes(StandardCharsets.UTF_8).length, text, sample[1]);
        }
        String smile = samples[4][0];
        System.out.println("charAt(0) gives one code unit: " + Integer.toHexString(smile.charAt(0))
                + ", codePointAt(0): " + Integer.toHexString(smile.codePointAt(0)));
        String composed = samples[1][0];
        String decomposed = samples[2][0];
        System.out.println("the two Josés are equal: " + composed.equals(decomposed));
        System.out.println("equal after NFC normalization: " + Normalizer.normalize(composed, Normalizer.Form.NFC)
                .equals(Normalizer.normalize(decomposed, Normalizer.Form.NFC)));
    }
}

C++ · compiled/lengths.cpp

// What size() counts in C++: the bytes of the string. clang stores ordinary string literals as UTF-8, so a code
// point takes 1 to 4 bytes; they are counted by hand, since every byte except a continuation byte (10xxxxxx) starts one.
#include <cstddef>
#include <cstdio>
#include <string>
#include <string_view>

std::size_t code_points(std::string_view s) {
    std::size_t count = 0;
    for (unsigned char byte : s) count += (byte & 0xC0) != 0x80;
    return count;
}

int main() {
    struct Sample {
        std::string text;
        const char* note;
    };
    const Sample samples[] = {
        {"Asha", "plain ASCII"},
        {"Jos\u00e9", "é as one code point, U+00E9"},
        {"Jose\u0301", "e followed by U+0301, a combining acute accent"},
        {"\u0928\u0947\u0939\u093e", "Neha in Devanagari: two letters, each with a vowel sign"},
        {"\U0001F600", "one emoji, U+1F600"},
        {"\U0001F1EE\U0001F1F3", "a flag: the regional indicator letters I and N"},
    };
    std::printf("size  code points  text\n");
    for (const auto& [text, note] : samples) {
        std::printf("%4zu  %11zu  %s (%s)\n", text.size(), code_points(text), text.c_str(), note);
    }
    const std::string& jose = samples[1].text;
    std::printf("an index gives one byte: %d as char, %d as unsigned char\n", jose[3], static_cast<unsigned char>(jose[3]));
}

Go · compiled/lengths.go

// What len counts in Go: the bytes of the string, which Go keeps as UTF-8. unicode/utf8 counts the code points
// (runes), and unicode/utf16 encodes them, to compare with Java and JavaScript.
package main

import (
	"fmt"
	"unicode/utf16"
	"unicode/utf8"
)

func main() {
	samples := []struct{ text, note string }{
		{"Asha", "plain ASCII"},
		{"Jos\u00e9", "é as one code point, U+00E9"},
		{"Jose\u0301", "e followed by U+0301, a combining acute accent"},
		{"\u0928\u0947\u0939\u093e", "Neha in Devanagari: two letters, each with a vowel sign"},
		{"\U0001F600", "one emoji, U+1F600"},
		{"\U0001F1EE\U0001F1F3", "a flag: the regional indicator letters I and N"},
	}
	fmt.Println("len  runes  UTF-16  text")
	for _, s := range samples {
		units := len(utf16.Encode([]rune(s.text)))
		fmt.Printf("%3d  %5d  %6d  %s (%s)\n", len(s.text), utf8.RuneCountInString(s.text), units, s.text, s.note)
	}
	word := "Jos\u00e9\U0001F600"
	fmt.Println("an index gives one byte:", word[3])
	fmt.Print("range gives each rune with the byte offset where it starts:")
	for offset, r := range word {
		fmt.Printf(" %d:%c", offset, r)
	}
	fmt.Println()
}

Build and run all three · compiled/lengths_session.py

from session import run, versions

# Lengths.java runs as a single source file, lengths.cpp is built with the sanitizers on, and go run builds lengths.go.
versions("java -version", "clang++ --version", "go version")
run("""
java Lengths.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined lengths.cpp -o lengths && ./lengths
go run lengths.go
""")

Output

# openjdk version "25.0.4.1" LTS
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# go version go1.27.1 darwin/arm64
$ java Lengths.java
length  code points  UTF-8  text
     4            4      4  Asha (plain ASCII)
     4            4      5  José (é as one code point, U+00E9)
     5            5      6  José (e followed by U+0301, a combining acute accent)
     4            4     12  नेहा (Neha in Devanagari: two letters, each with a vowel sign)
     2            1      4  😀 (one emoji, U+1F600)
     4            2      8  🇮🇳 (a flag: the regional indicator letters I and N)
charAt(0) gives one code unit: d83d, codePointAt(0): 1f600
the two Josés are equal: false
equal after NFC normalization: true
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined lengths.cpp -o lengths && ./lengths
size  code points  text
   4            4  Asha (plain ASCII)
   5            4  José (é as one code point, U+00E9)
   6            5  José (e followed by U+0301, a combining acute accent)
  12            4  नेहा (Neha in Devanagari: two letters, each with a vowel sign)
   4            1  😀 (one emoji, U+1F600)
   8            2  🇮🇳 (a flag: the regional indicator letters I and N)
an index gives one byte: -61 as char, 195 as unsigned char
$ go run lengths.go
len  runes  UTF-16  text
  4      4       4  Asha (plain ASCII)
  5      4       4  José (é as one code point, U+00E9)
  6      5       5  José (e followed by U+0301, a combining acute accent)
 12      4       4  नेहा (Neha in Devanagari: two letters, each with a vowel sign)
  4      1       2  😀 (one emoji, U+1F600)
  8      2       4  🇮🇳 (a flag: the regional indicator letters I and N)
an index gives one byte: 195
range gives each rune with the byte offset where it starts: 0:J 1:o 2:s 3:é 5:😀

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lengths_session.py

Read the output one string at a time:

  • Asha is 4 in every column. For ASCII text, bytes, code units, code points and characters are the same thing, which is why problems whose input is limited to English letters or printable ASCII never meet the rest of this lesson.
  • The two Josés look the same and are different strings. The first stores é as one code point, U+00E9; the second as the letter e followed by U+0301, a combining acute accent. Every length of the second is one larger, and ==, === and equals all report false. Unicode Standard Annex #15 calls the two spellings canonically equivalent, and normalising both to NFC, the composed form, makes them equal. Python’s unicodedata.normalize, JavaScript’s normalize() and Java’s java.text.Normalizer do it. Go keeps normalisation outside its standard library, in the package golang.org/x/text/unicode/norm, and C++ needs a library such as ICU.
  • नेहा (Neha) is two characters to a reader and four code points, because each of its two letters carries a vowel sign. Every Devanagari code point takes three bytes in UTF-8, so the name is 12 bytes long.
  • 😀 is one code point above U+FFFF. Python counts 1, JavaScript and Java count the 2 code units of its surrogate pair, and Go and C++ count its 4 UTF-8 bytes.
  • 🇮🇳 is one flag and two code points, the regional indicator letters I and N, so it is 4 code units long in UTF-16 and 8 bytes long in UTF-8.

The last lines show what an index hands back. Python returns a whole code point as a string of length 1. JavaScript’s smile[0] and Java’s charAt(0) both return d83d, the first half of the surrogate pair, which is no character on its own; codePointAt(0) reads the whole pair. Go’s word[3] is the byte 195, the first of the two bytes of é, while ranging over the string decodes UTF-8 and reports the byte where each rune starts, so é starts at 3 and the emoji at 5. C++ printed that same byte as −61: the standard leaves it to the compiler whether a plain char is signed, so compare bytes as unsigned char. It also leaves the encoding of ordinary string literals to the compiler, and clang stores them as UTF-8, which is why é takes two bytes in the C++ output.

Building a string without copying it on every pass

In Python, JavaScript, Java and Go a string never changes once it exists, and every operation that seems to change one builds a new string. A loop that runs s = s + piece therefore copies the whole of s on every pass. Adding one character n times copies 1 + 2 + … + n characters, about n²/2, so the work grows with the square of the final length. The Python documentation states this quadratic cost for repeated concatenation of any immutable sequence. C++ is the exception: a std::string can grow in place. The standard requires push_back to take amortised constant time, and s += piece appends to s itself, while s = s + piece first copies s, as the standard’s definition of + spells out.

Each language has a way to build a long string in linear time:

  • Python: collect the pieces in a list and call "".join(pieces) once, or write them to an io.StringIO.
  • JavaScript: push the pieces into an array and call join(""), or use +=, which both engines measured below handle well.
  • Java: append to a StringBuilder and call toString() once. An append copies only the new piece, apart from the occasional enlargement of the builder’s buffer when it fills up. The Java toolkit lesson uses one to print a large answer in one go.
  • C++: use s += piece or s.push_back(c), and s.reserve(n) first when you know the final size.
  • Go: write into a strings.Builder, whose documentation says it minimises memory copying, and call String() once.

The next programs build a string of 100,000 characters several ways and time each way as the best of three runs. The times differ from one run and one computer to the next, so each program ends with comparisons that do not.

Four ways to build 100,000 characters in Python, timed Python · py/build.py
"""Building strings in Python, each way timed as the best of three runs.

Part 1 builds 100,000 characters four ways. Part 2 appends 20,000 lines of 50 characters with +=, once inside a
function and once at the top level of this script. The times change from run to run and from computer to computer;
the comparisons do not.
"""

import time

N = 100_000
LINE = "x" * 49 + "\n"  # one line of 50 characters


def append(n):
    """s += piece: CPython can often grow s where it is."""
    s = ""
    for _ in range(n):
        s += "x"
    return s


def prepend(n):
    """piece + s: a new string, and a copy of s, on every pass."""
    s = ""
    for _ in range(n):
        s = "x" + s
    return s


def append_while_shared(n):
    """s += piece while a second name refers to the same string."""
    s = ""
    for _ in range(n):
        previous = s  # a second reference to the same string
        s += "x"
    return s


def join_at_end(n):
    """Collect the pieces in a list and join them once."""
    pieces = []
    for _ in range(n):
        pieces.append("x")
    return "".join(pieces)


def append_lines(count):
    report = ""
    for _ in range(count):
        report += LINE
    return report


def best_of_three(build, size, length):
    times = []
    for _ in range(3):
        start = time.perf_counter()
        text = build(size)
        times.append(time.perf_counter() - start)
        assert len(text) == length
    return min(times)


print("Part 1: 100,000 characters, one at a time")
best = {}
for build in (append, prepend, append_while_shared, join_at_end):
    best[build.__name__] = best_of_three(build, N, N)
    print(f"  {build.__name__:20} {best[build.__name__] * 1000:8.1f} ms")
join_time = best["join_at_end"]
print("  prepend took over 10 times as long as join_at_end:", best["prepend"] > 10 * join_time)
print("  append_while_shared took over 10 times as long:", best["append_while_shared"] > 10 * join_time)
print("  append stayed within 10 times of join_at_end:", best["append"] < 10 * join_time)

print("Part 2: 20,000 lines of 50 characters with +=")
LINES = 20_000
in_function = best_of_three(append_lines, LINES, LINES * len(LINE))
top_level = float("inf")
for _ in range(3):
    start = time.perf_counter()
    report = ""  # a global variable: this loop is not inside a function
    for _ in range(LINES):
        report += LINE
    top_level = min(top_level, time.perf_counter() - start)
    assert len(report) == LINES * len(LINE)
print(f"  inside a function  {in_function * 1000:8.1f} ms")
print(f"  at the top level   {top_level * 1000:8.1f} ms")
print("  at the top level += took over 10 times as long:", top_level > 10 * in_function)

Output

Part 1: 100,000 characters, one at a time
  append                    5.5 ms
  prepend                 190.9 ms
  append_while_shared     332.0 ms
  join_at_end               2.5 ms
  prepend took over 10 times as long as join_at_end: True
  append_while_shared took over 10 times as long: True
  append stayed within 10 times of join_at_end: True
Part 2: 20,000 lines of 50 characters with +=
  inside a function       1.2 ms
  at the top level      291.8 ms
  at the top level += took over 10 times as long: True

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build.py

Three ways to build 100,000 characters in JavaScript, timed JavaScript · js/build.mjs
// Three ways to build a string of 100,000 characters in JavaScript, each timed as the best of three runs. The times
// change from run to run and from computer to computer; the comparison at the end does not.
const N = 100_000;

function append(n) {
  let s = '';
  for (let i = 0; i < n; i++) s += 'x';
  return s;
}

function prepend(n) {
  let s = '';
  for (let i = 0; i < n; i++) s = 'x' + s;
  return s;
}

function joinAtEnd(n) {
  const pieces = [];
  for (let i = 0; i < n; i++) pieces.push('x');
  return pieces.join('');
}

function bestOfThree(build) {
  let best = Infinity;
  for (let run = 0; run < 3; run++) {
    const start = Date.now();
    const text = build(N);
    best = Math.min(best, Date.now() - start);
    if (text.length !== N) throw new Error(`${build.name} built ${text.length} characters`);
  }
  return best;
}

const times = { append: bestOfThree(append), prepend: bestOfThree(prepend), joinAtEnd: bestOfThree(joinAtEnd) };
for (const [name, ms] of Object.entries(times)) console.log(`${name.padEnd(9)} ${String(ms).padStart(5)} ms`);
console.log('all three finished in under a second:', Object.values(times).every((ms) => ms < 1000));

Output

append        0 ms
prepend       1 ms
joinAtEnd     1 ms
all three finished in under a second: true

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node build.mjs

Building 100,000 characters in Java, C++ and Go, timed

Java · compiled/Build.java

// Building a string of 100,000 characters in Java. += makes a new String, a copy of everything so far, on every
// pass; a StringBuilder appends to one buffer. The times change from run to run; the comparison at the end does not.
public class Build {
    static final int N = 100_000;

    public static void main(String[] args) {
        long start = System.nanoTime();
        String s = "";
        for (int i = 0; i < N; i++) {
            s += "x";
        }
        long plus = System.nanoTime() - start;

        start = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < N; i++) {
            sb.append('x');
        }
        String built = sb.toString();
        long builder = System.nanoTime() - start;

        System.out.println("+= in a loop:  " + plus / 1_000_000 + " ms");
        System.out.println("StringBuilder: " + builder / 1_000_000 + " ms");
        System.out.println("same text: " + s.equals(built));
        System.out.println("+= took over 10 times as long: " + (plus > 10 * builder));
    }
}

C++ · compiled/build.cpp

// Building a string of 100,000 characters in C++. A std::string can change in place, so s += 'x' appends to it;
// s = s + 'x' builds a new string, a copy of everything so far, on every pass. The times change from run to run;
// the comparison at the end does not.
#include <chrono>
#include <cstdio>
#include <string>

int main() {
    constexpr int n = 100'000;
    using clock = std::chrono::steady_clock;

    auto start = clock::now();
    std::string copied;
    for (int i = 0; i < n; ++i) copied = copied + 'x';
    const auto copy_time = clock::now() - start;

    start = clock::now();
    std::string appended;
    for (int i = 0; i < n; ++i) appended += 'x';
    const auto append_time = clock::now() - start;

    auto ms = [](auto d) { return static_cast<long long>(std::chrono::duration_cast<std::chrono::milliseconds>(d).count()); };
    std::printf("s = s + 'x': %lld ms\n", ms(copy_time));
    std::printf("s += 'x':    %lld ms\n", ms(append_time));
    std::printf("same text: %s\n", copied == appended ? "true" : "false");
    std::printf("s = s + 'x' took over 10 times as long: %s\n", copy_time > 10 * append_time ? "true" : "false");
}

Go · compiled/build.go

// Building a string of 100,000 bytes in Go. += makes a new string, a copy of everything so far, on every pass; a
// strings.Builder appends to one buffer. The times change from run to run; the comparison at the end does not.
package main

import (
	"fmt"
	"strings"
	"time"
)

const n = 100_000

func main() {
	start := time.Now()
	s := ""
	for range n {
		s += "x"
	}
	plus := time.Since(start)

	start = time.Now()
	var b strings.Builder
	for range n {
		b.WriteByte('x')
	}
	built := b.String()
	builder := time.Since(start)

	fmt.Println("+= in a loop:   ", plus.Milliseconds(), "ms")
	fmt.Println("strings.Builder:", builder.Milliseconds(), "ms")
	fmt.Println("same text:", s == built)
	fmt.Println("+= took over 10 times as long:", plus > 10*builder)
}

Build and run all three · compiled/build_session.py

from session import run, versions

# Build.java runs as a single source file, build.cpp is built with the sanitizers on, and go run builds build.go.
versions("java -version", "clang++ --version", "go version")
run("""
java Build.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined build.cpp -o build && ./build
go run build.go
""")

Output

# openjdk version "25.0.4.1" LTS
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# go version go1.27.1 darwin/arm64
$ java Build.java
+= in a loop:  1595 ms
StringBuilder: 5 ms
same text: true
+= took over 10 times as long: true
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined build.cpp -o build && ./build
s = s + 'x': 2531 ms
s += 'x':    0 ms
same text: true
s = s + 'x' took over 10 times as long: true
$ go run build.go
+= in a loop:    868 ms
strings.Builder: 0 ms
same text: true
+= took over 10 times as long: true

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py

Java’s +=, Go’s += and C++’s s = s + 'x' each took far longer than the builder, much more than the tenfold the programs check for, because each pass copied the whole string again. Python is the surprising one. Prepending took over ten times as long as joining, as the documentation predicts, yet plain appending with += kept pace with join. CPython 3.14 turns s += piece into an append in place, without a new string, but only when two conditions hold: s is a local variable of a function, and nothing else refers to the string. Its source code says the purpose is to spare programs that forget str.join() from quadratic time. Part 1 breaks the second condition with one extra name, previous = s. Part 2 breaks the first by running the same loop at the top level of the script, where report is a global variable: 20,000 lines took far longer there than inside a function. Both times += went back to copying the whole string on every pass. PEP 8 asks code not to rely on the optimisation for this reason: it is fragile even in CPython, and Python implementations without reference counting do not have it.

In JavaScript, both Node.js and QuickJS, the engine behind the Run button, handled += quickly in either direction, and all three ways finished in well under a second. The language specification describes what + returns, not how long it takes, so that speed belongs to the engines, while join is linear by construction in every language.

Reversing names with accents and emoji

“Reverse a string” is a classic warm-up, and a natural follow-up asks what happens with emoji. These programs reverse six names in each language’s usual ways and compare every result with what a reader expects: the characters they see, in reverse order.

Reversing six names with [::-1] in Python Python · py/reverse.py
"""Reversing names in Python. Slicing with [::-1] reverses the code points. A reader expects the characters they see
(grapheme clusters) to come back in reverse order, which is the second string of each pair."""

NAMES = [
    ("Asha", "ahsA"),
    ("Jos\u00e9", "\u00e9soJ"),
    ("Jose\u0301", "e\u0301soJ"),
    ("\u0928\u0947\u0939\u093e", "\u0939\u093e\u0928\u0947"),
    ("Kabir \U0001F44B\U0001F3FD", "\U0001F44B\U0001F3FD ribaK"),
    ("Priya \U0001F1EE\U0001F1F3", "\U0001F1EE\U0001F1F3 ayirP"),
]

right = 0
for name, expected in NAMES:
    result = name[::-1]
    right += result == expected
    print(f"{name} -> {result}  {'right' if result == expected else 'wrong'}")
print(f"[::-1] reversed {right} of {len(NAMES)} names as a reader expects")

Output

Asha -> ahsA  right
José -> ésoJ  right
José -> ́esoJ  wrong
नेहा -> ाहेन  wrong
Kabir 👋🏽 -> 🏽👋 ribaK  wrong
Priya 🇮🇳 -> 🇳🇮 ayirP  wrong
[::-1] reversed 2 of 6 names as a reader expects

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reverse.py

Slicing with [::-1] reverses code points, and it got two names right. The other four broke in four different ways:

  • The combining accent of the second José came first, with no letter to sit on, and the e lost its accent.
  • The vowel signs of नेहा landed in front of their consonants, so the result starts with a sign that has nothing to attach to.
  • Kabir’s skin-tone modifier moved in front of the hand, so the hand lost its skin tone and the modifier shows as a separate swatch.
  • The flag of India is the letters I and N. Reversed, they read N and I, and Unicode’s emoji data lists that pair as the flag of Nicaragua.

Python 3.14’s standard library has no function for grapheme clusters. JavaScript has one built in, and the second program compares it with the two usual ways:

Reversing six names three ways in JavaScript JavaScript · js/reverse.mjs
// Reversing names in JavaScript three ways: by UTF-16 code unit, by code point and by grapheme cluster. The second
// string of each pair is what a reader expects: the characters they see, in reverse order.
const names = [
  ['Asha', 'ahsA'],
  ['Jos\u00e9', '\u00e9soJ'],
  ['Jose\u0301', 'e\u0301soJ'],
  ['\u0928\u0947\u0939\u093e', '\u0939\u093e\u0928\u0947'],
  ['Kabir \u{1F44B}\u{1F3FD}', '\u{1F44B}\u{1F3FD} ribaK'],
  ['Priya \u{1F1EE}\u{1F1F3}', '\u{1F1EE}\u{1F1F3} ayirP'],
];

const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const ways = [
  ["split('')", (s) => s.split('').reverse().join('')],
  ['[...s]', (s) => [...s].reverse().join('')],
  ['Intl.Segmenter', (s) => Array.from(segmenter.segment(s), (part) => part.segment).reverse().join('')],
];

for (const [label, reverse] of ways) {
  let right = 0;
  for (const [name, expected] of names) {
    const result = reverse(name);
    if (result === expected) right++;
    console.log(`${label}: ${name} -> ${result}  ${result === expected ? 'right' : 'wrong'}`);
  }
  console.log(`${label} reversed ${right} of ${names.length} names as a reader expects`);
}

Output

split(''): Asha -> ahsA  right
split(''): José -> ésoJ  right
split(''): José -> ́esoJ  wrong
split(''): नेहा -> ाहेन  wrong
split(''): Kabir 👋🏽 -> �🁋� ribaK  wrong
split(''): Priya 🇮🇳 -> �🇮� ayirP  wrong
split('') reversed 2 of 6 names as a reader expects
[...s]: Asha -> ahsA  right
[...s]: José -> ésoJ  right
[...s]: José -> ́esoJ  wrong
[...s]: नेहा -> ाहेन  wrong
[...s]: Kabir 👋🏽 -> 🏽👋 ribaK  wrong
[...s]: Priya 🇮🇳 -> 🇳🇮 ayirP  wrong
[...s] reversed 2 of 6 names as a reader expects
Intl.Segmenter: Asha -> ahsA  right
Intl.Segmenter: José -> ésoJ  right
Intl.Segmenter: José -> ésoJ  right
Intl.Segmenter: नेहा -> हाने  right
Intl.Segmenter: Kabir 👋🏽 -> 👋🏽 ribaK  right
Intl.Segmenter: Priya 🇮🇳 -> 🇮🇳 ayirP  right
Intl.Segmenter reversed 6 of 6 names as a reader expects

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node reverse.mjs

  • split('') splits into UTF-16 code units, so the surrogate pairs came apart, and a half left on its own prints as �. In Kabir’s name two halves from different emoji met in the middle and formed a valid pair, which is the domino tile 🁋 (U+1F04B), a character nobody typed.
  • [...s] iterates code points, so it does exactly as well as Python’s slice, and no better.
  • Intl.Segmenter with granularity: 'grapheme' splits the text into grapheme clusters, and reversing those got all six names right.

This example shows recorded output from Node.js only. The Run button runs JavaScript in QuickJS, a small engine that has no Intl object; your browser’s own engine has Intl.Segmenter, and the JavaScript compiler linked below runs code there.

The same three levels exist in Java, while C++ and Go stop at code points:

Reversing six names in Java, C++ and Go

Java · compiled/Reverse.java

// Reversing names in Java three ways: by UTF-16 code unit (swapping the chars of an array), by code point
// (StringBuilder.reverse keeps each surrogate pair together) and by grapheme cluster (the regular expression \X).
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

public class Reverse {
    static final String[][] NAMES = {
        {"Asha", "ahsA"},
        {"Jos\u00e9", "\u00e9soJ"},
        {"Jose\u0301", "e\u0301soJ"},
        {"\u0928\u0947\u0939\u093e", "\u0939\u093e\u0928\u0947"},
        {"Kabir \uD83D\uDC4B\uD83C\uDFFD", "\uD83D\uDC4B\uD83C\uDFFD ribaK"},
        {"Priya \uD83C\uDDEE\uD83C\uDDF3", "\uD83C\uDDEE\uD83C\uDDF3 ayirP"},
    };

    static String byCodeUnit(String s) {
        char[] a = s.toCharArray();
        for (int i = 0, j = a.length - 1; i < j; i++, j--) {
            char t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
        return new String(a);
    }

    static String byCodePoint(String s) {
        return new StringBuilder(s).reverse().toString();
    }

    static final Pattern GRAPHEME = Pattern.compile("\\X");

    static String byGrapheme(String s) {
        List<String> clusters = GRAPHEME.matcher(s).results().map(MatchResult::group).toList();
        return String.join("", clusters.reversed());
    }

    static void report(String label, UnaryOperator<String> reverse) {
        int right = 0;
        for (String[] pair : NAMES) {
            String result = reverse.apply(pair[0]);
            boolean ok = result.equals(pair[1]);
            if (ok) right++;
            System.out.println(label + ": " + pair[0] + " -> " + result + "  " + (ok ? "right" : "wrong"));
        }
        System.out.println(label + " reversed " + right + " of " + NAMES.length + " names as a reader expects");
    }

    public static void main(String[] args) {
        report("char[] swap", Reverse::byCodeUnit);
        report("StringBuilder.reverse", Reverse::byCodePoint);
        report("regex \\X", Reverse::byGrapheme);
    }
}

C++ · compiled/reverse.cpp

// Reversing names in C++ two ways: std::ranges::reverse on the bytes, and by code point, keeping each UTF-8
// sequence (a lead byte and its continuation bytes, 10xxxxxx) together. The standard library has no grapheme clusters.
#include <algorithm>
#include <cstdio>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

std::string by_byte(std::string_view s) {
    std::string out(s);
    std::ranges::reverse(out);
    return out;
}

std::string by_code_point(std::string_view s) {
    std::vector<std::string_view> points;
    for (std::size_t i = 0; i < s.size();) {
        std::size_t j = i + 1;
        while (j < s.size() && (static_cast<unsigned char>(s[j]) & 0xC0) == 0x80) ++j;
        points.push_back(s.substr(i, j - i));
        i = j;
    }
    std::string out;
    out.reserve(s.size());
    for (auto it = points.rbegin(); it != points.rend(); ++it) out += *it;
    return out;
}

const std::pair<std::string, std::string> names[] = {
    {"Asha", "ahsA"},
    {"Jos\u00e9", "\u00e9soJ"},
    {"Jose\u0301", "e\u0301soJ"},
    {"\u0928\u0947\u0939\u093e", "\u0939\u093e\u0928\u0947"},
    {"Kabir \U0001F44B\U0001F3FD", "\U0001F44B\U0001F3FD ribaK"},
    {"Priya \U0001F1EE\U0001F1F3", "\U0001F1EE\U0001F1F3 ayirP"},
};

void report(const char* label, std::string (*reverse)(std::string_view)) {
    int right = 0;
    for (const auto& [name, expected] : names) {
        const std::string result = reverse(name);
        const bool ok = result == expected;
        right += ok;
        std::printf("%s: %s -> %s  %s\n", label, name.c_str(), result.c_str(), ok ? "right" : "wrong");
    }
    std::printf("%s reversed %d of %zu names as a reader expects\n", label, right, std::size(names));
}

int main() {
    report("std::ranges::reverse", by_byte);
    report("by code point", by_code_point);
}

Go · compiled/reverse.go

// Reversing names in Go two ways: the bytes of the string, and its runes (code points). The standard library has
// no grapheme clusters.
package main

import (
	"fmt"
	"slices"
)

func byByte(s string) string {
	b := []byte(s)
	slices.Reverse(b)
	return string(b)
}

func byRune(s string) string {
	r := []rune(s)
	slices.Reverse(r)
	return string(r)
}

func main() {
	names := [][2]string{
		{"Asha", "ahsA"},
		{"Jos\u00e9", "\u00e9soJ"},
		{"Jose\u0301", "e\u0301soJ"},
		{"\u0928\u0947\u0939\u093e", "\u0939\u093e\u0928\u0947"},
		{"Kabir \U0001F44B\U0001F3FD", "\U0001F44B\U0001F3FD ribaK"},
		{"Priya \U0001F1EE\U0001F1F3", "\U0001F1EE\U0001F1F3 ayirP"},
	}
	ways := []struct {
		label   string
		reverse func(string) string
	}{{"[]byte", byByte}, {"[]rune", byRune}}
	for _, way := range ways {
		right := 0
		for _, pair := range names {
			result := way.reverse(pair[0])
			verdict := "wrong"
			if result == pair[1] {
				verdict = "right"
				right++
			}
			fmt.Printf("%s: %s -> %s  %s\n", way.label, pair[0], result, verdict)
		}
		fmt.Printf("%s reversed %d of %d names as a reader expects\n", way.label, right, len(names))
	}
}

Build and run all three · compiled/reverse_session.py

from session import run, versions

# Reverse.java runs as a single source file, reverse.cpp is built with the sanitizers on, and go run builds reverse.go.
versions("java -version", "clang++ --version", "go version")
run("""
java Reverse.java
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined reverse.cpp -o reverse && ./reverse
go run reverse.go
""")

Output

# openjdk version "25.0.4.1" LTS
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# go version go1.27.1 darwin/arm64
$ java Reverse.java
char[] swap: Asha -> ahsA  right
char[] swap: José -> ésoJ  right
char[] swap: José -> ́esoJ  wrong
char[] swap: नेहा -> ाहेन  wrong
char[] swap: Kabir 👋🏽 -> ?🁋? ribaK  wrong
char[] swap: Priya 🇮🇳 -> ?🇮? ayirP  wrong
char[] swap reversed 2 of 6 names as a reader expects
StringBuilder.reverse: Asha -> ahsA  right
StringBuilder.reverse: José -> ésoJ  right
StringBuilder.reverse: José -> ́esoJ  wrong
StringBuilder.reverse: नेहा -> ाहेन  wrong
StringBuilder.reverse: Kabir 👋🏽 -> 🏽👋 ribaK  wrong
StringBuilder.reverse: Priya 🇮🇳 -> 🇳🇮 ayirP  wrong
StringBuilder.reverse reversed 2 of 6 names as a reader expects
regex \X: Asha -> ahsA  right
regex \X: José -> ésoJ  right
regex \X: José -> ésoJ  right
regex \X: नेहा -> हाने  right
regex \X: Kabir 👋🏽 -> 👋🏽 ribaK  right
regex \X: Priya 🇮🇳 -> 🇮🇳 ayirP  right
regex \X reversed 6 of 6 names as a reader expects
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined reverse.cpp -o reverse && ./reverse
std::ranges::reverse: Asha -> ahsA  right
std::ranges::reverse: José -> ��soJ  wrong
std::ranges::reverse: José -> ��esoJ  wrong
std::ranges::reverse: नेहा -> ��๤���ਤ�  wrong
std::ranges::reverse: Kabir 👋🏽 -> �������� ribaK  wrong
std::ranges::reverse: Priya 🇮🇳 -> ���𮇟� ayirP  wrong
std::ranges::reverse reversed 1 of 6 names as a reader expects
by code point: Asha -> ahsA  right
by code point: José -> ésoJ  right
by code point: José -> ́esoJ  wrong
by code point: नेहा -> ाहेन  wrong
by code point: Kabir 👋🏽 -> 🏽👋 ribaK  wrong
by code point: Priya 🇮🇳 -> 🇳🇮 ayirP  wrong
by code point reversed 2 of 6 names as a reader expects
$ go run reverse.go
[]byte: Asha -> ahsA  right
[]byte: José -> ��soJ  wrong
[]byte: José -> ��esoJ  wrong
[]byte: नेहा -> ��๤���ਤ�  wrong
[]byte: Kabir 👋🏽 -> �������� ribaK  wrong
[]byte: Priya 🇮🇳 -> ���𮇟� ayirP  wrong
[]byte reversed 1 of 6 names as a reader expects
[]rune: Asha -> ahsA  right
[]rune: José -> ésoJ  right
[]rune: José -> ́esoJ  wrong
[]rune: नेहा -> ाहेन  wrong
[]rune: Kabir 👋🏽 -> 🏽👋 ribaK  wrong
[]rune: Priya 🇮🇳 -> 🇳🇮 ayirP  wrong
[]rune reversed 2 of 6 names as a reader expects

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reverse_session.py

  • Java. Swapping the chars of an array reverses code units, with the same damage as split(''); Java’s UTF-8 encoder printed each lone surrogate as ?. StringBuilder.reverse() treats a surrogate pair as one character, as its documentation promises, so it matches Python. The regular expression \X matches one extended grapheme cluster, and reversing the list of its matches got all six right. BreakIterator.getCharacterInstance() is the other built-in way: its documentation says it follows the same extended grapheme cluster rules.
  • C++ and Go. Reversing the bytes breaks every character that is not ASCII, because a UTF-8 sequence read backwards starts with a continuation byte, which can never start a character. Only Asha survived; the � marks show bytes that are no longer valid UTF-8, and a few reversed byte runs happened to form valid but unrelated characters. Reversing Go’s []rune, or the decoded code points in C++, matches Python. Neither standard library knows grapheme clusters: in C++ the usual tool is ICU’s BreakIterator, and Go needs a third-party package.

New in Python 3.15

Python 3.15 adds unicodedata.iter_graphemes(), which splits a string into grapheme clusters by the extended rules of Unicode Standard Annex #29; converting each item it yields with str() gives the cluster’s text. The examples on this page run on Python 3.14.8, which does not have it, and on an online judge with an older Python a call to it fails with an AttributeError.

Over the six names, reversing bytes got 1 right, reversing UTF-16 code units 2, reversing code points 2, and reversing grapheme clusters all 6. The tools that reverse what a reader sees are Intl.Segmenter in JavaScript, the regular expression \X or BreakIterator in Java, iter_graphemes in unicodedata from Python 3.15, a library such as ICU in C++, and a third-party package in Go.

Settle the alphabet before you code

Most coding problems pin the alphabet down in their constraints, with lines such as “s consists of lowercase English letters” or “printable ASCII characters”. Under such a constraint bytes, code units, code points and characters are all the same, and every language’s indexing, slicing and reversing is correct. When the statement leaves it open, ask. If the answer is “any text a user can type”, say which unit your solution counts, and why:

  • Code points are enough when the task only moves text around without splitting characters, such as counting the frequency of each code point, or comparing two strings after normalising them.
  • Grapheme clusters are needed when the task is about what a person sees: reversing for display, cutting a name to 20 characters, moving a cursor or counting characters for a limit shown to the user.
  • Bytes are the unit of storage and transfer: a file, a network message, a database column or an API whose limit is stated in bytes.

The alphabet is one of the questions on the checklist of edge cases that is worth settling before any code is written.

Splitting at an ASCII separator is safe in every language, whatever the text holds. In UTF-8 the bytes of ASCII characters never appear inside the encoding of any other character, as RFC 3629 points out, and in UTF-16 the two halves of a surrogate pair lie in the range D800 to DFFF, so a unit with the value of a space is always a space. A search for " " or "," therefore never cuts an emoji or a Devanagari letter in half, and that is what the exercise below relies on.

Practise: reverse the words, keep every space

The exercise asks for the words of a line in reverse order, with every run of spaces left exactly where it was. It uses two ideas from this lesson: cutting at an ASCII space keeps every word whole, whatever it contains, and the result is collected in pieces and joined once, so it takes linear time on a line of 100,000 words.

Exercise · Easy · Python, JavaScript

Reverse the words, keep every space

A caption editor has a button that reverses the order of the words in a line and leaves its spacing alone: every run of spaces stays where it was, just as wide. Write reverse_words_keep_spaces(s) (reverseWordsKeepSpaces(s) in JavaScript), which returns the line with its words in reverse order.

  • A word is a longest run of characters other than the space " ", and a gap is a longest run of spaces. Only " " counts as a space: a tab belongs to the word it touches.
  • The gaps stay in place. A line that starts or ends with spaces still does, and the third gap from the left is still the third gap from the left.
  • Words come out unchanged, whatever they hold: accents, emoji, Devanagari.
  • The result takes time linear in the length of s, also for a line of 100,000 words.
reverse_words_keep_spaces("one two  three")   # "three two  one"
reverse_words_keep_spaces("  hi there ")      # "  there hi "
reverse_words_keep_spaces("   ")              # "   "

Run the sample tests when you are done.

Python · Starter code · reverse_words.py

def reverse_words_keep_spaces(s):
    """The words of s in reverse order, with every run of spaces left where it was and as wide as it was.

    A word is a longest run of characters other than " "; a gap is a longest run of " ". Words come out unchanged.
    Take time linear in len(s).
    """
    # Replace this line with your code.
    return ""
The sample tests · test_reverse_words.py
import random
import re

from reverse_words import reverse_words_keep_spaces


def test_the_examples():
    """the words swap places and the gaps stay"""
    assert reverse_words_keep_spaces("one two  three") == "three two  one"
    assert reverse_words_keep_spaces("  hi there ") == "  there hi "


def test_one_word_or_none():
    """one word, an empty line and a line of spaces come back as they are"""
    assert reverse_words_keep_spaces("word") == "word"
    assert reverse_words_keep_spaces("  solo ") == "  solo "
    assert reverse_words_keep_spaces("") == ""
    assert reverse_words_keep_spaces("   ") == "   "


def test_words_with_accents_emoji_and_devanagari():
    """words come out unchanged, whatever they hold"""
    neha, wave, flag = "\u0928\u0947\u0939\u093e", "\U0001F44B\U0001F3FD", "\U0001F1EE\U0001F1F3"
    assert reverse_words_keep_spaces(f"{neha} {wave} Kabir") == f"Kabir {wave} {neha}"
    assert reverse_words_keep_spaces(f"Jose\u0301  {flag}") == f"{flag}  Jose\u0301"


def test_only_the_space_separates_words():
    """a tab or a line break belongs to the word it touches"""
    assert reverse_words_keep_spaces("a\tb c") == "c a\tb"
    assert reverse_words_keep_spaces("x\ny z") == "z x\ny"


def test_random_lines_keep_their_gaps():
    """500 random lines: the same gaps in the same order, the words reversed"""
    rng = random.Random(28)
    pieces = ["a", "bc", "\u00e9", "e\u0301", "\U0001F600", " ", "  "]
    for _ in range(500):
        line = "".join(rng.choice(pieces) for _ in range(rng.randint(0, 12)))
        out = reverse_words_keep_spaces(line)
        assert re.findall(r" +", out) == re.findall(r" +", line), line
        assert re.findall(r"[^ ]+", out) == re.findall(r"[^ ]+", line)[::-1], line
        assert out.startswith(" ") == line.startswith(" "), line
        assert len(out) == len(line), line


def test_a_long_line():
    """100,000 words, in time linear in the length of the line"""
    words = [f"w{i}" for i in range(100_000)]
    assert reverse_words_keep_spaces(" ".join(words)) == " ".join(reversed(words))

JavaScript · Starter code · reverse_words.mjs

/**
 * The words of s in reverse order, with every run of spaces left where it was and as wide as it was. A word is a
 * longest run of characters other than " "; a gap is a longest run of " ". Words come out unchanged. Take time
 * linear in s.length.
 */
export function reverseWordsKeepSpaces(s) {
  // Replace this line with your code.
  return '';
}
The sample tests · reverse_words.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { reverseWordsKeepSpaces } from './reverse_words.mjs';

let seed = 28;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const randInt = (lo, hi) => lo + Math.floor(random() * (hi - lo + 1));
const runs = (text, pattern) => text.match(pattern) ?? [];

test('the words swap places and the gaps stay', () => {
  assert.equal(reverseWordsKeepSpaces('one two  three'), 'three two  one');
  assert.equal(reverseWordsKeepSpaces('  hi there '), '  there hi ');
});

test('one word, an empty line and a line of spaces come back as they are', () => {
  assert.equal(reverseWordsKeepSpaces('word'), 'word');
  assert.equal(reverseWordsKeepSpaces('  solo '), '  solo ');
  assert.equal(reverseWordsKeepSpaces(''), '');
  assert.equal(reverseWordsKeepSpaces('   '), '   ');
});

test('words come out unchanged, whatever they hold', () => {
  const neha = '\u0928\u0947\u0939\u093e';
  const wave = '\u{1F44B}\u{1F3FD}';
  const flag = '\u{1F1EE}\u{1F1F3}';
  assert.equal(reverseWordsKeepSpaces(`${neha} ${wave} Kabir`), `Kabir ${wave} ${neha}`);
  assert.equal(reverseWordsKeepSpaces(`Jose\u0301  ${flag}`), `${flag}  Jose\u0301`);
});

test('a tab or a line break belongs to the word it touches', () => {
  assert.equal(reverseWordsKeepSpaces('a\tb c'), 'c a\tb');
  assert.equal(reverseWordsKeepSpaces('x\ny z'), 'z x\ny');
});

test('500 random lines: the same gaps in the same order, the words reversed', () => {
  const pieces = ['a', 'bc', '\u00e9', 'e\u0301', '\u{1F600}', ' ', '  '];
  for (let trial = 0; trial < 500; trial++) {
    const line = Array.from({ length: randInt(0, 12) }, () => pieces[randInt(0, pieces.length - 1)]).join('');
    const out = reverseWordsKeepSpaces(line);
    assert.deepEqual(runs(out, / +/g), runs(line, / +/g), JSON.stringify(line));
    assert.deepEqual(runs(out, /[^ ]+/g), runs(line, /[^ ]+/g).reverse(), JSON.stringify(line));
    assert.equal(out.startsWith(' '), line.startsWith(' '), JSON.stringify(line));
    assert.equal(out.length, line.length, JSON.stringify(line));
  }
});

test('100,000 words, in time linear in the length of the line', () => {
  const words = Array.from({ length: 100_000 }, (_, i) => `w${i}`);
  assert.equal(reverseWordsKeepSpaces(words.join(' ')), [...words].reverse().join(' '));
});
A hint

Cut the line into runs that alternate between words and gaps, in order. In Python, re.findall(r" +|[^ ]+", s) returns exactly those runs; in JavaScript, s.match(/ +|[^ ]+/g) does the same (it returns null for an empty string). Collect the words in a list. Then walk the runs again: keep every gap as it is, and put in a word from the end of the word list each time you meet a word. Collect the pieces and join them once at the end. Why does cutting at spaces never split an emoji or a Devanagari letter?

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

JavaScript & TypeScript Online Compiler Paste reverse.mjs and run it in your browser's own engine, which has Intl.Segmenter. Unicode Character Inspector Paste the two Josés and see the code points and UTF-8 bytes behind each one. Character Counter Paste a name with emoji and compare its characters, code points, UTF-16 units and UTF-8 bytes. Reverse Text, Mirror & Upside Down Reverse text by grapheme cluster and check that flags and skin tones stay whole.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 In Java, what does "🇮🇳".length() return for the flag of India?

    Choose one answer.

    Show the answer to question 1

    Answer: 4

    The flag is two code points, the regional indicator letters I and N, and both lie above U+FFFF. Java strings count UTF-16 code units, and each of those code points needs a surrogate pair, so the length is 4. Python's len gives 2 (code points), Go's len gives 8 (UTF-8 bytes), and a reader sees 1 flag.

  2. Question 2 of 6 In Go, len("José") is called with é written as the single code point U+00E9. What does it return?

    Choose one answer.

    Show the answer to question 2

    Answer: 5

    Go's len counts bytes. J, o and s take one byte each in UTF-8, and é (U+00E9) takes two, C3 A9, so the string is 5 bytes long. Written as e followed by the combining accent U+0301, the same-looking name would be 6 bytes long.

  3. Question 3 of 6 नेहा is four code points: two Devanagari letters, each with a vowel sign. How many bytes does it take in UTF-8?

    Type a number.

    Show the answer to question 3

    Answer: 12 bytes

    Devanagari lies between U+0900 and U+097F, inside the range U+0800 to U+FFFF that UTF-8 encodes with three bytes, so four code points take 4 × 3 = 12 bytes. A reader sees two characters, and Python's len reports 4.

  4. Question 4 of 6 Which of these reverses "Kabir 👋🏽" so that the waving hand keeps its skin tone?

    Choose one answer.

    Show the answer to question 4

    Answer: JavaScript: the segments of Intl.Segmenter with granularity "grapheme", reversed

    The hand with a skin tone is two code points, the hand and a modifier, that form one grapheme cluster. Slicing in Python, StringBuilder.reverse and spreading into an array all reverse code points, so the modifier ends up in front of the hand. Intl.Segmenter keeps the cluster together, and so does Java's regular expression \X.

  5. Question 5 of 6 Each loop below builds a string of 100,000 characters in CPython. In the lesson's run, which took over ten times as long as collecting the pieces and joining them once? Choose all that apply.

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • s = "x" + s
    • previous = s followed by s += "x"

    Prepending always builds a new string and copies the old one. Appending with += is often fast in CPython 3.14, which grows the string in place, but only for a local variable of a function and only while nothing else refers to the string; the extra name previous made it copy the whole string on every pass. The join version is linear by construction.

  6. Question 6 of 6 Why can a program split UTF-8 or UTF-16 text at every space without cutting an emoji in half?

    Choose one answer.

    Show the answer to question 6

    Answer: The bytes of ASCII characters never occur inside another character's UTF-8 encoding, and surrogate halves lie in D800 to DFFF

    RFC 3629 points out that ASCII byte values do not appear anywhere else in a UTF-8 stream, and in UTF-16 the two halves of a surrogate pair always lie between D800 and DFFF, so a byte or unit equal to a space is a real space. Splitting at spaces, commas or newlines therefore keeps every other character whole.

Interview questions

Warm-up (fresher to mid-level): Why does "😀".length give 2 in JavaScript and Java, while len("😀") gives 1 in Python and 4 in Go?

Each language counts a different unit. 😀 is one code point, U+1F600, which lies above U+FFFF. Python strings are sequences of code points, so len counts 1. JavaScript and Java strings are sequences of UTF-16 code units, and UTF-16 stores a code point above U+FFFF as a surrogate pair of two units, so the length is 2 and an index returns one half of the pair. Go strings are bytes, and UTF-8 needs four bytes for this code point, so len is 4, as size() is for a C++ std::string. None of the four counts what a reader sees: 🇮🇳 is one flag but two code points. Count code points with [...s].length in JavaScript, codePointCount in Java and utf8.RuneCountInString in Go, and count user-perceived characters with Intl.Segmenter or Java’s \X.

Warm-up (fresher to mid-level): Is building a string with += in a loop quadratic in Python?

By the language’s rules, yes: str is immutable, and the Python documentation says that building a sequence by repeated concatenation costs time quadratic in its total length. CPython 3.14 often avoids that cost: when s is a local variable of a function and nothing else refers to the string, s += piece appends in place. This lesson’s run shows how easily that breaks. A second name for the string, or the same loop at the top level of a script, where the variable is global, made += copy the whole string on every pass and take over ten times as long. PEP 8 asks code not to rely on the optimisation, which is fragile even in CPython and absent from implementations without reference counting. Collect the pieces in a list and call "".join(pieces) once, or write them to an io.StringIO.

Key takeaways

  • Python counts code points, JavaScript and Java count UTF-16 code units, and Go and C++ count bytes; a reader counts grapheme clusters. For ASCII text all four agree.
  • An index returns that same unit: a whole code point in Python, possibly half an emoji in JavaScript and Java, and one byte in Go and C++.
  • Strings are immutable in Python, JavaScript, Java and Go, so s = s + piece in a loop is quadratic. Use join, StringBuilder or strings.Builder; in C++, append with +=.
  • CPython’s fast += is an optimisation, not a promise: prepending, a second reference or a global variable makes it quadratic again.
  • Reversing bytes breaks every non-ASCII character, reversing code units breaks emoji, and reversing code points breaks accents, vowel signs, skin tones and flags. Only grapheme clusters reverse correctly: Intl.Segmenter, Java’s \X or, from Python 3.15, unicodedata.iter_graphemes.
  • Normalise to NFC before you compare text that may come from different sources.
  • Read the constraints for the alphabet, ask when it is missing, and say which unit your code counts.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.