gstr_case.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
  2. //
  3. // This Source Code Form is subject to the terms of the MIT License.
  4. // If a copy of the MIT was not distributed with this file,
  5. // You can obtain one at https://github.com/gogf/gf.
  6. //
  7. // | Function | Result |
  8. // |-----------------------------------|--------------------|
  9. // | CaseSnake(s) | any_kind_of_string |
  10. // | CaseSnakeScreaming(s) | ANY_KIND_OF_STRING |
  11. // | CaseSnakeFirstUpper("RGBCodeMd5") | rgb_code_md5 |
  12. // | CaseKebab(s) | any-kind-of-string |
  13. // | CaseKebabScreaming(s) | ANY-KIND-OF-STRING |
  14. // | CaseDelimited(s, '.') | any.kind.of.string |
  15. // | CaseDelimitedScreaming(s, '.') | ANY.KIND.OF.STRING |
  16. // | CaseCamel(s) | AnyKindOfString |
  17. // | CaseCamelLower(s) | anyKindOfString |
  18. package gstr
  19. import (
  20. "regexp"
  21. "strings"
  22. )
  23. // CaseType is the type for Case.
  24. type CaseType string
  25. // The case type constants.
  26. const (
  27. Camel CaseType = "Camel"
  28. CamelLower CaseType = "CamelLower"
  29. Snake CaseType = "Snake"
  30. SnakeFirstUpper CaseType = "SnakeFirstUpper"
  31. SnakeScreaming CaseType = "SnakeScreaming"
  32. Kebab CaseType = "Kebab"
  33. KebabScreaming CaseType = "KebabScreaming"
  34. Lower CaseType = "Lower"
  35. )
  36. var (
  37. numberSequence = regexp.MustCompile(`([a-zA-Z]{0,1})(\d+)([a-zA-Z]{0,1})`)
  38. firstCamelCaseStart = regexp.MustCompile(`([A-Z]+)([A-Z]?[_a-z\d]+)|$`)
  39. firstCamelCaseEnd = regexp.MustCompile(`([\w\W]*?)([_]?[A-Z]+)$`)
  40. )
  41. // CaseTypeMatch matches the case type from string.
  42. func CaseTypeMatch(caseStr string) CaseType {
  43. caseTypes := []CaseType{
  44. Camel,
  45. CamelLower,
  46. Snake,
  47. SnakeFirstUpper,
  48. SnakeScreaming,
  49. Kebab,
  50. KebabScreaming,
  51. Lower,
  52. }
  53. for _, caseType := range caseTypes {
  54. if Equal(caseStr, string(caseType)) {
  55. return caseType
  56. }
  57. }
  58. return CaseType(caseStr)
  59. }
  60. // CaseConvert converts a string to the specified naming convention.
  61. // Use CaseTypeMatch to match the case type from string.
  62. func CaseConvert(s string, caseType CaseType) string {
  63. if s == "" || caseType == "" {
  64. return s
  65. }
  66. switch caseType {
  67. case Camel:
  68. return CaseCamel(s)
  69. case CamelLower:
  70. return CaseCamelLower(s)
  71. case Kebab:
  72. return CaseKebab(s)
  73. case KebabScreaming:
  74. return CaseKebabScreaming(s)
  75. case Snake:
  76. return CaseSnake(s)
  77. case SnakeFirstUpper:
  78. return CaseSnakeFirstUpper(s)
  79. case SnakeScreaming:
  80. return CaseSnakeScreaming(s)
  81. case Lower:
  82. return ToLower(s)
  83. default:
  84. return s
  85. }
  86. }
  87. // CaseCamel converts a string to CamelCase.
  88. func CaseCamel(s string) string {
  89. return toCamelInitCase(s, true)
  90. }
  91. // CaseCamelLower converts a string to lowerCamelCase.
  92. func CaseCamelLower(s string) string {
  93. if s == "" {
  94. return s
  95. }
  96. if r := rune(s[0]); r >= 'A' && r <= 'Z' {
  97. s = strings.ToLower(string(r)) + s[1:]
  98. }
  99. return toCamelInitCase(s, false)
  100. }
  101. // CaseSnake converts a string to snake_case.
  102. func CaseSnake(s string) string {
  103. return CaseDelimited(s, '_')
  104. }
  105. // CaseSnakeScreaming converts a string to SNAKE_CASE_SCREAMING.
  106. func CaseSnakeScreaming(s string) string {
  107. return CaseDelimitedScreaming(s, '_', true)
  108. }
  109. // CaseSnakeFirstUpper converts a string like "RGBCodeMd5" to "rgb_code_md5".
  110. // TODO for efficiency should change regexp to traversing string in future.
  111. func CaseSnakeFirstUpper(word string, underscore ...string) string {
  112. replace := "_"
  113. if len(underscore) > 0 {
  114. replace = underscore[0]
  115. }
  116. m := firstCamelCaseEnd.FindAllStringSubmatch(word, 1)
  117. if len(m) > 0 {
  118. word = m[0][1] + replace + TrimLeft(ToLower(m[0][2]), replace)
  119. }
  120. for {
  121. m = firstCamelCaseStart.FindAllStringSubmatch(word, 1)
  122. if len(m) > 0 && m[0][1] != "" {
  123. w := strings.ToLower(m[0][1])
  124. w = w[:len(w)-1] + replace + string(w[len(w)-1])
  125. word = strings.Replace(word, m[0][1], w, 1)
  126. } else {
  127. break
  128. }
  129. }
  130. return TrimLeft(word, replace)
  131. }
  132. // CaseKebab converts a string to kebab-case
  133. func CaseKebab(s string) string {
  134. return CaseDelimited(s, '-')
  135. }
  136. // CaseKebabScreaming converts a string to KEBAB-CASE-SCREAMING.
  137. func CaseKebabScreaming(s string) string {
  138. return CaseDelimitedScreaming(s, '-', true)
  139. }
  140. // CaseDelimited converts a string to snake.case.delimited.
  141. func CaseDelimited(s string, del byte) string {
  142. return CaseDelimitedScreaming(s, del, false)
  143. }
  144. // CaseDelimitedScreaming converts a string to DELIMITED.SCREAMING.CASE or delimited.screaming.case.
  145. func CaseDelimitedScreaming(s string, del uint8, screaming bool) string {
  146. s = addWordBoundariesToNumbers(s)
  147. s = strings.Trim(s, " ")
  148. n := ""
  149. for i, v := range s {
  150. // treat acronyms as words, eg for JSONData -> JSON is a whole word
  151. nextCaseIsChanged := false
  152. if i+1 < len(s) {
  153. next := s[i+1]
  154. if (v >= 'A' && v <= 'Z' && next >= 'a' && next <= 'z') || (v >= 'a' && v <= 'z' && next >= 'A' && next <= 'Z') {
  155. nextCaseIsChanged = true
  156. }
  157. }
  158. if i > 0 && n[len(n)-1] != del && nextCaseIsChanged {
  159. // add underscore if next letter case type is changed
  160. if v >= 'A' && v <= 'Z' {
  161. n += string(del) + string(v)
  162. } else if v >= 'a' && v <= 'z' {
  163. n += string(v) + string(del)
  164. }
  165. } else if v == ' ' || v == '_' || v == '-' || v == '.' {
  166. // replace spaces/underscores with delimiters
  167. n += string(del)
  168. } else {
  169. n = n + string(v)
  170. }
  171. }
  172. if screaming {
  173. n = strings.ToUpper(n)
  174. } else {
  175. n = strings.ToLower(n)
  176. }
  177. return n
  178. }
  179. func addWordBoundariesToNumbers(s string) string {
  180. r := numberSequence.ReplaceAllFunc([]byte(s), func(bytes []byte) []byte {
  181. var result []byte
  182. match := numberSequence.FindSubmatch(bytes)
  183. if len(match[1]) > 0 {
  184. result = append(result, match[1]...)
  185. result = append(result, []byte(" ")...)
  186. }
  187. result = append(result, match[2]...)
  188. if len(match[3]) > 0 {
  189. result = append(result, []byte(" ")...)
  190. result = append(result, match[3]...)
  191. }
  192. return result
  193. })
  194. return string(r)
  195. }
  196. // Converts a string to CamelCase
  197. func toCamelInitCase(s string, initCase bool) string {
  198. s = addWordBoundariesToNumbers(s)
  199. s = strings.Trim(s, " ")
  200. n := ""
  201. capNext := initCase
  202. for _, v := range s {
  203. if v >= 'A' && v <= 'Z' {
  204. n += string(v)
  205. }
  206. if v >= '0' && v <= '9' {
  207. n += string(v)
  208. }
  209. if v >= 'a' && v <= 'z' {
  210. if capNext {
  211. n += strings.ToUpper(string(v))
  212. } else {
  213. n += string(v)
  214. }
  215. }
  216. if v == '_' || v == ' ' || v == '-' || v == '.' {
  217. capNext = true
  218. } else {
  219. capNext = false
  220. }
  221. }
  222. return n
  223. }