├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftString │ ├── StringExtensions.swift │ ├── StringHTML.swift │ └── StringURL.swift ├── String+Extensions.podspec ├── SwiftString.xcodeproj ├── Configs │ ├── Debug.xcconfig │ ├── Project.xcconfig │ └── Release.xcconfig ├── SwiftStringTests_Info.plist ├── SwiftString_Info.plist ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── SwiftString.xcscheme │ └── xcschememanagement.plist └── Tests ├── LinuxMain.swift └── SwiftStringTests ├── StringURLTests.swift └── SwiftStringTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | .build/ 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | .build/ 40 | 41 | # CocoaPods 42 | # 43 | # We recommend against adding the Pods directory to your .gitignore. However 44 | # you should judge for yourself, the pros and cons are mentioned at: 45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 46 | # 47 | # Pods/ 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | 68 | Packages/ 69 | docs/ 70 | 71 | *.xcodeproj 72 | Package.pins 73 | Package.resolved 74 | .package_lin.pins 75 | .package_lin.resolved 76 | .build_lin 77 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8.1 3 | script: 4 | - swift test 5 | before_install: 6 | - TOOLCHAIN_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-03-24-a 7 | - curl -O https://swift.org/builds/development/xcode/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-osx.pkg 8 | - sudo installer -pkg ${TOOLCHAIN_VERSION}-osx.pkg -target / 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Andrew Mayne 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftString", 7 | products: [ 8 | .library(name: "SwiftString", targets: ["SwiftString"]) 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target( 13 | name: "SwiftString", 14 | dependencies: [] 15 | ), 16 | .testTarget( 17 | name: "SwiftStringTests", 18 | dependencies: [], 19 | path: "./Tests/SwiftStringTests/" 20 | ), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftString 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/String+Extensions.svg?style=flat)](http://cocoapods.org/pods/String+Extensions) 4 | [![License](https://img.shields.io/cocoapods/l/String+Extensions.svg?style=flat)](http://cocoapods.org/pods/String+Extensions) ![Language](https://img.shields.io/badge/language-Swift%203.0-orange.svg) ![Language](https://img.shields.io/badge/language-Swift%204.0-orange.svg) 5 | 6 | 7 | SwiftString is a lightweight string extension for Swift 3 and 4. 8 | This library was motivated by having to search StackOverflow for common string operations, 9 | and wanting them to be in one place with test coverage. 10 | 11 | Note the original client side Swift 2 repo can be found here: 12 | [https://github.com/amayne/SwiftString](https://github.com/amayne/SwiftString) 13 | 14 | ## This Fork 15 | 16 | This fork is intended as a server side utility. 17 | 18 | * It is Swift 3.x, 4.0 and Swift Package Manager (SPM) ready. 19 | * Added sigificant test coverage 20 | 21 | ## Swift 3.0.2, 3.1 only 22 | 23 | If you are targeting Swift 3.0.2 or 3.1 only, please use the majorVersion:1 in your Package.swift file: 24 | 25 | ``` swift 26 | .Package(url: "https://github.com/iamjono/SwiftString.git", majorVersion: 1), 27 | ``` 28 | 29 | This release works for both Swift 3.0.2 and 3.1, on macOS and Linux. 30 | 31 | 32 | ## Swift 3.0.2, 3.1, 3.2 and Swift 4.0 33 | 34 | If you have updated to Swift 4, or plan to in the near future, use the majorVersion:2 in your Package.swift file. This version brings compatibility with Swift 4, and is backwards compatible with the 3.x codebases. 35 | 36 | ``` swift 37 | .Package(url: "https://github.com/iamjono/SwiftString.git", majorVersion: 2), 38 | ``` 39 | 40 | The latest release works for Swift 3.x and Swift 4 on macOS and Linux. 41 | 42 | 43 | ## Usage 44 | 45 | ```swift 46 | import SwiftString 47 | ``` 48 | 49 | ## Methods 50 | 51 | **between(left, right)** 52 | 53 | ``` swift 54 | "foo".between("", "") // "foo" 55 | "foo".between("", "") // "foo" 56 | "foo".between("", "") // nil 57 | "Some strings } are very {weird}, dont you think?".between("{", "}") // "weird" 58 | "".between("", "") // nil 59 | "foo".between("", "") // nil 60 | ``` 61 | 62 | **camelize()** 63 | 64 | ```swift 65 | "os version".camelize() // "osVersion" 66 | "HelloWorld".camelize() // "helloWorld" 67 | "someword With Characters".camelize() // "somewordWithCharacters" 68 | "data_rate".camelize() // "dataRate" 69 | "background-color".camelize() // "backgroundColor" 70 | ``` 71 | 72 | 73 | **capitalize()** 74 | 75 | ```swift 76 | "hello world".capitalize() // "Hello World" 77 | ``` 78 | 79 | **chompLeft(string)** 80 | 81 | ```swift 82 | "foobar".chompLeft("foo") // "bar" 83 | "foobar".chompLeft("bar") // "foo" 84 | ``` 85 | 86 | **chompRight(string)** 87 | 88 | ```swift 89 | "foobar".chompRight("bar") // "foo" 90 | "foobar".chompRight("foo") // "bar" 91 | ``` 92 | 93 | **cleanPath()** 94 | 95 | ```swift 96 | var foo = "hello//world/..///stuff.txt" 97 | foo.cleanPath() // foo == "hello/stuff.txt" 98 | ``` 99 | 100 | **cleanedPath()** 101 | 102 | ```swift 103 | "hello//world/..///stuff.txt".cleanedPath() // "hello/stuff.txt" 104 | ``` 105 | 106 | **collapseWhitespace()** 107 | 108 | ```swift 109 | " String \t libraries are \n\n\t fun\n! ".collapseWhitespace() // "String libraries are fun !") 110 | ``` 111 | 112 | **count(string)** 113 | 114 | ```swift 115 | "hi hi ho hey hihey".count("hi") // 3 116 | ``` 117 | 118 | **decodeHTML()** 119 | 120 | ```swift 121 | "The Weekend ‘King Of The Fall’".decodeHTML() // "The Weekend ‘King Of The Fall’" 122 | " 4 < 5 & 3 > 2 . Price: 12 €. @ ".decodeHTML() // " 4 < 5 & 3 > 2 . Price: 12 €. @ " 123 | "this is so "good"".decodeHTML() // "this is so \"good\"" 124 | ``` 125 | 126 | **endsWith(suffix)** 127 | 128 | ```swift 129 | "hello world".endsWith("world") // true 130 | "hello world".endsWith("foo") // false 131 | ``` 132 | 133 | **ensureLeft(prefix)** 134 | 135 | ```swift 136 | "/subdir".ensureLeft("/") // "/subdir" 137 | "subdir".ensureLeft("/") // "/subdir" 138 | ``` 139 | 140 | **ensureRight(suffix)** 141 | 142 | ```swift 143 | "subdir/".ensureRight("/") // "subdir/" 144 | "subdir".ensureRight("/") // "subdir/" 145 | ``` 146 | 147 | **extension** 148 | 149 | ```swift 150 | "/hello/world.txt".extension // "txt" 151 | "/hello/world.tmp.txt".extension // "txt" 152 | ``` 153 | 154 | **file** 155 | 156 | ```swift 157 | "/hello/world.txt".file // "world.txt" 158 | "/hello/there/".file // "there" 159 | ``` 160 | 161 | **fileName** 162 | 163 | ```swift 164 | "/hello/world.txt".fileName // "world" 165 | "/hello/there/".fileName // "there" 166 | ``` 167 | 168 | **index(of: substring)** 169 | 170 | ```swift 171 | "hello".index(of: "hell"), // 0 172 | "hello".index(of: "lo"), // 3 173 | "hello".index(of: "world") // -1 174 | "hellohello".index(of: "hel", after: 2) // 5 175 | ``` 176 | 177 | **indexOf(substring)** - deprecated in favor of `index(of:)` above 178 | 179 | ```swift 180 | "hello".indexOf("hell"), // 0 181 | "hello".indexOf("lo"), // 3 182 | "hello".indexOf("world") // nil 183 | ``` 184 | 185 | **initials()** 186 | 187 | ```swift 188 | "First".initials(), // "F" 189 | "First Last".initials(), // "FL" 190 | "First Middle1 Middle2 Middle3 Last".initials() // "FMMML" 191 | ``` 192 | 193 | **initialsFirstAndLast()** 194 | 195 | ```swift 196 | "First Last".initialsFirstAndLast(), // "FL" 197 | "First Middle1 Middle2 Middle3 Last".initialsFirstAndLast() // "FL" 198 | ``` 199 | 200 | **isAlpha()** 201 | 202 | ```swift 203 | "fdafaf3".isAlpha() // false 204 | "afaf".isAlpha() // true 205 | "dfdf--dfd".isAlpha() // false 206 | ``` 207 | 208 | **isAlphaNumeric()** 209 | 210 | ```swift 211 | "afaf35353afaf".isAlphaNumeric() // true 212 | "FFFF99fff".isAlphaNumeric() // true 213 | "99".isAlphaNumeric() // true 214 | "afff".isAlphaNumeric() // true 215 | "-33".isAlphaNumeric() // false 216 | "aaff..".isAlphaNumeric() // false 217 | ``` 218 | 219 | **isEmpty()** 220 | 221 | ```swift 222 | " ".isEmpty() // true 223 | "\t\t\t ".isEmpty() // true 224 | "\n\n".isEmpty() // true 225 | "helo".isEmpty() // false 226 | ``` 227 | 228 | **isNumeric()** 229 | 230 | ```swift 231 | "abc".isNumeric() // false 232 | "123a".isNumeric() // false 233 | "1".isNumeric() // true 234 | "22".isNumeric() // true 235 | "33.0".isNumeric() // true 236 | "-63.0".isNumeric() // true 237 | ``` 238 | 239 | **join(paths...)** 240 | 241 | ```swift 242 | var root = "/foo" 243 | root.join("bar", "/baz", "..", "//somedata.txt") // root == "/foo/bar/somedata.txt" 244 | 245 | var root = "/foo" 246 | root.join(paths: ["bar", "/baz", "..", "//somedata.txt"]) // root == "/foo/bar/somedata.txt" 247 | ``` 248 | 249 | **joining(paths...)** 250 | 251 | ```swift 252 | "/foo".joining("bar", "/baz", "..", "//somedata.txt") // "/foo/bar/somedata.txt" 253 | "/foo".joining(paths: ["bar", "/baz", "..", "//somedata.txt"]) // "/foo/bar/somedata.txt" 254 | ``` 255 | 256 | **lastIndex(of: substring)** 257 | 258 | ```swift 259 | "hellohellohello".lastIndex(of: "hell"), // 10 260 | "hellohellohello".lastIndex(of: "lo"), // 13 261 | "hellohellohello".lastIndex(of: "world") // -1 262 | "hellohellohello".lastIndex(of: "hel", before: 10) // 5 263 | ``` 264 | 265 | **latinize()** 266 | 267 | ```swift 268 | "šÜįéïöç".latinize() // "sUieioc" 269 | "crème brûlée".latinize() // "creme brulee" 270 | ``` 271 | 272 | **lines()** 273 | 274 | ```swift 275 | "test".lines() // ["test"] 276 | "test\nsentence".lines() // ["test", "sentence"] 277 | "test \nsentence".lines() // ["test ", "sentence"] 278 | ``` 279 | 280 | **pad(n, string)** 281 | 282 | ```swift 283 | "hello".pad(2) // " hello " 284 | "hello".pad(1, "\t") // "\thello\t" 285 | ``` 286 | 287 | **padLeft(n, string)** 288 | 289 | ```swift 290 | "hello".padLeft(10) // " hello" 291 | "what?".padLeft(2, "!") // "!!what?" 292 | ``` 293 | 294 | **padRight(n, string)** 295 | 296 | ```swift 297 | "hello".padRight(10) // "hello " 298 | "hello".padRight(2, "!") // "hello!!" 299 | ``` 300 | 301 | **parent** 302 | 303 | ```swift 304 | "/hello/there/world.txt".parent // "/hello/there" 305 | "/hello/there".parent // "/hello" 306 | ``` 307 | 308 | **startsWith(prefix)** 309 | 310 | ```swift 311 | "hello world".startsWith("hello") // true 312 | "hello world".startsWith("foo") // false 313 | ``` 314 | 315 | **split(separator)** 316 | 317 | ```swift 318 | "hello world".split(" ")[0] // "hello" 319 | "hello world".split(" ")[1] // "world" 320 | "helloworld".split(" ")[0] // "helloworld" 321 | ``` 322 | 323 | **times(n)** 324 | 325 | ```swift 326 | "hi".times(3) // "hihihi" 327 | " ".times(10) // " " 328 | ``` 329 | 330 | **toBool()** 331 | 332 | ```swift 333 | "asdwads".toBool() // nil 334 | "true".toBool() // true 335 | "false".toBool() // false 336 | ``` 337 | 338 | **toFloat()** 339 | 340 | ```swift 341 | "asdwads".toFloat() // nil 342 | "2.00".toFloat() // 2.0 343 | "2".toFloat() // 2.0 344 | ``` 345 | 346 | **toInt()** 347 | 348 | ```swift 349 | "asdwads".toInt() // nil 350 | "2.00".toInt() // 2 351 | "2".toInt() // 2 352 | ``` 353 | 354 | **toDate()** 355 | 356 | ```swift 357 | "asdwads".toDate() // nil 358 | "2014-06-03".toDate() // NSDate 359 | ``` 360 | 361 | **toDateTime()** 362 | 363 | ```swift 364 | "asdwads".toDateTime() // nil 365 | "2014-06-03 13:15:01".toDateTime() // NSDate 366 | ``` 367 | 368 | **toDouble()** 369 | 370 | ```swift 371 | "asdwads".toDouble() // nil 372 | "2.00".toDouble() // 2.0 373 | "2".toDouble() // 2.0 374 | ``` 375 | 376 | **trimmedLeft()** 377 | 378 | ```swift 379 | " How are you? ".trimmedLeft() // "How are you? " 380 | ``` 381 | 382 | **trimmedRight()** 383 | 384 | ```swift 385 | " How are you? ".trimmedRight() // " How are you?" 386 | ``` 387 | 388 | **trimmed()** 389 | 390 | ```swift 391 | " How are you? ".trimmed() // "How are you?" 392 | ``` 393 | 394 | **slugify()** 395 | 396 | ```swift 397 | "Global Thermonuclear Warfare".slugify() // "global-thermonuclear-warfare" 398 | "Crème brûlée".slugify() // "creme-brulee" 399 | ``` 400 | 401 | **stripPunctuation()** 402 | 403 | ```swift 404 | "My, st[ring] *full* of %punct)".stripPunctuation() // "My string full of punct" 405 | ``` 406 | 407 | **substring(startIndex, length)** 408 | 409 | ```swift 410 | "hello world".substring(0, length: 1) // "h" 411 | "hello world".substring(0, length: 11) // "hello world" 412 | ``` 413 | 414 | **[subscript]** 415 | 416 | ```swift 417 | "hello world"[0...1] // "he" 418 | "hello world"[0..<1] // "h" 419 | "hello world"[0] // "h" 420 | "hello world"[0...10] // "hello world" 421 | "hello world"[safe: -1...1] // "he" 422 | "hello world"[safe: 9...20] // "ld" 423 | ``` 424 | 425 | ## Requirements 426 | 427 | - Swift version 3.x or Swift 4. Please see notes above. 428 | 429 | ## Installation 430 | 431 | ### Install via Swift Package Manager 432 | 433 | * Add the following to your `Package.swift` file: 434 | 435 | ``` swift 436 | .Package( 437 | url: "https://github.com/iamjono/SwiftString.git", 438 | majorVersion: 2 439 | ), 440 | ``` 441 | 442 | Then, regenerate your Xcode project: 443 | 444 | ``` 445 | swift package generate-xcodeproj 446 | ``` 447 | 448 | ### Install via Cocoapods 449 | 450 | ```ruby 451 | pod "String+Extensions" 452 | ``` 453 | 454 | ## Author 455 | 456 | Andrew Mayne, andrew@redbricklab.com 457 | 458 | Swift 3 & 4 SPM module, Jonathan Guthrie, jono@guthrie.net.nz 459 | 460 | Cocoapods, Koji Murata, malt.koji@gmail.com 461 | 462 | ## License 463 | 464 | SwiftString is available under the MIT license. See the LICENSE file for more info. 465 | -------------------------------------------------------------------------------- /Sources/SwiftString/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftString.swift 3 | // SwiftString 4 | // 5 | // Created by Andrew Mayne on 30/01/2016. 6 | // Copyright © 2016 Red Brick Labs. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | public extension String { 11 | 12 | /// Finds the string between two bookend strings if it can be found. 13 | /// 14 | /// - parameter left: The left bookend 15 | /// - parameter right: The right bookend 16 | /// 17 | /// - returns: The string between the two bookends, or nil if the bookends cannot be found, the bookends are the same or appear contiguously. 18 | func between(_ left: String, _ right: String) -> String? { 19 | guard 20 | let leftRange = range(of:left), let rightRange = range(of: right, options: .backwards), 21 | left != right && leftRange.upperBound != rightRange.lowerBound 22 | else { return nil } 23 | 24 | return String(self[leftRange.upperBound...index(before: rightRange.lowerBound)]) 25 | 26 | } 27 | 28 | // https://gist.github.com/stevenschobert/540dd33e828461916c11 29 | func camelize() -> String { 30 | let source = clean(with: " ", allOf: "-", "_") 31 | if source.contains(" ") { 32 | let first = self[self.startIndex...self.index(after: startIndex)] //source.substringToIndex(source.index(after: startIndex)) 33 | let cammel = source.capitalized.replacingOccurrences(of: " ", with: "") 34 | // let cammel = String(format: "%@", strip) 35 | let rest = String(cammel.dropFirst()) 36 | return "\(first)\(rest)" 37 | } else { 38 | let first = source[self.startIndex...self.index(after: startIndex)].lowercased() 39 | let rest = String(source.dropFirst()) 40 | return "\(first)\(rest)" 41 | } 42 | } 43 | 44 | func capitalize() -> String { 45 | return capitalized 46 | } 47 | 48 | // func contains(_ substring: String) -> Bool { 49 | // return range(of: substring) != nil 50 | // } 51 | 52 | func chompLeft(_ prefix: String) -> String { 53 | if let prefixRange = range(of: prefix) { 54 | if prefixRange.upperBound >= endIndex { 55 | return String(self[startIndex.. String { 64 | if let suffixRange = range(of: suffix, options: .backwards) { 65 | if suffixRange.upperBound >= endIndex { 66 | return String(self[startIndex.. String { 75 | let thecomponents = components(separatedBy: NSCharacterSet.whitespacesAndNewlines).filter { !$0.isEmpty } 76 | return thecomponents.joined(separator: " ") 77 | } 78 | 79 | func clean(with: String, allOf: String...) -> String { 80 | var string = self 81 | for target in allOf { 82 | string = string.replacingOccurrences(of: target, with: with) 83 | } 84 | return string 85 | } 86 | 87 | func count(_ substring: String) -> Int { 88 | return components(separatedBy: substring).count-1 89 | } 90 | 91 | func endsWith(_ suffix: String) -> Bool { 92 | return hasSuffix(suffix) 93 | } 94 | 95 | func ensureLeft(_ prefix: String) -> String { 96 | if startsWith(prefix) { 97 | return self 98 | } else { 99 | return "\(prefix)\(self)" 100 | } 101 | } 102 | 103 | func ensureRight(_ suffix: String) -> String { 104 | if endsWith(suffix) { 105 | return self 106 | } else { 107 | return "\(self)\(suffix)" 108 | } 109 | } 110 | 111 | @available(*, deprecated, message: "Use `index(of:)` instead") 112 | func indexOf(_ substring: String) -> Int? { 113 | if let range = range(of: substring) { 114 | return self.distance(from: startIndex, to: range.lowerBound) 115 | // return startIndex.distanceTo(range.lowerBound) 116 | } 117 | return nil 118 | } 119 | 120 | func initials() -> String { 121 | let words = self.components(separatedBy: " ") 122 | return words.reduce(""){$0 + $1[startIndex...startIndex]} 123 | // return words.reduce(""){$0 + $1[0...0]} 124 | } 125 | 126 | func initialsFirstAndLast() -> String { 127 | let words = self.components(separatedBy: " ") 128 | return words.reduce("") { ($0 == "" ? "" : String($0[startIndex...startIndex])) + $1[startIndex...startIndex]} 129 | } 130 | 131 | func isAlpha() -> Bool { 132 | for chr in self { 133 | if (!(chr >= "a" && chr <= "z") && !(chr >= "A" && chr <= "Z") ) { 134 | return false 135 | } 136 | } 137 | return true 138 | } 139 | 140 | func isAlphaNumeric() -> Bool { 141 | let alphaNumeric = NSCharacterSet.alphanumerics 142 | let output = self.unicodeScalars.split { !alphaNumeric.contains($0)}.map(String.init) 143 | if output.count == 1 { 144 | if output[0] != self { 145 | return false 146 | } 147 | } 148 | return output.count == 1 149 | // return componentsSeparatedByCharactersInSet(alphaNumeric).joinWithSeparator("").length == 0 150 | } 151 | 152 | func isEmpty() -> Bool { 153 | return self.trimmingCharacters(in: .whitespacesAndNewlines).length == 0 154 | } 155 | 156 | func isNumeric() -> Bool { 157 | if let _ = defaultNumberFormatter().number(from: self) { 158 | return true 159 | } 160 | return false 161 | } 162 | 163 | private func join(_ elements: S) -> String { 164 | return elements.map{String(describing: $0)}.joined(separator: self) 165 | } 166 | 167 | func latinize() -> String { 168 | return self.folding(options: .diacriticInsensitive, locale: .current) 169 | // stringByFoldingWithOptions(.DiacriticInsensitiveSearch, locale: NSLocale.currentLocale()) 170 | } 171 | 172 | func lines() -> [String] { 173 | return self.components(separatedBy: NSCharacterSet.newlines) 174 | } 175 | 176 | var length: Int { 177 | get { 178 | return self.count 179 | } 180 | } 181 | 182 | func pad(_ n: Int, _ string: String = " ") -> String { 183 | return "".join([string.times(n), self, string.times(n)]) 184 | } 185 | 186 | func padLeft(_ n: Int, _ string: String = " ") -> String { 187 | return "".join([string.times(n), self]) 188 | } 189 | 190 | func padRight(_ n: Int, _ string: String = " ") -> String { 191 | return "".join([self, string.times(n)]) 192 | } 193 | 194 | func slugify(withSeparator separator: Character = "-") -> String { 195 | let slugCharacterSet = NSCharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\(separator)") 196 | return latinize() 197 | .lowercased() 198 | .components(separatedBy: slugCharacterSet.inverted) 199 | .filter { $0 != "" } 200 | .joined(separator: String(separator)) 201 | } 202 | 203 | /// split the string into a string array by white spaces 204 | func tokenize() -> [String] { 205 | return self.components(separatedBy: .whitespaces) 206 | } 207 | 208 | func split(_ separator: Character = " ") -> [String] { 209 | return self.split{$0 == separator}.map(String.init) 210 | } 211 | 212 | func startsWith(_ prefix: String) -> Bool { 213 | return hasPrefix(prefix) 214 | } 215 | 216 | func stripPunctuation() -> String { 217 | return components(separatedBy: .punctuationCharacters) 218 | .joined(separator: "") 219 | .components(separatedBy: " ") 220 | .filter { $0 != "" } 221 | .joined(separator: " ") 222 | } 223 | 224 | func times(_ n: Int) -> String { 225 | var x = self 226 | for _ in 1.. Float? { 234 | if let number = defaultNumberFormatter().number(from: self) { 235 | return number.floatValue 236 | } 237 | return nil 238 | } 239 | 240 | func toInt() -> Int? { 241 | if let number = defaultNumberFormatter().number(from: self) { 242 | return number.intValue 243 | } 244 | return nil 245 | } 246 | 247 | func toBool() -> Bool? { 248 | let trimmed = self.trimmed().lowercased() 249 | if Int(trimmed) != 0 { 250 | return true 251 | } 252 | switch trimmed { 253 | case "true", "yes", "1": 254 | return true 255 | case "false", "no", "0": 256 | return false 257 | default: 258 | return false 259 | } 260 | } 261 | 262 | func toDate(_ format: String = "yyyy-MM-dd") -> Date? { 263 | return dateFormatter(format).date(from: self) as Date? 264 | } 265 | 266 | func toDateTime(_ format: String = "yyyy-MM-dd HH:mm:ss") -> Date? { 267 | return toDate(format) 268 | } 269 | 270 | func trimmedLeft() -> String { 271 | if let range = rangeOfCharacter(from: NSCharacterSet.whitespacesAndNewlines.inverted) { 272 | return String(self[range.lowerBound.. String { 278 | if let range = rangeOfCharacter(from: NSCharacterSet.whitespacesAndNewlines.inverted, options: NSString.CompareOptions.backwards) { 279 | return String(self[startIndex.. String { 285 | return self.trimmingCharacters(in: .whitespacesAndNewlines) 286 | } 287 | 288 | subscript(_ r: CountableRange) -> String { 289 | get { 290 | let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) 291 | let endIndex = self.index(self.startIndex, offsetBy: r.upperBound) 292 | return String(self[startIndex..) -> String { 297 | get { 298 | return self[range.lowerBound..) -> String { 303 | get { 304 | if length == 0 { return "" } 305 | let lower = range.lowerBound < 0 ? 0 : range.lowerBound 306 | let upper = range.upperBound < 0 ? 0 : range.upperBound 307 | let s = index(startIndex, offsetBy: lower, limitedBy: endIndex) ?? endIndex 308 | let e = index(startIndex, offsetBy: upper, limitedBy: endIndex) ?? endIndex 309 | return String(self[s..) -> String { 314 | get { 315 | if length == 0 { return "" } 316 | let closedEndIndex = index(endIndex, offsetBy: -1, limitedBy: startIndex) ?? startIndex 317 | let lower = range.lowerBound < 0 ? 0 : range.lowerBound 318 | let upper = range.upperBound < 0 ? 0 : range.upperBound 319 | let s = index(startIndex, offsetBy: lower, limitedBy: closedEndIndex) ?? closedEndIndex 320 | let e = index(startIndex, offsetBy: upper, limitedBy: closedEndIndex) ?? closedEndIndex 321 | return String(self[s...e]) 322 | } 323 | } 324 | 325 | func substring(_ startIndex: Int, length: Int) -> String { 326 | let start = self.index(self.startIndex, offsetBy: startIndex) 327 | let end = self.index(self.startIndex, offsetBy: startIndex + length) 328 | return String(self[start.. Character { 332 | get { 333 | let index = self.index(self.startIndex, offsetBy: i) 334 | return self[index] 335 | } 336 | } 337 | 338 | // /// get the left part of the string before the index 339 | // func left(_ range:Range?) -> String { 340 | // return self.substring(to: (range?.lowerBound)!) 341 | // } 342 | // /// get the right part of the string after the index 343 | // func right(_ range:Range?) -> String { 344 | // return self.substring(from: self.index((range?.lowerBound)!, offsetBy:1)) 345 | // } 346 | 347 | /// The first index of the given string 348 | func indexRaw(of str: String, after: Int = 0, options: String.CompareOptions = .literal, locale: Locale? = nil) -> String.Index? { 349 | guard str.length > 0 else { 350 | // Can't look for nothing 351 | return nil 352 | } 353 | guard (str.length + after) <= self.length else { 354 | // Make sure the string you're searching for will actually fit 355 | return nil 356 | } 357 | 358 | let startRange = self.index(self.startIndex, offsetBy: after).. Int { 363 | guard let index = indexRaw(of: str, after: after, options: options, locale: locale) else { 364 | return -1 365 | } 366 | return self.distance(from: self.startIndex, to: index) 367 | } 368 | 369 | /// The last index of the given string 370 | func lastIndexRaw(of str: String, before: Int = 0, options: String.CompareOptions = .literal, locale: Locale? = nil) -> String.Index? { 371 | guard str.length > 0 else { 372 | // Can't look for nothing 373 | return nil 374 | } 375 | guard (str.length + before) <= self.length else { 376 | // Make sure the string you're searching for will actually fit 377 | return nil 378 | } 379 | 380 | let startRange = self.startIndex.. Int { 385 | guard let index = lastIndexRaw(of: str, before: before, options: options, locale: locale) else { 386 | return -1 387 | } 388 | return self.distance(from: self.startIndex, to: index) 389 | } 390 | 391 | } 392 | 393 | private enum ThreadLocalIdentifier { 394 | case dateFormatter(String) 395 | 396 | case defaultNumberFormatter 397 | case localeNumberFormatter(Locale) 398 | 399 | var objcDictKey: String { 400 | switch self { 401 | case .dateFormatter(let format): 402 | return "SS\(self)\(format)" 403 | case .localeNumberFormatter(let l): 404 | return "SS\(self)\(l.identifier)" 405 | default: 406 | return "SS\(self)" 407 | } 408 | } 409 | } 410 | 411 | private func threadLocalInstance(_ identifier: ThreadLocalIdentifier, initialValue: @autoclosure () -> T) -> T { 412 | #if os(Linux) 413 | var storage = Thread.current.threadDictionary 414 | #else 415 | let storage = Thread.current.threadDictionary 416 | #endif 417 | let k = identifier.objcDictKey 418 | 419 | let instance: T = storage[k] as? T ?? initialValue() 420 | if storage[k] == nil { 421 | storage[k] = instance 422 | } 423 | 424 | return instance 425 | } 426 | 427 | private func dateFormatter(_ format: String) -> DateFormatter { 428 | return threadLocalInstance(.dateFormatter(format), initialValue: { 429 | let df = DateFormatter() 430 | df.dateFormat = format 431 | return df 432 | }()) 433 | } 434 | 435 | private func defaultNumberFormatter() -> NumberFormatter { 436 | return threadLocalInstance(.defaultNumberFormatter, initialValue: NumberFormatter()) 437 | } 438 | 439 | private func localeNumberFormatter(_ locale: Locale) -> NumberFormatter { 440 | return threadLocalInstance(.localeNumberFormatter(locale), initialValue: { 441 | let nf = NumberFormatter() 442 | nf.locale = locale 443 | return nf 444 | }()) 445 | } 446 | 447 | /// Add the `inserting` and `removing` functions 448 | private extension OptionSet where Element == Self { 449 | /// Duplicate the set and insert the given option 450 | func inserting(_ newMember: Self) -> Self { 451 | var opts = self 452 | opts.insert(newMember) 453 | return opts 454 | } 455 | 456 | /// Duplicate the set and remove the given option 457 | func removing(_ member: Self) -> Self { 458 | var opts = self 459 | opts.remove(member) 460 | return opts 461 | } 462 | } 463 | 464 | public extension String { 465 | 466 | func isValidEmail() -> Bool { 467 | #if os(Linux) && !swift(>=3.1) 468 | let regex = try? RegularExpression(pattern: "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", options: .caseInsensitive) 469 | #else 470 | let regex = try? NSRegularExpression(pattern: "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", options: .caseInsensitive) 471 | #endif 472 | return regex?.firstMatch(in: self, options: [], range: NSMakeRange(0, self.count)) != nil 473 | } 474 | 475 | 476 | /// Encode a String to Base64 477 | func toBase64() -> String { 478 | return Data(self.utf8).base64EncodedString() 479 | } 480 | 481 | /// Decode a String from Base64. Returns nil if unsuccessful. 482 | func fromBase64() -> String? { 483 | guard let data = Data(base64Encoded: self) else { return nil } 484 | return String(data: data, encoding: .utf8) 485 | } 486 | } 487 | 488 | -------------------------------------------------------------------------------- /Sources/SwiftString/StringHTML.swift: -------------------------------------------------------------------------------- 1 | // adapted from https://gist.github.com/mwaterfall/25b4a6a06dc3309d9555 2 | 3 | import Foundation 4 | 5 | public extension String { 6 | 7 | fileprivate struct HTMLEntities { 8 | static let characterEntities : [String: Character] = [ 9 | 10 | // XML predefined entities: 11 | """ : "\"", 12 | "&" : "&", 13 | "'" : "'", 14 | "<" : "<", 15 | ">" : ">", 16 | 17 | // HTML character entity references: 18 | " " : "\u{00A0}", 19 | "¡" : "\u{00A1}", 20 | "¢" : "\u{00A2}", 21 | "£" : "\u{00A3}", 22 | "¤" : "\u{00A4}", 23 | "¥" : "\u{00A5}", 24 | "¦" : "\u{00A6}", 25 | "§" : "\u{00A7}", 26 | "¨" : "\u{00A8}", 27 | "©" : "\u{00A9}", 28 | "ª" : "\u{00AA}", 29 | "«" : "\u{00AB}", 30 | "¬" : "\u{00AC}", 31 | "­" : "\u{00AD}", 32 | "®" : "\u{00AE}", 33 | "¯" : "\u{00AF}", 34 | "°" : "\u{00B0}", 35 | "±" : "\u{00B1}", 36 | "²" : "\u{00B2}", 37 | "³" : "\u{00B3}", 38 | "´" : "\u{00B4}", 39 | "µ" : "\u{00B5}", 40 | "¶" : "\u{00B6}", 41 | "·" : "\u{00B7}", 42 | "¸" : "\u{00B8}", 43 | "¹" : "\u{00B9}", 44 | "º" : "\u{00BA}", 45 | "»" : "\u{00BB}", 46 | "¼" : "\u{00BC}", 47 | "½" : "\u{00BD}", 48 | "¾" : "\u{00BE}", 49 | "¿" : "\u{00BF}", 50 | "À" : "\u{00C0}", 51 | "Á" : "\u{00C1}", 52 | "Â" : "\u{00C2}", 53 | "Ã" : "\u{00C3}", 54 | "Ä" : "\u{00C4}", 55 | "Å" : "\u{00C5}", 56 | "Æ" : "\u{00C6}", 57 | "Ç" : "\u{00C7}", 58 | "È" : "\u{00C8}", 59 | "É" : "\u{00C9}", 60 | "Ê" : "\u{00CA}", 61 | "Ë" : "\u{00CB}", 62 | "Ì" : "\u{00CC}", 63 | "Í" : "\u{00CD}", 64 | "Î" : "\u{00CE}", 65 | "Ï" : "\u{00CF}", 66 | "Ð" : "\u{00D0}", 67 | "Ñ" : "\u{00D1}", 68 | "Ò" : "\u{00D2}", 69 | "Ó" : "\u{00D3}", 70 | "Ô" : "\u{00D4}", 71 | "Õ" : "\u{00D5}", 72 | "Ö" : "\u{00D6}", 73 | "×" : "\u{00D7}", 74 | "Ø" : "\u{00D8}", 75 | "Ù" : "\u{00D9}", 76 | "Ú" : "\u{00DA}", 77 | "Û" : "\u{00DB}", 78 | "Ü" : "\u{00DC}", 79 | "Ý" : "\u{00DD}", 80 | "Þ" : "\u{00DE}", 81 | "ß" : "\u{00DF}", 82 | "à" : "\u{00E0}", 83 | "á" : "\u{00E1}", 84 | "â" : "\u{00E2}", 85 | "ã" : "\u{00E3}", 86 | "ä" : "\u{00E4}", 87 | "å" : "\u{00E5}", 88 | "æ" : "\u{00E6}", 89 | "ç" : "\u{00E7}", 90 | "è" : "\u{00E8}", 91 | "é" : "\u{00E9}", 92 | "ê" : "\u{00EA}", 93 | "ë" : "\u{00EB}", 94 | "ì" : "\u{00EC}", 95 | "í" : "\u{00ED}", 96 | "î" : "\u{00EE}", 97 | "ï" : "\u{00EF}", 98 | "ð" : "\u{00F0}", 99 | "ñ" : "\u{00F1}", 100 | "ò" : "\u{00F2}", 101 | "ó" : "\u{00F3}", 102 | "ô" : "\u{00F4}", 103 | "õ" : "\u{00F5}", 104 | "ö" : "\u{00F6}", 105 | "÷" : "\u{00F7}", 106 | "ø" : "\u{00F8}", 107 | "ù" : "\u{00F9}", 108 | "ú" : "\u{00FA}", 109 | "û" : "\u{00FB}", 110 | "ü" : "\u{00FC}", 111 | "ý" : "\u{00FD}", 112 | "þ" : "\u{00FE}", 113 | "ÿ" : "\u{00FF}", 114 | "Œ" : "\u{0152}", 115 | "œ" : "\u{0153}", 116 | "Š" : "\u{0160}", 117 | "š" : "\u{0161}", 118 | "Ÿ" : "\u{0178}", 119 | "ƒ" : "\u{0192}", 120 | "ˆ" : "\u{02C6}", 121 | "˜" : "\u{02DC}", 122 | "Α" : "\u{0391}", 123 | "Β" : "\u{0392}", 124 | "Γ" : "\u{0393}", 125 | "Δ" : "\u{0394}", 126 | "Ε" : "\u{0395}", 127 | "Ζ" : "\u{0396}", 128 | "Η" : "\u{0397}", 129 | "Θ" : "\u{0398}", 130 | "Ι" : "\u{0399}", 131 | "Κ" : "\u{039A}", 132 | "Λ" : "\u{039B}", 133 | "Μ" : "\u{039C}", 134 | "Ν" : "\u{039D}", 135 | "Ξ" : "\u{039E}", 136 | "Ο" : "\u{039F}", 137 | "Π" : "\u{03A0}", 138 | "Ρ" : "\u{03A1}", 139 | "Σ" : "\u{03A3}", 140 | "Τ" : "\u{03A4}", 141 | "Υ" : "\u{03A5}", 142 | "Φ" : "\u{03A6}", 143 | "Χ" : "\u{03A7}", 144 | "Ψ" : "\u{03A8}", 145 | "Ω" : "\u{03A9}", 146 | "α" : "\u{03B1}", 147 | "β" : "\u{03B2}", 148 | "γ" : "\u{03B3}", 149 | "δ" : "\u{03B4}", 150 | "ε" : "\u{03B5}", 151 | "ζ" : "\u{03B6}", 152 | "η" : "\u{03B7}", 153 | "θ" : "\u{03B8}", 154 | "ι" : "\u{03B9}", 155 | "κ" : "\u{03BA}", 156 | "λ" : "\u{03BB}", 157 | "μ" : "\u{03BC}", 158 | "ν" : "\u{03BD}", 159 | "ξ" : "\u{03BE}", 160 | "ο" : "\u{03BF}", 161 | "π" : "\u{03C0}", 162 | "ρ" : "\u{03C1}", 163 | "ς" : "\u{03C2}", 164 | "σ" : "\u{03C3}", 165 | "τ" : "\u{03C4}", 166 | "υ" : "\u{03C5}", 167 | "φ" : "\u{03C6}", 168 | "χ" : "\u{03C7}", 169 | "ψ" : "\u{03C8}", 170 | "ω" : "\u{03C9}", 171 | "ϑ" : "\u{03D1}", 172 | "ϒ" : "\u{03D2}", 173 | "ϖ" : "\u{03D6}", 174 | " " : "\u{2002}", 175 | " " : "\u{2003}", 176 | " " : "\u{2009}", 177 | "‌" : "\u{200C}", 178 | "‍" : "\u{200D}", 179 | "‎" : "\u{200E}", 180 | "‏" : "\u{200F}", 181 | "–" : "\u{2013}", 182 | "—" : "\u{2014}", 183 | "‘" : "\u{2018}", 184 | "’" : "\u{2019}", 185 | "‚" : "\u{201A}", 186 | "“" : "\u{201C}", 187 | "”" : "\u{201D}", 188 | "„" : "\u{201E}", 189 | "†" : "\u{2020}", 190 | "‡" : "\u{2021}", 191 | "•" : "\u{2022}", 192 | "…" : "\u{2026}", 193 | "‰" : "\u{2030}", 194 | "′" : "\u{2032}", 195 | "″" : "\u{2033}", 196 | "‹" : "\u{2039}", 197 | "›" : "\u{203A}", 198 | "‾" : "\u{203E}", 199 | "⁄" : "\u{2044}", 200 | "€" : "\u{20AC}", 201 | "ℑ" : "\u{2111}", 202 | "℘" : "\u{2118}", 203 | "ℜ" : "\u{211C}", 204 | "™" : "\u{2122}", 205 | "ℵ" : "\u{2135}", 206 | "←" : "\u{2190}", 207 | "↑" : "\u{2191}", 208 | "→" : "\u{2192}", 209 | "↓" : "\u{2193}", 210 | "↔" : "\u{2194}", 211 | "↵" : "\u{21B5}", 212 | "⇐" : "\u{21D0}", 213 | "⇑" : "\u{21D1}", 214 | "⇒" : "\u{21D2}", 215 | "⇓" : "\u{21D3}", 216 | "⇔" : "\u{21D4}", 217 | "∀" : "\u{2200}", 218 | "∂" : "\u{2202}", 219 | "∃" : "\u{2203}", 220 | "∅" : "\u{2205}", 221 | "∇" : "\u{2207}", 222 | "∈" : "\u{2208}", 223 | "∉" : "\u{2209}", 224 | "∋" : "\u{220B}", 225 | "∏" : "\u{220F}", 226 | "∑" : "\u{2211}", 227 | "−" : "\u{2212}", 228 | "∗" : "\u{2217}", 229 | "√" : "\u{221A}", 230 | "∝" : "\u{221D}", 231 | "∞" : "\u{221E}", 232 | "∠" : "\u{2220}", 233 | "∧" : "\u{2227}", 234 | "∨" : "\u{2228}", 235 | "∩" : "\u{2229}", 236 | "∪" : "\u{222A}", 237 | "∫" : "\u{222B}", 238 | "∴" : "\u{2234}", 239 | "∼" : "\u{223C}", 240 | "≅" : "\u{2245}", 241 | "≈" : "\u{2248}", 242 | "≠" : "\u{2260}", 243 | "≡" : "\u{2261}", 244 | "≤" : "\u{2264}", 245 | "≥" : "\u{2265}", 246 | "⊂" : "\u{2282}", 247 | "⊃" : "\u{2283}", 248 | "⊄" : "\u{2284}", 249 | "⊆" : "\u{2286}", 250 | "⊇" : "\u{2287}", 251 | "⊕" : "\u{2295}", 252 | "⊗" : "\u{2297}", 253 | "⊥" : "\u{22A5}", 254 | "⋅" : "\u{22C5}", 255 | "⌈" : "\u{2308}", 256 | "⌉" : "\u{2309}", 257 | "⌊" : "\u{230A}", 258 | "⌋" : "\u{230B}", 259 | "⟨" : "\u{2329}", 260 | "⟩" : "\u{232A}", 261 | "◊" : "\u{25CA}", 262 | "♠" : "\u{2660}", 263 | "♣" : "\u{2663}", 264 | "♥" : "\u{2665}", 265 | "♦" : "\u{2666}", 266 | ] 267 | } 268 | 269 | // Convert the number in the string to the corresponding 270 | // Unicode character, e.g. 271 | // decodeNumeric("64", 10) --> "@" 272 | // decodeNumeric("20ac", 16) --> "€" 273 | fileprivate func decodeNumeric(_ string : String, base : Int32) -> Character? { 274 | let code = UInt32(strtoul(string, nil, base)) 275 | return Character(UnicodeScalar(code)!) 276 | } 277 | 278 | // Decode the HTML character entity to the corresponding 279 | // Unicode character, return `nil` for invalid input. 280 | // decode("@") --> "@" 281 | // decode("€") --> "€" 282 | // decode("<") --> "<" 283 | // decode("&foo;") --> nil 284 | fileprivate func decode(_ entity : String) -> Character? { 285 | if entity.hasPrefix("&#x") || entity.hasPrefix("&#X"){ 286 | return decodeNumeric(entity.substring(from: entity.index(entity.startIndex, offsetBy: 3)), base: 16) 287 | } else if entity.hasPrefix("&#") { 288 | return decodeNumeric(entity.substring(from: entity.index(entity.startIndex, offsetBy: 2)), base: 10) 289 | } else { 290 | return HTMLEntities.characterEntities[entity] 291 | } 292 | } 293 | 294 | 295 | /// Returns a new string made by replacing in the `String` 296 | /// all HTML character entity references with the corresponding 297 | /// character. 298 | func decodeHTML() -> String { 299 | var result = "" 300 | var position = startIndex 301 | 302 | // Find the next '&' and copy the characters preceding it to `result`: 303 | while let ampRange = self.range(of: "&", range: position ..< endIndex) { 304 | result.append(String(self[position ..< ampRange.lowerBound])) 305 | position = ampRange.lowerBound 306 | 307 | // Find the next ';' and copy everything from '&' to ';' into `entity` 308 | if let semiRange = self.range(of: ";", range: position ..< endIndex) { 309 | let entity = self[position ..< semiRange.upperBound] 310 | position = semiRange.upperBound 311 | 312 | if let decoded = decode(String(entity)) { 313 | // Replace by decoded character: 314 | result.append(decoded) 315 | } else { 316 | // Invalid entity, copy verbatim: 317 | result.append(String(entity)) 318 | } 319 | } else { 320 | // No matching ';'. 321 | break 322 | } 323 | } 324 | // Copy remaining characters to `result`: 325 | result.append(String(self[position ..< endIndex])) 326 | return result 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /Sources/SwiftString/StringURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringHTTP.swift 3 | // SwiftString 4 | // 5 | // Created by thislooksfun on 1/3/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | private let parentBacktrackRegex = "([^/]*)?\\/+\\.\\." 12 | public extension String { 13 | /// The parent directory or url 14 | /// i.e. calling `"/foo/bar/baz".parent` will return `"/foo/bar"` 15 | var parent: String? { 16 | var ref = self 17 | if self.lastIndex(of: "/") == self.length - 1 { 18 | ref = self[0.. -1 { 23 | return ref[0.. -1 { 37 | print(index, self.length) 38 | return ref[index+1.. -1 { 51 | return f[0.. -1 { 64 | return f[index+1.. String { 77 | // Remove duplicate slashes 78 | var str = self 79 | while str.contains("//") { 80 | str = str.replacingOccurrences(of: "//", with: "/") 81 | } 82 | // Parse `..`s 83 | while let range = str.range(of: parentBacktrackRegex, options: .regularExpression) { 84 | str.removeSubrange(range) 85 | 86 | while str.contains("//") { 87 | str = str.replacingOccurrences(of: "//", with: "/") 88 | } 89 | } 90 | return str 91 | } 92 | 93 | /// Join a series of paths together with this as the base 94 | /// i.e. calling `"/foo".join("bar/", "../baz.txt")` will result in `"/foo/baz.txt"` 95 | /// Note: This calls `cleanPath()`, so there is no need to do that yourself 96 | mutating func join(_ paths: String...) { 97 | join(paths: paths) 98 | } 99 | /// Join an array of paths together with this as the base 100 | /// i.e. calling `"/foo".join(paths: ["bar/", "../baz.txt"])` will result in `"/foo/baz.txt"` 101 | /// Note: This calls `cleanPath()`, so there is no need to do that yourself 102 | mutating func join(paths: [String]) { 103 | for path in paths { 104 | self += "/\(path)" 105 | } 106 | self.cleanPath() 107 | } 108 | 109 | /// Join a series of paths together with this as the base, and return it 110 | /// i.e. calling `"/foo".joining("bar/", "../baz.txt")` will return `"/foo/baz.txt"` 111 | /// Note: This calls `cleanPath()`, so there is no need to do that yourself 112 | func joining(_ paths: String...) -> String { 113 | return joining(paths: paths) 114 | } 115 | /// Join an array of paths together with this as the base, and return it 116 | /// i.e. calling `"/foo".joining(paths: ["bar/", "../baz.txt"])` will return `"/foo/baz.txt"` 117 | /// Note: This calls `cleanPath()`, so there is no need to do that yourself 118 | func joining(paths: [String]) -> String { 119 | var tmp = self 120 | tmp.join(paths: paths) 121 | return tmp 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /String+Extensions.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint String+Extensions.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'String+Extensions' 11 | s.version = '1.1.0' 12 | s.summary = 'A lightweight string extension for Swift' 13 | 14 | s.description = <<-DESC 15 | String+Extensions is a lightweight string extension for Swift 3. 16 | This library was motivated by having to search StackOverflow for common string operations, and wanting them to be in one place with test coverage. 17 | 18 | Note the original client side Swift 2 repo can be found here: https://github.com/amayne/SwiftString 19 | DESC 20 | 21 | s.homepage = 'https://github.com/iamjono/SwiftString' 22 | s.license = { :type => 'MIT', :file => 'LICENSE' } 23 | s.authors = { 24 | "Andrew Mayne" => "andrew@redbricklab.com", 25 | "Jonathan Guthrie" => "jono@guthrie.net.nz", 26 | "Koji Murata" => 'malt.koji@gmail.com' 27 | } 28 | s.source = { :git => 'https://github.com/iamjono/SwiftString.git', :tag => s.version.to_s } 29 | 30 | s.requires_arc = true 31 | s.ios.deployment_target = '8.0' 32 | s.osx.deployment_target = '10.10' 33 | s.tvos.deployment_target = '9.0' 34 | s.watchos.deployment_target = '2.0' 35 | 36 | s.source_files = 'Sources/**/*' 37 | end 38 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Project.xcconfig" 2 | COPY_PHASE_STRIP = NO 3 | DEBUG_INFORMATION_FORMAT = dwarf 4 | ENABLE_NS_ASSERTIONS = YES 5 | GCC_OPTIMIZATION_LEVEL = 0 6 | ONLY_ACTIVE_ARCH = YES 7 | SWIFT_OPTIMIZATION_LEVEL = -Onone 8 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/Configs/Project.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_NAME = $(TARGET_NAME) 2 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator 3 | MACOSX_DEPLOYMENT_TARGET = 10.10 4 | DYLIB_INSTALL_NAME_BASE = @rpath 5 | OTHER_SWIFT_FLAGS = -DXcode 6 | COMBINE_HIDPI_IMAGES = YES 7 | USE_HEADERMAP = NO 8 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Project.xcconfig" 2 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 3 | GCC_OPTIMIZATION_LEVEL = s 4 | SWIFT_OPTIMIZATION_LEVEL = -O 5 | COPY_PHASE_STRIP = YES 6 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/SwiftStringTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/SwiftString_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F40014791E1C823400AF13BE /* StringURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40014781E1C823400AF13BE /* StringURL.swift */; }; 11 | F400147C1E1C8C2B00AF13BE /* StringURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F400147A1E1C8B5D00AF13BE /* StringURLTests.swift */; }; 12 | OBJ_22 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* StringExtensions.swift */; }; 13 | OBJ_23 /* StringHTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* StringHTML.swift */; }; 14 | OBJ_30 /* SwiftStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* SwiftStringTests.swift */; }; 15 | OBJ_32 /* SwiftString.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* SwiftString.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | F40014771E1C820D00AF13BE /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = OBJ_1 /* Project object */; 22 | proxyType = 1; 23 | remoteGlobalIDString = OBJ_17; 24 | remoteInfo = SwiftString; 25 | }; 26 | /* End PBXContainerItemProxy section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | F40014781E1C823400AF13BE /* StringURL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringURL.swift; sourceTree = ""; }; 30 | F400147A1E1C8B5D00AF13BE /* StringURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringURLTests.swift; sourceTree = ""; }; 31 | OBJ_10 /* StringHTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringHTML.swift; sourceTree = ""; }; 32 | OBJ_13 /* SwiftStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftStringTests.swift; sourceTree = ""; }; 33 | OBJ_15 /* SwiftString.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftString.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | OBJ_16 /* SwiftStringTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = SwiftStringTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 36 | OBJ_9 /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | OBJ_24 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 0; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | OBJ_31 /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 0; 50 | files = ( 51 | OBJ_32 /* SwiftString.framework in Frameworks */, 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | OBJ_11 /* Tests */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | OBJ_12 /* SwiftStringTests */, 62 | ); 63 | path = Tests; 64 | sourceTree = ""; 65 | }; 66 | OBJ_12 /* SwiftStringTests */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | OBJ_13 /* SwiftStringTests.swift */, 70 | F400147A1E1C8B5D00AF13BE /* StringURLTests.swift */, 71 | ); 72 | name = SwiftStringTests; 73 | path = Tests/SwiftStringTests; 74 | sourceTree = SOURCE_ROOT; 75 | }; 76 | OBJ_14 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | OBJ_15 /* SwiftString.framework */, 80 | OBJ_16 /* SwiftStringTests.xctest */, 81 | ); 82 | name = Products; 83 | sourceTree = BUILT_PRODUCTS_DIR; 84 | }; 85 | OBJ_5 = { 86 | isa = PBXGroup; 87 | children = ( 88 | OBJ_6 /* Package.swift */, 89 | OBJ_7 /* Sources */, 90 | OBJ_11 /* Tests */, 91 | OBJ_14 /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | OBJ_7 /* Sources */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | OBJ_8 /* SwiftString */, 99 | ); 100 | path = Sources; 101 | sourceTree = ""; 102 | }; 103 | OBJ_8 /* SwiftString */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | OBJ_9 /* StringExtensions.swift */, 107 | OBJ_10 /* StringHTML.swift */, 108 | F40014781E1C823400AF13BE /* StringURL.swift */, 109 | ); 110 | name = SwiftString; 111 | path = Sources; 112 | sourceTree = SOURCE_ROOT; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | OBJ_17 /* SwiftString */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = OBJ_18 /* Build configuration list for PBXNativeTarget "SwiftString" */; 120 | buildPhases = ( 121 | OBJ_21 /* Sources */, 122 | OBJ_24 /* Frameworks */, 123 | ); 124 | buildRules = ( 125 | ); 126 | dependencies = ( 127 | ); 128 | name = SwiftString; 129 | productName = SwiftString; 130 | productReference = OBJ_15 /* SwiftString.framework */; 131 | productType = "com.apple.product-type.framework"; 132 | }; 133 | OBJ_25 /* SwiftStringTests */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = OBJ_26 /* Build configuration list for PBXNativeTarget "SwiftStringTests" */; 136 | buildPhases = ( 137 | OBJ_29 /* Sources */, 138 | OBJ_31 /* Frameworks */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | OBJ_33 /* PBXTargetDependency */, 144 | ); 145 | name = SwiftStringTests; 146 | productName = SwiftStringTests; 147 | productReference = OBJ_16 /* SwiftStringTests.xctest */; 148 | productType = "com.apple.product-type.bundle.unit-test"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | OBJ_1 /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | LastUpgradeCheck = 9999; 157 | }; 158 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "SwiftString" */; 159 | compatibilityVersion = "Xcode 3.2"; 160 | developmentRegion = English; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | ); 165 | mainGroup = OBJ_5; 166 | productRefGroup = OBJ_14 /* Products */; 167 | projectDirPath = ""; 168 | projectRoot = ""; 169 | targets = ( 170 | OBJ_17 /* SwiftString */, 171 | OBJ_25 /* SwiftStringTests */, 172 | ); 173 | }; 174 | /* End PBXProject section */ 175 | 176 | /* Begin PBXSourcesBuildPhase section */ 177 | OBJ_21 /* Sources */ = { 178 | isa = PBXSourcesBuildPhase; 179 | buildActionMask = 0; 180 | files = ( 181 | F40014791E1C823400AF13BE /* StringURL.swift in Sources */, 182 | OBJ_22 /* StringExtensions.swift in Sources */, 183 | OBJ_23 /* StringHTML.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | OBJ_29 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 0; 190 | files = ( 191 | F400147C1E1C8C2B00AF13BE /* StringURLTests.swift in Sources */, 192 | OBJ_30 /* SwiftStringTests.swift in Sources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXSourcesBuildPhase section */ 197 | 198 | /* Begin PBXTargetDependency section */ 199 | OBJ_33 /* PBXTargetDependency */ = { 200 | isa = PBXTargetDependency; 201 | target = OBJ_17 /* SwiftString */; 202 | targetProxy = F40014771E1C820D00AF13BE /* PBXContainerItemProxy */; 203 | }; 204 | /* End PBXTargetDependency section */ 205 | 206 | /* Begin XCBuildConfiguration section */ 207 | OBJ_19 /* Debug */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ENABLE_TESTABILITY = YES; 211 | FRAMEWORK_SEARCH_PATHS = ( 212 | "$(inherited)", 213 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 214 | ); 215 | HEADER_SEARCH_PATHS = "$(inherited)"; 216 | INFOPLIST_FILE = SwiftString.xcodeproj/SwiftString_Info.plist; 217 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 218 | OTHER_LDFLAGS = "$(inherited)"; 219 | OTHER_SWIFT_FLAGS = "$(inherited)"; 220 | PRODUCT_BUNDLE_IDENTIFIER = SwiftString; 221 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 222 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 223 | TARGET_NAME = SwiftString; 224 | }; 225 | name = Debug; 226 | }; 227 | OBJ_20 /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ENABLE_TESTABILITY = YES; 231 | FRAMEWORK_SEARCH_PATHS = ( 232 | "$(inherited)", 233 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 234 | ); 235 | HEADER_SEARCH_PATHS = "$(inherited)"; 236 | INFOPLIST_FILE = SwiftString.xcodeproj/SwiftString_Info.plist; 237 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 238 | OTHER_LDFLAGS = "$(inherited)"; 239 | OTHER_SWIFT_FLAGS = "$(inherited)"; 240 | PRODUCT_BUNDLE_IDENTIFIER = SwiftString; 241 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 242 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 243 | TARGET_NAME = SwiftString; 244 | }; 245 | name = Release; 246 | }; 247 | OBJ_27 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 251 | FRAMEWORK_SEARCH_PATHS = ( 252 | "$(inherited)", 253 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 254 | ); 255 | HEADER_SEARCH_PATHS = "$(inherited)"; 256 | INFOPLIST_FILE = SwiftString.xcodeproj/SwiftStringTests_Info.plist; 257 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 258 | OTHER_LDFLAGS = "$(inherited)"; 259 | OTHER_SWIFT_FLAGS = "$(inherited)"; 260 | TARGET_NAME = SwiftStringTests; 261 | }; 262 | name = Debug; 263 | }; 264 | OBJ_28 /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 268 | FRAMEWORK_SEARCH_PATHS = ( 269 | "$(inherited)", 270 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 271 | ); 272 | HEADER_SEARCH_PATHS = "$(inherited)"; 273 | INFOPLIST_FILE = SwiftString.xcodeproj/SwiftStringTests_Info.plist; 274 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 275 | OTHER_LDFLAGS = "$(inherited)"; 276 | OTHER_SWIFT_FLAGS = "$(inherited)"; 277 | TARGET_NAME = SwiftStringTests; 278 | }; 279 | name = Release; 280 | }; 281 | OBJ_3 /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | COMBINE_HIDPI_IMAGES = YES; 285 | COPY_PHASE_STRIP = NO; 286 | DEBUG_INFORMATION_FORMAT = dwarf; 287 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 288 | ENABLE_NS_ASSERTIONS = YES; 289 | GCC_OPTIMIZATION_LEVEL = 0; 290 | MACOSX_DEPLOYMENT_TARGET = 10.10; 291 | ONLY_ACTIVE_ARCH = YES; 292 | OTHER_SWIFT_FLAGS = "-DXcode"; 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | SDKROOT = macosx; 295 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 296 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 297 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 298 | SWIFT_VERSION = 3.0; 299 | USE_HEADERMAP = NO; 300 | }; 301 | name = Debug; 302 | }; 303 | OBJ_4 /* Release */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | COMBINE_HIDPI_IMAGES = YES; 307 | COPY_PHASE_STRIP = YES; 308 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 309 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 310 | GCC_OPTIMIZATION_LEVEL = s; 311 | MACOSX_DEPLOYMENT_TARGET = 10.10; 312 | OTHER_SWIFT_FLAGS = "-DXcode"; 313 | PRODUCT_NAME = "$(TARGET_NAME)"; 314 | SDKROOT = macosx; 315 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 316 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 317 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 318 | SWIFT_VERSION = 3.0; 319 | USE_HEADERMAP = NO; 320 | }; 321 | name = Release; 322 | }; 323 | /* End XCBuildConfiguration section */ 324 | 325 | /* Begin XCConfigurationList section */ 326 | OBJ_18 /* Build configuration list for PBXNativeTarget "SwiftString" */ = { 327 | isa = XCConfigurationList; 328 | buildConfigurations = ( 329 | OBJ_19 /* Debug */, 330 | OBJ_20 /* Release */, 331 | ); 332 | defaultConfigurationIsVisible = 0; 333 | defaultConfigurationName = Debug; 334 | }; 335 | OBJ_2 /* Build configuration list for PBXProject "SwiftString" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | OBJ_3 /* Debug */, 339 | OBJ_4 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Debug; 343 | }; 344 | OBJ_26 /* Build configuration list for PBXNativeTarget "SwiftStringTests" */ = { 345 | isa = XCConfigurationList; 346 | buildConfigurations = ( 347 | OBJ_27 /* Debug */, 348 | OBJ_28 /* Release */, 349 | ); 350 | defaultConfigurationIsVisible = 0; 351 | defaultConfigurationName = Debug; 352 | }; 353 | /* End XCConfigurationList section */ 354 | }; 355 | rootObject = OBJ_1 /* Project object */; 356 | } 357 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/xcshareddata/xcschemes/SwiftString.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /SwiftString.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | SwiftString.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftStringTests 3 | 4 | XCTMain([ 5 | testCase(SwiftStringTests.allTests), 6 | testCase(StringURLTests.allTests), 7 | ]) 8 | -------------------------------------------------------------------------------- /Tests/SwiftStringTests/StringURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringURLTests.swift 3 | // SwiftString 4 | // 5 | // Created by thislooksfun on 1/3/17. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import SwiftString 12 | 13 | class StringURLTests: XCTestCase { 14 | 15 | let rootPathToDir = "/foo/bar/baz/" 16 | let rootPathToDirNoTrail = "/foo/bar/baz" 17 | let rootPathToFile = "/foo/bar/baz.txt" 18 | let rootPathSingleLetters = "/f/b/z" 19 | 20 | let relPathToDir = "foo/bar/baz/" 21 | let relPathToDirNoTrail = "foo/bar/baz" 22 | let relPathToFile = "foo/bar/baz.txt" 23 | let relPathSingleLetters = "f/b/z" 24 | 25 | let randomString = "Quick brown fox, or something." 26 | 27 | func testParent() { 28 | let resultRootPathToDir = rootPathToDir.parent 29 | XCTAssertNotNil(resultRootPathToDir) 30 | XCTAssertEqual(resultRootPathToDir, "/foo/bar") 31 | 32 | let resultRootPathToDirNoTrail = rootPathToDirNoTrail.parent 33 | XCTAssertNotNil(resultRootPathToDirNoTrail) 34 | XCTAssertEqual(resultRootPathToDirNoTrail, "/foo/bar") 35 | 36 | let resultRootPathToFile = rootPathToFile.parent 37 | XCTAssertNotNil(resultRootPathToFile) 38 | XCTAssertEqual(resultRootPathToFile, "/foo/bar") 39 | 40 | let resultRootPathSingleLetters = rootPathSingleLetters.parent 41 | XCTAssertNotNil(resultRootPathSingleLetters) 42 | XCTAssertEqual(resultRootPathSingleLetters, "/f/b") 43 | 44 | let resultRelPathToDir = relPathToDir.parent 45 | XCTAssertNotNil(resultRelPathToDir) 46 | XCTAssertEqual(resultRelPathToDir, "foo/bar") 47 | 48 | let resultRelPathToDirNoTrail = relPathToDirNoTrail.parent 49 | XCTAssertNotNil(resultRelPathToDirNoTrail) 50 | XCTAssertEqual(resultRelPathToDirNoTrail, "foo/bar") 51 | 52 | let resultRelPathToFile = relPathToFile.parent 53 | XCTAssertNotNil(resultRelPathToFile) 54 | XCTAssertEqual(resultRelPathToFile, "foo/bar") 55 | 56 | let resultRelPathSingleLetters = relPathSingleLetters.parent 57 | XCTAssertNotNil(resultRelPathSingleLetters) 58 | XCTAssertEqual(resultRelPathSingleLetters, "f/b") 59 | 60 | let resultRandomString = randomString.parent 61 | XCTAssertNil(resultRandomString) 62 | } 63 | 64 | func testFile() { 65 | let resultRootPathToDir = rootPathToDir.file 66 | XCTAssertNotNil(resultRootPathToDir) 67 | XCTAssertEqual(resultRootPathToDir, "baz") 68 | 69 | let resultRootPathToDirNoTrail = rootPathToDirNoTrail.file 70 | XCTAssertNotNil(resultRootPathToDirNoTrail) 71 | XCTAssertEqual(resultRootPathToDirNoTrail, "baz") 72 | 73 | let resultRootPathToFile = rootPathToFile.file 74 | XCTAssertNotNil(resultRootPathToFile) 75 | XCTAssertEqual(resultRootPathToFile, "baz.txt") 76 | 77 | let resultRootPathSingleLetters = rootPathSingleLetters.file 78 | XCTAssertNotNil(resultRootPathSingleLetters) 79 | XCTAssertEqual(resultRootPathSingleLetters, "z") 80 | 81 | let resultRelPathToDir = relPathToDir.file 82 | XCTAssertNotNil(resultRelPathToDir) 83 | XCTAssertEqual(resultRelPathToDir, "baz") 84 | 85 | let resultRelPathToDirNoTrail = relPathToDirNoTrail.file 86 | XCTAssertNotNil(resultRelPathToDirNoTrail) 87 | XCTAssertEqual(resultRelPathToDirNoTrail, "baz") 88 | 89 | let filetRelPathToFile = relPathToFile.file 90 | XCTAssertNotNil(filetRelPathToFile) 91 | XCTAssertEqual(filetRelPathToFile, "baz.txt") 92 | 93 | let resultRelPathSingleLetters = relPathSingleLetters.file 94 | XCTAssertNotNil(resultRelPathSingleLetters) 95 | XCTAssertEqual(resultRelPathSingleLetters, "z") 96 | 97 | let resultRandomString = randomString.file 98 | XCTAssertNil(resultRandomString) 99 | } 100 | 101 | func testFileName() { 102 | let resultRootPathToDir = rootPathToDir.fileName 103 | XCTAssertNotNil(resultRootPathToDir) 104 | XCTAssertEqual(resultRootPathToDir, "baz") 105 | 106 | let resultRootPathToDirNoTrail = rootPathToDirNoTrail.fileName 107 | XCTAssertNotNil(resultRootPathToDirNoTrail) 108 | XCTAssertEqual(resultRootPathToDirNoTrail, "baz") 109 | 110 | let resultRootPathToFile = rootPathToFile.fileName 111 | XCTAssertNotNil(resultRootPathToFile) 112 | XCTAssertEqual(resultRootPathToFile, "baz") 113 | 114 | let resultRootPathSingleLetters = rootPathSingleLetters.fileName 115 | XCTAssertNotNil(resultRootPathSingleLetters) 116 | XCTAssertEqual(resultRootPathSingleLetters, "z") 117 | 118 | let resultRelPathToDir = relPathToDir.fileName 119 | XCTAssertNotNil(resultRelPathToDir) 120 | XCTAssertEqual(resultRelPathToDir, "baz") 121 | 122 | let resultRelPathToDirNoTrail = relPathToDirNoTrail.fileName 123 | XCTAssertNotNil(resultRelPathToDirNoTrail) 124 | XCTAssertEqual(resultRelPathToDirNoTrail, "baz") 125 | 126 | let filetRelPathToFile = relPathToFile.fileName 127 | XCTAssertNotNil(filetRelPathToFile) 128 | XCTAssertEqual(filetRelPathToFile, "baz") 129 | 130 | let resultRelPathSingleLetters = relPathSingleLetters.fileName 131 | XCTAssertNotNil(resultRelPathSingleLetters) 132 | XCTAssertEqual(resultRelPathSingleLetters, "z") 133 | 134 | let resultRandomString = randomString.fileName 135 | XCTAssertNil(resultRandomString) 136 | } 137 | 138 | func testExtension() { 139 | let resultRootPathToDir = rootPathToDir.extension 140 | XCTAssertNil(resultRootPathToDir) 141 | 142 | let resultRootPathToDirNoTrail = rootPathToDirNoTrail.extension 143 | XCTAssertNil(resultRootPathToDirNoTrail) 144 | 145 | let resultRootPathToFile = rootPathToFile.extension 146 | XCTAssertNotNil(resultRootPathToFile) 147 | XCTAssertEqual(resultRootPathToFile, "txt") 148 | 149 | let resultRootPathSingleLetters = rootPathSingleLetters.extension 150 | XCTAssertNil(resultRootPathSingleLetters) 151 | 152 | let resultRelPathToDir = relPathToDir.extension 153 | XCTAssertNil(resultRelPathToDir) 154 | 155 | let resultRelPathToDirNoTrail = relPathToDirNoTrail.extension 156 | XCTAssertNil(resultRelPathToDirNoTrail) 157 | 158 | let filetRelPathToFile = relPathToFile.extension 159 | XCTAssertNotNil(filetRelPathToFile) 160 | XCTAssertEqual(filetRelPathToFile, "txt") 161 | 162 | let resultRelPathSingleLetters = relPathSingleLetters.extension 163 | XCTAssertNil(resultRelPathSingleLetters) 164 | 165 | let resultRandomString = randomString.extension 166 | XCTAssertNil(resultRandomString) 167 | } 168 | 169 | func testCleanPath() { 170 | var testStringA = "/foo/bar//baz/..////data.txt" 171 | testStringA.cleanPath() 172 | XCTAssertEqual(testStringA, "/foo/bar/data.txt") 173 | 174 | var testStringB = "../foo/bar//baz/..///..//data.txt" 175 | testStringB.cleanPath() 176 | XCTAssertEqual(testStringB, "../foo/data.txt") 177 | } 178 | 179 | func testCleanedPath() { 180 | let testStringA = "/foo/bar//baz/..///data.txt" 181 | XCTAssertEqual(testStringA.cleanedPath(), "/foo/bar/data.txt") 182 | 183 | let testStringB = "../foo/bar//baz/..///..//data.txt" 184 | XCTAssertEqual(testStringB.cleanedPath(), "../foo/data.txt") 185 | } 186 | 187 | func testJoinVararg() { 188 | var base1 = "/foo/bar" 189 | base1.join("/baz", "..", "/..//data.txt") 190 | XCTAssertEqual(base1, "/foo/data.txt") 191 | 192 | var base2 = "/foo/bar/" 193 | base2.join("baz/", "..", "//data.txt") 194 | XCTAssertEqual(base2, "/foo/bar/data.txt") 195 | } 196 | 197 | func testJoinArray() { 198 | var base1 = "/foo/bar" 199 | base1.join(paths: ["/baz", "..", "/..//data.txt"]) 200 | XCTAssertEqual(base1, "/foo/data.txt") 201 | 202 | var base2 = "/foo/bar/" 203 | base2.join(paths: ["baz/", "..", "//data.txt"]) 204 | XCTAssertEqual(base2, "/foo/bar/data.txt") 205 | } 206 | 207 | func testJoiningVararg() { 208 | let base1 = "/foo/bar" 209 | XCTAssertEqual(base1.joining("/baz", "..", "/..//data.txt"), "/foo/data.txt") 210 | 211 | let base2 = "/foo/bar/" 212 | XCTAssertEqual(base2.joining("baz/", "..", "//data.txt"), "/foo/bar/data.txt") 213 | } 214 | 215 | func testJoiningArray() { 216 | let base1 = "/foo/bar" 217 | XCTAssertEqual(base1.joining(paths: ["/baz", "..", "/..//data.txt"]), "/foo/data.txt") 218 | 219 | let base2 = "/foo/bar/" 220 | XCTAssertEqual(base2.joining(paths: ["baz/", "..", "//data.txt"]), "/foo/bar/data.txt") 221 | } 222 | 223 | static var allTests : [(String, (StringURLTests) -> () throws -> Void)] { 224 | return [ 225 | ("testParent", testParent), 226 | ("testFile", testFile), 227 | ("testFileName", testFileName), 228 | ("testExtension", testExtension), 229 | ("testCleanPath", testCleanPath), 230 | ("testCleanedPath", testCleanedPath), 231 | ("testJoinVararg", testJoinVararg), 232 | ("testJoinArray", testJoinArray), 233 | ("testJoiningVararg", testJoiningVararg), 234 | ("testJoiningArray", testJoiningArray), 235 | ] 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Tests/SwiftStringTests/SwiftStringTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if os(Linux) 3 | import SwiftGlibc 4 | #else 5 | import Darwin 6 | #endif 7 | @testable import SwiftString 8 | 9 | class SwiftStringTests: XCTestCase { 10 | override func setUp() { 11 | super.setUp() 12 | } 13 | override func tearDown() { 14 | super.tearDown() 15 | } 16 | 17 | func testBetween() { 18 | let s = "The stupid brown fox" 19 | XCTAssert(((s.between("stupid", "fox")?.count) != nil), "Between is invalid") 20 | } 21 | func testCamelize() { 22 | let s = "The stupid brown fox" 23 | XCTAssert((s.camelize() != "The Stupid Brown Fox"), "Camelize is invalid") 24 | } 25 | func testCapitalize() { 26 | let s = "the Fox" 27 | XCTAssert((s.capitalize() != "THE FOX"), "Capitalize is invalid") 28 | } 29 | func testChompLeft() { 30 | let s = "The stupid brown Fox" 31 | XCTAssertEqual(s.chompLeft("The "), "stupid brown Fox", "ChompLeft is invalid") 32 | } 33 | 34 | func testChompRight() { 35 | let s = "The stupid brown Fox" 36 | XCTAssertEqual(s.chompLeft(" Fox"), "The stupid brown", "ChompRight is invalid") 37 | } 38 | 39 | //collapseWhitespace 40 | func testcollapseWhitespace() { 41 | let s = "The stupid brown fox" 42 | XCTAssertEqual(s.collapseWhitespace(), "The stupid brown fox", "collapseWhitespace is invalid") 43 | } 44 | 45 | //clean 46 | func testclean() { 47 | let s = "The stupid brøwn føx" 48 | XCTAssertEqual(s.clean(with: "o",allOf: "ø"), "The stupid brown fox", "clean is invalid") 49 | } 50 | //count 51 | func testcount() { 52 | let s = "The stupid brown fox" 53 | XCTAssertEqual(s.count("o"), 2, "count is invalid") 54 | } 55 | //endsWith 56 | func testendsWith() { 57 | let s = "The stupid brown fox" 58 | XCTAssertEqual(s.endsWith("foc"), false, "endsWith is invalid") 59 | XCTAssertEqual(s.endsWith("fox"), true, "endsWith is invalid") 60 | } 61 | //ensureLeft 62 | func testensureLeft() { 63 | let s = "The stupid brown fox" 64 | XCTAssertNotEqual(s.ensureLeft("Tha"), s, "ensureLeft is invalid") 65 | XCTAssertEqual(s.ensureLeft("The"), "The stupid brown fox", "ensureLeft is invalid") 66 | XCTAssertEqual(s.ensureLeft("And "), "And The stupid brown fox", "ensureLeft is invalid") 67 | } 68 | //ensureRight 69 | func testensureRight() { 70 | let s = "The stupid brown fox" 71 | XCTAssertNotEqual(s.ensureRight("fax"), s, "ensureRight is invalid") 72 | XCTAssertEqual(s.ensureRight("fox"), "The stupid brown fox", "ensureRight is invalid") 73 | XCTAssertEqual(s.ensureRight(" died."), "The stupid brown fox died.", "ensureRight is invalid") 74 | } 75 | //indexOf 76 | func testindexOf() { 77 | let s = "The stupid brøwn føx" 78 | XCTAssertEqual(s.index(of:"s"), 4, "indexOf is invalid") 79 | XCTAssertNotEqual(s.index(of:"s"), 7, "indexOf is invalid") 80 | } 81 | //initials 82 | func testinitials() { 83 | let s = "brown Fox" 84 | XCTAssertEqual(s.initials(), "bF", "initials is invalid") 85 | XCTAssertNotEqual(s.initials(), "BS", "initials is invalid") 86 | } 87 | //initialsFirstAndLast 88 | func testinitialsFirstAndLast() { 89 | let s = "stupid brown Fox" 90 | XCTAssertEqual(s.initialsFirstAndLast(), "sF", "initialsFirstAndLast is invalid") 91 | XCTAssertNotEqual(s.initialsFirstAndLast(), "bF", "initialsFirstAndLast is invalid") 92 | } 93 | //isAlpha 94 | func testisAlpha() { 95 | let s = "stupid brown Fox" 96 | XCTAssertEqual(s.isAlpha(), false, "isAlpha is invalid") 97 | 98 | let sn = "stupid2Fox" 99 | XCTAssertEqual(sn.isAlpha(), false, "isAlpha is invalid") 100 | 101 | let ss = "stupidbrownFox" 102 | XCTAssertEqual(ss.isAlpha(), true, "isAlpha is invalid") 103 | } 104 | //isAlphaNumeric 105 | func testisAlphaNumeric() { 106 | let s = "stupid brown Fox!" 107 | XCTAssertEqual(s.isAlphaNumeric(), false, "isAlphaNumeric is invalid") 108 | 109 | let snx = "stupid2Fox!" 110 | XCTAssertEqual(snx.isAlphaNumeric(), false, "isAlphaNumeric is invalid") 111 | 112 | let sn = "stupid2Fox" 113 | XCTAssertEqual(sn.isAlphaNumeric(), true, "isAlphaNumeric is invalid") 114 | 115 | let ss = "stupidbrownFox" 116 | XCTAssertEqual(ss.isAlphaNumeric(), true, "isAlphaNumeric is invalid") 117 | } 118 | //isEmpty 119 | func testisEmpty() { 120 | let s = "stupid brown Fox!" 121 | XCTAssertEqual(s.isEmpty(), false, "isEmpty is invalid") 122 | 123 | let snx = "" 124 | XCTAssertEqual(snx.isEmpty(), true, "isEmpty is invalid") 125 | } 126 | //isNumeric 127 | func testisNumeric() { 128 | let s = "stupid brown Fox!" 129 | XCTAssertEqual(s.isNumeric(), false, "isNumeric is invalid") 130 | 131 | let snx = "12" 132 | XCTAssertEqual(snx.isNumeric(), true, "isNumeric is invalid") 133 | } 134 | //latinize 135 | func testlatinize() { 136 | let s = "The stüpid brown Fox" 137 | XCTAssertEqual(s.latinize(), "The stupid brown Fox", "latinize is invalid") 138 | } 139 | //lines 140 | func testlines() { 141 | let s = "The stupid brown Fox\nis dead." 142 | XCTAssertEqual(s.lines(), ["The stupid brown Fox","is dead."], "lines is invalid") 143 | } 144 | //length 145 | func testlength() { 146 | let s = "The stupid brown Fox" 147 | XCTAssertEqual(s.length, 20, "length is invalid") 148 | } 149 | //pad 150 | func testpad() { 151 | let s = "The stupid brown Fox" 152 | XCTAssertEqual(s.pad(3,"x"), "xxxThe stupid brown Foxxxx", "pad is invalid") 153 | } 154 | //padLeft 155 | func testpadLeft() { 156 | let s = "The stupid brown Fox" 157 | XCTAssertEqual(s.padLeft(3,"x"), "xxxThe stupid brown Fox", "padLeft is invalid") 158 | } 159 | //padRight 160 | func testpadRight() { 161 | let s = "The stupid brown Fox" 162 | XCTAssertEqual(s.padRight(3,"x"), "The stupid brown Foxxxx", "padRight is invalid") 163 | } 164 | //slugify 165 | func testslugify() { 166 | let s = "The stupid brown Fox" 167 | XCTAssertEqual(s.slugify(), "the-stupid-brown-fox", "slugify is invalid") 168 | } 169 | //split 170 | func testsplit() { 171 | let s = "The stupid brown Fox - is dead" 172 | XCTAssertEqual(s.split(), ["The","stupid","brown","Fox","-","is","dead"], "split is invalid") 173 | XCTAssertEqual(s.split("-"), ["The stupid brown Fox "," is dead"], "split is invalid") 174 | } 175 | //startsWith 176 | func teststartsWith() { 177 | let s = "The stupid brown Fox" 178 | XCTAssertEqual(s.startsWith("The stupid"), true, "startsWith is invalid") 179 | XCTAssertEqual(s.startsWith("The nice"), false, "startsWith is invalid") 180 | } 181 | //stripPunctuation 182 | func teststripPunctuation() { 183 | let s = "The stupid! brown Fox" 184 | XCTAssertEqual(s.stripPunctuation(), "The stupid brown Fox", "stripPunctuation is invalid") 185 | } 186 | //toFloat 187 | func testtoFloat() { 188 | let s = "2.0" 189 | XCTAssertEqual(s.toFloat(), 2.0, "toFloat is invalid") 190 | } 191 | //toInt 192 | func testtoInt() { 193 | let s = "2" 194 | XCTAssertEqual(s.toInt(), 2, "toInt is invalid") 195 | } 196 | //toBool 197 | func testtoBool() { 198 | let s = "0" 199 | XCTAssertEqual(s.toBool(), false, "toBool is invalid") 200 | let s1 = "-1" 201 | XCTAssertEqual(s1.toBool(), true, "toBool is invalid") 202 | let s2 = "1" 203 | XCTAssertEqual(s2.toBool(), true, "toBool is invalid") 204 | let s3 = "2" 205 | XCTAssertEqual(s3.toBool(), true, "toBool is invalid") 206 | } 207 | #if !os(Linux) 208 | //toDate 209 | func testtoDate() { 210 | let s = "2016-03-01" 211 | let formatter = DateFormatter() 212 | formatter.dateFormat = "yyyy/MM/dd" 213 | let someDateTime = formatter.date(from: "2016-03-01") 214 | XCTAssertEqual(s.toDate(),someDateTime, "toDate is invalid") 215 | } 216 | //toDateTime 217 | func testtoDateTime() { 218 | let s = "2016-03-01 18:31:00" 219 | let formatter = DateFormatter() 220 | formatter.dateFormat = "yyyy/MM/dd HH:mm:SS" 221 | let someDateTime = formatter.date(from: "2016-03-01 18:31:00") 222 | XCTAssertEqual(s.toDateTime(),someDateTime, "toDateTime is invalid") 223 | } 224 | #endif 225 | //trimmedLeft 226 | func testtrimmedLeft() { 227 | let s = " The stupid brown fox " 228 | XCTAssertEqual(s.trimmedLeft(), "The stupid brown fox ", "trimmedLeft is invalid") 229 | } 230 | //trimmedRight 231 | func testtrimmedRight() { 232 | let s = " The stupid brown fox " 233 | XCTAssertEqual(s.trimmedRight(), " The stupid brown fox", "trimmedRight is invalid") 234 | } 235 | //trimmed 236 | func testtrimmed() { 237 | let s = " The stupid brown fox " 238 | XCTAssertEqual(s.trimmed(), "The stupid brown fox", "trimmed is invalid") 239 | } 240 | //substring 241 | func testsubstring() { 242 | let s = "The stupid brown fox" 243 | XCTAssertEqual(s.substring(4,length: 9), "stupid br", "trimmed is invalid") 244 | } 245 | //subscript 246 | func testsubscript() { 247 | let s = "0123456789" 248 | XCTAssertEqual(s[4..<9], "45678", "subscript is invalid") 249 | XCTAssertEqual(s[4...8], "45678", "subscript is invalid") 250 | } 251 | //safesubscript 252 | func testsafesubscript() { 253 | let s = "0123456789" 254 | XCTAssertEqual(s[safe: 1 ..< 6], "12345", "safesubscript is invalid") 255 | XCTAssertEqual(s[safe: 1 ... 6], "123456", "safesubscript is invalid") 256 | XCTAssertEqual(s[safe: 7 ..< 100], "789", "safesubscript is invalid") 257 | XCTAssertEqual(s[safe: 7 ... 100], "789", "safesubscript is invalid") 258 | XCTAssertEqual(s[safe: -100 ..< 2], "01", "safesubscript is invalid") 259 | XCTAssertEqual(s[safe: -100 ... 2], "012", "safesubscript is invalid") 260 | XCTAssertEqual(s[safe: -100 ..< 100], "0123456789", "safesubscript is invalid") 261 | XCTAssertEqual(s[safe: -100 ... 100], "0123456789", "safesubscript is invalid") 262 | XCTAssertEqual(s[safe: -100 ..< -20], "", "safesubscript is invalid") 263 | XCTAssertEqual(s[safe: -100 ... -20], "0", "safesubscript is invalid") 264 | XCTAssertEqual(s[safe: 20 ..< 100], "", "safesubscript is invalid") 265 | XCTAssertEqual(s[safe: 20 ... 100], "9", "safesubscript is invalid") 266 | XCTAssertEqual(""[safe: 20 ..< 100], "", "safesubscript is invalid") 267 | XCTAssertEqual(""[safe: 20 ... 100], "", "safesubscript is invalid") 268 | } 269 | 270 | func testBase64() { 271 | let str = "hello:world" 272 | let encoded = str.toBase64() 273 | let decoded = encoded.fromBase64() 274 | XCTAssertEqual(str, decoded, "Base64 does not match") 275 | } 276 | 277 | static var allTests : [(String, (SwiftStringTests) -> () throws -> Void)] { 278 | return [ 279 | ("testBetween", testBetween), 280 | ("testCamelize", testCamelize), 281 | ("testCapitalize", testCapitalize), 282 | ("testChompLeft", testChompLeft), 283 | ("testChompRight", testChompRight), 284 | ("testcollapseWhitespace", testcollapseWhitespace), 285 | ("testclean", testclean), 286 | ("testcount", testcount), 287 | ("testendsWith", testendsWith), 288 | ("testensureLeft", testensureLeft), 289 | ("testensureRight", testensureRight), 290 | ("testindexOf", testindexOf), 291 | ("testinitials", testinitials), 292 | ("testinitialsFirstAndLast", testinitialsFirstAndLast), 293 | ("testisAlpha", testisAlpha), 294 | ("testisAlphaNumeric", testisAlphaNumeric), 295 | ("testisEmpty", testisEmpty), 296 | ("testisNumeric", testisNumeric), 297 | ("testlatinize", testlatinize), 298 | ("testlines", testlines), 299 | ("testlength", testlength), 300 | ("testpad", testpad), 301 | ("testpadLeft", testpadLeft), 302 | ("testpadRight", testpadRight), 303 | ("testslugify", testslugify), 304 | ("testsplit", testsplit), 305 | ("teststartsWith", teststartsWith), 306 | ("teststripPunctuation", teststripPunctuation), 307 | ("testtoFloat", testtoFloat), 308 | ("testtoInt", testtoInt), 309 | ("testtoBool", testtoBool), 310 | ("testtrimmedLeft", testtrimmedLeft), 311 | ("testtrimmedRight", testtrimmedRight), 312 | ("testtrimmed", testtrimmed), 313 | ("testsubstring", testsubstring), 314 | ("testsubscript", testsubscript), 315 | ("testsafesubscript", testsafesubscript), 316 | ] 317 | } 318 | 319 | } 320 | --------------------------------------------------------------------------------