Skip to content

Instantly share code, notes, and snippets.

@eNV25
Last active January 12, 2024 11:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save eNV25/57a3f76ffc6fbd7b1419a12acf17ad11 to your computer and use it in GitHub Desktop.
Save eNV25/57a3f76ffc6fbd7b1419a12acf17ad11 to your computer and use it in GitHub Desktop.
`wcwidth` implementation for Go. Uses the `unicode` and `golang.org/x/text/width` packages.
package textwidth
import (
"unicode"
"golang.org/x/text/width"
)
// IsComb returns true if r is a Unicode combining character. Alias of:
//
// unicode.Is(unicode.Mn, r)
//
func IsComb(r rune) bool { return unicode.Is(unicode.Mn, r) }
// RuneWidth returns fixed-width width of rune.
// https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms#In_Unicode
func RuneWidth(r rune) int {
if r == '\x00' || !unicode.IsPrint(r) || IsComb(r) {
return 0
}
k := width.LookupRune(r)
switch k.Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
case width.EastAsianNarrow, width.EastAsianHalfwidth, width.EastAsianAmbiguous, width.Neutral:
return 1
default:
return 0
}
}
// StringWidth returns fixed-width width of string.
// https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms#In_Unicode
func StringWidth(s string) (n int) {
for _, r := range s {
n += RuneWidth(r)
}
return n
}
package textwidth
import (
"testing"
"github.com/mattn/go-runewidth"
)
// test cases copied from https://github.com/mattn/go-runewidth/raw/master/runewidth_test.go
var stringwidthtests = []struct {
in string
out int
eaout int
}{
{"■㈱の世界①", 10, 12},
{"スター☆", 7, 8},
{"つのだ☆HIRO", 11, 12},
}
func BenchmarkStringWidth(b *testing.B) {
for i := 0; i < b.N; i++ {
StringWidth(stringwidthtests[i%len(stringwidthtests)].in)
}
}
func BenchmarkStringWidthOriginal(b *testing.B) {
for i := 0; i < b.N; i++ {
runewidth.StringWidth(stringwidthtests[i%len(stringwidthtests)].in)
}
}
func TestStringWidth(t *testing.T) {
for _, tt := range stringwidthtests {
if out := StringWidth(tt.in); out != tt.out {
t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.out)
}
}
//c := runewidth.NewCondition()
//c.EastAsianWidth = false
//for _, tt := range stringwidthtests {
// if out := c.StringWidth(tt.in); out != tt.out {
// t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.out)
// }
//}
//c.EastAsianWidth = true
//for _, tt := range stringwidthtests {
// if out := c.StringWidth(tt.in); out != tt.eaout {
// t.Errorf("StringWidth(%q) = %d, want %d (EA)", tt.in, out, tt.eaout)
// }
//}
}
var runewidthtests = []struct {
in rune
out int
eaout int
nseout int
}{
{'世', 2, 2, 2},
{'界', 2, 2, 2},
{'セ', 1, 1, 1},
{'カ', 1, 1, 1},
{'イ', 1, 1, 1},
{'☆', 1, 2, 2}, // double width in ambiguous
{'☺', 1, 1, 2},
{'☻', 1, 1, 2},
{'♥', 1, 2, 2},
{'♦', 1, 1, 2},
{'♣', 1, 2, 2},
{'♠', 1, 2, 2},
{'♂', 1, 2, 2},
{'♀', 1, 2, 2},
{'♪', 1, 2, 2},
{'♫', 1, 1, 2},
{'☼', 1, 1, 2},
{'↕', 1, 2, 2},
{'‼', 1, 1, 2},
{'↔', 1, 2, 2},
{'\x00', 0, 0, 0},
{'\x01', 0, 0, 0},
{'\u0300', 0, 0, 0},
{'\u2028', 0, 0, 0},
{'\u2029', 0, 0, 0},
{'a', 1, 1, 1}, // ASCII classified as "na" (narrow)
{'⟦', 1, 1, 1}, // non-ASCII classified as "na" (narrow)
{'👁', 1, 1, 2},
}
func BenchmarkRuneWidth(b *testing.B) {
for i := 0; i < b.N; i++ {
RuneWidth(runewidthtests[i%len(runewidthtests)].in)
}
}
func BenchmarkRuneWidthOriginal(b *testing.B) {
for i := 0; i < b.N; i++ {
runewidth.RuneWidth(runewidthtests[i%len(runewidthtests)].in)
}
}
func TestRuneWidth(t *testing.T) {
for i, tt := range runewidthtests {
if out := RuneWidth(tt.in); out != tt.out {
t.Errorf("case %d: RuneWidth(%q) = %d, want %d", i, tt.in, out, tt.out)
}
}
//c := runewidth.NewCondition()
//c.EastAsianWidth = false
//for _, tt := range runewidthtests {
// if out := c.RuneWidth(tt.in); out != tt.out {
// t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=false)", tt.in, out, tt.out)
// }
//}
//c.EastAsianWidth = true
//for _, tt := range runewidthtests {
// if out := c.RuneWidth(tt.in); out != tt.eaout {
// t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=true)", tt.in, out, tt.eaout)
// }
//}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment