Convert clipboard date to yyyy-mm-dd

Dear friends,

I’m trying to do something that I thought I could figure out on my own. Boy was I wrong!

I basically would like to be able to copy any date format from receipts I receive in my email and then be able to paste those dates into a consistent format every time. That format would be “yyyy-mm-dd” to be more specific.

Keep in mind that date formats from email receipts can come in many different ways. I suspect it’s not possible to easily convert all possible formats though.

In case someone’s curious about why I need this: I want to be able to save all my electronic receipts that come via email as PDFs but organize them by date. In case I receive two receipts on that same day, I would just add a trailing 2 digit number to the end of that date.
So I would amass a folder throughout the year that would have PDFs with file names like this:

2016-05-03_01.pdf
2016-05-03_02.pdf
2016-06-30_01.pdf
etc…

When tax time arrives, I’ll have all my receipts nicely organized by date!

Is this possible? Maybe I’m asking for a lot from KM! : )

One last thing: I find KM’s interface a bit confusing so please explain in specific steps any instructions you’d like me to follow.

Thanks!

Would the email date be enough or do you strictly want the date from the attached receipt?

Parsing all possible input date formats is pretty much impossible. For one things, it’s not at all clear whether 1/2/16 is January 2 or February 1, so at the least you will have to deal with that ambiguity.

That said, if you collect a sample of the different formats you wish to process, then it is likely that we can come up with a macro that converts them to ISO 8601 Date Format.

Yes, but...   :sunglasses:

AppleScriptObjC provides access to data-detectors, and they're pretty good at deciphering date strings.

(Failures are most likely to occur in date-strings where the day precedes the month and is less-than or equal to 12.)

I don't have time to make this pretty, but it will take a wide variety of date-string input and produce an ISO 8601 formatted date-string output.

-Chris

* Note for the uninitiated – this is an AppleScript that uses AppleScriptObjC. It can be run directly in the Script Editor.app or in a Keyboard Maestro Execute an AppleScript action.

------------------------------------------------------------
# Auth: Christopher Stone <scriptmeister@thestoneforge.com>
# Auth: Shane Stanley graciously provided the ASObjC handlers.
# dCre: 2016/03/17 03:45
# dMod: 2016/03/17 04:09
# Appl: AppleScriptObjC
# Task: Use Data-Detectors to extract a date from unstructured string input.
#     : Output an ISO 8601 formatted date-string.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @ASObjC, @Date, @String
------------------------------------------------------------
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
------------------------------------------------------------

# Example Date String Input
set theDate to my getDatesIn:"Some text containing Nov 5, for example"
set theDate to my getDatesIn:"2015/1/5"
set theDate to my getDatesIn:"1/5/2016"
set theDate to my getDatesIn:"4 May 1961"
set theDate to my getDatesIn:"jan 1 2012"

considering numeric strings
  if AppleScript's version < "2.5" then
    set theDate to my makeASDateFrom:theDate
  else
    set theDate to theDate as date
  end if
end considering

# Date String Output
set outputDate to my formatDate:(theDate) usingFormat:"y-MM-dd"
return outputDate

------------------------------------------------------------
--» HANDLERS
------------------------------------------------------------
on formatDate:theDate usingFormat:formatString
  if class of theDate is date then set theDate to my makeNSDateFrom:theDate
  set theFormatter to current application's NSDateFormatter's new()
  theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
  theFormatter's setDateFormat:formatString
  set theString to theFormatter's stringFromDate:theDate
  return theString as text
end formatDate:usingFormat:
------------------------------------------------------------
on getDatesIn:aString
  # Convert string to Cocoa string
  set anNSString to current application's NSString's stringWithString:aString
  # Create data detector
  set theDetector to current application's NSDataDetector's dataDetectorWithTypes:(current application's NSTextCheckingTypeDate) |error|:(missing value)
  # Find first match in string; returns an NSTextCheckingResult object
  set theMatch to theDetector's firstMatchInString:anNSString options:0 range:{0, anNSString's |length|()}
  if theMatch = missing value then error "No date found"
  # Get the date property of the NSTextCheckingResult
  set theDate to theMatch's |date|()
  return theDate
end getDatesIn:
------------------------------------------------------------
# Required before 10.11
on makeASDateFrom:theNSDate
  set theCalendar to current application's NSCalendar's currentCalendar()
  set comps to theCalendar's componentsInTimeZone:(missing value) fromDate:theNSDate # 'missing value' means current time zone
  tell (current date) to set {theASDate, year, day, its month, day, time} to ¬
    {it, comps's |year|(), 1, comps's |month|(), comps's |day|(), (comps's hour()) * hours + (comps's minute()) * minutes + (comps's |second|())}
  return theASDate
end makeASDateFrom:
------------------------------------------------------------
on makeNSDateFrom:theASDate
  set {theYear, theMonth, theDay, theSeconds} to theASDate's {year, month, day, time}
  if theYear < 0 then
    set theYear to -theYear
    set theEra to 0
  else
    set theEra to 1
  end if
  set theCalendar to current application's NSCalendar's currentCalendar()
  set newDate to theCalendar's dateWithEra:theEra |year|:theYear |month|:(theMonth as integer) ¬
    |day|:theDay hour:0 minute:0 |second|:theSeconds nanosecond:0
  return newDate
end makeNSDateFrom:
------------------------------------------------------------
6 Likes

@Tom: I prefer the date on the receipt because my email client (Postbox 4) only displays the time the email was received if it was received today. IOW: it only shows the date if it’s older than one day.

Ok, I’m going to give this a shot but how do I get it to work in KM? I know I can call an AppleScript from within KM but is that enough or do I need to set the clipboard or something?

Sorry but as I mentioned earlier. KM for me is unintuitive.

Thank you very much for your help.

I prefer the date on the receipt because my email client (Postbox 4) only displays the time the email was received if it was received today. IOW: it only shows the date if it's older than one day.

I asked because you can get the date from locally stored emails with mdls -name kMDItemContentCreationDate. This date is already standardized and can easily be converted to whatever format.

I’ve written a script around that to bulk export mails from Apple Mail and renaming them with creation date, subject, etc. There’s still the step to convert these to PDF though. (And Postbox I don’t know.)

Hey @project_guru,

Keyboard Maestro becomes more intuitive as you gain familiarity with it. Give it time.

Well, firstly – Postbox's AppleScript dictionary is pretty rudimentary and doesn't allow for exporting attachments.

You need to provide an itemized list of your process, because this job is anything but simple.

-Chris

@ccstone: For now, I’m going to just manually save PDFs of the email receipts (this is easy to do via the print dialog window in OS X).

I just need to be able to copy the date from the receipt and paste it when the PDF SAVE AS dialog box prompts to do so.

Is that possible?

Thanks

Yes.

Using the great AppleScript that Chris (@ccstone) wrote, I put together this macro that does just that.

The process is:

  1. Use KM Action to Copy your selection of a date in any format
  2. Set a KM Variable to the clipboard
  3. Execute the AppleScript, which will read the KM Variable
  4. AppleScript outputs converted date to another KM Variable
  5. Put the KM Variable on the clipboard, ready for you to paste wherever

The key change to the AppleScript that Chris wrote is adding a call to KM to get the date string that was copied from your selection.

--- GET DATE STRING FROM KM VARIABLE ---  ##JMichaelTX

set dateStr to getKMVar("Date_String")
set theDate to (my getDatesIn:dateStr)

For the code of the getKMVar handler (function), see below the macro image.

###Macro Library [DATE] Convert Date String to ISO Std Format

[DATE] Convert Date String to ISO Std Format.kmmacros (27 KB)

This macro is currently setup in PRODUCTION mode:

  1. Select the DATE you want to convert
  2. Run this macro
    3, IF date cannot be converted, user is prompted to enter a date
  3. Date in format YYYY-MM-DD is put on clipboard

For TEST MODE:

  1. DISABLE the "Cancel This Macro" Action

I have prepared a list of test dates that can be run against the AppleScript so you can see which formats can and cannot be converted.

