internal/export/unicode: add CategoryAliases, Cn, and LC

CategoryAliases is for regexp to use, for things like \p{Letter} as an alias for \p{L}.
Cn and LC are special-case categories that were never implemented
but should have been.

For golang/go#70780.

Change-Id: I1401c1be42106a0ebecabb085c25e97485c363cf
Reviewed-on: https://go-review.googlesource.com/c/text/+/641395
Auto-Submit: Russ Cox <rsc@golang.org>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
diff --git a/internal/export/unicode/gen.go b/internal/export/unicode/gen.go
index 4e8295c..01cfe55 100644
--- a/internal/export/unicode/gen.go
+++ b/internal/export/unicode/gen.go
@@ -13,8 +13,10 @@
 	"flag"
 	"fmt"
 	"log"
+	"maps"
 	"os"
 	"regexp"
+	"slices"
 	"sort"
 	"strings"
 	"unicode"
@@ -90,13 +92,15 @@
 var category = map[string]bool{
 	// Nd Lu etc.
 	// We use one-character names to identify merged categories
-	"L": true, // Lu Ll Lt Lm Lo
-	"P": true, // Pc Pd Ps Pe Pu Pf Po
-	"M": true, // Mn Mc Me
-	"N": true, // Nd Nl No
-	"S": true, // Sm Sc Sk So
-	"Z": true, // Zs Zl Zp
-	"C": true, // Cc Cf Cs Co Cn
+	"L":  true, // Lu Ll Lt Lm Lo
+	"LC": true, // Lu Ll Lt
+	"P":  true, // Pc Pd Ps Pe Pu Pf Po
+	"M":  true, // Mn Mc Me
+	"N":  true, // Nd Nl No
+	"S":  true, // Sm Sc Sk So
+	"Z":  true, // Zs Zl Zp
+	"C":  true, // Cc Cf Cs Co Cn
+	"Cn": true, // unassigned
 }
 
 // This contains only the properties we're interested in.
@@ -149,6 +153,9 @@
 }
 
 func loadChars() {
+	for code := range chars {
+		chars[code].category = "Cn" // unassigned
+	}
 	ucd.Parse(gen.OpenUCDFile("UnicodeData.txt"), func(p *ucd.Parser) {
 		c := Char{codePoint: p.Rune(0)}
 
@@ -201,6 +208,7 @@
 }
 
 var categoryMapping = map[string]string{
+	"LC": "Letter, cased: Ll | Lt | Lu",
 	"Lu": "Letter, uppercase",
 	"Ll": "Letter, lowercase",
 	"Lt": "Letter, titlecase",
@@ -257,6 +265,7 @@
 			printf("\t%q: %s,\n", k, k)
 		}
 		print("}\n\n")
+		printCategoryAliases()
 	}
 
 	decl := make(sort.StringSlice, len(list))
@@ -315,14 +324,14 @@
 		}
 		decl[ndecl] = varDecl
 		ndecl++
+		match := func(cat string) bool { return cat == name }
 		if len(name) == 1 { // unified categories
-			dumpRange(
-				"_"+name,
-				func(code rune) bool { return categoryOp(code, name[0]) })
-			continue
+			match = func(cat string) bool { return strings.HasPrefix(cat, name) }
 		}
-		dumpRange("_"+name,
-			func(code rune) bool { return chars[code].category == name })
+		if name == "LC" { // special unified category
+			match = func(cat string) bool { return cat == "Ll" || cat == "Lt" || cat == "Lu" }
+		}
+		dumpRange("_"+name, func(code rune) bool { return match(chars[code].category) })
 	}
 	decl.Sort()
 	println("// These variables have type *RangeTable.")
@@ -333,6 +342,35 @@
 	print(")\n\n")
 }
 
+func printCategoryAliases() {
+	known := make(map[string]bool)
+	for _, name := range allCategories() {
+		known[name] = true
+	}
+
+	table := make(map[string]string)
+	ucd.Parse(gen.OpenUCDFile("PropertyValueAliases.txt"), func(p *ucd.Parser) {
+		if p.String(0) != "gc" {
+			return
+		}
+		name := p.String(1)
+		if !known[name] {
+			logger.Print("unknown category: ", name)
+		}
+		table[p.String(2)] = name
+		if a := p.String(3); a != "" {
+			table[a] = name
+		}
+	})
+
+	println("// CategoryAliases maps category aliases to standard category names.")
+	println("var CategoryAliases = map[string]string{")
+	for _, name := range slices.Sorted(maps.Keys(table)) {
+		printf("\t%q: %q,\n", name, table[name])
+	}
+	print("}\n\n")
+}
+
 type Op func(code rune) bool
 
 func dumpRange(name string, inCategory Op) {