Recherche de l'index du caractère dans la chaîne Swift


203

Il est temps d'admettre la défaite ...

Dans Objective-C, je pourrais utiliser quelque chose comme:

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

Dans Swift, je vois quelque chose de similaire:

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

... mais cela me donne juste un String.Index, que je peux utiliser pour réinscrire dans la chaîne d'origine, mais pas extraire un emplacement de.

FWIW, qui String.Indexa un ivar privé appelé _positionqui a la bonne valeur. Je ne vois tout simplement pas comment cela est exposé.

Je sais que je pourrais facilement ajouter ceci à String moi-même. Je suis plus curieux de savoir ce qui me manque dans cette nouvelle API.


Voici un projet GitHub qui contient de nombreuses méthodes d'extension pour la manipulation de chaînes Swift: github.com/iamjono/SwiftString
RenniePet

La meilleure implémentation que j'ai trouvée est ici: stackoverflow.com/a/32306142/4550651
Carlos García

Avez-vous besoin de faire la distinction entre les points de code Unicode et les grappes de graphèmes étendues?
Ben Leggiero

Réponses:


248

Vous n'êtes pas le seul à ne pas trouver la solution.

Stringne met pas en œuvre RandomAccessIndexType. Probablement parce qu'ils permettent des caractères avec des longueurs d'octets différentes. C'est pourquoi nous devons utiliser string.characters.count( countou countElementsdans Swift 1.x) pour obtenir le nombre de caractères. Cela s'applique également aux postes. Il _positions'agit probablement d'un index dans le tableau brut d'octets et ils ne veulent pas l'exposer. Le String.Indexest destiné à nous protéger de l'accès aux octets au milieu des caractères.

Cela signifie que tout index que vous obtenez doit être créé à partir de String.startIndexou String.endIndex( String.Indeximplémente BidirectionalIndexType). Tout autre indice peut être créé à l'aide des méthodes successorou predecessor.

Maintenant, pour nous aider avec les indices, il existe un ensemble de méthodes (fonctions dans 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

Travailler avec String.Indexest lourd mais utiliser un wrapper pour indexer par des entiers (voir https://stackoverflow.com/a/25152652/669586 ) est dangereux car il cache l'inefficacité d'une véritable indexation.

Notez que l'implémentation de l'indexation Swift a le problème que les index / plages créés pour une chaîne ne peuvent pas être utilisés de manière fiable pour une chaîne différente , par exemple:

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]  

1
Cela peut sembler gênant, cela semble être la réponse. Espérons que ces deux fonctions de gamme seront intégrées dans la documentation quelque temps avant la version finale.
Matt Wilding

De quel type est rangeenvar range = text.rangeOfString("b")
Zaph

5
@Zaph Every Collectiona un typealias IndexType. Pour les tableaux, il est défini comme Int, car Stringil est défini comme String.Index. Les tableaux et les chaînes peuvent également utiliser des plages (pour créer des sous-réseaux et des sous-chaînes). La gamme est un type spécial Range<T>. Pour les chaînes, c'est Range<String.Index>pour les tableaux Range<Int>.
Sulthan

1
In Swift 2.0, distance(text.startIndex, range.startIndex)devienttext.startIndex.distanceTo(range.startIndex)
superarts.org

1
@devios String, exactement comme NSStringdans Foundation, a une méthode appelée hasPrefix(_:).
Sulthan

86

Swift 3.0 rend cela un peu plus détaillé:

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

Extension:

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

Dans Swift 2.0, cela est devenu plus facile:

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

Extension:

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

Implémentation de Swift 1.x:

Pour une solution Swift pure, on peut utiliser:

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

En extension à String:

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

1
caractères est obsolète !!
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
    }
}

7
J'ai pensé à faire cela, mais je pense que c'est un problème qu'il cache la sémantique de l'accès aux chaînes. Imaginez que vous créez une API pour accéder aux listes liées qui ressemble à l'API d'un tableau. Les gens aimeraient écrire du code horriblement inefficace.
Erik Engheim

16

J'ai trouvé cette solution pour swift2:

var str = "abcdefghi"
let indexForCharacterInString = str.characters.indexOf("c") //returns 2

quel sera l'index lorsque 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        
  }
}

return index (of: element) .map {target.distance (from: startIndex, to: $ 0)}
frogcjn

8

Je ne sais pas comment extraire la position de String.Index, mais si vous êtes prêt à vous rabattre sur certains cadres Objective-C, vous pouvez passer à objective-c et le faire de la même manière que vous le faisiez auparavant.

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

Il semble que certaines méthodes NSString n'aient pas encore été (ou ne seront peut-être pas) portées sur String. Contient aussi vient à l'esprit.


Il apparaît en fait que l'accès à la propriété location de la valeur de retour est suffisant pour que le compilateur infère un type NSString, donc l' bridgeToObjectiveC()appel n'est pas nécessaire. Mon problème semble se manifester uniquement lors de l'appel rangeOfStringà une chaîne Swift existante. Cela ressemble à un problème d'API ...
Matt Wilding

c'est intéressant. Je ne savais pas que cela était inféré dans ces cas. Eh bien, quand c'est déjà une chaîne, vous pouvez toujours utiliser le pont.
Connor

8

Voici une extension de chaîne propre qui répond à la question:

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

7

Vous pouvez également trouver des index d'un caractère dans une seule chaîne comme celle-ci,

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

}

Ce qui donne le résultat dans [String.Distance] ie. [Int], comme

"apple".indexes(of: "p") // [1, 2]
"element".indexes(of: "e") // [0, 2, 4]
"swift".indexes(of: "j") // []

5

Si vous souhaitez utiliser NSString familier, vous pouvez le déclarer explicitement:

