I am attempting to learn how to utilize Dictionary in Swift. My program reads a local JSON file, which contains five value elements for each key, and puts it into a dictionary. The keys are dates in string format "yyyy-MM-dd" and the values represent dollars 409.34593 in string format. I have been able to sort and filter the dictionary.
In addition use mapValues() to create a new dictionary with one key and one value with the value converted to an optional double. What eludes me is how to convert all 5 of the value elements to double i.e. have a dictionary identical to the one read in from the JSON file but with all 5 values as double or optional double.
Below is a snippet of the JSON file and my code. Any you can do to point me in the right direction would be appreciated.
"Time Series (Daily)": {
"2021-09-22": {
"1. open": "402.1700",
"2. high": "405.8500",
"3. low": "401.2600",
"4. close": "403.9000",
"5. volume": "5979811"
},
"2021-09-21": {
"1. open": "402.6600",
"2. high": "403.9000",
"3. low": "399.4400",
"4. close": "400.0400",
"5. volume": "6418124"
},
struct Stock: Codable {
let timeSeriesDaily: [String : TimeSeriesDaily] // creates a dictionary, key = string : TimeSeriesDaily = value
enum CodingKeys: String, CodingKey {
case timeSeriesDaily = "Time Series (Daily)"
}
}
struct TimeSeriesDaily: Codable {
let Open, High, Low, Close: String
let Volume: String
enum CodingKeys: String, CodingKey {
case Open = "1. open"
case High = "2. high"
case Low = "3. low"
case Close = "4. close"
case Volume = "5. volume"
}
}
class ReadData: ObservableObject {
@Published var tmpData = Stock(timeSeriesDaily : [:]) // initialize dictionary as empty when struct Stock is created
// type info has to be made availabe upon creation, is done in the Stock struct
init() {
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "VOO", withExtension: "json")
else {
print("Json file not found")
return
}
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: url)
self.tmpData = try decoder.decode(Stock.self, from: data)
// print(self.tmpData)
} catch {
print(error)
}
}
}
struct ContentView: View {
@ObservedObject var vooData = ReadData()
var body: some View {
ScrollView {
VStack (alignment: .leading) {
let lastYear = getOneYearAgo()
let filteredDict = vooData.tmpData.timeSeriesDaily.filter { $0.key > lastYear } // Works
let sortedFilteredDict = filteredDict.sorted { $0.key < $1.key } // Works
let justCloseArray = sortedFilteredDict.map { ($0.key, $0.value.Close) } // returns array [(String, String)]
let justCloseDict = Dictionary(uniqueKeysWithValues: justCloseArray) // returns dictionary with 1 key & 1 val
let sortedCloseDict = justCloseDict.sorted { $0.key < $1.key } // works
let newDict = filteredDict.mapValues { Double($0.Close) }
let sortedNewDict = newDict.sorted { $0.key < $1.key }
Spacer()
// ForEach ( sortedCloseDict.map { ($0.key, $0.value) }, id: \.0 ) { keyValuePair in
ForEach ( sortedNewDict.map { ($0.key, $0.value) }, id: \.0 ) { keyValuePair in // map converts dictionary to arrap
HStack {
Text (keyValuePair.0)
Text ("\(keyValuePair.1!)")
}
}
Spacer()
} // end vstack
} .frame(width: 600, height: 400, alignment: .center) // end scroll view
}
}
CodePudding user response:
The problem here is that your dollar values are not numbers, they are Strings.
Put this in a Playground:
import Foundation
let jsonData = """
{
"Time Series (Daily)": {
"2021-09-22": {
"1. open": "402.1700",
"2. high": "405.8500",
"3. low": "401.2600",
"4. close": "403.9000",
"5. volume": "5979811"
},
"2021-09-21": {
"1. open": "402.6600",
"2. high": "403.9000",
"3. low": "399.4400",
"4. close": "400.0400",
"5. volume": "6418124"
}
}
}
"""
.data(using: .utf8)!
struct ReadData {
let timeSeriesDaily: [Date: TimeSeriesDaily]
}
struct TimeSeriesDaily {
let open: Double
let high: Double
let low: Double
let close: Double
let volume: Double
}
extension ReadData: Decodable {
enum CodingKeys: String, CodingKey {
case timeSeriesDaily = "Time Series (Daily)"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
timeSeriesDaily = Dictionary(
uniqueKeysWithValues: try container
.decode([String: TimeSeriesDaily].self, forKey: .timeSeriesDaily)
.map { (dateFormatter.date(from: $0)!, $1) }
)
}
}
/** In the above, the container can decode [String: T] but it can't decode [Date: T], so we convert the Strings to Dates using a dateFormatter */
extension TimeSeriesDaily: Decodable {
enum CodingKeys: String, CodingKey {
case open = "1. open"
case high = "2. high"
case low = "3. low"
case close = "4. close"
case volume = "5. volume"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
open = Double(try container.decode(String.self, forKey: .open))!
high = Double(try container.decode(String.self, forKey: .high))!
low = Double(try container.decode(String.self, forKey: .low))!
close = Double(try container.decode(String.self, forKey: .close))!
volume = Double(try container.decode(String.self, forKey: .volume))!
}
}
/** Since the values in the json are actually Strings, we decode them as such and then convert to Doubles. */
let dateFormatter: DateFormatter = {
let result = DateFormatter()
result.dateFormat = "yyyy-MM-dd"
return result
}()
print(try JSONDecoder().decode(ReadData.self, from: jsonData))
If you don't like all the force casting, you can bring in a castOrThrow
type function.
CodePudding user response:
Create a custom decoder and add the date to your TimeSeriesDaily structure as well. Note that it is Swift naming convention to name your properties and cases starting with a lowercase letter. I would also use Decimal instead of Double and coerce your string into a Decimal while parsing your json. Something like:
struct Stock: Codable {
let timeSeries: [TimeSeries]
enum CodingKeys: String, CodingKey {
case timeSeries = "Time Series (Daily)"
}
}
struct TimeSeries: Codable {
let date: Date
let open, high, low, close, volume: Decimal
}
extension String {
var decimal: Decimal? { Decimal(string: self) }
}
extension Stock {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dict = try container.decode([String:[String:String]].self, forKey: .timeSeries)
let formatter = DateFormatter()
formatter.locale = .init(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
formatter.timeZone = .init(secondsFromGMT: 0)!
self.timeSeries = dict.compactMap { key, value in
guard let date = formatter.date(from: key),
let open = value["1. open"]?.decimal,
let high = value["2. high"]?.decimal,
let low = value["3. low"]?.decimal,
let close = value["4. close"]?.decimal,
let volume = value["5. volume"]?.decimal
else { return nil }
return .init(date: date, open: open, high: high, low: low, close: close, volume: volume)
}
}
}