This challenge was inspired by a seemingly simple task requested at work that couldn't easily be done in Microsoft Teams. At least not from what we could find!


Your task, if you choose to accept it, is to output the last week day of any month in any year.


You can input the desired month/year in whatever format you like, but it must take in an input. Having these hard-coded into the calculations doesn't count!

Input examples include but are not limited to:

  • 08/2023
  • October 1678
  • 1/2067


Your output must be the last week day of the month in the below format:

Tuesday 31st

Test cases

Input Output
10/2023 Tuesday 31st
February 2017 Tuesday 28th
09/1674 Friday 28th
1/2043 Friday 30th

Assumptions / Stipulations

  • You can assume the inputs are always in a workable format and not null
  • Calendar is the Gregorian Calendar
  • Monday, Tuesday, Wednesday, Thursday and Friday are all week days. Saturday and Sunday are NOT valid week days for this task
  • You can ignore public holidays

PS. I got the test cases from timeanddate.com

Current Winner (28th November 2023)

PHP - Kaddath - 55 Bytes https://codegolf.stackexchange.com/a/266340/91342

  • 4
    \$\begingroup\$ The output format might be overly restricted, as it requires hardcoding the day names if the language doesn't support it by default. Perhaps you can allow to output the day number, so Monday is 1, Tuesday is 2, and so on (and let the answer choose if it uses 0-indexing or 1-indexing) \$\endgroup\$ Commented Oct 27, 2023 at 15:06
  • 2
    \$\begingroup\$ 09/1674 should result in Friday 28th, see here \$\endgroup\$
    – Ajax1234
    Commented Oct 27, 2023 at 15:07
  • 8
    \$\begingroup\$ @I.TDelinquent You've selected United Kingdom, which was still using the Julian Calendar at that time, not the Gregorian calendar. \$\endgroup\$
    – Arnauld
    Commented Oct 27, 2023 at 15:30
  • 8
    \$\begingroup\$ Having that fixed output format is a bit annoying... :-) \$\endgroup\$
    – Luis Mendo
    Commented Oct 27, 2023 at 18:45
  • 4
    \$\begingroup\$ If you don't want to take holidays into account, then why not just say "weekdays" instead of "working days"? \$\endgroup\$ Commented Oct 28, 2023 at 18:10

21 Answers 21


Bash and GNU Date, 84 83 bytes

Thanks to gildux for shaving 1 byte.

s=(1 2)
date -d$m-$((s[`date -d$m +%w`]+1))day +%A\ %eth|sed s/1th/1st/

Input format is the month name and year as a single word, e.g. October2023 or sep2026.

An English locale (such as the standard C locale) is required for the day names to be in English.

You might need to unset TZ environment variable to get proleptic Gregorian dates.

How it works

  1. m=1$1+month: Create a date input that is one month later than the start of the target month (ie. the first day of the following month). GNU Date allows us to omit all the spaces, which is helpful.

  2. date -d$m +%w: Obtain the day of week of the first day of the following month.

  3. $((s[…]+1)): We'll need to back up one day, plus an additional day if following month starts on Sunday (weekday 0) or an additional two days if it starts on Monday (weekday 1).

    The array s is incomplete; the arithmetic works because empty string concatenates with +1 to give the result we want.

  4. date -d$m-$((…))day +%A\ %eth: Format that last working day, appending th to give a candidate ordinal.

  5. |sed s/1th/1st/: Rewrite 31th as 31st. The last working day must be in the range 26th...31st, so there are no nds or rds to consider.

Full demo


s=(1 2)
date -d$m-$((s[`date -d$m +%w`]+1))day +%A\ %eth|sed s/1th/1st/

test "$*" || set -- $(for i in {0..35}; do date -d 1jan00+${i}month +%b%Y; done)
for i
    printf '%s: %s\n' $i "$(last-working-day $i)"

Output (no args):

