Skip to content

Commit

Permalink
Merge pull request #29 from rivo/graphemestringwidth
Browse files Browse the repository at this point in the history
Fixed StringWidth() implementation by using proper Unicode grapheme cluster segmentation. Fixes #28
  • Loading branch information
mattn committed Jan 11, 2021
2 parents 14e809f + f1f639b commit 59616a2
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 52 deletions.
2 changes: 2 additions & 0 deletions go.mod
@@ -1,3 +1,5 @@
module github.com/mattn/go-runewidth

go 1.9

require github.com/rivo/uniseg v0.1.0
2 changes: 2 additions & 0 deletions go.sum
@@ -0,0 +1,2 @@
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
80 changes: 32 additions & 48 deletions runewidth.go
Expand Up @@ -2,6 +2,8 @@ package runewidth

import (
"os"

"github.com/rivo/uniseg"
)

//go:generate go run script/generate.go
Expand All @@ -10,9 +12,6 @@ var (
// EastAsianWidth will be set true if the current locale is CJK
EastAsianWidth bool

// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ
ZeroWidthJoiner bool

// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{}
)
Expand All @@ -30,7 +29,6 @@ func handleEnv() {
}
// update DefaultCondition
DefaultCondition.EastAsianWidth = EastAsianWidth
DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
}

type interval struct {
Expand Down Expand Up @@ -85,15 +83,13 @@ var nonprint = table{

// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
EastAsianWidth bool
ZeroWidthJoiner bool
EastAsianWidth bool
}

// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
ZeroWidthJoiner: ZeroWidthJoiner,
EastAsianWidth: EastAsianWidth,
}
}

Expand All @@ -110,66 +106,54 @@ func (c *Condition) RuneWidth(r rune) int {
}
}

func (c *Condition) stringWidth(s string) (width int) {
for _, r := range []rune(s) {
width += c.RuneWidth(r)
}
return width
}

func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
r1, r2 := rune(0), rune(0)
for _, r := range []rune(s) {
if r == 0xFE0E || r == 0xFE0F {
continue
}
w := c.RuneWidth(r)
if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
if width < w {
width = w
}
} else {
width += w
}
r1, r2 = r2, r
}
return width
}

// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
if c.ZeroWidthJoiner {
return c.stringWidthZeroJoiner(s)
g := uniseg.NewGraphemes(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
}
}
width += chWidth
}
return c.stringWidth(s)
return
}

// Truncate return string truncated with w cells
func (c *Condition) Truncate(s string, w int, tail string) string {
if c.StringWidth(s) <= w {
return s
}
r := []rune(s)
tw := c.StringWidth(tail)
w -= tw
width := 0
i := 0
for ; i < len(r); i++ {
cw := c.RuneWidth(r[i])
if width+cw > w {
w -= c.StringWidth(tail)
var width int
pos := len(s)
g := uniseg.NewGraphemes(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
if width+chWidth > w {
pos, _ = g.Positions()
break
}
width += cw
width += chWidth
}
return string(r[0:i]) + tail
return s[:pos] + tail
}

// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
width := 0
out := ""
for _, r := range []rune(s) {
cw := RuneWidth(r)
cw := c.RuneWidth(r)
if r == '\n' {
out += string(r)
width = 0
Expand Down
7 changes: 3 additions & 4 deletions runewidth_test.go
Expand Up @@ -243,7 +243,7 @@ func TestStringWidth(t *testing.T) {
c.EastAsianWidth = true
for _, tt := range stringwidthtests {
if out := c.StringWidth(tt.in); out != tt.eaout {
t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.eaout)
t.Errorf("StringWidth(%q) = %d, want %d (EA)", tt.in, out, tt.eaout)
}
}
}
Expand Down Expand Up @@ -400,9 +400,8 @@ func TestEnv(t *testing.T) {
}
}

func TestZeroWidthJointer(t *testing.T) {
func TestZeroWidthJoiner(t *testing.T) {
c := NewCondition()
c.ZeroWidthJoiner = true

var tests = []struct {
in string
Expand All @@ -414,7 +413,7 @@ func TestZeroWidthJointer(t *testing.T) {
{"‍🍳", 2},
{"👨‍👨", 2},
{"👨‍👨‍👧", 2},
{"🏳️‍🌈", 2},
{"🏳️‍🌈", 1},
{"あ👩‍🍳い", 6},
{"あ‍🍳い", 6},
{"あ‍い", 4},
Expand Down

0 comments on commit 59616a2

Please sign in to comment.