Help us shape the future of Go! Take the 2026 Go Developer Survey

namefold

package
v0.56.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package namefold is the one rule for when two spellings of a name are the same text: what a search compares, and the part of a tag's identity that is about letters rather than whitespace.

Two things make one name look like two, and both are folded here, in this order, for both sides of every comparison:

  1. How the letters are ENCODED. A keyboard types `ü` as one code point (Unicode normalisation form C); macOS hands a filename over decomposed, `u` followed by U+0308 COMBINING DIAERESIS (form D). filex stores the name it is given — the storage needs those bytes — so a catalogue holds both. Measured on a production catalogue on 2026-09-24: 71 388 of 169 471 names were decomposed, about nine in ten of the names with a Turkish letter in them (GitHub PR #46). Canonical composes.

  2. How the letters are CASED. Go's default case mapping is wrong for Turkish in exactly the letters a Turkish user types most: `I` lowers to `i`, not `ı`, and `ı` has no default partner at all, so "IŞIK" and "ışık" — one word — are different text to strings.ToLower. Turkish casing is wrong the other way: "INVOICE" lowers to "ınvoıce". So the four Latin i's — I, ı, İ, i — are ONE letter here, and everything else is lower-cased the default way. That is the rule internal/tagname has used for tags since v0.43.0; a search that disagreed with it would call a tag and the file name it was copied from two different words.

The price of (2) is that two Turkish words spelt apart only by the dot — "ılık" and "ilik" — are one word to a search. The opposite rule fails every capitalised Turkish name typed on an English keyboard and every English name typed on a Turkish one.

Accents stay significant: "müşteri" and "musteri" are different words. The stored name is never changed; this is how names are COMPARED.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AllPlain

func AllPlain(s string) bool

AllPlain reports whether every rune of a non-empty s is Plain.

func Canonical

func Canonical(s string) string

Canonical returns s composed (NFC), with a dot above dropped where it sits on a letter that already has one or is about to lose it: any of the four i's, or j.

The dot: lower-casing `İ` the full Unicode way — JavaScript's toLowerCase(), Python's lower() — gives `i` + U+0307, which composition leaves as it is, so a client or an agent that lower-cases a query before sending it would never meet the `i` on the other side (found verifying PR #46 against the production catalogue, where 4 of 10 sampled names carried an `İ`).

A string that is already composed and holds no U+0307 — ASCII among them — is returned as it is, without allocating: this runs on every candidate of every search. ⚠ QuickSpanString, not IsNormalString: the latter allocates on every call, fast path included (two allocations per candidate in BenchmarkScorerPerCandidate, which is otherwise flat zero). U+0307 can compose, so a string holding it never takes the fast path.

func IsI

func IsI(r rune) bool

IsI reports whether r is one of the four Latin i's.

func Loose added in v0.54.0

func Loose(s string) string

Loose is String with the accents dropped as well: "Güvenlik" and "guvenlik", "şifre" and "sifre" are one word to it.

⚠ It is NOT how a file name is searched - accents count there (the package comment: "müşteri" is not "musteri"), and the search, the explorer's folder filter (search.NameMatcher) and every store comparison use String. Loose is the rule for words a person types to find a LABEL - a page of the admin panel, a setting, a colleague's name - on whatever keyboard they have (the admin panel's search, handlers/panel_search.go). It lives here, beside String, so the two rules are one package and one definition each (filex 0.54, audit D5: the panel had its own copy).

func Plain

func Plain(r rune) bool

Plain reports whether r, an ASCII letter or digit, is stored as itself in every spelling of a name that folds to it — so a database can look for it in the stored bytes as they are, with nothing but its own ASCII case folding, and cheaply. Every ASCII letter and digit is, except three:

  • `i`: `ı`, `I` and `İ` fold to it, and a stored i may carry a dot above that Canonical drops;
  • `j`: a stored j may carry a dot above that Canonical drops;
  • `k`: U+212A KELVIN SIGN composes to `K`.

TestPlain_NothingElseFoldsIntoPlainASCII walks every code point to keep this list honest.

func PlainPrefix

func PlainPrefix(s string) string

PlainPrefix is the longest prefix of s made of Plain letters and digits: all of s when AllPlain(s). A folded word that is all Plain is stored as it is in every spelling of a name that folds to it, so a database finds it with its own case-insensitive LIKE — no normaliser needed, and none of its cost; a word that is not can still be narrowed by its plain prefix.

func Rune

func Rune(r rune) rune

Rune is the case fold of one rune: the four Latin i's are one letter, `i`; everything else is lower-cased the Unicode default way. One rune in, one rune out, so a caller that compares position by position (the scorer) keeps its positions.

func String

func String(s string) string

String is s as a search compares it: Canonical, then Rune on every rune. Allocation-free for a string that is already composed and folded.

func Words

func Words(words []string) []string

Words is each of words through String, empty results and repeats dropped, in their first order — the words a store compares a folded name with.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL