Swift JSON Search Date format by Year-month? - json

I am using Eodhistoricaldata.com's API to get the monthly values of issues.
https://eodhistoricaldata.com/api/eod/VTSMX?from=2017-09-01&api_token=xxxxxx&period=m&fmt=json
And even though it is monthly data they are assigning the first trade date to the results. -01, -02, -03, etc
This means I can not use a generic date of -01. So YYYY-MM-01 does not work.
So I either have to change all the dates to -01 or search by just the year and month like "2017-10"
What is the best way to accomplish this using Swift 4 and SwiftyJSON.
Thanks.
Here is their data.
[{"date":"2017-09-01","open":"61.9300","high":63.03,"low":"61.4400","close":63.03,"adjusted_close":61.6402,"volume":0},
{"date":"2017-10-02","open":"63.3400","high":"64.5300","low":"63.3400","close":64.39,"adjusted_close":62.9703,"volume":0},
{"date":"2017-11-01","open":"64.4400","high":66.35,"low":"64.0600","close":66.35,"adjusted_close":64.8872,"volume":0},
{"date":"2017-12-01","open":"66.2100","high":"67.3500","low":"65.7700","close":66.7,"adjusted_close":65.5322,"volume":0},
{"date":"2018-01-02","open":"67.2500","high":"71.4800","low":"67.2500","close":70.24,"adjusted_close":69.0102,"volume":0},
{"date":"2018-02-01","open":"70.2400","high":"70.2400","low":"64.4000","close":67.63,"adjusted_close":66.4458,"volume":0},
....
{"date":"2018-12-03","open":"69.5700","high":"69.5700","low":"58.1700","close":62.08,"adjusted_close":62.08,"volume":0}]

Drop SwiftyJSON and use Decodable to parse the JSON into a struct. Parsing dates is highly customizable. You can add your own logic which extracts year and month from the date string and create a Date instance.
struct HistoricalData: Decodable {
let date: Date
let open, low, high : String
let close, adjustedClose, volume : Double
}
...
let jsonString = """
[{"date":"2017-09-01","open":"61.9300","high":"63.03","low":"61.4400","close":63.03,"adjusted_close":61.6402,"volume":0},
{"date":"2017-10-02","open":"63.3400","high":"64.5300","low":"63.3400","close":64.39,"adjusted_close":62.9703,"volume":0},
{"date":"2017-11-01","open":"64.4400","high":"66.35","low":"64.0600","close":66.35,"adjusted_close":64.8872,"volume":0},
{"date":"2017-12-01","open":"66.2100","high":"67.3500","low":"65.7700","close":66.7,"adjusted_close":65.5322,"volume":0},
{"date":"2018-01-02","open":"67.2500","high":"71.4800","low":"67.2500","close":70.24,"adjusted_close":69.0102,"volume":0},
{"date":"2018-02-01","open":"70.2400","high":"70.2400","low":"64.4000","close":67.63,"adjusted_close":66.4458,"volume":0},
{"date":"2018-12-03","open":"69.5700","high":"69.5700","low":"58.1700","close":62.08,"adjusted_close":62.08,"volume":0}]
"""
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom { decoder -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
let components = dateStr.components(separatedBy: "-")
guard components.count > 2, let year = Int(components[0]), let month = Int(components[1]) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date string") }
return Calendar.current.date(from: DateComponents(year: year, month: month))!
}
do {
let result = try decoder.decode([HistoricalData].self, from: data)
print(result)
} catch { print(error) }
Alternatively you can decode the string to format yyyy-MM however you have to write an initializer and add CodingKeys
struct HistoricalData: Decodable {
let date: String
let open, low, high : String
let close, adjustedClose, volume : Double
private enum CodingKeys : String, CodingKey {
case date, open, low, high, close, adjustedClose, volume
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .date)
guard let secondDashRange = dateString.range(of: "-", options: .backwards) else {
throw DecodingError.dataCorruptedError(forKey: .date, in: container, debugDescription: "Invalid date string")
}
date = String(dateString[..<secondDashRange.lowerBound])
open = try container.decode(String.self, forKey: .open)
low = try container.decode(String.self, forKey: .low)
high = try container.decode(String.self, forKey: .high)
close = try container.decode(Double.self, forKey: .close)
adjustedClose = try container.decode(Double.self, forKey: .adjustedClose)
volume = try container.decode(Double.self, forKey: .volume)
}
}
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let result = try decoder.decode([HistoricalData].self, from: data)
print(result)
} catch { print(error) }

I was able to work with the API company to overcome this issue with eodhistorical data.
They now have written a special API to get back just the last monthly data - so no more need to write crazy Json code.
There is now a special request for the data using "period = eom"
jsonUrl = "https://eodhistoricaldata.com/api/eod/(symbol).US?from=(beg)&to=(end)&api_token=(EOD_KEY)&period=eom&fmt=json"
this really made live much better. (After wasting house trying to overcome their data.)

