2015-12-23 8 views
0

То, что я хочу что-то вродеНайдите диапазон от N-го слова в строке

"word1 word2 word3".rangeOfWord(2) => 6 to 10 

В результате может прийти как Range или кортежа или любой другой.

Я бы предпочел не выполнять грубую силу итерации над персонажами и использование конечного автомата. Зачем изобретать лексер? Есть ли способ лучше?

+0

Это вряд ли «лексер», который вы будете внедрять! – Noldorin

+0

Привет, Андрей - Вы знаете о NSLinguisticTagger? - Или, в вашем довольно простодушном примере, недостаточно ли было бы NSRegularExpression? – matt

+0

Вы знаете, как (бывший) хакер Perl, я должен был подумать о REs. Хотя мне не интересно просто найти N-е слово, но найти его диапазон. Могу ли я сделать это с помощью RE? Конечно, не чистая компьютерная наука, но, возможно, с улучшенными. –

ответ

2

В вашем примере, ваши слова являются уникальными, и вы можете использовать следующий метод:

let myString = "word1 word2 word3" 
let wordNum = 2 
let myRange = myString.rangeOfString(myString.componentsSeparatedByString(" ")[wordNum-1]) 
    // 6..<11 

Как указал, Эндрю Дункан в комментариях ниже, выше, действует только тогда, когда ваши слова являются уникальными. Если у вас есть не-уникальные слова, вы можете использовать это несколько меньше, аккуратнее метод:

let myString = "word1 word2 word3 word2 word1 word3 word1" 
let wordNum = 7 // 2nd instance (out of 3) of "word1" 
let arr = myString.componentsSeparatedByString(" ") 
var fromIndex = arr[0..<wordNum-1].map { $0.characters.count }.reduce(0, combine: +) + wordNum - 1 

let myRange = Range<String.Index>(start: myString.startIndex.advancedBy(fromIndex), end: myString.startIndex.advancedBy(fromIndex+arr[wordNum-1].characters.count)) 
let myWord = myString.substringWithRange(myRange) 
    // string "word1" (from range 36..<41) 

Наконец, позволяет использовать последний построить расширение String как вы желали в вашем примере вопроса:

extension String { 
    private func rangeOfNthWord(wordNum: Int, wordSeparator: String) -> Range<String.Index>? { 
     let arr = myString.componentsSeparatedByString(wordSeparator) 

     if arr.count < wordNum { 
      return nil 
     } 
     else { 
      let fromIndex = arr[0..<wordNum-1].map { $0.characters.count }.reduce(0, combine: +) + (wordNum - 1)*wordSeparator.characters.count 
      return Range<String.Index>(start: myString.startIndex.advancedBy(fromIndex), end: myString.startIndex.advancedBy(fromIndex+arr[wordNum-1].characters.count)) 
     } 
    } 
} 

let myString = "word1 word2 word3 word2 word1 word3 word1" 
let wordNum = 7 // 2nd instance (out of 3) of "word1" 

if let myRange = myString.rangeOfNthWord(wordNum, wordSeparator: " ") { 
     // myRange: 36..<41 
    print(myString.substringWithRange(myRange)) // prints "word1" 
} 

Вы можете настроить метод .rangeOfNthWord(...), если разделение слов не является уникальным (например, некоторые слова разделены двумя пробелами " ").


Кроме того, отметил в комментариях ниже, использование .rangeOfString(...) не является, по сути, чистый Swift. Это, однако, отнюдь не плохая практика. От Swift Language Guide - Strings and Characters:

Строка типа Swift соединена с классом NSString Foundation. Если вы работаете с базой Foundation в Cocoa, весь API NSString доступен для вызова любого строкового значения, которое вы создаете, когда тип применяется к NSString, как описано в AnyObject. Вы также можете использовать значение строки с любым API, для которого требуется экземпляр NSString.

Смотрите также NSString class reference for rangeOfString method:

// Swift Declaration: 
func rangeOfString(_ searchString: String) -> NSRange 
+0

Обратите внимание, что это не чисто Swift, поскольку использует методы NSString (Foundation). – Noldorin

+0

Я думаю, что это не с исходным текстом, например, «aaa bbb aaa ccc». Поиск третьего слова вернет первое. –

+0

@AndrewDuncan Вы правы, спасибо. Я отредактировал ответ, чтобы включить исправление для строки, содержащей неповторимые слова (разделенные символом «»). – dfri

0

Я пошел вперед и написал государственную машину. (Grumble ..) FWIW, вот он:

extension String { 
    private func halfOpenIntervalOfBlock(n:Int, separator sep:Character? = nil) -> (Int, Int)? { 
     enum State { 
      case InSeparator 
      case InPrecedingSeparator 
      case InWord 
      case InTarget 
      case Done 
     } 

     guard n > 0 else { 
      return nil 
     } 

     var state:State 
     if n == 1 { 
      state = .InPrecedingSeparator 
     } else { 
      state = .InSeparator 
     } 

     var separatorNum = 0 
     var startIndex:Int = 0 
     var endIndex:Int = 0 

     for (i, c) in self.characters.enumerate() { 
      let inSeparator:Bool 
      // A bit inefficient to keep doing this test. 
      if let s = sep { 
       inSeparator = c == s 
      } else { 
       inSeparator = c == " " || c == "\n" 
      } 
      endIndex = i 

      switch state { 
      case .InPrecedingSeparator: 
       if !inSeparator { 
        state = .InTarget 
        startIndex = i 
       } 

      case .InTarget: 
       if inSeparator { 
        state = .Done 
       } 

      case .InWord: 
       if inSeparator { 
        separatorNum += 1 
        if separatorNum == n - 1 { 
         state = .InPrecedingSeparator 
        } else { 
         state = .InSeparator 
        } 
       } 

      case .InSeparator: 
       if !inSeparator { 
        state = .InWord 
       } 

      case .Done: 
       break 
      } 

      if state == .Done { 
       break 
      } 
     } 

     if state == .Done { 
      return (startIndex, endIndex) 
     } else if state == .InTarget { 
      return (startIndex, endIndex + 1) // We ran off end. 
     } else { 
      return nil 
     } 
    } 

    func rangeOfWord(n:Int) -> Range<Index>? { 
     guard let (s, e) = self.halfOpenIntervalOfBlock(n) else { 
      return nil 
     } 
     let ss = self.startIndex.advancedBy(s) 
     let ee = self.startIndex.advancedBy(e) 
     return Range(start:ss, end:ee) 
    } 

} 
+0

Aargh! String.enumerateSubstringsInRange (..., options: .byWord) –