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