How do you compare strings for UI versus for identifiers?

1.9.3 · L3 · Interview

Predict first

What does this print?

let composed = "é"
let decomposed = "e\u{301}"
print(composed == decomposed)
  • true
  • false

Mental model. For humans, “café” and “cafe” should probably match in a search. For a database key, they must not.

UI / search: use localized, human-facing comparison — localizedStandardCompare, localizedLowercase, formatted(), and .diacriticInsensitive search options. This respects the user's locale and collation.

Identifiers / caches / file names: use canonical equality (== on normalized strings), and avoid locale-dependent transforms — a Turkish locale changes the lowercase of I, which will break your key.

They are different operations with different correctness requirements. Do not reuse the UI comparator for keys.

Swift compares strings by canonical equivalence, so these are equal even though their bytes differ. For identifiers you usually want to normalise first and compare bytes.

Common traps

  • Using lowercased() for identifiers. It is locale-insensitive but still does full Unicode case folding; folding(options: .caseInsensitive, locale: nil) or a stable hash is usually better.
  • Assuming == is byte equality. Swift compares canonically equivalent strings as equal — good for users, but it means two different byte sequences can compare equal.
  • Localizing file names or keys. Locale-dependent transformations make data unreachable when the user changes region.

They might follow up with

  • What is localizedStandardCompare for? Finder-style ordering where “file 2” sorts before “file 10”.
  • How do you build a stable cache key? Normalize explicitly (e.g. NFC), then hash the UTF-8 bytes — do not use locale-aware transforms.

Related questions

  • 1.9.1 Why is String not a RandomAccessCollection of Character? Why is string[i + 1] not O(1)?
  • 1.9.2 Character vs Unicode.Scalar vs UTF-8 code unit?

← Character vs Unicode.Scalar vs UTF-8 code unit?