Date-Time Groups

2016-07-26

The date-time group (DTG) is a compact date/time format commonly used in militaries around the world. A date-time group includes the time, time zone and date, and therefore represents a fixed and unambiguous point in time.

This post contains a summary of the date-time group format and sample code to implement it. All of the information contained here was sourced from publicly available, non-authoritative sources and is likely to be incomplete. Those with access to an official specification of the format should use that instead of relying on anything written here.

Overview

The basic date-time group format is: ddhhmmZMMMyy, where Z is a letter representing the time zone. Judicious use of spaces can greatly aid human comprehension.

Examples:

25 July 2016 18:41 UTC+10 = 25 1841K JUL 16
23 February 2016 09:45 UTC = 23 0945Z FEB 16

Time zone letters

The time zone letters represent the UTC offset of the timezone in use, and are as follows:

Z UTC+00

A UTC+01
B UTC+02
C UTC+03
D UTC+04
E UTC+05
F UTC+06
G UTC+07
H UTC+08
I UTC+09
K UTC+10
L UTC+11
M UTC+12

N UTC-01
O UTC-02
P UTC-03
Q UTC-04
R UTC-05
S UTC-06
T UTC-07
U UTC-08
V UTC-09
W UTC-10
X UTC-11
Y UTC-12

Notes:

Month abbreviation

Months are abbreviated as follows:

January     JAN
February    FEB
March       MAR
April       APR
May         MAY
June        JUN
July        JUL
August      AUG
September   SEP
October     OCT
November    NOV
December    DEC

Common Mistakes

Code

The following code to produce date-time groups is written in swift (2.2) and implemented as an extension on NSDate. It needs the version of Foundation supplied with Xcode, not the open source Foundation that is part of the swift project.

I have chosen to give a two letter code for a half hour offset, a two letter plus asterisk code for a quarter hour offset, and the code M+ for timezones more than 12 hours ahead of UTC. The two letter code intuitively suggests the correct time zone, and the presence of a symbol for quarter hour offsets and offsets greater than 12 hours will hopefully encourage the user to investigate their time zone further.

import Foundation

extension NSDate {
    private static let dtgTimeZoneLetters: [Character] = ["Y", "X", "W", "V", "U",
                                                          "T", "S", "R", "Q", "P",
                                                          "O", "N", "Z", "A", "B",
                                                          "C", "D", "E", "F", "G",
                                                          "H", "I", "K", "L", "M"]
    private static let dtgMonthAbbreviations = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
                                                "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]

    func dateTimeGroup(timeZone timeZone: NSTimeZone) -> String? {
        let utcOffset = timeZone.secondsFromGMT
        
        // make sure we aren't more than 12 hours behind UTC
        guard utcOffset >= -12 * 3600 else { return nil }
        
        // make sure we aren't more than 14 hours ahead of UTC
        guard utcOffset <= 14 * 3600 else { return nil }
        
        // make sure we can get the gregorian calendar
        guard let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian) else { return nil }
        calendar.timeZone = timeZone
        
        let timeZoneString: String
        
        switch utcOffset {
        case let utcOffset where utcOffset > 12 * 3600:
            // more than 12 hour offset
            timeZoneString = "M+"
        case let utcOffset where utcOffset % 3600 == 0:
            // whole hour offset
            timeZoneString = String(NSDate.dtgTimeZoneLetters[utcOffset / 3600 + 12])
        default:
            // half hour or quarter hour offset
            let index = Int(floor(Double(utcOffset) / 3600.0)) + 12
            let firstLetter = String(NSDate.dtgTimeZoneLetters[index])
            let secondLetter = String(NSDate.dtgTimeZoneLetters[index + 1])
            let thirdLetter = utcOffset % 1800 == 0 ? "" : "*"
            timeZoneString = firstLetter + secondLetter + thirdLetter
        }
        
        let requiredCalendarUnits: NSCalendarUnit = [.Year, .Month, .Day, .Hour, .Minute]
        let dateComponents = calendar.components(requiredCalendarUnits, fromDate: self)
        let monthString = NSDate.dtgMonthAbbreviations[dateComponents.month - 1]
        
        return String(format: "%02d %02d%02d%@ %@ %02d",
                      dateComponents.day, dateComponents.hour, dateComponents.minute,
                      timeZoneString, monthString, dateComponents.year % 1000)
    }
    
    func dateTimeGroupLocal() -> String? {
        return self.dateTimeGroup(timeZone: NSTimeZone.localTimeZone())
    }
    func dateTimeGroupZulu() -> String? {
        return self.dateTimeGroup(timeZone: NSTimeZone(forSecondsFromGMT: 0))
    }
}

Usage:

let date = NSDate()

date.dateTimeGroupZulu()
date.dateTimeGroupLocal()

Here’s another implementation in Python (Python 3).

from datetime import datetime, timezone, timedelta
import math


DTG_TZ_LETTERS = 'YXWVUTSRQPONZABCDEFGHIKLM'
DTG_MONTHS = ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
              'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')


def dtg(dt, tz=timezone.utc):
    """
    Outputs a date time group in timezone tz for datetime dt.
    dt must be a localized datetime.
    """
    utc_offset = int(tz.utcoffset(dt).total_seconds())

    if utc_offset < -12 * 3600 or utc_offset > 14 * 3600:
        return None

    tz_string = None

    if utc_offset > 12 * 3600:
        tz_string = "M+"
    elif utc_offset % 3600 == 0:
        index = utc_offset // 3600 + 12
        tz_string = DTG_TZ_LETTERS[index: index + 1]
    else:
        index = int(math.floor(utc_offset / 3600.0)) + 12
        tz_string = DTG_TZ_LETTERS[index: index + 2]
        if utc_offset % 1800:
            tz_string += '*'

    dt_adjusted = dt.astimezone(tz)
    return '{:02d} {:02d}{:02d}{} {} {:02d}'.format(
        dt_adjusted.day,
        dt_adjusted.hour,
        dt_adjusted.minute,
        tz_string,
        DTG_MONTHS[dt_adjusted.month - 1],
        dt_adjusted.year % 1000)


if __name__ == '__main__':
    dt = datetime.now(tz=timezone.utc)
    print(dtg(dt))
    print(dtg(dt, timezone(timedelta(hours=10))))
    print(dtg(dt, timezone(timedelta(hours=5, minutes=45))))
    print(dtg(dt, timezone(timedelta(hours=14))))
    print(dtg(dt, timezone(timedelta(hours=-9))))

References:

  1. Wikipedia - Date-Time Group
  2. Wikipedia - List of Military Time Zones
  3. Astronomical Phenomena - General Interest Material Reprinted from the Astronomical Almanac
  4. Military Time Conversion & Time Zones Charts