Convert Date String from One Format to Another [Example]

Continuing the discussion from Convert clipboard date to yyyy-mm-dd:

First, let me give my sincere thanks to, and credit to, @ComplexPoint for his excellent script posted in the above link. I have made some significant mods to this script, but the core processing function remains the same -- it still uses the JXA ObjC interface to do all of the date processing.


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

~~~ VER: 1.0    2018-05-01 ~~~

DOWNLOAD:

Convert Date String from One Format to Another [Example].kmmacros (88 KB)
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.

** As always, if you have any issues, comments, or suggestions concerning this macro/script, please feel free to post below.**


Use Case

  1. Provide a general solution for converting a date string from almost any standard format, into another format.
  2. This makes it very easy for you to get the date format you want.
  3. Just enter the date and desired format in the User Prompt, and click OK to get the converted format.
  4. If you often use the same format, you can change the defaults in the Prompt and in the Script to use your favorite format.
  5. The macro and script use KM Local Variables , which means that they will be auto-deleted when the macro ends/exits.
    • But you can easily preserve the data, and use as defaults in the Prompt by setting a global variable to the Local variable and using that in the Prompt.
    • If you don't know how to do this, just ask.

Example Output

image

image

image


ReleaseNotes

Author.@JMichaelTX (based on script by @ComplexPoint)

PURPOSE:

  • Convert a Date String from One Format to Another

REQUIRES:

  1. KM 8.0.2+
  • But it can be written in KM 7.3.1+
  • It is KM8 specific just because some of the Actions have changed to make things simpler, but equivalent Actions are available in KM 7.3.1.
    .
  1. macOS 10.11.6 (El Capitan)
  • KM 8 Requires Yosemite or later, so this macro will probably run on Yosemite, but I make no guarantees. :wink:

NOTICE: This macro/script is just an Example

  • It has had very limited testing.
  • You need to test further before using in a production environment.
  • It does not have extensive error checking/handling.
  • It may not be complete. It is provided as an example to show you one approach to solving a problem.

How To Use

  1. Trigger this macro.
  • It will then prompt you for the date and format to be converted
  • If you enter nothing, script will use defaults of "today" and ISO date format "yyyy-mm-dd"

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. ??
      .
  1. Assign a Trigger to this maro..
  2. Move this macro to a Macro Group that is only Active when you need this Macro.
  3. ENABLE this Macro.
    .
  • REVIEW/CHANGE THE FOLLOWING MACRO ACTIONS:
    (all shown in the magenta color)
    • Prompt for User Input (add/change default date formats)
    • Display Results

TAGS: @Date @JXA @Convert

USER SETTINGS:

  • Any Action in magenta color is designed to be changed by end-user

ACTION COLOR CODES

  • To facilitate the reading, customizing, and maintenance of this macro,
    key Actions are colored as follows:
  • GREEN -- Key Comments designed to highlight main sections of macro
  • MAGENTA -- Actions designed to be customized by user
  • YELLOW -- Primary Actions (usually the main purpose of the macro)
  • ORANGE -- Actions that permanently destroy Variables or Clipboards,
    OR IF/THEN and PAUSE Actions

USE AT YOUR OWN RISK

  • While I have given this limited 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.

image


JXA Script for Convert Date Formats

If you need any help in understanding, using, or modifying this script, feel free to ask.

