fatal error: Can't form a Character from an empty String - swift

So I have function that strips off a trailing ", " (comma + space) at the end of a string and I'm getting the above error even though I'm ensuring that the string is not empty. The following code:
print("stripping trailing commas")
for var detail in details {
if detail.contains(",") {
print(detail)
detail.remove(at: detail.endIndex) // <-- Removes last space
detail.remove(at: detail.endIndex) // <-- Removes last comma
}
}
...results int the following console output:
stripping trailing commas
2016,
fatal error: Can't form a Character from an empty String
The first instance detail.remove(at: detail.endIndex) is being highlighted by the debugger, and while I can't be sure the space is present from the console message, I'm adding ", " at the end of each entry in a list so any string that actually contains a comma should not only have characters (as the console indicates) but it should have an extra two chars at the end that need to be stripped.
Thanks in advance, for any help on what's causing the error and how to resolve?

Try change
detail.remove(at: detail.endIndex)
to
detail.remove(at: detail.index(before: detail.endIndex))

A simple way to do this without using import Foundation (needed for contains()) or calculating indexes would be something like this:
let details = ["one, two, three, ", "a, b", "once, twice, thrice, so nice, ", ""]
let filtered = details.map { (original: String) -> String in
guard original.hasSuffix(", ") else { return original }
return String(original.characters.dropLast(2))
}
print(filtered) // -> "["one, two, three", "a, b", "once, twice, thrice, so nice", ""]\n"
This won't remove any empty strings that are in the returned array. If you need that functionality it can easily be added.

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.

Regular expression in Swift

I am trying to parse a string with a regex, I am getting some problems trying to extract all the information in substrings. I am almost done, but I am stacked at this point:
For a string like this:
[00/0/00, 00:00:00] User: This is the message text and any other stuff
I can parse Date, User and Message in Swift with this code:
let line = "[00/0/00, 00:00:00] User: This is the message text and any other stuff"
let result = line.match("(.+)\\s([\\S ]*):\\s(.*\n(?:[^-]*)*|.*)$")
extension String {
func match(_ regex: String) -> [[String]] {
let nsString = self as NSString
return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, count)).map { match in
(0..<match.numberOfRanges).map { match.range(at: $0).location == NSNotFound ? "" : nsString.substring(with: match.range(at: $0)) }
} ?? []
}
}
The resulting array is something like this:
[["[00/0/00, 00:00:00] User: This is the message text and any other stuff","[00/0/00, 00:00:00]","User","This is the message text and any other stuff"]]
Now my problem is this, if the message has a ':' on it, the resulting array is not following the same format and breaks the parsing function.
So I think I am missing some cases in the regex, can anyone help me with this? Thanks in advance.
In the pattern, you are making use of parts that are very broad matches.
For example, .+ will first match until the end of the line, [\\S ]* will match either a non whitespace char or a space and [^-]* matches any char except a -
The reason it could potentially break is that the broad matches first match until the end of the string. As a single : is mandatory in your pattern, it will backtrack from the end of the string until it can match a : followed by a whitespace, and then tries to match the rest of the pattern.
Adding another : in the message part, may cause the backtracking to stop earlier than you would expect making the message group shorter.
You could make the pattern a bit more precise, so that the last part can also contain : without breaking the groups.
(\[[^][]*\])\s([^:]*):\s(.*)$
(\[[^][]*\]) Match the part from an opening till closing square bracket [...] in group 1
\s Match a whitespace char
([^:]*): Match any char except : in group 2, then match the expected :
\s(.*) Match a whitespace char, and capture 0+ times any char in group 3
$ End of string
Regex demo

How can I individually replace each occurence of a string?

