Znajdowanie indeksu znaku w Swift String

203

Czas przyznać się do porażki ...

W Objective-C mógłbym użyć czegoś takiego:

NSString* str = @"abcdefghi";
[str rangeOfString:@"c"].location; // 2

W Swift widzę coś podobnego:

var str = "abcdefghi"
str.rangeOfString("c").startIndex

... ale to po prostu daje mi opcję String.Index, której mogę użyć, aby zapisać indeks z powrotem do oryginalnego ciągu, ale nie wyodrębnić lokalizacji.

FWIW, który String.Indexma prywatny ivar o nazwie _positiono poprawnej wartości. Po prostu nie widzę, jak to się dzieje

Wiem, że sam mógłbym łatwo dodać to do String. Jestem ciekaw, czego brakuje mi w tym nowym interfejsie API.

Matt Wilding
źródło
Oto projekt GitHub, który zawiera wiele metod rozszerzenia do manipulacji ciągami Swift: github.com/iamjono/SwiftString
RenniePet
Najlepsze wdrożenie, jakie znalazłem, znajduje się tutaj: stackoverflow.com/a/32306142/4550651
Carlos García
Czy musisz rozróżniać punkty kodowe Unicode od klastrów Grapheme Extended?
Ben Leggiero,

Odpowiedzi:

248

Nie jesteś jedynym, który nie mógł znaleźć rozwiązania.

Stringnie implementuje RandomAccessIndexType. Prawdopodobnie dlatego, że umożliwiają znaki o różnych długościach bajtów. Dlatego musimy użyć string.characters.count( countlub countElementsw Swift 1.x), aby uzyskać liczbę znaków. Dotyczy to również pozycji. Jest _positionto prawdopodobnie indeks do surowej tablicy bajtów i nie chcą tego ujawniać. Ma String.Indexto na celu ochronę nas przed dostępem do bajtów w środku znaków.

Oznacza to, że każdy indeks, który otrzymujesz, musi zostać utworzony z String.startIndexlub String.endIndex( String.Indeximplementuje BidirectionalIndexType). Wszelkie inne indeksy można tworzyć za pomocą successorlub predecessormetod.

Teraz, aby pomóc nam z indeksami, istnieje zestaw metod (funkcje w Swift 1.x):

