Fatal error in dictionary - Index out of range / Swift - swift

I have a txt text file. The text file contains the text attached to each date.
Example:
30.10.2022 : Hello Stack Overflow.
31.10.2022 : Hello programmers.
01.11.2022 : We are the best.
02.11.2022 : Swift, is the best programming language.
etc…
I need to bind each sentence to date and show only the sentence that corresponds to today's date.
Example:
If today is 30.10.2022 - show in textView only the result: Hello Stack Overflow. And the same for every day... If the date changes from 30.10.2022 to 31.10.2022, we will see the result in textView - Hello programmers!
I tried to split my text from file text to dictionary and got an error - Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range.
I want to note that yesterday everything was working, but today it is not working, despite the fact that I did not change the code. Got confused. I can't understand what I missed.
Code with error...
import Foundation
class NameDaysModel {
private var dataArray: [String] = []
func loadDictionary() -> Dictionary<String, String> {
let pathToFile = Bundle.main.path(forResource: "NameDays", ofType: "txt")
if let path = pathToFile {
let countriesString = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
dataArray = countriesString.components(separatedBy: "\n")
}
var dictionary = [String: String]()
for line in dataArray {
let components = line.components(separatedBy: " : ")
dictionary[components[0]] = components[1] // = Thread 1: Fatal error: Index out of range
}
return dictionary
}
private func getDate() -> String {
let date = NSDate()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
let str = dateFormatter.string(from: date as Date)
return str
}
func getNameDay() -> String {
let date = getDate()
let nameDays = loadDictionary()
return nameDays["\(date)"] ?? ""
}
}

Based on the error you're getting, it sounds like some of the lines in your text file don't contain the string " : ". In that case your components array from the line let components = line.components(separatedBy: " : ") would only contain 1 element: The entire line of text.
You need to either make sure your data always contains a " : " in every line or add checking code that makes sure each value of components contains at least 2 elements before attempting to index into components[1].

Related

How do you remove only punctuation within words in a string?

