COBOL Y2K redux

36

7

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.

user15259

Posted 2018-07-05T04:01:17.137

Reputation:

May output be a [Y,M,D] numeric list? Does it make a difference if that is my language's normal date format? – Adám – 2018-07-05T05:33:57.007

4"But do not program in COBOL if you can avoid it." -- The Tao of Programming – tsh – 2018-07-05T07:57:51.300

9

Related HNQ: How to attract people to work on very old and outdated technologies?

– pipe – 2018-07-05T09:14:41.573

Why from year 2000 and not 2900? – user202729 – 2018-07-05T10:59:45.270

1@user202729 because yymmdd doesn't work for for years >=2000, that's the entire point of the Y2K debacle. – JAD – 2018-07-05T11:15:38.067

2@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. – None – 2018-07-05T11:51:59.213

Can we take input as an integer rather than as a string? – Giuseppe – 2018-07-05T12:02:18.347

4@Giuseppe -- In the spirit of COBOL which doesn't really differentiate strings and numbers, yes! Provided you can input leading zeros, e.g. 001300. – None – 2018-07-05T12:04:05.223

Suggested test cases: 000101 and 010101. – Jonathan Allan – 2018-07-05T14:30:13.793

Additional suggested test cases: 010367, 000000, -99101, RABBIT – Jay – 2018-07-05T15:25:07.483

@Jay -- CodeGolf usually assumes input is valid. BTW 000000 is often used as an empty date in COBOL (akin to null). You can crash many a system by forcing a date field to all zeroes in a record! – None – 2018-07-05T15:31:01.710

1@JonathanAllan -- Thank you, have added them including 681231. I was thinking the conventional dates were too trivial to bother with but your comments below have convinced me otherwise! – None – 2018-07-05T15:49:04.047

Do we ignore leap years? – Alexander Revo – 2018-07-12T14:03:09.213

@AlexanderRevo - No, leap years are important. – None – 2018-07-12T15:30:09.567

Answers

5

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.

BradC

Posted 2018-07-05T04:01:17.137

Reputation: 6 099

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. – None – 2018-07-06T12:18:04.350

@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 – BradC – 2018-07-06T13:26:59.887

1I think you can remove an extra character by replacing -1300,'2000' with -935,'1999'. – Razvan Socol – 2018-07-11T04:51:52.147

Cool idea, @RazvanSocol. I tried going back further multiples of 365 days, but unfortunately couldn't find anything shorter than that. – BradC – 2018-07-18T16:12:51.313

5

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

digEmAll

Posted 2018-07-05T04:01:17.137

Reputation: 4 599

4Fails for inputs representing dates prior to January the first 1969 (e.g. 000101 or 681231) – Jonathan Allan – 2018-07-05T14:33:18.760

2@JonathanAllan: well spotted, thanks. Now it should be fixed (unfortunately requiring 5 more bytes...) – digEmAll – 2018-07-05T18:13:35.883

4

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])))

tsh

Posted 2018-07-05T04:01:17.137

Reputation: 13 072

You can save 13 bytes with .toJSON().

– Arnauld – 2018-07-05T09:52:19.987

And you can save 9 more bytes by splitting the input string into three 2-char substrings.

– Arnauld – 2018-07-05T09:57:34.677

@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. – tsh – 2018-07-05T10:03:23.710

Since we define languages by their implementation (here 'Node.js running on TIO'), is it really invalid? – Arnauld – 2018-07-05T10:20:18.943

For the bullet-proof version, you can do it that way to save 1 byte.

– Arnauld – 2018-07-05T10:28:56.770

@Arnauld updated. just never tested this on tio, and don't know its working. – tsh – 2018-07-05T10:28:58.680

TIO has a timezone offset of exactly 0. Thanks, Dennis! ;) – Arnauld – 2018-07-05T10:31:32.133

2

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!

Chas Brown

Posted 2018-07-05T04:01:17.137

Reputation: 8 959

Nice trick using ... and ... or ... instead of ... if ... else .... – Alexander Revo – 2018-07-12T16:11:40.737

2

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

Maz

Posted 2018-07-05T04:01:17.137

Reputation: 191

Excellent, I like it. Now all we need is a version in MUMPS and we'll survive anything! – None – 2018-07-10T12:52:35.587

1@YiminRong Thanks! Your COBOL-based question basically asked for something like this, I had no choice. – Maz – 2018-07-10T13:25:28.880

1

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!

JohnWells

Posted 2018-07-05T04:01:17.137

Reputation: 611