-1

I came across an issue when I was doing an online exercise for Swfit on Exercism. However, the code I wrote haven't been able to pass the test suite provided by the website. The problem seems to lie with the date format of the resulting date object wrapped in an optional.

I was able to get the formate of incoming dateString to conform to the test suite date format. However, I was unable to make the destinationDate also conform to the test suite date format.

I tried to use the ISO8601DateFormatter, but the compiler on my old Mac doesn't suport this class. I tried my code on online Swift compilers, but the results haven't been satisfying so far, either.

The exercise is described as follows:

Calculate the moment when someone has lived for 10^9 seconds.

A gigasecond is 10^9 (1,000,000,000) seconds.

I wrote the following code:

    import Foundation
    
    func Gigasecond(from dateString: String) -> Date? {
        let GIGASECOND: Double = 1_000_000_000
        let RFC3339DateFormatter = DateFormatter()
    
        RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX")
        RFC3339DateFormatter.dateFormat = "yyyy-MM-dd'T'hh:mm:ss"
    
        let sourceDate = RFC3339DateFormatter.date(from: dateString)
        var destinationDate: Date? = Date(timeInterval: GIGASECOND, since: sourceDate ?? Date())
        let destDateString = RFC3339DateFormatter.string(from: destinationDate ?? Date())
        destinationDate = RFC3339DateFormatter.date(from: destDateString)
      
        return destinationDate
    }

The test suite for this exercise provided by the website is as follows:

//GigasecondTests.swift

import XCTest
@testable import Gigasecond

class GigasecondTests: XCTestCase {

    func test1 () {
        let gs = Gigasecond(from: "2011-04-25T00:00:00")?.description
        XCTAssertEqual("2043-01-01T01:46:40", gs)
    }

    func test2 () {
        let gs = Gigasecond(from: "1977-06-13T00:00:00")?.description
        XCTAssertEqual("2009-02-19T01:46:40", gs)
    }

    func test3 () {
        let gs = Gigasecond(from: "1959-07-19T00:00:00")?.description
        XCTAssertEqual("1991-03-27T01:46:40", gs)
    }

    func testTimeWithSeconds () {
        let gs = Gigasecond(from: "1959-07-19T23:59:59")?.description
        XCTAssertEqual("1991-03-28T01:46:39", gs)
    }

    func testFullTimeSpecified () {
        let gs = Gigasecond(from: "2015-01-24T22:00:00")?.description
        XCTAssertEqual("2046-10-02T23:46:40", gs)
    }

    func testFullTimeWithDayRollOver () {
        let gs = Gigasecond(from: "2015-01-24T23:59:59")?.description
        XCTAssertEqual("2046-10-03T01:46:39", gs)
    }

    static var allTests: [(String, (GigasecondTests) -> () throws -> Void)] {
        return [
            ("test1 ", test1 ),
            ("test2 ", test2 ),
            ("test3 ", test3 ),
            ("testTimeWithSeconds ", testTimeWithSeconds ),
            ("testFullTimeSpecified ", testFullTimeSpecified ),
            ("testFullTimeWithDayRollOver ", testFullTimeWithDayRollOver ),
        ]
    }
} 

// LinuxMain.swift
import XCTest
@testable import GigasecondTests

XCTMain([
    testCase(GigasecondTests.allTests),
    ])

Please help see what problem there is with my code. Thank you very much!

Michael May
  • 286
  • 2
  • 11
  • 1
    You are using the wrong dateFormat `hh` is for 01-12 what you need is `HH` 00-23. `dateFormat = "yyyy-MM-dd'T'HH:mm:ss"`. Note that description will result in a UTC date description (not the current timezone) – Leo Dabus Jul 14 '20 at 05:51
  • 1
    Must your method return a `Date`? It should return a `String`, judging from the test cases... – Sweeper Jul 14 '20 at 05:57
  • @Sweeper Test case will never be thrust worthy if OP uses a Date. It will always loose precision. He would need to use date formatter to generate the string from the parsed date to test for equality – Leo Dabus Jul 14 '20 at 06:02
  • @Sweeper https://stackoverflow.com/questions/60136982/how-to-resolve-error-in-unit-testing-when-we-have-date-comparison-in-codable#comment106361861_60136982 – Leo Dabus Jul 14 '20 at 06:23
  • @LeoDabus Yeah, that's what I mean... – Sweeper Jul 14 '20 at 06:25
  • Thank you, @LeoDabus, for your advice. I tried using "yyyy-MM-dd'T'HH:mm:ss", but the tests still failed. – Michael May Jul 14 '20 at 07:15
  • Thank you, @Sweeper, for your suggestion. I changed the return type to `String?' and the `dateFormat` to `"yyyy-MM-dd'T'HH:mm:ss"`, and ran the tests. Two out of the six tests passed. The formats of the `Strings` seemed to match, but there were differences in time in terms of hours. – Michael May Jul 14 '20 at 07:19

3 Answers3

4

You are using the wrong dateFormat hh is for 01-12 hours. What you need is HH 00-23 hours. Your dateFormat should be "yyyy-MM-dd'T'HH:mm:ss". Note that Date description will return a UTC date description (not the current timezone). You would need to use the same date formatter to generate the string from the parsed date to test for equality:

extension Formatter {
    static let rfc3339: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        return dateFormatter
    }()
}

Your gigasecond method can be simplified as:

func gigasecond(from dateString: String) -> Date? {
    return Formatter.rfc3339.date(from: dateString)?
        .addingTimeInterval(1_000_000_000)
}

Testing:

if let gs = gigasecond(from: "2011-04-25T00:00:00") {
    "2043-01-01T01:46:40" == Formatter.rfc3339.string(from: gs)  // true
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thank you, @LeoDabus, for your solution. I ran it through the test suite and the tests failed. The test report is too long to paste in here. In summary, the dates produced by your code didn't match those provided by the test suite. Anyway, thank you again for your kind help. – Michael May Jul 14 '20 at 07:29
  • 1
    I have tested test1 and it worked. Feel free to edit your question – Leo Dabus Jul 14 '20 at 07:53
0

I made some tweaks to my code and had the luck of having it pass the test suite. My modified code is as follows:

import Foundation

func Gigasecond(from dateString: String) -> String? {
    let GIGASECOND: Double = 1_000_000_000
    let RFC3339DateFormatter = DateFormatter()

    RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX")
    RFC3339DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
    RFC3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)

    let sourceDate = RFC3339DateFormatter.date(from: dateString)
    var destinationDate: Date? = Date(timeInterval: GIGASECOND, since: sourceDate ?? Date())
    var destDateString = RFC3339DateFormatter.string(from: destinationDate ?? Date())
    destinationDate = RFC3339DateFormatter.date(from: destDateString)
    destDateString = RFC3339DateFormatter.string(from: destinationDate ?? Date())
  
    return destDateString
}

It turned out that the resulting destinationDate needs to be reformatted with the date format "yyyy-MM-dd'T'HH:mm:ss" to obtain the required date string.

My heartfelt thanks again go to all those who have participated in the discussion about the solutions to this question. Without their advice, I wouldn't have moved towards working out a solution.

I'm expecting further discussion about more and better ways of solving the question. Thanks.

Michael May
  • 286
  • 2
  • 11
  • 1
    There is no timezone info in your string. Usually if the string has no timezone you can't really know if it represents a local time or UTC time. Generally when you store the date in a server you use UTC timezone and add `Z`, "+0000" or "+00:00" to your date string to avoid this kind of situation. I wonder why would you have some tests with current local time and others with UTC/GMT time. – Leo Dabus Jul 14 '20 at 15:04
  • Thank you, @LeoDabus, for your comment. You're right. I should have added a `TimeZone(identifier: "UTC")` to the date formatter. That's also one of the suggestions for revision given by my mentor on the Exercism website. According to my mentor's suggestions, I've rewritten the code which is suited to a more broader context of application. I'm bringing up the revised code as an alternative solution for this thread. – Michael May Jul 15 '20 at 00:58
0

Here's an alternative solution to this question, which is suitable for a broader context of application. This solution is based on the suggestions of my mentor on the Exercism website.

My thanks also go to Leo Dabus who suggested that I add time zone information to the date formatter to avoid the effects of the implicitly adopted default local time zone.

The code is as follows:

import Foundation

struct Gigasecond {
    public let gigasecond : Double = 1_000_000_000
    private let rfc3339DateFormatter : DateFormatter = { 
            let myFormatter = DateFormatter()
            myFormatter.timeZone = TimeZone(identifier: "UTC")
            myFormatter.locale = Locale(identifier: "en_US_POSIX")
            myFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"

            return myFormatter 
    }()
    let description : String

    init?(from dateString: String) {

        let sourceDate = rfc3339DateFormatter.date(from: dateString)
        guard let srcDate = sourceDate else { 
            description = "" 
            return 
        }
        let destinationDate = Date(timeInterval: gigasecond, since: srcDate)
        description = rfc3339DateFormatter.string(from: destinationDate)
    }
}

Please let me know if there's anything I can do to improve the above code. Thank you ver much.

Michael May
  • 286
  • 2
  • 11
  • 1
    no need to add multiple answers to your question. You could edit your original post or your other answer – Leo Dabus Jul 15 '20 at 01:17
  • 1
    Note that you should set your DateFormatter locale to "en_US_POSIX" as I have already mentioned in my post – Leo Dabus Jul 15 '20 at 01:19
  • 1
    It is Swift naming convention to name your properties starting with a lowercase letter – Leo Dabus Jul 15 '20 at 01:20
  • 1
    If you would like to see how to create a proper RFC3339 date formatter check this post https://stackoverflow.com/questions/28016578/how-to-create-a-date-time-stamp-and-format-as-iso-8601-rfc-3339-utc-time-zone/28016692#28016692 – Leo Dabus Jul 15 '20 at 01:21
  • Thank you, @LeoDabus, for your suggestions. I've edited my code in the above answer accordingly. I put up multiple answers here to collect comments on them so that I can know the good and bad points in each answer. Thanks again for your helpful suggestions. – Michael May Jul 15 '20 at 01:30
  • 1
    Just make sure to always set your locale before setting the dateFormat. It should usually be the first property to be set. – Leo Dabus Jul 15 '20 at 01:32