Convert clipboard date to yyyy-mm-dd

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

Thanks to the script @ComplexPoint posted above, there is now a universal way to convert between date formats. I have built on his script, and provided a general purpose macro to do just this.

MACRO: Convert Date String from One Format to Another [Example]

Note that I have set the macro defaults to convert to the ISO format, which I think most people want. But you an easily change the default output format after you download install the macro. If you need any help with this, just post in the macro thread.

This is the bee’s knees. I made a slight modification to just copy a selected date to the clipboard and use that as the input, and from there it is easy-peasy.

Thanks very much to both of you.

1 Like

Is that anything like the "cat's meow"? (a saying favored by my grandmother. :smile: )