Project P138

Text truncation

Fitting text into a column with an ellipsis against the one-liner s[:w-3] + ..., titles that come out the same when cut, and three whitespace tidy-ups that each settle in one pass and merge different amounts.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-17

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

Fitting text into a column with an ellipsis, and tidying whitespace.

  • main.eml - the menu, one text at a width both ways, every width counted, titles that come out the same, and three whitespace tidy-ups
  • text.eml - fitting a text, the one-liner, the survey over widths, collisions, the tidy-ups, and the sample values

How each part works:

  • Fitting: the text if it fits; otherwise as much of it as leaves room for "...", or dots only when even one character will not go. It is never wider than the width and always a prefix of the text with dots.
  • As in the corpus case text-truncate-ellipsis, the one-liner s[:w - 3] + "..." is wrong three ways, all silently: it cuts text that already fits, a width below 3 makes the stop negative so it counts from the right end, and it comes out wider than the width. Over the 8 titles at every width from 0 to 30 it is too wide 24 times and cuts a fitting title 109 times; fitting does neither.
  • Cut short, different titles come out the same: at width 14 the 8 titles show as 4 labels.
  • As in the corpus case whitespace-normalization-loss, trim, collapse (trim and one space inside) and strip all each settle after one pass. They differ in what they merge: 10 values become 8, 7 and 4. Strip all makes AB 12, AB12 and A B12 one value; whether those were different is a fact about the field. A blank value is something before a trim and nothing after it.

What is checked: menu choices 1 to 5; a text of up to 60 characters and a width from 0 to 60. An empty answer cancels.

Sessions: sessions/basic.in gives: - one text each: a title at width 12, Q3 at 10 (the one-liner cuts it), and Release notes at 2 (the stop counts from the right); - every width, titles at widths 14 and 30, and the whitespace tidy-ups.

sessions/bad-input.in gives: - menu choices 0 and x; - texts: a cancel and 61 characters, then abc with a cancel, 61 and x for the width, and abc at width 0; - titles: a cancel, 99, and width 0, where every title is one label.

Built on the verified corpus cases text-truncate-ellipsis (the one-line truncation and its three silent bugs) and whitespace-normalization-loss (three idempotent tidy-ups that merge different amounts).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Text truncation ==
Fitting text into a column with an ellipsis, and tidying whitespace.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 0
Pick a number from 1 to 5.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> x
Pick a number from 1 to 5.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> 
Cancelled.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Up to 60 characters.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> abc
width (0 to 60)> 
Cancelled.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> abc
width (0 to 60)> 61
Type a whole number from 0 to 60.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> abc
width (0 to 60)> x
Type a whole number from 0 to 60.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> abc
width (0 to 60)> 0
  fit:         []  0 characters
  s[:w-3]+...: [...]  3 characters
  The one-liner is 3 wider than the width.
  With a width below 3 the stop -3 counts from the right end of the text.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 3
width (0 to 60)> 
Cancelled.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 3
width (0 to 60)> 99
Type a whole number from 0 to 60.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 3
width (0 to 60)> 0
At width 0, titles that come out the same:
  []  from [Quarterly report: sales], [Quarterly report: staff], [Quarterly review], [Release notes 4.1], [Release notes 4.2], [Board meeting minutes], [Board meeting agenda], [Q3]
8 titles show as 1 label.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 5
Bye.
What was typed (25 lines)
0
x
1

1
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1
abc

1
abc
61
1
abc
x
1
abc
0
3

3
99
3
0
5

basic

interpreter: byte-equal
== Text truncation ==
Fitting text into a column with an ellipsis, and tidying whitespace.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> Quarterly report: sales
width (0 to 60)> 12
  fit:         [Quarterly...]  12 characters
  s[:w-3]+...: [Quarterly...]  12 characters

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> Q3
width (0 to 60)> 10
  fit:         [Q3]  2 characters
  s[:w-3]+...: [Q3...]  5 characters
  The text fits in 10, and the one-liner cut it anyway.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 1
text (up to 60 characters)> Release notes
width (0 to 60)> 2
  fit:         [..]  2 characters
  s[:w-3]+...: [Release note...]  15 characters
  The one-liner is 13 wider than the width.
  With a width below 3 the stop -1 counts from the right end of the text.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 2
The 8 titles at every width from 0 to 30, 248 cases:
  s[:w-3]+...: wider than the width 24 times, a fitting title cut 109 times
  fit:         wider than the width 0 times, a fitting title cut 0 times
A slice never complains about its bounds, so nothing about the one-liner fails loudly.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 3
width (0 to 60)> 14
At width 14, titles that come out the same:
  [Quarterly r...]  from [Quarterly report: sales], [Quarterly report: staff], [Quarterly review]
  [Release not...]  from [Release notes 4.1], [Release notes 4.2]
  [Board meeti...]  from [Board meeting minutes], [Board meeting agenda]
8 titles show as 4 different labels.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 3
width (0 to 60)> 30
At width 30 every title stays different.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 4
Values: [Ann Lee]  [ Ann Lee]  [Ann  Lee]  [Ann Lee ]  [AB 12]  [AB12]  [A B12]  [555 1234 567]  [5551 234 567]  [   ]
  as they are: 10 different
  trim:      8 different; a second pass changes nothing for 10 of 10
  collapse:  7 different; a second pass changes nothing for 10 of 10
  strip all: 4 different; a second pass changes nothing for 10 of 10
  strip all makes [AB 12], [AB12] and [A B12] one value, and the two phone numbers, grouped differently, one: whether those were different is a fact about the field, not the string.
The blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it.

1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit
choice> 5
Bye.
What was typed (16 lines)
1
Quarterly report: sales
12
1
Q3
10
1
Release notes
2
2
3
14
3
30
4
5

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P138 text truncation: one text at a width both ways, every width
# counted, titles that come out the same, and three whitespace tidy-ups.
import text

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def ask_width(prompt):
    plain(input(prompt)) => t
    if t == "":
        "Cancelled." ^0
        return 0 - 1
    if len(t) > 2:
        "Type a whole number from 0 to 60." ^0
        return 0 - 1
    for c in t:
        if c < "0" or c > "9":
            "Type a whole number from 0 to 60." ^0
            return 0 - 1
    if int(t) > 60:
        "Type a whole number from 0 to 60." ^0
        return 0 - 1
    return int(t)

def one():
    input("text (up to 60 characters)> ") => s
    if s == "":
        "Cancelled." ^0
        return
    if len(s) > 60:
        "Up to 60 characters." ^0
        return
    ask_width("width (0 to 60)> ") => w
    if w < 0:
        return
    text.fit(s, w) => a
    text.naive(s, w) => b
    ("  fit:         [" + a + "]  " + str(len(a)) + " characters") ^0
    ("  s[:w-3]+...: [" + b + "]  " + str(len(b)) + " characters") ^0
    if len(s) <= w and b != s:
        ("  The text fits in " + str(w) + ", and the one-liner cut it anyway.") ^0
    if len(b) > w:
        ("  The one-liner is " + str(len(b) - w) + " wider than the width.") ^0
    if w < 3 and len(s) > w:
        ("  With a width below 3 the stop " + str(w - 3) + " counts from the right end of the text.") ^0

def every():
    text.survey(text.titles(), 30) => r
    ("The " + str(len(text.titles())) + " titles at every width from 0 to 30, " + str(r[0]) + " cases:") ^0
    ("  s[:w-3]+...: wider than the width " + str(r[1]) + " times, a fitting title cut " + str(r[2]) + " times") ^0
    ("  fit:         wider than the width " + str(r[3]) + " times, a fitting title cut " + str(r[4]) + " times") ^0
    "A slice never complains about its bounds, so nothing about the one-liner fails loudly." ^0

def same():
    ask_width("width (0 to 60)> ") => w
    if w < 0:
        return
    text.collisions(text.titles(), w) => cs
    if len(cs) == 0:
        ("At width " + str(w) + " every title stays different.") ^0
        return
    0 => lost
    ("At width " + str(w) + ", titles that come out the same:") ^0
    for c in cs:
        "" => names
        for s in c[1]:
            if names != "":
                names + ", " => names
            names + "[" + s + "]" => names
        ("  [" + c[0] + "]  from " + names) ^0
        lost + len(c[1]) - 1 => lost
    len(text.titles()) - lost => left
    if left == 1:
        (str(len(text.titles())) + " titles show as 1 label.") ^0
    else:
        (str(len(text.titles())) + " titles show as " + str(left) + " different labels.") ^0

def tidy():
    text.values() => vs
    "" => shown
    for v in vs:
        if shown != "":
            shown + "  " => shown
        shown + "[" + v + "]" => shown
    ("Values: " + shown) ^0
    ("  as they are: " + str(text.distinct(vs)) + " different") ^0
    for how in ["trim", "collapse", "strip all"]:
        [] => out
        0 => settled
        for v in vs:
            text.tidy(how, v) => t
            out + [t] => out
            if text.tidy(how, t) == t:
                settled + 1 => settled
        ("  " + pad(how + ":", 11) + str(text.distinct(out)) + " different; a second pass changes nothing for " + str(settled) + " of " + str(len(vs))) ^0
    "  strip all makes [AB 12], [AB12] and [A B12] one value, and the two phone numbers, grouped differently, one: whether those were different is a fact about the field, not the string." ^0
    "The blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it." ^0

