blob: f9802c9c445fd8812832815c3453bee6fe1f4d52 [file] [log] [blame]
// Copyright 2022 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 comment
import (
"flag"
"fmt"
"math/rand"
"testing"
"time"
"unicode/utf8"
)
var wrapSeed = flag.Int64("wrapseed", 0, "use `seed` for wrap test (default auto-seeds)")
func TestWrap(t *testing.T) {
if *wrapSeed == 0 {
*wrapSeed = time.Now().UnixNano()
}
t.Logf("-wrapseed=%#x\n", *wrapSeed)
r := rand.New(rand.NewSource(*wrapSeed))
// Generate words of random length.
s := "1234567890αβcdefghijklmnopqrstuvwxyz"
sN := utf8.RuneCountInString(s)
var words []string
for i := 0; i < 100; i++ {
n := 1 + r.Intn(sN-1)
if n >= 12 {
n++ // extra byte for β
}
if n >= 11 {
n++ // extra byte for α
}
words = append(words, s[:n])
}
for n := 1; n <= len(words) && !t.Failed(); n++ {
t.Run(fmt.Sprint("n=", n), func(t *testing.T) {
words := words[:n]
t.Logf("words: %v", words)
for max := 1; max < 100 && !t.Failed(); max++ {
t.Run(fmt.Sprint("max=", max), func(t *testing.T) {
seq := wrap(words, max)
// Compute score for seq.
start := 0
score := int64(0)
if len(seq) == 0 {
t.Fatalf("wrap seq is empty")
}
if seq[0] != 0 {
t.Fatalf("wrap seq does not start with 0")
}
for _, n := range seq[1:] {
if n <= start {
t.Fatalf("wrap seq is non-increasing: %v", seq)
}
if n > len(words) {
t.Fatalf("wrap seq contains %d > %d: %v", n, len(words), seq)
}
size := -1
for _, s := range words[start:n] {
size += 1 + utf8.RuneCountInString(s)
}
if n-start == 1 && size >= max {
// no score
} else if size > max {
t.Fatalf("wrap used overlong line %d:%d: %v", start, n, words[start:n])
} else if n != len(words) {
score += int64(max-size)*int64(max-size) + wrapPenalty(words[n-1])
}
start = n
}
if start != len(words) {
t.Fatalf("wrap seq does not use all words (%d < %d): %v", start, len(words), seq)
}
// Check that score matches slow reference implementation.
slowSeq, slowScore := wrapSlow(words, max)
if score != slowScore {
t.Fatalf("wrap score = %d != wrapSlow score %d\nwrap: %v\nslow: %v", score, slowScore, seq, slowSeq)
}
})
}
})
}
}
// wrapSlow is an O(n²) reference implementation for wrap.
// It returns a minimal-score sequence along with the score.
// It is OK if wrap returns a different sequence as long as that
// sequence has the same score.
func wrapSlow(words []string, max int) (seq []int, score int64) {
// Quadratic dynamic programming algorithm for line wrapping problem.
// best[i] tracks the best score possible for words[:i],
// assuming that for i < len(words) the line breaks after those words.
// bestleft[i] tracks the previous line break for best[i].
best := make([]int64, len(words)+1)
bestleft := make([]int, len(words)+1)
best[0] = 0
for i, w := range words {
if utf8.RuneCountInString(w) >= max {
// Overlong word must appear on line by itself. No effect on score.
best[i+1] = best[i]
continue
}
best[i+1] = 1e18
p := wrapPenalty(w)
n := -1
for j := i; j >= 0; j-- {
n += 1 + utf8.RuneCountInString(words[j])
if n > max {
break
}
line := int64(n-max)*int64(n-max) + p
if i == len(words)-1 {
line = 0 // no score for final line being too short
}
s := best[j] + line
if best[i+1] > s {
best[i+1] = s
bestleft[i+1] = j
}
}
}
// Recover least weight sequence from bestleft.
n := 1
for m := len(words); m > 0; m = bestleft[m] {
n++
}
seq = make([]int, n)
for m := len(words); m > 0; m = bestleft[m] {
n--
seq[n] = m
}
return seq, best[len(words)]
}