Localizando o índice de caractere na Swift String

203

É hora de admitir a derrota ...

No Objective-C, eu poderia usar algo como:

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

Em Swift, vejo algo semelhante:

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

... mas isso me dá um String.Index, que eu posso usar para subscrever de volta na string original, mas não extrair um local.

FWIW, que String.Indexpossui um ivar privado chamado _positionque possui o valor correto. Só não vejo como é exposto.

Eu sei que poderia adicionar isso facilmente a String. Estou mais curioso sobre o que estou perdendo nesta nova API.

Matt Wilding
fonte
Aqui está um projeto GitHub que contém uma grande quantidade de métodos de extensão para string de manipulação Swift: github.com/iamjono/SwiftString
RenniePet
A melhor implementação que encontrei está aqui: stackoverflow.com/a/32306142/4550651 #
Carlos García
Você precisa distinguir entre pontos de código Unicode e clusters de Grapheme estendido?
precisa saber é o seguinte

Respostas:

248

Você não é o único que não conseguiu encontrar a solução.

Stringnão implementa RandomAccessIndexType. Provavelmente porque eles habilitam caracteres com diferentes comprimentos de bytes. É por isso que precisamos usar string.characters.count( countou countElementsno Swift 1.x) para obter o número de caracteres. Isso também se aplica a posições. O _positioné provavelmente um índice para a matriz matéria de bytes e eles não querem expor isso. O String.Indexobjetivo é proteger-nos do acesso a bytes no meio de caracteres.

Isso significa que qualquer índice que obtiver devem ser criados a partir de String.startIndexou String.endIndex( String.Indeximplementos BidirectionalIndexType). Quaisquer outros índices podem ser criados usando métodos successorou predecessor.

Agora, para nos ajudar com os índices, há um conjunto de métodos (funções no 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

Trabalhar com String.Indexé complicado, mas usar um wrapper para indexar por números inteiros (consulte https://stackoverflow.com/a/25152652/669586 ) é perigoso porque oculta a ineficiência da indexação real.

Observe que a implementação da indexação Swift tem o problema de que índices / intervalos criados para uma sequência não podem ser usados ​​com segurança para uma sequência diferente , por exemplo:

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]  
Sulthan
fonte
1
Por mais estranho que fosse, essa parece ser a resposta. Esperamos que essas duas funções de alcance entrem na documentação algum tempo antes do lançamento final.
Matt Wilding
Que tipo está rangedentrovar range = text.rangeOfString("b")
Zaph 14/06
5
@ Zaph Every Collectiontem um typealias IndexType. Para matrizes, é definido como Int, Stringpois é definido como String.Index. Matrizes e seqüências de caracteres também podem usar intervalos (para criar sub-matrizes e substrings). O intervalo é um tipo especial Range<T>. Para strings, é Range<String.Index>, para matrizes Range<Int>.
precisa
1
Em Swift 2.0, distance(text.startIndex, range.startIndex)tornatext.startIndex.distanceTo(range.startIndex)
superarts.org 10/10
1
@ devios String, exatamente como NSStringno Foundation, tem um método chamado hasPrefix(_:).
Sulthan
86

O Swift 3.0 torna isso um pouco mais detalhado:

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

Extensão:

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

No Swift 2.0, isso ficou mais fácil:

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

Extensão:

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

Implementação do Swift 1.x:

Para uma solução Swift pura, pode-se usar:

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

Como uma extensão para String:

extension String {
    public func indexOfCharacter(char: Character) -> Int? {
        if let idx = find(self, char) {
            return distance(self.startIndex, idx)
        }
        return nil
    }
}
Pascal
fonte
1
caracteres está obsoleto !!
Shivam Pokhriyal 13/11
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
fonte
7
Pensei em fazer isso, mas acho que é um problema que oculte a semântica do acesso a strings. Imagine criar uma API para acessar listas vinculadas que se parece com a API de uma matriz. As pessoas gostariam de escrever código terrivelmente ineficiente.
precisa saber é o seguinte
16

Eu encontrei esta solução para swift2:

var str = "abcdefghi"
let indexForCharacterInString = str.characters.indexOf("c") //returns 2
VYT
fonte
qual será o índice quando 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
fonte
return index (of: element) .map {target.distance (from: startIndex, to: $ 0)}
frogcjn
8

Não tenho certeza de como extrair a posição de String.Index, mas se você estiver disposto a recorrer a algumas estruturas de Objective-C, poderá fazer a ponte para a objetiva-c e fazê-lo da mesma maneira que costumava.

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

Parece que alguns métodos NSString ainda não foram (ou talvez não serão) portados para String. Contém também vem à mente.

Connor
fonte
Na verdade, parece que acessar a propriedade location do valor de retorno é suficiente para o compilador inferir um tipo NSString; portanto, a bridgeToObjectiveC()chamada não é necessária. Meu problema parece se manifestar apenas ao chamar rangeOfStringa Swift String existente anteriormente. Parece um problema API ...
Matt Wilding
isso é interessante. Eu não sabia disso inferido nesses casos. Bem, quando já é uma String, você sempre pode usar a ponte.
Connor
8

Aqui está uma extensão limpa de String que responde à pergunta:

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

Você também pode encontrar índices de um caractere em uma única string como esta,

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

}

O que fornece o resultado em [String.Distance] ou seja. [Int], como

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

