Swift中字符串子串的索引

108

我习惯使用JavaScript来做这件事:

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

Swift 没有这个函数,如何做类似的事情?


@eric-d 这不是你提到的那个重复问题。OP 是关于 indexOf() 而不是 substring()。 - mdupls
在Swift 2中,有一个String.rangeOfString(String)方法,它返回一个Range。 - mdupls
11个回答

182

编辑/更新:

Xcode 11.4 • Swift 5.2或更高版本

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
    }
}

使用方法:

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}]

不区分大小写的示例

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

正则表达式示例

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"]

3
这并不完全正确,因为 "ab".indexOf("a")"ab".indexOf("c") 都会返回 0 - Graham
对于那些已经升级到Swift 3.0的人:extension String {func indexOf(string: String) -> String.Index? { return range(of: string, options: .literal, range: nil, locale: nil)?.lowerBound } } - gammachill
2
请确保您导入了Foundation,否则这将无法工作。因为在这一点上,您只是在使用NSString。 - CommaToast
1
这是一项繁重的工作 - 并不是 Swift 原生的方式。请参见 @Inder Kumar Rathore 的下面的答案 - 简单使用 '.range(of: "text")' 方法。 - Marchy
显示剩余13条评论

70

使用 String[Range<String.Index>] 下标可以获取子字符串。你需要提供起始索引和结束索引来创建范围,如下所示:

使用 String[Range<String.Index>] 下标可以获取子字符串。您需要提供起始索引和结束索引以创建范围,方法如下:

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..<range.lowerBound] 替代 str[..<range.lowerBound]


38

Swift 5

查找子字符串的索引

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")
}

查找字符的索引

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")
}

8

在Swift 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从字符串创建子字符串(前缀和后缀):

要从字符串中创建前缀和后缀的子字符串,请使用以下方法: 前缀:
let str = "Hello, World"
let prefix = str.prefix(5) // "Hello"

这里,我们使用了 prefix(_ maxLength: Int) 方法来获取字符串的前五个字符。 后缀:
let str = "Hello, World"
let suffix = str.suffix(5) // "World"

同样地,我们使用 suffix(_ maxLength: Int) 方法来获取字符串的最后五个字符。
希望这能帮助你创建子字符串。
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)")
}

输出

prefix , suffix : ilike
prefix i, suffix : like
prefix il, suffix : ike
prefix ili, suffix : ke
prefix ilik, suffix : e
prefix ilike, suffix : 

如果你想生成两个索引之间的子字符串,使用以下方法:

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

“_compoundOffset”是什么?它代表该字符串到达该点之前的字节数。 - ArielSD
这种方法非常低效。它会在每次迭代中从起始索引偏移字符串。你应该只保留索引位置,并在每次迭代中获取索引(after:)。请注意,string[startIndex...endIndex] 会导致崩溃。顺便说一下,在 Swift 5 或更高版本中,你可以使用 PartialRangeFrom 下标来实现 let substring1 = str[str.startIndex...] - Leo Dabus

7
这在 Swift 中也是可以实现的,但需要更多代码行数。这里有一个函数 indexOf(),可以完成所需功能:
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"
}

这个函数并没有进行优化,但是对于短字符串来说它可以完成任务。


7
他们还没有把这个加入Swift库中,这真的让人非常沮丧! - Maury Markowitz
我正在向一个名为utils.swift的类中添加一个extension String,这将需要对所有其他类都可用。 - WestCoastProjects
顺便提一下,上面看起来在字符串长度方面是 O(N^2) 的时间复杂度..? - WestCoastProjects

5

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
    }

请注意,字符串索引不是基于整数的。您将无法使用它来对集合进行下标访问并访问元素(字符或子字符串)。 - Leo Dabus

4
这里有三个紧密相关的问题:
  • 在Cocoa NSString世界(Foundation)中,所有子字符串查找方法都已经过时了

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

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

考虑到这些问题,让我们思考如何编写:
func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
    // ?
}

在使用String Foundation方法查找字符串s中的子字符串s2时,我们得到了一个范围,但这个范围不是Foundation方法返回的NSRange类型,而是一个包装在Optional中的String.Index类型的范围(如果我们没有找到子字符串)。然而,另一个数字from是一个Int类型。因此,我们不能形成涉及两者的任何范围。

但是我们不必!我们所要做的就是使用一个以String.Index为参数的方法切掉原始字符串的结尾,并使用一个以Int为参数的方法切掉原始字符串的开头。幸运的是,这样的方法是存在的!像这样:

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
}

或者,如果您希望能够直接将此方法应用于字符串,可以像这样...
let output = "abcde".substring(from:0, toSubstring:"cd")

...然后将其作为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
    }
}

这是否复制了原始字符串?如果原始字符串很长,而且这是一个重复的操作怎么办?在jvm世界中这可以通过零数据复制来完成。 - WestCoastProjects
好的 - 我看到了 dropFirst,但还没有查看它的实现方式。我们如何将最终返回的 Substring 提取为一个 String?我看到一些超长的帖子只是关于这个问题的... - WestCoastProjects
只需强制转换为字符串即可。我不确定此时是否存在副本;只要未修改此字符串或原始字符串,可能不存在副本,但我对String采用写时复制的详细信息不清楚。 - matt
好的,谢谢 - 强制类型转换,我们开始吧。 当我执行 as! String 时,出现警告“从 Substring 转换为 String 的强制转换始终失败”。 - WestCoastProjects
哦!谢谢你。我甚至没有意识到在jvm上“cast”/“coercion”术语的区别。 - WestCoastProjects
显示剩余4条评论

3

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
        }
    }
}

测试:

 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)
    }

在字符串扩展中添加“String.”前缀是多余的。只需使用“SearchDirection”即可。另请注意,Swift是一种类型推断语言。如果结果类型不是泛型,则无需显式设置。 - Leo Dabus

2

Leo Dabus的回答很好。这里是基于他的回答,使用compactMap来避免Index out of range错误的方法。

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]

2
在Swift 3版本中,String没有像-这样的函数
str.index(of: String)

如果需要获取子字符串的索引,其中一种方法是获取范围。字符串中有以下函数返回范围 -

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

例如,在str中查找play的第一次出现的索引
var str = "play play play"
var range = str.range(of: "play")
range?.lowerBound //Result : 0
range?.upperBound //Result : 4

注意:range是可选的。如果无法找到字符串,它将使其为nil。例如:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接