1// Copyright 2015 Huan Du. All rights reserved.
2// Licensed under the MIT license that can be found in the LICENSE file.
3
4package xstrings
5
6import (
7	"bytes"
8	"math/rand"
9	"unicode"
10	"unicode/utf8"
11)
12
13// ToCamelCase is to convert words separated by space, underscore and hyphen to camel case.
14//
15// Some samples.
16//     "some_words"      => "SomeWords"
17//     "http_server"     => "HttpServer"
18//     "no_https"        => "NoHttps"
19//     "_complex__case_" => "_Complex_Case_"
20//     "some words"      => "SomeWords"
21func ToCamelCase(str string) string {
22	if len(str) == 0 {
23		return ""
24	}
25
26	buf := &bytes.Buffer{}
27	var r0, r1 rune
28	var size int
29
30	// leading connector will appear in output.
31	for len(str) > 0 {
32		r0, size = utf8.DecodeRuneInString(str)
33		str = str[size:]
34
35		if !isConnector(r0) {
36			r0 = unicode.ToUpper(r0)
37			break
38		}
39
40		buf.WriteRune(r0)
41	}
42
43	if len(str) == 0 {
44		// A special case for a string contains only 1 rune.
45		if size != 0 {
46			buf.WriteRune(r0)
47		}
48
49		return buf.String()
50	}
51
52	for len(str) > 0 {
53		r1 = r0
54		r0, size = utf8.DecodeRuneInString(str)
55		str = str[size:]
56
57		if isConnector(r0) && isConnector(r1) {
58			buf.WriteRune(r1)
59			continue
60		}
61
62		if isConnector(r1) {
63			r0 = unicode.ToUpper(r0)
64		} else {
65			r0 = unicode.ToLower(r0)
66			buf.WriteRune(r1)
67		}
68	}
69
70	buf.WriteRune(r0)
71	return buf.String()
72}
73
74// ToSnakeCase can convert all upper case characters in a string to
75// snake case format.
76//
77// Some samples.
78//     "FirstName"    => "first_name"
79//     "HTTPServer"   => "http_server"
80//     "NoHTTPS"      => "no_https"
81//     "GO_PATH"      => "go_path"
82//     "GO PATH"      => "go_path"  // space is converted to underscore.
83//     "GO-PATH"      => "go_path"  // hyphen is converted to underscore.
84//     "http2xx"      => "http_2xx" // insert an underscore before a number and after an alphabet.
85//     "HTTP20xOK"    => "http_20x_ok"
86//     "Duration2m3s" => "duration_2m3s"
87//     "Bld4Floor3rd" => "bld4_floor_3rd"
88func ToSnakeCase(str string) string {
89	return camelCaseToLowerCase(str, '_')
90}
91
92// ToKebabCase can convert all upper case characters in a string to
93// kebab case format.
94//
95// Some samples.
96//     "FirstName"    => "first-name"
97//     "HTTPServer"   => "http-server"
98//     "NoHTTPS"      => "no-https"
99//     "GO_PATH"      => "go-path"
100//     "GO PATH"      => "go-path"  // space is converted to '-'.
101//     "GO-PATH"      => "go-path"  // hyphen is converted to '-'.
102//     "http2xx"      => "http-2xx" // insert an underscore before a number and after an alphabet.
103//     "HTTP20xOK"    => "http-20x-ok"
104//     "Duration2m3s" => "duration-2m3s"
105//     "Bld4Floor3rd" => "bld4-floor-3rd"
106func ToKebabCase(str string) string {
107	return camelCaseToLowerCase(str, '-')
108}
109
110func camelCaseToLowerCase(str string, connector rune) string {
111	if len(str) == 0 {
112		return ""
113	}
114
115	buf := &bytes.Buffer{}
116	wt, word, remaining := nextWord(str)
117
118	for len(remaining) > 0 {
119		if wt != connectorWord {
120			toLower(buf, wt, word, connector)
121		}
122
123		prev := wt
124		last := word
125		wt, word, remaining = nextWord(remaining)
126
127		switch prev {
128		case numberWord:
129			for wt == alphabetWord || wt == numberWord {
130				toLower(buf, wt, word, connector)
131				wt, word, remaining = nextWord(remaining)
132			}
133
134			if wt != invalidWord && wt != punctWord {
135				buf.WriteRune(connector)
136			}
137
138		case connectorWord:
139			toLower(buf, prev, last, connector)
140
141		case punctWord:
142			// nothing.
143
144		default:
145			if wt != numberWord {
146				if wt != connectorWord && wt != punctWord {
147					buf.WriteRune(connector)
148				}
149
150				break
151			}
152
153			if len(remaining) == 0 {
154				break
155			}
156
157			last := word
158			wt, word, remaining = nextWord(remaining)
159
160			// consider number as a part of previous word.
161			// e.g. "Bld4Floor" => "bld4_floor"
162			if wt != alphabetWord {
163				toLower(buf, numberWord, last, connector)
164
165				if wt != connectorWord && wt != punctWord {
166					buf.WriteRune(connector)
167				}
168
169				break
170			}
171
172			// if there are some lower case letters following a number,
173			// add connector before the number.
174			// e.g. "HTTP2xx" => "http_2xx"
175			buf.WriteRune(connector)
176			toLower(buf, numberWord, last, connector)
177
178			for wt == alphabetWord || wt == numberWord {
179				toLower(buf, wt, word, connector)
180				wt, word, remaining = nextWord(remaining)
181			}
182
183			if wt != invalidWord && wt != connectorWord && wt != punctWord {
184				buf.WriteRune(connector)
185			}
186		}
187	}
188
189	toLower(buf, wt, word, connector)
190	return buf.String()
191}
192
193func isConnector(r rune) bool {
194	return r == '-' || r == '_' || unicode.IsSpace(r)
195}
196
197type wordType int
198
199const (
200	invalidWord wordType = iota
201	numberWord
202	upperCaseWord
203	alphabetWord
204	connectorWord
205	punctWord
206	otherWord
207)
208
209func nextWord(str string) (wt wordType, word, remaining string) {
210	if len(str) == 0 {
211		return
212	}
213
214	var offset int
215	remaining = str
216	r, size := nextValidRune(remaining, utf8.RuneError)
217	offset += size
218
219	if r == utf8.RuneError {
220		wt = invalidWord
221		word = str[:offset]
222		remaining = str[offset:]
223		return
224	}
225
226	switch {
227	case isConnector(r):
228		wt = connectorWord
229		remaining = remaining[size:]
230
231		for len(remaining) > 0 {
232			r, size = nextValidRune(remaining, r)
233
234			if !isConnector(r) {
235				break
236			}
237
238			offset += size
239			remaining = remaining[size:]
240		}
241
242	case unicode.IsPunct(r):
243		wt = punctWord
244		remaining = remaining[size:]
245
246		for len(remaining) > 0 {
247			r, size = nextValidRune(remaining, r)
248
249			if !unicode.IsPunct(r) {
250				break
251			}
252
253			offset += size
254			remaining = remaining[size:]
255		}
256
257	case unicode.IsUpper(r):
258		wt = upperCaseWord
259		remaining = remaining[size:]
260
261		if len(remaining) == 0 {
262			break
263		}
264
265		r, size = nextValidRune(remaining, r)
266
267		switch {
268		case unicode.IsUpper(r):
269			prevSize := size
270			offset += size
271			remaining = remaining[size:]
272
273			for len(remaining) > 0 {
274				r, size = nextValidRune(remaining, r)
275
276				if !unicode.IsUpper(r) {
277					break
278				}
279
280				prevSize = size
281				offset += size
282				remaining = remaining[size:]
283			}
284
285			// it's a bit complex when dealing with a case like "HTTPStatus".
286			// it's expected to be splitted into "HTTP" and "Status".
287			// Therefore "S" should be in remaining instead of word.
288			if len(remaining) > 0 && isAlphabet(r) {
289				offset -= prevSize
290				remaining = str[offset:]
291			}
292
293		case isAlphabet(r):
294			offset += size
295			remaining = remaining[size:]
296
297			for len(remaining) > 0 {
298				r, size = nextValidRune(remaining, r)
299
300				if !isAlphabet(r) || unicode.IsUpper(r) {
301					break
302				}
303
304				offset += size
305				remaining = remaining[size:]
306			}
307		}
308
309	case isAlphabet(r):
310		wt = alphabetWord
311		remaining = remaining[size:]
312
313		for len(remaining) > 0 {
314			r, size = nextValidRune(remaining, r)
315
316			if !isAlphabet(r) || unicode.IsUpper(r) {
317				break
318			}
319
320			offset += size
321			remaining = remaining[size:]
322		}
323
324	case unicode.IsNumber(r):
325		wt = numberWord
326		remaining = remaining[size:]
327
328		for len(remaining) > 0 {
329			r, size = nextValidRune(remaining, r)
330
331			if !unicode.IsNumber(r) {
332				break
333			}
334
335			offset += size
336			remaining = remaining[size:]
337		}
338
339	default:
340		wt = otherWord
341		remaining = remaining[size:]
342
343		for len(remaining) > 0 {
344			r, size = nextValidRune(remaining, r)
345
346			if size == 0 || isConnector(r) || isAlphabet(r) || unicode.IsNumber(r) || unicode.IsPunct(r) {
347				break
348			}
349
350			offset += size
351			remaining = remaining[size:]
352		}
353	}
354
355	word = str[:offset]
356	return
357}
358
359func nextValidRune(str string, prev rune) (r rune, size int) {
360	var sz int
361
362	for len(str) > 0 {
363		r, sz = utf8.DecodeRuneInString(str)
364		size += sz
365
366		if r != utf8.RuneError {
367			return
368		}
369
370		str = str[sz:]
371	}
372
373	r = prev
374	return
375}
376
377func toLower(buf *bytes.Buffer, wt wordType, str string, connector rune) {
378	buf.Grow(buf.Len() + len(str))
379
380	if wt != upperCaseWord && wt != connectorWord {
381		buf.WriteString(str)
382		return
383	}
384
385	for len(str) > 0 {
386		r, size := utf8.DecodeRuneInString(str)
387		str = str[size:]
388
389		if isConnector(r) {
390			buf.WriteRune(connector)
391		} else if unicode.IsUpper(r) {
392			buf.WriteRune(unicode.ToLower(r))
393		} else {
394			buf.WriteRune(r)
395		}
396	}
397}
398
399// SwapCase will swap characters case from upper to lower or lower to upper.
400func SwapCase(str string) string {
401	var r rune
402	var size int
403
404	buf := &bytes.Buffer{}
405
406	for len(str) > 0 {
407		r, size = utf8.DecodeRuneInString(str)
408
409		switch {
410		case unicode.IsUpper(r):
411			buf.WriteRune(unicode.ToLower(r))
412
413		case unicode.IsLower(r):
414			buf.WriteRune(unicode.ToUpper(r))
415
416		default:
417			buf.WriteRune(r)
418		}
419
420		str = str[size:]
421	}
422
423	return buf.String()
424}
425
426// FirstRuneToUpper converts first rune to upper case if necessary.
427func FirstRuneToUpper(str string) string {
428	if str == "" {
429		return str
430	}
431
432	r, size := utf8.DecodeRuneInString(str)
433
434	if !unicode.IsLower(r) {
435		return str
436	}
437
438	buf := &bytes.Buffer{}
439	buf.WriteRune(unicode.ToUpper(r))
440	buf.WriteString(str[size:])
441	return buf.String()
442}
443
444// FirstRuneToLower converts first rune to lower case if necessary.
445func FirstRuneToLower(str string) string {
446	if str == "" {
447		return str
448	}
449
450	r, size := utf8.DecodeRuneInString(str)
451
452	if !unicode.IsUpper(r) {
453		return str
454	}
455
456	buf := &bytes.Buffer{}
457	buf.WriteRune(unicode.ToLower(r))
458	buf.WriteString(str[size:])
459	return buf.String()
460}
461
462// Shuffle randomizes runes in a string and returns the result.
463// It uses default random source in `math/rand`.
464func Shuffle(str string) string {
465	if str == "" {
466		return str
467	}
468
469	runes := []rune(str)
470	index := 0
471
472	for i := len(runes) - 1; i > 0; i-- {
473		index = rand.Intn(i + 1)
474
475		if i != index {
476			runes[i], runes[index] = runes[index], runes[i]
477		}
478	}
479
480	return string(runes)
481}
482
483// ShuffleSource randomizes runes in a string with given random source.
484func ShuffleSource(str string, src rand.Source) string {
485	if str == "" {
486		return str
487	}
488
489	runes := []rune(str)
490	index := 0
491	r := rand.New(src)
492
493	for i := len(runes) - 1; i > 0; i-- {
494		index = r.Intn(i + 1)
495
496		if i != index {
497			runes[i], runes[index] = runes[index], runes[i]
498		}
499	}
500
501	return string(runes)
502}
503
504// Successor returns the successor to string.
505//
506// If there is one alphanumeric rune is found in string, increase the rune by 1.
507// If increment generates a "carry", the rune to the left of it is incremented.
508// This process repeats until there is no carry, adding an additional rune if necessary.
509//
510// If there is no alphanumeric rune, the rightmost rune will be increased by 1
511// regardless whether the result is a valid rune or not.
512//
513// Only following characters are alphanumeric.
514//     * a - z
515//     * A - Z
516//     * 0 - 9
517//
518// Samples (borrowed from ruby's String#succ document):
519//     "abcd"      => "abce"
520//     "THX1138"   => "THX1139"
521//     "<<koala>>" => "<<koalb>>"
522//     "1999zzz"   => "2000aaa"
523//     "ZZZ9999"   => "AAAA0000"
524//     "***"       => "**+"
525func Successor(str string) string {
526	if str == "" {
527		return str
528	}
529
530	var r rune
531	var i int
532	carry := ' '
533	runes := []rune(str)
534	l := len(runes)
535	lastAlphanumeric := l
536
537	for i = l - 1; i >= 0; i-- {
538		r = runes[i]
539
540		if ('a' <= r && r <= 'y') ||
541			('A' <= r && r <= 'Y') ||
542			('0' <= r && r <= '8') {
543			runes[i]++
544			carry = ' '
545			lastAlphanumeric = i
546			break
547		}
548
549		switch r {
550		case 'z':
551			runes[i] = 'a'
552			carry = 'a'
553			lastAlphanumeric = i
554
555		case 'Z':
556			runes[i] = 'A'
557			carry = 'A'
558			lastAlphanumeric = i
559
560		case '9':
561			runes[i] = '0'
562			carry = '0'
563			lastAlphanumeric = i
564		}
565	}
566
567	// Needs to add one character for carry.
568	if i < 0 && carry != ' ' {
569		buf := &bytes.Buffer{}
570		buf.Grow(l + 4) // Reserve enough space for write.
571
572		if lastAlphanumeric != 0 {
573			buf.WriteString(str[:lastAlphanumeric])
574		}
575
576		buf.WriteRune(carry)
577
578		for _, r = range runes[lastAlphanumeric:] {
579			buf.WriteRune(r)
580		}
581
582		return buf.String()
583	}
584
585	// No alphanumeric character. Simply increase last rune's value.
586	if lastAlphanumeric == l {
587		runes[l-1]++
588	}
589
590	return string(runes)
591}
592