14
\$\begingroup\$

Convert short date format into English long date in as few bytes as possible.

Input

Input will be in the form of a string with format, yyyy-mm-dd, with zero padding optional for all values. You can assume that this is syntactically correct, but not necessarily a valid date. Negative year values do not need to be supported.

Output

You must convert the date into the English long date format (e.g. 14th February 2017). Zero padding here is not allowed.

If the date is invalid (e.g. 2011-02-29), then this must be recognised in some way. Throwing an exception is allowed.

More examples can be seen below.

Test Cases

"1980-05-12" -> 12th May 1980
"2005-12-3"  -> 3rd December 2005
"150-4-21"   -> 21st April 150
"2011-2-29"  -> (error/invalid)
"1999-10-35" -> (error/invalid)
\$\endgroup\$
  • 5
    \$\begingroup\$ Is zero-padding allowed? aka 03rd instead of 3rd \$\endgroup\$ – Value Ink Jun 29 '17 at 2:00
  • \$\begingroup\$ @ValueInk If you read my previous comment, ignore it; I misunderstood the question. Zero padding in the output is not allowed. \$\endgroup\$ – GarethPW Jun 29 '17 at 8:27
  • \$\begingroup\$ Should we consider a year on more than 4 characters (eg 10987-01-01)? \$\endgroup\$ – mdahmoune Jun 29 '17 at 9:39
  • \$\begingroup\$ @mdahmoune You don't need to support this unless it's easier to do so. \$\endgroup\$ – GarethPW Jun 29 '17 at 10:25
  • \$\begingroup\$ What about 2016-2-29? \$\endgroup\$ – Olivier Grégoire Jun 29 '17 at 14:12

14 Answers 14

5
\$\begingroup\$

PostgreSQL, 61 characters

prepare f(date)as select to_char($1,'fmDDth fmMonth fmYYYY');

Prepared statement, takes input as parameter.

Sample run:

Tuples only is on.
Output format is unaligned.
psql (9.6.3, server 9.4.8)
Type "help" for help.

psql=# prepare f(date)as select to_char($1,'fmDDth fmMonth fmYYYY');
PREPARE

psql=# execute f('1980-05-12');
12th May 1980

psql=# execute f('2005-12-3');
3rd December 2005

psql=# execute f('150-4-21');
21st April 150

psql=# execute f('2011-2-29');
ERROR:  date/time field value out of range: "2011-2-29"
LINE 1: execute f('2011-2-29');
                  ^
psql=# execute f('1999-10-35');
ERROR:  date/time field value out of range: "1999-10-35"
LINE 1: execute f('1999-10-35');
                  ^
HINT:  Perhaps you need a different "datestyle" setting.
\$\endgroup\$
  • \$\begingroup\$ Nice, wish MS-SQL recognized the "th" formatting style. \$\endgroup\$ – BradC Jun 29 '17 at 14:50
  • \$\begingroup\$ Considering you managed to complete the task properly with the fewest bytes, I suppose your solution is the winner! \$\endgroup\$ – GarethPW Jun 29 '17 at 23:26
  • \$\begingroup\$ Wow. Thanks. That's completely unexpected. Didn't noticed until now the absence of dedicated golfing languages. However accepting a solution after just one day is a bit soon. \$\endgroup\$ – manatwork Jun 30 '17 at 0:01
  • \$\begingroup\$ @manatwork I did wonder if it might be a little early. But I can change it if need be anyway. \$\endgroup\$ – GarethPW Jun 30 '17 at 0:34
7
\$\begingroup\$

Python 3.6, 137 129 bytes

from datetime import*
def f(k):g=[*map(int,k.split('-'))];n=g[2];return f"{date(*g):%-d{'tsnrhtdd'[n%5*(n^15>4>n%10)::4]} %B %Y}"

Try it online!

