| // 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)] |
| } |