chore: move frontend files
This commit is contained in:
95
frontend/src/helpers/time/calculateDayInterval.test.ts
Normal file
95
frontend/src/helpers/time/calculateDayInterval.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import {test, expect} from 'vitest'
|
||||
|
||||
import {calculateDayInterval} from './calculateDayInterval'
|
||||
|
||||
const days = {
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
wednesday: 3,
|
||||
thursday: 4,
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
sunday: 0,
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in days) {
|
||||
test(`today on a ${n}`, () => {
|
||||
expect(calculateDayInterval('today', days[n])).toBe(0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const n in days) {
|
||||
test(`tomorrow on a ${n}`, () => {
|
||||
expect(calculateDayInterval('tomorrow', days[n])).toBe(1)
|
||||
})
|
||||
}
|
||||
|
||||
const nextMonday = {
|
||||
monday: 0,
|
||||
tuesday: 6,
|
||||
wednesday: 5,
|
||||
thursday: 4,
|
||||
friday: 3,
|
||||
saturday: 2,
|
||||
sunday: 1,
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in nextMonday) {
|
||||
test(`next monday on a ${n}`, () => {
|
||||
expect(calculateDayInterval('nextMonday', days[n])).toBe(nextMonday[n])
|
||||
})
|
||||
}
|
||||
|
||||
const thisWeekend = {
|
||||
monday: 5,
|
||||
tuesday: 4,
|
||||
wednesday: 3,
|
||||
thursday: 2,
|
||||
friday: 1,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in thisWeekend) {
|
||||
test(`this weekend on a ${n}`, () => {
|
||||
expect(calculateDayInterval('thisWeekend', days[n])).toBe(thisWeekend[n])
|
||||
})
|
||||
}
|
||||
|
||||
const laterThisWeek = {
|
||||
monday: 2,
|
||||
tuesday: 2,
|
||||
wednesday: 2,
|
||||
thursday: 2,
|
||||
friday: 0,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterThisWeek) {
|
||||
test(`later this week on a ${n}`, () => {
|
||||
expect(calculateDayInterval('laterThisWeek', days[n])).toBe(laterThisWeek[n])
|
||||
})
|
||||
}
|
||||
|
||||
const laterNextWeek = {
|
||||
monday: 7 + 2,
|
||||
tuesday: 7 + 2,
|
||||
wednesday: 7 + 2,
|
||||
thursday: 7 + 2,
|
||||
friday: 7 + 0,
|
||||
saturday: 7 + 0,
|
||||
sunday: 7 + 0,
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterNextWeek) {
|
||||
test(`later next week on a ${n} (this week)`, () => {
|
||||
expect(calculateDayInterval('laterNextWeek', days[n])).toBe(laterNextWeek[n])
|
||||
})
|
||||
}
|
||||
|
||||
for (const n in days) {
|
||||
test(`next week on a ${n}`, () => {
|
||||
expect(calculateDayInterval('nextWeek', days[n])).toBe(7)
|
||||
})
|
||||
}
|
28
frontend/src/helpers/time/calculateDayInterval.ts
Normal file
28
frontend/src/helpers/time/calculateDayInterval.ts
Normal file
@ -0,0 +1,28 @@
|
||||
type Day<T extends number = number> = T
|
||||
|
||||
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())): Day {
|
||||
switch (dateString) {
|
||||
case 'today':
|
||||
return 0
|
||||
case 'tomorrow':
|
||||
return 1
|
||||
case 'nextMonday':
|
||||
// Monday is 1, so we calculate the distance to the next 1
|
||||
return (currentDay + (8 - currentDay * 2)) % 7
|
||||
case 'thisWeekend':
|
||||
// Saturday is 6 so we calculate the distance to the next 6
|
||||
return (6 - currentDay) % 6
|
||||
case 'laterThisWeek':
|
||||
if (currentDay === 5 || currentDay === 6 || currentDay === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 2
|
||||
case 'laterNextWeek':
|
||||
return calculateDayInterval('laterThisWeek', currentDay) + 7
|
||||
case 'nextWeek':
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
24
frontend/src/helpers/time/calculateNearestHours.ts
Normal file
24
frontend/src/helpers/time/calculateNearestHours.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export function calculateNearestHours(currentDate: Date = new Date()): number {
|
||||
if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) {
|
||||
return 9
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 12) {
|
||||
return 12
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 15) {
|
||||
return 15
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 18) {
|
||||
return 18
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 21) {
|
||||
return 21
|
||||
}
|
||||
|
||||
// Same case as in the first if, will never be called
|
||||
return 9
|
||||
}
|
92
frontend/src/helpers/time/calculateNearestTime.test.ts
Normal file
92
frontend/src/helpers/time/calculateNearestTime.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import {test, expect} from 'vitest'
|
||||
|
||||
import {calculateNearestHours} from './calculateNearestHours'
|
||||
|
||||
test('5:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(5)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('7:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(7)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('7:41', () => {
|
||||
const date = new Date()
|
||||
date.setHours(7)
|
||||
date.setMinutes(41)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('9:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(9)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('10:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(10)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(12)
|
||||
})
|
||||
|
||||
test('12:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(12)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(12)
|
||||
})
|
||||
|
||||
test('13:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(13)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(15)
|
||||
})
|
||||
|
||||
test('15:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(15)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(15)
|
||||
})
|
||||
|
||||
test('16:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(16)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(18)
|
||||
})
|
||||
|
||||
test('18:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(18)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(18)
|
||||
})
|
||||
|
||||
test('19:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(19)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(21)
|
||||
})
|
||||
|
||||
test('22:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(22)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('22:40', () => {
|
||||
const date = new Date()
|
||||
date.setHours(22)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
15
frontend/src/helpers/time/createDateFromString.test.ts
Normal file
15
frontend/src/helpers/time/createDateFromString.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {test, expect} from 'vitest'
|
||||
|
||||
import {createDateFromString} from './createDateFromString'
|
||||
|
||||
test('YYYY-MM-DD HH:MM', () => {
|
||||
const dateString = '2021-02-06 12:00'
|
||||
const date = createDateFromString(dateString)
|
||||
expect(date).toBeInstanceOf(Date)
|
||||
expect(date.getDate()).toBe(6)
|
||||
expect(date.getMonth()).toBe(1)
|
||||
expect(date.getFullYear()).toBe(2021)
|
||||
expect(date.getHours()).toBe(12)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
expect(date.getSeconds()).toBe(0)
|
||||
})
|
19
frontend/src/helpers/time/createDateFromString.ts
Normal file
19
frontend/src/helpers/time/createDateFromString.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Returns a new date from any format in a way that all browsers, especially safari, can understand.
|
||||
*
|
||||
* @see https://kolaente.dev/vikunja/frontend/issues/207
|
||||
*
|
||||
* @param dateString
|
||||
* @returns {Date}
|
||||
*/
|
||||
export function createDateFromString(dateString: string | Date) {
|
||||
if (dateString instanceof Date) {
|
||||
return dateString
|
||||
}
|
||||
|
||||
if (dateString.includes('-')) {
|
||||
dateString = dateString.replace(/-/g, '/')
|
||||
}
|
||||
|
||||
return new Date(dateString)
|
||||
}
|
68
frontend/src/helpers/time/formatDate.ts
Normal file
68
frontend/src/helpers/time/formatDate.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {format, formatDistanceToNow} from 'date-fns'
|
||||
|
||||
// FIXME: support all locales and load dynamically
|
||||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||
|
||||
import {i18n} from '@/i18n'
|
||||
import {createSharedComposable, type MaybeRef} from '@vueuse/core'
|
||||
import {computed, unref} from 'vue'
|
||||
|
||||
const locales = {en: enGB, de, ch: de, fr, ru}
|
||||
|
||||
export function dateIsValid(date: Date | null) {
|
||||
if (date === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return date instanceof Date && !isNaN(date)
|
||||
}
|
||||
|
||||
export const formatDate = (date, f, locale = i18n.global.t('date.locale')) => {
|
||||
if (!dateIsValid(date)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
date = createDateFromString(date)
|
||||
|
||||
return date ? format(date, f, {locale: locales[locale]}) : ''
|
||||
}
|
||||
|
||||
export function formatDateLong(date) {
|
||||
return formatDate(date, 'PPPPpppp')
|
||||
}
|
||||
|
||||
export function formatDateShort(date) {
|
||||
return formatDate(date, 'PPpp')
|
||||
}
|
||||
|
||||
export const formatDateSince = (date) => {
|
||||
if (!dateIsValid(date)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
date = createDateFromString(date)
|
||||
|
||||
return formatDistanceToNow(date, {
|
||||
locale: locales[i18n.global.t('date.locale')],
|
||||
addSuffix: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function formatISO(date) {
|
||||
return date ? new Date(date).toISOString() : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Because `Intl.DateTimeFormat` is expensive to instatiate we try to reuse it as often as possible,
|
||||
* by creating a shared composable.
|
||||
*/
|
||||
export const useDateTimeFormatter = createSharedComposable((options?: MaybeRef<Intl.DateTimeFormatOptions>) => {
|
||||
return computed(() => new Intl.DateTimeFormat(i18n.global.locale.value, unref(options)))
|
||||
})
|
||||
|
||||
export function useWeekDayFromDate() {
|
||||
const dateTimeFormatter = useDateTimeFormatter({weekday: 'short'})
|
||||
|
||||
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
|
||||
}
|
5
frontend/src/helpers/time/getNextWeekDate.ts
Normal file
5
frontend/src/helpers/time/getNextWeekDate.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {MILLISECONDS_A_WEEK} from '@/constants/date'
|
||||
|
||||
export function getNextWeekDate(): Date {
|
||||
return new Date((new Date()).getTime() + MILLISECONDS_A_WEEK)
|
||||
}
|
16
frontend/src/helpers/time/isoToKebabDate.ts
Normal file
16
frontend/src/helpers/time/isoToKebabDate.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
// ✅ Format a date to YYYY-MM-DD (or any other format)
|
||||
function padTo2Digits(num: number) {
|
||||
return num.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
export function isoToKebabDate(isoDate: DateISO) {
|
||||
const date = new Date(isoDate)
|
||||
return [
|
||||
date.getFullYear(),
|
||||
padTo2Digits(date.getMonth() + 1), // January is 0, but we want it to be 1
|
||||
padTo2Digits(date.getDate()),
|
||||
].join('-') as DateKebab
|
||||
}
|
5
frontend/src/helpers/time/parseBooleanProp.ts
Normal file
5
frontend/src/helpers/time/parseBooleanProp.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function parseBooleanProp(booleanProp: string | undefined) {
|
||||
return (booleanProp === 'false' || booleanProp === '0')
|
||||
? false
|
||||
: Boolean(booleanProp)
|
||||
}
|
357
frontend/src/helpers/time/parseDate.ts
Normal file
357
frontend/src/helpers/time/parseDate.ts
Normal file
@ -0,0 +1,357 @@
|
||||
import {calculateDayInterval} from './calculateDayInterval'
|
||||
import {calculateNearestHours} from './calculateNearestHours'
|
||||
import {replaceAll} from '../replaceAll'
|
||||
|
||||
interface dateParseResult {
|
||||
newText: string,
|
||||
date: Date | null,
|
||||
}
|
||||
|
||||
interface dateFoundResult {
|
||||
foundText: string | null,
|
||||
date: Date | null,
|
||||
}
|
||||
|
||||
const monthsRegexGroup = '(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
|
||||
|
||||
function matchesDateExpr(text: string, dateExpr: string): boolean {
|
||||
return text.match(new RegExp('(^| )' + dateExpr, 'gi')) !== null
|
||||
}
|
||||
|
||||
export const parseDate = (text: string, now: Date = new Date()): dateParseResult => {
|
||||
if (matchesDateExpr(text, 'today')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
|
||||
}
|
||||
if (matchesDateExpr(text, 'tomorrow')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
|
||||
}
|
||||
if (matchesDateExpr(text, 'next monday')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday')
|
||||
}
|
||||
if (matchesDateExpr(text, 'this weekend')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend')
|
||||
}
|
||||
if (matchesDateExpr(text, 'later this week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week')
|
||||
}
|
||||
if (matchesDateExpr(text, 'later next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week')
|
||||
}
|
||||
if (matchesDateExpr(text, 'next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
|
||||
}
|
||||
if (matchesDateExpr(text, 'next month')) {
|
||||
const date: Date = new Date()
|
||||
date.setDate(1)
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
date.setHours(calculateNearestHours(date))
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
|
||||
return addTimeToDate(text, date, 'next month')
|
||||
}
|
||||
if (matchesDateExpr(text, 'end of month')) {
|
||||
const curDate: Date = new Date()
|
||||
const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
date.setHours(calculateNearestHours(date))
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
|
||||
return addTimeToDate(text, date, 'end of month')
|
||||
}
|
||||
|
||||
let parsed = getDateFromWeekday(text)
|
||||
if (parsed.date !== null) {
|
||||
return addTimeToDate(text, parsed.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDayFromText(text)
|
||||
if (parsed.date !== null) {
|
||||
const month = getMonthFromText(text, parsed.date)
|
||||
return addTimeToDate(month.newText, month.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDateFromTextIn(text, now)
|
||||
if (parsed.date !== null) {
|
||||
return addTimeToDate(text, parsed.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDateFromText(text)
|
||||
|
||||
if (parsed.date === null) {
|
||||
return {
|
||||
newText: replaceAll(text, parsed.foundText, ''),
|
||||
date: parsed.date,
|
||||
}
|
||||
}
|
||||
|
||||
return addTimeToDate(text, parsed.date, parsed.foundText)
|
||||
}
|
||||
|
||||
const addTimeToDate = (text: string, date: Date, previousMatch: string | null): dateParseResult => {
|
||||
previousMatch = previousMatch?.trim() || ''
|
||||
text = replaceAll(text, previousMatch, '')
|
||||
if (previousMatch === null) {
|
||||
return {
|
||||
newText: text,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const timeRegex = ' (at|@) ([0-9][0-9]?(:[0-9][0-9]?)?( ?(a|p)m)?)'
|
||||
const matcher = new RegExp(timeRegex, 'ig')
|
||||
const results = matcher.exec(text)
|
||||
|
||||
if (results !== null) {
|
||||
const time = results[2]
|
||||
const parts = time.split(':')
|
||||
let hours = parseInt(parts[0])
|
||||
let minutes = 0
|
||||
if (time.endsWith('pm')) {
|
||||
hours += 12
|
||||
}
|
||||
if (parts.length > 1) {
|
||||
minutes = parseInt(parts[1])
|
||||
}
|
||||
|
||||
date.setHours(hours)
|
||||
date.setMinutes(minutes)
|
||||
date.setSeconds(0)
|
||||
}
|
||||
|
||||
const replace = results !== null ? results[0] : previousMatch
|
||||
return {
|
||||
newText: replaceAll(text, replace, '').trim(),
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDateFromText = (text: string, now: Date = new Date()) => {
|
||||
const fullDateRegex = /(^| )([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig
|
||||
|
||||
// 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021
|
||||
let results: string[] | null = fullDateRegex.exec(text)
|
||||
let result: string | null = results === null ? null : results[0]
|
||||
let foundText: string | null = result
|
||||
let containsYear = true
|
||||
if (result === null) {
|
||||
// 2. Try parsing the date as something like "jan 21" or "21 jan"
|
||||
const monthRegex = new RegExp(`(^| )(${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig')
|
||||
results = monthRegex.exec(text)
|
||||
result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim()
|
||||
foundText = results === null ? '' : results[0].trim()
|
||||
containsYear = false
|
||||
|
||||
if (result === null) {
|
||||
// 3. Try parsing the date as "27/01" or "01/27"
|
||||
const monthNumericRegex = /(^| )([0-9][0-9]?\/[0-9][0-9]?)/ig
|
||||
results = monthNumericRegex.exec(text)
|
||||
|
||||
// Put the year before or after the date, depending on what works
|
||||
result = results === null ? null : `${now.getFullYear()}/${results[0]}`
|
||||
if (result === null) {
|
||||
return {
|
||||
foundText,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
foundText = results === null ? '' : results[0]
|
||||
if (result === null || isNaN(new Date(result).getTime())) {
|
||||
result = results === null ? null : `${results[0]}/${now.getFullYear()}`
|
||||
}
|
||||
if (result === null || (isNaN(new Date(result).getTime()) && foundText !== '')) {
|
||||
const parts = foundText.split('/')
|
||||
result = `${parts[1]}/${parts[0]}/${now.getFullYear()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
return {
|
||||
foundText,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(result)
|
||||
if (isNaN(date.getTime())) {
|
||||
return {
|
||||
foundText,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsYear && date < now) {
|
||||
date.setFullYear(date.getFullYear() + 1)
|
||||
}
|
||||
|
||||
return {
|
||||
foundText,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDateFromTextIn = (text: string, now: Date = new Date()) => {
|
||||
const regex = /(in [0-9]+ (hours?|days?|weeks?|months?))/ig
|
||||
const results = regex.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: '',
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const foundText: string = results[0]
|
||||
const date = new Date(now)
|
||||
const parts = foundText.split(' ')
|
||||
switch (parts[2]) {
|
||||
case 'hours':
|
||||
case 'hour':
|
||||
date.setHours(date.getHours() + parseInt(parts[1]))
|
||||
break
|
||||
case 'days':
|
||||
case 'day':
|
||||
date.setDate(date.getDate() + parseInt(parts[1]))
|
||||
break
|
||||
case 'weeks':
|
||||
case 'week':
|
||||
date.setDate(date.getDate() + parseInt(parts[1]) * 7)
|
||||
break
|
||||
case 'months':
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() + parseInt(parts[1]))
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
foundText,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDateFromWeekday = (text: string): dateFoundResult => {
|
||||
const matcher = /(^| )(next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
|
||||
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const date: Date = new Date()
|
||||
const currentDay: number = date.getDay()
|
||||
let day = 0
|
||||
|
||||
switch (results[3]) {
|
||||
case 'mon':
|
||||
case 'monday':
|
||||
day = 1
|
||||
break
|
||||
case 'tue':
|
||||
case 'tuesday':
|
||||
day = 2
|
||||
break
|
||||
case 'wed':
|
||||
case 'wednesday':
|
||||
day = 3
|
||||
break
|
||||
case 'thu':
|
||||
case 'thursday':
|
||||
day = 4
|
||||
break
|
||||
case 'fri':
|
||||
case 'friday':
|
||||
day = 5
|
||||
break
|
||||
case 'sat':
|
||||
case 'saturday':
|
||||
day = 6
|
||||
break
|
||||
case 'sun':
|
||||
case 'sunday':
|
||||
day = 0
|
||||
break
|
||||
default:
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const distance: number = (day + 7 - currentDay) % 7
|
||||
date.setDate(date.getDate() + distance)
|
||||
|
||||
// This a space at the end of the found text to not break parsing suffix strings like "at 14:00" in cases where the
|
||||
// matched string comes with a space at the end (last part of the regex).
|
||||
let foundText = results[0]
|
||||
if (foundText.endsWith(' ')) {
|
||||
foundText = foundText.slice(0, foundText.length - 1)
|
||||
}
|
||||
|
||||
return {
|
||||
foundText: foundText,
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDayFromText = (text: string) => {
|
||||
const matcher = /(^| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig
|
||||
const results = matcher.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const date = new Date(now)
|
||||
const day = parseInt(results[0])
|
||||
date.setDate(day)
|
||||
|
||||
// If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days,
|
||||
// setting the day to 31 will "overflow" the date to the next month, but the first.
|
||||
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
|
||||
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
|
||||
while (date < now) {
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
}
|
||||
|
||||
if (date.getDate() !== day) {
|
||||
date.setDate(day)
|
||||
}
|
||||
|
||||
return {
|
||||
foundText: results[0],
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
const getMonthFromText = (text: string, date: Date) => {
|
||||
const matcher = new RegExp(monthsRegexGroup, 'ig')
|
||||
const results = matcher.exec(text)
|
||||
|
||||
if (results === null) {
|
||||
return {
|
||||
newText: text,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
const fullDate = new Date(`${results[0]} 1 ${(new Date()).getFullYear()}`)
|
||||
date.setMonth(fullDate.getMonth())
|
||||
return {
|
||||
newText: replaceAll(text, results[0], ''),
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDateFromInterval = (interval: number): Date => {
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate), 0, 0)
|
||||
|
||||
return newDate
|
||||
}
|
15
frontend/src/helpers/time/parseDateOrString.ts
Normal file
15
frontend/src/helpers/time/parseDateOrString.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export function parseDateOrString(rawValue: string | undefined | null, fallback: unknown): (unknown | string | Date) {
|
||||
if (rawValue === null || typeof rawValue === 'undefined') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (rawValue.toLowerCase().includes('now') || rawValue.toLowerCase().includes('||')) {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
const d = new Date(rawValue)
|
||||
|
||||
return !isNaN(+d)
|
||||
? d
|
||||
: rawValue
|
||||
}
|
30
frontend/src/helpers/time/parseDateProp.ts
Normal file
30
frontend/src/helpers/time/parseDateProp.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
export function parseDateProp(kebabDate: DateKebab | undefined): string | undefined {
|
||||
try {
|
||||
|
||||
if (!kebabDate) {
|
||||
throw new Error('No value')
|
||||
}
|
||||
const dateValues = kebabDate.split('-')
|
||||
const [, monthString, dateString] = dateValues
|
||||
const [year, month, date] = dateValues.map(val => Number(val))
|
||||
const dateValuesAreValid = (
|
||||
!Number.isNaN(year) &&
|
||||
monthString.length >= 1 && monthString.length <= 2 &&
|
||||
!Number.isNaN(month) &&
|
||||
month >= 1 && month <= 12 &&
|
||||
dateString.length >= 1 && dateString.length <= 31 &&
|
||||
!Number.isNaN(date) &&
|
||||
date >= 1 && date <= 31
|
||||
)
|
||||
if (!dateValuesAreValid) {
|
||||
throw new Error('Invalid date values')
|
||||
}
|
||||
return new Date(year, month - 1, date).toISOString() as DateISO
|
||||
} catch(e) {
|
||||
// ignore nonsense route queries
|
||||
return
|
||||
}
|
||||
}
|
7
frontend/src/helpers/time/parseKebabDate.ts
Normal file
7
frontend/src/helpers/time/parseKebabDate.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {parse} from 'date-fns'
|
||||
import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
export function parseKebabDate(date: DateKebab): Date {
|
||||
return parse(date, DATEFNS_DATE_FORMAT_KEBAB, new Date())
|
||||
}
|
44
frontend/src/helpers/time/period.ts
Normal file
44
frontend/src/helpers/time/period.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
SECONDS_A_DAY,
|
||||
SECONDS_A_HOUR,
|
||||
SECONDS_A_MINUTE,
|
||||
SECONDS_A_WEEK,
|
||||
} from '@/constants/date'
|
||||
|
||||
export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
|
||||
|
||||
/**
|
||||
* Convert time period given as seconds to days, hour, minutes, seconds
|
||||
*/
|
||||
export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } {
|
||||
if (seconds % SECONDS_A_DAY === 0) {
|
||||
if (seconds % SECONDS_A_WEEK === 0) {
|
||||
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
|
||||
} else {
|
||||
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
unit: 'hours',
|
||||
amount: seconds / SECONDS_A_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time period of days, hour, minutes, seconds to duration in seconds
|
||||
*/
|
||||
export function periodToSeconds(period: number, unit: PeriodUnit): number {
|
||||
switch (unit) {
|
||||
case 'minutes':
|
||||
return period * SECONDS_A_MINUTE
|
||||
case 'hours':
|
||||
return period * SECONDS_A_HOUR
|
||||
case 'days':
|
||||
return period * SECONDS_A_DAY
|
||||
case 'weeks':
|
||||
return period * SECONDS_A_WEEK
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
Reference in New Issue
Block a user