Skip to content

Instantly share code, notes, and snippets.

@vibhavsinha
Last active September 17, 2022 02:27
Show Gist options
  • Save vibhavsinha/a104c789c4e71633b3517a494a2910aa to your computer and use it in GitHub Desktop.
Save vibhavsinha/a104c789c4e71633b3517a494a2910aa to your computer and use it in GitHub Desktop.
Textexpander snippet for date formatting with configurable options. Made to run on both text-expander and as node cli for testing.
// Use tab keys to quickly go to OK button
let dateFormat = '%fillpopup:name=dateFormat:default=YYYY-MM-DD:MMM DD, YYYY:DD/MM/YY:MM/DD/YY:none%';
let timeFormat = '%fillpopup:name=timeFormat:HH MM:HH MM SS:default=HH MM ZONE:HH MM SS ZONE:none%';
let hour24 = '%fillpart:name=24 hour format:default=yes%hour24%fillpartend%';
let includeYear = '%fillpart:name=Include year:default=yes%includeYear%fillpartend%';
// keeping these at the top because TextExpander has to show the
// full function code every time.
// This is very long code. Use tab keys to quickly go to ok button.
// Time options have spaces instead of colons because I couldn't
// find a way to escape the colons for TextExpander.
// main function
function printDate({dateFormat, timeFormat, hour24, includeYear}) {
includeYear = Boolean(includeYear);
hour24 = Boolean(hour24);
const date = getDate();
let {zone, dateOptions} = parseDateFormat(date, dateFormat, includeYear);
let timeOptions = parseTimeFormat(date, timeFormat, hour24);
const options = {...dateOptions, ...timeOptions};
return new Intl.DateTimeFormat(zone, options).format(date);
}
// TODO support output to more time zones
// TODO support input from configurable timezone
// function to maintain a single readline interface. this interface
// can be used multiple times but should be generated only once.
// readline will not be available in TextExpander hence it can not
// be required and kept outside all functions
var rl;
// parses the TextExpander syntax of fillpopup to get options list
// this function will only be called from nodejs when testing
function ask(fillStr) {
const {fillType} = fillStr.match(/^%fill(?<fillType>\w+)/).groups;
switch (fillType) {
case 'part':
return askPart(fillStr);
case 'popup':
return askPopup(fillStr);
default:
throw new Error('Invalid fill type: ' + fillType);
}
}
function askPopup(fillStr) {
const options = fillStr
.slice(1, -1).split(':').slice(1)
.filter(o => !o.startsWith('name='))
.map(o => (o.startsWith('default=') ? o.slice(8) : o));
if (options.length === 0) throw new Error('empty options list');
// print an index for each option to terminal to make is easier to select
// printing it as an object instead of string concat gives it color too
options.forEach((cv, i) => console.log(i, cv));
return new Promise(r => {
// reading from stdin will only give string
rl.question('Choose one of the above: ', i => r(options[parseInt(i)]));
});
}
function askPart(fillStr) {
const [, firstPart, content] = fillStr.split('%');
const nameKey = firstPart.split(':').find(o => o.startsWith('name='));
const defaultYes = firstPart.split(':').includes('default=yes');
const name = nameKey.slice(5) || 'Unnamed optional section';
return new Promise(r => {
// reading from stdin will only give string
rl.question(
name + ' (y/n) ',
a => a === 'y' || (defaultYes && a !== 'n') ? r(content) : r('')
);
});
}
function getDate() {
// TextExpander will replace this placeholder with clipboard contents
// but the contents will not be escaped. Backticks are much less likely
// to happen in clipboard contents compared to single and double quotes.
// If it contains a backtick, this code will break and output will give
// a js error. There doesn't seem a way to make it perfect.
// Also note that this means that code from clipboard can get executed
// as javascript if it escapes the backtick.
let dateInput = `%clipboard`;
if (dateInput.match(/^\d+$/)) dateInput = parseInt(dateInput);
// unix timestamps could be in seconds or milliseconds. This condition
// checks if it is too small, it is likely to be in seconds
// This means it only work from 1971 to 2968. This is a trade-off to
// support both seconds and milliseconds.
if (dateInput < new Date('1971').getTime())
dateInput = dateInput * 1000;
let date = new Date(dateInput);
// if clipboard does not make a date, then take current date
if (isNaN(date.getTime())) date = new Date();
return date;
}
// Reference list all date time format options: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
// javascript does not have a placeholder based date/time formatting
// what it has are locale specific formats and here we have a choice
// of some commonly used formats. There could be more here.
// These locales were might not work in old OS with old JS.
function parseDateFormat(date, format, includeYear) {
let zone = 'en-IN';
let dateOptions = {};
switch (format) {
case 'YYYY-MM-DD':
// https://stackoverflow.com/a/58633686/1615721
zone = 'sv-SE';
dateOptions = {year: 'numeric', month: '2-digit', day: '2-digit', };
break;
case 'MMM DD, YYYY':
zone = 'en-US';
dateOptions = {year: 'numeric', month: 'short', day: 'numeric', };
break;
case 'DD/MM/YY':
zone = 'en-IN';
dateOptions = {year: '2-digit', month: '2-digit', day: '2-digit', };
break;
case 'MM/DD/YY':
zone = 'en-US';
dateOptions = {year: '2-digit', month: '2-digit', day: '2-digit', };
break;
}
if (!includeYear) dateOptions.year = undefined;
return {zone, dateOptions};
}
function parseTimeFormat(date, format, hour24) {
let timeOptions = {};
switch (format) {
case 'HH MM':
timeOptions = {hour: '2-digit', minute: '2-digit'};
break;
case 'HH MM SS':
timeOptions = {timeOptions: 'medium'};
timeOptions = {hour: '2-digit', minute: '2-digit', second: '2-digit'};
break;
case 'HH MM ZONE':
timeOptions = {hour: '2-digit', minute: '2-digit', timeZoneName: 'short'};
break;
case 'HH MM SS ZONE':
timeOptions = {hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short'};
break;
}
timeOptions.hour12 = !hour24;
return timeOptions;
}
// Neither window, nor global is accessible inside TextExpander
// window.TextExpander or global.TextExpander will not work
if (typeof TextExpander !== 'undefined') {
printDate({dateFormat, timeFormat, hour24, includeYear});
} else {
// nodejs
(async () => {
const {stdin: input, stdout: output} = process;
if (!rl) rl = require('readline').createInterface({input, output});
dateFormat = await ask(dateFormat);
timeFormat = await ask(timeFormat);
hour24 = await ask(hour24);
includeYear = await ask(includeYear);
console.log(printDate({dateFormat, timeFormat, hour24, includeYear}));
// not closing the interface will not exit the program
rl.close();
})();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment