I'm used to do this in JavaScript:
var domains = "abcde".substring(0, "abcde".indexOf("cd")) // Returns "ab"
Swift doesn't have this function, how to do something similar?
edit/update:
Xcode 11.4 • Swift 5.2 or later
import Foundation
extension StringProtocol {
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.lowerBound
}
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.upperBound
}
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
ranges(of: string, options: options).map(\.lowerBound)
}
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
usage:
let str = "abcde"
if let index = str.index(of: "cd") {
let substring = str[..<index] // ab
let string = String(substring)
print(string) // "ab\n"
}
let str = "Hello, playground, playground, playground"
str.index(of: "play") // 7
str.endIndex(of: "play") // 11
str.indices(of: "play") // [7, 19, 31]
str.ranges(of: "play") // [{lowerBound 7, upperBound 11}, {lowerBound 19, upperBound 23}, {lowerBound 31, upperBound 35}]
case insensitive sample
let query = "Play"
let ranges = str.ranges(of: query, options: .caseInsensitive)
let matches = ranges.map { str[$0] } //
print(matches) // ["play", "play", "play"]
regular expression sample
let query = "play"
let escapedQuery = NSRegularExpression.escapedPattern(for: query)
let pattern = "\\b\(escapedQuery)\\w+" // matches any word that starts with "play" prefix
let ranges = str.ranges(of: pattern, options: .regularExpression)
let matches = ranges.map { str[$0] }
print(matches) // ["playground", "playground", "playground"]
Using String[Range<String.Index>] subscript you can get the sub string. You need starting index and last index to create the range and you can do it as below
let str = "abcde"
if let range = str.range(of: "cd") {
let substring = str[..<range.lowerBound] // or str[str.startIndex..<range.lowerBound]
print(substring) // Prints ab
}
else {
print("String not present")
}
If you don't define the start index this operator ..< , it take the starting index. You can also use str[str.startIndex..<range.lowerBound] instead of str[..<range.lowerBound]
Swift 5
Find index of substring
let str = "abcdecd"
if let range: Range<String.Index> = str.range(of: "cd") {
let index: Int = str.distance(from: str.startIndex, to: range.lowerBound)
print("index: ", index) //index: 2
}
else {
print("substring not found")
}
Find index of Character
let str = "abcdecd"
if let firstIndex = str.firstIndex(of: "c") {
let index = str.distance(from: str.startIndex, to: firstIndex)
print("index: ", index) //index: 2
}
else {
print("symbol not found")
}
In Swift 4 :
Getting Index of a character in a string :
let str = "abcdefghabcd"
if let index = str.index(of: "b") {
print(index) // Index(_compoundOffset: 4, _cache: Swift.String.Index._Cache.character(1))
}
Creating SubString (prefix and suffix) from String using Swift 4:
let str : String = "ilike"
for i in 0...str.count {
let index = str.index(str.startIndex, offsetBy: i) // String.Index
let prefix = str[..<index] // String.SubSequence
let suffix = str[index...] // String.SubSequence
print("prefix \(prefix), suffix : \(suffix)")
}
Output
prefix , suffix : ilike
prefix i, suffix : like
prefix il, suffix : ike
prefix ili, suffix : ke
prefix ilik, suffix : e
prefix ilike, suffix :
If you want to generate a substring between 2 indices , use :
let substring1 = string[startIndex...endIndex] // including endIndex
let subString2 = string[startIndex..<endIndex] // excluding endIndex
Doing this in Swift is possible but it takes more lines, here is a function indexOf() doing what is expected:
func indexOf(source: String, substring: String) -> Int? {
let maxIndex = source.characters.count - substring.characters.count
for index in 0...maxIndex {
let rangeSubstring = source.startIndex.advancedBy(index)..<source.startIndex.advancedBy(index + substring.characters.count)
if source.substringWithRange(rangeSubstring) == substring {
return index
}
}
return nil
}
var str = "abcde"
if let indexOfCD = indexOf(str, substring: "cd") {
let distance = str.startIndex.advancedBy(indexOfCD)
print(str.substringToIndex(distance)) // Returns "ab"
}
This function is not optimized but it does the job for short strings.
There are three closely connected issues here:
All the substring-finding methods are over in the Cocoa NSString world (Foundation)
Foundation NSRange has a mismatch with Swift Range; the former uses start and length, the latter uses endpoints
In general, Swift characters are indexed using String.Index, not Int, but Foundation characters are indexed using Int, and there is no simple direct translation between them (because Foundation and Swift have different ideas of what constitutes a character)
Given all that, let's think about how to write:
func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
// ?
}
The substring s2 must be sought in s using a String Foundation method. The resulting range comes back to us, not as an NSRange (even though this is a Foundation method), but as a Range of String.Index (wrapped in an Optional, in case we didn't find the substring at all). However, the other number, from, is an Int. Thus we cannot form any kind of range involving them both.
But we don't have to! All we have to do is slice off the end of our original string using a method that takes a String.Index, and slice off the start of our original string using a method that takes an Int. Fortunately, such methods exist! Like this:
func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
guard let r = s.range(of:s2) else {return nil}
var s = s.prefix(upTo:r.lowerBound)
s = s.dropFirst(from)
return s
}
Or, if you prefer to be able to apply this method directly to a string, like this...
let output = "abcde".substring(from:0, toSubstring:"cd")
...then make it an extension on String:
extension String {
func substring(from:Int, toSubstring s2 : String) -> Substring? {
guard let r = self.range(of:s2) else {return nil}
var s = self.prefix(upTo:r.lowerBound)
s = s.dropFirst(from)
return s
}
}
Swift 5
let alphabet = "abcdefghijklmnopqrstuvwxyz"
var index: Int = 0
if let range: Range<String.Index> = alphabet.range(of: "c") {
index = alphabet.distance(from: alphabet.startIndex, to: range.lowerBound)
print("index: ", index) //index: 2
}
Swift 5
extension String {
enum SearchDirection {
case first, last
}
func characterIndex(of character: Character, direction: String.SearchDirection) -> Int? {
let fn = direction == .first ? firstIndex : lastIndex
if let stringIndex: String.Index = fn(character) {
let index: Int = distance(from: startIndex, to: stringIndex)
return index
} else {
return nil
}
}
}
tests:
func testFirstIndex() {
let res = ".".characterIndex(of: ".", direction: .first)
XCTAssert(res == 0)
}
func testFirstIndex1() {
let res = "12345678900.".characterIndex(of: "0", direction: .first)
XCTAssert(res == 9)
}
func testFirstIndex2() {
let res = ".".characterIndex(of: ".", direction: .last)
XCTAssert(res == 0)
}
func testFirstIndex3() {
let res = "12345678900.".characterIndex(of: "0", direction: .last)
XCTAssert(res == 10)
}
In the Swift version 3, String doesn't have functions like -
str.index(of: String)
If the index is required for a substring, one of the ways to is to get the range. We have the following functions in the string which returns range -
str.range(of: <String>)
str.rangeOfCharacter(from: <CharacterSet>)
str.range(of: <String>, options: <String.CompareOptions>, range: <Range<String.Index>?>, locale: <Locale?>)
For example to find the indexes of first occurrence of play in str
var str = "play play play"
var range = str.range(of: "play")
range?.lowerBound //Result : 0
range?.upperBound //Result : 4
Note : range is an optional. If it is not able to find the String it will make it nil. For example
var str = "play play play"
var range = str.range(of: "zoo") //Result : nil
range?.lowerBound //Result : nil
range?.upperBound //Result : nil
Leo Dabus's answer is great. Here is my answer based on his answer using compactMap to avoid Index out of range error.
Swift 5.1
extension StringProtocol {
func ranges(of targetString: Self, options: String.CompareOptions = [], locale: Locale? = nil) -> [Range<String.Index>] {
let result: [Range<String.Index>] = self.indices.compactMap { startIndex in
let targetStringEndIndex = index(startIndex, offsetBy: targetString.count, limitedBy: endIndex) ?? endIndex
return range(of: targetString, options: options, range: startIndex..<targetStringEndIndex, locale: locale)
}
return result
}
}
// Usage
let str = "Hello, playground, playground, playground"
let ranges = str.ranges(of: "play")
ranges.forEach {
print("[\($0.lowerBound.utf16Offset(in: str)), \($0.upperBound.utf16Offset(in: str))]")
}
// result - [7, 11], [19, 23], [31, 35]
Have you considered using NSRange?
if let range = mainString.range(of: mySubString) {
//...
}
extension String {
var masked: String {
// some logic which I have to write to mask string.
// I tried following and just shows x 🤦♂️
// replacingOccurrences(
// of: ".(.+).",
// with: "x",
// options: .regularExpression,
// range: nil
//)
}
}
let helloWorld = "Hello World"
print("Masked string is - \(helloWorld.masked)")
Expected output is - "Hxxxxxxxxxd"
There is a Regular Expression way with lookaround
extension String {
var masked: String {
replacingOccurrences(
of: "(?!^).(?!$)", // RegEx
with: "x", // Replacement
options: .regularExpression // Option to set RegEx
)
}
}
You can enumerate the string and apply map transform to get the expected output:
extension String {
var masked: String {
self.enumerated().map({ (index, ch) in
if index == 0
|| index == self.count - 1 {
return String(ch)
}
return "x"
}).joined()
}
}
let str = "hello"
print("after masking \(str.masked)") // Output - hxxxo
The map transform will return an array, so use joined() to convert the array back to String. Also, note that you have to typecast ch to String as String(ch) because the type of ch is 'String.Element' (aka 'Character').
extension Sequence {
func replacingEachInteriorElement(with replacement: Element) -> [Element] {
let prefix = dropLast()
return
prefix.prefix(1)
+ prefix.dropFirst().map { _ in replacement }
+ suffix(1)
}
}
extension String {
var masked: Self {
.init( replacingEachInteriorElement(with: "x") )
}
}
"Hello World".masked == "Hxxxxxxxxxd" // true
"H🦾👄🐺🥻🐸🦈🏄♂️🍯🪐d".masked == "Hello World".masked // true
"🥮".masked // 🥮
"🥶😎".masked // 🥶😎
[].replacingEachInteriorElement(with: 500) // []
My solution without using Regular Expression:
extension String {
var masked: String {
if self.count < 2 { return self }
var output = self
let range = self.index(after: self.startIndex)..<self.index(before: endIndex)
let replacement = String.init(repeating: "x", count: output.count - 2)
output.replaceSubrange(range, with: replacement)
return output
}
}
So far, I've found following solution.
extension String {
var masked: String {
var newString = ""
for index in 0..<count {
if index != 0 && index != count-1 {
newString.append(contentsOf: "x")
} else {
let array = Array(self)
let char = array[index]
let string = String(char)
newString.append(string)
}
}
return newString
}
}
If you want to leave first and last letters you can use this ->
public extension String {
var masked: String {
return prefix(1) + String(repeating: "x", count: Swift.max(0, count-2)) + suffix(1)
}
}
USAGE
let hello = "Hello"
hello.masked
// Hxxxo
OR
you can pass unmasked character count ->
public extension String {
func masked(with unmaskedCount: Int) -> String {
let unmaskedPrefix = unmaskedCount/2
return prefix(unmaskedPrefix) + String(repeating: "x", count: Swift.max(0, count-unmaskedPrefix)) + suffix(unmaskedPrefix)
}
}
USAGE
let hello = "Hello"
hello.masked(with: 2)
// Hxxxo
let number = "5555555555"
number.masked(with: 4)
// 55xxxxxx55
I've created a method which generates and returns a random string of both letters and numbers, but for some reason I only get a string with numbers and the length of the string doesn't come close to what I asked it to be. A few examples of strings that have been returned: "478388299949939566" (inserted 18 as the length), "3772919388584334" (inserted 9 as the length), "2293010089409293945" (inserted 6 as the length). Anyone can see what I'm missing here?
func generateRandomStringWithLength(length:Int) -> String {
let randomString:NSMutableString = NSMutableString(capacity: length)
let letters:NSMutableString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for index in 0...length {
let randomIndex:Int = Int(arc4random_uniform(UInt32(62)))
randomString.append("\(letters.character(at: randomIndex))")
}
return String(randomString)
}
Your problem is here:
letters.character(at: randomIndex)
it's function returns the character at a given UTF-16 code unit index, not not just a character
Here is my version, I guess its more swiftly.
func generateRandomStringWithLength(length: Int) -> String {
var randomString = ""
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for _ in 1...length {
let randomIndex = Int(arc4random_uniform(UInt32(letters.characters.count)))
let a = letters.index(letters.startIndex, offsetBy: randomIndex)
randomString += String(letters[a])
}
return randomString
}
generateRandomStringWithLength(length: 5)
Use this:
func generateRandomStringWithLength(length:Int) -> String {
let randomString:NSMutableString = NSMutableString(capacity: length)
let letters:NSMutableString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var i: Int = 0
while i < length {
let randomIndex:Int = Int(arc4random_uniform(UInt32(letters.length)))
randomString.appendString("\(Character( UnicodeScalar( letters.characterAtIndex(randomIndex))))")
i += 1
}
return String(randomString)
}
Calling generateRandomStringWithLength method:
print(generateRandomStringWithLength(5))
print(generateRandomStringWithLength(10))
print(generateRandomStringWithLength(20))
print(generateRandomStringWithLength(7))
print(generateRandomStringWithLength(14))
Sample Output:
GIrqb
nWmieQRVdk
r0It9V1xkGFRa2HVwtCw
RLIRuVI
nXnFGV2LQ3CjbD
I have the following string I would like to edit:
var someString = "I wan't this text {something I don't want}"
I would like to remove all the text contained in the two braces, no matter how long that text is. I have been using the follow code to remove a section of a String when I know the range:
extension String {
mutating func deleteCharactersInRange(range: NSRange) {
let mutableSelf = NSMutableString(string: self)
mutableSelf.deleteCharactersInRange(range)
self = mutableSelf
}
}
However, I do not know the range in my problem. Any ideas?
Working with strings and ranges can be quite challenging when mixing NSString and NSRange with Swift's String and Range.
Here is a pure Swift solution.
var someString = "I wan't this text {something I don't want}"
let rangeOpenCurl = someString.rangeOfString("{")
let rangeCloseCurl = someString.rangeOfString("}")
if let startLocation = rangeOpenCurl?.startIndex,
let endLocation = rangeCloseCurl?.endIndex {
someString.replaceRange(startLocation ..< endLocation, with: "")
}
With a RegEx pattern to match anything enclosed with curly brackets:
var sourceString: String = "I wan\'t this text {something I don't want}"
let destinationString = sourceString.stringByReplacingOccurrencesOfString("\\{(.*?)\\}", withString: "", options: .RegularExpressionSearch)
print(destinationString)
This will print "I wan't this text " without the double quotes.
extension String {
func getCurlyBraceRanges() -> [NSRange] {
var results = [NSRange]()
var leftCurlyBrace = -1
for index in 0..<self.characters.count {
let char = self[self.startIndex.advancedBy(index)]
if char == Character("{") {
leftCurlyBrace = index
} else if char == Character("}") {
if leftCurlyBrace != -1 {
results.append(NSRange(location: leftCurlyBrace, length: index - leftCurlyBrace + 1))
leftCurlyBrace = -1
}
}
}
return results
}
mutating func deleteCharactersInRange(range: NSRange) {
let mutableSelf = NSMutableString(string: self)
mutableSelf.deleteCharactersInRange(range)
self = String(mutableSelf)
}
mutating func deleteCharactersInRanges(ranges: [NSRange]) {
var tmpString = self
for i in (0..<ranges.count).reverse() {
tmpString.deleteCharactersInRange(ranges[i])
print(tmpString)
}
self = tmpString
}
}
var testString = "I wan't this text {something I don't want}"
testString.deleteCharactersInRanges(testString.getCurlyBraceRanges())
Output: "I wan't this text "
I found some code to encode a Base10-String with to a custom BaseString:
func stringToCustomBase(encode: Int, alphabet: String) -> String {
var base = alphabet.count, int = encode, result = ""
repeat {
let index = alphabet.index(alphabet.startIndex, offsetBy: (int % base))
result = [alphabet[index]] + result
int /= base
} while (int > 0)
return result
}
... calling it with this lines:
let encoded = stringToCustomBase(encode: 9291, alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
print(encoded)
The encoding above works pretty good. But what about decoding the encoded string?
So because I got no idea how to decode a (in this case Base62 [alphabet.count=62]) to a human readable string (in this case [Base10]) any help would be super appreciated.
PS: (A full code solution is not required, I can also come up with some kind of pseudo-code or maybe just a few-lines of code)
This is what I've tried so far:
func reVal(num: Int) -> Character {
if (num >= 0 && num <= 9) {
return Character("\(num)")
}
return Character("\(num - 10)A");
}
func convertBack() {
var index = 0;
let encoded = "w2RDn3"
var decoded = [Character]()
var inputNum = encoded.count
repeat {
index+=1
decoded[index] = reVal(num: inputNum % 62)
//encoded[index] = reVal(inputNum % 62);
inputNum /= 62;
} while (inputNum > 0)
print(decoded);
}
Based on the original algorithm you need to iterate through each character of the encoded string, find the location of that character within the alphabet, and calculate the new result.
Here are both methods and some test code:
func stringToCustomBase(encode: Int, alphabet: String) -> String {
var base = alphabet.count, string = encode, result = ""
repeat {
let index = alphabet.index(alphabet.startIndex, offsetBy: (string % base))
result = [alphabet[index]] + result
string /= base
} while (string > 0)
return result
}
func customBaseToInt(encoded: String, alphabet: String) -> Int? {
let base = alphabet.count
var result = 0
for ch in encoded {
if let index = alphabet.index(of: ch) {
let mult = result.multipliedReportingOverflow(by: base)
if (mult.overflow) {
return nil
} else {
let add = mult.partialValue.addingReportingOverflow(alphabet.distance(from: alphabet.startIndex, to: index))
if (add.overflow) {
return nil
} else {
result = add.partialValue
}
}
} else {
return nil
}
}
return result
}
let startNum = 234567
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let codedNum = stringToCustomBase(encode: startNum, alphabet: alphabet)
let origNun = customBaseToInt(encoded: codedNum, alphabet: alphabet)
I made the customBaseToInt method return an optional result in case there are characters in the encoded value that are not in the provided alphabet.
You can achieve this via reduce:
enum RadixDecodingError: Error {
case invalidCharacter
case overflowed
}
func customRadixToInt(str: String, alphabet: String) throws -> Int {
return try str.reduce(into: 0) {
guard let digitIndex = alphabet.index(of: $1) else {
throw RadixDecodingError.invalidCharacter
}
let multiplied = $0.multipliedReportingOverflow(by: alphabet.count)
guard !multiplied.overflow else {
throw RadixDecodingError.overflowed
}
let added = multiplied.partialValue.addingReportingOverflow(alphabet.distance(from: alphabet.startIndex, to: digitIndex))
guard !added.overflow else {
throw RadixDecodingError.overflowed
}
$0 = added.partialValue
}
}
I used the exception throwing mechanism so that the caller can distinguish between invalid characters or overflow errors.