Efficiently remove the last word from a string in Swift - swift

I am trying to build an autocorrect system, so I need to be able to delete the last word typed and replace it with the correct one. My solution:
func autocorrect() {
hasWordReadyToCorrect = false
var wordProxy = self.textDocumentProxy as UITextDocumentProxy
var stringOfWords = wordProxy.documentContextBeforeInput
fullString = "Unset Value"
if stringOfWords != nil {
var words = stringOfWords.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
for word in words {
arrayOfWords += [word]
}
println("The last word of the array is \(arrayOfWords.last)")
for (mistake, word) in autocorrectList {
println("The mistake is \(mistake)")
if mistake == arrayOfWords.last {
fullString = word
hasWordReadyToCorrect = true
}
}
println("The corrected String is \(fullString)")
}
}
This method is called after each keystroke, and if the space is pressed, it corrects the word. My problem comes in when the string of text becomes longer than about 20 words. It takes a while for it to fill the array each time a character is pressed, and it starts to lag to a point of not being able to use it. Is there a more efficient and elegant Swift way of writing this function? I'd appreciate any help!

This doesn't answer the OP's "autocorrect" issue directly, but this is code is probably the easiest way to answer the question posed in the title:
Swift 3
let myString = "The dog jumped over a fence"
let myStringWithoutLastWord = myString.components(separatedBy: " ").dropLast().joined(separator: " ")

1.
One thing, iteration isn't necessary for this:
for word in words {
arrayOfWords += [word]
}
You can just do:
arrayOfWords += words
2.
Breaking the for loop will prevent iterating unnecessarily:
for (mistake, word) in autocorrectList {
println("The mistake is \(mistake)")
if mistake == arrayOfWords.last {
fullString = word
hasWordReadyToCorrect = true
break; // Add this to stop iterating through 'autocorrectList'
}
}
Or even better, forget the for-loop completely:
if let word = autocorrectList[arrayOfWords.last] {
fullString = word
hasWordReadyToCorrect = true
}
Ultimately what you're doing is seeing if the last word of the entered text matches any of the keys in the autocorrect list. You can just try to get the value directly using optional binding like this.
---
I'll let you know if I think of more.

Related

Access an optional capture by name when using Swift Regex Builder

I'm just getting started with regular expressions and Swift Regex, so a heads up that my terminology my be incorrect. I have boiled this problem down to a very simple task:
I have input lines that have either just one word (a name) or start with the word "Test" followed by one space and then a name. I want to extract the name and also be able to access - without using match indices - the match to "Test " (which may be nil). Here is code that better describes the problem:
import RegexBuilder
let line1 = "Test John"
let line2 = "Robert"
let nameReference = Reference(String.self)
let testReference = Reference(String.self)
let regex = Regex {
Optionally {
Capture(as:testReference) {
"Test "
} transform : { text in
String(text)
}
}
Capture(as:nameReference) {
OneOrMore(.any)
} transform : { text in
String(text)
}
}
if let matches = try? regex.wholeMatch(in: line1) { // USE line1 OR line2 HERE
let theName = matches[nameReference]
print("Name is \(theName)")
// using index to access the test flag works fine for both line1 and line2:
if let flag = matches.1, flag == "Test " {
print("Using index: This is a test line")
} else {
print("Using index: Not a test line")
}
// but for line2, attempting to access with testReference crashes:
if matches[testReference] == "Test " { // crashes for line2 (not surprisingly)
print("Using reference: This is a test line")
} else {
print("Using reference: Not a test line")
}
}
When regex.wholeMatch() is called with line1 things work as expected with output:
Name is John
Using index: This is a test line
Using reference: This is a test line
but when called with line2 it crashes with a SIGABRT and output:
Name is Robert
Using index: Not a test line
Could not cast value of type 'Swift.Optional<Swift.Substring>' (0x7ff84bf06f20) to 'Swift.String' (0x7ff84ba6e918).
The crash is not surprising, because the Capture(as:testReference) was never matched.
My question is: is there a way to do this without using match indices (matches.1)? An answer using Regex Builder would be much appreciated:-)
The documentation says Regex.Match has a subscript(String) method which "returns nil if there's no capture with that name". That would be ideal, but it works only when the match output is type AnyRegexOutput.
I don't think you can get away with not using indexes, or at least code that knows the index but might hide it. Regular expression parsing works like that in any language, because it's always assumed that you know the order of elements in the expression.
For something like this, your example could be simplified to something like
let nameRegex = Regex {
ZeroOrMore("Test ")
Capture { OneOrMore(.anyNonNewline) }
}
if let matches = try? nameRegex.wholeMatch(in: line2) {
let (_, name) = matches.output
print("Name: \(name)")
}
That works for both of your sample lines. The let (_, name) doesn't use a numeric index but it's effectively the same thing since it uses index 1 as the value for name.
If your data is as straightforward as these examples, a regular expression may be overkill. You could work with if line1.hasPrefix("Test ") to detect lines with Test and then drop the first 5 characters, for example.

