In Go, string character values are Unicode characters encoded in UTF-8. UTF-8 is a variable-length encoding which uses one to four bytes per character.
Rewriting your ASCII code for Unicode,
unique.go
:
package main
import (
"fmt"
"unicode/utf8"
)
func ascii(s string) (int, bool) {
if len(s) > utf8.RuneSelf {
return 0, false
}
var ascii [utf8.RuneSelf]bool
for i := 0; i < len(s); i++ {
b := int(s[i])
if b >= utf8.RuneSelf {
return 0, false
}
if ascii[b] {
return len(s), false
}
ascii[b] = true
}
return len(s), true
}
func hashmap(s string) bool {
var chars = make(map[rune]struct{}, len(s)/(utf8.UTFMax-1))
for _, r := range s {
if _, ok := chars[r]; ok {
if r != utf8.RuneError {
return false
}
}
chars[r] = struct{}{}
}
return true
}
func unique(s string) bool {
if i, u := ascii(s); i >= len(s) {
return u
}
var chars []uint8
for _, r := range s {
if int(r) >= 8*len(chars) {
var t []uint8
if r >= rune(1<<16) {
if len(s) <= 1000 {
return hashmap(s)
}
t = make([]uint8, (utf8.MaxRune+1+7)/8)
} else if r >= rune(1<<8) {
t = make([]uint8, (1<<16)/8)
} else {
t = make([]uint8, (1<<8)/8)
}
copy(t, chars)
chars = t
}
char := chars[r/8]
if char&(1<<uint(r%8)) != 0 {
if r != utf8.RuneError {
return false
}
}
char |= (1 << uint(r%8))
chars[r/8] = char
}
return true
}
func main() {
var r [utf8.MaxRune + 1]rune
for i := range r {
r[i] = rune(i)
}
s := string(r[:])
fmt.Println(unique(s))
r[0] = r[len(r)-1]
s = string(r[:])
fmt.Println(unique(s))
}
Playground: https://play.golang.org/p/31i7YLzJDhM
Output:
true
false
A benchmark using the 95 printable ASCII characters.
$ go test unique.go ascii_test.go -bench=. -benchmem
asciiChar: n 95; min U+0020; max U+007E
BenchmarkASCII-8 18052088 66.0 ns/op 0 B/op 0 allocs/op
$
ascii_test.go
:
package main
import (
"fmt"
"testing"
"unicode"
)
// ASCII printable characters
// asciiChar: n 95; min U+0020; max U+007E
var asciiChar = func() []rune {
var ascii []rune
for r := rune(0); r <= unicode.MaxASCII; r++ {
if unicode.IsPrint(r) {
ascii = append(ascii, r)
}
}
fmt.Printf("asciiChar: n %d; min %U; max %U\n", len(ascii), ascii[0], ascii[len(ascii)-1])
return ascii
}()
var u bool
func BenchmarkASCII(b *testing.B) {
s := string(asciiChar)
b.ResetTimer()
for N := 0; N < b.N; N++ {
u = unique(s)
if !u {
b.Fatal(u)
}
}
}
A benchmark (stress test) using the 89,233 Unicode Han script characters. For Version 12.0 of the Unicode Standard (2019) there are a total of 137,929 characters.
$ go test unique.go han_test.go -bench=. -benchmem
hanChar: n 89233; min U+2E80; max U+2FA1D
BenchmarkHan-8 1602 740748 ns/op 147456 B/op 2 allocs/op
$
han_test.go
:
package main
import (
"fmt"
"testing"
"unicode"
)
// Unicode Han script
// hanChar: n 89233; min U+2E80; max U+2FA1D
var hanChar = func() []rune {
var han []rune
for r := rune(0); r <= unicode.MaxRune; r++ {
if unicode.In(r, unicode.Han) {
han = append(han, r)
}
}
fmt.Printf("hanChar: n %d; min %U; max %U\n", len(han), han[0], han[len(han)-1])
return han
}()
var u bool
func BenchmarkHan(b *testing.B) {
s := string(hanChar)
b.ResetTimer()
for N := 0; N < b.N; N++ {
u = unique(s)
if !u {
b.Fatal(u)
}
}
}
*string
argument here instead of a simplestring
. Although Go is pass-by-value the value of a Gostring
is effectively two int-sized values (an integer string length and a pointer to the start of the string) so passing a string of any size by value is efficient. \$\endgroup\$