Skip to content
Allin

What a Palindrome Checker Has to Decide Before It Can Answer

Published 6/2/2025 · 12 min read · Text & language tools

Daniel Okonkwo

Daniel OkonkwoFront-end developer and tech writer at Allin

Web performance · File formats

Checked against 5 sources

View profile
In short

"Is this a palindrome" has no answer until four normalisation choices are made, and each one changes the verdict on real sentences. Run in Node: "A man, a plan, a canal: Panama" is not a palindrome as typed, nor with case folded, nor with whitespace removed — it becomes one only once punctuation is dropped, giving amanaplanacanalpanama. The Italian "I topi non avevano nipoti" needs case and whitespace but no punctuation rule. The German "Ein Esel lese nie" is already a palindrome with case folding alone, because its spaces happen to fall symmetrically. And the French "Ésope reste ici et se repose", the Spanish "Dábale arroz a la zorra el abad" and the Portuguese "Socorram-me, subi no ônibus em Marrocos" all need a fourth rule, diacritic folding, because É against e and á against a are different characters. The second half of the problem is that reversing a string is itself undefined. Reversing by UTF-16 code unit splits surrogate pairs and produces text that is not well-formed; reversing by code point detaches a combining accent from its letter, turning a decomposed Grüße into eß̈urG. Reverse by grapheme cluster, with Intl.Segmenter, and state the four policies.

Case, whitespace, punctuation and diacritics: four policies, six real sentences, and the answer changes with each one. Then the harder half — reversing a string is itself undefined, and code-unit reversal breaks emoji and detaches accents, demonstrated in Node.

The question is undefined until four choices are made

A palindrome reads the same forwards and backwards. That definition is complete only for a string of bare lowercase letters, and almost no real text is one. The moment a sentence has capitals, spaces, commas or accents, the question splits into four smaller ones: does an uppercase letter equal its lowercase form, do spaces count as characters, do punctuation marks count, and is an accented letter the same letter as its unaccented base? Four yes-or-no answers, sixteen combinations, and the ones that matter form a nested sequence, each admitting more sentences than the last.

The table above is that sequence applied to six sentences, run rather than argued. Read it column by column and every column earns its place: whitespace alone promotes the Italian sentence, punctuation alone promotes the English one, and diacritic folding alone promotes the French, Spanish and Portuguese ones. A checker that hard-codes any one of these choices is answering a different question from the one its user asked, and answering it silently.

Four sentences, four different stopping points

Start with the English one, because it is the sentence everybody quotes. "A man, a plan, a canal: Panama" is not a palindrome as typed — the capital A at one end faces a lowercase a at the other. Fold the case and it still is not one: the spaces do not fall symmetrically. Remove the whitespace and it still is not one, because a comma and a colon remain. Remove the punctuation as well and it finally is: amanaplanacanalpanama, twenty-one characters, symmetric. Three policies had to be switched on before the famous answer appeared.

The German "Ein Esel lese nie" stops one step earlier than everything else in the table. Fold the case and it is already a palindrome: einesellesenie reads the same both ways, and so does the sentence with its spaces left in place, because its word lengths — three, four, four, three — are symmetric by construction. A checker that always strips whitespace gets the right answer here, but for the wrong reason, and it will never be able to tell you that this sentence is a palindrome in the stronger sense that its spaces are too. The other German example, "O, Genie, der Herr ehre dein Ego!", needs the punctuation rule and nothing more.

The three Romance sentences all need the fourth rule and fail every earlier one. "Ésope reste ici et se repose" with punctuation stripped but accents kept gives ésoperesteicietserepose, whose reverse is esoperesteicietsereposé — the accent has moved from the first character to the last, and the strings differ. Fold the diacritic and both become esoperesteicietserepose. The same happens with dábale…, where the reverse ends in á, and with socorram-me…, where the ô lands in the wrong place. Note what this means in practice: a checker built for English will report three perfectly good European palindromes as failures, and will do it without any error message.

Reversing a string is not one operation either

Every palindrome checker compares a text with its reverse, and the reverse is where the second half of the problem lives. The one-line reversal everybody writes — s.split("").reverse().join("") — reverses UTF-16 code units. A companion article in this series covers what a character is; the consequence here is that any character outside the Basic Multilingual Plane is stored as two code units, and reversing them individually swaps the pair. Reverse "ça va 😀" that way and the result contains a low surrogate followed by a high surrogate: eight code points instead of seven, and isWellFormed() returns false. On screen it is a replacement box.

The usual fix is to reverse code points instead: [...s].reverse().join(""). It repairs the emoji — "ça va 😀" becomes "😀 av aç", still seven code points — and it is still not enough. Take "José" in decomposed form, where the é is an e followed by a combining acute accent: five code points, four graphemes. Reversing the code points puts the combining accent first, where it has no letter to attach to, and leaves a bare e: the result is five graphemes where there were four, and the accent renders on nothing. The German case is more vivid still. Decompose "Grüße" and reverse its code points and you get eß̈urG: the umlaut has jumped off the u and landed on the ß.

Emoji built from several code points fail the same way. The family 👨‍👩‍👧 is three people joined by zero-width joiners: sixteen code units, thirteen code points, one grapheme. Reversing its code points produces a family that renders as 👧‍👩‍👨 — daughter, mother, father — which is a different emoji, not a reversed one, and reversing its code units produces something that renders as fragments. Only reversing by grapheme cluster leaves it untouched.

Reverse by grapheme cluster

A grapheme cluster is Unicode's name for what a reader calls a character: a base letter plus whatever combines onto it, or a whole emoji sequence. JavaScript exposes the segmentation rules directly through Intl.Segmenter, and a correct reversal is three lines: build a segmenter with granularity "grapheme", spread its output, reverse, join. On every example above it does the right thing — the emoji survives, the family stays a family, and the decomposed Grüße comes back as eßürG, with the umlaut still on its u.

There is a normalisation decision hiding underneath, and the French word été shows it in one line. In composed form it is three code points and reversing them gives été back: a palindrome. In decomposed form it is five code points, reversing them gives a string with the accents in the wrong places, and the same visible word is reported as not a palindrome. Reversing by grapheme cluster gives the right answer in both forms — but if you compare the two forms to each other they are still unequal, which is why a checker should normalise to NFC before it does anything else. The article on accents in this series covers the NFC and NFD forms in full.

A number palindrome is a different problem

Numbers look like the easy case and are not. A number palindrome is a property of a value in a base, and a text palindrome is a property of a rendering. 12321 is a palindrome in base ten. Rendered for a reader it stops being one in every locale in this article: 12,321 reversed is 123,21 in English, 12.321 reversed is 123.21 in German, Spanish and Italian, and 12 321 reversed is 123 21 in French and Portuguese — where the separator is a narrow no-break space U+202F in French and a plain non-breaking space U+00A0 in Portuguese, two invisible characters that a naive comparison will treat as different anyway.

Three more differences follow from the same distinction. A sign is a character but not a digit: -121 as text reverses to 121- and fails, while as a value the question of whether a negative number can be a palindrome is a convention you have to pick. A decimal point is likewise a character: 0.1 reverses to 1.0, two different strings that denote different values. And leading zeros exist in text and not in numbers: 010 is a palindrome as text, and as a number it is 10, which is not. If your input is a number, strip the formatting and work on the digits; if your input is text that happens to look numeric, say so and treat it as text.

What to build, in order

The recipe that survives all six languages is short. Normalise to NFC first, so that the same visible text always has the same representation. Then apply the policies you have decided on and can name: lowercase, drop whitespace, drop everything that is not a letter or a digit, fold diacritics by decomposing and removing the marks. Then reverse by grapheme cluster and compare. Four steps, and every one of them is a choice you can put in the interface rather than a default you hide in the code.

One last observation about the reversal, because it is counter-intuitive and it hides bugs. Reversing twice always returns the original string, whichever unit you reverse by — code units, code points or graphemes are all involutions on the sequence they operate on. So a round-trip test tells you nothing about whether your reversal is correct. The only test that catches the broken cases is looking at the intermediate result, and the fastest way to look at it is to count graphemes before and after: for a decomposed José they are four and five.

Six sentences under four cumulative policies, run in Node 26.3. Each column adds one rule to the one before it, and each column changes at least one verdict — which is the whole argument that "palindrome" is not a property of a text but of a text plus a policy.
SentenceCase folded only+ whitespace ignored+ punctuation ignored+ diacritics folded
A man, a plan, a canal: PanamaNoNoYesYes
I topi non avevano nipotiNoYesYesYes
Ein Esel lese nieYesYesYesYes
Ésope reste ici et se reposeNoNoNoYes
Dábale arroz a la zorra el abadNoNoNoYes
Socorram-me, subi no ônibus em MarrocosNoNoNoYes
Palindrome checkerCheck whether your text reads the same forwards and backwards.Try the tool