Se você deseja usar o NSString familiar, pode declará-lo explicitamente:

var someString: NSString = "abcdefghi"

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

Ainda não tenho certeza de como fazer isso no Swift.

Logan
fonte
1
Isso certamente funciona e parece que o compilador é bastante agressivo ao deduzir tipos de NSString para você. Eu realmente esperava uma maneira pura e rápida de fazer isso, já que parece um caso de uso bastante comum.
Matt Wilding
Sim, estou olhando em volta, mas não vejo. É possível que eles se concentrassem em áreas não suportadas pela ObjC, porque podem preencher essas lacunas sem muitos recursos perdidos. Apenas pensando em voz alta :)
Logan
4

Se você deseja conhecer a posição de um caractere em uma string como um valor int, use isto:

let loc = newString.range(of: ".").location
Algum desenvolvedor alemão
fonte
Swift 5: value of type 'Range<String.Index>?' has no member 'location'.
Neph 16/07/19
3

Eu sei que isso é antigo e uma resposta foi aceita, mas você pode encontrar o índice da string em algumas linhas de código usando:

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

Algumas outras ótimas informações sobre cordas Swift aqui Cordas em Swift

Jack
fonte
findnão está disponível no Swift 3
AamirR
2

Isso funcionou para mim,

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

isso funcionou também,

var myRange: NSRange = "abcdefghi".rangeOfString("c")
var loc = myRange.location
NSLog("%d", loc);
rdelmar
fonte
Ambos parecem ter recorrido ao comportamento do NSString de alguma forma. No meu playground, eu estava usando uma variável intermediária para a string. var str = "abcdef"; str.rangeOfString("c").locationlevanta um erro sobre String.Index não ter um membro do local nomeado ...
Matt Wilding
1
Ele funciona apenas porque "string" é NSString e não da Swift Cordas
gderaco
2

O tipo variável String no Swift contém funções diferentes em comparação com o NSString no Objective-C. E como Sulthan mencionou,

Swift String não implementa RandomAccessIndex

O que você pode fazer é baixar sua variável do tipo String para NSString (isso é válido no Swift). Isso lhe dará acesso às funções no NSString.

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

Se você pensar bem, na verdade não precisa da versão Int exata do local. O Range ou mesmo o String.Index são suficientes para obter a substring novamente, se necessário:

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
fonte
A melhor resposta para mim é que func indexOf (target: String) -> Int {return (self como NSString) .rangeOfString (alvo) .location}
YannSteph
2

A maneira mais simples é:

No 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
fonte
1

String é um tipo de ponte para NSString, então adicione

import Cocoa

ao seu arquivo rápido e use todos os métodos "antigos".

IgnazioC
fonte
1

Em termos de pensamento, isso pode ser chamado de INVERSÃO. Você descobre que o mundo é redondo em vez de plano. "Você realmente não precisa conhecer o ÍNDICE do personagem para fazer as coisas com ele." E como programador em C também achei difícil de aguentar! Sua linha "deixe index = letters.characters.indexOf (" c ")!" é suficiente por si só. Por exemplo, para remover oc você pode usar ... (colar no playground)

    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

No entanto, se você deseja um índice, você precisa retornar um índice real, não um Int como valor Int, exigiria etapas adicionais para qualquer uso prático. Essas extensões retornam um índice, uma contagem de um caractere específico e um intervalo que esse código compatível com plug-in de playground demonstrará.

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!)
Homem da montanha
fonte
1

Solução completa Swift 4:

OffsetIndexableCollection (String usando o Índice 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
fonte
1

extensão 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
fonte
1

Swift 5

Encontrar índice de 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")
}

Encontrar índice de caracteres

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
fonte
0

Para obter o índice de uma substring em uma string com o Swift 2:

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

In 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")
}
Yoosaf Abdulla
fonte
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");
}
Profundo
fonte
0

Eu brinco com o seguinte

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

até que eu entenda o fornecido agora é apenas uma matriz de caracteres

e com

let c = Array(str.characters)
Peter Ahlberg
fonte
O que isso faz? Como o seu primeiro bloco de código é melhor que o segundo?
precisa saber é o seguinte
0

Se você precisa apenas do índice de um caractere, a solução mais simples e rápida (como já indicado por Pascal) é:

let index = string.characters.index(of: ".")
let intIndex = string.distance(from: string.startIndex, to: index)
Markymark
fonte
caracteres é depreciado agora ... infelizmente, mesmo que isso funciona
user3069232
0

Sobre o assunto de transformar um String.Indexem um Int, esta extensão funciona para mim:

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

Exemplo de uso relevante para esta pergunta:

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

Importante:

Como você pode ver, ele agrupa grupos de grafemas estendidos e une os caracteres de maneira diferente String.Index. Claro, é por isso que temos String.Index. Você deve ter em mente que esse método considera os clusters como caracteres singulares, o que está mais próximo de corrigir. Se seu objetivo é dividir uma string pelo ponto de código Unicode, essa não é a solução para você.

Ben Leggiero
fonte
0

No Swift 2.0 , a função a seguir retorna uma substring antes de um determinado caractere.

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
fonte
0

Você pode encontrar o número de índice de um caractere em uma string com este:

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

Na minha perspectiva, a melhor maneira de conhecer a própria lógica está abaixo

 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
fonte