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.
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); }#| 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." |#@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); }