-
-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathStringHandling.swift
More file actions
146 lines (130 loc) · 6.36 KB
/
Copy pathStringHandling.swift
File metadata and controls
146 lines (130 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
extension StringProtocol where Self: RangeReplaceableCollection, Self.Element: Equatable {
/// Provides a version of `StringProtocol.firstRange(of:)` guaranteed to be available.
@inlinable
func sqlkit_firstRange(of other: some StringProtocol) -> Range<Self.Index>? {
guard self.count >= other.count, let starter = other.first else { return nil }
var index = self.startIndex
let lastIndex = self.index(self.endIndex, offsetBy: -other.count)
while index <= lastIndex, let start = self[index...].firstIndex(of: starter) {
guard let upperIndex = self.index(start, offsetBy: other.count, limitedBy: self.endIndex) else {
return nil
}
if self[start ..< upperIndex] == other {
return start ..< upperIndex
}
index = self.index(after: start)
}
return nil
}
/// Provides a version of `StringProtocol.replacing(_:with:)` which is guaranteed to be available.
#if !DEBUG && !canImport(Darwin)
@inline(__always)
#endif
@inlinable
func sqlkit_replacing(_ search: some StringProtocol, with replacement: some StringProtocol) -> String {
#if DEBUG || canImport(Darwin)
// On Apple platforms, this hand-rolled implementation is MUCH faster (10x or more) than the stdlib version, for some reason.
// We also want to use this implementation in debug builds, so that the tests test it rather than the stdlib.
guard !self.isEmpty, !search.isEmpty, self.count >= search.count else { return .init(self) }
var result = "", prevIndex = self.startIndex
result.reserveCapacity(self.count + replacement.count)
while let range = self[prevIndex...].sqlkit_firstRange(of: search) {
result.append(contentsOf: self[prevIndex ..< range.lowerBound])
result.append(contentsOf: replacement)
prevIndex = range.upperBound
}
result.append(contentsOf: self[prevIndex...])
return result
#else
// On non-Apple platforms, the stdlib's version is better than our hand-rolled one.
return String(self.replacing(search, with: replacement))
#endif
}
/// Returns the string with its first character lowercased.
@inlinable
var decapitalized: String {
self.isEmpty ? "" : "\(self[self.startIndex].lowercased())\(self.dropFirst())"
}
/// Returns the string with its first character uppercased.
@inlinable
var encapitalized: String {
self.isEmpty ? "" : "\(self[self.startIndex].uppercased())\(self.dropFirst())"
}
/// Returns the string with any `snake_case` converted to `camelCase`.
///
/// This is a modified version of Foundation's implementation:
/// https://github.com/apple/swift-foundation/blob/8010dfe6b1c38cdf363c8d3d3b43d7d4f4c9987b/Sources/FoundationEssentials/JSON/JSONDecoder.swift
///
/// > Note: This method is _not_ idempotent with respect to `convertedToSnakeCase` for all inputs.
var convertedFromSnakeCase: String {
guard !self.isEmpty, let firstNonUnderscore = self.firstIndex(where: { $0 != "_" }) else {
return .init(self)
}
var lastNonUnderscore = self.endIndex
repeat {
self.formIndex(before: &lastNonUnderscore)
} while lastNonUnderscore > firstNonUnderscore && self[lastNonUnderscore] == "_"
let keyRange = self[firstNonUnderscore...lastNonUnderscore]
let leading = self[self.startIndex..<firstNonUnderscore]
let trailing = self[self.index(after: lastNonUnderscore)..<self.endIndex]
let words = keyRange.split(separator: "_")
guard words.count > 1 else {
return "\(leading)\(keyRange)\(trailing)"
}
return "\(leading)\(([words[0].decapitalized] + words[1...].map(\.encapitalized)).joined())\(trailing)"
}
/// Returns the string with any `camelCase` converted to `snake_case`.
///
/// This is a modified version of Foundation's implementation:
/// https://github.com/apple/swift-foundation/blob/8010dfe6b1c38cdf363c8d3d3b43d7d4f4c9987b/Sources/FoundationEssentials/JSON/JSONEncoder.swift
///
/// > Note: This method is _not_ idempotent with respect to `convertedFromSnakeCase` for all inputs.
var convertedToSnakeCase: String {
guard !self.isEmpty else {
return .init(self)
}
var words: [Range<String.Index>] = []
var wordStart = self.startIndex, searchIndex = self.index(after: wordStart)
while let upperCaseIndex = self[searchIndex...].firstIndex(where: \.isUppercase) {
words.append(wordStart..<upperCaseIndex)
wordStart = upperCaseIndex
guard let lowerCaseIndex = self[upperCaseIndex...].firstIndex(where: \.isLowercase) else {
break
}
searchIndex = lowerCaseIndex
if lowerCaseIndex != self.index(after: upperCaseIndex) {
let beforeLowerIndex = self.index(before: lowerCaseIndex)
words.append(upperCaseIndex..<beforeLowerIndex)
wordStart = beforeLowerIndex
}
}
words.append(wordStart..<self.endIndex)
return words.map { self[$0].decapitalized }.joined(separator: "_")
}
/// A necessarily inelegant polyfill for conformance to `CodingKeyRepresentable`, due to availability problems.
@inlinable
var codingKeyValue: any CodingKey {
#if !DEBUG
if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *) {
return String(self).codingKey
}
#endif
return SomeCodingKey(stringValue: .init(self))
}
/// Remove the given optional prefix from the string, if present.
///
/// - Parameter prefix: The prefix to remove, if non-`nil`.
/// - Returns: The string with the prefix removed, if it exists. The string unmodified if not,
/// or if `prefix` is `nil`.
func drop(prefix: (some StringProtocol)?) -> Self.SubSequence {
#if !DEBUG
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
return prefix.map(self.trimmingPrefix(_:)) ?? self[...]
}
#endif
guard let prefix, self.starts(with: prefix) else {
return self[self.startIndex ..< self.endIndex]
}
return self.dropFirst(prefix.count)
}
}