Another Date Format Conversion Request

Hi all,

Sorry to ask for help on something that seems to be widely discussed, however, I have been unable to find a solution for case use in Bear.

Copying Bear Creation date and pasting into note

Copy a date in this format: 27 DEC 2019 AT 18:35 and convert it to 20191227 18:35. Then on paste, enclose it, like this: [20191227 18:35]

I can't quite figure out how to RegEx the clipboard and turn the month abbr. into a numerical value, then copy it back to clipboard to paste.

RegEx String: (\d{2})(\ )(\w{3})(\ )(\d{4})(\ )(\w{2})(\ )(\d{2})(\:)(\d{2})

Help would be greatly appreciated.

One general approach here – executes a copy action, and then rewrites the clipboard contents to the new format, ready for pasting.

Convert a Bear date.kmmacros (28.9 KB)

Bear date parser – JS source
// bearDate :: Parser Date
const bearDate = () => {
    const
        nDigits = n => fmapP(compose(
            x => x.padStart(n, '0'),
            concat
        ))(some(digit()));
    return bindP(
        nDigits(2)
    )(day => bindP(
        whiteSpace()
    )(_ => bindP(
        fmapP(compose(
            maybe('01')(
                x => x.toString()
                .padStart(2, '0')
            ),
            enTriglyphMonth,
            concat
        ))(many(letter()))
    )(month => bindP(
        whiteSpace()
    )(_ => bindP(
        nDigits(4)
    )(year => bindP(
        some(satisfy(c => !isDigit(c)))
    )(_ => bindP(
        nDigits(2)
    )(hour => bindP(
        char(':')
    )(_ => bindP(
        nDigits(2)
    )(min => pureP(
        new Date(
            `${year}-${month}-${day}T${hour}:${min}`
        )
    ))))))))));
};
Parser combinator library – JS source
// ---------------- PARSER COMBINATORS -----------------
// Rob Trew @2020

// Parser :: String -> [(a, String)] -> Parser a
const Parser = f =>
    // A function lifted into a Parser object.
    ({
        type: 'Parser',
        parser: f
    });


// altP (<|>) :: Parser a -> Parser a -> Parser a
const altP = p =>
    // p, or q if p doesn't match.
    q => Parser(s => {
        const xs = parse(p)(s);
        return 0 < xs.length ? (
            xs
        ) : parse(q)(s);
    });


// apP <*> :: Parser (a -> b) -> Parser a -> Parser b
const apP = pf =>
    // A new parser obtained by the application 
    // of a Parser-wrapped function,
    // to a Parser-wrapped value.
    p => Parser(
        s => parse(pf)(s).flatMap(
            vr => parse(
                fmapP(vr[0])(p)
            )(vr[1])
        )
    );


// bindP (>>=) :: Parser a -> 
// (a -> Parser b) -> Parser b
const bindP = p =>
    // A new parser obtained by the application of 
    // a function to a Parser-wrapped value.
    // The function must enrich its output, lifting it 
    // into a new Parser.
    // Allows for the nesting of parsers.
    f => Parser(
        s => parse(p)(s).flatMap(
            tpl => parse(f(tpl[0]))(tpl[1])
        )
    );


// char :: Char -> Parser Char
const char = x =>
    // A particular single character.
    satisfy(c => x == c);


// count :: Int -> Parser a -> Parser [a]
const count = n =>
    // A list of n successive instances of p.
    p => sequenceP(
        replicate(n)(p)
    );


// digit :: Parser Char
const digit = () =>
    // A single digit.
    satisfy(isDigit);


// fmapP :: (a -> b) -> Parser a -> Parser b  
const fmapP = f =>
    // A new parser derived by the structure-preserving 
    // application of f to the value in p.
    p => Parser(
        s => parse(p)(s).flatMap(
            first(f)
        )
    );


// letter :: Parser Char
const letter = () =>
    // A single alphabetic character.
    satisfy(isAlpha);


// liftA2P :: (a -> b -> c) -> 
// Parser a -> Parser b -> Parser c
const liftA2P = op =>
    // The binary function op, lifted
    // to a function over two parsers.
    p => apP(fmapP(op)(p));