Swift 4.x

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.index(text.startIndex, offsetBy: 2)
let lastChar2 = text[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 3.0

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.characters.index(text.characters.startIndex, offsetBy: 2)
let lastChar2 = text.characters[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 2.x

let text = "abc"
let index2 = text.startIndex.advancedBy(2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!
let lastChar2 = text.characters[index2] //will do the same as above

let range: Range<String.Index> = text.rangeOfString("b")!
let index: Int = text.startIndex.distanceTo(range.startIndex) //will call successor/predecessor several times until the indices match

Swift 1.x

let text = "abc"
let index2 = advance(text.startIndex, 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let range = text.rangeOfString("b")
let index: Int = distance(text.startIndex, range.startIndex) //will call succ/pred several times

Praca z String.Indexjest uciążliwa, ale używanie otoki do indeksowania według liczb całkowitych (patrz https://stackoverflow.com/a/25152652/669586 ) jest niebezpieczne, ponieważ ukrywa nieefektywność rzeczywistego indeksowania.

Należy pamiętać, że implementacja szybkiego indeksowania ma problem polegający na tym, że indeksy / zakresy utworzone dla jednego ciągu nie mogą być niezawodnie użyte dla innego ciągu , na przykład:

Swift 2.x

let text: String = "abc"
let text2: String = "🎾🏇🏈"

let range = text.rangeOfString("b")!

//can randomly return a bad substring or throw an exception
let substring: String = text2[range]

//the correct solution
let intIndex: Int = text.startIndex.distanceTo(range.startIndex)
let startIndex2 = text2.startIndex.advancedBy(intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]

Swift 1.x

let text: String = "abc"
let text2: String = "🎾🏇🏈"

let range = text.rangeOfString("b")

//can randomly return nil or a bad substring 
let substring: String = text2[range] 

//the correct solution
let intIndex: Int = distance(text.startIndex, range.startIndex)    
let startIndex2 = advance(text2.startIndex, intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]  
Sułtan
źródło
1
Może to niezręczne, ale wydaje się, że to odpowiedź. Mam nadzieję, że te dwie funkcje zakresu znajdą się w dokumentacji na jakiś czas przed ostatecznym wydaniem.
Matt Wilding
Jakiego typu jest rangewvar range = text.rangeOfString("b")
zaph
5
@Zaph Every Collectionma typealias IndexType. W przypadku tablic jest on zdefiniowany jako Int, Stringponieważ jest zdefiniowany jako String.Index. Zarówno tablice, jak i łańcuchy mogą również używać zakresów (do tworzenia podtablic i podciągów). Zakres jest specjalnym typem Range<T>. W przypadku ciągów znaków to Range<String.Index>tablice Range<Int>.
Sulthan
1
W Swift 2.0, distance(text.startIndex, range.startIndex)staje siętext.startIndex.distanceTo(range.startIndex)
superarts.org,
1
@devios String, dokładnie tak jak NSStringw Foundation ma metodę o nazwie hasPrefix(_:).
Sulthan,
86

Swift 3.0 sprawia, że ​​jest to bardziej szczegółowe:

let string = "Hello.World"
let needle: Character = "."
if let idx = string.characters.index(of: needle) {
    let pos = string.characters.distance(from: string.startIndex, to: idx)
    print("Found \(needle) at position \(pos)")
}
else {
    print("Not found")
}

Rozbudowa:

extension String {
    public func index(of char: Character) -> Int? {
        if let idx = characters.index(of: char) {
            return characters.distance(from: startIndex, to: idx)
        }
        return nil
    }
}

W Swift 2.0 stało się to łatwiejsze:

let string = "Hello.World"
let needle: Character = "."
if let idx = string.characters.indexOf(needle) {
    let pos = string.startIndex.distanceTo(idx)
    print("Found \(needle) at position \(pos)")
}
else {
    print("Not found")
}

Rozbudowa:

extension String {
    public func indexOfCharacter(char: Character) -> Int? {
        if let idx = self.characters.indexOf(char) {
            return self.startIndex.distanceTo(idx)
        }
        return nil
    }
}

Implementacja Swift 1.x:

Dla czystego rozwiązania Swift można użyć:

let string = "Hello.World"
let needle: Character = "."
if let idx = find(string, needle) {
    let pos = distance(string.startIndex, idx)
    println("Found \(needle) at position \(pos)")
}
else {
    println("Not found")
}

Jako rozszerzenie do String:

extension String {
    public func indexOfCharacter(char: Character) -> Int? {
        if let idx = find(self, char) {
            return distance(self.startIndex, idx)
        }
        return nil
    }
}
Pascal
źródło
1
znaki są przestarzałe !!
Shivam Pokhriyal
23
extension String {

    // MARK: - sub String
    func substringToIndex(index:Int) -> String {
        return self.substringToIndex(advance(self.startIndex, index))
    }
    func substringFromIndex(index:Int) -> String {
        return self.substringFromIndex(advance(self.startIndex, index))
    }
    func substringWithRange(range:Range<Int>) -> String {
        let start = advance(self.startIndex, range.startIndex)
        let end = advance(self.startIndex, range.endIndex)
        return self.substringWithRange(start..<end)
    }

    subscript(index:Int) -> Character{
        return self[advance(self.startIndex, index)]
    }
    subscript(range:Range<Int>) -> String {
        let start = advance(self.startIndex, range.startIndex)
            let end = advance(self.startIndex, range.endIndex)
            return self[start..<end]
    }


    // MARK: - replace
    func replaceCharactersInRange(range:Range<Int>, withString: String!) -> String {
        var result:NSMutableString = NSMutableString(string: self)
        result.replaceCharactersInRange(NSRange(range), withString: withString)
        return result
    }
}
huiquhome
źródło
7
Myślałem o zrobieniu tego, ale myślę, że problem polega na tym, że ukrywa semantykę dostępu do łańcucha. Wyobraź sobie utworzenie interfejsu API do uzyskiwania dostępu do list połączonych, który wygląda podobnie jak interfejs API dla tablicy. Ludzie chcieliby pisać strasznie nieefektywny kod.
Erik Engheim
16

Znalazłem to rozwiązanie dla swift2:

var str = "abcdefghi"
let indexForCharacterInString = str.characters.indexOf("c") //returns 2
VYT
źródło
co będzie indeksować, gdy str = "abcdefgchi"
Vatsal Shukla
10

Swift 5.0

public extension String {  
  func indexInt(of char: Character) -> Int? {
    return firstIndex(of: char)?.utf16Offset(in: self)
  }
}

Swift 4.0

public extension String {  
  func indexInt(of char: Character) -> Int? {
    return index(of: char)?.encodedOffset        
  }
}
Vincenso
źródło
return index (of: element) .map {target.distance (od: startIndex, do: $ 0)}
frogcjn
8

Nie jestem pewien, jak wyodrębnić pozycję z String.Index, ale jeśli chcesz oprzeć się o niektóre frameworki Objective-C, możesz połączyć się z celem-c i zrobić to tak, jak kiedyś.

"abcdefghi".bridgeToObjectiveC().rangeOfString("c").location

Wygląda na to, że niektóre metody NSString nie zostały (a może nie zostaną) przeniesione do String. Zawiera również przychodzi na myśl.

Connor
źródło
Wygląda na to, że dostęp do właściwości położenia zwracanej wartości wystarcza kompilatorowi do wnioskowania o typie NSString, więc bridgeToObjectiveC()wywołanie nie jest potrzebne. Mój problem wydaje się objawiać tylko podczas wywoływania rangeOfStringwcześniej istniejącego Swift String. Wygląda na problem z interfejsem API ...
Matt Wilding
to interesujące. Nie wiedziałem o tym w tych przypadkach. Cóż, gdy jest to już String, zawsze możesz użyć mostu.
Connor,
8

Oto czyste rozszerzenie String, które odpowiada na pytanie:

Swift 3:

extension String {
    var length:Int {
        return self.characters.count
    }

    func indexOf(target: String) -> Int? {

        let range = (self as NSString).range(of: target)

        guard range.toRange() != nil else {
            return nil
        }

        return range.location

    }
    func lastIndexOf(target: String) -> Int? {



        let range = (self as NSString).range(of: target, options: NSString.CompareOptions.backwards)

        guard range.toRange() != nil else {
            return nil
        }

        return self.length - range.location - 1

    }
    func contains(s: String) -> Bool {
        return (self.range(of: s) != nil) ? true : false
    }
}

Swift 2.2:

extension String {    
    var length:Int {
        return self.characters.count
    }

    func indexOf(target: String) -> Int? {

        let range = (self as NSString).rangeOfString(target)

        guard range.toRange() != nil else {
            return nil
        }

        return range.location

    }
    func lastIndexOf(target: String) -> Int? {



        let range = (self as NSString).rangeOfString(target, options: NSStringCompareOptions.BackwardsSearch)

        guard range.toRange() != nil else {
            return nil
        }

        return self.length - range.location - 1

    }
    func contains(s: String) -> Bool {
        return (self.rangeOfString(s) != nil) ? true : false
    }
}
YannSteph
źródło
7

Możesz także znaleźć indeksy znaku w jednym ciągu, takim jak ten,

extension String {

  func indexes(of character: String) -> [Int] {

    precondition(character.count == 1, "Must be single character")

    return self.enumerated().reduce([]) { partial, element  in
      if String(element.element) == character {
        return partial + [element.offset]
      }
      return partial
    }
  }

}

Co daje wynik w [String.Distance] tj. [Int], jak

"apple".indexes(of: "p") // [1, 2]
"element".indexes(of: "e") // [0, 2, 4]
"swift".indexes(of: "j") // []
Sandeep
źródło
5

Jeśli chcesz użyć znanego narzędzia NSString, możesz to wyraźnie zadeklarować:

var someString: NSString = "abcdefghi"

var someRange: NSRange = someString.rangeOfString("c")

Nie jestem jeszcze pewien, jak to zrobić w Swift.

Logan
źródło
1
To z pewnością działa i wydaje się, że kompilator jest dość agresywny w wnioskowaniu o typy NSString. Naprawdę liczyłem na czysty, szybki sposób na zrobienie tego, ponieważ wydaje się to dość powszechnym przypadkiem użycia.
Matt Wilding
Tak, rozglądam się, ale nie widzę tego. Możliwe, że skupili się na obszarach nieobsługiwanych przez ObjC, ponieważ mogą wypełnić te luki bez utraty zbyt wielu możliwości. Właśnie głośno myślę :)
Logan
4

Jeśli chcesz poznać stanowisko o charakterze w ciąg jako int wykorzystania wartości to:

let loc = newString.range(of: ".").location
Jakiś niemiecki programista
źródło
Swift 5: value of type 'Range<String.Index>?' has no member 'location'.
Neph
3

Wiem, że jest stary i odpowiedź została zaakceptowana, ale indeks ciągu można znaleźć w kilku wierszach kodu, używając:

var str : String = "abcdefghi"
let characterToFind: Character = "c"
let characterIndex = find(str, characterToFind)  //returns 2

Kilka innych świetnych informacji na temat łańcuchów Swift tutaj Strings in Swift

Jacek
źródło
findnie jest dostępny w Swift 3
AamirR
2

To zadziałało dla mnie

var loc = "abcdefghi".rangeOfString("c").location
NSLog("%d", loc);

to też działało

var myRange: NSRange = "abcdefghi".rangeOfString("c")
var loc = myRange.location
NSLog("%d", loc);
rdelmar
źródło
Oba wydają się w jakiś sposób opierać na zachowaniu NSString. Na moim placu zabaw użyłem zmiennej pośredniej dla łańcucha. var str = "abcdef"; str.rangeOfString("c").locationzgłasza błąd związany z brakiem elementu String.Index o nazwie lokalizacja ...
Matt Wilding
1
Działa tylko dlatego, że „string” to NSString, a nie Swift's String
gderaco
2

Typ zmiennej String w Swift zawiera inne funkcje w porównaniu do NSString w Objective-C. I jak wspomniał Sulthan,

Swift String nie implementuje RandomAccessIndex

Możesz sprowadzić zmienną typu String do NSString (jest to poprawne w Swift). Umożliwi to dostęp do funkcji w NSString.

var str = "abcdefghi" as NSString
str.rangeOfString("c").locationx   // returns 2
Alexander G.
źródło
2

Jeśli się nad tym zastanowisz, tak naprawdę nie potrzebujesz dokładnej wersji Int lokalizacji. Range, a nawet String.Index wystarczy, aby w razie potrzeby ponownie pobrać podciąg:

let myString = "hello"

let rangeOfE = myString.rangeOfString("e")

if let rangeOfE = rangeOfE {
    myString.substringWithRange(rangeOfE) // e
    myString[rangeOfE] // e

    // if you do want to create your own range
    // you can keep the index as a String.Index type
    let index = rangeOfE.startIndex
    myString.substringWithRange(Range<String.Index>(start: index, end: advance(index, 1))) // e

    // if you really really need the 
    // Int version of the index:
    let numericIndex = distance(index, advance(index, 1)) // 1 (type Int)
}
NatashaTheRobot
źródło
Najlepszą odpowiedzią dla mnie jest to, że func indexOf (target: String) -> Int {return (self as NSString) .rangeOfString (target) .location}
YannSteph
2

Najprostszym sposobem jest:

W Swift 3 :

 var textViewString:String = "HelloWorld2016"
    guard let index = textViewString.characters.index(of: "W") else { return }
    let mentionPosition = textViewString.distance(from: index, to: textViewString.endIndex)
    print(mentionPosition)
Pragnesh Vitthani
źródło
1

String jest typem mostka dla NSString, więc dodaj

import Cocoa

do szybkiego pliku i użyj wszystkich „starych” metod.

IgnazioC
źródło
1

Pod względem myślenia można to nazwać INWERSJĄ. Odkrywasz, że świat jest okrągły zamiast płaskiego. „Naprawdę nie musisz znać INDEKSU postaci, aby coś z tym zrobić”. A jako programista C też ciężko mi było to wziąć! Twój wiersz „let index = letters.characters.indexOf („ c ”)!” sam w sobie wystarcza. Na przykład, aby usunąć c, którego możesz użyć ... (wklej na placu zabaw)

    var letters = "abcdefg"
  //let index = letters.rangeOfString("c")!.startIndex //is the same as
    let index = letters.characters.indexOf("c")!
    range = letters.characters.indexOf("c")!...letters.characters.indexOf("c")!
    letters.removeRange(range)
    letters

Jeśli jednak potrzebujesz indeksu, musisz zwrócić rzeczywisty INDEKS, a nie wartość Int, ponieważ wartość Int wymagałaby dodatkowych kroków w celu praktycznego zastosowania. Te rozszerzenia zwracają indeks, liczbę określonych znaków i zakres, który pokaże ten kod wtyczki placu zabaw.

extension String
{
    public func firstIndexOfCharacter(aCharacter: Character) -> String.CharacterView.Index? {

        for index in self.characters.indices {
            if self[index] == aCharacter {
                return index
            }

        }
        return nil
    }

    public func returnCountOfThisCharacterInString(aCharacter: Character) -> Int? {

        var count = 0
        for letters in self.characters{

            if aCharacter == letters{

                count++
            }
        }
        return count
    }


    public func rangeToCharacterFromStart(aCharacter: Character) -> Range<Index>? {

        for index in self.characters.indices {
            if self[index] == aCharacter {
                let range = self.startIndex...index
                return range
            }

        }
        return nil
    }

}



var MyLittleString = "MyVery:important String"

var theIndex = MyLittleString.firstIndexOfCharacter(":")

var countOfColons = MyLittleString.returnCountOfThisCharacterInString(":")

var theCharacterAtIndex:Character = MyLittleString[theIndex!]

var theRange = MyLittleString.rangeToCharacterFromStart(":")
MyLittleString.removeRange(theRange!)
Góral
źródło
1

Kompletne rozwiązanie Swift 4:

OffsetIndexableCollection (ciąg znaków przy użyciu indeksu Int)

https://github.com/frogcjn/OffsetIndexableCollection-String-Int-Indexable-

let a = "01234"

print(a[0]) // 0
print(a[0...4]) // 01234
print(a[...]) // 01234

print(a[..<2]) // 01
print(a[...2]) // 012
print(a[2...]) // 234
print(a[2...3]) // 23
print(a[2...2]) // 2

if let number = a.index(of: "1") {
    print(number) // 1
    print(a[number...]) // 1234
}

if let number = a.index(where: { $0 > "1" }) {
    print(number) // 2
}
frogcjn
źródło
1

rozszerzenie String {

//Fucntion to get the index of a particular string
func index(of target: String) -> Int? {
    if let range = self.range(of: target) {
        return characters.distance(from: startIndex, to: range.lowerBound)
    } else {
        return nil
    }
}
//Fucntion to get the last index of occurence of a given string
func lastIndex(of target: String) -> Int? {
    if let range = self.range(of: target, options: .backwards) {
        return characters.distance(from: startIndex, to: range.lowerBound)
    } else {
        return nil
    }
}

}

Uddiptta Ujjawal
źródło
1

Szybki 5

Znajdź indeks podciągów

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

Znajdź indeks postaci

let str = "abcdecd"
if let firstIndex = str.firstIndex(of: "c") {
    let index: Int = str.distance(from: str.startIndex, to: firstIndex)
    print("index: ", index)   //index: 2
}
else {
    print("symbol not found")
}
Viktor
źródło
0

Aby uzyskać indeks podłańcucha w ciągu za pomocą Swift 2:

let text = "abc"
if let range = text.rangeOfString("b") {
   var index: Int = text.startIndex.distanceTo(range.startIndex) 
   ...
}
Tony
źródło
0

W szybkim 2.0

var stringMe="Something In this.World"
var needle="."
if let idx = stringMe.characters.indexOf(needle) {
    let pos=stringMe.substringFromIndex(idx)
    print("Found \(needle) at position \(pos)")
}
else {
    print("Not found")
}
Yoosaf Abdulla
źródło
0
let mystring:String = "indeep";
let findCharacter:Character = "d";

if (mystring.characters.contains(findCharacter))
{
    let position = mystring.characters.indexOf(findCharacter);
    NSLog("Position of c is \(mystring.startIndex.distanceTo(position!))")

}
else
{
    NSLog("Position of c is not found");
}
W głębokich
źródło
0

Gram z podążaniem

extension String {
    func allCharactes() -> [Character] {
         var result: [Character] = []
         for c in self.characters {
             result.append(c)
         }
         return 
    }
}

dopóki nie zrozumiem, że pod warunkiem, że teraz jest to tylko tablica postaci

i z

let c = Array(str.characters)
Peter Ahlberg
źródło
Co to osiąga? Jak Twój pierwszy blok kodu jest lepszy od drugiego?
Ben Leggiero,
0

Jeśli potrzebujesz tylko indeksu znaku, najprostszym i najszybszym rozwiązaniem (jak już wskazał Pascal) jest:

let index = string.characters.index(of: ".")
let intIndex = string.distance(from: string.startIndex, to: index)
Markymark
źródło
Znaki są teraz amortyzowane ... niestety, nawet jeśli to działa
użytkownik3069232
0

Jeśli chodzi o przekształcenie String.Indexw an Int, to rozszerzenie działa dla mnie:

public extension Int {
    /// Creates an `Int` from a given index in a given string
    ///
    /// - Parameters:
    ///    - index:  The index to convert to an `Int`
    ///    - string: The string from which `index` came
    init(_ index: String.Index, in string: String) {
        self.init(string.distance(from: string.startIndex, to: index))
    }
}

Przykładowe użycie związane z tym pytaniem:

var testString = "abcdefg"

Int(testString.range(of: "c")!.lowerBound, in: testString)     // 2

testString = "🇨🇦🇺🇸🇩🇪👩‍👩‍👧‍👦\u{1112}\u{1161}\u{11AB}"

Int(testString.range(of: "🇨🇦🇺🇸🇩🇪")!.lowerBound, in: testString) // 0
Int(testString.range(of: "👩‍👩‍👧‍👦")!.lowerBound, in: testString)    // 1
Int(testString.range(of: "한")!.lowerBound, in: testString)    // 5

Ważny:

Jak widać, grupuje rozszerzone klastry grafem i łączy postacie inaczej niż String.Index. Oczywiście właśnie dlatego String.Index. Należy pamiętać, że ta metoda traktuje klastry jako znaki pojedyncze, co jest bliższe do poprawienia. Jeśli Twoim celem jest podzielenie łańcucha według punktu kodowego Unicode, nie jest to rozwiązanie dla Ciebie.

Ben Leggiero
źródło
0

W Swift 2.0 następująca funkcja zwraca podłańcuch przed danym znakiem.

func substring(before sub: String) -> String {
    if let range = self.rangeOfString(sub),
        let index: Int = self.startIndex.distanceTo(range.startIndex) {
        return sub_range(0, index)
    }
    return ""
}
superarts.org
źródło
0

Możesz znaleźć numer indeksu znaku w ciągu:

var str = "abcdefghi"
if let index = str.firstIndex(of: "c") {
    let distance = str.distance(from: str.startIndex, to: index)
    // distance is 2
}
Ale Mohamad
źródło
0

Z mojej perspektywy, lepszy sposób na poznanie samej logiki jest poniżej

 let testStr: String = "I love my family if you Love us to tell us I'm with you"
 var newStr = ""
 let char:Character = "i"

 for value in testStr {
      if value == char {
         newStr = newStr + String(value)
   }

}
print(newStr.count)
Deepak Yadeedya
źródło