Convert SQLite Julian date to a specific date string format for display

I have a function that queries an SQLite database and returns names, dates and values. In SQLite the dates are in Julian format. I convert the dates in the SQLite query to a date time string which give me "2022-08-01 00:00:00". I want to display this date as the date string "Aug 1, 2022". The only way I have been able to achieve this is to convert the first date string to a date via a date formatter then convert this date to the desired date string via a second date formatter. Is there a more direct way to do this?

func AccessSQLiteDB(db: OpaquePointer?) {

    let queryTradingDaysStatement = """

    WITH
    TempTable1
    AS
    (
    SELECT
        max(TimeStamp) - 365.25 as StartingDate
    FROM
        TradingDays
    WHERE
        FundName = 'Fund1'
    ),
    
    TempTable2
    AS
    (
    SELECT
        main.FundName,
        datetime(main.TimeStamp) as SQLDateString,
        main.Close
    FROM
        TradingDays main, TempTable1 as temp
    WHERE
        main.FundName = 'Fund1'
        AND
        main.TimeStamp >= temp.StartingDate
    )
    
    SELECT
        FundName,
        SQLDateString,
        Close
    FROM
        TempTable2
    ORDER By
        SQLDateString ASC
    ;
    """

    let sqlDateStringFormatter = DateFormatter()
    sqlDateStringFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    sqlDateStringFormatter.timeZone = .gmt
    
    var queryTradingDaysCPtr: OpaquePointer?
    
    if sqlite3_prepare_v2(db, queryTradingDaysStatement, -1, &queryTradingDaysCPtr, nil) == SQLITE_OK {
        
        while (sqlite3_step(queryTradingDaysCPtr) == SQLITE_ROW) {

            let fundName = sqlite3_column_text(queryTradingDaysCPtr, 0)
            let timeStamp = sqlite3_column_text(queryTradingDaysCPtr, 1)
            let close = sqlite3_column_double(queryTradingDaysCPtr, 2)
            
            let fundNameAsString = String(cString: fundName!)
            let timeStampAsString = String(cString: timeStamp!)
            print(timeStampAsString)                   // returns this format 2022-08-01 00:00:00
            
            let timeStampAsDate: Date = sqlDateStringFormatter.date(from: timeStampAsString)!
            print("\(timeStampAsDate)")                // returns this format 2022-08-01 00:00:00 +0000
            
            let mediumDataFormatter = DateFormatter()
            mediumDataFormatter.dateStyle = .medium
            mediumDataFormatter.timeZone = .gmt
            
            let dateString = mediumDataFormatter.string(from: timeStampAsDate)
            print(dateString)                          //  returns this format Aug 1, 2022
            
            let closeAsString = String(format: "$%.2f", close)
            
            print(fundNameAsString + " - " + dateString + " - " + closeAsString)
            
        } // end while loop
        
    } else {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("\nQuery is not prepared \(errorMessage)")
    }
    sqlite3_finalize(queryTradingDaysCPtr)
}

Accepted Reply

Is there a more direct way to do this?

Definitely. Currently it’s doing all this:

  1. calling datetime() within SQLite to convert a stored Julian value to a string;
  2. using one DateFormatter to parse that string into a Date value;
  3. using a second DateFormatter to convert that Date value back to a string.

You can eliminate 1 and 2. Just project the raw Julian value in your query (fetching it via sqlite3_column_double) and then construct your Date value directly using init(timeIntervalSinceReferenceDate:). It just requires simple math to convert from Julian (days since 4713 BC) to Apple reference date (seconds since 2001).

Also this will help your ORDER BY clause a little, since now it will be sorting by a number rather than a string.

And (basic “code review” comment) you can construct mediumDateFormatter just once, before the loop. No need to do it inside the loop.

Replies

Is there a more direct way to do this?

Definitely. Currently it’s doing all this:

  1. calling datetime() within SQLite to convert a stored Julian value to a string;
  2. using one DateFormatter to parse that string into a Date value;
  3. using a second DateFormatter to convert that Date value back to a string.

You can eliminate 1 and 2. Just project the raw Julian value in your query (fetching it via sqlite3_column_double) and then construct your Date value directly using init(timeIntervalSinceReferenceDate:). It just requires simple math to convert from Julian (days since 4713 BC) to Apple reference date (seconds since 2001).

Also this will help your ORDER BY clause a little, since now it will be sorting by a number rather than a string.

And (basic “code review” comment) you can construct mediumDateFormatter just once, before the loop. No need to do it inside the loop.

Great solution. It makes the query simpler and as you indicated, only requires some simple math to convert between Julian Days and Apples seconds. And yes you are absolutely right that it was a mistake for me to declare a date formatter in a loop. Below is the updated code.

func AccessSQLiteData(db: OpaquePointer?) {

    let queryTradingDaysStatement = """

    WITH
    TempTable1
    AS
    (
    SELECT
        max(TimeStamp) - 365.25 as StartingDate
    FROM
        TradingDays
    WHERE
        FundName = 'Fund1'
    )
    
    SELECT
        main.FundName,
        main.TimeStamp,
        main.Close
    FROM
        TradingDays as main, TempTable1 as temp
    WHERE
        main.FundName = 'Fund1'
        AND
        main.TimeStamp >= temp.StartingDate
    ORDER By
        main.TimeStamp ASC
    ;
    """

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .medium
    dateFormatter.timeZone = .gmt
    
    let daysBetween4713And2001: Double = 2451910.500000
    let secondsPerDay: Double = 86400.00
    
    var queryTradingDaysCPtr: OpaquePointer?
    
    if sqlite3_prepare_v2(db, queryTradingDaysStatement, -1, &queryTradingDaysCPtr, nil) == SQLITE_OK {
        
        while (sqlite3_step(queryTradingDaysCPtr) == SQLITE_ROW) {

            let fundName = sqlite3_column_text(queryTradingDaysCPtr, 0)
            let daysSince4713BC = sqlite3_column_double(queryTradingDaysCPtr, 1)
            let close = sqlite3_column_double(queryTradingDaysCPtr, 2)
            
            let fundNameAsString = String(cString: fundName!)
            let daysSinceJanOne2001 = daysSince4713BC - daysBetween4713And2001
            let secondsSinceJanOne2001 = daysSinceJanOne2001 * secondsPerDay
            let timeStamp = Date(timeIntervalSinceReferenceDate: secondsSinceJanOne2001)
            let formattedTimeStamp = dateFormatter.string(from: timeStamp)
            let closeAsString = String(format: "$%.2f", close)
            
            print(fundNameAsString + " - " + formattedTimeStamp + " - " + closeAsString)
        } // end while loop
        
    } else {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("\nQuery is not prepared \(errorMessage)")
    }
    sqlite3_finalize(queryTradingDaysCPtr)
}