// many :: Parser a -> Parser [a]
const many = p => {
    // Zero or more instances of p.
    // Lifts a parser for a simple type of value 
    // to a parser for a list of such values.
    const some_p = p =>
        liftA2P(
            x => xs => [x].concat(xs)
        )(p)(many(p));
    return Parser(
        s => parse(
            0 < s.length ? (
                altP(some_p(p))(pureP([]))
            ) : pureP([])
        )(s)
    );
};


// oneOf :: [Char] -> Parser Char
const oneOf = s =>
    // One instance of any character found
    // the given string.
    satisfy(c => s.includes(c));


// parse :: Parser a -> String -> [(a, String)]
const parse = p =>
    // The result of parsing s with p.
    s => {
        // showLog('s', s)
        return p.parser([...s]);
    };

// pureP :: a -> Parser a
const pureP = x =>
    // The value x lifted, unchanged, 
    // into the Parser monad.
    Parser(s => [Tuple(x)(s)]);


// satisfy :: (Char -> Bool) -> Parser Char
const satisfy = test =>
    // Any character for which the 
    // given predicate returns true.
    Parser(
        s => 0 < s.length ? (
            test(s[0]) ? [
                Tuple(s[0])(s.slice(1))
            ] : []
        ) : []
    );


// sequenceP :: [Parser a] -> Parser [a]
const sequenceP = ps =>
    // A single parser for a list of values, derived
    // from a list of parsers for single values.
    Parser(
        s => ps.reduce(
            (a, q) => a.flatMap(
                vr => parse(q)(snd(vr)).flatMap(
                    first(xs => fst(vr).concat(xs))
                )
            ),
            [Tuple([])(s)]
        )
    );


// some :: Parser a -> Parser [a]
const some = p => {
    // One or more instances of p.
    // Lifts a parser for a simple type of value 
    // to a parser for a list of such values.
    const many_p = p =>
        altP(some(p))(pureP([]));
    return Parser(
        s => parse(
            liftA2P(
                x => xs => [x].concat(xs)
            )(p)(many_p(p))
        )(s)
    );
};


// whiteSpace :: Parser String
const whiteSpace = () =>
    // Zero or more non-printing characters.
    many(oneOf(' \t\n\r'));

For a gentle intro to parser combinators:

Unfortunately KM does not have a native way to get the month number from a month name, so we will need use JavaScript.

This should do the trick:

Below is just an example written in response to your request. You will need to use as an example and/or change to meet your workflow automation needs.

Please let us know if it meets your needs.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

MACRO:   Parse String to Extract Date [Example]

-~~~ VER: 1.0    2020-09-02 ~~~
Requires: KM 8.2.4+   macOS 10.11 (El Capitan)+
(Macro was written & tested using KM 9.0+ on macOS 10.14.5 (Mojave))

DOWNLOAD Macro File:

Parse String to Extract Date [Example].kmmacros
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.


ReleaseNotes

Macro Author: @JMichaelTX

This is just an example written in response to the below KM Forum Topic. You will need to use as an example and/or change to meet your workflow automation needs.

MACRO SETUP

  • Carefully review the Release Notes and the Macro Actions
    • Make sure you understand what the Macro will do.
    • You are responsible for running the Macro, not me.

==USE AT YOUR OWN RISK==

  • While I have given this a modest amount of testing, and to the best of my knowledge will do no harm, I cannot guarantee it.
  • If you have any doubts or questions:
    • Ask first
    • Turn on the KM Debugger from the KM Status Menu, and step through the macro, making sure you understand what it is doing with each Action.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@ComplexPoint, Thank you for your help :smile:

Parsing... is still well outside my capabilities I fear (Just getting my head around RegEx and very basic Python), but trying :wink:

Thank you for the video link, I will be sure to check it out!

The macro works, thank you :+1: however, not entirely as expected.

The output pasted looks like this [2020813 20:36] instead of this [2020-8-13 20:36].

I isolated, what I thought was the date formatting string:

  new Date(
                `${year}-${month}-${day}T${hour}:${min}`
            )

... and I can see there is a dash seperator between the year-month-day... however the result is not producing this.

I couldn't see anywhere else in the script (that I could make sense of, anyway) that indicated how I can change this.

Your help would be greatly appreciated!

Thank you in advance!

@JMichaelTX, thank you for the help with this also! Really appreciate it!

Strangely, I couldn't get this macro to work.

I set a hot key trigger, thinking the macro would run similar to the macro provided above by @ComplexPoint, however, nothing seemed to happen :woman_shrugging: and all that pasted was what was previously on the clipboard...

