Skip to content

fc is how to compare without case

Level: 201 · you compare user input, keys or names regardless of case

One line: lc($a) eq lc($b) says Straße and STRASSE differ, and so do σας and ΣΑΣ; fc — fold case, use v5.16 and later — says they match, as /i already did; and along the way uc 'ß' is two letters, lc 'İ' is two code points, and no language's own rule, like Turkish I to ı, ever applies.

Measured

Verified output of fc_pl.pl — regenerated by tools/run_examples.py, never hand-typed.

--- lc, uc and fc ---
  Straße:  lc straße  uc STRASSE  fc strasse
  STRASSE:  lc strasse  uc STRASSE  fc strasse
  σας:  lc σας  uc ΣΑΣ  fc σασ
  ΣΑΣ:  lc σασ  uc ΣΑΣ  fc σασ
  İstanbul:  lc i̇stanbul  uc İSTANBUL  fc i̇stanbul

--- comparing ---
  lc('Straße') eq lc('STRASSE')   no
  fc('Straße') eq fc('STRASSE')   yes
  lc('σας') eq lc('ΣΑΣ')          no
  fc('σας') eq fc('ΣΑΣ')          yes
  'STRASSE' =~ /^straße$/i        yes

--- lengths change ---
  length 'ß' = 1, length uc 'ß' = 2
  length 'İ' = 1, length lc 'İ' = 2  (U+0069 U+0307)

--- title case is a third case ---
  ucfirst 'džungla' = Džungla  (U+01C5), uc 'džungla' = DŽUNGLA  (U+01C4)

--- one mapping for every language ---
  lc 'I' = i  (U+0069)   Turkish lower-cases I to ı, U+0131

Why lc is not enough

lc maps a string to lower case, and the lower case of STRASSE is strasse while the lower case of Straße is straße. Case folding is a different mapping, made for comparison: it sends both to one form, and ß folds to ss.

Greek has two lower-case sigmas, σ inside a word and ς at the end. fc sends both to σ. lc has no notion of a word's end, so lc('ΣΑΣ') is σασ — not the correctly spelled σας, and not equal to it.

/i in a regex already uses folding, so 'STRASSE' =~ /^straße$/i matches even though the lengths differ.

Lengths change

uc 'ß' is SS, one letter becoming two. lc 'İ' — the Turkish capital dotted I — is i followed by U+0307 COMBINING DOT ABOVE, one code point becoming two. Anything that assumed a case change keeps the length — a fixed-width field, a cursor position, an index computed before the change — is wrong afterwards.

Title case is a third case

dž is one code point standing for the Croatian digraph dž, and it has three cases: lower dž, upper DŽ and title Dž, for the start of a capitalised word. ucfirst uses the title case, uc the upper.

One mapping for every language

lc 'I' is i. In Turkish and Azerbaijani the lower case of I is ı, dotless, and İ pairs with i — but Perl's lc, uc and fc take no language, and apply Unicode's default mappings everywhere. A program that needs Turkish rules needs a library that takes a locale, such as ICU; the C library's ICU4C: Unicode text in C ↗ shows one.

What to do instead

fc($a) eq fc($b)                            # compare
$seen{ fc $name }++                         # key a hash
sort { fc($a) cmp fc($b) or $a cmp $b } @names   # sort, with a tie-break

Fold after normalizing, not instead of it — see length counts code points — and remember fc needs use v5.16 or later, which use v5.36 includes.

If you are coming from another language

See also