Formatted Dates

February 21, 2020

Regular readers of this blog know that I work with a team of programmers, sysadmins and database administrators to maintain a large legacy database application, running on Oracle and HP-UX, and Scheme is nowhere in sight. Lately I have been “stealth programming” by writing awk programs in shell wrappers, because shell programming is a normal part of our environment. One thing I have been doing is formatting reports with awk. That frequently requires a formatted date string, either for today or some other day; gawk provides the strftime function to format dates, but Posix awk, which is what HP-UX provides, doesn’t. So I wrote my own.

Your task is to write a function that formats dates; use any convention you like to determine how the date is formatted. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.

Advertisement

Pages: 1 2

3 Responses to “Formatted Dates”

  1. matthew said

    Let’s display the Modified Julian Date. Some C:

    #include <time.h>
    #include <stdio.h>
    
    int main() {
      time_t t = time(0);
      printf("MJD %.5f\n", double(t)/86400 + 40587);
    }
    
    $ g++ -Wall date.cpp -o date
    $ ./date
    MJD 58900.62108
    
  2. Pascal Bourguignon said
    
    
    #|
    
    In Common Lisp, dates are represented as universal-time, ie. an
    non-negative integer number of seconds since 1900-01-01 00:00:00 UTC.
    CL provides functions to convert between this universal-time integer,
    and seconds, minutes, hours, day, month and year (given a timezone).
    
    universal time n. time, represented as a non-negative integer number
    of seconds. Absolute universal time is measured as an offset from the
    beginning of the year 1900 (ignoring leap seconds). See Section
    25.1.4.2 (Universal Time).
    
    (decode-universal-time 0 0)
    --> 0
        0
        0
        1
        1
        1900
        0
        nil
        0
    
    Note, that for timezones west of GMT, decode-universal-time can give
    dates before 1900-01-01 (GMT-x) (also, note that since CL was
    standardized by Americans, timezones such as GMT-x are represented by
    x, while timezones such as GMT+x are represneted by -x).
    
    (decode-universal-time 0 8)
    --> 0
        0
        16
        31
        12
        1899
        6
        nil
        8
    
    Implementations could have the extensions of accepting a negative
    integer as universal-time, but:
    
    1- this would be an extension that has to be documented, or else, it
       would be non-conforming to the standard, and,
    
    2- this doesn't make much sense anyway, since the conversion functions
       assume the Gregorian calendar, which wasn't accepted by some big
       countries before 1900.  Even Saudi Arabia didn't use it before 2016!!!
    
       https://en.wikipedia.org/wiki/Gregorian_calendar#Adoption_of_the_Gregorian_Calendar
    
    The problem of (historical) past dates is a difficult relativistic time-space problem.
    
    
    (get-universal-time) --> 3791288667 ; now
    
    (decode-universal-time 3791288667) ; IN LOCAL TIMEZONE
    27
    44
    16
    21
    2
    2020
    4
    nil
    -1
    
    (encode-universal-time 27 44 16 21 2 2020)
    --> 3791288667
    
    
    
    Anyways, back to our question.  Given this easy access to the date and
    time components, and given the rich CL:FORMAT specifiers, CL doesn't
    provide any specific date formating function.
    
    (multiple-value-bind (se mi ho da mo ye dow dst tz) (decode-universal-time (get-universal-time))
       (format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D ~[Mon~;Tue~;Wed~;Thi~;Fri~;Sat~;Sun~] ~:[not DST~;DST~] GMT~:[-~;+~]~A"
                  ye mo da ho se mi
                  dow dst (minusp tz) (abs tz)))
    --> "2020-02-21 14:34:27 Fri not DST GMT-3/2"
    
    (multiple-value-bind (se mi ho da mo ye dow dst tz) (decode-universal-time (get-universal-time) 0)
       (format nil "~4,'0D~2,'0D~2,'0DT~2,'0D~2,'0D~2,'0DZ"
                  ye mo da ho se mi))
    --> "20200221T152258Z"
    
    
    Note however that once we enter the territory of localized date
    formatting, the variants in the "spelling" of dates and numbers are
    such that it's difficult to use a mere format control string however
    sophisticated it is, to cover all the languages.
    
    For example, in English we add "st", "nd", "rd" or "th" after the day
    number, but in French we add "er" or "e".  I hear that Polish has even
    more sophisticated a rule there.  This selection needs to be computed
    algorithmically.
    
    In CL, we could use the ~/ format specifier which allows to hook in
    function calls.  Therefore we could provide a set of localized
    functions to format dates:
    
    |#
    
    
    (defun cl-user::iso8601-timestamp (stream universal-time colon at &rest parameters)
      (declare (ignore colon at parameters))
      (multiple-value-bind (se mi ho da mo ye) (decode-universal-time universal-time 0)
        (format stream "~4,'0D~2,'0D~2,'0DT~2,'0D~2,'0D~2,'0DZ"
                ye mo da ho se mi)))
    
    (defun english-ordinal-suffix (integer)
      (cond
        ((< 10 integer 20) "th")
        (t (case (mod integer 10)
             (1         "st")
             (2         "nd")
             (3         "rd")
             (otherwise "th")))))
    
    (defun cl-user::english-date (stream universal-time colon at &rest parameters)
      (declare (ignore colon at parameters))
      (multiple-value-bind (se mi ho da mo ye dow) (decode-universal-time universal-time 0)
        (declare (ignore se mi ho))
        (format stream "~[Monday~;Tuesday~;Wednesday~;Thirsday~;Friday~;Saturday~;Sunday~], ~
                        ~[~;January~;February~;March~;April~;May~;June~;July~;August~;September~;October~;November~;December~] ~
                        ~D~A, ~D"
                dow mo da (english-ordinal-suffix da) ye)))
    
    (defun cl-user::french-date (stream universal-time colon at &rest parameters)
      (declare (ignore colon at parameters))
      (multiple-value-bind (se mi ho da mo ye dow) (decode-universal-time universal-time 0)
        (declare (ignore se mi ho))
        (format stream "~[Lundi~;Mardi~;Mercredi~;Jeudi~;Vendredi~;Samedi~;Dimanche~] ~D ~
                        ~[~;Janvier~;Février~;Mars~;Avril~;Mai~;Juin~;Juillet~;Août~;Septembre~;Octobre~;Novembre~;Decembre~] ~
                        ~D"
                dow da mo ye)))
    
    #|
    
    (format nil "Today is: ~/english-date/" (get-universal-time))
    --> "Today is: Friday, February 21st, 2020"
    
    (format nil "Aujourd'hui, ~/french-date/" (get-universal-time))
    --> "Aujourd'hui, Vendredi 21 Février 2020"
    
    
    (format nil "~/iso8601-timestamp/ Date Printed." (get-universal-time))
    --> "20200221T164241Z Date Printed."
    
    |#
    
    
    
  3. matthew said

    @Pascal: nice stuff. Common Lisp formatting is amazing.

    Here’s an improved version of my minimal approach that using the Posix clock_gettime function for a more accurate result. Also, I made the format precision a parameter – default is to show microdays, which are slightly less that a tenth of a second:

    #include <time.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    double mjd(const timespec &tp) {
      return (double(tp.tv_sec) + double(tp.tv_nsec)/1e9)/86400 + 40587;
    }
    
    int main(int argc, char *argv[]) {
      int prec = 6;
      if (argc > 1) prec = strtoul(argv[1],0,0);
      timespec tp;
      clock_gettime(CLOCK_REALTIME,&tp);
      double t = mjd(tp);
      printf("MJD %.*f\n",prec,t);
    }
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: