Skip to content

Commit

Permalink
fix: camel casing algorithm
Browse files Browse the repository at this point in the history
Improve the camel casing algorithm by making it simpler
  • Loading branch information
maticzav authored Nov 16, 2021
2 parents 59484a6 + 184d9fe commit d7db775
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 66 deletions.
122 changes: 58 additions & 64 deletions Sources/SwiftGraphQLCodegen/Extensions/String+Case.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +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.
var words = [Range<String.Index>]()

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

while let delimiterRange = rangeOfCharacter(from: upperDelimiters, options: [], range: searchRange) {
let range = wordStart ..< delimiterRange.lowerBound

// We hit a special character. If there's something to capture, capture it.
// Move one up and continue in the next cycle.
guard self[delimiterRange.lowerBound].isLetter else {
if !range.isEmpty { words.append(range) }
wordStart = index(after: range.upperBound)
searchRange = wordStart ..< searchRange.upperBound
var result = ""

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
}

// We know that we hit an uppercase character.
// Set the word start to that character, and search from the next character onwards.
wordStart = delimiterRange.lowerBound
searchRange = index(after: wordStart) ..< searchRange.upperBound

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

// 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
@@ -1,9 +1,10 @@
@testable import SwiftGraphQL
@testable import SwiftGraphQLCodegen
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

1 comment on commit d7db775

@vercel
Copy link

@vercel vercel bot commented on d7db775 Nov 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.