Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# StringCase

Converts `String` to `lowerCamelCase`, `UpperCamelCase` and `snake_case`.
Converts `String` to `lowerCamelCase`, `UpperCamelCase`, `snake_case` and `kebab-case`.

## Usage

Expand All @@ -12,6 +12,7 @@ let input = "Keynote Event"
input.lowerCamelCased() // "keynoteEvent"
input.upperCamelCased() // "KeynoteEvent"
input.snakeCased() // "keynote_event"
input.kebabCased() // "keynote-event"
```

```swift
Expand All @@ -20,6 +21,7 @@ let input = "_this is* not-Very%difficult"
input.lowerCamelCased() // "thisIsNotVeryDifficult"
input.upperCamelCased() // "ThisIsNotVeryDifficult"
input.snakeCased() // "this_is_not_very_difficult"
input.kebabCased() // "this-is-not-very-difficult"
```

### Boolean checks
Expand All @@ -42,6 +44,12 @@ input.snakeCased() // "this_is_not_very_difficult"
"keynote_event".isSnakeCase // true
```

```swift
"KeynoteEvent".isKebabCase // false
"keynoteEvent".isKebabCase // false
"keynote-event".isKebabCase // true
```

## Contact

* Devran "Cosmo" Uenal
Expand Down
57 changes: 48 additions & 9 deletions Sources/StringCase/StringCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,26 @@ public extension String {
/// In snake case, words are separated by underscores.
var isSnakeCase: Bool {
// Strip all underscores and check if the rest is lowercase
return self.filter{ $0 != "_" }.allSatisfy { $0.isLowercase }
return self
.filter { $0 != "_" }
.allSatisfy(\.isLowercase)
}

/// A Boolean value indicating whether this string is considered kebab case.
///
/// For example, the following strings are all kebab case:
///
/// - "kebab-case"
/// - "example"
/// - "date-formatter"
///
/// String can contain lowercase letters and dashes only.
/// In kebab case, words are separated by dashes.
var isKebabCase: Bool {
// Strip all dashes and check if the rest is lowercase
return self
.filter { $0 != "-" }
.allSatisfy(\.isLowercase)
}

/// A Boolean value indicating whether this string is considered lower camel case.
Expand All @@ -28,10 +47,11 @@ public extension String {
/// In lower camel case, words are separated by uppercase letters.
var isLowerCamelCase: Bool {
// Check if the first character is lowercase and the rest contains letters
if let firstCharacter = self.first, firstCharacter.isLowercase && self.allSatisfy { $0.isLetter } {
return true
if let firstCharacter = self.first {
return firstCharacter.isLowercase && self.allSatisfy(\.isLetter)
} else {
return false
}
return false
}

/// A Boolean value indicating whether this string is considered upper camel case.
Expand All @@ -46,10 +66,11 @@ public extension String {
/// In upper camel case, words are separated by uppercase letters.
var isUpperCamelCase: Bool {
// Check if the first character is uppercase and the rest contains letters
if let firstCharacter = self.first, firstCharacter.isUppercase && self.allSatisfy { $0.isLetter } {
return true
if let firstCharacter = self.first {
return firstCharacter.isUppercase && self.allSatisfy(\.isLetter)
} else {
return false
}
return false
}
}

Expand All @@ -74,7 +95,7 @@ public extension String {
lastCharacter = character
}

return results.map { $0.capitalized }
return results.map(\.capitalized)
}

/// Returns a lower camel case version of the string.
Expand Down Expand Up @@ -120,6 +141,24 @@ public extension String {
/// - Returns: A snake case copy of the string.
func snakeCased() -> String {
if self.isSnakeCase { return self }
return lowercasedStrings().map{ $0.lowercased() }.joined(separator: "_")
return lowercasedStrings()
.map { $0.lowercased() }
.joined(separator: "_")
}

/// Returns kebab case version of the string.
///
/// Here's an example of transforming a string to kebab case.
///
/// let event = "Keynote Event"
/// print(event.kebabCased())
/// // Prints "keynote-event"
///
/// - Returns: A kebab case copy of the string.
func kebabCased() -> String {
if self.isKebabCase { return self }
return lowercasedStrings()
.map { $0.lowercased() }
.joined(separator: "-")
}
}
24 changes: 24 additions & 0 deletions Tests/StringCaseTests/StringCaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ final class StringCaseTests: XCTestCase {
XCTAssertTrue("snake".isSnakeCase)
XCTAssertTrue("snake_case".isSnakeCase)
XCTAssertTrue("snake_case_example".isSnakeCase)
XCTAssertFalse("SNAKE".isSnakeCase)
XCTAssertFalse("not_a_SNAKECASE_String".isSnakeCase)
XCTAssertFalse("notSnakeCase".isSnakeCase)
XCTAssertFalse("AlsoNotSnakeCase".isSnakeCase)

XCTAssertEqual("snake".snakeCased(), "snake")
XCTAssertEqual("SNAKE".snakeCased(), "snake")
XCTAssertEqual("snake cased".snakeCased(), "snake_cased")
XCTAssertEqual("snakeCased".snakeCased(), "snake_cased")
XCTAssertEqual("snake Cased_String".snakeCased(), "snake_cased_string")
Expand All @@ -20,11 +22,13 @@ final class StringCaseTests: XCTestCase {
func testLowerCamelCase() throws {
XCTAssertTrue("lower".isLowerCamelCase)
XCTAssertTrue("lowerCamelCase".isLowerCamelCase)
XCTAssertFalse("LOWER".isLowerCamelCase)
XCTAssertFalse("lowerCamelCase_with_underscore".isLowerCamelCase)
XCTAssertFalse("UpperCamelCase".isLowerCamelCase)
XCTAssertFalse("snake_case".isLowerCamelCase)

XCTAssertEqual("lower".lowerCamelCased(), "lower")
XCTAssertEqual("LOWER".lowerCamelCased(), "lower")
XCTAssertEqual("LowerCamelCased".lowerCamelCased(), "lowerCamelCased")
XCTAssertEqual("lower_camel_cased".lowerCamelCased(), "lowerCamelCased")
XCTAssertEqual("Lower Camel cased".lowerCamelCased(), "lowerCamelCased")
Expand All @@ -35,20 +39,40 @@ final class StringCaseTests: XCTestCase {
XCTAssertTrue("Upper".isUpperCamelCase)
XCTAssertTrue("UpperCamelCase".isUpperCamelCase)
XCTAssertTrue("UpperCamelCaseExample".isUpperCamelCase)
XCTAssertFalse("UPPER".isUpperCamelCase)
XCTAssertFalse("UpperCamelCase_with_underscore".isUpperCamelCase)
XCTAssertFalse("snake_case".isUpperCamelCase)
XCTAssertFalse("lowerCamelCase".isUpperCamelCase)

XCTAssertEqual("Upper".upperCamelCased(), "Upper")
XCTAssertEqual("UPPER".upperCamelCased(), "Upper")
XCTAssertEqual("upperCamelCased".upperCamelCased(), "UpperCamelCased")
XCTAssertEqual("upper_camel_cased".upperCamelCased(), "UpperCamelCased")
XCTAssertEqual("upper_camel Cased".upperCamelCased(), "UpperCamelCased")
XCTAssertEqual("_this is* not-Very%difficult".upperCamelCased(), "ThisIsNotVeryDifficult")
}

func testKebabCase() throws {
XCTAssertTrue("kebab".isKebabCase)
XCTAssertTrue("kebab-case".isKebabCase)
XCTAssertTrue("kebab-case-example".isKebabCase)
XCTAssertFalse("KEBAB".isKebabCase)
XCTAssertFalse("not-a-KEBABCASE-String".isKebabCase)
XCTAssertFalse("notKebabCase".isKebabCase)
XCTAssertFalse("AlsoNotKebabCase".isKebabCase)

XCTAssertEqual("kebab".kebabCased(), "kebab")
XCTAssertEqual("KEBAB".kebabCased(), "kebab")
XCTAssertEqual("kebab cased".kebabCased(), "kebab-cased")
XCTAssertEqual("kebabCased".kebabCased(), "kebab-cased")
XCTAssertEqual("kebab Cased_String".kebabCased(), "kebab-cased-string")
XCTAssertEqual("_this is* not-Very%difficult".kebabCased(), "this-is-not-very-difficult")
}

static var allTests = [
("testSnakeCase", testSnakeCase),
("testLowerCamelCase", testLowerCamelCase),
("testUpperCamelCase", testUpperCamelCase),
("testKebabCase", testKebabCase),
]
}