Skip to content

Commit 37796e5

Browse files
committed
Optimize EastAsian RuneWidth with precomputed width table
- Build a merged width table (eastAsianWidth) at init that combines the zero-width and wide intervals into a single sorted list carrying the cell width, so the EastAsian path resolves a rune with one binary search instead of the previous zerowidth/narrow/widewidth chain - Precompute widths for r < 0x300 into a 768-byte array (eastAsianWidth0) for a direct array lookup on the most common runes - Flatten the non-EastAsian RuneWidth switch into early returns - Replace the isAllASCII pre-scan in StringWidth with a single combined ASCII scan that falls through to the grapheme segmenter on first non-ASCII byte, and add a single-byte fast path Verified by TestRuneWidthChecksums.
1 parent e8dc57a commit 37796e5

1 file changed

Lines changed: 117 additions & 34 deletions

File tree

runewidth.go

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package runewidth
22

33
import (
44
"os"
5+
"sort"
56
"strings"
67
"unicode/utf8"
78

@@ -25,13 +26,19 @@ var (
2526
)
2627

2728
var (
28-
zerowidth table // combining + nonprint merged for faster zero-width lookup
29-
widewidth table // ambiguous + doublewidth merged for EA path
29+
zerowidth table // combining + nonprint merged for faster zero-width lookup
30+
widewidth table // ambiguous + doublewidth merged for EA path
31+
eastAsianWidth widthTable
32+
eastAsianWidth0 [0x300]byte
3033
)
3134

3235
func init() {
3336
zerowidth = mergeIntervals(combining, nonprint)
3437
widewidth = mergeIntervals(ambiguous, doublewidth)
38+
eastAsianWidth = makeWidthTable(zerowidth, widewidth)
39+
for r := range eastAsianWidth0 {
40+
eastAsianWidth0[r] = byte(runeWidthEastAsian(rune(r)))
41+
}
3542
handleEnv()
3643
}
3744

@@ -90,6 +97,14 @@ type interval struct {
9097

9198
type table []interval
9299

100+
type widthInterval struct {
101+
first rune
102+
last rune
103+
width byte
104+
}
105+
106+
type widthTable []widthInterval
107+
93108
func inTable(r rune, t table) bool {
94109
if r < t[0].first {
95110
return false
@@ -116,6 +131,71 @@ func inTable(r rune, t table) bool {
116131
return false
117132
}
118133

134+
func makeWidthTable(zero, two table) widthTable {
135+
wt := make(widthTable, 0, len(zero)+len(two))
136+
zi := 0
137+
for _, iv := range two {
138+
start := iv.first
139+
for zi < len(zero) && zero[zi].last < start {
140+
zi++
141+
}
142+
for i := zi; i < len(zero) && zero[i].first <= iv.last; i++ {
143+
if start < zero[i].first {
144+
wt = append(wt, widthInterval{start, zero[i].first - 1, 2})
145+
}
146+
if start <= zero[i].last {
147+
start = zero[i].last + 1
148+
}
149+
if start > iv.last {
150+
break
151+
}
152+
}
153+
if start <= iv.last {
154+
wt = append(wt, widthInterval{start, iv.last, 2})
155+
}
156+
}
157+
for _, iv := range zero {
158+
wt = append(wt, widthInterval{iv.first, iv.last, 0})
159+
}
160+
sort.Slice(wt, func(i, j int) bool {
161+
return wt[i].first < wt[j].first
162+
})
163+
return wt
164+
}
165+
166+
func inWidthTable(r rune, t widthTable) (int, bool) {
167+
if r < t[0].first {
168+
return 0, false
169+
}
170+
if r > t[len(t)-1].last {
171+
return 0, false
172+
}
173+
174+
bot := 0
175+
top := len(t) - 1
176+
for top >= bot {
177+
mid := (bot + top) >> 1
178+
179+
switch {
180+
case t[mid].last < r:
181+
bot = mid + 1
182+
case t[mid].first > r:
183+
top = mid - 1
184+
default:
185+
return int(t[mid].width), true
186+
}
187+
}
188+
189+
return 0, false
190+
}
191+
192+
func runeWidthEastAsian(r rune) int {
193+
if w, ok := inWidthTable(r, eastAsianWidth); ok {
194+
return w
195+
}
196+
return 1
197+
}
198+
119199
var private = table{
120200
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
121201
}
@@ -153,34 +233,35 @@ func (c *Condition) RuneWidth(r rune) int {
153233
}
154234
// optimized version, verified by TestRuneWidthChecksums()
155235
if !c.EastAsianWidth {
156-
switch {
157-
case r < 0x20:
236+
if r < 0x20 {
158237
return 0
159-
case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
160-
return 0
161-
case r < 0x300:
162-
return 1
163-
case inTable(r, zerowidth):
238+
}
239+
if (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint
164240
return 0
165-
case inTable(r, doublewidth):
166-
return 2
167-
default:
241+
}
242+
if r < 0x300 {
168243
return 1
169244
}
170-
} else {
171245
switch {
172246
case inTable(r, zerowidth):
173247
return 0
174-
case inTable(r, narrow):
175-
return 1
176-
case inTable(r, widewidth):
177-
return 2
178-
case !c.StrictEmojiNeutral && inTable(r, emoji):
248+
case inTable(r, doublewidth):
179249
return 2
180250
default:
181251
return 1
182252
}
183253
}
254+
255+
if r < 0x300 {
256+
return int(eastAsianWidth0[r])
257+
}
258+
if w, ok := inWidthTable(r, eastAsianWidth); ok {
259+
return w
260+
}
261+
if !c.StrictEmojiNeutral && inTable(r, emoji) {
262+
return 2
263+
}
264+
return 1
184265
}
185266

186267
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
@@ -206,22 +287,33 @@ func (c *Condition) CreateLUT() {
206287

207288
// StringWidth return width as you can see
208289
func (c *Condition) StringWidth(s string) (width int) {
290+
if len(s) == 1 {
291+
b := s[0]
292+
if b < 0x20 || b == 0x7F {
293+
return 0
294+
}
295+
return 1
296+
}
209297
if len(s) > 0 && len(s) <= utf8.UTFMax {
210298
r, size := utf8.DecodeRuneInString(s)
211299
if size == len(s) {
212300
return c.RuneWidth(r)
213301
}
214302
}
215303
// ASCII fast path: no grapheme clustering needed for pure ASCII
216-
if isAllASCII(s) {
217-
for i := 0; i < len(s); i++ {
218-
b := s[i]
219-
if b >= 0x20 && b != 0x7F {
220-
width++
221-
}
304+
for i := 0; i < len(s); i++ {
305+
b := s[i]
306+
if b >= 0x80 {
307+
goto graphemes
308+
}
309+
if b >= 0x20 && b != 0x7F {
310+
width++
222311
}
223-
return
224312
}
313+
return
314+
315+
graphemes:
316+
width = 0
225317
g := graphemes.FromString(s)
226318
for g.Next() {
227319
var chWidth int
@@ -236,15 +328,6 @@ func (c *Condition) StringWidth(s string) (width int) {
236328
return
237329
}
238330

239-
func isAllASCII(s string) bool {
240-
for i := 0; i < len(s); i++ {
241-
if s[i] >= 0x80 {
242-
return false
243-
}
244-
}
245-
return true
246-
}
247-
248331
// Truncate return string truncated with w cells
249332
func (c *Condition) Truncate(s string, w int, tail string) string {
250333
if c.StringWidth(s) <= w {

0 commit comments

Comments
 (0)