2
\$\begingroup\$

I want to significantly tidy this code as its cluttered and repetitive

var myDate = "No MOT"

if ((myData[0]["motTests"][0]["expiryDate"].string) != nil) {

    let theDate=myData[0]["motTests"][0]["expiryDate"].string
    let msYear = theDate!.prefix(4)
    let msMonth = theDate!.suffix(5).prefix(2)
    let msDay = theDate!.suffix(2)
    let mYear = Int (msYear)
    let mMonth = Int (msMonth)
    let mDay = Int(msDay)
    self.motDue = self.convertDate(aYear: mYear!, aMonth: mMonth!, aDay: mDay!)

    myDate=msDay+("/")+msMonth+"/"+msYear
}
if ((myData[0]["motTestExpiryDate"].string) != nil){
    let theDate=myData[0]["motTestExpiryDate"].string

    let msYear = theDate!.prefix(4)
    let msMonth = theDate!.suffix(5).prefix(2)
    let msDay = theDate!.suffix(2)

    let mYear = Int (msYear)
    let mMonth = Int (msMonth)
    let mDay = Int(msDay)
    self.motDue = self.convertDate(aYear: mYear!, aMonth: mMonth!, aDay: mDay!)

    myDate=msDay+("/")+msMonth+"/"+msYear
}

self.myMOT.text=String(myDate)

The code checks to see if the vehicle has an MOT, if it does its sets myDate to the date. If not it checks when the first MOT is due and sets myDate to that's and if not myDate stays set to "No MOT"

It works but needs improving.

\$\endgroup\$
  • 1
    \$\begingroup\$ Can you describe the exact format of myData, and the possible formats of the expiry date that you want to parse? \$\endgroup\$ – Martin R Nov 29 '19 at 18:51
3
\$\begingroup\$

Best practice is to get away from manipulating date strings yourself. And use Date types in your model.

So, when you parse the JSON, use DateFormatter to convert the non-user-friendly date strings to Date objects. And when presenting the dates in the UI, use another DateFormatter to present the date in a nice, user-friendly format. I’d also suggest using JSONDecoder for decoding the JSON into your model, so you don’t have to sprinkle your code with cryptic, error-prone, dictionary keys.

So, first, I would suggest specifying a date formatter when you decode. Because most of the dates are in the form yyyy.MM.dd, I’d use that. So, for example:

do {
    let decoder = JSONDecoder()
    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.dateFormat = "yyyy.MM.dd"
    decoder.dateDecodingStrategy = .formatted(formatter)
    let result = try decoder.decode([Vehicle].self, from: data)
    print(result)
} catch {
    print(error)
}

The trick, though, is that one field, completedDate in MOTTest, uses a different string format, namely, yyyy.MM.dd HH:mm:ss, so I’d have JSONDecoder just parse that as a string, but then have a computed property to translate that one date/time string into a Date:

struct Vehicle: Decodable {
    let registration: String
    let make: String
    let model: String
    let firstUsedDate: String
    let fuelType: String
    let primaryColour: String
    let vehicleId: String
    let registrationDate: Date
    let manufactureDate: Date
    let engineSize: String
    let motTests: [MOTTest]
}

struct MOTTest: Decodable {
    private static let formatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy.MM.dd HH:mm:ss"
        return formatter
    }()

    let completedDateString: String
    var completedDate: Date? { MOTTest.formatter.date(from: completedDateString) }
    let testResult: String
    let expiryDate: Date?
    let odometerValue: String
    let odometerUnit: String
    let odometerResultType: String
    let motTestNumber: String
    let rfrAndComments: [ReasonForRejection]

    private enum CodingKeys: String, CodingKey {
        case completedDateString = "completedDate"
        case testResult, expiryDate, odometerValue, odometerUnit, odometerResultType, motTestNumber, rfrAndComments
    }
}

struct ReasonForRejection: Decodable {
    let text: String
    let type: String
    let dangerous: Bool
}

That way, JSONDecoder not only does the decoding for us to our model structure (avoiding that unstructured collection returned by JSONSerialization), but it also converts all of those yyyy.MM.dd strings to Date instances for us. And for completedDate, our computed property takes care of that for us.


By the way, I used the following sample JSON:

[
    {
        "registration": "ZZ99ABC",
        "make": "FORD",
        "model": "FOCUS",
        "firstUsedDate": "2010.11.13",
        "fuelType": "Petrol",
        "primaryColour": "Yellow",
        "vehicleId": "4Tq319nVKLz+25IRaUo79w==",
        "registrationDate": "2010.11.13",
        "manufactureDate": "2010.11.13",
        "engineSize": "1800",
        "motTests":[
            {
                "completedDate": "2013.11.03 09:33:08",
                "testResult": "PASSED",
                "expiryDate": "2014.11.02",
                "odometerValue": "47125",
                "odometerUnit": "mi",
                "odometerResultType": "READ",
                "motTestNumber": "914655760009",
                "rfrAndComments": []
            },
            {
                "completedDate": "2013.11.01 11:28:34",
                "testResult": "FAILED",
                "odometerValue": "47118",
                "odometerUnit": "mi",
                "odometerResultType": "READ",
                "motTestNumber": "841470560098",
                "rfrAndComments":[
                    {
                        "text": "Front brake disc excessively pitted (3.5.1h)",
                        "type": "FAIL",
                        "dangerous": true
                    },
                    {
                        "text": "Nearside Rear wheel bearing has slight play (2.6.2)",
                        "type": "ADVISORY",
                        "dangerous": false
                    }
                ]
            },
            {
                "completedDate": "2018.05.20 11:28:34",
                "testResult": "FAILED",
                "odometerValue": "57318",
                "odometerUnit": "mi",
                "odometerResultType": "READ",
                "motTestNumber": "741489560458",
                "rfrAndComments":[
                    {
                        "text": "Nearside Parking brake efficiency below requirements (1.4.2 (a) (i))",
                        "type": "MAJOR",
                        "dangerous": false
                    },
                    {
                        "text": "Front brake disc excessively pitted (3.5.1h)",
                        "type": "DANGEROUS",
                        "dangerous": false
                    },
                    {
                        "text": "tyres wearing unevenly",
                        "type": "USER ENTERED",
                        "dangerous": true
                    }
                ]
            }
        ]
    },
    {
        "registration": "YY09DEF",
        "make": "BMW",
        "model": "Z4",
        "firstUsedDate": "2009.01.25",
        "fuelType": "Petrol",
        "primaryColour": "Green",
        "vehicleId": "3Fv916dPLGx=43PRaKa45e++",
        "registrationDate": "2009.01.25",
        "manufactureDate": "2009.01.25",
        "engineSize": "1800",
        "motTests":[
            {
                "completedDate": "2012.01.10 10:27:56",
                "testResult": "PASSED",
                "expiryDate": "2013.01.09",
                "odometerValue": "12345",
                "odometerUnit": "mi",
                "odometerResultType": "READ",
                "motTestNumber": "345655760009",
                "rfrAndComments": []
            }
        ]
    }
]

Needless to say, if you want to decode manually, like you did in your question, just have two parsing date formatters, one for the yyyy.MM.dd format and another for the yyyy.MM.dd HH:mm:ss format.


Finally, you might ask “how do I show these Date objects in my UI?”

You define UI date formatters:

let dateOnlyFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
}()

let dateTimeFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .medium
    return formatter
}()

Then you can do things like:

if let date = result.first?.motTests.first?.expiryDate {
    let string = dateOnlyFormatter.string(from: date)
    print("Expiry date:", string)
}

Expiry date: 2 Nov 2014

And

if let date = result.first?.motTests.first?.completedDate {
    let string = dateTimeFormatter.string(from: date)
    print("Completed date:", string)
}

Completed date: 3 Nov 2013 at 9:33:08

Clearly, use whatever dateStyle and timeStyle you want. But the idea is that we’ll show these strings in a nice, localized format as dictated by the user’s device settings.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

I'm not sure exactly what the code is trying to do, and I don't know Swift, so my answer is a bit of a mess.

I'm assuming that:

  • there is a date in the format of YYYY/MM/dd, so for example 2019/11/30

  • that date could be in one of seperate places of a data structure

  • the end result is a date which is extracted from two seperate places and is converted into a Swift date format class that can be used for other things

  • I don't know what self.convertDate does; I assume that it is taking the date and returning a Swift date class of some kind, or adding X number of years to calculate when the next inspection will be.

I got the date parsing code from https://stackoverflow.com/questions/36861732/convert-string-to-date-in-swift

Since the bodies of the two if statements are identical, I factored it out. This returns the parsed version of the date in a Swift class (I presume then it can be printed out in any way you want using Swift's date formatter.)

This code is incomplete but is the best I could do:

var myDate = "No MOT"
var theDate = ""
if ((myData[0]["motTests"][0]["expiryDate"].string) != nil) {
    theDate = myData[0]["motTests"][0]["expiryDate"].string
} else if ((myData[0]["motTestExpiryDate"].string) != nil){
    theDate = myData[0]["motTestExpiryDate"].string
}

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
let date = dateFormatter.date(from:theDate)!
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.