####getKMVar Handler (function)

###BEGIN~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#     Name:     getKMVar(pKMVarName)      
#     Purpose:  Get the value of a KM Variable
#-------------------------------------------------------------------------------------
#     Ver 1.0   2016-03-14
#     AUTHOR:   JMichaelTX
#     REF:      
###——————————————————————————————————————————————————————————————————————————————————

on getKMVar(pKMVarName)
  
  tell application "Keyboard Maestro Engine"
    
    if exists variable pKMVarName then
      set KMVarValue to value of variable pKMVarName
      
    else
      set titleStr to "Script: " & (name of me) & return & return & "KM Variable:"
      set msgStr to return ¬
        & "▶▶▶ " & pKMVarName & " ◀◀◀" & return & return & "was NOT Found"
      
      beep
      display alert "▶▶▶ ERROR ◀◀◀" & return & titleStr message msgStr as critical ¬
        buttons {"Cancel"} -- last button is default
      
      set KMVarValue to missing value
      
      error "***ERROR***" & return & titleStr & return & msgStr
      
    end if -- KM Var exists
    
  end tell -- Keyboard Maestro Engine
  
  return KMVarValue
  
end getKMVar
###END~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 Likes

You’re a genius JMichaelTX!

After much trial an error (remember KM newbie) I did exactly what said and was able to get the test to run. I can now see what dates won’t work but I think this will work for my purposes.

BTW: I added a step in my macro that will automatically do CMD-V so it will paste.

Thank you all very much, you have made my life a bit easier!

1 Like

Not really. Chris (@ccstone) is the genius here. I just took his great AppleScript and put a KM wrapper around it.

Glad we could help. I think this script/macro could be of use to many people.

1 Like

It looks like JM (@JMichaelTX) did a good job providing a turnkey macro.

When I print-to-pdf on a regular basis I build a custom print service, so I don't have to fiddle with it.

See my post in this thread:

How to set Destination Folder from Safari “Print” and/or Save As PDF?

It lets me assign a custom destination and fiddle with the name.

I really need to rewrite the template to make it more user-friendly (I found it on the net somewhere), but time and other constraints have kept me from fooling with it much.

-Chris

1 Like

How would one go the other way - i.e. from yyyy-mm-dd to a Month Day, Year date string.
That is, now that there is a universal way to get to something like 2018-06-24, how can one convert that to June 24, 2018?
I have searched and tried a lot of things (javascipts from Stack Overflow using moment library, etc.) but can’t seem to get it right. Would prefer an AppleScript (or regex) implementation to other scripting languages because I am not a programmer in any way shape or form. TIA

It sounds like a Javascript action is not what you want, but FWIW, you can get as far as the '5/1/2018' format by using the standard JS Date.toLocaleString() method with the locale en-US.

(() => {
    'use strict';

    // ANY ISO 8601 DATE IN THE CLIPBOARD
    // CONVERTED ON AN 'en-US' LOCALE DATE STRING
    // (OTHER STRINGS LEFT UNCHANGED)

    // e.g.
    // 2018-05-01 -> 5/1/2018

    // main :: () -> IO String
    const main = () => {
        const
            sa = standardSEAdditions(),
            s = sa.theClipboard();

        return bindMay(
            localeStringFromISO8601May('en-US')(s),
            strDateTime => {
                const strDate = strDateTime.split(',')[0];
                return (
                    sa.setTheClipboardTo(strDate),
                    Just(strDate)
                );
            }
        ).Just || s;
    };

    // PARSING AND FORMATTING DATES -----------------------

    // localeStringFromISO8601May :: String -> String -> Maybe Date
    const localeStringFromISO8601May = strLocale => strISODate =>
        /\d{4}-\d{2}-\d{2}/.test(strISODate) ? Just(
            (new Date(Date.parse(strISODate)))
            .toLocaleString(strLocale)
        ) : Nothing();

    // JSA STANDARD ADDITIONS FOR SYSTEM EVENTS  ----------

    // standardSEAdditions :: () -> Application
    const standardSEAdditions = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        });

    // GENERIC FUNCTIONS ----------------------------------

    // Just :: a -> Just a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Nothing :: () -> Nothing
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

    // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    const bindMay = (mb, mf) =>
        mb.Nothing ? mb : mf(mb.Just);

    return main();

})();