\$\endgroup\$
  • 3
    \$\begingroup\$ %-d is the no-padding version of %d that you can use in your string formatting instead of {g[2]}. Also, 12 should become 12th, not 12nd (numbers from 10-19 don't follow the same rules as 1-9 and 20+) \$\endgroup\$ – Value Ink Jun 29 '17 at 2:15
  • 1
    \$\begingroup\$ +1. didn't know about the f strings \$\endgroup\$ – Felipe Nardi Batista Jun 29 '17 at 12:44
  • \$\begingroup\$ @ValueInk thanks! also, fixed the ordinals issue \$\endgroup\$ – Uriel Jun 29 '17 at 13:02
5
\$\begingroup\$

JavaScript (ES6), 142 140 bytes

Outputs NaNth Invalid Date for invalid dates.

The code for ordinal numbers was adapted from this answer.

d=>`${s=(D=new Date(d)).getDate()+''}${[,'st','nd','rd'][s.match`1?.$`]||'th'} `+D.toLocaleDateString('en-GB',{month:'long',year:'numeric'})

f=
d=>`${s=(D=new Date(d)).getDate()+''}${[,'st','nd','rd'][s.match`1?.$`]||'th'} `+D.toLocaleDateString('en-GB',{month:'long',year:'numeric'})

console.log(
  f('2005-12-3'),
  f('1980-05-12'),
  f('2005-12-3'),
  f('150-4-21'),
  f('2011-2-29'),
  f('1999-10-35')
)

\$\endgroup\$
  • 1
    \$\begingroup\$ Gives "1st March 2011" for 2011-2-29 in Chrome. That may be a tough fix. \$\endgroup\$ – Rick Hitchcock Jun 29 '17 at 3:35
5
\$\begingroup\$

Python 3.6, 154 bytes

from datetime import*
s=[*map(int,input().split('-'))]
b=s[2]
print(date(*s).strftime(f"%-d{'th'if(3<b<21)+(23<b<31)else('st','nd','rd')[b%10-1]} %B %Y"))

Try it online! (Set input stream and then run.)

Thanks to good suggestions from commenters below.

\$\endgroup\$
  • \$\begingroup\$ You can save a byte by removing the space between int(x) and for in your list comp. \$\endgroup\$ – Christian Dean Jun 29 '17 at 0:03
  • \$\begingroup\$ @ChristianDean Thanks, done! \$\endgroup\$ – Luke Sawczak Jun 29 '17 at 0:05
  • \$\begingroup\$ (('st','nd','rd')[b%10-1]if b<4 or 20<b<24 else'th') instead of your current conditional for -3 bytes. \$\endgroup\$ – Value Ink Jun 29 '17 at 2:19
  • \$\begingroup\$ @ValueInk Sadly, that will produce 31th. Another way I thought of breaking it down was 'th' if not 0<b%10<4 or 10<b<14 but it didn't save any bytes. \$\endgroup\$ – Luke Sawczak Jun 29 '17 at 4:08
  • \$\begingroup\$ In that case, abuse type coersion. (3<b<21)+(23<b<31) for -1 byte. Try it online! \$\endgroup\$ – Value Ink Jun 29 '17 at 4:19
5
\$\begingroup\$

PHP, 87 bytes

<?=checkdate(($a=explode("-",$argn))[1],$a[2],$a[0])?date("jS F Y",strtotime($argn)):E;

Run as pipe with -F or test it online. Always prints a 4 digit year; fails for years > 9999.

no validity check, 35 bytes:

<?=date("jS F Y",strtotime($argn));
\$\endgroup\$
5
\$\begingroup\$

Bash + coreutils, 115 78

  • 2 bytes saved thanks to @manatwork.
d="date -d$1 +%-e"
t=`$d`
f=thstndrd
$d"${f:t/10-1?t%10<4?t%10*2:0:0:2} %B %Y"

Try it online.

\$\endgroup\$
  • 1
    \$\begingroup\$ Seems that using string instead of array would help a bit: f=thstndrd; $d"${f:t/10-1?t%10<4?t%10*2:0:0:2} %B %Y". \$\endgroup\$ – manatwork Jun 29 '17 at 9:52
  • 1
    \$\begingroup\$ BTW, your revision 1 inspired a Bash tip. ;) \$\endgroup\$ – manatwork Jun 29 '17 at 10:14
  • \$\begingroup\$ @manatwork yes - its funny - I considered trying that but didn't think it would help. Thanks for the nudge. \$\endgroup\$ – Digital Trauma Jun 29 '17 at 20:23