"== Text truncation ==" ^0
"Fitting text into a column with an ellipsis, and tidying whitespace." ^0
True => running
while running:
    "" ^0
    "1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        one()
    elif choice == "2":
        every()
    elif choice == "3":
        same()
    elif choice == "4":
        tidy()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import text

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def ask_width(prompt):
    t = plain(input(prompt))
    if t == "":
        print("Cancelled.")
        return 0 - 1
    if len(t) > 2:
        print("Type a whole number from 0 to 60.")
        return 0 - 1
    for c in t:
        if c < "0" or c > "9":
            print("Type a whole number from 0 to 60.")
            return 0 - 1
    if int(t) > 60:
        print("Type a whole number from 0 to 60.")
        return 0 - 1
    return int(t)

def one():
    s = input("text (up to 60 characters)> ")
    if s == "":
        print("Cancelled.")
        return
    if len(s) > 60:
        print("Up to 60 characters.")
        return
    w = ask_width("width (0 to 60)> ")
    if w < 0:
        return
    a = text.fit(s, w)
    b = text.naive(s, w)
    print("  fit:         [" + a + "]  " + str(len(a)) + " characters")
    print("  s[:w-3]+...: [" + b + "]  " + str(len(b)) + " characters")
    if len(s) <= w and b != s:
        print("  The text fits in " + str(w) + ", and the one-liner cut it anyway.")
    if len(b) > w:
        print("  The one-liner is " + str(len(b) - w) + " wider than the width.")
    if w < 3 and len(s) > w:
        print("  With a width below 3 the stop " + str(w - 3) + " counts from the right end of the text.")

def every():
    r = text.survey(text.titles(), 30)
    print("The " + str(len(text.titles())) + " titles at every width from 0 to 30, " + str(r[0]) + " cases:")
    print("  s[:w-3]+...: wider than the width " + str(r[1]) + " times, a fitting title cut " + str(r[2]) + " times")
    print("  fit:         wider than the width " + str(r[3]) + " times, a fitting title cut " + str(r[4]) + " times")
    print("A slice never complains about its bounds, so nothing about the one-liner fails loudly.")

def same():
    w = ask_width("width (0 to 60)> ")
    if w < 0:
        return
    cs = text.collisions(text.titles(), w)
    if len(cs) == 0:
        print("At width " + str(w) + " every title stays different.")
        return
    lost = 0
    print("At width " + str(w) + ", titles that come out the same:")
    for c in cs:
        names = ""
        for s in c[1]:
            if names != "":
                names = names + ", "
            names = names + "[" + s + "]"
        print("  [" + c[0] + "]  from " + names)
        lost = lost + len(c[1]) - 1
    left = len(text.titles()) - lost
    if left == 1:
        print(str(len(text.titles())) + " titles show as 1 label.")
    else:
        print(str(len(text.titles())) + " titles show as " + str(left) + " different labels.")

def tidy():
    vs = text.values()
    shown = ""
    for v in vs:
        if shown != "":
            shown = shown + "  "
        shown = shown + "[" + v + "]"
    print("Values: " + shown)
    print("  as they are: " + str(text.distinct(vs)) + " different")
    for how in ["trim", "collapse", "strip all"]:
        out = []
        settled = 0
        for v in vs:
            t = text.tidy(how, v)
            out = out + [t]
            if text.tidy(how, t) == t:
                settled = settled + 1
        print("  " + pad(how + ":", 11) + str(text.distinct(out)) + " different; a second pass changes nothing for " + str(settled) + " of " + str(len(vs)))
    print("  strip all makes [AB 12], [AB12] and [A B12] one value, and the two phone numbers, grouped differently, one: whether those were different is a fact about the field, not the string.")
    print("The blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it.")