var someString: NSString = "abcdefghi"

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

Je ne sais pas encore comment faire cela dans Swift.


1
Cela fonctionne certainement, et il semble que le compilateur soit assez agressif pour déduire les types NSString pour vous. J'espérais vraiment une méthode Swift pure, car cela semble être un cas d'utilisation assez courant.
Matt Wilding

Oui, je regarde autour de moi, mais je ne le vois pas. Il est possible qu'ils se soient concentrés sur des domaines non pris en charge par ObjC car ils peuvent combler ces lacunes sans trop de capacités perdues. Je pense juste à haute voix :)
Logan

4

Si vous voulez connaître la position d'un caractère dans une chaîne en tant que valeur int , utilisez ceci:

let loc = newString.range(of: ".").location

Swift 5: value of type 'Range<String.Index>?' has no member 'location'.
Neph

3

Je sais que cela est ancien et qu'une réponse a été acceptée, mais vous pouvez trouver l'index de la chaîne dans quelques lignes de code en utilisant:

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

Quelques autres bonnes informations sur les cordes Swift ici Strings in Swift


findn'est pas disponible dans Swift 3
AamirR

2

Cela a fonctionné pour moi,

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

cela a fonctionné aussi,

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

Les deux semblent se replier sur le comportement de NSString d'une manière ou d'une autre. Dans mon terrain de jeu, j'utilisais une variable intermédiaire pour la chaîne. var str = "abcdef"; str.rangeOfString("c").locationsoulève une erreur à propos de String.Index n'ayant pas de membre nommé location ...
Matt Wilding

1
Cela fonctionne simplement parce que "string" est NSString et non Swift's String
gderaco

2

La chaîne de type variable dans Swift contient des fonctions différentes par rapport à NSString dans Objective-C. Et comme Sulthan l'a mentionné,

Swift String n'implémente pas RandomAccessIndex

Ce que vous pouvez faire est de convertir votre variable de type String en NSString (ceci est valide dans Swift). Cela vous donnera accès aux fonctions de NSString.

var str = "abcdefghi" as NSString
str.rangeOfString("c").locationx   // returns 2

2

Si vous y réfléchissez, vous n'avez en fait pas vraiment besoin de la version Int exacte de l'emplacement. La plage ou même le String.Index est suffisant pour extraire à nouveau la sous-chaîne si nécessaire:

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

La meilleure réponse pour moi est que func indexOf (target: String) -> Int {return (self as NSString) .rangeOfString (target) .location}
YannSteph

2

La manière la plus simple est:

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

1

La chaîne est un type de pont pour NSString, alors ajoutez

import Cocoa

à votre fichier rapide et utilisez toutes les "anciennes" méthodes.


1

En termes de réflexion, cela pourrait être appelé une INVERSION. Vous découvrez que le monde est rond au lieu d'être plat. "Vous n'avez pas vraiment besoin de connaître l'INDEX du personnage pour faire des choses avec." Et en tant que programmeur C, j'ai trouvé cela difficile à prendre aussi! Votre ligne "let index = letters.characters.indexOf (" c ")!" est suffisant en soi. Par exemple, pour supprimer le c que vous pourriez utiliser ... (coller le terrain de jeu)

    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

Cependant, si vous voulez un index, vous devez renvoyer un INDEX réel et non un Int, car une valeur Int nécessiterait des étapes supplémentaires pour toute utilisation pratique. Ces extensions renvoient un index, un décompte d'un caractère spécifique et une plage que ce code plug-in capable de jeu va démontrer.

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

1

Solution complète Swift 4:

OffsetIndexableCollection (chaîne utilisant l'index 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
}

1

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

}


1

Swift 5

Trouver l'index de la sous-chaîne

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

Trouver l'index du personnage

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


0

Pour obtenir l'index d'une sous-chaîne dans une chaîne avec Swift 2:

let text = "abc"
if let range = text.rangeOfString("b") {
   var index: Int = text.startIndex.distanceTo(range.startIndex) 
   ...
}

0

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

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

0

Je joue avec les suivants

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

jusqu'à ce que je comprenne celui fourni maintenant c'est juste un tableau de caractères

et avec

let c = Array(str.characters)

Qu'est-ce que cela accomplit? Comment votre premier bloc de code est-il meilleur que le second?
Ben Leggiero

0

Si vous n'avez besoin que de l'index d'un caractère, la solution la plus simple et rapide (comme déjà souligné par Pascal) est:

let index = string.characters.index(of: ".")
let intIndex = string.distance(from: string.startIndex, to: index)

les caractères sont dépréciés maintenant ... malheureusement, même si cela fonctionne
user3069232

0

Au sujet de transformer un String.Indexen un Int, cette extension fonctionne pour moi:

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

Exemple d'utilisation correspondant à cette question:

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

Important:

Comme vous pouvez le constater, il regroupe les grappes de graphèmes étendues et les caractères joints différemment de String.Index. Bien sûr, c'est pourquoi nous l'avons fait String.Index. Vous devez garder à l'esprit que cette méthode considère les clusters comme des caractères singuliers, ce qui est plus proche de la correction. Si votre objectif est de diviser une chaîne par point de code Unicode, ce n'est pas la solution pour vous.


0

Dans Swift 2.0 , la fonction suivante renvoie une sous-chaîne avant un caractère donné.

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

0

Vous pouvez trouver le numéro d'index d'un caractère dans une chaîne avec ceci:

var str = "abcdefghi"
if let index = str.firstIndex(of: "c") {
    let distance = str.distance(from: str.startIndex, to: index)
    // distance is 2
}

0

Comme mon point de vue, la meilleure façon de connaître la logique elle-même est ci-dessous

 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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.