var ptyScriptName   = "Convert Any Date String to Any Formatted Date";
var ptyScriptVer     = "2.0";
var ptyScriptDate   = "2018-05-01";
var ptyScriptAuthor = "JMichaelTX";  // heavy lifting by @ComplexPoint;

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PURPOSE:
  • Convert Any* Date String to Any* Formatted Date, using data from KM Macro
    where "Any" is probably those that conform to the Unicode Date formats (Ref #2)
  
RETURNS:  Date/Time String formatted as requested, 
          both as a script return, and as a KM Variable "Local__DateOutput"

REQUIRED:
  1.  macOS 10.11.6+
  2.  Mac Applications
      • Keyboard Maestro 8.2+
      
  3.  Keyboard Maestro Variables
        •  GET (input)
          •  Local__DateSourceStr
          • Local__DateOutputFormat
          
        • SET (output)
          •  Local__DateOutput    
        
TAGS:  

REF:  The following were used in some way in the writing of this script.

  1.  2018-05-01, ComplexPoint, Keyboard Maestro Discourse
      Convert clipboard date to yyyy-mm-dd
      https://forum.keyboardmaestro.com/t/convert-clipboard-date-to-yyyy-mm-dd/3155/16?u=jmichaeltx
      
  2.  2013-09-18, unicode.org/Peter Edberg, www.unicode.org
      UTS #35: Unicode LDML: Dates, Ver 33
      https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
(() => {    // this function will auto-run when script is executed
    'use strict';
    
    var scriptResults = "TBD";

try {  //~~~~~~~~~~~ START TRY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  

  var app = Application.currentApplication()
  app.includeStandardAdditions = true
    
  //--- EXAMPLES --- (all statements in this block may be removed)
  
  var date1Str = dateStrToFormattedDate("today", "MMM dd, yyyy");
  //-->"May 01, 2018"
  
  
  var date2Str = dateStrToFormattedDate("2018-04-15", "");
  //-->"Apr 15 2018"
  
  var date3Str = dateStrToFormattedDate("", "");
  //-->"May 1 2018"

  
  //-----------------------------------------------------------
  
  //--- GET KM VARS with Default Values ---
  
  
  var kmeApp             = Application("Keyboard Maestro Engine");
  var kmInst             = app.systemAttribute("KMINSTANCE");

  var dateSourceStr     = kmeApp.getvariable('Local__DateSourceStr',  {instance: kmInst}) || "today";
  var dateOutputFormat   = kmeApp.getvariable('Local__DateOutputFormat',  {instance: kmInst}) || "yyyy-MM-dd";
  
  var dateOutputStr     = dateStrToFormattedDate(dateSourceStr, dateOutputFormat);

  //--- Set KM Output Variable Even if Date is empty string --

  kmeApp.setvariable('Local__DateOutput', { to: dateOutputStr,  instance: kmInst });
  
  if (dateOutputStr) {
  scriptResults          = dateOutputStr;
  
  } else {    // Return Error Msg if Data Can't be converted
  
    throw new Error("Unable to Convert Date from: " + dateSourceStr + "   Using format: '" + dateOutputFormat + "'");
  }
  
} //~~~~ END TRY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

catch (oError) {
  
  if (oError.errorNumber === -128) {  // User Canceled
  
    scriptResults =  "[USER_CANCELED]\n\n"
      + "SCRIPT: " + ptyScriptName + "   Ver: " + ptyScriptVer
  }
  
  else {
    var errNum = oError.errorNumber || "Custom Error";
    scriptResults = "[ERROR]\n\n"
      + "Error Number: " + errNum + "\n"
      + oError.message
      + "\n\nSCRIPT: " + ptyScriptName + "   Ver: " + ptyScriptVer
      
  } // END if/else on ERROR Number
  
} //~~~~ END TRY/CATCH BLOCK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  
  return scriptResults;
  
  //~~~~~~~~~~~~~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
  function dateStrToFormattedDate(pDateStr, pFormatStr) {  
  
  /*
    For Date/Time Format Syntax, see
    https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
    
    For macOS Locale Identifiers, see
    ##TBD##
  */
    //--- SET DEFAULT VALUES ---
  
    pDateStr     = pDateStr     || 'today';        // Default input date string
    pFormatStr   = pFormatStr   || 'yyyy-MM-dd';  // Default output format

    //--- USE ObjC to Convert Input Date String and Output as Formatted ---
    
    const nsDateFmtr = $.NSDateFormatter.alloc.init;
    
    nsDateFmtr.setLocale($.NSLocale.localeWithLocaleIdentifier('en_US_POSIX'));
    nsDateFmtr.setDateFormat(pFormatStr);
    
    //--- Use .js to Convert DataType from ObjC to JavaScript (same as ObjC.unwrap) ---
    var dateStrNewFormat = nsDateFmtr.stringFromDate($.NSDate.dateWithNaturalLanguageString(pDateStr)).js;
    
    //--- Return Empty String if Conversion Fails (undefined) ---
    return (dateStrNewFormat || "");
  };

})();
6 Likes

And for flexible batch use (converting a set of dates, or a set of lines, all containing a date), either for a specific locale or the current default locale, you could, in JS, write various simpler and more specific functions in terms of a more general one.

For example, rewriting:

[
    'today', 'tomorrow', 'not a date',
    '1956-01-24', '1960-04-08', '1965-08-16', '1971-01-14'
]

to

May 2 2018
May 3 2018
"not a date" could not be parsed as a date.
Jan 24 1956
Apr 8 1960
Aug 16 1965
Jan 14 1971

With something like the following (in which results are returned wrapped in a Dict, with a Left field for error messages, and a Right field for successful results)

(() => {
    'use strict';

    const main = () => {

        // SIMPLIFIED VERSIONS OF A GENERAL FUNCTION:
        const
            // A version which always uses the current locale,
            asLocalDateStringLR = reformattedDateStringLR(Nothing()),

            // and a version which (also) always uses a specific format.
            asMMMdy = asLocalDateStringLR('MMM d y');

        return unlines(
            [
                'today', 'tomorrow', 'not a date',
                '1956-01-24', '1960-04-08', '1965-08-16', '1971-01-14'
            ]
            .map(x => {
                const lrDateString = asMMMdy(x);
                return lrDateString.Left || lrDateString.Right
            })
        );
    };

    // GENERAL DATE REFORMATTING FUNCTION ----------------------------------

    // reformattedDateStringLR :: Maybe String -> String -> String ->
    //                                              Either String String
    const reformattedDateStringLR = mbLocaleStr => strFormat => strDate => {
        const dateParsed = $.NSDate.dateWithNaturalLanguageString(strDate);
        return bindLR(
            dateParsed.js !== undefined ? Right(
                dateParsed
            ) : Left('"' + strDate + '" could not be parsed as a date.'),
            objcDate => {
                const fmtr = $.NSDateFormatter.alloc.init;
                return (
                    fmtr.setLocale(
                        $.NSLocale.localeWithLocaleIdentifier(
                            (
                                (mbLocaleStr && mbLocaleStr.Just) ||
                                $.NSLocale.currentLocale.localeIdentifier.js
                            ) + '_POSIX'
                        )
                    ),
                    fmtr.setDateFormat(strFormat || 'yyyy-mm-dd'),
                    Right(
                        fmtr.stringFromDate(objcDate).js
                    )
                );
            }
        );
    };

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

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

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) => [].concat.apply([], xs.map(f));

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // MAIN --------------------------------------------------
    return main();
})();

