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


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.


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)
  • 5
    \$\begingroup\$ Is zero-padding allowed? aka 03rd instead of 3rd \$\endgroup\$
    – Value Ink
    Jun 29, 2017 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\$
    – Gareth
    Jun 29, 2017 at 8:27
  • \$\begingroup\$ Should we consider a year on more than 4 characters (eg 10987-01-01)? \$\endgroup\$
    – mdahmoune
    Jun 29, 2017 at 9:39
  • \$\begingroup\$ @mdahmoune You don't need to support this unless it's easier to do so. \$\endgroup\$
    – Gareth
    Jun 29, 2017 at 10:25
  • \$\begingroup\$ What about 2016-2-29? \$\endgroup\$ Jun 29, 2017 at 14:12

14 Answers 14


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!

  • 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, 2017 at 2:15
  • 1
    \$\begingroup\$ +1. didn't know about the f strings \$\endgroup\$ Jun 29, 2017 at 12:44
  • \$\begingroup\$ @ValueInk thanks! also, fixed the ordinals issue \$\endgroup\$
    – Uriel
    Jun 29, 2017 at 13:02

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');

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.
  • \$\begingroup\$ Nice, wish MS-SQL recognized the "th" formatting style. \$\endgroup\$
    – BradC
    Jun 29, 2017 at 14:50
  • \$\begingroup\$ Considering you managed to complete the task properly with the fewest bytes, I suppose your solution is the winner! \$\endgroup\$
    – Gareth
    Jun 29, 2017 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, 2017 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\$
    – Gareth
    Jun 30, 2017 at 0:34

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'})

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


  • 1
    \$\begingroup\$ Gives "1st March 2011" for 2011-2-29 in Chrome. That may be a tough fix. \$\endgroup\$ Jun 29, 2017 at 3:35

Python 3.6, 154 bytes

from datetime import*
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.

  • \$\begingroup\$ You can save a byte by removing the space between int(x) and for in your list comp. \$\endgroup\$
    – Chris
    Jun 29, 2017 at 0:03
  • \$\begingroup\$ @ChristianDean Thanks, done! \$\endgroup\$ Jun 29, 2017 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, 2017 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\$ Jun 29, 2017 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, 2017 at 4:19

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

Bash + coreutils, 115 78

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

Try it online.

  • 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, 2017 at 9:52
  • 1
    \$\begingroup\$ BTW, your revision 1 inspired a Bash tip. ;) \$\endgroup\$
    – manatwork
    Jun 29, 2017 at 10:14
  • \$\begingroup\$ @manatwork yes - its funny - I considered trying that but didn't think it would help. Thanks for the nudge. \$\endgroup\$ Jun 29, 2017 at 20:23

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.

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

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

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!

  • \$\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, 2017 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, 2017 at 12:08
  • \$\begingroup\$ Ah, I see. Well, then cool trick @ValueInk. \$\endgroup\$
    – manatwork
    Jun 29, 2017 at 12:14
  • \$\begingroup\$ Almost forgot, a tiny change: "th":th. \$\endgroup\$
    – manatwork
    Jun 29, 2017 at 12:30

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;


  • \$\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\$ Jun 29, 2017 at 8:34
  • \$\begingroup\$ @KevinCruijssen Thanks! \$\endgroup\$
    – kakkarot
    Jun 29, 2017 at 8:38
  • \$\begingroup\$ You also need to include using System; or fully qualify the DateTime object. \$\endgroup\$ Jun 29, 2017 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\$ Jun 29, 2017 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\$ Jun 29, 2017 at 9:47

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

Swift 3 : 298 bytes

let d=DateFormatter()
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!

  • 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\$ Jun 29, 2017 at 12:38

T-SQL, 194 bytes


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:

    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.


q/kdb+ 210 bytes, non-competing


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)};


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"


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.

   // 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;
   // 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)


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.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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