4
\$\begingroup\$

C#, 147 143 bytes

s=>{var t=System.DateTime.Parse(s);int d=t.Day,o=d%10;return d+((d/10)%10==1?"th":o==1?"st":o==2?"nd":o==3?"rd":"th")+t.ToString(" MMMM yyy");}

Saved 4 bytes thanks to @The_Lone_Devil.

\$\endgroup\$
  • \$\begingroup\$ Could you not replace the second t.Day with d for a 4 byte saving? \$\endgroup\$ – The_Lone_Devil Jun 29 '17 at 12:19
  • \$\begingroup\$ @The_Lone_Devil Of course I could thanks, don't know how I missed that. \$\endgroup\$ – TheLethalCoder Jun 29 '17 at 12:34
4
\$\begingroup\$

mIRC version 7.49 (197 bytes)

//tokenize 45 2-2-2 | say $iif($3 isnum 1- $iif($2 = 2,$iif(4 // $1 && 25 \\ $1||16//$1,29,28),$iif($or($2,6) isin 615,30,31))&&$2 isnum1-12&&1//$1,$asctime($ctime($+($1,-,$2,-,$3)date), doo mmmm yyyy))
\$\endgroup\$
3
\$\begingroup\$

Ruby, 104 103 102+8 = 112 111 110 bytes

Uses -rdate -p program flags.

-1 byte from manatwork.

sub(/.*-(\d*)/){Date.parse($&).strftime"%-d#{d=eval$1;(d<4||d>20)&&"..stndrd"[d%10*2,2]||:th} %B %-Y"}

Try it online!

\$\endgroup\$
  • \$\begingroup\$ Am I missing a reason why you not used ternary operator? d<4||d>20?"..stndrd"[d%10*2,2]:"th" \$\endgroup\$ – manatwork Jun 29 '17 at 11:56
  • \$\begingroup\$ @manatwork A number like 26 will try to access indices 12..13 in the lookup string, which is out of bounds, and thus returns nil. Thus using the ternary makes it d<4||d>20?"..stndrd"[d%10*2,2]||"th":"th", which is longer by 2 bytes. \$\endgroup\$ – Value Ink Jun 29 '17 at 12:08
  • \$\begingroup\$ Ah, I see. Well, then cool trick @ValueInk. \$\endgroup\$ – manatwork Jun 29 '17 at 12:14
  • \$\begingroup\$ Almost forgot, a tiny change: "th":th. \$\endgroup\$ – manatwork Jun 29 '17 at 12:30
2
\$\begingroup\$

C# (.NET Core), 167 197 bytes

s=>s.Equals(DateTime.MinValue)?"":s.Day+((s.Day%10==1&s.Day!=11)?"st":(s.Day%10==2&s.Day!=12)?"nd":(s.Day%10==3&s.Day!=13)?"rd":"th")+" "+s.ToString("MMMM")+" "+s.Year

Try it online!

+30 bytes for

using System;

DateTime.Parse()