print("== Text truncation ==")
print("Fitting text into a column with an ellipsis, and tidying whitespace.")
running = True
while running:
    print("")
    print("1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        one()
    elif choice == "2":
        every()
    elif choice == "3":
        same()
    elif choice == "4":
        tidy()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

text.eml

eml
# P138 text truncation - fitting text into a column with an ellipsis, and
# tidying whitespace. As in the corpus case text-truncate-ellipsis, the
# one-line truncation s[:width - 3] + "..." is wrong three ways, and all
# silently: it cuts text that already fits, a width below 3 makes the stop
# negative and cuts from the right end, and it can come out wider than the
# width. As in whitespace-normalization-loss, three tidy-ups all settle
# after one pass, and differ in how many different values they merge.

def fit(s, width):
    # at most width characters: the text if it fits, else as much of it as
    # leaves room for "...", or dots only when even one character will not go
    if len(s) <= width:
        return s
    if width <= 3:
        return "..."[0:width]
    return s[0:width - 3] + "..."

def naive(s, width):
    return s[0:width - 3] + "..."

def titles():
    return ["Quarterly report: sales", "Quarterly report: staff", "Quarterly review", "Release notes 4.1", "Release notes 4.2", "Board meeting minutes", "Board meeting agenda", "Q3"]

def survey(texts, top):
    # over every width 0 to top: [cases, naive too wide, naive cut text that
    # fit, fit too wide, fit changed text that fit]
    0 => cases
    0 => wide
    0 => cut
    0 => fwide
    0 => fcut
    for w in [0:top]:
        for s in texts:
            cases + 1 => cases
            naive(s, w) => a
            fit(s, w) => b
            if len(a) > w:
                wide + 1 => wide
            if len(s) <= w and a != s:
                cut + 1 => cut
            if len(b) > w:
                fwide + 1 => fwide
            if len(s) <= w and b != s:
                fcut + 1 => fcut
    return [cases, wide, cut, fwide, fcut]

def collisions(texts, width):
    # groups of different texts that come out the same at this width
    {} => by
    [] => order
    for s in texts:
        fit(s, width) => k
        if k in by:
            by[k] + [s] => by[k]
        else:
            [s] => by[k]
            order + [k] => order
    [] => out
    for k in order:
        if len(by[k]) > 1:
            out + [[k, by[k]]] => out
    return out

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def collapse(s):
    # trimmed, and every run of spaces inside made one space
    "" => out
    False => gap
    for c in trim(s):
        if c == " ":
            True => gap
        else:
            if gap:
                out + " " => out
            False => gap
            out + c => out
    return out

def strip_all(s):
    "" => out
    for c in s:
        if c != " ":
            out + c => out
    return out

def tidy(how, s):
    if how == "trim":
        return trim(s)
    if how == "collapse":
        return collapse(s)
    return strip_all(s)

def values():
    return ["Ann Lee", " Ann Lee", "Ann  Lee", "Ann Lee ", "AB 12", "AB12", "A B12", "555 1234 567", "5551 234 567", "   "]

def distinct(xs):
    {} => seen
    for x in xs:
        1 => seen[x]
    return len(seen)
Python projection (text.py)
def fit(s, width):
    if len(s) <= width:
        return s
    if width <= 3:
        return "..."[0:width]
    return s[0:width - 3] + "..."

def naive(s, width):
    return s[0:width - 3] + "..."

def titles():
    return ["Quarterly report: sales", "Quarterly report: staff", "Quarterly review", "Release notes 4.1", "Release notes 4.2", "Board meeting minutes", "Board meeting agenda", "Q3"]

def survey(texts, top):
    cases = 0
    wide = 0
    cut = 0
    fwide = 0
    fcut = 0
    for w in range(0, top+1):
        for s in texts:
            cases = cases + 1
            a = naive(s, w)
            b = fit(s, w)
            if len(a) > w:
                wide = wide + 1
            if len(s) <= w and a != s:
                cut = cut + 1
            if len(b) > w:
                fwide = fwide + 1
            if len(s) <= w and b != s:
                fcut = fcut + 1
    return [cases, wide, cut, fwide, fcut]

def collisions(texts, width):
    by = {}
    order = []
    for s in texts:
        k = fit(s, width)
        if k in by:
            by[k] = by[k] + [s]
        else:
            by[k] = [s]
            order = order + [k]
    out = []
    for k in order:
        if len(by[k]) > 1:
            out = out + [[k, by[k]]]
    return out

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def collapse(s):
    out = ""
    gap = False
    for c in trim(s):
        if c == " ":
            gap = True
        else:
            if gap:
                out = out + " "
            gap = False
            out = out + c
    return out

def strip_all(s):
    out = ""
    for c in s:
        if c != " ":
            out = out + c
    return out

def tidy(how, s):
    if how == "trim":
        return trim(s)
    if how == "collapse":
        return collapse(s)
    return strip_all(s)

def values():
    return ["Ann Lee", " Ann Lee", "Ann  Lee", "Ann Lee ", "AB 12", "AB12", "A B12", "555 1234 567", "5551 234 567", "   "]

def distinct(xs):
    seen = {}
    for x in xs:
        seen[x] = 1
    return len(seen)

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.