I am trying to replace every occurrence of a space with two random characters (out of 103). The problem is that it is always the same 2 characters every time, which makes sense if you look at the code
I'm pretty new to Swift and Xcode and I've already tried a bunch of things, like using a "for" loop.
newSentence = passedSentence.replacingOccurrences(of: " ", with: " \(randomArray[Int.random(in: 0...103)]) \(randomArray[Int.random(in: 0...103)]) ")
resultText.text = newSentence
As I said before, it is always the same 2 characters when I want it to "refresh" for every occurrence of a space.
Using map(_:) method instead of replacingOccurrences(of:with:) to get the desired result.
replacingOccurrences(of:with:)
A new string in which all occurrences of target in the receiver are
replaced by replacement.
It replaces all the occurrences with the same instance that is passed in the replacementString i.e. randomArray[Int.random(in: 0...103) (randomArray[Int.random(in: 0...103)] is executed only once and used throughout the string for all occurrences of " ".
let passedSentence = "This is a sample sentence"
let newSentence = (passedSentence.map { (char) -> String in
if char == " " {
return " \(randomArray[Int.random(in: 0...103)]) \(randomArray[Int.random(in: 0...103)]) "
}
return String(char)
}).joined()
print(newSentence)
In case the you're using the whole range of randomArray, i.e. if randomArray contains 104 elements i.e. 0...103, you can directly use randomElement() on randomArray instead of using random(in:) on Int, i.e.
Use
randomArray.randomElement()
instead of
randomArray[Int.random(in: 0...103)]
Yes, your behavior is expected given your code. The parameter passed in with: is only executed once so if it generates "SX", that will be used to replace ALL the occurrences of " "(space) in your passedSentence.
To get your expected behavior, you would have to loop:
var rangeOfEmptyString = passedSentence.range(of: " ")
while rangeOfEmptyString != nil {
let randomModifier = "\(randomArray[Int.random(in: 0...103)])\(randomArray[Int.random(in: 0...103)])"
passedSentence = passedSentence.replacingCharacters(in: rangeOfEmptyString!, with: randomModifier)
rangeOfEmptyString = passedSentence.range(of: " ")
}
You should replace each space with different random elements of the array. replacingOccurrences method replaces all ranges at once. Get the range of space one by one and replace with unique random elements
let randomArray:[String] = ["a","b","c","d"]
var passedSentence = "This is a long text"
var start = passedSentence.startIndex
while let range = passedSentence[start...].rangeOfCharacter(from: .whitespaces),
let random1 = randomArray.randomElement(),
let random2 = randomArray.randomElement() {
let replacementString = " \(random1) \(random2) "
passedSentence.replaceSubrange(range, with: " \(random1) \(random2) ")
start = passedSentence.index(range.upperBound, offsetBy: replacementString.count)
}
print(passedSentence)
To get the random element from an array don't use randomArray[Int.random(in: 0...103)]. You can use randomElement()

What is an easier way to replace multiple characters with other characters in a string in swift?

I am currently trying to setup a string to be added to a HTTP POST request where a user would type text and tap 'enter' and a request would be sent.
I know multiple characters (^,+,<,>) can be replaced with a single character ('_') like so:
userText.replacingOccurrences(of: "[^+<>]", with: "_"
I currently am using multiple functions of:
.replacingOccurrences(of: StringProtocol, with:StringProtocol)
like so:
let addAddress = userText.replacingOccurrences(of: " ", with: "_").replacingOccurrences(of: ".", with: "%2E").replacingOccurrences(of: "-", with: "%2D").replacingOccurrences(of: "(", with: "%28").replacingOccurrences(of: ")", with: "%29").replacingOccurrences(of: ",", with: "%2C").replacingOccurrences(of: "&", with: "%26")
Is there a more efficient way of doing this?
What you are doing is to manually encode the string with a percent encoding.
If that's the case, this will help you:
addingPercentEncoding(withAllowedCharacters:)
Returns a new string made from the receiver by replacing all characters not in the specified set with percent-encoded characters.
https://developer.apple.com/documentation/foundation/nsstring/1411946-addingpercentencoding
For your specific case this should work:
userText.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
I think the only problem you will run into with using addingPercentEncoding is that your question states that a space " " should be replaced with an underscore. Using addingPercentEncoding for a space " " will return %20. You should be able to combine some of these answers, define the remaining characters from your list that should return standard character replacement and get your desired outcome.
var userText = "This has.lots-of(symbols),&stuff"
userText = userText.replacingOccurrences(of: " ", with: "_")
let allowedCharacterSet = (CharacterSet(charactersIn: ".-(),&").inverted)
var newText = userText.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)
print(newText!) // Returns This_has%2Elots%2Dof%28symbols%29%2C%26stuff
Ideally to use .urlHostAllowed CharacterSet since it works almost always.
textInput.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
But the best is combining all the possible options like here which will make sure you make it right.

swift why is characters.split used for? and why is map(String.init) used for

import Foundation
for i in 1 ... n {
let entry = readLine()!.characters.split(" ").map(String.init)
let name = entry[0]
let phone = Int(entry[1])!
phoneBook[name] = phone``
}
//can someone explain this piece of code`
I assume you know everything else in the code except this line:
let entry = readLine()!.characters.split(" ").map(String.init)
readLine() reads user input and returns it. Let's say the user input is
Sweeper 12345678
using .characters.split(" "), we split the input using a separator. What is this separator? A space (" ")! Now the input has been split into two - "Sweeper" and "12345678".
We want the two split parts to be strings, right? Strings are much more easier to manipulate. Currently the split parts are stored in an array of String.CharacterView.SubSequence. We want to turn each String.CharacterView.SubSequence into a string. That is why we use map. map applies a certain function to everything in a collection. So
.map(String.init)
is like
// this is for demonstration purposes only, not real code
for x in readLine()!.characters.split(" ") {
String.init(x)
}
We have now transformed the whole collection into strings!
There is error in your code replace it like below:
let entry = readLine()!.characters.split(separator: " ").map(String.init)
Alternative to the above code is:
let entry = readLine()!.components(separatedBy: " ")
Example:
var str = "Hello, playground"
let entry = str.characters.split(separator: " ").map(String.init)
print(entry)
Now characters.split with split the characters with the separator you give in above case " "(space). So it will generate an array of characters. And you need to use it as string so you are mapping characters into String type by map().