35
\$\begingroup\$

In the 1990s, COBOL computer engineers worked out a way to extend six-digit date fields by converting them to YYYDDD where YYY is the year - 1900 and DDD is the day of the year [001 to 366]. This scheme could extend the maximum date to 2899-12-31.

In the year 2898, the engineers started panicking because their 900 year old code bases were going to fail. Being from the year 2898, they just used their time machine to send a lone Codeinator to the year 1998 with this algorithm and the task of getting it implemented as widely as possible:

Use a scheme PPQQRR where if 01 ≤ QQ ≤ 12 then it's a standard YYMMDD date in the 1900s, but if QQ > 12 then it represents the days after 2000-01-01 in base 100 for PP and RR but base 87 for QQ - 13.

This scheme extends far beyond year 2899 and is also backwards compatible with standard dates, so no modifications of existing archives are required.

Some examples:

PPQQRR  YYYY-MM-DD
000101  1900-01-01  -- minimum conventional date suggested by J. Allen
010101  1901-01-01  -- edge case suggested by J. Allen
681231  1968-12-31  -- as above
991231  1999-12-31  -- maximum conventional date
001300  2000-01-01  -- zero days after 2000-01-01
008059  2018-07-04  -- current date
378118  2899-12-31  -- maximum date using YYYDDD scheme
999999  4381-12-23  -- maximum date using PPQQRR scheme

Your challenge is to write a program or function to accept input as PPQQRR and output as an ISO date YYYY-MM-DD. Input method can be parameter, console or command line, whatever is easiest.

For your amusement, here is a noncompeting solution in COBOL-85:

IDENTIFICATION DIVISION.
    PROGRAM-ID. DATE-CONVERSION.
DATA DIVISION.
    WORKING-STORAGE SECTION.
    01 T PIC 9(8).
    01 U PIC 9(8).
    01 D VALUE '999999'. 
        05 P PIC 9(2).
        05 Q PIC 9(2).
        05 R PIC 9(2).
    01 F.
        05 Y PIC 9(4).
        05 M PIC 9(2).
        05 D PIC 9(2).
PROCEDURE DIVISION.
    IF Q OF D > 12 THEN
        MOVE FUNCTION INTEGER-OF-DATE(20000101) TO T
        COMPUTE U = R OF D + 100 * ((Q OF D - 13) + 87 * P OF D) + T
        MOVE FUNCTION DATE-OF-INTEGER(U) TO F
        DISPLAY "Date: " Y OF F "-" M OF F "-" D OF F
    ELSE
        DISPLAY "Date: 19" P OF D "-" Q OF D "-" R OF D 
    END-IF.
STOP RUN.
\$\endgroup\$
  • 4
    \$\begingroup\$ "But do not program in COBOL if you can avoid it." -- The Tao of Programming \$\endgroup\$ – tsh Jul 5 '18 at 7:57
  • 9
    \$\begingroup\$ Related HNQ: How to attract people to work on very old and outdated technologies? \$\endgroup\$ – pipe Jul 5 '18 at 9:14
  • 1
    \$\begingroup\$ @user202729 because yymmdd doesn't work for for years >=2000, that's the entire point of the Y2K debacle. \$\endgroup\$ – JAD Jul 5 '18 at 11:15
  • 2
    \$\begingroup\$ @Adám -- In the spirit of COBOL which is very fussy w.r.t. I/O, I'll have to say it needs to be in ISO yyyy-mm-dd format. \$\endgroup\$ – Yimin Rong Jul 5 '18 at 11:51
  • 4
    \$\begingroup\$ @Giuseppe -- In the spirit of COBOL which doesn't really differentiate strings and numbers, yes! Provided you can input leading zeros, e.g. 001300. \$\endgroup\$ – Yimin Rong Jul 5 '18 at 12:04
6
\$\begingroup\$

T-SQL, 99 98 bytes

SELECT CONVERT(DATE,IIF(ISDATE(i)=1,'19'+i,
       DATEADD(d,8700*LEFT(i,2)+RIGHT(i,4)-935,'1999')))FROM t

Line break is for readability only. Thank goodness for implicit casting.

Input is via a pre-existing table t with CHAR column i, per our IO rules.

