Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(198)

Delta Between Two Patch Sets: src/pkg/strings/replace_test.go

Issue 6492076: code review 6492076: strings: implement a faster generic Replacer (Closed)
Left Patch Set: diff -r 5c4859bc123f https://go.googlecode.com/hg Created 11 years, 6 months ago
Right Patch Set: diff -r cdee8bf43694 https://code.google.com/p/go Created 11 years, 6 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« src/pkg/strings/replace.go ('K') | « src/pkg/strings/replace.go ('k') | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 // Copyright 2009 The Go Authors. All rights reserved. 1 // Copyright 2009 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style 2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file. 3 // license that can be found in the LICENSE file.
4 4
5 package strings_test 5 package strings_test
6 6
7 import ( 7 import (
8 "bytes" 8 "bytes"
9 "fmt" 9 "fmt"
10 "log"
11 . "strings" 10 . "strings"
12 "testing" 11 "testing"
13 ) 12 )
14 13
15 var _ = log.Printf 14 var htmlEscaper = NewReplacer(
16 15 » "&", "&",
17 type ReplacerTest struct { 16 » "<", "&lt;",
18 » r *Replacer 17 » ">", "&gt;",
19 » in string 18 » `"`, "&quot;",
20 » out string 19 » "'", "&apos;",
21 } 20 )
22 21
23 var htmlEscaper = NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", "\"", "&qu ot;", "'", "&apos;") 22 var htmlUnescaper = NewReplacer(
24 var htmlUnescaper = NewReplacer("&amp;", "&", "&lt;", "<", "&gt;", ">", "&quot;" , `"`, "&apos;", "'") 23 » "&amp;", "&",
24 » "&lt;", "<",
25 » "&gt;", ">",
26 » "&quot;", `"`,
27 » "&apos;", "'",
28 )
25 29
26 // The http package's old HTML escaping function. 30 // The http package's old HTML escaping function.
27 func oldhtmlEscape(s string) string { 31 func oldHTMLEscape(s string) string {
28 s = Replace(s, "&", "&amp;", -1) 32 s = Replace(s, "&", "&amp;", -1)
29 s = Replace(s, "<", "&lt;", -1) 33 s = Replace(s, "<", "&lt;", -1)
30 s = Replace(s, ">", "&gt;", -1) 34 s = Replace(s, ">", "&gt;", -1)
31 » s = Replace(s, "\"", "&quot;", -1) 35 » s = Replace(s, `"`, "&quot;", -1)
32 s = Replace(s, "'", "&apos;", -1) 36 s = Replace(s, "'", "&apos;", -1)
33 return s 37 return s
34 } 38 }
35 39
36 var replacer = NewReplacer("aaa", "3[aaa]", "aa", "2[aa]", "a", "1[a]", "i", "i" ,
37 "longerst", "most long", "longer", "medium", "long", "short",
38 "X", "Y", "Y", "Z")
39
40 var capitalLetters = NewReplacer("a", "A", "b", "B") 40 var capitalLetters = NewReplacer("a", "A", "b", "B")
41 41
42 var blankToXReplacer = NewReplacer("", "X", "o", "O") 42 // TestReplacer tests the replacer implementations.
43
44 var ReplacerTests = []ReplacerTest{
45 » // byte->string
46 » {htmlEscaper, "No changes", "No changes"},
47 » {htmlEscaper, "I <3 escaping & stuff", "I &lt;3 escaping &amp; stuff"},
48 » {htmlEscaper, "&&&", "&amp;&amp;&amp;"},
49
50 » // generic
51 » {replacer, "fooaaabar", "foo3[aaa]b1[a]r"},
52 » {replacer, "long, longerst, longer", "short, most long, medium"},
53 » {replacer, "XiX", "YiY"},
54
55 » // byte->byte
56 » {capitalLetters, "brad", "BrAd"},
57 » {capitalLetters, Repeat("a", (32<<10)+123), Repeat("A", (32<<10)+123)},
58
59 » // hitting "" special case
60 » {blankToXReplacer, "oo", "XOXOX"},
61 }
62
63 func TestReplacer(t *testing.T) { 43 func TestReplacer(t *testing.T) {
64 » for i, tt := range ReplacerTests { 44 » type testCase struct {
65 » » if s := tt.r.Replace(tt.in); s != tt.out { 45 » » r *Replacer
66 » » » t.Errorf("%d. Replace(%q) = %q, want %q", i, tt.in, s, t t.out) 46 » » in, out string
47 » }
48 » var testCases []testCase
49
50 » // str converts 0xff to "\xff". This isn't just string(b) since that con verts to UTF-8.
51 » str := func(b byte) string {
52 » » return string([]byte{b})
53 » }
54 » var s []string
55
56 » // inc maps "\x00"->"\x01", ..., "a"->"b", "b"->"c", ..., "\xff"->"\x00" .
57 » s = nil
58 » for i := 0; i < 256; i++ {
59 » » s = append(s, str(byte(i)), str(byte(i+1)))
60 » }
61 » inc := NewReplacer(s...)
62
63 » // Test cases with 1-byte old strings, 1-byte new strings.
64 » testCases = append(testCases,
65 » » testCase{capitalLetters, "brad", "BrAd"},
66 » » testCase{capitalLetters, Repeat("a", (32<<10)+123), Repeat("A", (32<<10)+123)},
67 » » testCase{capitalLetters, "", ""},
68
69 » » testCase{inc, "brad", "csbe"},
70 » » testCase{inc, "\x00\xff", "\x01\x00"},
71 » » testCase{inc, "", ""},
72
73 » » testCase{NewReplacer("a", "1", "a", "2"), "brad", "br1d"},
74 » )
75
76 » // repeat maps "a"->"a", "b"->"bb", "c"->"ccc", ...
77 » s = nil
78 » for i := 0; i < 256; i++ {
79 » » n := i + 1 - 'a'
80 » » if n < 1 {
81 » » » n = 1
82 » » }
83 » » s = append(s, str(byte(i)), Repeat(str(byte(i)), n))
84 » }
85 » repeat := NewReplacer(s...)
86
87 » // Test cases with 1-byte old strings, variable length new strings.
88 » testCases = append(testCases,
89 » » testCase{htmlEscaper, "No changes", "No changes"},
90 » » testCase{htmlEscaper, "I <3 escaping & stuff", "I &lt;3 escaping &amp; stuff"},
91 » » testCase{htmlEscaper, "&&&", "&amp;&amp;&amp;"},
92 » » testCase{htmlEscaper, "", ""},
93
94 » » testCase{repeat, "brad", "bbrrrrrrrrrrrrrrrrrradddd"},
95 » » testCase{repeat, "abba", "abbbba"},
96 » » testCase{repeat, "", ""},
97
98 » » testCase{NewReplacer("a", "11", "a", "22"), "brad", "br11d"},
99 » )
100
101 » // The remaining test cases have variable length old strings.
102
103 » testCases = append(testCases,
104 » » testCase{htmlUnescaper, "&amp;amp;", "&amp;"},
105 » » testCase{htmlUnescaper, "&lt;b&gt;HTML&apos;s neat&lt;/b&gt;", " <b>HTML's neat</b>"},
106 » » testCase{htmlUnescaper, "", ""},
107
108 » » testCase{NewReplacer("a", "1", "a", "2", "xxx", "xxx"), "brad", "br1d"},
109
110 » » testCase{NewReplacer("a", "1", "aa", "2", "aaa", "3"), "aaaa", " 1111"},
111
112 » » testCase{NewReplacer("aaa", "3", "aa", "2", "a", "1"), "aaaa", " 31"},
113 » )
114
115 » // gen1 has multiple old strings of variable length. There is no
116 » // overall non-empty common prefix, but some pairwise common prefixes.
117 » gen1 := NewReplacer(
118 » » "aaa", "3[aaa]",
119 » » "aa", "2[aa]",
120 » » "a", "1[a]",
121 » » "i", "i",
122 » » "longerst", "most long",
123 » » "longer", "medium",
124 » » "long", "short",
125 » » "xx", "xx",
126 » » "x", "X",
127 » » "X", "Y",
128 » » "Y", "Z",
129 » )
130 » testCases = append(testCases,
131 » » testCase{gen1, "fooaaabar", "foo3[aaa]b1[a]r"},
132 » » testCase{gen1, "long, longerst, longer", "short, most long, medi um"},
133 » » testCase{gen1, "xxxxx", "xxxxX"},
134 » » testCase{gen1, "XiX", "YiY"},
135 » » testCase{gen1, "", ""},
136 » )
137
138 » // gen2 has multiple old strings with no pairwise common prefix.
139 » gen2 := NewReplacer(
140 » » "roses", "red",
141 » » "violets", "blue",
142 » » "sugar", "sweet",
143 » )
144 » testCases = append(testCases,
145 » » testCase{gen2, "roses are red, violets are blue...", "red are re d, blue are blue..."},
146 » » testCase{gen2, "", ""},
147 » )
148
149 » // gen3 has multiple old strings with an overall common prefix.
150 » gen3 := NewReplacer(
151 » » "abracadabra", "poof",
152 » » "abracadabrakazam", "splat",
153 » » "abraham", "lincoln",
154 » » "abrasion", "scrape",
155 » » "abraham", "isaac",
156 » )
157 » testCases = append(testCases,
158 » » testCase{gen3, "abracadabrakazam abraham", "poofkazam lincoln"},
159 » » testCase{gen3, "abrasion abracad", "scrape abracad"},
160 » » testCase{gen3, "abba abram abrasive", "abba abram abrasive"},
161 » » testCase{gen3, "", ""},
162 » )
163
164 » // foo{1,2,3,4} have multiple old strings with an overall common prefix
165 » // and 1- or 2- byte extensions from the common prefix.
166 » foo1 := NewReplacer(
167 » » "foo1", "A",
168 » » "foo2", "B",
169 » » "foo3", "C",
170 » )
171 » foo2 := NewReplacer(
172 » » "foo1", "A",
173 » » "foo2", "B",
174 » » "foo31", "C",
175 » » "foo32", "D",
176 » )
177 » foo3 := NewReplacer(
178 » » "foo11", "A",
179 » » "foo12", "B",
180 » » "foo31", "C",
181 » » "foo32", "D",
182 » )
183 » foo4 := NewReplacer(
184 » » "foo12", "B",
185 » » "foo32", "D",
186 » )
187 » testCases = append(testCases,
188 » » testCase{foo1, "fofoofoo12foo32oo", "fofooA2C2oo"},
189 » » testCase{foo1, "", ""},
190
191 » » testCase{foo2, "fofoofoo12foo32oo", "fofooA2Doo"},
192 » » testCase{foo2, "", ""},
193
194 » » testCase{foo3, "fofoofoo12foo32oo", "fofooBDoo"},
195 » » testCase{foo3, "", ""},
196
197 » » testCase{foo4, "fofoofoo12foo32oo", "fofooBDoo"},
198 » » testCase{foo4, "", ""},
199 » )
200
201 » // genAll maps "\x00\x01\x02...\xfe\xff" to "[all]", amongst other thing s.
202 » allBytes := make([]byte, 256)
203 » for i := range allBytes {
204 » » allBytes[i] = byte(i)
205 » }
206 » allString := string(allBytes)
207 » genAll := NewReplacer(
208 » » allString, "[all]",
209 » » "\xff", "[ff]",
210 » » "\x00", "[00]",
211 » )
212 » testCases = append(testCases,
213 » » testCase{genAll, allString, "[all]"},
214 » » testCase{genAll, "a\xff" + allString + "\x00", "a[ff][all][00]"} ,
215 » » testCase{genAll, "", ""},
216 » )
217
218 » // Test cases with empty old strings.
219
220 » blankToX1 := NewReplacer("", "X")
221 » blankToX2 := NewReplacer("", "X", "", "")
222 » blankHighPriority := NewReplacer("", "X", "o", "O")
223 » blankLowPriority := NewReplacer("o", "O", "", "X")
224 » blankNoOp1 := NewReplacer("", "")
225 » blankNoOp2 := NewReplacer("", "", "", "A")
226 » blankFoo := NewReplacer("", "X", "foobar", "R", "foobaz", "Z")
227 » testCases = append(testCases,
228 » » testCase{blankToX1, "foo", "XfXoXoX"},
229 » » testCase{blankToX1, "", "X"},
230
231 » » testCase{blankToX2, "foo", "XfXoXoX"},
232 » » testCase{blankToX2, "", "X"},
233
234 » » testCase{blankHighPriority, "oo", "XOXOX"},
235 » » testCase{blankHighPriority, "ii", "XiXiX"},
236 » » testCase{blankHighPriority, "iooi", "XiXOXOXiX"},
nigeltao 2012/09/17 01:49:24 I'd also add an "oiio" test.
237 » » testCase{blankHighPriority, "", "X"},
238
239 » » testCase{blankLowPriority, "oo", "OOX"},
240 » » testCase{blankLowPriority, "ii", "XiXiX"},
241 » » testCase{blankLowPriority, "iooi", "XiOOXiX"},
242 » » testCase{blankLowPriority, "", "X"},
243
244 » » testCase{blankNoOp1, "foo", "foo"},
245 » » testCase{blankNoOp1, "", ""},
246
247 » » testCase{blankNoOp2, "foo", "foo"},
248 » » testCase{blankNoOp2, "", ""},
249
250 » » testCase{blankFoo, "foobarfoobaz", "XRXZX"},
251 » » testCase{blankFoo, "foobar-foobaz", "XRX-XZX"},
252 » » testCase{blankFoo, "", "X"},
253 » )
254
255 » // No-arg test cases.
256
257 » nop := NewReplacer()
258 » testCases = append(testCases,
259 » » testCase{nop, "abc", "abc"},
260 » » testCase{nop, "", ""},
261 » )
262
263 » // Run the test cases.
264
265 » for i, tc := range testCases {
266 » » if s := tc.r.Replace(tc.in); s != tc.out {
267 » » » t.Errorf("%d. Replace(%q) = %q, want %q", i, tc.in, s, t c.out)
67 } 268 }
68 var buf bytes.Buffer 269 var buf bytes.Buffer
69 » » n, err := tt.r.WriteString(&buf, tt.in) 270 » » n, err := tc.r.WriteString(&buf, tc.in)
70 if err != nil { 271 if err != nil {
71 t.Errorf("%d. WriteString: %v", i, err) 272 t.Errorf("%d. WriteString: %v", i, err)
72 continue 273 continue
73 } 274 }
74 got := buf.String() 275 got := buf.String()
75 » » if got != tt.out { 276 » » if got != tc.out {
76 » » » t.Errorf("%d. WriteString(%q) wrote %q, want %q", i, tt. in, got, tt.out) 277 » » » t.Errorf("%d. WriteString(%q) wrote %q, want %q", i, tc. in, got, tc.out)
77 continue 278 continue
78 } 279 }
79 » » if n != len(tt.out) { 280 » » if n != len(tc.out) {
80 t.Errorf("%d. WriteString(%q) wrote correct string but r eported %d bytes; want %d (%q)", 281 t.Errorf("%d. WriteString(%q) wrote correct string but r eported %d bytes; want %d (%q)",
81 » » » » i, tt.in, n, len(tt.out), tt.out) 282 » » » » i, tc.in, n, len(tc.out), tc.out)
82 » » } 283 » » }
83 » } 284 » }
84 } 285 }
85 286
86 // pickAlgorithmTest is a test that verifies that given input for a 287 // TestPickAlgorithm tests that NewReplacer picks the correct algorithm.
87 // Replacer that we pick the correct algorithm.
88 type pickAlgorithmTest struct {
89 » r *Replacer
90 » want string // name of algorithm
91 }
92
93 var pickAlgorithmTests = []pickAlgorithmTest{
94 » {capitalLetters, "*strings.byteReplacer"},
95 » {NewReplacer("12", "123"), "*strings.genericReplacer"},
96 » {NewReplacer("1", "12"), "*strings.byteStringReplacer"},
97 » {htmlEscaper, "*strings.byteStringReplacer"},
98 }
99
100 func TestPickAlgorithm(t *testing.T) { 288 func TestPickAlgorithm(t *testing.T) {
101 » for i, tt := range pickAlgorithmTests { 289 » testCases := []struct {
102 » » got := fmt.Sprintf("%T", tt.r.Replacer()) 290 » » r *Replacer
103 » » if got != tt.want { 291 » » want string
104 » » » t.Errorf("%d. algorithm = %s, want %s", i, got, tt.want) 292 » }{
293 » » {capitalLetters, "*strings.byteReplacer"},
294 » » {htmlEscaper, "*strings.byteStringReplacer"},
295 » » {NewReplacer("12", "123"), "*strings.genericReplacer"},
296 » » {NewReplacer("1", "12"), "*strings.byteStringReplacer"},
297 » » {NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.gener icReplacer"},
298 » }
299 » for i, tc := range testCases {
300 » » got := fmt.Sprintf("%T", tc.r.Replacer())
301 » » if got != tc.want {
302 » » » t.Errorf("%d. algorithm = %s, want %s", i, got, tc.want)
303 » » }
304 » }
305 }
306
307 // TestGenericTrieBuilding verifies the structure of the generated trie. There
308 // is one node per line, and the key ending with the current line is in the
309 // trie if it ends with a "+".
310 func TestGenericTrieBuilding(t *testing.T) {
311 » testCases := []struct{ in, out string }{
312 » » {"abc;abdef;abdefgh;xx;xy;z", `-
313 » » » a-
314 » » » .b-
315 » » » ..c+
316 » » » ..d-
317 » » » ...ef+
318 » » » .....gh+
319 » » » x-
320 » » » .x+
321 » » » .y+
322 » » » z+
323 » » » `},
324 » » {"abracadabra;abracadabrakazam;abraham;abrasion", `-
325 » » » a-
326 » » » .bra-
327 » » » ....c-
328 » » » .....adabra+
329 » » » ...........kazam+
330 » » » ....h-
331 » » » .....am+
332 » » » ....s-
333 » » » .....ion+
334 » » » `},
335 » » {"aaa;aa;a;i;longerst;longer;long;xx;x;X;Y", `-
336 » » » X+
337 » » » Y+
338 » » » a+
339 » » » .a+
340 » » » ..a+
341 » » » i+
342 » » » l-
343 » » » .ong+
344 » » » ....er+
345 » » » ......st+
346 » » » x+
347 » » » .x+
348 » » » `},
349 » » {"foo;;foo;foo1", `+
350 » » » f-
351 » » » .oo+
352 » » » ...1+
353 » » » `},
354 » }
355
356 » for _, tc := range testCases {
357 » » keys := Split(tc.in, ";")
358 » » args := make([]string, len(keys)*2)
359 » » for i, key := range keys {
360 » » » args[i*2] = key
361 » » }
362
363 » » got := NewReplacer(args...).PrintTrie()
364 » » // Remove tabs from tc.out
365 » » wantbuf := make([]byte, 0, len(tc.out))
366 » » for i := 0; i < len(tc.out); i++ {
367 » » » if tc.out[i] != '\t' {
368 » » » » wantbuf = append(wantbuf, tc.out[i])
369 » » » }
370 » » }
371 » » want := string(wantbuf)
372
373 » » if got != want {
374 » » » t.Errorf("PrintTrie(%q)\ngot\n%swant\n%s", tc.in, got, w ant)
105 } 375 }
106 } 376 }
107 } 377 }
108 378
109 func BenchmarkGenericNoMatch(b *testing.B) { 379 func BenchmarkGenericNoMatch(b *testing.B) {
110 str := Repeat("A", 100) + Repeat("B", 100) 380 str := Repeat("A", 100) + Repeat("B", 100)
111 generic := NewReplacer("a", "A", "b", "B", "12", "123") // varying lengt hs forces generic 381 generic := NewReplacer("a", "A", "b", "B", "12", "123") // varying lengt hs forces generic
112 for i := 0; i < b.N; i++ { 382 for i := 0; i < b.N; i++ {
113 generic.Replace(str) 383 generic.Replace(str)
114 } 384 }
115 } 385 }
116 386
117 var html = Repeat("It&apos;s &lt;b&gt;HTML&lt;/b&gt;!", 10) 387 func BenchmarkGenericMatch1(b *testing.B) {
nigeltao 2012/09/04 04:29:59 Make this a local variable in BenchmarkGenericMatc
Eric Roshan Eisner 2012/09/04 06:27:52 Done.
118 388 » str := Repeat("a", 100) + Repeat("b", 100)
119 func BenchmarkGenericMatch(b *testing.B) { 389 » generic := NewReplacer("a", "A", "b", "B", "12", "123")
120 » for i := 0; i < b.N; i++ { 390 » for i := 0; i < b.N; i++ {
121 » » htmlUnescaper.Replace(html) 391 » » generic.Replace(str)
392 » }
393 }
394
395 func BenchmarkGenericMatch2(b *testing.B) {
396 » str := Repeat("It&apos;s &lt;b&gt;HTML&lt;/b&gt;!", 100)
397 » for i := 0; i < b.N; i++ {
398 » » htmlUnescaper.Replace(str)
122 } 399 }
123 } 400 }
124 401
125 func BenchmarkByteByteNoMatch(b *testing.B) { 402 func BenchmarkByteByteNoMatch(b *testing.B) {
126 str := Repeat("A", 100) + Repeat("B", 100) 403 str := Repeat("A", 100) + Repeat("B", 100)
127 for i := 0; i < b.N; i++ { 404 for i := 0; i < b.N; i++ {
128 capitalLetters.Replace(str) 405 capitalLetters.Replace(str)
129 } 406 }
130 } 407 }
131 408
(...skipping 14 matching lines...) Expand all
146 func BenchmarkHTMLEscapeNew(b *testing.B) { 423 func BenchmarkHTMLEscapeNew(b *testing.B) {
147 str := "I <3 to escape HTML & other text too." 424 str := "I <3 to escape HTML & other text too."
148 for i := 0; i < b.N; i++ { 425 for i := 0; i < b.N; i++ {
149 htmlEscaper.Replace(str) 426 htmlEscaper.Replace(str)
150 } 427 }
151 } 428 }
152 429
153 func BenchmarkHTMLEscapeOld(b *testing.B) { 430 func BenchmarkHTMLEscapeOld(b *testing.B) {
154 str := "I <3 to escape HTML & other text too." 431 str := "I <3 to escape HTML & other text too."
155 for i := 0; i < b.N; i++ { 432 for i := 0; i < b.N; i++ {
156 » » oldhtmlEscape(str) 433 » » oldHTMLEscape(str)
157 } 434 }
158 } 435 }
159 436
160 // BenchmarkByteByteReplaces compares byteByteImpl against multiple Replaces. 437 // BenchmarkByteByteReplaces compares byteByteImpl against multiple Replaces.
161 func BenchmarkByteByteReplaces(b *testing.B) { 438 func BenchmarkByteByteReplaces(b *testing.B) {
162 str := Repeat("a", 100) + Repeat("b", 100) 439 str := Repeat("a", 100) + Repeat("b", 100)
163 for i := 0; i < b.N; i++ { 440 for i := 0; i < b.N; i++ {
164 Replace(Replace(str, "a", "A", -1), "b", "B", -1) 441 Replace(Replace(str, "a", "A", -1), "b", "B", -1)
165 } 442 }
166 } 443 }
167 444
168 // BenchmarkByteByteMap compares byteByteImpl against Map. 445 // BenchmarkByteByteMap compares byteByteImpl against Map.
169 func BenchmarkByteByteMap(b *testing.B) { 446 func BenchmarkByteByteMap(b *testing.B) {
170 str := Repeat("a", 100) + Repeat("b", 100) 447 str := Repeat("a", 100) + Repeat("b", 100)
171 fn := func(r rune) rune { 448 fn := func(r rune) rune {
172 switch r { 449 switch r {
173 case 'a': 450 case 'a':
174 return 'A' 451 return 'A'
175 case 'b': 452 case 'b':
176 return 'B' 453 return 'B'
177 } 454 }
178 return r 455 return r
179 } 456 }
180 for i := 0; i < b.N; i++ { 457 for i := 0; i < b.N; i++ {
181 Map(fn, str) 458 Map(fn, str)
182 } 459 }
183 } 460 }
LEFTRIGHT

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b