{
  "id": "P138",
  "slug": "text-truncation",
  "key": "P138-text-truncation",
  "title": "Text truncation",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P138 - Text truncation\n\nFitting text into a column with an ellipsis, and tidying whitespace.\n\n- `main.eml` - the menu, one text at a width both ways, every width\n  counted, titles that come out the same, and three whitespace tidy-ups\n- `text.eml` - fitting a text, the one-liner, the survey over widths,\n  collisions, the tidy-ups, and the sample values\n\nHow each part works:\n\n- Fitting: the text if it fits; otherwise as much of it as leaves room for\n  \"...\", or dots only when even one character will not go. It is never\n  wider than the width and always a prefix of the text with dots.\n- As in the corpus case `text-truncate-ellipsis`, the one-liner\n  s[:w - 3] + \"...\" is wrong three ways, all silently: it cuts text that\n  already fits, a width below 3 makes the stop negative so it counts from\n  the right end, and it comes out wider than the width. Over the 8\n  titles at every width from 0 to 30 it is too wide 24 times and cuts a\n  fitting title 109 times; fitting does neither.\n- Cut short, different titles come out the same: at width 14 the 8 titles\n  show as 4 labels.\n- As in the corpus case `whitespace-normalization-loss`, trim, collapse\n  (trim and one space inside) and strip all each settle after one pass.\n  They differ in what they merge: 10 values become 8, 7 and 4. Strip all\n  makes AB 12, AB12 and A B12 one value; whether those were different is\n  a fact about the field. A blank value is something before a trim and\n  nothing after it.\n\nWhat is checked: menu choices 1 to 5; a text of up to 60 characters and a\nwidth from 0 to 60. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- one text each: a title at width 12, Q3 at 10 (the one-liner cuts it),\n  and Release notes at 2 (the stop counts from the right);\n- every width, titles at widths 14 and 30, and the whitespace tidy-ups.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- texts: a cancel and 61 characters, then abc with a cancel, 61 and x for\n  the width, and abc at width 0;\n- titles: a cancel, 99, and width 0, where every title is one label.\n\nBuilt on the verified corpus cases `text-truncate-ellipsis` (the one-line\ntruncation and its three silent bugs) and `whitespace-normalization-loss`\n(three idempotent tidy-ups that merge different amounts).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P138 text truncation: one text at a width both ways, every width\n# counted, titles that come out the same, and three whitespace tidy-ups.\nimport text\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef ask_width(prompt):\n    plain(input(prompt)) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return 0 - 1\n    if len(t) > 2:\n        \"Type a whole number from 0 to 60.\" ^0\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            \"Type a whole number from 0 to 60.\" ^0\n            return 0 - 1\n    if int(t) > 60:\n        \"Type a whole number from 0 to 60.\" ^0\n        return 0 - 1\n    return int(t)\n\ndef one():\n    input(\"text (up to 60 characters)> \") => s\n    if s == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(s) > 60:\n        \"Up to 60 characters.\" ^0\n        return\n    ask_width(\"width (0 to 60)> \") => w\n    if w < 0:\n        return\n    text.fit(s, w) => a\n    text.naive(s, w) => b\n    (\"  fit:         [\" + a + \"]  \" + str(len(a)) + \" characters\") ^0\n    (\"  s[:w-3]+...: [\" + b + \"]  \" + str(len(b)) + \" characters\") ^0\n    if len(s) <= w and b != s:\n        (\"  The text fits in \" + str(w) + \", and the one-liner cut it anyway.\") ^0\n    if len(b) > w:\n        (\"  The one-liner is \" + str(len(b) - w) + \" wider than the width.\") ^0\n    if w < 3 and len(s) > w:\n        (\"  With a width below 3 the stop \" + str(w - 3) + \" counts from the right end of the text.\") ^0\n\ndef every():\n    text.survey(text.titles(), 30) => r\n    (\"The \" + str(len(text.titles())) + \" titles at every width from 0 to 30, \" + str(r[0]) + \" cases:\") ^0\n    (\"  s[:w-3]+...: wider than the width \" + str(r[1]) + \" times, a fitting title cut \" + str(r[2]) + \" times\") ^0\n    (\"  fit:         wider than the width \" + str(r[3]) + \" times, a fitting title cut \" + str(r[4]) + \" times\") ^0\n    \"A slice never complains about its bounds, so nothing about the one-liner fails loudly.\" ^0\n\ndef same():\n    ask_width(\"width (0 to 60)> \") => w\n    if w < 0:\n        return\n    text.collisions(text.titles(), w) => cs\n    if len(cs) == 0:\n        (\"At width \" + str(w) + \" every title stays different.\") ^0\n        return\n    0 => lost\n    (\"At width \" + str(w) + \", titles that come out the same:\") ^0\n    for c in cs:\n        \"\" => names\n        for s in c[1]:\n            if names != \"\":\n                names + \", \" => names\n            names + \"[\" + s + \"]\" => names\n        (\"  [\" + c[0] + \"]  from \" + names) ^0\n        lost + len(c[1]) - 1 => lost\n    len(text.titles()) - lost => left\n    if left == 1:\n        (str(len(text.titles())) + \" titles show as 1 label.\") ^0\n    else:\n        (str(len(text.titles())) + \" titles show as \" + str(left) + \" different labels.\") ^0\n\ndef tidy():\n    text.values() => vs\n    \"\" => shown\n    for v in vs:\n        if shown != \"\":\n            shown + \"  \" => shown\n        shown + \"[\" + v + \"]\" => shown\n    (\"Values: \" + shown) ^0\n    (\"  as they are: \" + str(text.distinct(vs)) + \" different\") ^0\n    for how in [\"trim\", \"collapse\", \"strip all\"]:\n        [] => out\n        0 => settled\n        for v in vs:\n            text.tidy(how, v) => t\n            out + [t] => out\n            if text.tidy(how, t) == t:\n                settled + 1 => settled\n        (\"  \" + pad(how + \":\", 11) + str(text.distinct(out)) + \" different; a second pass changes nothing for \" + str(settled) + \" of \" + str(len(vs))) ^0\n    \"  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\n    \"The blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it.\" ^0\n\n\"== Text truncation ==\" ^0\n\"Fitting text into a column with an ellipsis, and tidying whitespace.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        one()\n    elif choice == \"2\":\n        every()\n    elif choice == \"3\":\n        same()\n    elif choice == \"4\":\n        tidy()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import text\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef ask_width(prompt):\n    t = plain(input(prompt))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return 0 - 1\n    if len(t) > 2:\n        print(\"Type a whole number from 0 to 60.\")\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            print(\"Type a whole number from 0 to 60.\")\n            return 0 - 1\n    if int(t) > 60:\n        print(\"Type a whole number from 0 to 60.\")\n        return 0 - 1\n    return int(t)\n\ndef one():\n    s = input(\"text (up to 60 characters)> \")\n    if s == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(s) > 60:\n        print(\"Up to 60 characters.\")\n        return\n    w = ask_width(\"width (0 to 60)> \")\n    if w < 0:\n        return\n    a = text.fit(s, w)\n    b = text.naive(s, w)\n    print(\"  fit:         [\" + a + \"]  \" + str(len(a)) + \" characters\")\n    print(\"  s[:w-3]+...: [\" + b + \"]  \" + str(len(b)) + \" characters\")\n    if len(s) <= w and b != s:\n        print(\"  The text fits in \" + str(w) + \", and the one-liner cut it anyway.\")\n    if len(b) > w:\n        print(\"  The one-liner is \" + str(len(b) - w) + \" wider than the width.\")\n    if w < 3 and len(s) > w:\n        print(\"  With a width below 3 the stop \" + str(w - 3) + \" counts from the right end of the text.\")\n\ndef every():\n    r = text.survey(text.titles(), 30)\n    print(\"The \" + str(len(text.titles())) + \" titles at every width from 0 to 30, \" + str(r[0]) + \" cases:\")\n    print(\"  s[:w-3]+...: wider than the width \" + str(r[1]) + \" times, a fitting title cut \" + str(r[2]) + \" times\")\n    print(\"  fit:         wider than the width \" + str(r[3]) + \" times, a fitting title cut \" + str(r[4]) + \" times\")\n    print(\"A slice never complains about its bounds, so nothing about the one-liner fails loudly.\")\n\ndef same():\n    w = ask_width(\"width (0 to 60)> \")\n    if w < 0:\n        return\n    cs = text.collisions(text.titles(), w)\n    if len(cs) == 0:\n        print(\"At width \" + str(w) + \" every title stays different.\")\n        return\n    lost = 0\n    print(\"At width \" + str(w) + \", titles that come out the same:\")\n    for c in cs:\n        names = \"\"\n        for s in c[1]:\n            if names != \"\":\n                names = names + \", \"\n            names = names + \"[\" + s + \"]\"\n        print(\"  [\" + c[0] + \"]  from \" + names)\n        lost = lost + len(c[1]) - 1\n    left = len(text.titles()) - lost\n    if left == 1:\n        print(str(len(text.titles())) + \" titles show as 1 label.\")\n    else:\n        print(str(len(text.titles())) + \" titles show as \" + str(left) + \" different labels.\")\n\ndef tidy():\n    vs = text.values()\n    shown = \"\"\n    for v in vs:\n        if shown != \"\":\n            shown = shown + \"  \"\n        shown = shown + \"[\" + v + \"]\"\n    print(\"Values: \" + shown)\n    print(\"  as they are: \" + str(text.distinct(vs)) + \" different\")\n    for how in [\"trim\", \"collapse\", \"strip all\"]:\n        out = []\n        settled = 0\n        for v in vs:\n            t = text.tidy(how, v)\n            out = out + [t]\n            if text.tidy(how, t) == t:\n                settled = settled + 1\n        print(\"  \" + pad(how + \":\", 11) + str(text.distinct(out)) + \" different; a second pass changes nothing for \" + str(settled) + \" of \" + str(len(vs)))\n    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.\")\n    print(\"The blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it.\")\n\nprint(\"== Text truncation ==\")\nprint(\"Fitting text into a column with an ellipsis, and tidying whitespace.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        one()\n    elif choice == \"2\":\n        every()\n    elif choice == \"3\":\n        same()\n    elif choice == \"4\":\n        tidy()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "text.eml",
      "eml": "# P138 text truncation - fitting text into a column with an ellipsis, and\n# tidying whitespace. As in the corpus case text-truncate-ellipsis, the\n# one-line truncation s[:width - 3] + \"...\" is wrong three ways, and all\n# silently: it cuts text that already fits, a width below 3 makes the stop\n# negative and cuts from the right end, and it can come out wider than the\n# width. As in whitespace-normalization-loss, three tidy-ups all settle\n# after one pass, and differ in how many different values they merge.\n\ndef fit(s, width):\n    # at most width characters: the text if it fits, else as much of it as\n    # leaves room for \"...\", or dots only when even one character will not go\n    if len(s) <= width:\n        return s\n    if width <= 3:\n        return \"...\"[0:width]\n    return s[0:width - 3] + \"...\"\n\ndef naive(s, width):\n    return s[0:width - 3] + \"...\"\n\ndef titles():\n    return [\"Quarterly report: sales\", \"Quarterly report: staff\", \"Quarterly review\", \"Release notes 4.1\", \"Release notes 4.2\", \"Board meeting minutes\", \"Board meeting agenda\", \"Q3\"]\n\ndef survey(texts, top):\n    # over every width 0 to top: [cases, naive too wide, naive cut text that\n    # fit, fit too wide, fit changed text that fit]\n    0 => cases\n    0 => wide\n    0 => cut\n    0 => fwide\n    0 => fcut\n    for w in [0:top]:\n        for s in texts:\n            cases + 1 => cases\n            naive(s, w) => a\n            fit(s, w) => b\n            if len(a) > w:\n                wide + 1 => wide\n            if len(s) <= w and a != s:\n                cut + 1 => cut\n            if len(b) > w:\n                fwide + 1 => fwide\n            if len(s) <= w and b != s:\n                fcut + 1 => fcut\n    return [cases, wide, cut, fwide, fcut]\n\ndef collisions(texts, width):\n    # groups of different texts that come out the same at this width\n    {} => by\n    [] => order\n    for s in texts:\n        fit(s, width) => k\n        if k in by:\n            by[k] + [s] => by[k]\n        else:\n            [s] => by[k]\n            order + [k] => order\n    [] => out\n    for k in order:\n        if len(by[k]) > 1:\n            out + [[k, by[k]]] => out\n    return out\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef collapse(s):\n    # trimmed, and every run of spaces inside made one space\n    \"\" => out\n    False => gap\n    for c in trim(s):\n        if c == \" \":\n            True => gap\n        else:\n            if gap:\n                out + \" \" => out\n            False => gap\n            out + c => out\n    return out\n\ndef strip_all(s):\n    \"\" => out\n    for c in s:\n        if c != \" \":\n            out + c => out\n    return out\n\ndef tidy(how, s):\n    if how == \"trim\":\n        return trim(s)\n    if how == \"collapse\":\n        return collapse(s)\n    return strip_all(s)\n\ndef values():\n    return [\"Ann Lee\", \" Ann Lee\", \"Ann  Lee\", \"Ann Lee \", \"AB 12\", \"AB12\", \"A B12\", \"555 1234 567\", \"5551 234 567\", \"   \"]\n\ndef distinct(xs):\n    {} => seen\n    for x in xs:\n        1 => seen[x]\n    return len(seen)\n",
      "python": "def fit(s, width):\n    if len(s) <= width:\n        return s\n    if width <= 3:\n        return \"...\"[0:width]\n    return s[0:width - 3] + \"...\"\n\ndef naive(s, width):\n    return s[0:width - 3] + \"...\"\n\ndef titles():\n    return [\"Quarterly report: sales\", \"Quarterly report: staff\", \"Quarterly review\", \"Release notes 4.1\", \"Release notes 4.2\", \"Board meeting minutes\", \"Board meeting agenda\", \"Q3\"]\n\ndef survey(texts, top):\n    cases = 0\n    wide = 0\n    cut = 0\n    fwide = 0\n    fcut = 0\n    for w in range(0, top+1):\n        for s in texts:\n            cases = cases + 1\n            a = naive(s, w)\n            b = fit(s, w)\n            if len(a) > w:\n                wide = wide + 1\n            if len(s) <= w and a != s:\n                cut = cut + 1\n            if len(b) > w:\n                fwide = fwide + 1\n            if len(s) <= w and b != s:\n                fcut = fcut + 1\n    return [cases, wide, cut, fwide, fcut]\n\ndef collisions(texts, width):\n    by = {}\n    order = []\n    for s in texts:\n        k = fit(s, width)\n        if k in by:\n            by[k] = by[k] + [s]\n        else:\n            by[k] = [s]\n            order = order + [k]\n    out = []\n    for k in order:\n        if len(by[k]) > 1:\n            out = out + [[k, by[k]]]\n    return out\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef collapse(s):\n    out = \"\"\n    gap = False\n    for c in trim(s):\n        if c == \" \":\n            gap = True\n        else:\n            if gap:\n                out = out + \" \"\n            gap = False\n            out = out + c\n    return out\n\ndef strip_all(s):\n    out = \"\"\n    for c in s:\n        if c != \" \":\n            out = out + c\n    return out\n\ndef tidy(how, s):\n    if how == \"trim\":\n        return trim(s)\n    if how == \"collapse\":\n        return collapse(s)\n    return strip_all(s)\n\ndef values():\n    return [\"Ann Lee\", \" Ann Lee\", \"Ann  Lee\", \"Ann Lee \", \"AB 12\", \"AB12\", \"A B12\", \"555 1234 567\", \"5551 234 567\", \"   \"]\n\ndef distinct(xs):\n    seen = {}\n    for x in xs:\n        seen[x] = 1\n    return len(seen)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\n\n1\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n1\nabc\n\n1\nabc\n61\n1\nabc\nx\n1\nabc\n0\n3\n\n3\n99\n3\n0\n5\n",
      "screen": "== Text truncation ==\nFitting text into a column with an ellipsis, and tidying whitespace.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> \nCancelled.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nUp to 60 characters.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> abc\nwidth (0 to 60)> \nCancelled.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> abc\nwidth (0 to 60)> 61\nType a whole number from 0 to 60.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> abc\nwidth (0 to 60)> x\nType a whole number from 0 to 60.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> abc\nwidth (0 to 60)> 0\n  fit:         []  0 characters\n  s[:w-3]+...: [...]  3 characters\n  The one-liner is 3 wider than the width.\n  With a width below 3 the stop -3 counts from the right end of the text.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 3\nwidth (0 to 60)> \nCancelled.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 3\nwidth (0 to 60)> 99\nType a whole number from 0 to 60.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 3\nwidth (0 to 60)> 0\nAt width 0, titles that come out the same:\n  []  from [Quarterly report: sales], [Quarterly report: staff], [Quarterly review], [Release notes 4.1], [Release notes 4.2], [Board meeting minutes], [Board meeting agenda], [Q3]\n8 titles show as 1 label.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nQuarterly report: sales\n12\n1\nQ3\n10\n1\nRelease notes\n2\n2\n3\n14\n3\n30\n4\n5\n",
      "screen": "== Text truncation ==\nFitting text into a column with an ellipsis, and tidying whitespace.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> Quarterly report: sales\nwidth (0 to 60)> 12\n  fit:         [Quarterly...]  12 characters\n  s[:w-3]+...: [Quarterly...]  12 characters\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> Q3\nwidth (0 to 60)> 10\n  fit:         [Q3]  2 characters\n  s[:w-3]+...: [Q3...]  5 characters\n  The text fits in 10, and the one-liner cut it anyway.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 1\ntext (up to 60 characters)> Release notes\nwidth (0 to 60)> 2\n  fit:         [..]  2 characters\n  s[:w-3]+...: [Release note...]  15 characters\n  The one-liner is 13 wider than the width.\n  With a width below 3 the stop -1 counts from the right end of the text.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 2\nThe 8 titles at every width from 0 to 30, 248 cases:\n  s[:w-3]+...: wider than the width 24 times, a fitting title cut 109 times\n  fit:         wider than the width 0 times, a fitting title cut 0 times\nA slice never complains about its bounds, so nothing about the one-liner fails loudly.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 3\nwidth (0 to 60)> 14\nAt width 14, titles that come out the same:\n  [Quarterly r...]  from [Quarterly report: sales], [Quarterly report: staff], [Quarterly review]\n  [Release not...]  from [Release notes 4.1], [Release notes 4.2]\n  [Board meeti...]  from [Board meeting minutes], [Board meeting agenda]\n8 titles show as 4 different labels.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 3\nwidth (0 to 60)> 30\nAt width 30 every title stays different.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 4\nValues: [Ann Lee]  [ Ann Lee]  [Ann  Lee]  [Ann Lee ]  [AB 12]  [AB12]  [A B12]  [555 1234 567]  [5551 234 567]  [   ]\n  as they are: 10 different\n  trim:      8 different; a second pass changes nothing for 10 of 10\n  collapse:  7 different; a second pass changes nothing for 10 of 10\n  strip all: 4 different; a second pass changes nothing for 10 of 10\n  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.\nThe blank value [   ] is something before a trim and nothing after: a required-field check gives opposite answers on either side of it.\n\n1) one text  2) every width  3) titles that come out the same  4) whitespace  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "text-truncate-ellipsis",
      "caseId": "213-text-truncate-ellipsis",
      "title": "Truncating with an ellipsis: three silent bugs in one line"
    },
    {
      "slug": "whitespace-normalization-loss",
      "caseId": "245-whitespace-normalization-loss",
      "title": "All three are idempotent; only one is lossless"
    }
  ],
  "updated": "2026-10-17"
}