Related

Translate a json dictionary object to Array of objects

I am getting back JSON that looks like this:
{
"success":true,
"timestamp":1650287883,
"base":"EUR",
"date":"2022-04-18",
"rates":{
"USD":1.080065,
"EUR":1,
"JPY":136.717309,
"GBP":0.828707,
"AUD":1.465437,
"CAD":1.363857
}
}
I was expecting rates to be an array, but it's an object. The currency codes may vary. Is there a way to Decode this with Swift's built-in tools?
I'm certain this won't work:
struct ExchangeRateResponse: Decodable {
let success: Bool
let base: String
let date: Date
let rates: [[String: Double]]
private enum ResponseKey: String, CodingKey {
case success, base, date, rates
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResponseKey.self)
success = try container.decode(Bool.self, forKey: .success)
base = try container.decode(String.self, forKey: .base)
date = try container.decode(Date.self, forKey: .date)
rates = try container.decode([[String: Double]].self, forKey: .rates)
}
}
your model is wrong. you can do this:
struct YourModelName {
let success: Bool
let timestamp: Int
let base, date: String
let rates: [String: Double]
}
after that you can try do decode it.
something like this:
do {
let jsonDecoder = JSONDecoder()
let loadData = try jsonDecoder.decode(YourModelName.self, from: data!)
// 'loadData' is your data that you want. for your problem you have to use 'loadData.rates'. Hint: you have to use it in 'for' loop!
DispatchQueue.main.async { _ in
// if you have to update your UI
}
} catch {
print(error)
}
First of all you cannot decode date to Date out of the box, but you can decode timestamp to Date.
Second of all it's impossible to decode a dictionary to an array. This is like apples and oranges.
But fortunately you can map the dictionary to an array because it behaves like an array (of tuples) when being mapped.
Just create an other struct Rate
struct Rate {
let code: String
let value: Double
}
struct ExchangeRateResponse: Decodable {
let success: Bool
let timestamp: Date
let base: String
let date: String
let rates: [Rate]
private enum CodingKeys: String, CodingKey {
case success, base, timestamp, date, rates
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
success = try container.decode(Bool.self, forKey: .success)
timestamp = try container.decode(Date.self, forKey: .timestamp)
base = try container.decode(String.self, forKey: .base)
date = try container.decode(String.self, forKey: .date)
let rateData = try container.decode([String: Double].self, forKey: .rates)
rates = rateData.map(Rate.init)
}
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeRateResponse.self, from: data)
print(result)
} catch {
print(error)
}
Or still shorter if you map the dictionary after decoding the stuff
struct ExchangeRateResponse: Decodable {
let success: Bool
let timestamp: Date
let base: String
let date: String
let rates: [String:Double]
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeRateResponse.self, from: data)
let rates = result.rates.map(Rate.init)
print(rates)
} catch {
print(error)
}

How to JSON encode multiple date formats within the same struct

Need to encode into JSON, a struct that has 2 Date instance variables (day and time), however, I need to encode each date instance variable with a different format, ie. for "day":"yyyy-M-d" and "time":"H:m:s".
Have written a custom decoder which works no problems. But not sure how to write the required custom encoder to solve this.
For example I can decode the following JSON string:
{ "biometrics" : [
{"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2},
{"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}]
}
However, when I encode it, I can only encode it in a single date format :(
Help, would be greatly appreciated.
Thank you.
import UIKit
let biometricsJson = """
{ "biometrics" : [
{"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2},
{"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}]
}
"""
struct Biometrics: Codable {
var biometrics: [Biometric]
}
struct Biometric: Codable {
var biometricId: Int
var unitId: Int
var source: String?
var amount: Double
var day: Date
var time: Date
init(biometricId: Int, unitId: Int, source: String, amount: Double, day: Date, time: Date){
self.biometricId = biometricId
self.unitId = unitId
self.source = source
self.amount = amount
self.day = day
self.time = time
}
}
extension Biometric {
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-M-d"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}()
}
let biometrics = try Biometric.decoder.decode(Biometrics.self, from:biometricsJson.data(using: .utf8)!)
let jsonEncoder = JSONEncoder()
let encodedJson = try jsonEncoder.encode(biometrics)
let jsonString = String(data: encodedJson, encoding: .utf8)
if biometricsJson != jsonString {
print("error: decoding, then encoding does not give the same string")
print("biometricsJson: \(biometricsJson)")
print("jsonString: \(jsonString!)")
}
I expect the encoded JSON, to be decodable by the decoder.
i.e. biometricsJson == jsonString
In a custom encode(to:), just encode each one as a string using the desired formatter. There's no "date" type in JSON; it's just a string. Something along these lines:
enum CodingKeys: CodingKey {
case biometricId, amount, source, day, time, unitId
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(biometricId, forKey: .biometricId)
try container.encode(unitId, forKey: .unitId)
try container.encode(source, forKey: .source)
try container.encode(amount, forKey: .amount)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
let timeString = formatter.string(from: time)
try container.encode(timeString, forKey: .time)
formatter.dateFormat = "yyyy-M-d"
let dayString = formatter.string(from: day)
try container.encode(dayString, forKey: .day)
}
But note that you can't test for equivalent strings. JSON dictionaries aren't order-preserving, so there's no way to guarantee a character-by-character match.
Note that if you really want to have days and times, you should consider DateComponents rather than a Date. A date is a specific instance in time; it's not in any time zone, and it can't be just an hour, minute, and second.
Also, your use of Double is going to cause rounding differences. So 2.1 will be encoded as 2.1000000000000001. If that's a problem, you should use Decimal for amount rather than Double.

Parsing json blob field in Swift

I am reading JSON from a web service in swift which is in the following format
[{
"id":1,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"5555555",
"logo":[-1,-40,-1,-32],
"shopPath":"test"
},
{
"id":2,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"66666666",
"logo":[-1,-50,-2,-2],
"shopPath":"test"
}]
I have managed to read all the strings easily but when it comes to the logo part I am not sure what should I do about it, this is a blob field in a mySQL database which represent an image that I want to retrieve in my swift UI, here is my code for doing that but i keep getting errors and not able to figure the right way to do it:
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: [String]
let path: String
}
func getBrandsJson() {
let url = URL(string: "http://10.211.55.4:8080/exam/Test")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else {
print(error!);
return
}
print(response.debugDescription)
let decoder = JSONDecoder()
let classes = try! decoder.decode([Brand].self, from: data)
for myClasses in classes {
print(myClasses.branch)
if let imageData:Data = myClasses.logo.data(using:String.Encoding.utf8){
let image = UIImage(data:imageData,scale:1.0)
var imageView : UIImageView!
}
}
}).resume()
}
Can someone explain how to do that the right way I have searched a lot but no luck
First of all, you should better tell the server side engineers of the web service, that using array of numbers is not efficient to return a binary data in JSON and that they should use Base-64 or something like that.
If they are stubborn enough to ignore your suggestion, you should better decode it as Data in your custom decoding initializer.
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: Data
let path: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: CodingKeys.id)
self.name = try container.decode(String.self, forKey: CodingKeys.name)
self.branch = try container.decode(String.self, forKey: CodingKeys.branch)
self.address = try container.decode(String.self, forKey: CodingKeys.address)
self.phone = try container.decode(String.self, forKey: CodingKeys.phone)
self.path = try container.decode(String.self, forKey: CodingKeys.path)
//Decode the JSON array of numbers as `[Int8]`
let bytes = try container.decode([Int8].self, forKey: CodingKeys.logo)
//Convert the result into `Data`
self.logo = Data(bytes: bytes.lazy.map{UInt8(bitPattern: $0)})
}
}
And you can write the data decoding part of your getBrandsJson() as:
let decoder = JSONDecoder()
do {
//You should never use `try!` when working with data returned by server
//Generally, you should not ignore errors or invalid inputs silently
let brands = try decoder.decode([Brand].self, from: data)
for brand in brands {
print(brand)
//Use brand.logo, which is a `Data`
if let image = UIImage(data: brand.logo, scale: 1.0) {
print(image)
//...
} else {
print("invalid binary data as an image")
}
}
} catch {
print(error)
}
I wrote some lines to decode array of numbers as Data by guess. So if you find my code not working with your actual data, please tell me with enough description and some examples of actual data. (At least, you need to show me a first few hundreds of the elements in the actual "logo" array.)
Replace
let logo: [String]
with
let logo: [Int]
to get errors use
do {
let classes = try JSONDecoder().decode([Brand].self, from: data)
}
catch {
print(error)
}

Swift4, JSON, keyNotFound, No value associated with key

I need to do Sunset Sunrise App, and this is my code. But I have this error:
Error serializing json: keyNotFound(Sunrise_Sunset.SunPosition.Results.(CodingKeys in _B4291256871B16D8D013EC8806040532).sunrise, Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key sunrise (\"sunrise\").", underlyingError: nil))
And I don't understand how to fix it. Maybe someone had this problem. I will be grateful for any help)
This is the API, which I have: https://sunrise-sunset.org/api
struct SunPosition: Codable {
struct Results: Codable {
let sunrise: String
let sunset: String
let solarNoon: String
let dayLenght: String
let civilTwilightBegin: String
let civilTwilightEnd: String
let nauticalTwilightBegin: String
let nauticalTwilightEnd: String
let astronomicalTwilightBegin: String
let astronomicalTwilightEnd: String
enum CodingKey:String, Swift.CodingKey {
case sunrise = "sunrise"
case sunset = "sunset"
case solarNoon = "solar_noon"
case dayLenght = "day_length"
case civilTwilightBegin = "civil_twilight_begin"
case civilTwilightEnd = "civil_twilight_end"
case nauticalTwilightBegin = "nautical_twilight_begin"
case nauticalTwilightEnd = "nautical_twilight_end"
case astronomicalTwilightBegin = "astronomical_twilight_begin"
case astronomicalTwilightEnd = "astronomical_twilight_end"
}
}
}
extension SunPosition.Results {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
sunrise = try values.decode(String.self, forKey: .sunrise)
sunset = try values.decode(String.self, forKey: .sunset)
solarNoon = try values.decode(String.self, forKey: .solarNoon)
dayLenght = try values.decode(String.self, forKey: .dayLenght)
civilTwilightBegin = try values.decode(String.self, forKey: .civilTwilightBegin)
civilTwilightEnd = try values.decode(String.self, forKey: .civilTwilightEnd)
nauticalTwilightBegin = try values.decode(String.self, forKey: .nauticalTwilightBegin)
nauticalTwilightEnd = try values.decode(String.self, forKey: .nauticalTwilightEnd)
astronomicalTwilightBegin = try values.decode(String.self, forKey: .astronomicalTwilightBegin)
astronomicalTwilightEnd = try values.decode(String.self, forKey: .astronomicalTwilightEnd)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let sunPosition = try JSONDecoder().decode(SunPosition.Results.self, from: data)
print(sunPosition)
}catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
}
Four issues:
Add let results : Results in the SunPosition struct.
Typo: private enum CodingKeys: String, CodingKey rather than enum CodingKey :String, Swift.CodingKey, note the singular / plural difference, the private attribute is recommended but does not cause the issue.
Wrong type to decode: JSONDecoder().decode(SunPosition.self, from: data) rather than JSONDecoder().decode(SunPosition.Results.self, from: data).
To get the results you have to print(sunPosition.results).
Three notes:
Delete the entire extension. In this case you get the initializer for free.
Add &formatted=0 to the URL and set the dateDecodingStrategy of the decoder to .iso8601 to get Date objects. Change the type of all date related properties from String to Date and the type of dayLenght from String to TimeInterval. To change dateDecodingStrategy write
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let sunPosition = try decoder.decode(SunPosition.self ...
I recommend to handle the status, too. Add this in the SunPosition struct
let status : String
var success : Bool { return status == "OK" }
To rule it out, if for some reason your endpoint is returning an error message or nil data, you'll get this error.

Swift 4 Not decoding JSON optional properly

Swift noob here.
I'm trying to follow the App Development With Swift book and am running into trouble with decoding JSON data from the NASA API as given in the examples. Here's the code I'm trying to use:
struct PhotoInfo: Codable {
var title: String
var description: String
var url: URL
var copyright: String?
enum CodingKeys: String, CodingKey {
case title
case description = "explanation"
case url
case copyright
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
self.title = try valueContainer.decode(String.self, forKey: CodingKeys.title)
self.description = try valueContainer.decode(String.self, forKey: CodingKeys.description)
self.url = try valueContainer.decode(URL.self, forKey: CodingKeys.url)
self.copyright = try valueContainer.decode(String.self, forKey: CodingKeys.copyright)
}
}
func fetchPhotoInfo(completion: #escaping (PhotoInfo?) -> Void) {
let baseURL = URL(string: "https:/
let query: [String: String] = [
"api_key": "yN3**0scRWo12gCa25TWBcfp3rcuAnoeqwbpvLPn",
"date": "2011-07-13"
]
let url = baseURL.withQueries(query)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let photoInfo = try? jsonDecoder.decode(PhotoInfo.self, from: data) {
print(data)
completion(photoInfo)
} else {
print("Either no data was returned, or data was not properly decoded.")
completion(nil)
}
}
task.resume()
}
When I remove the copyright code from the PhotoInfo struct, it decodes the JSON and prints the data (line 36). Otherwise, it doesn't deserialize it. Is there a way I can troubleshoot why this is happening? Does it have something to do with the optional?
If copyright is optional , then you can make use of decodeIfPresent.
self.copyright = try valueContainer.decodeIfPresent(String.self, forKey: CodingKeys.copyright)
EDIT: Updated per #matt's comments.
technerd's answer is probably best, but I faced the same problem and dealt with it by changing try to try?. That's how it was presented in previous exercises, anyway. However, as #matt points out, this doesn't deal with the possibility that the copyright information isn't simply missing, but in fact something other than what was expected. Using try? in tandem with an if let statement provides at least basic feedback.