todo-txt-cli
Version:
A CLI for todo.txt files - http://todotxt.org/
205 lines (188 loc) • 4.41 kB
JavaScript
/**
* @template T
* @param {T[]} arr
* @returns {T[]}
*/
export const uniq = (arr) => [...new Set(arr)]
/**
* @returns {Date}
*/
export const today = () => {
const now = new Date()
now.setHours(0, 0, 0, 0)
return now
}
export const isDate = (date) => !isNaN(new Date(date).getTime())
/**
* Checks if `value` is an integer
* @param {any} value The `value` to check
* @returns {boolean} Returns `true` if `value` is an integer, otherwise `false`
*/
export const isInteger = (value) => Number.isSafeInteger(Number(value))
/**
* @param {number} val
* @param {number} [max=2]
* @returns {string}
*/
const pad = (val, max = 2) => String(val).padStart(max, '0')
/**
* @param {Date|undefined} date
* @returns {string}
*/
export const formatDate = (date) =>
date
? [
pad(date.getFullYear(), 4),
pad(date.getMonth() + 1),
pad(date.getDate())
].join('-')
: ''
/**
* @param {Date} date
* @param {string|number} count
* @param {string} [unit]
* @returns {Date}
*/
export const changeDate = (date, count, unit) => {
const _date = new Date(date)
const num = Number(count)
switch (unit) {
// year
case 'y': {
const year = _date.getFullYear()
_date.setFullYear(year + num)
break
}
// month
case 'm': {
const month = _date.getMonth()
_date.setMonth(month + num)
break
}
// week
case 'w': {
const day = _date.getDate()
_date.setDate(day + 7 * num)
break
}
// day
default: {
const day = _date.getDate()
_date.setDate(day + num)
break
}
}
return _date
}
const RE_MOVE_DATE = /([-+]?\d+)(y|m|w|d)/
/**
* @param {Date} date
* @param {string} val
* @returns {Date|undefined}
*/
export const moveDate = (date, val) => {
if (!date || !val || typeof val !== 'string') {
return
}
const units = val.split(RE_MOVE_DATE)
units.splice(-1 * (units.length % 3))
if (!units.length) {
return
}
for (let i = 0; i < units.length; i += 3) {
const [count, unit] = units.slice(i + 1, i + 3)
date = changeDate(date, count, unit)
}
return date
}
const RE_ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
const RE_MM_DD_DATE = /^(\d{1,2})-(\d{1,2})$/
const RE_DD_MM_DATE = /^(\d{1,2})[\.\/](\d{1,2})\.?$/
/**
* @param {string} month
* @param {string} day
* @param {Date} now
* @returns {Date|undefined}
*/
const nonYearDate = (month, day, now) => {
const _month = Number(month)
const _day = Number(day)
if (!isInteger(_month) || _month > 12 || !isInteger(_day) || _day > 31) {
return
}
const year = now.getFullYear()
const date = new Date(year, _month - 1, _day)
if (date >= now) {
return date
}
return new Date(year + 1, _month - 1, _day)
}
/**
* @param {string} val
* @param {Date} [now]
* @returns {Date|undefined}
*/
export const isoOrRelativeDate = (val, now = today()) => {
if (!val || val === 'del') {
return
}
if (RE_ISO_DATE.test(val)) {
return new Date(val)
}
if (RE_MM_DD_DATE.test(val)) {
const [_, month, day] = RE_MM_DD_DATE.exec(val) || []
return nonYearDate(month, day, now)
}
if (RE_DD_MM_DATE.test(val)) {
const [_, day, month] = RE_DD_MM_DATE.exec(val) || []
return nonYearDate(month, day, now)
}
return moveDate(now, val)
}
/**
* escape regular expression; does not escape chars `|^$()`
* @param {*} string
* @returns
*/
export const escapeRegExp = (string) => string.replace(/[\\{}[\]+*?.]/g, '\\$&')
/**
* @param {string} prio
* @returns {RegExp|undefined}
*/
export const priorityToRegExp = (prio) => {
if (/^[A-Z.]$/i.test(prio)) {
return new RegExp(prio.toUpperCase())
}
if (/^[A-Z]-[A-Z]$/i.test(prio)) {
const [l, h] = prio.toUpperCase().split('-').sort()
return new RegExp(`[${l}-${h}]`)
}
}
export const digits = (length) => Math.ceil(Math.log10(length + 1))
const RE_RANGE = /^(\d+)-(\d+)$/
/**
* @param {string[]|[]|undefined} ids
* @returns {string[]|[]}
*/
export const range = (ids) => {
if (!ids?.length) {
return []
}
const aIds = []
for (const id of ids) {
const m = RE_RANGE.exec(id)
if (!m) {
aIds.push(id)
continue
}
const [low, high] = m.slice(1).map((n) => Number(n))
if (low > high) {
// ignore wrong order
continue
}
for (let i = low; i <= high; i += 1) {
aIds.push('' + i)
}
}
return aIds
}