Iterate trough every word from WordsArray to take every character from it

I'm making some hangman app so words i use should be displayed with "?" instead of letters
if let wordsUrl = Bundle.main.url(forResource: "start", withExtension: "txt"){
if let wordsContent = try? String(contentsOf: wordUrl){
var allWords = wordsContent.components(separatedBy: "\n")
I don't know how to index every word from allWords array.? After that i would change letters using another property which i would use to display
for letter in word {
usedLetters.append(letter)
promptWord.append("?")
I’d recommend creating a method which you can call whenever your text field needs updating due to something such as a new letter input from the user.
var wordTextField: UITextField!
var usedWords = [] // Array to track the words already used by the user
let word = "hangman" // Word for the user to guess
var promptWord = "" // What will be displayed in the wordTextField
func updateTextField() {
for letter in word.uppercased() {
let strLetter = String(letter)
if usedLetters.contains(strLetter) {
promptWord += strLetter
} else {
promptWord += "?"
}
}
wordTextField.text = promptWord
A brief explanation of what the code does:
Firstly it iterates through the word inspecting each letter (uppercase so that there are no inconsistencies with the characters when the comparison is made to what the user has entered as their guess).
Secondly it checks to see if the strLetter is contained within the usedLetters array if it is then it places the letter inside of the correct location in the promptWord.
Whenever the letter is not found to be contained within the usedWords array a “?” is instead added to the string.
Finally the text of the wordTextField is set to be the promptWord displaying the amount of letters which the user has left to guess and how many as well as which letters the user has guessed correctly.
You can convert a String to an array of characters:
let string = "a String"
let characters = Array(characters)
So you could map your array of words to an array of arrays of characters like this:
var allWordsAsCharacterArrays = allWords.map { Array($0) }
You can also populate strings with question marks using String.init(repeating:count:)
When you pick a word from your words array, you could convert it to an array of characters, and a working string that you would populate with an array of question marks. As the user picks letters, you could replace the question marks in the working string with the correct letters from the word they are guessing.
It looks like you are just trying to provide the user ultimately with a hidden word containing only question marks. May I suggest a more straight forward approach?
let wordToGuess = "Hangman"
let hiddenWord = String(repeating: "?", count: wordToGuess.count)
now when the user guesses you can replace the proper characters
let guess = "h" // get from your user input
if wordToGuess.localizedStandardContains(guess) {
var location = 0
for c in wordToGuess {
if c.lowercased() == guess.lowercased() {
let index = hiddenWord.index(hiddenWord.startIndex, offsetBy: location)
hiddenWord = hiddenWord.replacingCharacters(in: index...index , with: String(c) )
print("hidden word now: \(hiddenWord)")
}
location += 1
}
}
note this is pretty messy code. It works, but I'm sure there is a much better way.

How to get multiple lines of stdin Swift HackerRank?

I just tried out a HackerRank challenge, and if a question gives you x lines of input, putting x lines of let someVariable = readLine() simply doesn't cut it, because there are lot's of test cases that shoot way more input to the code we write, so hard coded readLine() for each line of input won't fly.
Is there some way to get multiple lines of input into one variable?
For anyone else out there who's trying a HackerRank challenge for the first time, you might need to know a couple of things that you may have never come across. I only recently learned about this piece of magic called the readLine() command, which is a native function in Swift.
When the HackerRank system executes your code, it passes your code lines of input and this is a way of retrieving that input.
let line1 = readLine()
let line2 = readLine()
let line3 = readLine()
line1 is now given the value of the first line of input mentioned in the question (or delivered to your code by one of the test cases), with line2 being the second and so on.
Your code may work just great but may fail on a bunch of other test cases. These test cases don't send your code the same number of lines of input. Here's food for thought:
var string = ""
while let thing = readLine() {
string += thing + " "
}
print(string)
Now the string variable contains all the input there was to receive (as a String, in this case).
Hope that helps someone
:)
Definitely you shouldn't do this:
while let readString = readLine() {
s += readString
}
This because Swift will expect an input string (from readLine) forever and will never terminate, causing your application die by timeout.
Instead you should think in a for loop assuming you know how many lines you need to read, which is usually this way in HackerRank ;)
Try something like this:
let n = Int(readLine()!)! // Number of test cases
for _ in 1 ... n { // Loop from 1 to n
let line = readLine()! // Read a single line
// do something with input
}
If you know that each line is an integer, you can use this:
let line = Int(readLine()!)!
Or if you know each line is an array of integers, use this:
let line = readLine()!.characters.split(" ").map{ Int(String($0))! }
Or if each line is an array of strings:
let line = readLine()!.characters.split(" ").map{ String($0) }
I hope this helps.
For new version, to get an array of numbers separated by space
let numbers = readLine()!.components(separatedBy: [" "]).map { Int($0)! }
Using readLine() and AnyGenerator to construct a String array of the std input lines
readLine() will read from standard input line-by-line until EOF is hit, whereafter it returns nil.
Returns Characters read from standard input through the end of the
current line or until EOF is reached, or nil if EOF has already been
reached.
This is quite neat, as it makes readLine() a perfect candidate for generating a sequence using the AnyGenerator initializer init(body:) which recursively (as next()) invokes body, terminating in case body equals nil.
AnyGenerator
init(body: () -> Element?)
Create a GeneratorType instance whose next method invokes body
and returns the result.
With this, there's no need to actually supply the amount of lines we expect from standard input, and hence, we can catch all input from standard input e.g. into a String array, where each element corresponds to an input line:
let allLines = AnyGenerator { readLine() }.map{ $0 }
// type: Array<String>
After which we can work with the String array to apply whatever operations needed to solve a given task (/HackerRank task).
// example standard input
4 3
<tag1 value = "HelloWorld">
<tag2 name = "Name1">
</tag2>
</tag1>
tag1.tag2~name
tag1~name
tag1~value
/* resulting allLines array:
["4 3", "<tag1 value = \"HelloWorld\">",
"<tag2 name = \"Name1\">",
"</tag2>",
"</tag1>",
"tag1.tag2~name",
"tag1~name",
"tag1~value"] */
I recently discovered a neat trick to get a certain amount of lines. I'm gonna assume the first line gives you the amount of lines you get:
guard let count = readLine().flatMap({ Int($0) }) else { fatalError("No count") }
let lines = AnyGenerator{ readLine() }.prefix(count)
for line in lines {
}
I usually use this form.
if let line = readLine(), let cnt = Int(line) {
for _ in 1...cnt {
if let line = readLine() {
// your code for a line
}
}
}
Following the answer from dfrib, for Swift 3+, AnyIterator can be used instead of AnyGenerator, in the same way:
let allLines = AnyIterator { readLine() }.map{ $0 }
// type: Array<String>

How to use NSStringEnumerationOptions.ByWords with punctuation

I'm using this code to find the NSRange and text content of the string contents of a NSTextField.
nstext.enumerateSubstringsInRange(NSMakeRange(0, nstext.length),
options: NSStringEnumerationOptions.ByWords, usingBlock: {
(substring, substringRange, _, _) -> () in
//Do something with substring and substringRange
}
The problem is that NSStringEnumerationOptions.ByWords ignores punctuation, so that
Stop clubbing, baby seals
becomes
"Stop" "clubbing" "baby" "seals"
not
"Stop" "clubbing," "baby" "seals
If all else fails I could just check the characters before or after a given word and see if they are on the exempted list (where would I find which characters .ByWords exempts?); but there must be a more elegant solution.
How can I find the NSRanges of a set of words, from a string which includes the punctuation as part of the word?
You can use componentsSeparatedByString instead
var arr = nstext.componentsSeparatedByString(" ")
Output :
"Stop" "clubbing," "baby" "seals
Inspired by Richa's answer, I used componentsSeparatedByString(" "). I had to add a bit of code to make it work for me, since I wanted the NSRanges from the output. I also wanted it to still work if there were two instances of the same word - e.g. 'please please stop clubbing, baby seals'.
Here's what I did:
var words: [String] = []
var ranges: [NSRange] = []
//nstext is a String I converted to a NSString
words = nstext.componentsSeparatedByString(" ")
//apologies for the poor naming
var nstextLessWordsWeHaveRangesFor = nstext
for word in words
{
let range:NSRange = nstextLessWordsWeHaveRangesFor.rangeOfString(word)
ranges.append(range)
//create a string the same length as word so that the 'ranges' don't change in the future (if I just replace it with "" then the future ranges will be wrong after removing the substring)
var fillerString:String = ""
for var i=0;i<word.characters.count;++i{
fillerString = fillerString.stringByAppendingString(" ")
}
nstextLessWordsWeHaveRangesFor = nstextLessWordsWeHaveRangesFor.stringByReplacingCharactersInRange(range, withString: fillerString)
}

What's a good way to iterate backwards through the Characters of a String?

What's the most Swiftian way to iterate backwards through the Characters in a String? i.e. like for ch in str, only in reverse?
I think I must be missing something obvious, because the best I could come up with just now was:
for var index = str.endIndex;
index != str.startIndex;
index = index.predecessor() {
let ch = str[index.predecessor()]
...
}
I realise "what's the best..." may be classed as subjective; I suppose what I'm really looking for is a terse yet readable way of doing this.
Edit: While reverse() works and is terse, it looks like this might be quite inefficient compared to the above, i.e. it seems like it's not actually iterating backwards, but creating a full reverse copy of the characters in the String. This would be much worse than my original if, say, you were looking for something that was usually a few characters from the end of a 10,000-character String. I'm therefore leaving this question open for a bit to attract other approaches.
The reversed function reverses a C: CollectionType and returns a ReversedCollection:
for char in "string".characters.reversed() {
// ...
}
If you find that reversed pre-reverses the string, try:
for char in "string".characters.lazy.reversed() {
// ...
}
lazy returns a lazily evaluated sequence (LazyBidirectionalCollection) then reversed() returns another LazyBidirectionalCollection that is visited in reverse.
As of December 2015 with Swift version 2.1, the proper way to do this is
for char in string.characters.reverse() {
//loop backwards
}
String no longer conforms to SequenceType<T> but its character set does.
Not sure about efficiency, but I will suggest
for ch in reverse(str) {
println(ch)
}
Here is a code for reversing a string that doesn't use reverse(str)
// Reverse String
func myReverse(str:String) -> String {
var buffer = ""
for character in str {
buffer.insert(character, atIndex: buffer.startIndex)
}
return buffer
}
myReverse("Paul") // gives “luaP”
Just a little experiment. For what its worth.
Ok, leant how to read the question....
Would this work Matt?
func ReverseIteration(str:String) {
func myReverse(str:String) -> String {
var buffer = ""
for character in str {
buffer.insert(character, atIndex: buffer.startIndex)
}
return buffer
}
// reverse string then iterate forward.
var newStr = myReverse(str)
for char in newStr {
println(char)
// do some code here
}
this?
extension String {
var reverse: String {
var reverseStr = ""
for character in self {
reverseStr = String(character) + reverseStr
}
return reverseStr
}
}