Swift - endIndex.predecessor() - swift

Do these comments make any sense ?
Trying to figure out why it removes the character of my reversed given string, instead of the regular given string in this case.
import Foundation
extension String {
func reverseWords() -> String {
var result = ""
let words = self.componentsSeparatedByString(" ")
for thisWord in words.reverse() {
result += thisWord + " "
}
print("..\(result)..")
// Result is the words array ( contains self ) reversed with seperator " "
print("..\(self)..")
result.removeAtIndex(self.endIndex.predecessor())
// So here i am checking self within result?, and am removing the last index
// of my currently reversed given string inside result?
// I do result in my intended last space removal with result.endIndex but i'm
// wondering what happens in this case with the self.endIndex :)
return result
}
}
var str = "This string contains a few elements"
str.reverseWords()

self still refers to the original unreversed String.
The correct code would be:
result.removeAtIndex(result.endIndex.predecessor())
You should never use a String index for another string. If you are indexing result, you shouldn't use an index from self.
With a simple string you won't seem a difference but if you start adding multi-byte characters, e.g. emojis, your application can crash.
For example, using result += thisWord + "😀" would result in:
elements😀few😀a😀contains😀string�This😀
with self.endIndex
and
elements😀few😀a😀contains😀string😀This
with result.endIndex.
endIndex is the index past the end of the String. It works the same as count for arrays. count in arrays doesn't represent the last element, count - 1 represents the last element.
If your aim is to change the original String, you have to declare the method as mutating and assign to self, e.g.:
mutating func reverseWords() {
var result = ""
let words = self.componentsSeparatedByString(" ")
for thisWord in words.reverse() {
result += thisWord + " "
}
self = result
self.removeAtIndex(self.endIndex.predecessor())
}
although that's rather uncommon in functional programming.

Using self.endIndex.predecessor() to remove from result won't effect self (the "given" String).
The index is just an abstraction of a number. That number is based off the string self, rather than result
But ultimately, that index is used to remove from result, not from self.

So this is what's happening, which indeed results into the conclusion that it's quite idiotic to use the index of self.. ?
result.removeAtIndex(self.endIndex.predecessor()) // <-- Stupid move
// self.endIndex = 35
// self.endIndex predecessor = 34 ( last index in self )
// Removes index 34 out of result
// result.endIndex = 36
// results.endIndex predecessor = 35 ( the " " )
// Removes the one before the " " inside results

Related

Count number of characters between two specific characters

Trying to make a func that will count characters in between two specified char like:
count char between "#" and "." or "#" and ".com"
If this is only solution could this code be written in a simple way with .count or something less confusing
func validateEmail(_ str: String) -> Bool {
let range = 0..<str.count
var numAt = Int()
numDot = Int()
if str.contains("#") && str.contains(".") && str.characters.first != "#" {
for num in range {
if str[str.index(str.startIndex, offsetBy: num)] == "#" {
numAt = num
print("The position of # is \(numAt)")
} else if
str[str.index(str.startIndex, offsetBy: num)] == "." {
numDot = num
print("The position of . is \(numDot)")
}
}
if (numDot - numAt) > 1 {
return true
}
}
return false
}
With help from #Βασίλης Δ. i made a direct if statement for func validateEmail that check if number of char in between are less than 1
if (str.split(separator: "#").last?.split(separator: ".").first!.count)! < 1{
return false
}
It could be usefull
There are many edge cases to what you're trying to do, and email validation is notoriously complicated. I recommend doing as little of it as possible. Many, many things are legal email addresses. So you will need to think carefully about what you want to test. That said, this addresses what you've asked for, which is the distance between the first # and the first . that follows it.
func lengthOfFirstComponentAfterAt(in string: String) -> Int? {
guard
// Find the first # in the string
let firstAt = string.firstIndex(of: "#"),
// Find the first "." after that
let firstDotAfterAt = string[firstAt...].firstIndex(of: ".")
else {
return nil
}
// Return the distance between them (not counting the dot itself)
return string.distance(from: firstAt, to: firstDotAfterAt) - 1
}
lengthOfFirstComponentAfterAt(in: "rob#example.org") // Optional(7)
There's a very important lesson about Collections in this code. Notice the expression:
string[firstAt...].firstIndex(of: ".")
When you subscript a Collection, each element of the resulting slice has the same index as in the original collection. The returned value from firstIndex can be used directly to subscript string without offsetting. This is very different than how indexes work in many other languages, and allows powerful algorithms, and also creates at lot of bugs when developers forget this.

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

Count the number of lines in a Swift String

After reading a medium sized file (about 500kByte) from a web-service I have a regular Swift String (lines) originally encoded in .isolatin1. Before actually splitting it I would like to count the number of lines (quickly) in order to be able to initialise a progress bar.
What is the best Swift idiom to achieve this?
I came up with the following:
let linesCount = lines.reduce(into: 0) { (count, letter) in
if letter == "\r\n" {
count += 1
}
}
This does not look too bad but I am asking myself if there is a shorter/faster way to do it. The characters property provides access to a sequence of Unicode graphemes which treat \r\n as only one entity. Checking this with all CharacterSet.newlines does not work, since CharacterSet is not a set of Character but a set of Unicode.Scalar (a little counter-intuitively in my book) which is a set of code points (where \r\n counts as two code points), not graphemes. Trying
var lines = "Hello, playground\r\nhere too\r\nGalahad\r\n"
lines.unicodeScalars.reduce(into: 0) { (cnt, letter) in
if CharacterSet.newlines.contains(letter) {
cnt += 1
}
}
will count to 6 instead of 3. So this is more general than the above method, but it will not work correctly for CRLF line endings.
Is there a way to allow for more line ending conventions (as in CharacterSet.newlines) that still achieves the correct result for CRLF? Can the number of lines be computed with less code (while still remaining readable)?
If it's ok for you to use a Foundation method on an NSString, I suggest using
enumerateLines(_ block: #escaping (String, UnsafeMutablePointer<ObjCBool>) -> Void)
Here's an example:
import Foundation
let base = "Hello, playground\r\nhere too\r\nGalahad\r\n"
let ns = base as NSString
ns.enumerateLines { (str, _) in
print(str)
}
It separates the lines properly, taking into account all linefeed types, such as "\r\n", "\n", etc:
Hello, playground
here too
Galahad
In my example I print the lines but it's trivial to count them instead, as you need to - my version is just for the demonstration.
As I did not find a generic way to count newlines I ended up just solving my problem by iterating through all the characters using
let linesCount = text.reduce(into: 0) { (count, letter) in
if letter == "\r\n" { // This treats CRLF as one "letter", contrary to UnicodeScalars
count += 1
}
}
I was sure this would be a lot faster than enumerating lines for just counting, but I resolved to eventually do the measurement. Today I finally got to it and found ... that I could not have been more wrong.
A 10000 line string counted lines as above in about 1.0 seconds , but counting through enumeration using
var enumCount = 0
text.enumerateLines { (str, _) in
enumCount += 1
}
only took around 0.8 seconds and was consistently faster by a little more than 20%. I do not know what tricks the Swift engineers hide in their sleves, but they sure manage to enumerateLines very quickly. This just for the record.
You can use the following extension
extension String {
var numberOfLines: Int {
return self.components(separatedBy: "\n").count
}
}
Swift 5 Extension
extension String {
func numberOfLines() -> Int {
return self.numberOfOccurrencesOf(string: "\n") + 1
}
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}
Example:
let testString = "First line\nSecond line\nThird line"
let numberOfLines = testString.numberOfLines() // returns 3
I use this, a CharacterSet which Apple provides, made for this task:
let newLines = text.components(separatedBy: .newlines).count - 1

Optimizing adding dashes to a long Swift String

I am trying to take a hex string and insert dashes between every other character (e.g. "b201a968" to "b2-01-a9-68"). I have found several ways to do it, but the problem is my string is fairly large (8066 characters) and the fastest I can get it to work it still takes several seconds. These are the ways I have tried and how long they are taking. Can anyone help me optimize this function?
//42.68 seconds
func reformatDebugString(string: String) -> String
{
var myString = string
var index = 2
while(true){
myString.insert("-", at: myString.index(myString.startIndex, offsetBy: index))
index += 3
if(index >= myString.characters.count){
break
}
}
return myString
}
//21.65 seconds
func reformatDebugString3(string: String) -> String
{
var myString = ""
let length = string.characters.count
var first = true
for i in 0...length-1{
let index = string.index(myString.startIndex, offsetBy: i)
let c = string[index]
myString += "\(c)"
if(!first){
myString += "-"
}
first = !first
}
return myString
}
//11.37 seconds
func reformatDebugString(string: String) -> String
{
var myString = string
var index = myString.characters.count - 2
while(true){
myString.insert("-", at: myString.index(myString.startIndex, offsetBy: index))
index -= 2
if(index == 0){
break
}
}
return myString
}
The problem with all three of your approaches is the use of index(_:offsetBy:) in order to get the index of the current character in your loop. This is an O(n) operation where n is the distance to offset by – therefore making all three of your functions run in quadratic time.
Furthermore, for solutions #1 and #3, your insertion into the resultant string is an O(n) operation, as all the characters after the insertion point have to be shifted up to accommodate the added character. It's generally cheaper to build up the string from scratch in this case, as we can just add a given character onto the end of the string, which is O(1) if the string has enough capacity, O(n) otherwise.
Also for solution #1, saying myString.characters.count is an O(n) operation, so not something you want to be doing at each iteration of the loop.
So, we want to build the string from scratch, and avoid indexing and calculating the character count inside the loop. Here's one way of doing that:
extension String {
func addingDashes() -> String {
var result = ""
for (offset, character) in characters.enumerated() {
// don't insert a '-' before the first character,
// otherwise insert one before every other character.
if offset != 0 && offset % 2 == 0 {
result.append("-")
}
result.append(character)
}
return result
}
}
// ...
print("b201a968".addingDashes()) // b2-01-a9-68
Your best solution (#3) in a release build took 37.79s on my computer, the method above took 0.023s.
As already noted in Hamish's answer, you should avoid these two things:
calculate each index with string.index(string.startIndex, offsetBy: ...)
modifying a large String with insert(_:at:)
So, this can be another way:
func reformatDebugString4(string: String) -> String {
var result = ""
var currentIndex = string.startIndex
while currentIndex < string.endIndex {
let nextIndex = string.index(currentIndex, offsetBy: 2, limitedBy: string.endIndex) ?? string.endIndex
if currentIndex != string.startIndex {
result += "-"
}
result += string[currentIndex..<nextIndex]
currentIndex = nextIndex
}
return result
}

String convert to Int and replace comma to Plus sign

Using Swift, I'm trying to take a list of numbers input in a text view in an app and create a sum of this list by extracting each number for a grade calculator. Also the amount of values put in by the user changes each time. An example is shown below:
String of: 98,99,97,96...
Trying to get: 98+99+97+96...
Please Help!
Thanks
Use components(separatedBy:) to break up the comma-separated string.
Use trimmingCharacters(in:) to remove spaces before and after each element
Use Int() to convert each element into an integer.
Use compactMap (previously called flatMap) to remove any items that couldn't be converted to Int.
Use reduce to sum up the array of Int.
let input = " 98 ,99 , 97, 96 "
let values = input.components(separatedBy: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) }
let sum = values.reduce(0, +)
print(sum) // 390
For Swift 3 and Swift 4.
Simple way: Hard coded. Only useful if you know the exact amount of integers coming up, wanting to get calculated and printed/used further on.
let string98: String = "98"
let string99: String = "99"
let string100: String = "100"
let string101: String = "101"
let int98: Int = Int(string98)!
let int99: Int = Int(string99)!
let int100: Int = Int(string100)!
let int101: Int = Int(string101)!
// optional chaining (if or guard) instead of "!" recommended. therefore option b is better
let finalInt: Int = int98 + int99 + int100 + int101
print(finalInt) // prints Optional(398) (optional)
Fancy way as a function: Generic way. Here you can put as many strings in as you need in the end. You could, for example, gather all the strings first and then use the array to have them calculated.
func getCalculatedIntegerFrom(strings: [String]) -> Int {
var result = Int()
for element in strings {
guard let int = Int(element) else {
break // or return nil
// break instead of return, returns Integer of all
// the values it was able to turn into Integer
// so even if there is a String f.e. "123S", it would
// still return an Integer instead of nil
// if you want to use return, you have to set "-> Int?" as optional
}
result = result + int
}
return result
}
let arrayOfStrings = ["98", "99", "100", "101"]
let result = getCalculatedIntegerFrom(strings: arrayOfStrings)
print(result) // prints 398 (non-optional)
let myString = "556"
let myInt = Int(myString)