Home > Net >  Convert dictionary values from string to double when there are 5 values per key
Convert dictionary values from string to double when there are 5 values per key

Time:09-26

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)
        }
    }
}
  • Related