Skip to content

Commit

Permalink
fix casing and simplify the algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
maticzav committed Nov 16, 2021
1 parent 91fb852 commit 184d9fe
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 79 deletions.
135 changes: 57 additions & 78 deletions Sources/SwiftGraphQLCodegen/Extensions/String+Case.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,93 +5,72 @@ extension String {

// Returns the string PascalCased.
var pascalCase: String {
let specialChars = CharacterSet.alphanumerics.inverted
let upperCaseChars = CharacterSet.uppercaseLetters
let lowerCaseChars = CharacterSet.lowercaseLetters
let upperDelimiters = CharacterSet().union(specialChars).union(upperCaseChars)
let lowerDelimiters = CharacterSet().union(specialChars).union(lowerCaseChars)

// This algorithm is heavily inspired by JSONEncoder's `_convertToSnakeCase` function in Swift source code.
// You can find the algorithm at https://github.com/apple/swift-corelibs-foundation/blob/558c1d526f14544da43fa77292e6d4155325c4b1/Sources/Foundation/JSONEncoder.swift#L27
//
// The general ide of this algorithm is to split words on
// - transaction from lower to upper case,
// - on transition of >1 upper case charaters to lowercase
// - on special characters, and
// - on spaces.
//
// Then, we simply concatanate the words and capitalise the first letter.
var result = ""

var words = [Range<String.Index>]()

var wordStart = startIndex
var searchRange: Range<String.Index> = wordStart ..< endIndex

// Find the next delimiter.
while let delimiterRange = self.rangeOfCharacter(from: upperDelimiters, options: [], range: searchRange) {
let untilDelimiter = wordStart ..< delimiterRange.lowerBound

// We hit a special character. If there's something to capture, capture it.
// Otherwise, leave it, update the range one up and continue in the next loop cycle.
//
// This makes sure that we capture letter delimiters - not just special characters,
// and leave sequences of special characters.
guard self[delimiterRange.lowerBound].isLetter else {
if !untilDelimiter.isEmpty {
words.append(untilDelimiter)
}
wordStart = index(after: untilDelimiter.upperBound)
searchRange = wordStart ..< searchRange.upperBound
var capitalize = true
var isAbbreviation = false

for index in self.indices {
let char = self[index]

if char.isNumber {
result.append(char)
capitalize = true
isAbbreviation = false
continue
}

// Set the word start to that character, and search from the next delimiter onwards.
wordStart = delimiterRange.lowerBound
searchRange = index(after: wordStart) ..< searchRange.upperBound

if !untilDelimiter.isEmpty {
words.append(untilDelimiter)
}

// If there are no more lower delimiters. Just end here and append all the remaining uppercase characters.
guard let lowerCaseRange = rangeOfCharacter(from: lowerDelimiters, options: [], range: searchRange) else {
break

// Skip all special characters.
guard char.isLetter else {
capitalize = true
isAbbreviation = false
continue
}

// We found the next small character delimiter. If it comes right after the current character,
// we should take care of it in the next cycle.
// Otherwise, we should take everything up to the next lowercase character.
let nextCharacterAfterDelimiter = index(after: delimiterRange.lowerBound)
if lowerCaseRange.lowerBound == nextCharacterAfterDelimiter {

if char.isLowercase {
if capitalize {
result.append(char.uppercased())
} else {
result.append(char)
}
capitalize = false
isAbbreviation = false
continue
} else {
// There was a range of >1 capital letters.

// If the next character after the capital letters is not a letter, turn all the capital letters into a word.
// Else turn all the capital letters up the second last index into a word.
if !self[lowerCaseRange.lowerBound].isLetter {
words.append(wordStart ..< lowerCaseRange.lowerBound)

// Next word starts after capital letters we just found
wordStart = lowerCaseRange.lowerBound
}

// abcABCDe
// abcAbcDe
if char.isUppercase {
if index < self.index(before: self.endIndex) {
let isNextCharLowerCase = self[self.index(after: index)].isLowercase

// D -> D
if isNextCharLowerCase {
isAbbreviation = false
result.append(char)
capitalize = false
continue
}
}

// A -> A
if !isAbbreviation {
isAbbreviation = true
result.append(char)
capitalize = false
} else {
words.append(wordStart ..< index(before: lowerCaseRange.lowerBound))

// Next word starts at the last capital letters we just found
wordStart = index(before: lowerCaseRange.lowerBound)
// B -> b
if capitalize {
result.append(char)
capitalize = false
} else {
result.append(char.lowercased())
}
}

searchRange = wordStart ..< searchRange.upperBound
continue
}
}

let remaining = wordStart ..< searchRange.upperBound
if !remaining.isEmpty {
// Append the last part of the word.
words.append(remaining)
}

let result = words.map { self[$0].capitalized }.joined()

return result
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import XCTest

final class StringExtensionsTest: XCTestCase {
func testCamelCase() {
XCTAssertEqual("___a very peculiarNameIndeed__wouldNot.you.agree.AMAZING?____".camelCase, "aVeryPeculiarNameIndeedWouldNotYouAgreeAmazing")
XCTAssertEqual(".AGREE?_".camelCase, "agree")
XCTAssertEqual("___a very peculiarName__wouldNot.you.AGREE?____".camelCase, "aVeryPeculiarNameWouldNotYouAgree")
XCTAssertEqual("ENUM".camelCase, "enum")
XCTAssertEqual("linkToURL".camelCase, "linkToUrl")
XCTAssertEqual("grandfather_father.son grandson".camelCase, "grandfatherFatherSonGrandson")
Expand Down

0 comments on commit 184d9fe

Please sign in to comment.