Date converter snippet

By email (thanks, Steve!)

Could you consider a tool to put a date into a specified format … so that, for example, selecting text such as "January 29, 2024” could be transformed to an international day-month-year format, such as “29 January 2024” or “29 Jan 2024” or “29.01.2024”, etc. You’d have to allow for a slew of possible input formats but fewer, I think, output formats.

Here is a snippet for this:

// #popclip
// name: DateFormatConverter
// icon: 📅
// description: Convert dates to a specified format.
// language: javascript

// Example function to convert date to the desired format
function convertDateToFormat(inputText) {
  // Attempt to parse the date using built-in Date parsing
  const parsedDate = new Date(inputText);

  // Check if the date is valid
  if (!isNaN(parsedDate)) {
    // Define the output formats
    const options1 = { day: '2-digit', month: 'long', year: 'numeric' }; // "29 January 2024"
    const options2 = { day: '2-digit', month: 'short', year: 'numeric' }; // "29 Jan 2024"
    const options3 = { day: '2-digit', month: '2-digit', year: 'numeric' }; // "29.01.2024"

    // Choose the desired output format here by uncommenting one of the lines below
    const outputFormat = new Intl.DateTimeFormat('en-GB', options1).format(parsedDate); // for "29 January 2024"
    //const outputFormat = new Intl.DateTimeFormat('en-GB', options2).format(parsedDate); // for "29 Jan 2024"
    //const outputFormat = new Intl.DateTimeFormat('en-GB', options3).format(parsedDate).replace(/\//g, '.'); // for "29.01.2024"

    return outputFormat;
  } else {
    // If the date is not valid, return the input text
    return "Invalid date format";
  }
}

// Main PopClip action
popclip.pasteText(convertDateToFormat(popclip.input.text));

(The above block is an extension snippet — select it then click “Install Extension” in PopClip.)

1 Like

This is very useful, but I’d like a way to toggle two things:

  1. Source: Choose whether date is either (a) current (system) date or (b) selected text
  2. Format: Choose either (a) options2 (month as 3-character abbreviation) or (b) options3 (month as two digits)

Obviously I could hard code four separate extensions.

But is there a way to write a single extension so that these Source and Format selections are made interactively as the extension runs?

I modified your example into a snippet (below) that toggles between using the current date vs selected text based on whether the selected text is only whitespace.

Unfortunately, PopClip doesn’t activate if only whitespace is selected. Is there away to activate my snippet with a selection of whitespace? Alternatively, do you have a better idea for signaling to simply insert the current date?

Thank you.

// #popclip
// name: YYYY-MM-DD
// icon: symbol:calendar
// description: Convert date to YYYY-MM-DD.
// language: javascript

function convertDateFormat(inputText) {
	// function to convert format of date passed as inputText
	//
	// 
	var inputDate; // Date to convert to string formatted as YYY-MM-DD
	// Check if inputText might be a date
	if( inputText.trim().length === 0 ) {
		// inputText is empty or all whitespace so format today's date
		inputDate = new Date();
	}
	else {
	 // Attempt to parse inputText into a date
	 inputDate = new Date(inputText);
	}
	// Check if the inputDate is valid
	if (isNaN(inputDate)) {
		// Date is not valid, return the input text
		return "'" + inputText + "' is not a valid date format";
	} 
	else {
	// Define the output format options
	const option1 = { day: '2-digit', month: 'long', year: 'numeric' }; // "29 January 2024"
	const option2 = { day: '2-digit', month: 'short', year: 'numeric' }; // "29 Jan 2024"
	const option3 = { day: '2-digit', month: '2-digit', year: 'numeric' }; // "29 01 2024"
	// Choose the desired output format here by uncommenting only one of the lines below
	// const outputFormat = new Intl.DateTimeFormat('en-GB', option1).format( inputDate); // for "29 January 2024"
	// const outputFormat = new Intl.DateTimeFormat('en-GB', option2).format( inputDate).replace(/\//g, '-'); // for "29-Jan-2024"
	// const outputFormat = new Intl.DateTimeFormat('en-GB', option3).format( inputDate).replace(/\//g, '-'); // for "29-01-2024"
	const outputFormat = inputDate.toISOString().split('T')[0]; // for "2024-01-29"
	return outputFormat;	
	}
}
// Main PopClip action
popclip.pasteText(convertDateFormat(popclip.input.text));

PopClip won’t activate on a whitespace-only selection. However it will activate on an empty selection, by long pressing the mouse button (hold for 0.5 seconds). Alternatiely, Shift-click. Or use the PopClip keyboard shortcut to make it appear. Double-click will work in some text areas too.

In the extension config you’ll need to set requirements: [] to tell PopClip to show the extension’s actions when there is no selected text.

example

// #popclip - appear with "Paste" only (no selected text needed)
// name: paster
// requirements: []
// language: javascript
popclip.pasteText(`input text is '${popclip.input.text}'`)

CleanShot 2024-04-11 at 08.26.48

1 Like

One way to do this is to have more than one action in the extension. This can be done by making the extension module-based and exporting more than one action. See example below.

Another way is to react to a modifier key – Option is the bets choice – by checking popclip.modifiers.option.

I’ll write up an example with everything in.

// #popclip
// name: DateFormatConverter
// icon: 📅
// description: Convert dates to a specified format.
// requirements: []
// language: javascript
// module: true

const dateOptions1 = { day: '2-digit', month: 'short', year: 'numeric' }; // "29 Jan 2024"
const dateOptions2 = { day: '2-digit', month: '2-digit', year: 'numeric' }; // "29/01/2024"

// Example function to convert date to the desired format
function convertDateToFormat(inputText, dateOptions) {
  // Attempt to parse the date using built-in Date parsing.
  // If no input text, use current date.
  const parsedDate = inputText.length > 0 ? new Date(inputText) : new Date();

  // Check if the date is valid
  if (!isNaN(parsedDate)) {
    return Intl.DateTimeFormat('en-GB', dateOptions).format(parsedDate);    
  } else {
    return "Invalid date format";
  }
}

// an action for each format
exports.actions = [{
  title: "DateFormat1",
  icon: "d1", // unimaginative icon for now
  code: () => popclip.pasteText(convertDateToFormat(popclip.input.text, dateOptions1))
},{
  title: "DateFormat2",
  icon: "d2",
  code: () => popclip.pasteText(convertDateToFormat(popclip.input.text, dateOptions2))
}];

Alternative using Option key to differentiate:

// ... rest as above

// choose format based on presence of option modifier.
// here we don't need to wrap the code in an object with title and icon,
// since we only have one action so we are happy to just inherit
// the extension's name and icon. (this is basically what happens under
// the hood when using a simple javascript action without `module: true`)
exports.action = () => {
  popclip.pasteText(convertDateToFormat(
    popclip.input.text,
    popclip.modifiers.option ? dateOptions2 : dateOptions1
  ));
}
1 Like

@nick

I combined your two comments for:

  1. Activate PopClip with no selection, and
  2. Multiple Actions. (I embellished your simple conditional statement by cribbing the examples in the developer documentation for Module-based extensions.)

My resulting Snippet (below) seems to work well. I’m very pleased with its coding structure because it can be easily extended to support more output formats simply by adding one of the following for each additional format:

  1. DateFormat,
  2. switch case, and
  3. exports.options item.

I struggled mightily to create icons that communicated the idea of formatting a date as either ISO (YYYY-MM-DD) or Short Month (DD-MMM-YYYY). Ideally, my icons would have consisted of two parts, an icon for a calendar followed by a modifier that conveyed the format. Ultimately, I settled on just the format because I couldn’t figure out how to combine an icon such as symbol:calendar with text representing the format.

However, I’m pleased to learn that mousing over an icon reveals the title, as you can see in this screenshot:

Please let me know if you have a better idea for either icon.

Thank you.

-nello