PROBLEM:
Remove all punctuation inside words in a string, not outside like beginning and end of a string. (Ex. we'll = well, We're = Were, etc.)
WHAT I'VE TRIED (Works except I have to name every single kind of punctuation to remove individually.. bad solution):
INPUT: let testString = "What's with this project I'm trying to build, it's cra!zy!"
if let resultRange = myString.range(of: "'") {
let startIndex = resultRange.lowerBound
let endIndex = resultRange.upperBound
let range = startIndex..<endIndex
let result = myString.replacingCharacters(in: range, with: "")
print(result) // outputs = "Whats with this project Im trying to build, its cra!zy"
}
ALSO TRIED (It's good, but it removes the punctuation from both the start and end of words, which fails the above requirements):
extension String {
func removingCharacters(inCharacterSet forbiddenCharacters:CharacterSet) -> String
{
var filteredString = self
while true {
if var forbiddenCharRange = filteredString.rangeOfCharacter(from: forbiddenCharacters) {
filteredString.removeSubrange(forbiddenCharRange)
}
else {
break
}
}
return filteredString
}
}
var resultMyString = myString.removingCharacters(inCharacterSet: .punctuationCharacters)
print(resultMyString) // outputs = "Whats with this project Im trying to build its crazy"
DESIRED OUTPUT: "Whats with this project Im trying to build, its crazy!"
Regex that accounts for all valid types of punctuation in a regular expression = [:punct:]
Combine that regex to account for word boundaries = "\b[:punct:]\b"
let testString = "What's with this project I'm trying to build, it's cra!zy!"
let result = testString
.replacingOccurrences(of: "\\b[:punct:]\\b", with: "", options: .regularExpression)
Outputs = "Whats with this project Im trying to build, its crazy!"

convert string to double gives nil

I'm parsing a text file to get the latitude and longitude of locations. I need to convert the lon/lat strings to doubles, but I can't do it.
I've tried both the Double(String) method and the (String as NSNumber).doubleValue. It always gives nil.
When I type in the numbers manually it works.
Here's the code snippet:
var items = [[String]]()
func readParkingData() {
guard let filepath = Bundle.main.path(forResource: "parking", ofType: "txt") else {
print("file not found")
return
}
print("file path : \(filepath)")
do{
let content = try String(contentsOfFile: filepath, encoding: .utf8)
let attributed = content.htmlAttributedString
let decoded : String = attributed!.string
let split = decoded.split(separator: ";")
var count = 0
var item = [String]()
for word in split {
item.append(String(word))
count += 1
if count == 30 {
items.append(item)
item = [String]()
count = 0
}
}
for entry in items {
print(entry[24])
print(entry[25])
let latString : String = entry[24]
let lonString : String = entry[25]
print(type(of: latString))
let lat = Double(latString)
print(lat)
}
}catch{
print("file read error \(filepath)")
}
}
I've looked through the other answers. The type of latString is String, not optional. Trimming white spaces didn't help either. lat is always nil.
What's going on here?
Apparently the floating point numbers are enclosed in quotation marks,
so you'll need not only trim whitespace but also quotation marks. Example:
let latString = "\"12.34\""
print(latString) // "12.34"
var cs = CharacterSet.whitespaces
cs.insert("\"")
let trimmedLatString = latString.trimmingCharacters(in: cs)
print(trimmedLatString) // 12.34
print(Double(trimmedLatString)!) // 12.34
Further remarks:
I do not see the reason to operate on the htmlAttributedString, you
probably should split the original content into lines and fields.
Is your input a CSV-formatted file? There are open source CSV reader libraries
which you might try.

Converting numbers to string in a given string in Swift

I am given a string like 4eysg22yl3kk and my output should be like this:
foureysgtweny-twoylthreekk or if I am given 0123 it should be output as one hundred twenty-three. So basically, as I scan the string, I need to convert numbers to string.
I do not know how to implement this in Swift as I iterate through the string? Any idea?
You actually have two basic problems.
The first is convert a "number" to "spelt out" value (ie 1 to one). This is actually easy to solve, as NumberFormatter has a spellOut style property
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let text = formatter.string(from: NSNumber(value: 1))
which will result in "one", neat.
The other issue though, is how to you separate the numbers from the text?
While I can find any number of solutions for "extract" numbers or characters from a mixed String, I can't find one which return both, split on their boundaries, so, based on your input, we'd end up with ["4", "eysg", "22", "yl", "3", "kk"].
So, time to role our own...
func breakApart(_ text: String, withPattern pattern: String) throws -> [String]? {
do {
let regex = try NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)
var previousRange: Range<String.Index>? = nil
var parts: [String] = []
for match in regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) {
guard let range = Range(match.range, in: text) else {
return nil
}
let part = text[range]
if let previousRange = previousRange {
let textRange = Range<String.Index>(uncheckedBounds: (lower: previousRange.upperBound, upper: range.lowerBound))
parts.append(String(text[textRange]))
}
parts.append(String(part))
previousRange = range
}
if let range = previousRange, range.upperBound != text.endIndex {
let textRange = Range<String.Index>(uncheckedBounds: (lower: range.upperBound, upper: text.endIndex))
parts.append(String(text[textRange]))
}
return parts
} catch {
}
return nil
}
Okay, so this is a little "dirty" (IMHO), but I can't seem to think of a better approach, hopefully someone will be kind enough to provide some hints towards one ;)
Basically what it does is uses a regular expression to find all the groups of numbers, it then builds an array, cutting the string apart around the matching boundaries - like I said, it's crude, but it gets the job done.
From there, we just need to map the results, spelling out the numbers as we go...
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let value = "4eysg22yl3kk"
if let parts = try breakApart(value, withPattern: pattern) {
let result = parts.map { (part) -> String in
if let number = Int(part), let text = formatter.string(from: NSNumber(value: number)) {
return text
}
return part
}.joined(separator: " ")
print(result)
}
This will end up printing four eysg twenty-two yl three kk, if you don't want the spaces, just get rid of separator in the join function
I did this in Playgrounds, so it probably needs some cleaning up
I was able to solve my question without dealing with anything extra than converting my String to an array and check char by char. If I found a digit I was saving it in a temp String and as soon as I found out the next char is not digit, I converted my digit to its text.
let inputString = Array(string.lowercased())

Get the string up to a specific character