Or, more flexibly, you could write a JXA function which returned May 1 2018 from a variety of strings, including 2018-05-01 and simply today:

(() => {
    'use strict';

    // mmmDyFromInformal :: String -> String
    const mmmDyFromInformal = strDate => {
        const fmtr = $.NSDateFormatter.alloc.init;
        return (
            fmtr.setLocale(
                $.NSLocale.localeWithLocaleIdentifier('en_US_POSIX')
            ),
            fmtr.setDateFormat('MMM d y'),
            fmtr.stringFromDate(
                $.NSDate.dateWithNaturalLanguageString(strDate)
            ).js
        );
    };

    return [
        mmmDyFromInformal('today'),
        mmmDyFromInformal('2018-05-01'),
    ].join('\n');
})();
1 Like

Thanks for sharing. This is a very useful function.

Would you mind explaining these statements:

        return (
            fmtr.setLocale(
                $.NSLocale.localeWithLocaleIdentifier('en_US_POSIX')
            ),
            fmtr.setDateFormat('MMM d y'),
            fmtr.stringFromDate(
                $.NSDate.dateWithNaturalLanguageString(strDate)
            ).js
        );

in particular:

  1. Why are there 3 statements in the return, when the result is only a string?
  2. What is the significance of the .js in this statement:
fmtr.stringFromDate(
                $.NSDate.dateWithNaturalLanguageString(strDate)
            ).js

Thanks.

The bracketed ( ... , ... , ... ) expression returns the result of evaluating its final term.

We could comment like this:

return (
    // EFFECTS
    fmtr.setLocale(
        $.NSLocale.localeWithLocaleIdentifier('en_US_POSIX')
    ),
    fmtr.setDateFormat('MMM d y'),
    // VALUE
    fmtr.stringFromDate(
        $.NSDate.dateWithNaturalLanguageString(strDate)
    ).js
);

and the effects are part of the assembly of the value.

My habit, for better or for worse, is to write functions with just two parts, a name-binding stage (const), and a return value (including effects) stage. Just a way of maintaining a kind of clarity for myself.

3 statements

One way of understanding this approach to constructing code is that it assembles compositions of values rather than sequencing 'statements' (which produce side effects but have no return value).

An example in would be the choice between:

  • if (...) { ... } (a statement – doesn't return a value), and
  • ... ? ... : ... (an expression – returns a value).

The trailing .js is syntactic sugar for an application of ObjC.unwrap, which converts from ObjC data types to JS data types.

YOU could write a JXA function, but I can’t :grinning:
But this seems to get me very close if I can substitute the clipboard or a KM variable in place of 2018-05-01

in the line:
mmmDyFromInformal(‘2018-05-01’)

Is that doable?
By the way, thank you for all your contributions to various forums that I frequent. You, Jmichael, Chris Stone, and gglick have made this forum the absolute best of all of them, however. Not to slight others such as Dan Thomas either, but just looking through my KM macros, you are the ones I’ve learned/stolen the most from.

2 Likes

Perhaps something along these lines ?

Date translation.kmmacros (18.9 KB)

(() => {
    'use strict';

    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            strMMMDy = mmmDyFromInformal(
                kme.getvariable('iso8601Date')
            );
        return (
            kme.setvariable('MMMdyDate', {
                to: strMMMDy
            }),
            strMMMDy
        );
    };

    // mmmDyFromInformal :: String -> String
    const mmmDyFromInformal = strDate => {
        const fmtr = $.NSDateFormatter.alloc.init;
        return (
            fmtr.setLocale(
                $.NSLocale.localeWithLocaleIdentifier('en_US_POSIX')
            ),
            fmtr.setDateFormat('MMM d y'),
            fmtr.stringFromDate(
                $.NSDate.dateWithNaturalLanguageString(strDate)
            ).js
        );
    };

    return main();
})();