// #popclip
// name: Reformat Date
// icon: symbol:calendar
// description: Convert date to either DD-MMM-YYYY or YYYY-MM-DD.
// requirements: []
// language: javascript
// module: true
//
// Define the output format options
const DateFormat1 = { day: '2-digit', month: 'long', year: 'numeric' };    // "29 January 2024"
const DateFormat2 = { day: '2-digit', month: 'short', year: 'numeric' };   // "29 Jan 2024"
const DateFormat3 = { day: '2-digit', month: '2-digit', year: 'numeric' }; // "29/01/2024"
// 
var strTemp
//
function convertDateFormat( inputText, dateOption ) {
  // function to convert format of date passed as inputText
  //
  // If inputText is all whitespace then use today's date instead
  const inputDate = inputText.trim().length > 0 ? new Date( inputText.trim() ) : new Date();
  // Check if the inputDate is valid
  if ( isNaN( inputDate ) ) {
    // Date is not valid, return the input text
    return "'" + inputText + "' is not a valid date format";
  } 
  else {
    switch( dateOption ) {
      case DateFormat1:
        // 29 January 2024
        return Intl.DateTimeFormat( 'en-GB', DateFormat1 ).format( inputDate );  // "29 January 2024"
        break;
      case DateFormat2:
        // 29-Jan-2024
        return Intl.DateTimeFormat( 'en-GB', DateFormat2 ).format( inputDate ).replace(/\s+/g, '-');;  // "29-Jan-2024"
        break;
      case DateFormat3:
        // 2024-01-29
        strTemp = Intl.DateTimeFormat( 'en-GB', DateFormat3 ).format( inputDate );  // "29/01/2024"
        strTemp = strTemp.split("/")
        return strTemp[2] + "-" + strTemp[1] + "-" + strTemp[0];                    // "2024-01-29"
        break;
    }
  }
}
// An action for each format
exports.actions = [{
  title: "DD-MMM-YYYY",
  icon: "monospaced square filled MMM",
  code: () => {
    popclip.pasteText( convertDateFormat( popclip.input.text, DateFormat2 ) );  // "29-Jan-2024"
  }
},{
  title: "YYYY-MM-DD",
  icon: "monospaced square filled ISO",
  code: () => {
    popclip.pasteText( convertDateFormat( popclip.input.text, DateFormat3 ) );  "2024-01-29"
  }
}];

Nice job! Love to usee user success. Icons look great to me, the main thing is that they convey recognisable meaning. It’s not possible to combine text and and image in the same icon, though I can imagine that might be a useful option.

1 Like

