├── .DS_Store ├── .gitignore ├── Makefile ├── Package.swift ├── README.md └── Sources ├── String+Numbers.swift ├── String+Rex.swift └── String.swift /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erica/SwiftString/b4ee9e5c3959babfe7fe8551f110236c8474350e/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files. 2 | *~ 3 | 4 | # Xcode user data. 5 | xcuserdata 6 | 7 | # Finder metadata 8 | .DS_Store 9 | 10 | # Built site content. 11 | /_site 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Less a MakeFile. More a Cheat Sheet. 2 | # git config —global push.followTags true 3 | 4 | MESSAGE=$(filter-out $@,$(MAKECMDGOALS)) 5 | 6 | all: 7 | /Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/swift build 8 | 9 | setup: 10 | git init ; git add . ; git commit -m "Commit" 11 | git tag -a 1.0.0 -m "Version 1.0.0" 12 | git remote add origin https://github.com/erica/SwiftString.git 13 | git push -u origin master 14 | git push --tags 15 | 16 | doit: 17 | git add . ; git commit -m "tweaked" ; git push 18 | 19 | pull: 20 | git clone https://github.com/erica/SwiftString.git 21 | 22 | push: 23 | git add -A 24 | git commit -m "$(MESSAGE)" 25 | git push 26 | echo "git tag -a 1.0.x -m message-in-quotes" 27 | echo "git tag -d 1.0.0" 28 | echo "make pushtag" 29 | 30 | pushtag: 31 | git push --tags 32 | 33 | cleaner: 34 | rm -rf .build 35 | rm -rf .git 36 | 37 | clean: 38 | rm *~ Sources/*~ .DS_Store */.DS_Store 39 | 40 | show: 41 | git tag 42 | git log --pretty=oneline 43 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "SwiftString" 5 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Strings 2 | 3 | BSD. Use at your own risk. 4 | 5 | * Character View: convert character view to string and count characters 6 | * Wackbards: reverse string 7 | * Ranges: use integer ranges with strings 8 | * NSString mimicking: rangeOfString, componentsSeparatedByString 9 | * Decomposition: first, last, butFirst, butLast, car, cdr, just, at, except 10 | * Subscripting: subscripting with get/set with integer subscripting 11 | * Numbers: Converting to and from hex, octal, binary -------------------------------------------------------------------------------- /Sources/String+Numbers.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | import Foundation 8 | 9 | #if os(Linux) 10 | import Glibc 11 | #else 12 | import Darwin 13 | #endif 14 | 15 | // -------------------------------------------------- 16 | // MARK: Numbers 17 | // 18 | // Note: although it is possible to use built-in Int 19 | // initializers, e.g. Int(self, radix: 16), 20 | // these are less flexible as they do not support 21 | // common prefixes. If you choose to replace 22 | // the unix calls with Int(_, radix:), return 23 | // `Int?` and allow the client to coalesce 24 | // 25 | // -------------------------------------------------- 26 | 27 | // Supports Swift prefixes (0b, 0o, 0x) and Unix (0, 0x / 0X) 28 | public extension String { 29 | /// Trims prefix from string 30 | func trim(prefix: String) -> String { 31 | return hasPrefix(prefix) ? String(characters.dropFirst(prefix.characters.count)) : self 32 | } 33 | 34 | /// Standard binary prefix 35 | public var binaryPrefix: String { return "0b" } 36 | 37 | /// Standard octal prefix 38 | public var octalPrefix: String { return "0o" } 39 | 40 | /// Standard hex prefix 41 | public var hexPrefix: String { return "0x" } 42 | 43 | /// Converts string to Int value 44 | public var integerValue: Int { return strtol(self, nil, 10) } 45 | 46 | /// Converts string to UInt value 47 | public var uintegerValue: UInt { return strtoul(self, nil, 10) } 48 | 49 | /// Converts string to Bool value 50 | public var booleanValue: Bool { return integerValue != 0 } 51 | 52 | /// Converts string to binary value, ignoring any 0b prefix 53 | public var binaryValue: Int? { 54 | return Int(trim(prefix: binaryPrefix), radix: 2) 55 | } 56 | 57 | /// Converts string to octal value, ignoring any 0o prefix, supporting 0 prefix 58 | public var octalValue: Int? { 59 | return Int(trim(prefix: octalPrefix), radix: 8) 60 | } 61 | 62 | /// Converts string to hex value. This supports 0x, 0X prefix if present 63 | public var hexValue: Int? { 64 | return Int(trim(prefix: hexPrefix).trim(prefix: "0X"), radix: 16) 65 | } 66 | 67 | /// Converts string to its unsigned binary value, ignoring any 0b prefix 68 | public var uBinaryValue: UInt? { 69 | return UInt(trim(prefix: binaryPrefix), radix: 2) 70 | } 71 | 72 | /// Converts string to its unsigned octal value, ignoring any 0o prefix 73 | public var uOctalValue: UInt? { 74 | return UInt(trim(prefix: octalPrefix), radix: 8) 75 | } 76 | 77 | /// Converts string to unsigned hex value. This supports 0x prefix if present 78 | public var uHexValue: UInt? { 79 | return UInt(trim(prefix: hexPrefix).trim(prefix: "0X"), radix: 16) 80 | } 81 | 82 | /// Left pads string to at least `minWidth` characters wide 83 | public func leftPad(_ character: Character, toWidth minWidth: Int) -> String { 84 | guard minWidth > characters.count else { return self } 85 | return String(repeating: String(character), count: minWidth - characters.count) + self 86 | } 87 | } 88 | 89 | public extension Int { 90 | 91 | /// Convert to binary string, no prefix 92 | public var binaryString: String { return String(self, radix:2) } 93 | 94 | /// Convert to octal string, no prefix 95 | public var octalString: String { return String(self, radix:8) } 96 | 97 | /// Returns Int's representation as hex string using 0-padding 98 | /// to represent the smallest standard memory footprint that can 99 | /// store the value. 100 | public var hexString : String { 101 | let unpaddedHex = String(self, radix:16, uppercase: true) 102 | let stringCharCount = unpaddedHex.characters.count 103 | let desiredPadding = 1 << Swift.max(fls(Int32(stringCharCount - 1)), 1) // Thanks, Greg Titus 104 | return unpaddedHex.leftPad("0", toWidth: Int(desiredPadding)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/String+Rex.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public func rexRange(_ pattern: String, caseInsensitive: Bool = true) -> Range? { 5 | var options: String.CompareOptions = [.literal, .diacriticInsensitive, .regularExpression ] 6 | if caseInsensitive { options.insert(.caseInsensitive) } 7 | 8 | return self.range(of: pattern, options: options) 9 | } 10 | 11 | public func rexIntRange(_ pattern: String, caseInsensitive: Bool = true) -> CountableClosedRange? { 12 | guard let range = rexRange(pattern, caseInsensitive: caseInsensitive) else { return nil } 13 | return closedRange(fromRange: range) 14 | } 15 | 16 | public func rexFind(_ pattern: String, caseInsensitive: Bool = true) -> Bool { 17 | return rexRange(pattern, caseInsensitive: caseInsensitive) != nil 18 | } 19 | 20 | public func rexReplace(_ pattern: String, with replacement: String, caseInsensitive: Bool = true) -> String { 21 | var options: String.CompareOptions = [.literal, .diacriticInsensitive, .regularExpression ] 22 | if caseInsensitive { options.insert(.caseInsensitive) } 23 | 24 | var copy = self 25 | while let range = copy.range(of: pattern, options: options) { 26 | copy = copy.replacingCharacters(in: range, with: replacement) 27 | } 28 | 29 | return copy 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /* 4 | 5 | Erica Sadun, http://ericasadun.com 6 | Like a bridge over troubled Foundation, I will lay me down 7 | 8 | */ 9 | 10 | #if os(Linux) 11 | #else 12 | #endif 13 | 14 | // -------------------------------------------------- 15 | // MARK: Character View 16 | // -------------------------------------------------- 17 | 18 | public extension String.CharacterView { 19 | /// Converts character view back to String 20 | public var stringValue: String { return String(self) } 21 | } 22 | 23 | public extension String { 24 | /// Returns length in characters 25 | public var characterLength: Int { return characters.count } 26 | } 27 | 28 | // -------------------------------------------------- 29 | // MARK: Wackbards 30 | // -------------------------------------------------- 31 | 32 | public extension String { 33 | /// Reverses a String instance by re-ordering its characters 34 | public var reversed: String { return String(characters.reversed()) } 35 | } 36 | 37 | // -------------------------------------------------- 38 | // MARK: Ranges 39 | // -------------------------------------------------- 40 | 41 | infix operator ..+ 42 | 43 | /// Returns a CountableClosedRange using location and length 44 | public func ..+ (location: Bound, length: Bound.Stride) -> CountableClosedRange { 45 | return location ... location.advanced(by: length - 1) 46 | } 47 | 48 | public extension String { 49 | /// Converts an closed Int range into a CharacterView.Index range 50 | public func characterRange(_ range: CountableClosedRange) -> Range { 51 | let start = characters.index(characters.startIndex, offsetBy: range.lowerBound) 52 | let end = characters.index(start, offsetBy: range.count) 53 | return start ..< end 54 | } 55 | 56 | /// Replaces a character-based range with a replacement string 57 | public func replacing(range: CountableClosedRange, with replacementString: String) -> String { 58 | return self.replacingCharacters(in: characterRange(range), with: replacementString) 59 | } 60 | 61 | /// Retrieves a ranged substring 62 | public func substring(_ range: CountableClosedRange) -> String { 63 | return String(self.characters[characterRange(range)]) 64 | } 65 | 66 | /// Subscripts by closed Int range 67 | public subscript(_ range: CountableClosedRange) -> String { 68 | get { return substring(range) } 69 | set { self = replacing(range: range, with: newValue) } 70 | } 71 | 72 | /// Subscripts by index 73 | public subscript(index: Int) -> String { 74 | get { return substring(index ... index) } 75 | set { self[index ... index] = newValue } 76 | } 77 | 78 | /// Converts Range to CountableClosedRange 79 | public func closedRange(fromRange range: Range) -> CountableClosedRange { 80 | let start = distance(from: characters.startIndex, to: range.lowerBound) 81 | let end = distance(from: characters.startIndex, to: range.upperBound) 82 | return start ..+ (end - start) 83 | } 84 | 85 | /// Returns character-based range of substring 86 | public func characterRange(of substring: String) -> CountableClosedRange? { 87 | guard let range = range(of: substring) else { return nil } 88 | return closedRange(fromRange: range) 89 | } 90 | } 91 | 92 | 93 | // -------------------------------------------------- 94 | // MARK: Decomposition 95 | // -------------------------------------------------- 96 | 97 | public extension String { 98 | 99 | /// First character in the string 100 | public var first: String { 101 | return isEmpty ? "" : self[0] 102 | } 103 | 104 | /// All characters but the first 105 | public var butFirst: String { 106 | return String(characters.dropFirst()) 107 | } 108 | 109 | /// first alias for Lispies 110 | public var car: String { return first } 111 | 112 | /// butFirst / rest alias for Lispies 113 | public var cdr: String { return butFirst } 114 | 115 | /// Last character in the string 116 | public var last: String { 117 | return isEmpty ? "" : self[characterLength - 1] 118 | } 119 | 120 | /// All characters but the last 121 | public var butLast: String { return String(characters.dropLast()) } 122 | } 123 | 124 | // -------------------------------------------------- 125 | // MARK: Trimming 126 | // -------------------------------------------------- 127 | 128 | extension String { 129 | /// Trimming string whitespace 130 | public var trimmed: String { 131 | return trimmingCharacters(in: .whitespacesAndNewlines) 132 | } 133 | 134 | /// Returns string's path extension. Like NSString but Swift 135 | public var pathExtension: String { 136 | let wackbard = self.reversed 137 | if let range = wackbard.range(of: ".") { 138 | return wackbard.substring(to: range.lowerBound).reversed 139 | } else { return "" } 140 | } 141 | 142 | /// Returns string's last path component. Like NSString but Swift 143 | public var lastPathComponent: String { 144 | let wackbard = self.reversed 145 | if let range = wackbard.range(of: "/") { 146 | return wackbard.substring(to: range.lowerBound).reversed 147 | } else { return "" } 148 | } 149 | 150 | /// Returns string by deleting last path component. Like NSString but Swift 151 | public var deletingLastPathComponent: String { 152 | let wackbard = self.reversed 153 | if let range = wackbard.range(of: "/") { 154 | return wackbard.substring(from: range.lowerBound).reversed 155 | } else { return "" } 156 | } 157 | } 158 | --------------------------------------------------------------------------------