Last active
September 17, 2022 02:27
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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