Load numeric array from text file in Swift - arrays

I'm using a Swift function that successfully loads data from a text file into an Double array, but it is slow. Is there a way to load numeric data directly without using the String initializer that may be faster? Or any other suggestions to speed this up?
func arrayFromContentsOfFileWithPath(path: String) -> [Double]? {
do {
let content = try String(contentsOfFile:path, encoding: NSUTF8StringEncoding)
let stringArray = content.componentsSeparatedByString("\n").map{
$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
}
return stringArray.map{Double($0)}.flatMap{$0}
} catch _ as NSError {
return nil
}
}
EDIT:
To quantify things a bit, the data file is 10000 samples and the load time is 0.183 s for a single load (according to a measureBlock in my unit tests). In comparison, MATLAB loads the file in 0.033 s. Here are the first few samples of the data:
8.1472369e-01
9.0579194e-01
1.2698682e-01
9.1337586e-01
6.3235925e-01
9.7540405e-02
2.7849822e-01
5.4688152e-01
9.5750684e-01
9.6488854e-01
UPDATE:
Following #appzYourLife's advice to combine the mappings (I used .flatMap{Double($0)}) and to use a Release build, the load time is now 0.119 s. Much better, but still about 4x the time of MATLAB, which was very unexpected.

You can read data quite fast with NSScanner(). The scanDouble()
method skips leading whitespace, so no intermediate strings or arrays
are needed:
func arrayFromContentsOfFileWithPath(path: String) -> [Double]? {
do {
let content = try String(contentsOfFile:path, encoding: NSUTF8StringEncoding)
let scanner = NSScanner(string: content)
var doubleArray = [Double]()
var value = 0.0
while scanner.scanDouble(&value) {
doubleArray.append(value)
}
return doubleArray
} catch _ as NSError {
return nil
}
}
In my test, reading 10,000 samples in Release configuration is
done in 0.0034 seconds, compared to 0.077 seconds with your code,
that is an improvement of more than factor 20.
Update for Swift 3:
func arrayFromContentsOfFileWithPath(path: String) -> [Double]? {
guard let content = try? String(contentsOfFile:path, encoding: .utf8) else {
return nil
}
let scanner = Scanner(string: content)
var doubleArray = [Double]()
var value = 0.0
while scanner.scanDouble(&value) {
doubleArray.append(value)
}
return doubleArray
}

Related

How to convert string containing array to array in Swift?

I have a string in Swift that contains an array in it. Is it possible to convert the string into an array? All I have found on the internet is converting "abc" to ["a","b","c"], which I would not like to do.
String: "[\"value1\",\"value2\",\"value3\"]"
Result: ["value1","value2","value3"]
I am getting the string from a web request. The code for the request is here:
func webRequest(uri:String)->String{
var value = "";
let request = URLRequest(url: NSURL(string: uri)! as URL)
do {
let response: AutoreleasingUnsafeMutablePointer<URLResponse?>? = nil
let data = try NSURLConnection.sendSynchronousRequest(request, returning: response)
value = String(data: data, encoding: .utf8)!;
} catch _ {
}
return value;
}
First off, the problem here is not converting your string into an array. The problem is getting the array from the web request in the first place.
Let me update your web request function.
func webRequest(url: URL, completion: ([String]?) -> () { // I have updated this function to be asynchronous
let dataTask = URLSession.shared.dataTask(with: url) {
data, urlResponse, error in
// you might want to add more code in here to check the data is valid etc...
guard let data = data,
let arrayOfStrings = JSONDecoder().decode([String].self, from: data) else {
// something went wrong getting the array of strings so return nil here...
completion(nil)
return
}
completion(arrayOfStrings)
}
dataTask.resume()
}
Using this code instead of the code in your question you now have an asynchronous function that will not block the app and one that will pass your array of strings into the completion.
You can now run it like this...
webRequest(url: someURL) { strings in
guard let strings = strings else {
// strings is nil because something went wrong with the web request
return
}
print(strings)
}
Creating the URL
In your question you have this code... NSURL(string: someString)! as URL
You can change this to... let url = URL(string: someString)
Quick side note
Careful where you find tutorials and using code you find on the web. The code used in this question is very old. (at least 4 or 5 years "out of date").
If you're looking for tutorials to help with Swift then some recommendations are...
Ray Wenderlich
Hacking with swift

How do I append elements to a global array (in a for loop) in Swift?

I have an empty global array. The only simple thing I want to do is add an element to this array. It seems in swift this seemingly simple task is proving to be difficult. I am just left with an empty array and nothing is appending to my global array.
I can see that it prints out values in the for loop. So the values are actually there.
This is some stuff I have declared globally (Yes, I know global variables are bad but I will sort that out later):
struct HouseDetails: Decodable {
let median_price: String
let sale_year: String
let transaction_count: String
let type: String
}
var hsArray: [HouseDetails] = []
and in the viewDidLoad() function I have the data which I am storing in local variable "houses". When I loop through the array it prints median_price, showing that the values are there.
However when I do hsArray.append(h) it seems to do nothing.
let jsonUrlString = "https://data.melbourne.vic.gov.au/resource/i8px-csib.json"
guard let url = URL(string: jsonUrlString)
else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let houses = try JSONDecoder().decode([HouseDetails].self, from: data)
for h in houses {
hsArray.append(h)
print(h.median_price)
}
}
catch let jsonErr {
print("Error with json serialization", jsonErr)
}
}.resume()
Thank you for any help. In other languages I am used to being able to append an element to the end of an existing array, so I am sure it is just a small error.
Firstly, why don't you simply do
hsArray.append(contentsOf: houses)
instead of all that for loop
for h in houses {
hsArray.append(h)
print(h.median_price)
}
The issue might be the time at which you are using hsArray. See if the response is received after you use hsArray.