\$\endgroup\$
  • \$\begingroup\$ You can reverse the ternary check to get rid of the ! for -1 byte. And you can change the && to & for -3 bytes. Also, since you use s.Day 7 times it saves some bytes to create a temp value for it: s=>{var t=s.Day;return s.Equals(DateTime.MinValue)?"":t+((t%10==1&t!=11)?"st":(t%10==2&t!=12)?"nd":(t%10==3&t!=13)?"rd":"th")+" "+s.ToString("MMMM")+" "+s.Year;} \$\endgroup\$ – Kevin Cruijssen Jun 29 '17 at 8:34
  • \$\begingroup\$ @KevinCruijssen Thanks! \$\endgroup\$ – kakkarot Jun 29 '17 at 8:38
  • \$\begingroup\$ You also need to include using System; or fully qualify the DateTime object. \$\endgroup\$ – TheLethalCoder Jun 29 '17 at 9:11
  • \$\begingroup\$ Also DateTime.MinValue is 1-1-1 so I don't think you need that check. Which would also make my previous point irrelevant. \$\endgroup\$ – TheLethalCoder Jun 29 '17 at 9:22
  • 1
    \$\begingroup\$ Taking input as a DateTime and parsing outside of the method isn't acceptable you should do all the work inside the method. Or add an extra method to split work off. \$\endgroup\$ – TheLethalCoder Jun 29 '17 at 9:47
2
\$\begingroup\$

Excel, 212 bytes

=ABS(RIGHT(A1,2))&IF(ABS(ABS(RIGHT(A1,2))-12)<2,"th",SWITCH(RIGHT(A1,1),"1","st","2","nd","3","rd","th"))&TEXT(MID(A1,FIND("-",A1)+1,FIND("-",REPLACE(A1,1,FIND("-",A1),""))-1)*30," mmmm ")&LEFT(A1,FIND("-",A1)-1)

If you break it into chunks at every ampersand, you get these pieces:

  • ABS() pulls the day number from the last two characters in the string. Since that may include a hyphen, ABS converts it to positive.
  • IF((ABS-12)<2,"th",SWITCH()) adds the ordinal. The -12 bit is because 11, 12, and 13 don't follow the normal rule and they all get th instead of st, nd, and rd. This corrects for that.
    • Note: The SWITCH function is only available in Excel 2016 and later. (Source) It's shorter than CHOOSE in this case because it can return a value if no match is found whereas CHOOSE requires numeric input and must have a corresponding return for each possible value.
  • TEXT(MID()*30," mmmm ") extracts the month name. MID() pulls out the month number as a string and multiplying by 30 returns a number. Excel sees that number as a date (1900-01-30, 1900-02-29, 1900-03-30, etc.) and TEXT() formats it as a month name with a space on both ends. 28 and 29 would have also works but 30 looks "nicer".
  • LEFT() extracts the year number.

Now, given all that, it would have been way easier if the test cases were all in a date range that Excel can handle as an actual date: 1900-01-01 to 9999-12-31. The big advantage is that the entire date is formatted at once. That solution is 133 bytes:

=TEXT(DATEVALUE(A1),"d""" & IF(ABS(ABS(RIGHT(A1,2))-12)<2,"th",SWITCH(RIGHT(A1,1),"1","st","2","nd","3","rd","th")) & """ mmmm yyyy")

The other big hurdle was having to include the ordinal. Without that, the solution is just 34 bytes:

=TEXT(DATEVALUE(A1),"d mmmm yyyy")
\$\endgroup\$
1
\$\begingroup\$

Swift 3 : 298 bytes

let d=DateFormatter()
d.dateFormat="yyyy-MM-dd"
if let m=d.date(from:"1999-10-3"){let n=NumberFormatter()
n.numberStyle = .ordinal
let s=n.string(from:NSNumber(value:Calendar.current.component(.day, from:m)))
d.dateFormat="MMMM YYY"
print("\(s!) \(d.string(from:m))")}else{print("(error/invalid)")}

Try it online!

\$\endgroup\$
  • 8
    \$\begingroup\$ Welcome to the site! Here the goal is to make the code as short as possible, I can see you have long variable names and lots of whitespace, you can shorten and remove these to save a lot of bytes. We also usually include a header at the top of the answer in the form of # Language, N bytes. It would be good if you could add one on too. \$\endgroup\$ – TheLethalCoder Jun 29 '17 at 12:38
1
\$\begingroup\$

T-SQL, 194 bytes

DECLARE @ DATE;SELECT @=PARSE('00'+i AS DATE)FROM t;PRINT DATENAME(d,@)+CASE WHEN DAY(@)IN(1,21,31)THEN'st'WHEN DAY(@)IN(2,22)THEN'nd'WHEN DAY(@)IN(3,23)THEN'rd'ELSE'th'END+FORMAT(@,' MMMM yyy')

Input is via text column i in pre-existing table t, per our IO standards.

Works for dates from Jan 1, 0001 to Dec 31, 9999. The year is output with at least 3 digits (per 150AD example).

Invalid dates will result in the following ugly error:

Error converting string value 'foo' into data type date using culture ''.

Different default language/culture settings might change this behavior. If you want a slightly more graceful error output (NULL), add 4 bytes by changing PARSE() to TRY_PARSE().

Format and explanation:

DECLARE @ DATE;
SELECT @=PARSE('00'+i AS DATE)FROM t;
PRINT DATENAME(d,@) + 
    CASE WHEN DAY(@) IN (1,21,31) THEN 'st'
         WHEN DAY(@) IN (2,22)    THEN 'nd'
         WHEN DAY(@) IN (3,23)    THEN 'rd'
         ELSE 'th' END
    + FORMAT(@, ' MMMM yyy')

The DATE data type introduced in SQL 2008 allows much wider range than DATETIME, from Jan 1, 0001 to Dec 31, 9999.

Some very early dates can be parsed wrong with my US locality settings ("01-02-03" becomes "Jan 2 2003"), so I pre-pended a couple extra zeros so it knows that first value is the year.

After that, its just a messy CASE statement to add the ordinal suffix to the day. Annoyingly, the SQL FORMAT command has no way to do that automatically.

\$\endgroup\$
1
\$\begingroup\$

q/kdb+ 210 bytes, non-competing

Solution:

f:{a:"I"$"-"vs x;if[(12<a 1)|31<d:a 2;:0];" "sv(raze($)d,$[d in 1 21 31;`st;d in 2 22;`nd;d in 3 23;`rd;`th];$:[``January`February`March`April`May`June`July`August`September`October`November`December]a 1;($)a 0)};

Examples:

q)f "2017-08-03"
"3rd August 2017"
q)f "1980-05-12"
"12th May 1980"
q)f "2005-12-3"
"3rd December 2005"
q)f "150-4-21" 
"21st April 150"
q)f "2011-2-29"       / yes it's wrong :(
"29th February 2011"
q)f "1999-10-35"
0

Explanation:

This is a horrible challenge as there is no date formatting, so I have to create months from scratch (95 bytes) as well as generating the suffix.

Ungolfed solution is below, basically split the input string and then join back together after we've added the suffix and switched out the month.

f:{
   // split input on "-", cast to integers, save as variable a
   a:"I"$ "-" vs x;
   // if a[1] (month) > 12 or a[2] (day) > 31 return 0; note: save day in variable d for later
   if[(12<a 1) | 31<d:a 2;
     :0];
   // joins the list on " " (like " ".join(...) in python)
   " " sv (
           // the day with suffix
           raze string d,$[d in 1 21 31;`st;d in 2 22;`nd;d in 3 23;`rd;`th];
           // index into a of months, start with 0 as null, to mimic 1-indexing
           string[``January`February`March`April`May`June`July`August`September`October`November`December]a 1;
           // the year cast back to a string (removes any leading zeroes)
           string a 0)
  };

Notes:

Dates in q only go back to ~1709 so I don't have a trivial way of validating the date, hence this is a non-competing entry... The best I can do is check whether the day is > 31 or month is > 12 and return 0.

\$\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.