"Fancy" Ordinal Date Formatting

Hello, first post here, but large participant elsewhere.

Looking to build a quick snippet that automatically insert today's date text according to this format:

Month (1st, 2nd, 3rd, 4th), Year

And potentially, with the added benefit of auto calculation. I know it is possible, but it hasn't been presented here in complete form.

All the date macros I have run into are for standard dates, and not this. There are a couple scripts I have found, but I don't know enough about KM or code to get it to work.

1 Like

The code is pretty simple. You can just look at the last digit of the day and append "st" to "1" or "nd" to "2" or "rd" to "3" or "th" to anything else, correcting "11" to "th." And you can do that with a Switch/Case action on whatever the day variable is called.

This should get you started:

Ordinalize Macro (v9.0.5)

Ordinalize.kmmacros (4.4 KB)

1 Like

can you give a couple of test cases so i an understand what you are actually asking for

Thank you. But how can you turn that into a executable macro through a text string or hotkey? I am not familiar with the complexity of variables in Keyboard Maestro yet (I have experience in iOS shortcuts and that is about it).

I would like to type a text string, such as ,,today to format today's date, such as: April 11th, 2020 - the key is the th or st or nd depending on the date.

Ideally, I would also like to add a modifier where I could type ,,today+1 or ,,today+4 and automatically count forward. But that is a separate request and I feel like I could do that on my own once I get the key substitution into the macro.

I have a text expander snippet (shell script) that does this, but it has a menu associated with it demanding user input after the text string. I would like to have something default to expand today's date in the format above and then potentially modify that afterward as mentioned.

Here is the script I have from Bret Terpstra:


datestring="%filltext:name=Date String:default=today%"

### Convert Date action
### Copyright 2013 Brett Terpstra <http://brettterpstra.com>
### Freely distribute and edit, but please maintain attribution
require 'Time'