var hello = "hello, how are you?"
var hello2 = "hello, how are you #tom?"
i want to delete every letter behind the # sign.
result should be
var hello2 = "hello, how are you #tom?"
->
hello2.trimmed()
print(hello2.trimmed())
-> "hello, how are you"
Update
As i want to use it to link multiple users and replace the space behind #sign with the correct name, I always need the reference to the latest occurrence of the #sign to replace it.
text3 = "hey i love you #Tom #Marcus #Peter"
Example what the final version should look like
to start off
var text = "hello #tom #mark #mathias"
i want to always get the index of the latest # sign in the text
Expanding on #appzYourLife answer, the following will also trim off the whitespace characters after removing everything after the # symbol.
import Foundation
var str = "hello, how are you #tom"
if str.contains("#") {
let endIndex = str.range(of: "#")!.lowerBound
str = str.substring(to: endIndex).trimmingCharacters(in: .whitespacesAndNewlines)
}
print(str) // Output - "hello, how are you"
UPDATE:
In response to finding the last occurance of the # symbol in the string and removing it, here is how I would approach it:
var str = "hello, how are you #tom #tim?"
if str.contains("#") {
//Reverse the string
var reversedStr = String(str.characters.reversed())
//Find the first (last) occurance of #
let endIndex = reversedStr.range(of: "#")!.upperBound
//Get the string up to and after the # symbol
let newStr = reversedStr.substring(from: endIndex).trimmingCharacters(in: .whitespacesAndNewlines)
//Store the new string over the original
str = String(newStr.characters.reversed())
//str = "hello, how are you #tom"
}
Or looking at #appzYourLife answer use range(of:options:range:locale:) instead of literally reversing the characters
var str = "hello, how are you #tom #tim?"
if str.contains("#") {
//Find the last occurrence of #
let endIndex = str.range(of: "#", options: .backwards, range: nil, locale: nil)!.lowerBound
//Get the string up to and after the # symbol
let newStr = str.substring(from: endIndex).trimmingCharacters(in: .whitespacesAndNewlines)
//Store the new string over the original
str = newStr
//str = "hello, how are you #tom"
}
As an added bonus, here is how I would approach removing every # starting with the last and working forward:
var str = "hello, how are you #tom and #tim?"
if str.contains("#") {
while str.contains("#") {
//Reverse the string
var reversedStr = String(str.characters.reversed())
//Find the first (last) occurance of #
let endIndex = reversedStr.range(of: "#")!.upperBound
//Get the string up to and after the # symbol
let newStr = reversedStr.substring(from: endIndex).trimmingCharacters(in: .whitespacesAndNewlines)
//Store the new string over the original
str = String(newStr.characters.reversed())
}
//after while loop, str = "hello, how are you"
}
let text = "hello, how are you #tom?"
let trimSpot = text.index(of: "#") ?? text.endIndex
let trimmed = text[..<trimSpot]
Since a string is a collection of Character type, it can be accessed as such. The second line finds the index of the # sign and assigns its value to trimSpot, but if it is not there, the endIndex of the string is assigned through the use of the nil coalescing operator
??
The string, or collection of Characters, can be provided a range that will tell it what characters to get. The expression inside of the brackets,
..<trimSpot
is a range from 0 to trimSpot-1. So,
text[..<trimSpot]
returns an instance of type Substring, which points at the original String instance.
You need to find the range of the "#" and then use it to create a substring up to the index before.
import Foundation
let text = "hello, how are you #tom?"
if let range = text.range(of: "#") {
let result = text.substring(to: range.lowerBound)
print(result) // "hello, how are you "
}
Considerations
Please note that, following the logic you described and using the input text you provided, the output string will have a blank space as last character
Also note that if multiple # are presente in the input text, then the first occurrence will be used.
Last index [Update]
I am adding this new section to answer the question you posted in the comments.
If you have a text like this
let text = "hello #tom #mark #mathias"
and you want the index of the last occurrency of "#" you can write
if let index = text.range(of: "#", options: .backwards)?.lowerBound {
print(index)
}
Try regular expressions, they are much safer (if you know what you are doing...)
let hello2 = "hello, how are you #tom, #my #next #victim?"
let deletedStringsAfterAtSign = hello2.replacingOccurrences(of: "#\\w+", with: "", options: .regularExpression, range: nil)
print(deletedStringsAfterAtSign)
//prints "hello, how are you , ?"
And this code removes exactly what you need and leaves the characters after the strings clear, so you can see the , and ? still being there. :)
EDIT: what you asked in comments to this answer:
let hello2 = "hello, how are you #tom, #my #next #victim?"
if let elementIwannaAfterEveryAtSign = hello2.components(separatedBy: " #").last
{
let deletedStringsAfterAtSign = hello2.replacingOccurrences(of: "#\\w+", with: elementIwannaAfterEveryAtSign, options: .regularExpression, range: nil)
print(deletedStringsAfterAtSign)
//prints hello, how are you victim?, victim? victim? victim??
}

Swift 3: How to do string ranges?

Last night I had to convert my Swift 2.3 code to Swift 3.0 and my code is a mess after the conversion.
In Swift 2.3 I had the following code:
let maxChar = 40;
let val = "some long string";
var startRange = val.startIndex;
var endRange = val.startIndex.advancedBy(maxChar, limit: val.endIndex);
let index = val.rangeOfString(" ", options: NSStringCompareOptions.BackwardsSearch , range: startRange...endRange , locale: nil)?.startIndex;
Xcode converted my code to this which doesn't work:
let maxChar = 40;
let val = "some long string";
var startRange = val.startIndex;
var endRange = val.characters.index(val.startIndex, offsetBy: maxChar, limitedBy: val.endIndex);
let index = val.range(of: " ", options: NSString.CompareOptions.backwards , range: startRange...endRange , locale: nil)?.lowerBound
The error is in the parameter range in val.rage, saying No '...' candidates produce the expected contextual result type 'Range?'.
I tried using Range(startRange...endRange) as suggestd in the docs but I'm getting en error saying: connot invoke initiliazer for type ClosedRange<_> with an arguement list of type (ClosedRange). Seems like I'm missing something fundametnal.
Any help is appreciated.
Thanks!
Simple answer: the fundamental thing you are missing is that a closed range is now different from a range. So, change startRange...endRange to startRange..<endRange.
In more detail, here's an abbreviated version of your code (without the maxChar part):
let val = "some long string";
var startRange = val.startIndex;
var endRange = val.endIndex;
let index = val.range(
of: " ", options: .backwards, range: startRange..<endRange)?.lowerBound
// 9
Now you can use that as a basis to restore your actual desired functionality.
However, if all you want to do is split the string, then reinventing the wheel is kind of silly:
let arr = "hey ho ha".characters.split(separator:" ").map{String($0)}
arr // ["hey", "ho", "ha"]