I run a small lorem ipsum generator. It has a "Characters" mode, and the page's FAQ said this about it:
Produces text trimmed to the exact character count, without cutting off in the middle of a word.
I never tested that sentence. Last week I did, with 2,000 runs per setting. The function hit the requested length about 15% of the time. That is not a rounding error, it is a broken promise, so this post is the test, the numbers, and the fix, including the part of the fix that I think is still a compromise.
Why anyone needs an exact count
Lorem ipsum by paragraph is fine for layouts. Exact characters matter when the container is strict: a meta description field, a 160-character SMS preview, a database column with VARCHAR(100), a tweet-sized card, a form validation test that needs a string of exactly the limit and one over it. "About 160" is useless there, because the bug you are hunting lives at 160 and 161.
Three ways to produce N characters
1. Cut at the limit.
text.slice(0, n)
Always the right length. But the last word is cut in the middle most of the time, and the text rarely ends in punctuation.
2. Trim back to the last space (what my page did). Generate more text than needed, cut at n + 1, move back to the last space, strip trailing ,;:, add a period.
let cut = text.slice(0, n + 1);
cut = cut.slice(0, cut.lastIndexOf(' ')).replace(/[,;:]+$/, '');
if (!/[.!?]$/.test(cut)) cut += '.';
It never cuts a word. It also lands on the target only when a word boundary happens to sit at exactly position n, and that is luck.
3. Search, then fall back to a cut. Repeat approach 2 on freshly generated text until the length equals n (up to 80 attempts). If none fits, hard-cut to n - 1 characters and append a period, swapping a trailing space or comma for a letter so the output doesn't end in , ..
function hardCut(text, n) {
let s = text.slice(0, n - 1);
if (/[\s,;:\-]$/.test(s)) s = s.slice(0, -1) + "e";
return s + ".";
}
The test
The generator builds sentences from a fixed vocabulary, so I could check three properties on every output:
length === n- it ends with
.,!or? - the last token is a whole word from the vocabulary (not a fragment)
I loaded the original function and the patched one into Node with vm, and ran each for targets 20, 50, 100, 160, 280, 500, 1000 and 5000, in three styles, 2,000 runs per cell. Classic style, n = 160:
| exact length | whole last word | when not exact | |
|---|---|---|---|
slice(0, n) |
100% | 19% | n/a |
| Trim to last space (my original) | 14.8% | 100% | short by 4 on average, up to 13 |
| Search, then hard cut | 100% | 100% | n/a |
The other targets from 100 to 5,000 look the same: the original is exact 14–16% of the time in classic style. Hipster was 11–14% and corporate 10% in my runs. It never overshoots (it always cuts back), so every miss is short, never long. The first user-visible symptom would be "I asked for 160 and got 156", which is easy to blame on the user's counting.
The case where exactness and clean words disagree
The page has a "Start with Lorem ipsum dolor sit amet…" option, and that prefix is 55 characters. With the prefix on and a small target, there is no sentence structure that both ends on a whole word and totals exactly n:
| target | original | new |
|---|---|---|
| 20 |
Lorem ipsum dolor. (18 chars, never 20) |
Lorem ipsum dolor s. |
| 50 |
Lorem ipsum dolor sit amet, consectetur. (40) |
Lorem ipsum dolor sit amet, consectetur adipiscin. |
In these two cases (n = 20 and 50 with the prefix on) the new code falls back to the hard cut in 100% of runs. So the contract became: exact length always; whole words whenever the arithmetic allows it. With the prefix off, targets 20 and 50 fit cleanly. For n ≥ 100 in classic style the fallback fired in 0 of 2,000 runs per target, and at most 0.1% in hipster and corporate.
Is adipiscin. acceptable lorem ipsum? For a length test, I think yes, and the alternative is a wrong length, which is the whole thing the mode exists to prevent. But it is a trade-off, and the FAQ should say so.
Does a search loop cost anything?
I counted attempts before a clean fit over 5,000 samples per target in classic style:
- mean about 6.8, median 5
- 95th percentile about 20, 99th about 30
- max 47–66 depending on target; never reached the 80-attempt cap
Each attempt generates a few hundred characters of text; I did not time it in a browser, so check that on your own target. If your vocabulary has longer words, the odds of an exact fit per attempt drop and the cap matters more. Measure rather than copy my 80.
A short detour: where the text comes from
The classic text is not random Latin. It is scrambled text from Cicero's De finibus bonorum et malorum (sections 1.10.32–33, 1st century BC), and it was traced there in the 1990s by Richard McClintock of Hampden–Sydney College, who looked up the rare word consectetur. Per Wikipedia, in a 2026 interview he said his earlier claim that the passage dates to the 1500s was a guess. Letraset sheets used it from 1966. It reads like Latin and carries no meaning, so reviewers look at the layout instead of the copy.
What the other generators say about exactness
Everything below is from each vendor's own page, as read in Nov 2026. "Not stated" means the page does not say.
| Generator | Units offered | Exact characters | Runs where |
|---|---|---|---|
| lipsum.com | paragraphs, words, bytes, lists | not stated (has bytes) | not stated |
| FlipMyCase | up to 20 paragraphs, 50 sentences or 500 words; 5 styles | not a unit (shows a character count) | browser, text "never sent to a server" |
| ToolTiny | paragraphs 1–50, sentences 1–100, words 1–500, characters 20–5,000; Classic, Hipster, Corporate, Standard | yes, per the tests above | browser |
I could not read LoremGen's and Genie9's pages (their robots.txt blocked my fetch), so I make no claim about them. Note that "bytes" and "characters" differ for non-ASCII text, which does not matter for lorem ipsum but does matter if you use the counts for other scripts.
Checklist if you write your own
- Say in the UI which property wins when two conflict (exact length vs whole words).
- Test the boundaries: the smallest allowed target, and a target equal to your prefix length.
- Assert
length === nin a loop with many runs, not one run. - Remember that JavaScript's
lengthcounts UTF-16 code units. It is exact for this ASCII vocabulary, not in general. - Cap the retry loop and have a deterministic fallback.
Limits of this test
- The vocabulary is my own, synthetic, with its own word-length distribution; a different one changes the hit rate of the trim approach.
- One JavaScript engine (Node 22). The string operations are standard, but I did not run other engines.
- "Original" means the function as it was when I started this test, and the page now uses the new one.
- Vendor statements are as of Nov 2026 and may change.
If you have a different approach to hitting an exact length without a mid-word cut (padding with a short word, choosing the vocabulary words to fit the remainder), I would like to compare it with the same harness.

Top comments (0)