Goes through the following steps:

  1. Initial check is via the SQL function ISDATE(). (The behavior of this function changes based on language settings, it works as expected on my english-us server). Note this this is just a check for validity, if we tried to parse it directly, it would map 250101 as 2025-01-01, not 1925-01-01.
  2. If the string parses correctly as a date, tack 19 on the front (rather than change the server-level year cutoff setting). Final date conversion will come at the end.
  3. If the string does not parse as a date, convert it to a number instead. The shortest math I could find was 8700*PP + QQRR - 1300, which avoids the (very long) SQL SUBSTRING() function. This math checks out for the provided samples, I'm pretty sure it is right.
  4. Use DATEADD to add that many days to 2000-01-01, which can be shorted to 2000.
  5. Take that final result (either a string from step 2, or a DATETIME from step 4), and CONVERT() it to a pure DATE.

I thought at one point that I found a problematic date: 000229. This is the only date which parses differently for 19xx vs 20xx (since 2000 was a leap year, but 1900 was not, due to weird leap-year exceptions). Because of that, though, 000229 isn't even a valid input (since, as mentioned, 1900 was not a leap year), so doesn't have to be accounted for.

\$\endgroup\$
  • \$\begingroup\$ Good stuff. It's too bad ISDATE doesn't return a boolean, or that integers can't be implicitly converted to boolean in IIF otherwise you could save two bytes. \$\endgroup\$ – Yimin Rong Jul 6 '18 at 12:18
  • \$\begingroup\$ @YiminRong Yep, implicit casting in SQL is very trial-and-error, and works differently in some functions that are otherwise very similar. I'm lucky I didn't have to explicitly cast my LEFT() and RIGHT() function results to integers before multiplying them, that would really have messed up my byte count \$\endgroup\$ – BradC Jul 6 '18 at 13:26
  • 1
    \$\begingroup\$ I think you can remove an extra character by replacing -1300,'2000' with -935,'1999'. \$\endgroup\$ – Razvan Socol Jul 11 '18 at 4:51
  • \$\begingroup\$ Cool idea, @RazvanSocol. I tried going back further multiples of 365 days, but unfortunately couldn't find anything shorter than that. \$\endgroup\$ – BradC Jul 18 '18 at 16:12
6
\$\begingroup\$

R, 126 bytes

function(x,a=x%/%100^(2:0)%%100,d=as.Date)'if'(a[2]<13,d(paste(19e6+x),'%Y%m%d'),d(a[3]+100*((a[2]-13)+87*a[1]),'2000-01-01'))

Try it online!

  • -5 bytes thanks to @Giuseppe suggestion to take a numeric input instead of string
\$\endgroup\$
  • 4
    \$\begingroup\$ Fails for inputs representing dates prior to January the first 1969 (e.g. 000101 or 681231) \$\endgroup\$ – Jonathan Allan Jul 5 '18 at 14:33
  • 2
    \$\begingroup\$ @JonathanAllan: well spotted, thanks. Now it should be fixed (unfortunately requiring 5 more bytes...) \$\endgroup\$ – digEmAll Jul 5 '18 at 18:13
5
\$\begingroup\$

JavaScript (SpiderMonkey), 103 bytes

s=>new Date(...([a,b,c]=s.match(/../g),b>12?[2e3,0,(b-13+a*87)*100-~c]:[a,b-1,c])).toJSON().split`T`[0]

Try it online!


.toJSON will failed on a UTC+X timezone. This code works, but longer (+11bytes):

s=>Intl.DateTimeFormat`ii`.format(new Date(...([a,b,c]=s.match(/../g),b>12?[2e3,0,(b-13+a*87)*100-~c]:[a,b-1,c])))
\$\endgroup\$
  • \$\begingroup\$ You can save 13 bytes with .toJSON(). \$\endgroup\$ – Arnauld Jul 5 '18 at 9:52
  • \$\begingroup\$ And you can save 9 more bytes by splitting the input string into three 2-char substrings. \$\endgroup\$ – Arnauld Jul 5 '18 at 9:57
  • \$\begingroup\$ @Arnauld I was originally trying this on my machine. But it does not work since my timezone is UTC+8. But it at least works on TIO. \$\endgroup\$ – tsh Jul 5 '18 at 10:03
  • \$\begingroup\$ Since we define languages by their implementation (here 'Node.js running on TIO'), is it really invalid? \$\endgroup\$ – Arnauld Jul 5 '18 at 10:20
  • \$\begingroup\$ For the bullet-proof version, you can do it that way to save 1 byte. \$\endgroup\$ – Arnauld Jul 5 '18 at 10:28