Jan2000: Monday 31st
Feb2000: Tuesday 29th
Mar2000: Friday 31st
Apr2000: Friday 28th
May2000: Wednesday 31st
Jun2000: Friday 30th
Jul2000: Monday 31st
Aug2000: Thursday 31st
Sep2000: Friday 29th
Oct2000: Tuesday 31st
Nov2000: Thursday 30th
Dec2000: Friday 29th
Jan2001: Wednesday 31st
Feb2001: Wednesday 28th
Mar2001: Friday 30th
Apr2001: Monday 30th
May2001: Thursday 31st
Jun2001: Friday 29th
Jul2001: Tuesday 31st
Aug2001: Friday 31st
Sep2001: Friday 28th
Oct2001: Wednesday 31st
Nov2001: Friday 30th
Dec2001: Monday 31st
Jan2002: Thursday 31st
Feb2002: Thursday 28th
Mar2002: Friday 29th
Apr2002: Tuesday 30th
May2002: Friday 31st
Jun2002: Friday 28th
Jul2002: Wednesday 31st
Aug2002: Friday 30th
Sep2002: Monday 30th
Oct2002: Thursday 31st
Nov2002: Friday 29th
Dec2002: Tuesday 31st
  • \$\begingroup\$ Woohoo! Shell ahead of the golfing languages. At least for now; I expect that won't last long.. \$\endgroup\$ Commented Oct 28, 2023 at 16:44
  • \$\begingroup\$ Save one more character by replacing s[$(date -d$m +%w)] with weird s[`date -d$m +%w`] \$\endgroup\$
    – gildux
    Commented Oct 31, 2023 at 16:59
  • 1
    \$\begingroup\$ Thanks @gildux - sometimes ingrained good habits are detrimental to one's golf... \$\endgroup\$ Commented Nov 3, 2023 at 7:50

Python, 129 bytes

from datetime import*
def f(y,m,d=31,s="st"):
 try:q=date(y,m,d);1/(q.weekday()<5);print(f'{q:%A %d}{s}')

Attempt This Online!

  • \$\begingroup\$ I love how the try does double duty, very clever. Also your recognition that only 31 needs the "st" suffix. \$\endgroup\$ Commented Oct 29, 2023 at 2:48
  • \$\begingroup\$ The "st" default parameter is from Arnauld, I can't take credit for that. \$\endgroup\$
    – corvus_192
    Commented Oct 29, 2023 at 9:59

PHP, -F 55 bytes

<?=date('l jS',strtotime("$argn+1month last weekday"));

Try it online!

Input is in "yyyy-mm" format in my example, but strtotime supports many formats

It's so rare that PHP is competitive! I knew one day would be the glory of this pile of unconsistent mess, that I came to like golfing with ;) I needed to add one month because using strtotime without a day number returns the first day, and last is actually a relative term (opposite of next).

Probably still golfable, still searching for edge cases where it could not work (relative month +1 month can be broken I think, -1 month is for sure)

NOTE: my first version was 4 bytes shorter ++$argn." last weekday", I liked it because it uses PHP's string increment, but even if it works with the tests cases, it will fail for December -> "2023-12" would give "2023-13" and strtotime is not really clever

  • \$\begingroup\$ Simply excellent blue elephant. And the better answer by now. \$\endgroup\$
    – gildux
    Commented Oct 31, 2023 at 2:44
  • \$\begingroup\$ I was asking myself if Perl could do the same, but it will be a bit more longer because one need to import Date::Parse (for str2time) or Time::Piece (for strptime) :D \$\endgroup\$
    – gildux
    Commented Oct 31, 2023 at 16:30
  • 1
    \$\begingroup\$ @Kaddath so far this is the shortest answer! You'll be the winner soon if no shorter answers come in :-) \$\endgroup\$ Commented Nov 1, 2023 at 15:48
  • 1
    \$\begingroup\$ @gildux it's actually the code parsing aspect that made me like golfing with it, in lots of place you can omit spaces and stuff in a weird way, like the space removed in +1month because of the way the parser works \$\endgroup\$
    – Kaddath
    Commented Nov 3, 2023 at 10:15
  • 1
    \$\begingroup\$ PHP share many aspects with PERL, which was born with also the focus to ease text-parsing in system scripts: that's why people can write dirty and unmaintainable things… But it was also though to allow more traditional programming ways (it tend to embrase many paradigm) However, it's a very old langage so it standard miss some stuffs are missing from it's standard library. \$\endgroup\$
    – gildux
    Commented Nov 3, 2023 at 23:13

JavaScript (ES6), 129 bytes

-1 thanks to @l4m2
-2 thanks to @Shaggy

Expects ([year, month]), as integers.

f=(D,d=31,s="st")=>(q=new Date([...D,d])).getDate()>9&&q.getDay()%6?q.toLocaleString("EN",{weekday:"long"})+' '+d+s:f(D,d-1,"th")

Try it online!

Or 126 bytes if we assume a default English locale.