Frequently asked questions

Is "A man, a plan, a canal: Panama" a palindrome or not?
Under the usual policy, yes; as typed, no. Fold the case, drop the spaces and drop the punctuation and it becomes amanaplanacanalpanama, which is symmetric. Keep any one of those three and it is not. That is not a trick question — it is what the phrase "ignoring case, spaces and punctuation" in every textbook definition is doing, and a tool that lets you switch the three rules independently is telling you the truth rather than hiding it.
Why did reversing my text produce a broken square?
Because the reversal worked on UTF-16 code units and your text contained a character outside the Basic Multilingual Plane — an emoji, a rare CJK ideograph, a mathematical symbol. Those are stored as a surrogate pair, and reversing the two halves produces a sequence no font can render. Check it with isWellFormed(), which returns false on such a string, and switch the reversal to grapheme clusters with Intl.Segmenter.
Should a palindrome checker ignore accents?
It should offer the choice and default to folding them, because the tradition in French, Spanish and Portuguese wordplay treats é and e as the same letter. Without folding, three of the six sentences in the table above stop being palindromes: Ésope reste ici et se repose, Dábale arroz a la zorra el abad and Socorram-me, subi no ônibus em Marrocos all fail on their first and last characters. Fold by decomposing to NFD, deleting the combining marks, and recomposing — the accents article in this series covers why that is not the same as replacing letters one by one.
Is 12321 a palindrome?
As a number in base ten, yes. As displayed text, no — in every locale in this article. Formatted for a reader it becomes 12,321 in English and 12.321 or 12 321 elsewhere, and reversing those strings gives 123,21, 123.21 and 123 21. If you are checking numbers, work on the digits and decide explicitly what to do with a sign, a decimal separator and leading zeros: -121 reverses to 121-, 0.1 reverses to 1.0, and 010 is a palindrome as text while the number it denotes is 10.
Does reversing a string twice always give the original back?
Yes, and that is exactly why a round-trip test is worthless here. Reversing by code unit, by code point or by grapheme cluster are all involutions: apply any of them twice and you get the input back, even when the single reversal produced a broken string in between. Test the intermediate result instead — compare the grapheme count before and after, or call isWellFormed() on it.
Can a single word be a palindrome without any of these rules?
Yes, and those are the only cases where the question has one answer. All-lowercase ASCII words such as racecar, radar, kayak, the French ressasser, the Spanish reconocer, the Italian onorarono and the Portuguese reviver are palindromes with no policy at all — verified character by character. Capitalise them and you need the case rule: the German Reliefpfeiler, Rentner and Lagerregal are palindromes only once the initial capital is folded, which is one policy and not four.

Articles you may find interesting

All guides
ExplainerDetecting the Language of a Text, and Why Short Texts FailMeasured, not asserted: 90 short real phrases across six languages, none declined and 68 right — 76%, falling to 64% under sixteen letters. Four of the wrong answers came back at 100% confidence.ExplainerRemoving Accents Breaks Search — Until You Do It on Both SidesFolding diacritics is a normalisation step, and normalisation only works when the same function runs on the index and on the query. NFC against NFD with the code points shown, and the letters — ø, ł, ß, œ, ı — that survive the strip untouched.ExplainerEmoji Are Harder Than They Look: Why "Just Strip the Emoji" Has No One-Line AnswerOne visible emoji can be one code point or fourteen UTF-16 units. We ran three popular regexes against a real sentence and each broke differently — one deleted the digits. Here is why, which Unicode property answers which question, and the grapheme-cluster rule that actually works.How-toNumbering the Lines of a Text for a Review with Several PeopleNumbering starts at 1 and cannot be set to 0, the alignment is spaces rather than zeros, and the remover undoes eight of the eleven separators without touching the indentation. What it still cannot do is tell your numbers from its own.ExplainerSentence Case and Title Case: The Rules Differ by LanguageEnglish title case has three different cutoffs depending on the style guide. French, Spanish, Portuguese and Italian have none at all. German capitalises every noun. The tool knows about none of this — here is exactly what it does.How-toFiltering Lines by a Pattern Without a Command LineThis is grep for people who do not use grep, with one important difference: the match is a plain substring, so a real regular expression returns an empty box and no error. Every claim here was checked by running the tool.

Related tools

Sources

Spotted a mistake in this article?