Encontrar el índice del personaje en Swift String


203

Es hora de admitir la derrota ...

En Objective-C, podría usar algo como:

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

En Swift, veo algo similar:

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

... pero eso solo me da un String.Index, que puedo usar para volver a subíndice en la cadena original, pero no extraer una ubicación de.

FWIW, que String.Indextiene un ivar privado llamado _positionque tiene el valor correcto. Simplemente no veo cómo está expuesto.

Sé que podría agregar esto fácilmente a String. Tengo más curiosidad sobre lo que me falta en esta nueva API.


Aquí hay un proyecto de GitHub que contiene muchos métodos de extensión para la manipulación de cadenas Swift: github.com/iamjono/SwiftString
RenniePet

La mejor implementación que he encontrado está aquí: stackoverflow.com/a/32306142/4550651
Carlos García

¿Necesita distinguir entre los puntos de código Unicode y los grupos de grafemas extendidos?
Ben Leggiero

Respuestas:


248

No eres el único que no pudo encontrar la solución.

Stringno implementa RandomAccessIndexType. Probablemente porque habilitan caracteres con diferentes longitudes de bytes. Es por eso que tenemos que usar string.characters.count( counto countElementsen Swift 1.x) para obtener el número de caracteres. Eso también se aplica a los puestos. El _positiones probablemente un índice en la matriz de bytes en bruto y que no quieren exponer a eso. El String.Indexobjetivo es protegernos del acceso a bytes en medio de los caracteres.

Eso significa que cualquier índice que obtenga debe crearse desde String.startIndexo String.endIndex( String.Indeximplementa BidirectionalIndexType). Cualquier otro índice se puede crear usando successoro predecessormétodos.

Ahora, para ayudarnos con los índices, hay un conjunto de métodos (funciones en 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

Trabajar con String.Indexes engorroso, pero usar un contenedor para indexar por enteros (ver https://stackoverflow.com/a/25152652/669586 ) es peligroso porque oculta la ineficiencia de la indexación real.

Tenga en cuenta que la implementación de indexación Swift tiene el problema de que los índices / rangos creados para una cadena no pueden usarse de manera confiable para una cadena diferente , por ejemplo:

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
Aunque parezca incómodo, esta parece ser la respuesta. Esperemos que esas dos funciones de rango lleguen a la documentación en algún momento antes del lanzamiento final.
Matt Wilding

¿Qué tipo es rangeenvar range = text.rangeOfString("b")
Zaph

55
@Zaph Every Collectiontiene un typealias IndexType. Para las matrices, se define como Int, porque Stringse define como String.Index. Tanto las matrices como las cadenas también pueden usar rangos (para crear submatrices y subcadenas). La gama es un tipo especial Range<T>. Para cadenas, es Range<String.Index>, para matrices Range<Int>.
Sulthan

1
En Swift 2.0, se distance(text.startIndex, range.startIndex)conviertetext.startIndex.distanceTo(range.startIndex)
superarts.org

1
@devios String, exactamente como NSStringen Foundation tiene un método llamado hasPrefix(_:).
Sulthan

86

Swift 3.0 hace esto un poco más detallado:

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

Extensión:

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

En Swift 2.0 esto se ha vuelto más 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")
}

Extensión:

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

Implementación Swift 1.x:

Para una solución Swift pura se puede 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 una extensión para String:

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

1
¡los personajes están en desuso!
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
    }
}

77
Pensé en hacer esto, pero creo que es un problema que oculta la semántica del acceso a cadenas. Imagine crear una API para acceder a listas vinculadas que se parece a la API de una matriz. A la gente le gustaría escribir código horriblemente ineficiente.
Erik Engheim el

16

He encontrado esta solución para swift2:

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

cuál será el índice cuando 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        
  }
}

índice de retorno (de: elemento) .map {target.distance (de: startIndex, a: $ 0)}
frogcjn

8

No estoy seguro de cómo extraer la posición de String.Index, pero si está dispuesto a recurrir a algunos marcos de Objective-C, puede hacer un puente hacia el objetivo-c y hacerlo de la misma manera que solía hacerlo.

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

Parece que algunos métodos de NSString aún no han sido (o tal vez no serán) portados a String. Contiene también viene a la mente.


En realidad, parece que acceder a la propiedad de ubicación del valor de retorno es suficiente para que el compilador deduzca un tipo NSString, por lo que la bridgeToObjectiveC()llamada no es necesaria. Mi problema parece manifestarse solo cuando llamo rangeOfStringa Swift String previamente existente. Parece un problema de API ...
Matt Wilding

eso es interesante. No lo sabía inferido en esos casos. Bueno, cuando ya es una cadena, siempre puedes usar el puente.
Connor

8

Aquí hay una extensión de cadena limpia que responde a la pregunta:

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

