Commit ecd24f38 authored by Marcel van Lohuizen's avatar Marcel van Lohuizen

exp/norm: Added Iter type for iterating on segment boundaries. This type is mainly to be used

by other low-level libraries, like collate.  Extra care has been given to optimize the performance
of normalizing to NFD, as this is what will be used by the collator.  The overhead of checking
whether a string is normalized vs simply decomposing a string is neglible.  Assuming that most
strings are in the FCD form, this iterator can be used to decompose strings and normalize with
minimal overhead.

R=r
CC=golang-dev
https://golang.org/cl/5676057
parent 9666a959
...@@ -66,6 +66,18 @@ func (rb *reorderBuffer) flush(out []byte) []byte { ...@@ -66,6 +66,18 @@ func (rb *reorderBuffer) flush(out []byte) []byte {
return out return out
} }
// flushCopy copies the normalized segment to buf and resets rb.
// It returns the number of bytes written to buf.
func (rb *reorderBuffer) flushCopy(buf []byte) int {
p := 0
for i := 0; i < rb.nrune; i++ {
runep := rb.rune[i]
p += copy(buf[p:], rb.byte[runep.pos:runep.pos+runep.size])
}
rb.reset()
return p
}
// insertOrdered inserts a rune in the buffer, ordered by Canonical Combining Class. // insertOrdered inserts a rune in the buffer, ordered by Canonical Combining Class.
// It returns false if the buffer is not large enough to hold the rune. // It returns false if the buffer is not large enough to hold the rune.
// It is used internally by insert and insertString only. // It is used internally by insert and insertString only.
...@@ -96,32 +108,41 @@ func (rb *reorderBuffer) insertOrdered(info runeInfo) bool { ...@@ -96,32 +108,41 @@ func (rb *reorderBuffer) insertOrdered(info runeInfo) bool {
// insert inserts the given rune in the buffer ordered by CCC. // insert inserts the given rune in the buffer ordered by CCC.
// It returns true if the buffer was large enough to hold the decomposed rune. // It returns true if the buffer was large enough to hold the decomposed rune.
func (rb *reorderBuffer) insert(src input, i int, info runeInfo) bool { func (rb *reorderBuffer) insert(src input, i int, info runeInfo) bool {
if info.size == 3 { if rune := src.hangul(i); rune != 0 {
if rune := src.hangul(i); rune != 0 { return rb.decomposeHangul(rune)
return rb.decomposeHangul(rune)
}
} }
if info.hasDecomposition() { if info.hasDecomposition() {
dcomp := info.decomposition() return rb.insertDecomposed(info.decomposition())
rb.tmpBytes = inputBytes(dcomp) }
for i := 0; i < len(dcomp); { return rb.insertSingle(src, i, info)
info = rb.f.info(&rb.tmpBytes, i) }
pos := rb.nbyte
if !rb.insertOrdered(info) { // insertDecomposed inserts an entry in to the reorderBuffer for each rune
return false // in dcomp. dcomp must be a sequence of decomposed UTF-8-encoded runes.
} func (rb *reorderBuffer) insertDecomposed(dcomp []byte) bool {
end := i + int(info.size) saveNrune, saveNbyte := rb.nrune, rb.nbyte
copy(rb.byte[pos:], dcomp[i:end]) rb.tmpBytes = inputBytes(dcomp)
i = end for i := 0; i < len(dcomp); {
} info := rb.f.info(&rb.tmpBytes, i)
} else {
// insertOrder changes nbyte
pos := rb.nbyte pos := rb.nbyte
if !rb.insertOrdered(info) { if !rb.insertOrdered(info) {
rb.nrune, rb.nbyte = saveNrune, saveNbyte
return false return false
} }
src.copySlice(rb.byte[pos:], i, i+int(info.size)) i += copy(rb.byte[pos:], dcomp[i:i+int(info.size)])
}
return true
}
// insertSingle inserts an entry in the reorderBuffer for the rune at
// position i. info is the runeInfo for the rune at position i.
func (rb *reorderBuffer) insertSingle(src input, i int, info runeInfo) bool {
// insertOrder changes nbyte
pos := rb.nbyte
if !rb.insertOrdered(info) {
return false
} }
src.copySlice(rb.byte[pos:], i, i+int(info.size))
return true return true
} }
...@@ -182,8 +203,12 @@ const ( ...@@ -182,8 +203,12 @@ const (
jamoLVTCount = 19 * 21 * 28 jamoLVTCount = 19 * 21 * 28
) )
// Caller must verify that len(b) >= 3. const hangulUTF8Size = 3
func isHangul(b []byte) bool { func isHangul(b []byte) bool {
if len(b) < hangulUTF8Size {
return false
}
b0 := b[0] b0 := b[0]
if b0 < hangulBase0 { if b0 < hangulBase0 {
return false return false
...@@ -202,8 +227,10 @@ func isHangul(b []byte) bool { ...@@ -202,8 +227,10 @@ func isHangul(b []byte) bool {
return b1 == hangulEnd1 && b[2] < hangulEnd2 return b1 == hangulEnd1 && b[2] < hangulEnd2
} }
// Caller must verify that len(b) >= 3.
func isHangulString(b string) bool { func isHangulString(b string) bool {
if len(b) < hangulUTF8Size {
return false
}
b0 := b[0] b0 := b[0]
if b0 < hangulBase0 { if b0 < hangulBase0 {
return false return false
...@@ -234,6 +261,22 @@ func isHangulWithoutJamoT(b []byte) bool { ...@@ -234,6 +261,22 @@ func isHangulWithoutJamoT(b []byte) bool {
return c < jamoLVTCount && c%jamoTCount == 0 return c < jamoLVTCount && c%jamoTCount == 0
} }
// decomposeHangul writes the decomposed Hangul to buf and returns the number
// of bytes written. len(buf) should be at least 9.
func decomposeHangul(buf []byte, r rune) int {
const JamoUTF8Len = 3
r -= hangulBase
x := r % jamoTCount
r /= jamoTCount
utf8.EncodeRune(buf, jamoLBase+r/jamoVCount)
utf8.EncodeRune(buf[JamoUTF8Len:], jamoVBase+r%jamoVCount)
if x != 0 {
utf8.EncodeRune(buf[2*JamoUTF8Len:], jamoTBase+x)
return 3 * JamoUTF8Len
}
return 2 * JamoUTF8Len
}
// decomposeHangul algorithmically decomposes a Hangul rune into // decomposeHangul algorithmically decomposes a Hangul rune into
// its Jamo components. // its Jamo components.
// See http://unicode.org/reports/tr15/#Hangul for details on decomposing Hangul. // See http://unicode.org/reports/tr15/#Hangul for details on decomposing Hangul.
......
...@@ -47,14 +47,14 @@ func runTests(t *testing.T, name string, fm Form, f insertFunc, tests []TestCase ...@@ -47,14 +47,14 @@ func runTests(t *testing.T, name string, fm Form, f insertFunc, tests []TestCase
} }
} }
func TestFlush(t *testing.T) { type flushFunc func(rb *reorderBuffer) []byte
func testFlush(t *testing.T, name string, fn flushFunc) {
rb := reorderBuffer{} rb := reorderBuffer{}
rb.init(NFC, nil) rb.init(NFC, nil)
out := make([]byte, 0) out := fn(&rb)
out = rb.flush(out)
if len(out) != 0 { if len(out) != 0 {
t.Errorf("wrote bytes on flush of empty buffer. (len(out) = %d)", len(out)) t.Errorf("%s: wrote bytes on flush of empty buffer. (len(out) = %d)", name, len(out))
} }
for _, r := range []rune("world!") { for _, r := range []rune("world!") {
...@@ -65,16 +65,32 @@ func TestFlush(t *testing.T) { ...@@ -65,16 +65,32 @@ func TestFlush(t *testing.T) {
out = rb.flush(out) out = rb.flush(out)
want := "Hello world!" want := "Hello world!"
if string(out) != want { if string(out) != want {
t.Errorf(`output after flush was "%s"; want "%s"`, string(out), want) t.Errorf(`%s: output after flush was "%s"; want "%s"`, name, string(out), want)
} }
if rb.nrune != 0 { if rb.nrune != 0 {
t.Errorf("flush: non-null size of info buffer (rb.nrune == %d)", rb.nrune) t.Errorf("%s: non-null size of info buffer (rb.nrune == %d)", name, rb.nrune)
} }
if rb.nbyte != 0 { if rb.nbyte != 0 {
t.Errorf("flush: non-null size of byte buffer (rb.nbyte == %d)", rb.nbyte) t.Errorf("%s: non-null size of byte buffer (rb.nbyte == %d)", name, rb.nbyte)
} }
} }
func flushF(rb *reorderBuffer) []byte {
out := make([]byte, 0)
return rb.flush(out)
}
func flushCopyF(rb *reorderBuffer) []byte {
out := make([]byte, MaxSegmentSize)
n := rb.flushCopy(out)
return out[:n]
}
func TestFlush(t *testing.T) {
testFlush(t, "flush", flushF)
testFlush(t, "flushCopy", flushCopyF)
}
var insertTests = []TestCase{ var insertTests = []TestCase{
{[]rune{'a'}, []rune{'a'}}, {[]rune{'a'}, []rune{'a'}},
{[]rune{0x300}, []rune{0x300}}, {[]rune{0x300}, []rune{0x300}},
......
...@@ -7,7 +7,7 @@ package norm ...@@ -7,7 +7,7 @@ package norm
import "unicode/utf8" import "unicode/utf8"
type input interface { type input interface {
skipASCII(p int) int skipASCII(p, max int) int
skipNonStarter(p int) int skipNonStarter(p int) int
appendSlice(buf []byte, s, e int) []byte appendSlice(buf []byte, s, e int) []byte
copySlice(buf []byte, s, e int) copySlice(buf []byte, s, e int)
...@@ -18,8 +18,8 @@ type input interface { ...@@ -18,8 +18,8 @@ type input interface {
type inputString string type inputString string
func (s inputString) skipASCII(p int) int { func (s inputString) skipASCII(p, max int) int {
for ; p < len(s) && s[p] < utf8.RuneSelf; p++ { for ; p < max && s[p] < utf8.RuneSelf; p++ {
} }
return p return p
} }
...@@ -59,8 +59,8 @@ func (s inputString) hangul(p int) rune { ...@@ -59,8 +59,8 @@ func (s inputString) hangul(p int) rune {
type inputBytes []byte type inputBytes []byte
func (s inputBytes) skipASCII(p int) int { func (s inputBytes) skipASCII(p, max int) int {
for ; p < len(s) && s[p] < utf8.RuneSelf; p++ { for ; p < max && s[p] < utf8.RuneSelf; p++ {
} }
return p return p
} }
......
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package norm
const MaxSegmentSize = maxByteBufferSize
// An Iter iterates over a string or byte slice, while normalizing it
// to a given Form.
type Iter struct {
rb reorderBuffer
info runeInfo // first character saved from previous iteration
next iterFunc // implementation of next depends on form
p int // current position in input source
outStart int // start of current segment in output buffer
inStart int // start of current segment in input source
maxp int // position in output buffer after which not to start a new segment
maxseg int // for tracking an excess of combining characters
tccc uint8
done bool
}
type iterFunc func(*Iter, []byte) int
// SetInput initializes i to iterate over src after normalizing it to Form f.
func (i *Iter) SetInput(f Form, src []byte) {
i.rb.init(f, src)
if i.rb.f.composing {
i.next = nextComposed
} else {
i.next = nextDecomposed
}
i.p = 0
if i.done = len(src) == 0; !i.done {
i.info = i.rb.f.info(i.rb.src, i.p)
}
}
// SetInputString initializes i to iterate over src after normalizing it to Form f.
func (i *Iter) SetInputString(f Form, src string) {
i.rb.initString(f, src)
if i.rb.f.composing {
i.next = nextComposed
} else {
i.next = nextDecomposed
}
i.p = 0
if i.done = len(src) == 0; !i.done {
i.info = i.rb.f.info(i.rb.src, i.p)
}
}
// Pos returns the byte position at which the next call to Next will commence processing.
func (i *Iter) Pos() int {
return i.p
}
// Done returns true if there is no more input to process.
func (i *Iter) Done() bool {
return i.done
}
// Next writes f(i.input[i.Pos():n]...) to buffer buf, where n is the
// largest boundary of i.input such that the result fits in buf.
// It returns the number of bytes written to buf.
// len(buf) should be at least MaxSegmentSize.
// Done must be false before calling Next.
func (i *Iter) Next(buf []byte) int {
return i.next(i, buf)
}
func (i *Iter) initNext(outn, inStart int) {
i.outStart = 0
i.inStart = inStart
i.maxp = outn - MaxSegmentSize
i.maxseg = MaxSegmentSize
}
// setStart resets the start of the new segment to the given position.
// It returns true if there is not enough room for the new segment.
func (i *Iter) setStart(outp, inp int) bool {
if outp > i.maxp {
return true
}
i.outStart = outp
i.inStart = inp
i.maxseg = outp + MaxSegmentSize
return false
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// nextDecomposed is the implementation of Next for forms NFD and NFKD.
func nextDecomposed(i *Iter, out []byte) int {
var outp int
i.initNext(len(out), i.p)
doFast:
inCopyStart, outCopyStart := i.p, outp // invariant xCopyStart <= i.xStart
for {
if sz := int(i.info.size); sz <= 1 {
// ASCII or illegal byte. Either way, advance by 1.
i.p++
outp++
max := min(i.rb.nsrc, len(out)-outp+i.p)
if np := i.rb.src.skipASCII(i.p, max); np > i.p {
outp += np - i.p
i.p = np
if i.p >= i.rb.nsrc {
break
}
// ASCII may combine with consecutive runes.
if i.setStart(outp-1, i.p-1) {
i.p--
outp--
i.info.size = 1
break
}
}
} else if d := i.info.decomposition(); d != nil {
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.p)
p := outp + len(d)
if p > i.maxseg && i.setStart(outp, i.p) {
return outp
}
copy(out[outp:], d)
outp = p
i.p += sz
inCopyStart, outCopyStart = i.p, outp
} else if r := i.rb.src.hangul(i.p); r != 0 {
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.p)
for {
outp += decomposeHangul(out[outp:], r)
i.p += hangulUTF8Size
if r = i.rb.src.hangul(i.p); r == 0 {
break
}
if i.setStart(outp, i.p) {
return outp
}
}
inCopyStart, outCopyStart = i.p, outp
} else {
p := outp + sz
if p > i.maxseg && i.setStart(outp, i.p) {
break
}
outp = p
i.p += sz
}
if i.p >= i.rb.nsrc {
break
}
prevCC := i.info.tccc
i.info = i.rb.f.info(i.rb.src, i.p)
if cc := i.info.ccc; cc == 0 {
if i.setStart(outp, i.p) {
break
}
} else if cc < prevCC {
goto doNorm
}
}
if inCopyStart != i.p {
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.p)
}
i.done = i.p >= i.rb.nsrc
return outp
doNorm:
// Insert what we have decomposed so far in the reorderBuffer.
// As we will only reorder, there will always be enough room.
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.p)
if !i.rb.insertDecomposed(out[i.outStart:outp]) {
// Start over to prevent decompositions from crossing segment boundaries.
// This is a rare occurance.
i.p = i.inStart
i.info = i.rb.f.info(i.rb.src, i.p)
}
outp = i.outStart
for {
if !i.rb.insert(i.rb.src, i.p, i.info) {
break
}
if i.p += int(i.info.size); i.p >= i.rb.nsrc {
outp += i.rb.flushCopy(out[outp:])
i.done = true
return outp
}
i.info = i.rb.f.info(i.rb.src, i.p)
if i.info.ccc == 0 {
break
}
}
// new segment or too many combining characters: exit normalization
if outp += i.rb.flushCopy(out[outp:]); i.setStart(outp, i.p) {
return outp
}
goto doFast
}
// nextComposed is the implementation of Next for forms NFC and NFKC.
func nextComposed(i *Iter, out []byte) int {
var outp int
i.initNext(len(out), i.p)
doFast:
inCopyStart, outCopyStart := i.p, outp // invariant xCopyStart <= i.xStart
var prevCC uint8
for {
if !i.info.isYesC() {
goto doNorm
}
if cc := i.info.ccc; cc == 0 {
if i.setStart(outp, i.p) {
break
}
} else if cc < prevCC {
goto doNorm
}
prevCC = i.info.tccc
sz := int(i.info.size)
if sz == 0 {
sz = 1 // illegal rune: copy byte-by-byte
}
p := outp + sz
if p > i.maxseg && i.setStart(outp, i.p) {
break
}
outp = p
i.p += sz
max := min(i.rb.nsrc, len(out)-outp+i.p)
if np := i.rb.src.skipASCII(i.p, max); np > i.p {
outp += np - i.p
i.p = np
if i.p >= i.rb.nsrc {
break
}
// ASCII may combine with consecutive runes.
if i.setStart(outp-1, i.p-1) {
i.p--
outp--
i.info = runeInfo{size: 1}
break
}
}
if i.p >= i.rb.nsrc {
break
}
i.info = i.rb.f.info(i.rb.src, i.p)
}
if inCopyStart != i.p {
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.p)
}
i.done = i.p >= i.rb.nsrc
return outp
doNorm:
i.rb.src.copySlice(out[outCopyStart:], inCopyStart, i.inStart)
outp, i.p = i.outStart, i.inStart
i.info = i.rb.f.info(i.rb.src, i.p)
for {
if !i.rb.insert(i.rb.src, i.p, i.info) {
break
}
if i.p += int(i.info.size); i.p >= i.rb.nsrc {
i.rb.compose()
outp += i.rb.flushCopy(out[outp:])
i.done = true
return outp
}
i.info = i.rb.f.info(i.rb.src, i.p)
if i.info.boundaryBefore() {
break
}
}
i.rb.compose()
if outp += i.rb.flushCopy(out[outp:]); i.setStart(outp, i.p) {
return outp
}
goto doFast
}
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package norm
import (
"strings"
"testing"
)
var iterBufSizes = []int{
MaxSegmentSize,
1.5 * MaxSegmentSize,
2 * MaxSegmentSize,
3 * MaxSegmentSize,
100 * MaxSegmentSize,
}
func doIterNorm(f Form, buf []byte, s string) []byte {
acc := []byte{}
i := Iter{}
i.SetInputString(f, s)
for !i.Done() {
n := i.Next(buf)
acc = append(acc, buf[:n]...)
}
return acc
}
func runIterTests(t *testing.T, name string, f Form, tests []AppendTest, norm bool) {
for i, test := range tests {
in := test.left + test.right
gold := test.out
if norm {
gold = string(f.AppendString(nil, test.out))
}
for _, sz := range iterBufSizes {
buf := make([]byte, sz)
out := string(doIterNorm(f, buf, in))
if len(out) != len(gold) {
const msg = "%s:%d:%d: length is %d; want %d"
t.Errorf(msg, name, i, sz, len(out), len(gold))
}
if out != gold {
// Find first rune that differs and show context.
ir := []rune(out)
ig := []rune(gold)
for j := 0; j < len(ir) && j < len(ig); j++ {
if ir[j] == ig[j] {
continue
}
if j -= 3; j < 0 {
j = 0
}
for e := j + 7; j < e && j < len(ir) && j < len(ig); j++ {
const msg = "%s:%d:%d: runeAt(%d) = %U; want %U"
t.Errorf(msg, name, i, sz, j, ir[j], ig[j])
}
break
}
}
}
}
}
func rep(r rune, n int) string {
return strings.Repeat(string(r), n)
}
var iterTests = []AppendTest{
{"", ascii, ascii},
{"", txt_all, txt_all},
{"", "a" + rep(0x0300, MaxSegmentSize/2), "a" + rep(0x0300, MaxSegmentSize/2)},
}
var iterTestsD = []AppendTest{
{ // segment overflow on unchanged character
"",
"a" + rep(0x0300, MaxSegmentSize/2) + "\u0316",
"a" + rep(0x0300, MaxSegmentSize/2-1) + "\u0316\u0300",
},
{ // segment overflow on unchanged character + start value
"",
"a" + rep(0x0300, MaxSegmentSize/2+maxCombiningChars+4) + "\u0316",
"a" + rep(0x0300, MaxSegmentSize/2+maxCombiningChars) + "\u0316" + rep(0x300, 4),
},
{ // segment overflow on decomposition
"",
"a" + rep(0x0300, MaxSegmentSize/2-1) + "\u0340",
"a" + rep(0x0300, MaxSegmentSize/2),
},
{ // segment overflow on decomposition + start value
"",
"a" + rep(0x0300, MaxSegmentSize/2-1) + "\u0340" + rep(0x300, maxCombiningChars+4) + "\u0320",
"a" + rep(0x0300, MaxSegmentSize/2-1) + rep(0x300, maxCombiningChars+1) + "\u0320" + rep(0x300, 4),
},
{ // start value after ASCII overflow
"",
rep('a', MaxSegmentSize) + rep(0x300, maxCombiningChars+2) + "\u0320",
rep('a', MaxSegmentSize) + rep(0x300, maxCombiningChars) + "\u0320\u0300\u0300",
},
{ // start value after Hangul overflow
"",
rep(0xAC00, MaxSegmentSize/6) + rep(0x300, maxCombiningChars+2) + "\u0320",
strings.Repeat("\u1100\u1161", MaxSegmentSize/6) + rep(0x300, maxCombiningChars-1) + "\u0320" + rep(0x300, 3),
},
{ // start value after cc=0
"",
"您您" + rep(0x300, maxCombiningChars+4) + "\u0320",
"您您" + rep(0x300, maxCombiningChars) + "\u0320" + rep(0x300, 4),
},
{ // start value after normalization
"",
"\u0300\u0320a" + rep(0x300, maxCombiningChars+4) + "\u0320",
"\u0320\u0300a" + rep(0x300, maxCombiningChars) + "\u0320" + rep(0x300, 4),
},
}
var iterTestsC = []AppendTest{
{ // ordering of non-composing combining characters
"",
"\u0305\u0316",
"\u0316\u0305",
},
{ // segment overflow
"",
"a" + rep(0x0305, MaxSegmentSize/2+4) + "\u0316",
"a" + rep(0x0305, MaxSegmentSize/2-1) + "\u0316" + rep(0x305, 5),
},
}
func TestIterNextD(t *testing.T) {
runIterTests(t, "IterNextD1", NFKD, appendTests, true)
runIterTests(t, "IterNextD2", NFKD, iterTests, true)
runIterTests(t, "IterNextD3", NFKD, iterTestsD, false)
}
func TestIterNextC(t *testing.T) {
runIterTests(t, "IterNextC1", NFKC, appendTests, true)
runIterTests(t, "IterNextC2", NFKC, iterTests, true)
runIterTests(t, "IterNextC3", NFKC, iterTestsC, false)
}
type SegmentTest struct {
in string
out []string
}
var segmentTests = []SegmentTest{
{rep('a', MaxSegmentSize), []string{rep('a', MaxSegmentSize), ""}},
{rep('a', MaxSegmentSize+2), []string{rep('a', MaxSegmentSize-1), "aaa", ""}},
{rep('a', MaxSegmentSize) + "\u0300aa", []string{rep('a', MaxSegmentSize-1), "a\u0300", "aa", ""}},
}
// Note that, by design, segmentation is equal for composing and decomposing forms.
func TestIterSegmentation(t *testing.T) {
segmentTest(t, "SegmentTestD", NFD, segmentTests)
segmentTest(t, "SegmentTestC", NFC, segmentTests)
}
func segmentTest(t *testing.T, name string, f Form, tests []SegmentTest) {
iter := Iter{}
for i, tt := range segmentTests {
buf := make([]byte, MaxSegmentSize)
iter.SetInputString(f, tt.in)
for j, seg := range tt.out {
if seg == "" {
if !iter.Done() {
n := iter.Next(buf)
res := string(buf[:n])
t.Errorf(`%s:%d:%d: expected Done()==true, found segment "%s"`, name, i, j, res)
}
continue
}
if iter.Done() {
t.Errorf("%s:%d:%d: Done()==true, want false", name, i, j)
}
n := iter.Next(buf)
seg = f.String(seg)
if res := string(buf[:n]); res != seg {
t.Errorf(`%s:%d:%d" segment was "%s" (%d); want "%s" (%d)`, name, i, j, res, len(res), seg, len(seg))
}
}
}
}
...@@ -243,7 +243,7 @@ func quickSpan(rb *reorderBuffer, i int) int { ...@@ -243,7 +243,7 @@ func quickSpan(rb *reorderBuffer, i int) int {
lastSegStart := i lastSegStart := i
src, n := rb.src, rb.nsrc src, n := rb.src, rb.nsrc
for i < n { for i < n {
if j := src.skipASCII(i); i != j { if j := src.skipASCII(i, n); i != j {
i = j i = j
lastSegStart = i - 1 lastSegStart = i - 1
lastCC = 0 lastCC = 0
...@@ -448,11 +448,16 @@ func decomposeToLastBoundary(rb *reorderBuffer, buf []byte) []byte { ...@@ -448,11 +448,16 @@ func decomposeToLastBoundary(rb *reorderBuffer, buf []byte) []byte {
} }
// Check that decomposition doesn't result in overflow. // Check that decomposition doesn't result in overflow.
if info.hasDecomposition() { if info.hasDecomposition() {
dcomp := info.decomposition() if isHangul(buf) {
for i := 0; i < len(dcomp); { i += int(info.size)
inf := rb.f.info(inputBytes(dcomp), i)
i += int(inf.size)
n++ n++
} else {
dcomp := info.decomposition()
for i := 0; i < len(dcomp); {
inf := rb.f.info(inputBytes(dcomp), i)
i += int(inf.size)
n++
}
} }
} else { } else {
n++ n++
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package norm package norm
import ( import (
"bytes"
"strings" "strings"
"testing" "testing"
) )
...@@ -495,15 +496,40 @@ func TestAppend(t *testing.T) { ...@@ -495,15 +496,40 @@ func TestAppend(t *testing.T) {
runAppendTests(t, "TestString", NFKC, stringF, appendTests) runAppendTests(t, "TestString", NFKC, stringF, appendTests)
} }
func appendBench(f Form, in []byte) func() {
buf := make([]byte, 0, 4*len(in))
return func() {
f.Append(buf, in...)
}
}
func iterBench(f Form, in []byte) func() {
buf := make([]byte, 4*len(in))
iter := Iter{}
return func() {
iter.SetInput(f, in)
for !iter.Done() {
iter.Next(buf)
}
}
}
func appendBenchmarks(bm []func(), f Form, in []byte) []func() {
//bm = append(bm, appendBench(f, in))
bm = append(bm, iterBench(f, in))
return bm
}
func doFormBenchmark(b *testing.B, inf, f Form, s string) { func doFormBenchmark(b *testing.B, inf, f Form, s string) {
b.StopTimer() b.StopTimer()
in := inf.Bytes([]byte(s)) in := inf.Bytes([]byte(s))
buf := make([]byte, 2*len(in)) bm := appendBenchmarks(nil, f, in)
b.SetBytes(int64(len(in))) b.SetBytes(int64(len(in) * len(bm)))
b.StartTimer() b.StartTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
buf = f.Append(buf[0:0], in...) for _, fn := range bm {
buf = buf[0:0] fn()
}
} }
} }
...@@ -549,17 +575,21 @@ func BenchmarkNormalizeHangulNFD2NFD(b *testing.B) { ...@@ -549,17 +575,21 @@ func BenchmarkNormalizeHangulNFD2NFD(b *testing.B) {
doFormBenchmark(b, NFD, NFD, txt_kr) doFormBenchmark(b, NFD, NFD, txt_kr)
} }
var forms = []Form{NFC, NFD, NFKC, NFKD}
func doTextBenchmark(b *testing.B, s string) { func doTextBenchmark(b *testing.B, s string) {
b.StopTimer() b.StopTimer()
b.SetBytes(int64(len(s)) * 4)
in := []byte(s) in := []byte(s)
var buf = make([]byte, 0, 2*len(in)) bm := []func(){}
for _, f := range forms {
bm = appendBenchmarks(bm, f, in)
}
b.SetBytes(int64(len(s) * len(bm)))
b.StartTimer() b.StartTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
NFC.Append(buf, in...) for _, f := range bm {
NFD.Append(buf, in...) f()
NFKC.Append(buf, in...) }
NFKD.Append(buf, in...)
} }
} }
...@@ -584,6 +614,11 @@ func BenchmarkJapanese(b *testing.B) { ...@@ -584,6 +614,11 @@ func BenchmarkJapanese(b *testing.B) {
func BenchmarkChinese(b *testing.B) { func BenchmarkChinese(b *testing.B) {
doTextBenchmark(b, txt_cn) doTextBenchmark(b, txt_cn)
} }
func BenchmarkOverflow(b *testing.B) {
doTextBenchmark(b, overflow)
}
var overflow = string(bytes.Repeat([]byte("\u035D"), 4096)) + "\u035B"
// Tests sampled from the Canonical ordering tests (Part 2) of // Tests sampled from the Canonical ordering tests (Part 2) of
// http://unicode.org/Public/UNIDATA/NormalizationTest.txt // http://unicode.org/Public/UNIDATA/NormalizationTest.txt
......
...@@ -220,6 +220,17 @@ func cmpIsNormal(t *Test, name string, f norm.Form, test string, result, want bo ...@@ -220,6 +220,17 @@ func cmpIsNormal(t *Test, name string, f norm.Form, test string, result, want bo
func doTest(t *Test, f norm.Form, gold, test string) { func doTest(t *Test, f norm.Form, gold, test string) {
result := f.Bytes([]byte(test)) result := f.Bytes([]byte(test))
cmpResult(t, "Bytes", f, gold, test, string(result)) cmpResult(t, "Bytes", f, gold, test, string(result))
sresult := f.String(test)
cmpResult(t, "String", f, gold, test, sresult)
buf := make([]byte, norm.MaxSegmentSize)
acc := []byte{}
i := norm.Iter{}
i.SetInputString(f, test)
for !i.Done() {
n := i.Next(buf)
acc = append(acc, buf[:n]...)
}
cmpResult(t, "Iter.Next", f, gold, test, string(acc))
for i := range test { for i := range test {
out := f.Append(f.Bytes([]byte(test[:i])), []byte(test[i:])...) out := f.Append(f.Bytes([]byte(test[:i])), []byte(test[i:])...)
cmpResult(t, fmt.Sprintf(":Append:%d", i), f, gold, test, string(out)) cmpResult(t, fmt.Sprintf(":Append:%d", i), f, gold, test, string(out))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment