introducing leancase
i noticed that more and more people write like this. no capital letters, no period at the end of a line. it saves typing, and it removes noise when eyes parse the text, so i gave it a name: leancase
"lean" means minimal, it pairs with "lowercase", and it's a nod to lean, the language
the rules
- everything is lowercase, including sentence starts, names, and acronyms: ai, lean, wasm, c
- links with a scheme, @handles, #hashtags, and code marked with backticks keep their exact case, because changing them can break them or change their meaning
- no period at the end of a line or paragraph
- periods between sentences on the same line stay
- punctuation that carries meaning stays: commas, colons, apostrophes, hyphens in compounds like "ai-generated," and question marks
- spelling and grammar stay correct. the casualness is in the look, not in mistakes, which is what separates leancase from sloppy texting
- line breaks separate thoughts, roughly one idea per paragraph
- wording never changes. leancase only touches capitalization and final periods
examples
every pair below runs as a test:
import { assertEqual, leancase } from "@evolu/common";
assertEqual(
leancase("I noticed people write like this. It saves typing."),
"i noticed people write like this. it saves typing",
);
assertEqual(
leancase("I build AI tools with Lean, WASM, and C."),
"i build ai tools with lean, wasm, and c",
);
assertEqual(
leancase(
"Call `leancase()`, then read https://evolu.dev/docs/api-reference/common/Leancase.",
),
"call `leancase()`, then read https://evolu.dev/docs/api-reference/common/Leancase",
);
assertEqual(
leancase("Thanks @Steida for #LocalFirst!"),
"thanks @Steida for #LocalFirst!",
);
assertEqual(leancase("Wait..."), "wait...");
assertEqual(leancase("Is it idempotent?"), "is it idempotent?");
assertEqual(leancase("Apples, pears, etc."), "apples, pears, etc");
assertEqual(leancase("(An aside.)"), "(an aside)");
assertEqual(
leancase(
"Writing code is dead, long live reading tests, er, learning formal verification then formalizing all the things.",
),
"writing code is dead, long live reading tests, er, learning formal verification then formalizing all the things",
);
why it works
leancase marks every boundary once. a line break already ends a sentence, so a period before it repeats it. a period in the middle of a line already ends a sentence, so a capital letter after it repeats it. leancase drops the repeats
it also reads the way people talk: speech has no capital letters, and a line break works like a pause
names and acronyms are different. their capitals are not repeats, they carry information: us and US, it and IT, go and Go. leancase drops them anyway for a uniform look, and context almost always tells them apart
a normal form
rule 8 is the important one. it makes leancase a pure formatting transform: any text converts mechanically, and converting it again changes nothing. in lean terms, the conversion is idempotent, so leancase is a normal form: exactly the text the conversion leaves unchanged
the conversion enforces rules 1 to 4, and rules 5 and 8 say it changes nothing else. rules 6 and 7 are writing advice that no conversion can check
that's also how evolu validates it. LeancasedString accepts a string only when leancase returns it unchanged, the same way TrimmedString accepts a string only when trim does
try it
converting the result again changes nothing
in evolu
leancase ships in @evolu/common as an experimental api:
import {
assertEqual,
assertOk,
leancase,
LeancasedString,
} from "@evolu/common";
const docs = "https://evolu.dev/docs/api-reference/common/Leancase";
const post = leancase(`Thanks, @Steida. See ${docs}.`);
assertEqual(post, `thanks, @Steida. see ${docs}`);
assertOk(LeancasedString.fromUnknown(post), post);
the edge cases
rules for people can be loose. rules for code have to be exact, so these are the decisions behind leancase and LeancasedString:
- code keeps its case only when it's marked with backticks or a code fence. unmarked,
toLeancasewould become toleancase - a url needs a scheme, like
https://. bare domains such as evolu.dev are lowercased with any path after them, so write the scheme when a path is case-sensitive - a url can start right after other text, as in japanese, where no space comes before it
- a period hiding behind closing marks is still a final period:
(an aside.)becomes(an aside) - two or more periods in a row stay, so an ellipsis survives...
- an abbreviation at the end of a line loses its period:
etc.becomesetc - a line with nothing but periods, spaces, and closing marks ends no sentence, so it stays
- lowercasing ignores the locale, so your language settings don't change the result. the price is turkish, where
Ishould become a dotlessı - markdown is not supported. a soft line break inside a paragraph renders as a space, so dropping the period before it would merge two sentences. this post applies leancase per paragraph, not per source line
from tests to proofs
writing code is dead, long live reading tests, er, learning formal verification then formalizing all the things
leancase was specified by tests first: an example for every rule and edge case, then property tests with fast-check that throw thousands of random strings at three claims. converting twice equals converting once. line by line, only case changes and only final periods are removed. the result is always a valid LeancasedString
the property tests found a bug the examples missed. a line with nothing but a period between \r and \n lost the period, and the two line breaks merged into one windows line break. that's how the rule about lines without content was born
thousands of random strings make idempotence very likely, not certain. a proof in lean would make it certain
experimental
the rules may still change, so leancase is experimental and can change in a minor or patch release. if you find text where it does the wrong thing, open an issue
is this a joke?
half a joke, half serious. i noticed a writing style and wanted to give it a name. once the name had exact rules, code could convert text to it and validate it, and both can be useful:
- a chat or social app can offer leancase as a style for posts, so people who like the look don't have to fight their phone's autocapitalization
- an app whose ui text is all lowercase can validate it and its translations with
LeancasedString, so a capital letter or a final period never ships by accident - short casual text, such as notifications, status lines, and bot messages, can be written normally and converted to leancase before it's shown