class DateUtils

  def initialize(time_zone = 'America/Chicago')
    @time_zone = time_zone

  def parsedate(date,format,unpad=false)
    date = Time.parse(%x{php -r "date_default_timezone_set('#{@time_zone}');echo strftime('%c',strtotime('#{date}'));"})
    parsed = date.strftime(format).gsub(/%o/,"#{ordinal(date.strftime('%e').to_i)}")
    parsed.gsub!(/(^|-|\/|\s)0/,"\\1") if unpad
    # if there's no space between time and meridiem, downcase it
    parsed.gsub!(/(\d)(AM|PM)/) {|m| $1 + $2.downcase }
    return parsed =~ /1969/ ? false : parsed

  # Returns an ordinal number. 13 -> 13th, 21 -> 21st etc.
  def ordinal(number)
    if (11..13).include?(number.to_i % 100)
      case number.to_i % 10
      when 1; "#{number}st"
      when 2; "#{number}nd"
      when 3; "#{number}rd"
      else    "#{number}th"

  def timeize(time_string,format)
    # Uses standard strftime format, but %0I will pad single-digit hours
    if time_string =~ /(\d{1,2})(?::(\d\d))?(?:\s*(a|p)m?)?/i
      hour = $1.to_i
      minute = $2.nil? ? "00" : $2.to_i
      meridiem = $3
      meridiem = hour > 12 ? "p" : "a" if meridiem.nil?
      format.gsub!(/%H/) {|m|
        hour = hour < 12 && meridiem == "p" ? hour + 12 : hour
        hour = 0 if hour == 12 && meridiem == "a"
        hour < 10 ? "0" + hour.to_s : hour
      format.gsub!(/%(0)?I/) {|m|
        hour = hour > 12 ? hour - 12 : hour
        $1 == "0" && hour < 10 ? "0" + hour.to_s : hour
      format.gsub!(/%p/,meridiem.downcase + "m")
      format.gsub!(/%P/,meridiem.upcase + "M")

original = format + " " + datestring

# split input and handle thursday@3 format
input = original.gsub(/(\S)@(\S)/,"\\1 at \\2").split(" ")
unpad = true
du = DateUtils.new

# %%o is replaced with ordinal day
format = case input[0]
when 'date' then '%m/%d/%y' # slashed date
when 'local' then '%F' # localized date
when 'short' then '%a, %b %%o, %Y' # abbreviated full date
when 'long' then '%B %%o, %Y' # long full date
when 'iso' then '%Y-%m-%d'
else false

# turn off leading zero removal for certain types
unpad = false if input[0] =~ /(date|local|iso)/

# handle +# to advance # days
input.map! {|part|
  part.gsub(/^\+(\d+)$/,"\\1 days").gsub(/^at$/,'')

# time formatting
time_format = ""
time_string = ""
if original.gsub(/\+\d+/,'') =~ /((?:at|@) *)?(\d{1,2}(:\d\d)?(\s*(a|p)m?)?)/i
  time_string = $2
  time_format = case input[0]
  when 'iso' then du.timeize(time_string," %H:%M")
  when 'long' then du.timeize(time_string," at %I:%M%p")
  else du.timeize(time_string," %I:%M %P")

  input.delete_if { |item|
    item =~ /^(@|at|@?\d{1,2}(:\d\d)?((a|p)m?)?|((a|p)m?))$/i
date_string = ""
if format
  if input.length > 1
    if original.gsub(/((@|at)\s*)?#{time_string}/,'') =~ /^[\s\n]*$/m
      date_string = "today #{time_format}"
      date_string = input[1..input.length].join(" ")
    date_string = "today"
  if original.gsub(/((@|at)\s*)?#{time_string}/,'') =~ /^[\s\n]*$/m
	date_string = "today #{time_format}"
    date_string = input.join(" ")
  unpad = false
  format = '%F'

output = du.parsedate(date_string,format + time_format,unpad)

if output
  print output
  print original

Like this:

Date from Typed String Macro (v9.0.5)

Date from Typed String.kmmacros (46 KB)

Type [period]td for "April 13th, 2020" (or whatever today happens to be) or [Question Mark]4/20 (or any date) for a help screen showing all the options.

Not thoroughly tested but it should serve as an illustration at least.


Wonderful!!! Is there a way to set this as a variable that auto-expands within a larger snippet?? Thank you so much for your help.

For example I would like to expand this large snippet with a quick string like ,dn, and then have todays date (in the appropriate format) expand with it. You can see I inserted the string between double-brackets in a few places.

		{{[[query]]: {and: [[Inbox]] [[TODO]] {not: [[query]] {or: [[query]] [[ ,dn  ]] }}}}}
#[[Action List]]
	#[[Task List]]
			{{[[query]]: {and: [[TODO]] [[Action]] {not: [[query]]}}}}
			{{[[query]]: {and: [[TODO]] [[Action]] [[Due:]] {not: [[query]]} {between: [[January 1st, 2019]] [[ ,dn ]]} }}}
			{{[[query]]: {and: [[TODO]] [[ ,dn ]] {not: {or: [[Action]] [[query]]}}}} 
			All items due w/out action		
			{{[[query]]: {and: [[TODO]] [[InProgress]] {not: [[query]]}}}}
	#[[Start Date]]
			{{[[query]]: {and: [[TODO]] [[Start Date:]] [[ ,dn ]] {not: [[query]]}}}}

1 Like

Action: Execute a Shell Script
Paste in:


DaySuffix() {
  case `date +%d` in
    1|21|31) echo "st";;
    2|22)    echo "nd";;
    3|23)    echo "rd";;
    *)       echo "th";;
date "+%A, %B %d`DaySuffix`, %Y"

At this writing this returns Tuesday, April 21st, 2020

Note that this will also give you the day. Simply remove the +%A, if you don't want it. (Included for completeness.)

Also note that there are myriad varieties of formatting achievable with the built-in Unix date command. Review them here to mix and match to modify or augment the final output statement.

Just re-read to realize you also wanted the ability to choose an arbitrary date. Slightly more complicated, but only slightly…

# add or subtract your time here:
# use the flag `-v` followed by your desired
# outcome: +1d, -7d, etc., so…
# `-v+1d` will give you tomorrow    
# This will add ZERO days; change to desired transform…


DaySuffix() {
  case `date $dateTransform +%-d` in
    1|21|31) echo "st";;
    2|22)    echo "nd";;
    3|23)    echo "rd";;
    *)       echo "th";;

date $dateTransform "+%A, %B %-d`DaySuffix`, %Y"

Paste that into an "Execute Shell Script" action and choose your date transformation on the first line of the script. Unix will do the proper math should you, say, add 30 days. Other options for the date transform are w (week), m (month), and y (year).