f = (                 // recursive function taking:
  D,                  //   D[] = [year, month]
  d = 31,             //   d = day, starting at 31
  s = "st"            //   s = suffix, initialized to "st"
) =>                  //
( q = new Date([      // build the date q for:
    ...D,             //   the year and month stored in D[]
    d                 //   the day d
  ])                  //
).getDate() > 9       // if the resulting day is greater than 9
&&                    // (i.e. we didn't overflow to the next month)
q.getDay()            // and the 0-based day-of-week index
% 6 ?                 // is neither 0 or 6 (Sunday / Saturday):
  q.toLocaleString(   //   return the name of the day
    "EN", {           //   obtained with toLocaleString()
      weekday: "long" //   set up to English
    }                 //
  ) +                 //
  ' ' +               //   followed by a space
  d +                 //   followed by the day
  s                   //   followed by the suffix
:                     // else:
  f(D, d - 1, "th")   //   recursive call with d - 1 and s = "th"
  • 1
    \$\begingroup\$ 131 \$\endgroup\$
    – l4m2
    Commented Oct 27, 2023 at 17:13
  • 1
    \$\begingroup\$ 129, taking input as [y,m]. \$\endgroup\$
    – Shaggy
    Commented Oct 31, 2023 at 15:51

Factor + math.text.english, 110 bytes

[ last-day-of-month dup weekend? [ friday< ] when
dup day-name swap day>> dup ordinal-suffix [I ${} ${}${}I] ]

Attempt This Online!

Takes a timestamp (date object) as input.

  • last-day-of-month get the last day of the month
  • dup weekend? is it a weekend?
  • [ friday< ] when then change it to the Friday before
  • dup day-name get the day name ("Monday", "Tuesday", etc.)
  • swap day>> get the day number of the month
  • dup ordinal-suffix get its ordinal suffix
  • [I ${} ${}${}I] interpolate it all together and print

POSIX shell, 168 bytes

Not the shortest possible with the shell, but it doesn't use any bashism and is funny (see explanation bellow.)

cal $@|tail -2|awk 'NF>1{split("S Mon Tues Wednes Thurs Fri",d)
a=6;if(NF==1){b=$1-2}else if(NF==7){b=$6}else{a=NF;b=$NF}
print d[a]"day",(b==31)?b"st":b"th"}'|tail -1

With the following limitations:

  • months are given as integer only, not strings (January or jan for example)
  • separator is changed from slash to blanks (i.e. 1 2043 instead of 1/2043) which is natural shell arguments separator


We can use the POSIX cal to visually catch the last weekday.

$ # for February 2017
$ cal 2 2017
   February 2017      
Su Mo Tu We Th Fr Sa  
          1  2  3  4  
 5  6  7  8  9 10 11  
12 13 14 15 16 17 18  
19 20 21 22 23 24 25  
26 27 28              