swift array.removeAtIndex error

How come I'm getting the error "NSArray does not have a member named 'removeAtIndex'. How can I fix this? The error is on the fourth last line. Sorry if my question is stupid, I'm fairly new to programming. I appreciate all the help I get.
import Foundation
let userDefaults = NSUserDefaults.standardUserDefaults()
func isAppAlreadyLaunchedOnce()->Bool{
let defaults = NSUserDefaults.standardUserDefaults()
if let isAppAlreadyLaunchedOnce = defaults.stringForKey("isAppAlreadyLaunchedOnce"){
println("App already launched")
return true
}
else{
defaults.setBool(true, forKey: "isAppAlreadyLaunchedOnce")
println("App launched first time")
return false
}
}
struct newFactBook {
let factsArray = [
"Ants stretch when they wake up in the morning.",
"Ostriches can run faster than horses.",
"Olympic gold medals are actually made mostly of silver.",
"You are born with 300 bones; by the time you are an adult you will have 206.",
"It takes about 8 minutes for light from the Sun to reach Earth.",
"Some bamboo plants can grow almost a meter in just one day.",
"The state of Florida is bigger than England.",
"Some penguins can leap 2-3 meters out of the water.",
"On average, it takes 66 days to form a new habit.",
"Mammoths still walked the earth when the Great Pyramid was being built."]
}
var checkLaunch = isAppAlreadyLaunchedOnce()
var oldFunFactsArray = []
if(checkLaunch == false){
oldFunFactsArray = newFactBook().factsArray
}
else if (checkLaunch == true){
oldFunFactsArray = userDefaults.objectForKey("key") as! NSArray
}
func randomFacts1() -> (String, Int){
var unsignedArrayCount = UInt32(oldFunFactsArray.count)
var unsignedRandomNumber = arc4random_uniform(unsignedArrayCount)
var randomNumber = Int(unsignedRandomNumber)
return (oldFunFactsArray[randomNumber] as! String, randomNumber)
}
oldFunFactsArray.removeAtIndex[randomFacts1().1] //error here
userDefaults.setObject(oldFunFactsArray, forKey:"key")
userDefaults.synchronize()
println(oldFunFactsArray)
We have some problems here:
1 How to invoke a method
removeAtIndex is a method that accepts an Int as parameters. It cannot be invoked this way
removeAtIndex[randomFacts1().1]
instead you should write
removeAtIndex(randomFacts1().1)
2. The type of oldFunFactsArray is NSArray and it's wrong.
Intact when you write this:
var oldFunFactsArray = []
Swift does infer this:
var oldFunFactsArray : NSArray = []
But at this point you have an immutable NSArray so it does not have the removeAtIndex method.
Since you are using Swift I suggest you to declare the var oldFunFactsArray as follow:
var oldFunFactsArray : [String]
if checkLaunch == false {
oldFunFactsArray = newFactBook().factsArray
} else {
oldFunFactsArray = userDefaults.objectForKey("key") as! [String]
}
Please note that here I am declaring a Swift array of String(s). Since I use the var keyword this array will be mutable and we will be able to invoke removeAtIndex later on.
Also note that in the else branch I am force-casting the result of objectForKey to [String]. It will be fine since I see, below, you are writing the oldFunFactsArray in that slot.
Hope this helps.
You need to use NSMutableArray to use this method.
NSArray is not Mutable (can not change its content after it is intialized).