I’ve enhanced my Snippet, including adding more date formats (with corresponding exports.actions and icons:

Action Icons

Is there a way to turn actions on/off (other than adding/removing or commenting/uncommenting the code)?

For example, can Snippets have configuration “gears” that turn an action on or off?

PopClip Gears

I looked at the config dictionary portion of the PopClip Extensions Developer Reference but couldn’t see a way to turn on/off an action. Sorry if I overlooked or misunderstood.

Thank you.

I can post in more detail later but you want to look into the Options section.

Define a boolean option with identifier e.g. style1, style2 for each action and then in the action set a requirements array e.g. ["option-style1=1"] on the corresponding action.

Yes, please post an example for this action:

exports.actions = [{
  title: "MMMM D, YYYY",
  icon: "monospaced square filled LNG",
  code: () => {
    popclip.pasteText( convertDateFormat( popclip.input.text, DateFormat1 ) );  // "1 January 2024"
  }
]

I looked at the Options section but can’t quite understand exactly how to create the options because there is neither an example of an options array in general nor one for exported actions in particular.

Thank you.

Here’s how we can add an option menu to your previous example:

// #popclip
// name: Reformat Date
// icon: symbol:calendar
// description: Convert date to either DD-MMM-YYYY or YYYY-MM-DD.
// options:
// - { identifier: style-mmm, label: "DD-MMM-YYYY", type: boolean, icon: "monospaced square filled MMM" }
// - { identifier: style-iso, label: "YYYY-MM-DD", type: boolean, icon: "monospaced square filled ISO" }
// language: javascript
// module: true
//
// Define the output format options
const DateFormat1 = { day: "2-digit", month: "long", year: "numeric" }; // "29 January 2024"
const DateFormat2 = { day: "2-digit", month: "short", year: "numeric" }; // "29 Jan 2024"
const DateFormat3 = { day: "2-digit", month: "2-digit", year: "numeric" }; // "29/01/2024"
//
var strTemp;
//
function convertDateFormat(inputText, dateOption) {
  // function to convert format of date passed as inputText
  //
  // If inputText is all whitespace then use today's date instead
  const inputDate =
    inputText.trim().length > 0 ? new Date(inputText.trim()) : new Date();
  // Check if the inputDate is valid
  if (isNaN(inputDate)) {
    // Date is not valid, return the input text
    return "'" + inputText + "' is not a valid date format";
  } else {
    switch (dateOption) {
      case DateFormat1:
        // 29 January 2024
        return Intl.DateTimeFormat("en-GB", DateFormat1).format(inputDate); // "29 January 2024"
        break;
      case DateFormat2:
        // 29-Jan-2024
        return Intl.DateTimeFormat("en-GB", DateFormat2)
          .format(inputDate)
          .replace(/\s+/g, "-"); // "29-Jan-2024"
        break;
      case DateFormat3:
        // 2024-01-29
        strTemp = Intl.DateTimeFormat("en-GB", DateFormat3).format(inputDate); // "29/01/2024"
        strTemp = strTemp.split("/");
        return strTemp[2] + "-" + strTemp[1] + "-" + strTemp[0]; // "2024-01-29"
        break;
    }
  }
}
// An action for each format
exports.actions = [
  {
    title: "DD-MMM-YYYY",
    icon: "monospaced square filled MMM",
    requirements: ["option-style-mmm=1"],
    code: () => {
      popclip.pasteText(convertDateFormat(popclip.input.text, DateFormat2)); // "29-Jan-2024"
    },
  },
  {
    title: "YYYY-MM-DD",
    icon: "monospaced square filled ISO",
    requirements: ["option-style-iso=1"],
    code: () => {
      popclip.pasteText(convertDateFormat(popclip.input.text, DateFormat3));
      ("2024-01-29");
    },
  },
];
1 Like

Thanks, @nick and @nello. I do all dates in ISO format, so this is really useful for me! Greatly appreciated.

A question: The extension seems to work well with dates that have a day, month, and year. However, if a date has only a month and day, the extension defaults to the year 2000. Is there a way to make it so it defaults to the current year, which would be my probable use case for a date without a year?

This is a really good question. Easiest way would be to just detect dates set to 2000 and then replace with current year … but this would fail if the actual date was in 2000!

Need to think how to tell the two cases apart. I guess if the original link text contains the string 2000 we can assume it is actually 2000, otherwise we replace the year. I’ll have a think…

Apparently new Date(inputText.trim()) makes an assumption about a missing year only when inputText contains a month represented as text; numeric dates without a year are NOT a valid format:

For example, when I choose ISO:

Numeric month
6/8'6/8' is not a valid date format
6-18‘6-16' is not a valid date format

Text Month
June 182000-06-18 (but user wants 2024-06-18)
jun 182000-06-18 (but user wants 2024-06-18)

The year 2000 can be represented with any number of zeros
jun 18 02000-06-18
jun 18 002000-06-18
jun 18 0002000-06-18
jun 18 00002000-06-18
jun 18 000002000-06-18

Year delimiters may be many but not all single-characters
jun 18 20002000-06-18
jun 18-20002000-06-18
jun 18+20002000-06-18
jun 18=2000'jun 18=2000' is not a valid date format

Most multi-character year delimiters will be trapped BUT NOT ALL!
jun 18--20002001-06-18 (double-hyphen adds a year!?!)
jun 18++0'jun 18++0 ' is not a valid date format
jun 18++2000'jun 18++2000' is not a valid date format

So, apparently, the test for whether to change the year 2000 to the current year involves looking carefully at inputText.trim() and seeing whether if contains an “acceptable” — how many zeroes and what kinds of delimiters are valid vs typographical errors in the input? — representation of the year 2000.

If I were doing this, I’d add a new case to switch( dateOption ) so that year isn’t swapped without the user choosing to do so explicitly.

1 Like