StringExtensions.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2020 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. extension StringProtocol where SubSequence == Substring {
  12. func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
  13. let columns = columns - wrappingIndent
  14. guard columns > 0 else {
  15. // Skip wrapping logic if the number of columns is less than 1 in release
  16. // builds and assert in debug builds.
  17. assertionFailure(
  18. "`columns - wrappingIndent` should be always be greater than 0.")
  19. return ""
  20. }
  21. var result: [Substring] = []
  22. var currentIndex = startIndex
  23. while true {
  24. let nextChunk = self[currentIndex...].prefix(columns)
  25. if let lastLineBreak = nextChunk.lastIndex(of: "\n") {
  26. result.append(
  27. contentsOf: self[currentIndex..<lastLineBreak].split(
  28. separator: "\n", omittingEmptySubsequences: false))
  29. currentIndex = index(after: lastLineBreak)
  30. } else if nextChunk.endIndex == self.endIndex {
  31. result.append(self[currentIndex...])
  32. break
  33. } else if let lastSpace = nextChunk.lastIndex(of: " ") {
  34. result.append(self[currentIndex..<lastSpace])
  35. currentIndex = index(after: lastSpace)
  36. } else if let nextSpace = self[currentIndex...].firstIndex(of: " ") {
  37. result.append(self[currentIndex..<nextSpace])
  38. currentIndex = index(after: nextSpace)
  39. } else {
  40. result.append(self[currentIndex...])
  41. break
  42. }
  43. }
  44. return
  45. result
  46. .map {
  47. $0.isEmpty ? $0 : String(repeating: " ", count: wrappingIndent) + $0
  48. }
  49. .joined(separator: "\n")
  50. }
  51. /// Returns this string prefixed using a camel-case style.
  52. ///
  53. /// Example:
  54. ///
  55. /// "hello".addingIntercappedPrefix("my")
  56. /// // myHello
  57. func addingIntercappedPrefix(_ prefix: String) -> String {
  58. guard let firstChar = first else { return prefix }
  59. return "\(prefix)\(firstChar.uppercased())\(self.dropFirst())"
  60. }
  61. /// Returns this string prefixed using kebab-, snake-, or camel-case style
  62. /// depending on what can be detected from the string.
  63. ///
  64. /// Examples:
  65. ///
  66. /// "hello".addingPrefixWithAutodetectedStyle("my")
  67. /// // my-hello
  68. /// "hello_there".addingPrefixWithAutodetectedStyle("my")
  69. /// // my_hello_there
  70. /// "hello-there".addingPrefixWithAutodetectedStyle("my")
  71. /// // my-hello-there
  72. /// "helloThere".addingPrefixWithAutodetectedStyle("my")
  73. /// // myHelloThere
  74. func addingPrefixWithAutodetectedStyle(_ prefix: String) -> String {
  75. if contains("-") {
  76. return "\(prefix)-\(self)"
  77. } else if contains("_") {
  78. return "\(prefix)_\(self)"
  79. } else if first?.isLowercase == true && contains(where: { $0.isUppercase })
  80. {
  81. return addingIntercappedPrefix(prefix)
  82. } else {
  83. return "\(prefix)-\(self)"
  84. }
  85. }
  86. /// Returns a new string with the camel-case-based words of this string
  87. /// split by the specified separator.
  88. ///
  89. /// Examples:
  90. ///
  91. /// "myProperty".convertedToSnakeCase()
  92. /// // my_property
  93. /// "myURLProperty".convertedToSnakeCase()
  94. /// // my_url_property
  95. /// "myURLProperty".convertedToSnakeCase(separator: "-")
  96. /// // my-url-property
  97. func convertedToSnakeCase(separator: Character = "_") -> String {
  98. guard !isEmpty else { return "" }
  99. var result = ""
  100. // Whether we should append a separator when we see a uppercase character.
  101. var separateOnUppercase = true
  102. for index in indices {
  103. let nextIndex = self.index(after: index)
  104. let character = self[index]
  105. if character.isUppercase {
  106. if separateOnUppercase && !result.isEmpty {
  107. // Append the separator.
  108. result += "\(separator)"
  109. }
  110. // If the next character is uppercase and the next-next character is lowercase, like "L" in "URLSession", we should separate words.
  111. separateOnUppercase =
  112. nextIndex < endIndex && self[nextIndex].isUppercase
  113. && self.index(after: nextIndex) < endIndex
  114. && self[self.index(after: nextIndex)].isLowercase
  115. } else {
  116. // If the character is `separator`, we do not want to append another separator when we see the next uppercase character.
  117. separateOnUppercase = character != separator
  118. }
  119. // Append the lowercased character.
  120. result += character.lowercased()
  121. }
  122. return result
  123. }
  124. /// Returns the edit distance between this string and the provided target string.
  125. ///
  126. /// Uses the Levenshtein distance algorithm internally.
  127. ///
  128. /// See: https://en.wikipedia.org/wiki/Levenshtein_distance
  129. ///
  130. /// Examples:
  131. ///
  132. /// "kitten".editDistance(to: "sitting")
  133. /// // 3
  134. /// "bar".editDistance(to: "baz")
  135. /// // 1
  136. func editDistance(to target: String) -> Int {
  137. let rows = self.count
  138. let columns = target.count
  139. if rows <= 0 || columns <= 0 {
  140. return Swift.max(rows, columns)
  141. }
  142. // Trim common prefix and suffix
  143. var selfStartTrim = self.startIndex
  144. var targetStartTrim = target.startIndex
  145. while selfStartTrim < self.endIndex && targetStartTrim < target.endIndex
  146. && self[selfStartTrim] == target[targetStartTrim]
  147. {
  148. self.formIndex(after: &selfStartTrim)
  149. target.formIndex(after: &targetStartTrim)
  150. }
  151. var selfEndTrim = self.endIndex
  152. var targetEndTrim = target.endIndex
  153. while selfEndTrim > selfStartTrim && targetEndTrim > targetStartTrim {
  154. let selfIdx = self.index(before: selfEndTrim)
  155. let targetIdx = target.index(before: targetEndTrim)
  156. guard self[selfIdx] == target[targetIdx] else {
  157. break
  158. }
  159. selfEndTrim = selfIdx
  160. targetEndTrim = targetIdx
  161. }
  162. // Equal strings
  163. guard
  164. !(selfStartTrim == self.endIndex && targetStartTrim == target.endIndex)
  165. else {
  166. return 0
  167. }
  168. // After trimming common prefix and suffix, self is empty.
  169. guard selfStartTrim < selfEndTrim else {
  170. return target.distance(
  171. from: targetStartTrim,
  172. to: targetEndTrim)
  173. }
  174. // After trimming common prefix and suffix, target is empty.
  175. guard targetStartTrim < targetEndTrim else {
  176. return distance(
  177. from: selfStartTrim,
  178. to: selfEndTrim)
  179. }
  180. let newSelf = self[selfStartTrim..<selfEndTrim]
  181. let newTarget = target[targetStartTrim..<targetEndTrim]
  182. let m = newSelf.count
  183. let n = newTarget.count
  184. // Initialize the levenshtein matrix with only two rows
  185. // current and previous.
  186. var previousRow = [Int](repeating: 0, count: n + 1)
  187. var currentRow = [Int](0...n)
  188. var sourceIdx = newSelf.startIndex
  189. for i in 1...m {
  190. swap(&previousRow, &currentRow)
  191. currentRow[0] = i
  192. var targetIdx = newTarget.startIndex
  193. for j in 1...n {
  194. // If characteres are equal for the levenshtein algorithm the
  195. // minimum will always be the substitution cost, so we can fast
  196. // path here in order to avoid min calls.
  197. if newSelf[sourceIdx] == newTarget[targetIdx] {
  198. currentRow[j] = previousRow[j - 1]
  199. } else {
  200. let deletion = previousRow[j]
  201. let insertion = currentRow[j - 1]
  202. let substitution = previousRow[j - 1]
  203. currentRow[j] =
  204. Swift.min(deletion, Swift.min(insertion, substitution)) + 1
  205. }
  206. // j += 1
  207. newTarget.formIndex(after: &targetIdx)
  208. }
  209. // i += 1
  210. newSelf.formIndex(after: &sourceIdx)
  211. }
  212. return currentRow[n]
  213. }
  214. func indentingEachLine(by n: Int) -> String {
  215. let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
  216. let spacer = String(repeating: " ", count: n)
  217. return lines.map {
  218. $0.isEmpty ? $0 : spacer + $0
  219. }.joined(separator: "\n")
  220. }
  221. func hangingIndentingEachLine(by n: Int) -> String {
  222. let lines = self.split(
  223. separator: "\n",
  224. maxSplits: 1,
  225. omittingEmptySubsequences: false)
  226. guard lines.count == 2 else { return lines.joined(separator: "") }
  227. return "\(lines[0])\n\(lines[1].indentingEachLine(by: n))"
  228. }
  229. var nonEmpty: Self? {
  230. isEmpty ? nil : self
  231. }
  232. }