Swift字符串中子字符串的索引

Index of a substring in a string with Swift

我曾经在JavaScript中这样做:

1
var domains ="abcde".substring(0,"abcde".indexOf("cd")) // Returns"ab"

Swift没有此功能,如何做类似的事情?


编辑/更新:

Xcode 11? Swift 5.1或更高版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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] {
        var indices: [Index] = []
        var startIndex = self.startIndex
        while startIndex < endIndex,
            let range = self[startIndex...]
                .range(of: string, options: options) {
                indices.append(range.lowerBound)
                startIndex = range.lowerBound < range.upperBound ? range.upperBound :
                    index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
        }
        return indices
    }
    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
    }
}

用法:

1
2
3
4
5
6
7
let str ="abcde"
if let index = str.index(of:"cd") {
    let substring = str[..<index]   // ab
    let string = String(substring)
    print(string)  //"ab
"
}
1
2
3
4
5
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}]

不区分大小写的样本

1
2
3
4
let query ="Play"
let ranges = str.ranges(of: query, options: .caseInsensitive)
let matches = ranges.map { str[$0] }   //
print(matches)  // ["play","play","play"]

正则表达式样本

1
2
3
4
5
6
7
8
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"]


使用String[Range]下标可以获得子字符串。您需要起始索引和最后一个索引来创建范围,您可以按以下方式进行操作

1
2
3
4
5
6
7
8
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")
}

如果您没有定义此操作符..<的起始索引,它将采用起始索引。您也可以使用str[str.startIndex..代替str[..


在Swift 4中:

获取字符串中字符的索引:

1
2
3
4
let str ="abcdefghabcd"
if let index = str.index(of:"b") {
   print(index) // Index(_compoundOffset: 4, _cache: Swift.String.Index._Cache.character(1))
}

使用Swift 4从String创建SubString(前缀和后缀):

1
2
3
4
5
6
7
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)")
}

输出量

1
2
3
4
5
6
prefix , suffix : ilike
prefix i, suffix : like
prefix il, suffix : ike
prefix ili, suffix : ke
prefix ilik, suffix : e
prefix ilike, suffix :

如果要在两个索引之间生成子字符串,请使用:

1
2
let substring1 = string[startIndex...endIndex] // including endIndex
let subString2 = string[startIndex..<endIndex] // excluding endIndex


在Swift中可以执行此操作,但是它需要花费更多的行,这是一个函数indexOf(),可以完成预期的工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"
}

此功能尚未优化,但可以处理短字符串。


在Swift版本3中,String没有类似的功能-

1
str.index(of: String)

如果子字符串需要索引,则获取范围的方法之一是。在返回范围的字符串中,我们具有以下函数-

1
2
3
str.range(of: <String>)
str.rangeOfCharacter(from: <CharacterSet>)
str.range(of: <String>, options: <String.CompareOptions>, range: <Range<String.Index>?>, locale: <Locale?>)

例如,在str中查找游戏的首次出现的索引

1
2
3
4
var str ="play play play"
var range = str.range(of:"play")
range?.lowerBound //Result : 0
range?.upperBound //Result : 4

注意:范围是可选的。如果找不到字符串,它将为零。例如

1
2
3
4
var str ="play play play"
var range = str.range(of:"zoo") //Result : nil
range?.lowerBound //Result : nil
range?.upperBound //Result : nil

Leo Dabus的答案很好。这是我基于他使用compactMap以避免Index out of range错误的答案的答案。

迅捷5.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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]


您是否考虑过使用NSRange?

1
2
3
if let range = mainString.range(of: mySubString) {
  //...
}

这里有三个密切相关的问题:

  • 在Cocoa NSString世界(基础)中,所有子字符串查找方法均已结束。

  • Foundation NSRange与Swift Range不匹配;前者使用开始和长度,后者使用端点

  • 通常,Swift字符是使用String.Index而不是Int进行索引的,而Foundation字符是使用Int进行索引的,并且它们之间没有简单的直接转换(因为Foundation和Swift对组成字符的想法不同)

考虑到所有这些,让我们考虑如何编写:

1
2
3
func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
    // ?
}

必须使用String Foundation方法在s中搜索子字符串s2。结果范围返回给我们,而不是作为NSRange(即使这是一种Foundation方法),而是作为String.Index的范围(包装在Optional中,以防根本找不到子字符串)。但是,另一个数字from是一个Int。因此,我们无法形成涉及两者的任何范围。

但是我们不必!我们要做的就是使用采用String.Index的方法切掉原始字符串的末尾,并使用采用Int的方法切掉原始字符串的开始。幸运的是,存在这样的方法!像这样:

1
2
3
4
5
6
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
}

或者,如果您希望能够将此方法直接应用于字符串,例如...

1
let output ="abcde".substring(from:0, toSubstring:"cd")

...然后将其扩展为String:

1
2
3
4
5
6
7
8
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
    }
}

迅捷5

查找子串的索引

1
2
3
4
5
6
7
8
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")
}

查找字符索引

1
2
3
4
5
6
7
8
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")
}