The last week is on the visually shown last line (but there's an extra blank line), so with POSIX tail (for golfing purpose, tail -n -2 is shorten tail -2)

$ # last week of February 2017
$  cal 2 2017 | tail -n -2
26 27 28              

Finally we should pick the last day… Because the column position vary, awk is a better candidate than cut here (But one can prefer perl or some other scripting langage, but I'm trying to use commands that should be always available), hence the following script:

BEGIN  { # on start, create array of days names 
         split("Su Mo Tu We Th Fr Sa", d)
NF > 1 { # on filled line, print last column name and value
         print d[NF],$NF

If the script is put in file LastDay.awk, our chaining become:

$ # last day of February 2017
$  cal 2 2017 | tail -n -2 | awk -f LastDay.awk
Tu 28              

However, note that the first column is Sunday, and we need to deal with boundaries. The modification is:

BEGIN  { # on start, create array of days names 
         split("S Mo Tu We Th", d)
NF > 1 { # check last column
         if (NF==7) {
           print "Fr",$6 # Saturday: we read the previous field
         } else if (NF==1) {
           print "Fr",($1-2) # Sunday: we count two days less
         } else {
           print d[NF],$NF # Week-Day: pick column name and value

For golfing purpose,

  • I try to use ternary operator (which is an expression that expects three other expressions) without success
  • Initial array creation is not done in BEGIN but each time, and rule is evaluate in any case…
  • Replace $1 $2 with $@ (thanks Toby Speight for the comment) and remove extra spaces

But, in order to comply with the output requirements, we have to add more characters for days.

NF > 1 { split("S Mon Tues Wednes Thurs Fri", d)
         if (NF==7) { # sat
           print d[6] "day",$6
         } else if (NF==1) { # sun
           print d[6] "day",($1-2)
         } else { # non w-e
           print d[NF] "day",$NF

For compliance, we have to deal with ordinals too. To avoid repeating ourself many times, let's opt for assignments and put formatting at end.

NF > 1 { split("S Mon Tues Wednes Thurs Fri", d)
         if (NF==7) { # sat
         } else if (NF==1) { # sun
         } else { # non w-e
         print a "day", (b==31)?b"st":b"th"


Well, cal 12 2001 shows that there wasn't simply a blank line as I believed, the formatting is for six weeks… Then, before invoking AWK, our selection should be changed so:

- cal $1 $2 |                  tail -n -2
+ cal $1 $2 | grep -v '^ *$' | tail -n -1

Alternatively, the AWK output (Friday 28th and Monday 31st) must be filtered though tail -n -1. Either add 14 characters before (and save NF > 1 condition), or add 10 characters after (and keep 4 characters condition). Fifty-fifty, so it's a matter of taste.


C (gcc), 270 250 bytes

char*s[]={"Mon","Tues","Wednes","Thurs","Fri","sth"};l(y){return y/4-y/100+y/400;}
f(m,y){int d,p=l(y);y-=d=m<3;d=6+y+p+(31*(m+=12*d-2))/12;
printf("%sday %d%.2s\n",s[d-p],y,s[5]+(y<31));}

Try it online!

Doing it the hard way - a bunch of mod 7 math to count extra days since the first Monday of Year 0.

p=l(y) returns the number of leap years since 0.
d=m<3 is true for Feb and March
y-=d...m+=12*d-2 sets m to the month, where Mar is 1 and Jan and Feb are 11 and 12 of the prior year.
d=6+y+p+31*m/12 sets d to the number of extra days between the first Monday in year 0 and the last day of the prior month. (the 31*m/12 gives the correct pattern of +3s and +2s)
(m>11&p-l(y) is 1 if it's February of a leap year
12652612 is "31-days" for each month encoded in 2 bits, so
y=31-(12652612>>(m*2-2)&3)+... sets y to the last day in the query month.
d+=y...;d%=7 gets the day of the week (0==Mon) for the query.
p=d&d/4*3 sets p to the days needed to truncates Sat/Sun to Fri (4)
y-=p fixes the last day of the month
s[d-p] is the day string,
2 characters from s[5]+(y<31) is "st" or "th"


Ruby, 99 89 102 bytes

->y,m{(1..4).find{|d|(A=Date.new y,m,-d).wday<5}
A.strftime"%A %d#{A.day>30?'st':'th'}"}

Try it online!


Anonymous function tacking year and month as numbers.

We find which of the last 4 days of month ( checking backwards using -d ) are wdays (week days 0-indexed) less than 5 saving each time date object in constant A for later use.

When it's found loop is stopped so A is the last weekday of the month.

Then we format the output, we can only have 31st or XXth.

  • 1
    \$\begingroup\$ ITYM 31st or xxth. \$\endgroup\$
    – Neil
    Commented Oct 31, 2023 at 16:32
  • \$\begingroup\$ @Neil yeah! Where is my mind?? \$\endgroup\$
    Commented Oct 31, 2023 at 21:29

(Pure) Zsh, 126 bytes

zmodload zsh/datetime
for d ({31..9})$s -sT %A\ %dth `$s -r %F $1-$d`&&[[ $T = [^S]*$d\th ]]&&break

Try it online!

Using the builtin strftime rather than any external programs.

zmodload zsh/datetime
# "strftime" is long enough that setting $s and using $s twice is shorter than "strftime" twice.

# start at the 31st of the month, count downward
for d ({31..9})
    # Get the timestamp of "YYYY-MM-DD" with strftime -r (reverse/strptime)
    # and find "Nameofday DDth" from timestamp. (-s)ave in $T
    $s -sT %A\ %dth `$s -r %F $1-$d` &&
    # if Nameofday begins with an S or we end up on a different day of month
    # (like 02-31 converted to 03-03), keep going.
    [[ $T = [^S]*$d\th ]] && break

# Fix suffix

Python3, 153 bytes

from datetime import*
def f(m,y):
 while d.weekday()>4:d-=T
 return d.strftime('%A %d')+['th','st'][d.day>30] 

Try it online!

  • \$\begingroup\$ 133 bytes \$\endgroup\$ Commented Oct 27, 2023 at 16:42
  • \$\begingroup\$ @SuperStormer Thank you very much, updated \$\endgroup\$
    – Ajax1234
    Commented Oct 27, 2023 at 16:51
  • \$\begingroup\$ Unfortunately, 153 bytes to handle the "st"/"th" properly. \$\endgroup\$ Commented Oct 27, 2023 at 18:04
  • \$\begingroup\$ @SuperStormer Thanks, updated \$\endgroup\$
    – Ajax1234
    Commented Oct 27, 2023 at 18:07
  • 2
    \$\begingroup\$ -2 bytes: d.strftime('%A %d')+'tsht'[d.day>30::2] \$\endgroup\$
    – corvus_192
    Commented Oct 27, 2023 at 21:00

Charcoal, 137 136 bytes

≔I⪪S/θ≔⁻⁺×¹²⊟θ⊟θ²θ≔EE²⁻θι﹪Σ⟦÷ι⁴⁸⁰⁰±÷ι¹²⁰⁰÷ι⁴⁸÷ι¹²÷×¹³⁺⁴﹪ι¹²¦⁵⟧⁷θ≔⊟θη≔⊟θθ≔⁻⁺²⁸﹪⁻θη⁷∨¬θ⊗¬⊖θθ§⪪”⟲1U↘ዬ@²¦À≧≡FGv_E✂₂↘” ⁺θηday Iθ§⪪stth²‹θ³¹

Try it online! Link is to verbose version of code. Explanation: Uses my code for Zeller's congruence from my answer to ASCII Calendar Planner.


Split the date on /.


Convert to months since March 1, 1 BC.


Use a modified Zeller's congruence to extract the day of the week of the first day of next month and this month as a list.


Extract the list entries into variables to save bytes.


Calculate the last working day of the month.

§⪪”...” ⁺θηday 

Output the day of the week.


Output the day of the month.


Output the ordinal suffix.


Vyxal, 104 bytes

S½⌊Ḃ÷dN„4/?›13*5/⁰14=[¹›₁1"4*₁JḊ÷¬∧∨28:›"i|30⁰⇧5%₂+]:£Wf⌊∑7%:¥∇2<[›-6]⇩`°⋏ ß₀ †ƛ ßɖ »£`⌈ið„:31=«⟇'Ḋ«½iW∑

Try it Online! | All test cases

Takes the year and the month (one-based) seperately. January and February are considered the 13th and 14th months of the previous year.
Vyxal has no Date/Time built-ins, so this uses a variation of Zeller's Congruence.

S½⌊Ḃ÷dN„4/?›13*5/⁰14=[¹›₁1"4*₁JḊ÷¬∧∨28:›"i|30⁰⇧5%₂+]:£Wf⌊∑7%:¥∇2<[›-6]⇩`°⋏ ß₀ †ƛ ßɖ »£`⌈ið„:31=«⟇'Ḋ«½iW∑

S½⌊Ḃ÷dN„4/?›13*5/ ­⁡​‎‎  ## First Part of Zeller's Congruence Formula⁡⁠⁡‏⁠‎⁡⁠⁢‏⁠‎⁡⁠⁣‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁤‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁢‏⁠‎⁡⁠⁢⁣‏‏​⁡⁠⁡‌⁤​‎‎⁡⁠⁢⁤‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏‏​⁡⁠⁡‌⁢⁢​‎‎⁡⁠⁣⁣‏⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏⁠‎⁡⁠⁤⁤‏⁠‎⁡⁠⁢⁡⁡‏‏​⁡⁠⁡‌­
S½⌊                 # ‎⁡Split year into two halves [century, year of the century]
   Ḃ                # ‎⁢Bifurcate, push this and its reverse
    ÷dN             # ‎⁣Push each item to stack, multiply the year of the century by -2
       „            # ‎⁤Rotate stack ([century, year of the century] is on top)
        4/          # ‎⁢⁡Divide both by 4
          ?›13*5/   # ‎⁢⁢Multiply month + 1 with 13/5

⁰14=[¹›₁1"4*₁JḊ÷¬∧∨28:›"i|30⁰⇧5%₂+]­⁡​‎‎  ## Calculate number of days in month⁡⁠⁡‏⁠‎⁡⁠⁢‏⁠‎⁡⁠⁣‏⁠‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‎⁡⁠⁣⁡⁣‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁣⁢‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁢‏⁠‎⁡⁠⁢⁣‏⁠‎⁡⁠⁤⁣‏‏​⁡⁠⁡‌⁤​‎‎⁡⁠⁢⁤‏⁠‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏⁠‎⁡⁠⁣⁣‏⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁤⁤‏⁠‎⁡⁠⁢⁡⁡‏⁠‎⁡⁠⁢⁡⁢‏⁠‎⁡⁠⁢⁡⁣‏‏​⁡⁠⁡‌⁢⁢​‎‎⁡⁠⁢⁡⁡‏⁠‎⁡⁠⁢⁡⁢‏⁠‎⁡⁠⁢⁡⁣‏‏​⁡⁠⁡‌⁢⁣​‎‎⁡⁠⁢⁡⁤‏⁠‎⁡⁠⁢⁢⁡‏⁠‎⁡⁠⁢⁢⁢‏⁠‎⁡⁠⁢⁢⁣‏⁠‎⁡⁠⁢⁢⁤‏⁠‎⁡⁠⁢⁣⁡‏‏​⁡⁠⁡‌⁢⁤​‎‎⁡⁠⁢⁣⁢‏⁠‎⁡⁠⁣⁡⁣‏‏​⁡⁠⁡‌⁣⁡​‎⁠⁠‎⁡⁠⁢⁤⁡‏⁠‎⁡⁠⁢⁤⁢‏⁠‎⁡⁠⁢⁤⁣‏⁠‎⁡⁠⁢⁤⁤‏⁠‎⁡⁠⁣⁡⁡‏‏​⁡⁠⁡‌⁣⁢​‎‎⁡⁠⁢⁤⁡‏⁠‎⁡⁠⁢⁤⁢‏⁠‎⁡⁠⁢⁤⁣‏⁠‎⁡⁠⁢⁤⁤‏⁠‎⁡⁠⁣⁡⁡‏‏​⁡⁠⁡‌⁣⁣​‎‎⁡⁠⁢⁣⁣‏⁠‎⁡⁠⁢⁣⁤‏⁠‎⁡⁠⁣⁡⁢‏⁠‏​⁡⁠⁡‌­
⁰14=[                             ]  # ‎⁡‎⁡If month is 14 (February) ...
    [                    |             # ‎⁢‎⁢Leap year calculation:
     ¹›       Ḋ                        # ‎⁣Does year + 1 divide ...
       ₁1"4*₁J                         # ‎⁤... [400, 4, 100] (Pair [100, 1] * 4 and join 100)
               ÷¬∧∨                    # ‎⁢⁡‎⁢⁡Split on stack, logical not, and, or
                ¬∧∨                    # ‎⁢⁢[ (year + 1 divides 4 and not 100) or (year + 1 divides 400) ]
                   28:›"i              # ‎⁢⁣Index into [28, 29]
                         |        ]  # ‎⁢⁤‎⁢⁤Calculate days for other months:
                            ⁰⇧5%₂      # ‎⁣⁡((Year+2) % 5) % 2 == 0
                            ⁰⇧5%₂      # ‎⁣⁢Maps [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1] for months 3 to 13 (March=3, ..., January=13)
                          30     +     # ‎⁣⁣Add result to 30

:£Wf⌊∑7%­⁡​‎‎  ## Second Part of Zeller's Congruence Formula⁡⁠⁡‏⁠‎⁡⁠⁢‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁣‏⁠‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁢‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁣‏⁠‎⁡⁠⁢⁤‏‏​⁡⁠⁡‌­
:£        # ‎⁡Save a copy of number of days in month in register
  Wf⌊∑    # ‎⁢Wrap terms in list, take floor and sum all terms
      7%  # ‎⁣Result modulo 7 (Returns weekday as Saturday=0, Sunday=1, ...)

:¥∇2<[›-6]⇩`°⋏ ß₀ †ƛ ßɖ »£`⌈i­⁡​‎‎  ## Calculate workday (as day of the week and month)⁡⁠⁡‏⁠‎⁡⁠⁢‏⁠‎⁡⁠⁣‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁢‏⁠‎⁡⁠⁣⁢‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁣‏⁠‎⁡⁠⁢⁤‏⁠‎⁡⁠⁣⁡‏‏​⁡⁠⁡‌⁤​‎⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏⁠‎⁡⁠⁤⁤‏⁠‎⁡⁠⁢⁡⁡‏⁠‎⁡⁠⁢⁡⁢‏⁠‎⁡⁠⁢⁡⁣‏⁠‎⁡⁠⁢⁡⁤‏⁠‎⁡⁠⁢⁢⁡‏⁠‎⁡⁠⁢⁢⁢‏⁠‎⁡⁠⁢⁢⁣‏⁠‎⁡⁠⁢⁢⁤‏⁠‎⁡⁠⁢⁣⁡‏⁠‎⁡⁠⁢⁣⁢‏⁠‎⁡⁠⁢⁣⁣‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁣⁣‏⁠‎⁡⁠⁢⁣⁤‏⁠‎⁡⁠⁢⁤⁡‏‏​⁡⁠⁡‌­
:¥∇                            # ‎⁡Push the number of days in the month (register), weekday, weekday
   2<[   ]                     # ‎⁢If weekday < 2 (Saturday or Sunday)
      ›-6                      # ‎⁣Subtract weekday+1 from number of days in month (last workday), Push 6 (Friday)
           `°⋏ ß₀ †ƛ ßɖ »£`    # ‎⁤Compressed string of workdays
          ⇩                ⌈i  # ‎⁢⁡Index workday-2 into list (Monday=0, Tuesday=1, ...)

ð„:31=«⟇'Ḋ«½i­⁡​‎‎  ## Day of the Month formatting⁡⁠⁡‏⁠‎⁡⁠⁢‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁣‏⁠‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁢‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁣‏⁠‎⁡⁠⁢⁤‏⁠‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏⁠‎⁡⁠⁣⁣‏⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏‏​⁡⁠⁡‌⁤​‎⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏‏​⁡⁠⁡‌­
ð„             # ‎⁡Push space and rotate stack
  :31=         # ‎⁢Is the day of the month 31? (Returns 1 or 0)
      «⟇'Ḋ«½i  # ‎⁣Compressed string "thst", halve and index into string

W∑  # ‎⁤Join all values in the stack (workday as day of the week, space, workday as day of the month)

PowerShell Core, 118 bytes

param($y,$m)for(;!((($k=Date "$y/$m/1"|% *hs 1|% *ys(--$d))|% d*k)%6)){}"{0:dddd} {0:dd}"-f$k+('st','th')[$k.Day-ne31]

Try it online!

Takes in two parameters, $year and $month, returns a string.

Less golfed:

for(;(($k=(Date "$y/$m/1").AddMonths(1).AddDays(--$d)).DayOfWeek-in"Saturday","Sunday")){}
"{0:dddd} {0:dd}"-f$k+('st','th')[$k.Day-ne31]

Finds the last day of the month, while it is a Saturday or a Sunday, check one day before Then formats the date


Google Sheets / Microsoft Excel, 76 bytes

=let(d,workday(eomonth(A1,)+1,-1),text(d,"dddd d")&if(day(d)=31,"st","th"))

Put the month and year in cell A1 in one of the formats specified in the question (or any other valid date format), and the formula in B1.

The formula gets the first day of the next month and then finds the previous weekday before that, which is the same as the last weekday of the specified month.

The day number of the last weekday of a month will never end in 2 or 3, so it is enough to add st when the day is 31 and use th with any other date.

The formula is for Google Sheets, but I tested it in the most recent version of Microsoft Excel for the web and it seems to work the fine there as well.

The Google Sheets epoch is 30 December 1899. The formula will work with dates from January 1900 to December 99999. In Microsoft Excel, the range of valid dates seems more limited, and formula will work from January 1900 to November 9999.

input expected formula
10/2023 Tuesday 31st Tuesday 31st
February 2017 Tuesday 28th Tuesday 28th
09/1674 Friday 28th #NUM!
1/2043 Friday 30th Friday 30th

Python, 152 bytes

Since there is already multiple python answers I've tried to use the calendar library since I never used it before. It just uses the fact that the object monthcalendar gives weeks of the month and days of those weeks as a 2d array ->then just a check for highest number in the first 5 days of last week.

from calendar import *
def f(y,m,d=31,s="st"):
 if d in monthcalendar(y, m)[-1][:5]:
 else: f(y,m,d-1,s="th")

Attempt This Online!

  • \$\begingroup\$ Welcome to Code Golf, and nice answer! \$\endgroup\$ Commented Nov 14, 2023 at 14:12
  • \$\begingroup\$ @RydwolfPrograms thank you, looking forward to my stay and hopefully mastering some actual skillz \$\endgroup\$ Commented Nov 15, 2023 at 14:11

APL(Dyalog Unicode), 66 bytes SBCS

{⊃'Dddd DDoo'(1200⌶)⊃⌽w/⍨0≠6|7|w←∊{0::⍬⋄,¯1 1 ⎕DT⊂⍵}¨(⊂⌽⍵),¨25+⍳6}

Try it on APLgolf!

woo-hoo. Shaggy waiting room. Takes date as m y.


Go, 234 bytes

func f(m,y int)string{M,e:=Month(m),"th"
for w:=W(t);t.Month()!=M||w<1||w>5;{t=t.Add(Hour*-24);w=W(t)}
if t.Day()>30{e="st"}
return Sprintf("%s %d%s",W(t),t.Day(),e)}

Attempt This Online!


func f(m,y int)string{M,e:=Month(m),"th"
// start the date off at the 31st of the month, regardless of the month
// subtract 1 day while...
for w:=W(t);
// the months don't match, or
// the day is not a weekday
// replace "31th" with "31st"
if t.Day()>30{e="st"}
return Sprintf("%s %d%s",W(t),t.Day(),e)}

bash and BSD date, 96 bytes

As an alternative to Toby Speight's answer but using BSD implementation instead of GNU one

s=(1 2)
m="-v$2y -v$1 -v+1m -v1d"
date $m -v-$((s[`date $m +%w`]+1))d +%A\ %eth|sed s/1th/1st/

It has the following limitations:

  • months can be integers only, not strings (October or sep for example)
  • separator is changed from / to blanks which is natural shell arguments separator

There's certainly room for tweaking.


C#, 143 bytes

(y,m)=>new[]{DateTime.DaysInMonth(y,m)is{}z?z:z,z-1,z-2}.Select(x=>new DateTime(y,m,x).ToString("dddd dd")+(x>30?"st":"th")).First(x=>x[0]!=83)


f = (y, m) =>                       // lambda that takes year and month
  new[] {                           // create new implicitly typed array
      DateTime.DaysInMonth(y, m)    // get number of days in month
          is {} z ? z : z,          // hack to set variable z to this value inline
      z-1, z-2                      // add the 2 days previous to z
  }                                 // array contains the last 3 days of the given month in descending order
  .Select(                          // transform the elements of the array
      x => new DateTime(y, m, x)    // create new datetime with the given year and month
      .ToString("dddd dd")          // convert to string and format
      + (x > 30 ? "st" : "th"))     // add "st" if is the 31st, "th" otherwise
  .First(                           // get first element that matches condition
      x => x[0] != 83)              // first character is not "S" (i.e not day isn't saturday or sunday)

Try it online!


Scala 3, 235 bytes

235 bytes, it can be golfed more.

Golfed version. Attempt This Online!

(y,m)=>{var d=LocalDate.of(y,m,1).lengthOfMonth;var q=LocalDate.of(y,m,d);while(q.getDayOfWeek==SATURDAY||q.getDayOfWeek==SUNDAY){d-=1;q=LocalDate.of(y,m,d)};val s=if(d<2)"st"else"th";println(s"${q.getDayOfWeek} ${q.getDayOfMonth}$s")}

Ungolfed version. Attempt This Online!

import java.time.{LocalDate, DayOfWeek}

object Main {
  def main(args: Array[String]): Unit = {
    printLastWeekdayOfMonth(2023, 10)
    printLastWeekdayOfMonth(2017, 2)
    printLastWeekdayOfMonth(1674, 9)
    printLastWeekdayOfMonth(2043, 1)
    printLastWeekdayOfMonth(2023, 11)
    printLastWeekdayOfMonth(2023, 11)

  def printLastWeekdayOfMonth(year: Int, month: Int): Unit = {
    var day = LocalDate.of(year, month, 1).lengthOfMonth()  // Start from the last day of the month
    var date = LocalDate.of(year, month, day)

    // Loop backwards to find the last weekday (Monday to Friday)
    while (date.getDayOfWeek == DayOfWeek.SATURDAY || date.getDayOfWeek == DayOfWeek.SUNDAY) {
      day -= 1
      date = LocalDate.of(year, month, day)

    // Print the date in the required format with 'st' or 'th'
    val suffix = if (day == 1) "st" else "th"
    println(s"${date.getDayOfWeek} ${date.getDayOfMonth}$suffix")

R, 111 109 100 bytes

\(a,`?`=paste0)gsub("1th","1st",(d<-format(sort(as.Date(a?-1:-31),T),"%A %e")?"th")[d<"S"|d>"T"])[1]

Attempt This Online!

Inputs a string in yyyy-mm format, then creates a vector of dates for days 1 to 31 (paste negative numbers, the minus being the separation sign here). Actually, only days 26 to 31 are needed, but that way a byte is spared. The invalid dates are automatically converted to NAs which get lost in the sorting step. Argument T allows to sort in decreasing order, so that the first element will be the last date. d<"S" are Friday and Monday and d>"T" are Thursday, Tuesday and Wednesday.


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.