También puede encontrar índices de un carácter en una sola cadena 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
    }
  }

}

Lo que da el resultado en [String.Distance] es decir. [Int], como

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

5

Si desea utilizar NSString familiar, puede declararlo explícitamente:

var someString: NSString = "abcdefghi"

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

Todavía no estoy seguro de cómo hacer esto en Swift.


1
Esto ciertamente funciona, y parece que el compilador es bastante agresivo para inferir los tipos NSString para usted. Realmente esperaba una manera pura y rápida de hacer esto, ya que parece un caso de uso bastante común.
Matt Wilding

Sí, estoy mirando alrededor, pero no lo veo. Es posible que se hayan centrado en áreas no compatibles con ObjC porque pueden llenar estos vacíos sin demasiadas capacidades perdidas. Solo pensando en voz alta :)
Logan

4

Si desea conocer la posición de un carácter en una cadena como valor int , use esto:

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

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

3

Sé que esto es antiguo y se ha aceptado una respuesta, pero puede encontrar el índice de la cadena en un par de líneas de código usando:

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

Alguna otra gran información sobre las cadenas Swift aquí Cuerdas en Swift


findno está disponible en Swift 3
AamirR

2

Esto funcionó para mí

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

esto funcionó también,

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

Ambos parecen recurrir al comportamiento de NSString de alguna manera. En mi patio de recreo, estaba usando una variable intermedia para la cadena. var str = "abcdef"; str.rangeOfString("c").locationgenera un error sobre String.Index que no tiene un miembro llamado ubicación ...
Matt Wilding

1
Funciona solo porque "string" es NSString y no Swift's String
gderaco

2

La cadena de tipo variable en Swift contiene diferentes funciones en comparación con NSString en Objective-C. Y como mencionó Sulthan,

Swift String no implementa RandomAccessIndex

Lo que puede hacer es bajar su variable de tipo String a NSString (esto es válido en Swift). Esto le dará acceso a las funciones en NSString.

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

2

Si lo piensa, en realidad no necesita la versión Int exacta de la ubicación. El rango o incluso el String.Index es suficiente para volver a sacar la subcadena si es necesario:

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 mejor respuesta para mí es esa función indexOf (target: String) -> Int {return (self as NSString) .rangeOfString (target) .location}
YannSteph

2

La forma más simple es:

En 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 cadena es un tipo de puente para NSString, así que agregue

import Cocoa

a su archivo rápido y use todos los métodos "antiguos".


1

En términos de pensamiento, esto podría llamarse una INVERSIÓN. Descubres que el mundo es redondo en lugar de plano. "Realmente no necesitas saber el ÍNDICE del personaje para hacer cosas con él". Y como programador en C, ¡también me resultó difícil hacerlo! Su línea "let index = letters.characters.indexOf (" c ")!" Es suficiente por sí mismo. Por ejemplo, para eliminar la c que podría usar ... (pegar en el patio de recreo)

    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

Sin embargo, si desea un índice, debe devolver un ÍNDICE real, no un Int, ya que un valor Int requeriría pasos adicionales para cualquier uso práctico. Estas extensiones devuelven un índice, un recuento de un personaje específico y un rango que demostrará este código enchufable para juegos.

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

Solución completa Swift 4:

OffsetIndexableCollection (String usando Int Index)

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

cadena de extensión {

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

Encontrar índice de subcadena

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 personaje

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

Para obtener el índice de una subcadena en una cadena con Swift 2:

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

0

En 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

Juego con seguir

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

hasta que entiendo el proporcionado ahora es solo matriz de caracteres

y con

let c = Array(str.characters)

¿Qué logra esto? ¿Cómo es tu primer bloque de código mejor que el segundo?
Ben Leggiero

0

Si solo necesita el índice de un personaje, la solución más simple y rápida (como ya lo señaló Pascal) es:

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

los caracteres se deprecian ahora ... lamentablemente, incluso si esto funciona
user3069232

0

Sobre el tema de convertir un String.Indexen un Int, esta extensión funciona para mí:

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

Ejemplo de uso relevante a esta pregunta:

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 puede ver, agrupa grupos de grafemas extendidos y caracteres unidos de manera diferente String.Index. Por supuesto, es por eso que tenemos String.Index. Debe tener en cuenta que este método considera que los clústeres son caracteres singulares, lo que está más cerca de corregir. Si su objetivo es dividir una cadena por un punto de código Unicode, esta no es la solución para usted.


0

En Swift 2.0 , la siguiente función devuelve una subcadena antes de un carácter dado.

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

Puede encontrar el número de índice de un carácter en una cadena con esto:

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

0

Como mi perspectiva, la mejor manera de conocer la lógica en sí está por debajo

 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)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.