Am I doing something wrong?

Appreciate your thoughts :smile:

Thank you

That's easily adjusted

(for some reason your original post shows a hyphen-less form for the target, perhaps some kind of wiki formatting glitch ?)

The edit below restores the hyphens to the output:

Convert a Bear date.kmmacros (29.2 KB)

return 0 < matches.length ? (() => {
    const dte = matches[0][0];
    return [
        `[${dte.getFullYear()}-`,
        `${1 + dte.getMonth()}-`,
        `${dte.getDate()} `,
        `${dte.getHours()}:${dte.getMinutes()}]`
    ].join('');
})() : 'Not parseable as EN Bear date :: ' + (
    kme.getvariable('bearDate')
);

@ComplexPoint, that worked a treat! Thank you!

No glitch... simple human error I fear :blush:

Thank you for all your help! I've made a mental note as to the differences in the script for future use.

Thank you again :+1: appreciate the help!

1 Like

FWIW, as the output format that you need is a standard TaskPaper-style (informal iso8601) yyyy-mm-dd pattern, the output code could be reduced to:

(Use this version, which also updates the Bear date parser to allow for days and months with or without leading zeros.

Convert a Bear date.kmmacros (29.2 KB)

const main = () => {
    const
        kme = Application('Keyboard Maestro Engine'),
        matches = parse(bearDate())(
            kme.getvariable('bearDate')
        );

    return 0 < matches.length ? (
        taskPaperDateString(matches[0][0])
    ) : 'Not parseable as EN Bear date :: ' + (
        kme.getvariable('bearDate')
    );
};

where:

// ------------ INFORMAL ISO8601 DATE TIME -------------

// taskPaperDateString :: Date -> String
const taskPaperDateString = dte => {
    const [d, t] = iso8601Local(dte).split('T');
    return [d, t.slice(0, 5)].join(' ');
};

// iso8601Local :: Date -> String
const iso8601Local = dte =>
    new Date(dte - (6E4 * dte.getTimezoneOffset()))
    .toISOString();

Probably. :wink:

It is best NOT to assume anything, especially about how another macro works.

Did you make a selection of
27 DEC 2019 AT 18:35

and then trigger the macro?
I just tried it again and it worked fine.

It produced:
[20191227 18:35]

@JMichaelTX and @ComplexPoint , Thank you for your further help and advice. Greatly appreciated! :smile: Apologies for the delay in responding, been trying to work out some issues I have been having with the macros and Bear, but finally decided I wasn't getting anywhere fast... so I thought it best to return to the source :wink:

@JMichaelTX, I am still experiencing an issue with this macro, in that it doesn't seem to run?

  1. I set a hot trigger.
  2. Make a selection
  3. Fire the hot trigger
  4. Nothing...

I did notice a couple of times an error saying the macro timed-out before completing. But have no clue as to remedy this. Your thoughts would be welcomed.

Please refer to image below:

@ComplexPoint, Thank you so much for your help! The second macro works great! Just a question, is there a way I can amend the script to parse 1-2 digits for the day and paste yyyy-mm-dd mm:ss instead of yyyy-m-d mm:ss?

Again, thank you so much for all your help guys! Really appreciate it! :smile: :+1:

Aha I think you may have missed the 3rd version above : -)

Posted again here:

Convert a Bear date.kmmacros (29.2 KB)

(Parses single digit days and hours minutes, and pastes them zero padded to 2 digits where needed)

If it is not running, that means that either the Macro is disabled, or it is in a Macro Group that is either disabled or not active.
Try moving the Macro to a "Global Macro Group" so that it is always active.

@JMichaelTX... well, that's embarrassing :blush:

I can confirm, now, the macro works :+1: Thank you!

Just trying to see now if I can turn the single digit (1) in the Local__Day variable to a double digit (01).

Somedays I think I just about understand KM, other days, like today, I'm like "Girl, you are so out of your depth!" :laughing:

Thanks for all your help, really appreciate the input!

Easy. Just set the "Local_Day" variable to itself in a Set Variable to Calculation action and check and provide the format. Should be "00" in this case.

@JMichaelTX... well that went better than expected :sweat_smile:

Took a little while to figure out, but eventually did!

Thanks for the help! That works now as needed! :smile:

Congrats! Now that you have figured it out, you will most likely remember it in the future.