1 Like

Or to specify some locale other than the active one, for example to derive something like:

2018 mai 2
2018 mai 3
"not a date" could not be parsed as a date.
1956 janvier 24
1960 avril 8
1965 août 16
1971 janvier 14

2018 五月 2
2018 五月 3
"not a date" could not be parsed as a date.
1956 一月 24
1960 四月 8
1965 八月 16
1971 一月 14
(() => {
    'use strict';

    const locales = ['fr_FR', 'zh_Hans_CN'];

    const main = () =>
        locales.map(
            strLocale => {
                const
                    localeFormattedLR = reformattedDateStringLR(Just(strLocale)),
                    dateFormattedLR = localeFormattedLR('y MMMM d')

                return unlines(
                    [
                        'today', 'tomorrow', 'not a date',
                        '1956-01-24', '1960-04-08', '1965-08-16', '1971-01-14'
                    ]
                    .map(x => {
                        const lrDateString = dateFormattedLR(x);
                        return lrDateString.Left || lrDateString.Right
                    })
                )
            }
        ).join('\n\n');

    // GENERAL DATE REFORMATTING FUNCTION ----------------------------------

    // reformattedDateStringLR :: Maybe String -> String -> String ->
    //                                              Either String String
    const reformattedDateStringLR = mbLocaleStr => strFormat => strDate => {
        const dateParsed = $.NSDate.dateWithNaturalLanguageString(strDate);
        return bindLR(
            dateParsed.js !== undefined ? Right(
                dateParsed
            ) : Left('"' + strDate + '" could not be parsed as a date.'),
            objcDate => {
                const fmtr = $.NSDateFormatter.alloc.init;
                return (
                    fmtr.setLocale(
                        $.NSLocale.localeWithLocaleIdentifier(
                            (
                                (mbLocaleStr && mbLocaleStr.Just) ||
                                $.NSLocale.currentLocale.localeIdentifier.js
                            ) + '_POSIX'
                        )
                    ),
                    fmtr.setDateFormat(strFormat || 'yyyy-mm-dd'),
                    Right(
                        fmtr.stringFromDate(objcDate).js
                    )
                );
            }
        );
    };

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

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

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) => [].concat.apply([], xs.map(f));

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // MAIN --------------------------------------------------
    return main();
})();