Swift: AVAudioPlayer using array suddenly mutes

I've written a small class that uses AVFoundation to play audio using an array. Basically an new element is appended to the array every time 'playAudio' is called. This allows multiple sounds to play without cutting each other off. Also, so that the array doesn't infinitely increase in size I've set it to cycle back to index 0 after filling 5 slots in the array. Now everything works perfectly but after 'audioPlayer' has been called a bunch of times, audio suddenly stops and I start getting the 'Error' in the Catch section but my app continues to function normally as if it has just been muted. Can anyone tell me why this is happening?
var audioIndexA = 0
public class AudioPlayer: NSObject {
var playerA = [AVAudioPlayer]()
func playAudio(audioFile audioFile: String){
do{
let path = NSBundle.mainBundle().pathForResource(audioFile, ofType:"wav")
let fileURL = NSURL(fileURLWithPath: path!)
playerA.insert(try AVAudioPlayer(contentsOfURL: fileURL, fileTypeHint: nil), atIndex: audioIndexA)
playerA[audioIndexA].prepareToPlay()
if audioIndexA < 5{
playerA[audioIndexA].play()
audioIndexA++
} else{
playerA[audioIndexA].play()
audioIndexA = 0
}
} catch{
print("Error")
}
}
}

Swift - Write an Array to a Text File

I read into myArray (native Swift) from a file containing a few thousand lines of plain text..
myData = String.stringWithContentsOfFile(myPath, encoding: NSUTF8StringEncoding, error: nil)
var myArray = myData.componentsSeparatedByString("\n")
I change some of the text in myArray (no point pasting any of this code).
Now I want to write the updated contents of myArray to a new file.
I've tried this ..
let myArray2 = myArray as NSArray
myArray2.writeToFile(myPath, atomically: false)
but the file content is then in the plist format.
Is there any way to write an array of text strings to a file (or loop through an array and append each array item to a file) in Swift (or bridged Swift)?
As drewag points out in the accepted post, you can build a string from the array and then use the writeToFile method on the string.
However, you can simply use Swift's Array.joinWithSeparator to accomplish the same with less code and likely better performance.
For example:
// swift 2.0
let array = [ "hello", "goodbye" ]
let joined = array.joinWithSeparator("\n")
do {
try joined.writeToFile(saveToPath, atomically: true, encoding: NSUTF8StringEncoding)
} catch {
// handle error
}
// swift 1.x
let array = [ "hello", "goodbye" ]
let joined = "\n".join(array)
joined.writeToFile(...)
With Swift 5 and I guess with Swift 4 you can use code snippet which works fine to me.
let array = ["hello", "world"]
let joinedStrings = array.joined(separator: "\n")
do {
try joinedStrings.write(toFile: outputURL.path, atomically: true, encoding: .utf8)
} catch let error {
// handle error
print("Error on writing strings to file: \(error)")
}
You need to reduce your array back down to a string:
var output = reduce(array, "") { (existing, toAppend) in
if existing.isEmpty {
return toAppend
}
else {
return "\(existing)\n\(toAppend)"
}
}
output.writeToFile(...)
The reduce method takes a collection and merges it all into a single instance. It takes an initial instance and closure to merge all elements of the collection into that original instance.
My example takes an empty string as its initial instance. The closure then checks if the existing output is empty. If it is, it only has to return the text to append, otherwise, it uses String Interpolation to return the existing output and the new element with a newline in between.
Using various syntactic sugar features from Swift, the whole reduction can be reduced to:
var output = reduce(array, "") { $0.isEmpty ? $1 : "\($0)\n\($1)" }
Swift offers numerous ways to loop through an array. You can loop through the strings and print to a text file one by one. Something like so:
for theString in myArray {
theString.writeToFile(myPath, atomically: false, encoding: NSUTF8StringEncoding, error: nil);
}

Resources