Heroes / Esempi / wrap

examples/wrap/

Un programma che manda a capo il testo entro una larghezza prestabilita.

I test verificano i casi in cui è facile sbagliare di un carattere: una parola più lunga della riga, uno spazio nel punto in cui si va a capo e un testo vuoto.

Eseguilo

$ heroes run examples/wrap/main.hero
The quick brown fox
jumps over the lazy
dog and keeps going
for a while
4
19
a
supercalifragilistic
b
20
ab cd
ab
cd
0
true

exit 0 · 15 righe

Il programma

# Text folded to a width — the twentieth program, and the one whose edges are
# all off by one.
#
# Corpus and regression material (CLAUDE.md §9). It closes no measured form
# gap. It is here because wrapping is the smallest job whose every boundary is
# a decision somebody gets wrong:
#
#   - a word that fits EXACTLY on the line: does the space go before it?
#   - a word longer than the whole width: break it, or overflow the line?
#   - two spaces between words: kept, or collapsed?
#   - a trailing space at a break: on the line above, or dropped?
#
# There is no right answer to any of them, which is why they are all named in
# the module doc and all asserted below. A wrapper that answers them silently
# is a wrapper whose output nobody can predict, and predicting the output is
# the entire purpose of wrapping text.
#
# THIS ONE ANSWERS: a word that fits exactly stays on the line; a word longer
# than the width OVERFLOWS rather than being broken, because breaking a URL
# or an identifier is worse than a long line; runs of whitespace collapse to
# one space, because the input's spacing is not the output's; and a trailing
# space is never written, because it is invisible and it changes a diff.

function words_of(text: str) -> [str]
    out: [str] @= []
    piece: str @= ""

    for ch in text.chars()
        if ch == " " || ch == "\n" || ch == "\t"
            if piece.len() > 0
                out @ out.push(piece)
            piece @ ""
        else
            piece @ piece + ch

    if piece.len() > 0
        out @ out.push(piece)

    return out

function wrapped(text: str, width: i64) -> [str]?
    if width < 1
        return fail(code: "bad_width", msg: "a width below one leaves nowhere to put a word")
    lines: [str] @= []
    current: str @= ""

    for word in words_of(text)
        if current.len() == 0
            # First word on the line, whatever its length: a word longer than
            # the width overflows rather than being broken.
            current @ word
            continue

        # `+ 1` for the space that would join them. This is the off-by-one
        # every wrapper has, and `<=` rather than `<` is what makes a word
        # that fits EXACTLY stay on the line.
        if current.len() + 1 + word.len() <= width
            current @ current + " " + word
        else
            lines @ lines.push(current)
            current @ word

    if current.len() > 0
        lines @ lines.push(current)

    return ok(lines)

function rendered(text: str, width: i64) -> str?
    return ok(join(wrapped(text, width: width)?, "\n"))

# The longest line the wrapper produced, which is the number a caller checks
# against the width it asked for — and which may EXCEED it, by exactly as much
# as the longest single word does.
function longest(text: str, width: i64) -> i64?
    most: i64 @= 0

    for line in wrapped(text, width: width)?
        if line.len() > most
            most @ line.len()

    return ok(most)

constant PASSAGE: str
    "The quick brown fox jumps over the lazy dog and keeps going for a while"

function main()
    print(rendered(PASSAGE, width: 20).must())
    print(wrapped(PASSAGE, width: 20).must().len())
    print(longest(PASSAGE, width: 20).must())

    # A word longer than the width overflows, and the wrapper says how far.
    print(rendered("a supercalifragilistic b", width: 10).must())
    print(longest("a supercalifragilistic b", width: 10).must())

    # The exact fit, which is where `<=` and `<` disagree.
    print(rendered("ab cd", width: 5).must())
    print(rendered("ab cd", width: 4).must())

    print(wrapped("", width: 10).must().len())
    print(wrapped("x", width: 0).is_err())

test "a passage folds at the width and every line is inside it"
    said = wrapped(PASSAGE, width: 20).must()
    assert said.len() == 4

    for line in said
        assert line.len() <= 20

    assert said[0] == "The quick brown fox"

test "a word that fits exactly stays on the line"
    # `ab cd` is five characters with its space. At width 5 it is one line;
    # at 4 it is two. That pair is the whole difference between `<=` and `<`.
    assert rendered("ab cd", width: 5).must() == "ab cd"
    assert rendered("ab cd", width: 4).must() == "ab\ncd"
    assert rendered("abc de", width: 6).must() == "abc de"
    assert rendered("abc de", width: 5).must() == "abc\nde"

test "a word longer than the width overflows rather than being broken"
    # Breaking a URL or an identifier is worse than a long line, so the line
    # is allowed to exceed the width — and `longest` is how a caller finds out.
    said = wrapped("a supercalifragilistic b", width: 10).must()
    assert said.len() == 3
    assert said[1] == "supercalifragilistic"
    assert longest("a supercalifragilistic b", width: 10).must() == 20

test "runs of whitespace collapse to one space"
    assert rendered("a    b", width: 10).must() == "a b"
    assert rendered("a\nb", width: 10).must() == "a b"
    assert rendered("a\tb", width: 10).must() == "a b"
    assert rendered("  a  b  ", width: 10).must() == "a b"

test "no line ever ends in a space"
    said = wrapped(PASSAGE, width: 12).must()

    for line in said
        assert line.len() > 0
        assert line[line.len() - 1] != ' '

test "an empty input wraps to nothing, not to one empty line"
    assert wrapped("", width: 10).must().len() == 0
    assert wrapped("   ", width: 10).must().len() == 0
    assert rendered("", width: 10).must() == ""

test "a width below one is refused rather than looping"
    assert wrapped("x", width: 0).is_err()
    assert wrapped("x", width: 0 - 1).is_err()
    assert !wrapped("x", width: 1).is_err()

test "a width of one puts every word on its own line"
    assert rendered("a b c", width: 1).must() == "a\nb\nc"
    assert rendered("ab cd", width: 1).must() == "ab\ncd"

test "wrapping is idempotent at the same width"
    # The property a reader assumes and nobody asserts: folding text that is
    # already folded changes nothing, because the newline is whitespace and
    # the words are the same words.
    once = rendered(PASSAGE, width: 20).must()
    twice = rendered(once, width: 20).must()
    assert once == twice

test "every word survives, in order, whatever the width"
    for width in [1, 5, 12, 20, 100]
        said = wrapped(PASSAGE, width: width).must()
        back: [str] @= []

        for line in said
            for word in words_of(line)
                back @ back.push(word)

        assert back == words_of(PASSAGE)
examples/wrap/main.hero, 171 righe

impronta sha256 4190b1afc676 · 2 file · index.json

Successivo: sieve. Tutti gli esempi. ← Precedente: words

Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.