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:
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.
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:
------------------------------------------------------------
@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.
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.)
###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~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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!
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.
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();
})();
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
YOU could write a JXA function, but I can’t
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.