Thank you @JMichaelTX and @ComplexPoint soooo very much for this scrip/macrot! I just installed KM and ran this for the first time and it worked like a charm!

I know nothing about KM, but was wondering if it would be possible to default to the clipboard contents instead of "today." If so, how? I took C++ and Assembly in college a few decades ago - and I tried, but was having a hard time figuring out how to replace "today" with what I think needs to be a new "read from clipboard" var.

Since I have just clipped the date I want to convert before I start the macro, it seems a more efficient process.

Thoughts? If it is possible, would you be so kind as to share not just baby steps, but infant crawl help?

1 Like

Here is a version which converts and pastes the date expression in the clipboard.

Paste copied date in standardised format.kmmacros (23.1 KB)
updated

Javascript source

(() => {
    'use strict';

    const main = () => {
        const
            kmVar = Application('Keyboard Maestro Engine').getvariable,
            [strLocale, strPattern] = map(
                k => kmVar(k), ['dateFormatLocale', 'dateFormatPattern']
            ),
            lrDateString = reformattedDateStringLR(
                Boolean(strip(strLocale)) ? (
                    Right(strLocale)
                ) : Left('Default locale'),
                strPattern,
                standardAdditions().theClipboard()
            );
        return lrDateString.Left || lrDateString.Right;
    };

    // DATE REFORMATTING ----------------------------------

    // reformattedDateStringLR :: Either String String ->
    //                      String -> String -> Either String String
    const reformattedDateStringLR = (lrLocaleStr, strFormat, strDate) => {
        const dateParsed = $.NSDate.dateWithNaturalLanguageString(strDate);
        return bindLR(
            dateParsed.js !== undefined ? Right(
                dateParsed
            ) : Left('"' + strDate + '" could not be parsed as a date.'),
            objcDate => {
                const fmtr = $.NSDateFormatter.alloc.init;
                return (
                    fmtr.setLocale(
                        $.NSLocale.localeWithLocaleIdentifier(
                            (
                                (lrLocaleStr && lrLocaleStr.Right) ||
                                $.NSLocale.currentLocale.localeIdentifier.js
                            ) + '_POSIX'
                        )
                    ),
                    fmtr.setDateFormat(strFormat || 'yyyy-mm-dd'),
                    Right(
                        fmtr.stringFromDate(objcDate).js
                    )
                );
            }
        );
    };

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // not :: Bool -> Bool
    const not = b => !b;

    // strip :: String -> String
    const strip = s => s.trim();

    // JXA ------------------------------------------------

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });

    // MAIN ---
    return main();
})();

1 Like

Yes, this is very easy. Just change the Prompt to use %SystemClipboard% as the default value, like this:

image

Then, if you want the results to be put back on the Clipboard, just add this Action to the very end of the Macro:

image


Questions?

Brilliant!
Thank you @ComplexPoint and @JMichaelTX for your work on this macro.
It was easy enough to embed the core Javascript action in a "For Each Item" loop through all the selected files in the Finder.

A post was split to a new topic: How Do I Split Date Parts and Paste Each Part?