3
\$\begingroup\$

Python 2, 159 bytes

from datetime import*
def f(s):D=datetime;p,q,r=map(int,(s[:2],s[2:4],s[4:]));return str(q>12and D(2000,1,1)+timedelta(100*(q-13+87*p)+r)or D(1900+p,q,r))[:10]

Try it online!

\$\endgroup\$
  • \$\begingroup\$ Nice trick using ... and ... or ... instead of ... if ... else .... \$\endgroup\$ – Alexander Revo Jul 12 '18 at 16:11
3
\$\begingroup\$

ABAP, 173 171 bytes

Saved 2 bytes by further optimizing the output

According to the legends, an SAP customer in the early 21st century once said:

After a nuclear war of total destruction, the one thing remaining will be SAPGUI.

He was right. Today, in 2980, there is no more C++, no more COBOL. After the war everyone had to rewrite their code in SAP ABAP. To provide backwards compatibility to the leftovers of the 2800's COBOL programs, our scientists rebuilt it as a subroutine in ABAP.

FORM x USING s.DATA d TYPE d.IF s+2 < 1300.d ='19'&& s.ELSE.d ='20000101'.d = d + s+4 + 100 * ( ( s+2(2) - 13 ) + 87 * s(2) ).ENDIF.WRITE:d(4),d+4,9 d+6,8'-',5'-'.ENDFORM.

It can be called by a program like this:

REPORT z.
  PARAMETERS date(6) TYPE c. "Text input parameter
  PERFORM x USING date.      "Calls the subroutine

Explanation of my code:

FORM x USING s.     "Subroutine with input s
  DATA d TYPE d.    "Declare a date variable (internal format: YYYYMMDD)
  IF s+2 < 1300.    "If substring s from index 2 to end is less than 1300
    d ='19'&& s.    "the date is 19YYMMDD
  ELSE.             "a date past 2000
    d ='20000101'.  "Initial d = 2000 01 01 (yyyy mm dd)

    "The true magic. Uses ABAPs implicit chars to number cast
    "and the ability to add days to a date by simple addition.
    "Using PPQQRR as input:
    " s+4 = RR, s+2(2) = QQ, s(2) = PP
    d = d + s+4 + 100 * ( ( s+2(2) - 13 ) + 87 * s(2) ).
  ENDIF.
    "Make it an ISO date by splitting, concatenating and positioning the substrings of our date.
    WRITE:             "Explanation:
      d(4),            "d(4) = YYYY portion. WRITE adds a space after each parameter, so...
      5 '-' && d+4,    "place dash at absolute position 5. Concatenate '-' with MMDD...
      8 '-' && d+6.    "place dash at absolute position 8, overwriting DD. Concatenate with DD again.
ENDFORM.

ABAP's Date type has the odd property to be formatted as DDMMYYYY when using WRITE - might be locale dependent even - despite the internal format being YYYYMMDD. But when we use a substring selector like d(4) it selects the first 4 characters of the internal format, hence giving us YYYY.

Update: The output formatting in the explanation is now outdated, I optimized it by 2 bytes in the golfed version:

WRITE:  "Write to screen, example for 2000-10-29
 d(4),   "YYYY[space]                =>  2000
 d+4,    "MMDD[space]                =>  2000 1029
 9 d+6,  "Overwrites at position 9   =>  2000 10229
 8'-',   "Place dash at position 8   =>  2000 10-29
 5'-'.   "Place dash at position 5   =>  2000-10-29
\$\endgroup\$
  • \$\begingroup\$ Excellent, I like it. Now all we need is a version in MUMPS and we'll survive anything! \$\endgroup\$ – Yimin Rong Jul 10 '18 at 12:52
  • 1
    \$\begingroup\$ @YiminRong Thanks! Your COBOL-based question basically asked for something like this, I had no choice. \$\endgroup\$ – Maz Jul 10 '18 at 13:25
2
\$\begingroup\$

Kotlin, 222 bytes

Hard coded Calendar field names constants to save 49 bytes.

{d:Int->val p=d/10000
val q=d/100%100
val r=d%100
if(q<13)"19%02d-%02d-%02d".format(p,q,r)
else{val c=Calendar.getInstance()
c.set(2000,0,1)
c.add(5,(p*87+q-13)*100+r)
"%4d-%02d-%02d".format(c.get(1),c.get(2)+1,c.get(5))}}

Try it online!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.