Last active
January 12, 2024 11:44
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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