mirror of https://github.com/buster-so/buster.git
375 lines
10 KiB
TypeScript
375 lines
10 KiB
TypeScript
import dayjs from 'dayjs';
|
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
|
import utc from 'dayjs/plugin/utc';
|
|
import isNumber from 'lodash/isNumber';
|
|
import isString from 'lodash/isString';
|
|
import isDate from 'lodash/isDate';
|
|
import isNaN from 'lodash/isNaN';
|
|
import { isNumeric } from './numbers';
|
|
import { getBrowserLanguage } from './language';
|
|
import { SupportedLanguages } from '../config/languages';
|
|
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
|
|
import {
|
|
ColumnLabelFormat,
|
|
DEFAULT_DATE_FORMAT_MONTH_OF_YEAR,
|
|
DEFAULT_DATE_FORMAT_QUARTER,
|
|
DEFAULT_DAY_OF_WEEK_FORMAT
|
|
} from '@/api/asset_interfaces/metric';
|
|
|
|
dayjs.extend(relativeTime);
|
|
dayjs.extend(customParseFormat);
|
|
dayjs.extend(localizedFormat);
|
|
dayjs.extend(utc);
|
|
dayjs.extend(quarterOfYear);
|
|
|
|
export const getNow = () => dayjs();
|
|
export const createDayjsDate = (date: string | Date) => dayjs(date);
|
|
|
|
const KEY_DATE_FORMATS = [
|
|
'date',
|
|
'year',
|
|
'month',
|
|
'day',
|
|
'timestamp',
|
|
'time_stamp',
|
|
'created_at',
|
|
'_month'
|
|
];
|
|
const KEY_DATE_INCLUDE_FORMATS = ['_month', '_year'];
|
|
const VALID_DATE_FORMATS = [
|
|
'YYYY-MM-DD',
|
|
'YYYY-MM',
|
|
'YYYY-MM-DDTHH:mm:ss',
|
|
'YYYY-MM-DD HH:mm:ss',
|
|
'YYYY-MM-DDTHH:mm:ss.sssZ',
|
|
'YYYY-MM-DDTHH:mm:ssZ',
|
|
'YYYY-MM-DD HH:mm:ssZ',
|
|
'YYYY-MM-DD HH:mm:ss.sssZ',
|
|
'YYYY-MM-DD HH:mm:ss.sss',
|
|
'YYYY-MM-DDTHH:mm:ss.sss',
|
|
'YYYY-MM-DDTHH:mm:ssZ',
|
|
'YYYY-MM-DDTHH:mm:ss',
|
|
'YYYY-MM-DD HH:mm:ss',
|
|
'YYYY-MM-DD HH:mm',
|
|
'YYYY-MM-DDTHH:mm',
|
|
'YYYY-MM-DD HH',
|
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
|
'YYYY-MM-DDTHH:mm:ss.SSS',
|
|
'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
|
'YYYY-MM-DD HH:mm:ss.SSSZ',
|
|
'YYYY-MM-DDTHH:mm:ss.SSS',
|
|
'MM DD YYYY',
|
|
'M D YYYY',
|
|
'MMM D, YYYY',
|
|
'MMMM D, YYYY'
|
|
];
|
|
|
|
export const numberDateFallback = (
|
|
date: string | number | Date,
|
|
dateKey?: string,
|
|
convertNumberTo?: ColumnLabelFormat['convertNumberTo']
|
|
) => {
|
|
if (convertNumberTo === 'day_of_week' && isNumber(date) && valueIsValidDayOfWeek(date, dateKey)) {
|
|
return dayjs().day(Number(date)).startOf('day');
|
|
} else if (valueIsValidMonth(date, dateKey) || convertNumberTo === 'month_of_year') {
|
|
return dayjs()
|
|
.month(Number(date) - 1)
|
|
.startOf('month')
|
|
.startOf('day');
|
|
}
|
|
|
|
const numericDate = Number(date);
|
|
if (!isNaN(numericDate)) {
|
|
// Check for 13 digit timestamp (milliseconds)
|
|
if (String(numericDate).length === 13) {
|
|
return dayjs(numericDate);
|
|
}
|
|
// Check for 10 digit timestamp (seconds)
|
|
if (String(numericDate).length === 10) {
|
|
return dayjs.unix(numericDate);
|
|
}
|
|
}
|
|
|
|
return String(date);
|
|
};
|
|
|
|
const extractDateForFormatting = (
|
|
date: string | number | Date,
|
|
dateKey?: string,
|
|
convertNumberTo?: ColumnLabelFormat['convertNumberTo']
|
|
) => {
|
|
if (isString(date)) return date;
|
|
if (isNumber(date)) return numberDateFallback(date, dateKey, convertNumberTo);
|
|
if (isDate(date)) return new Date(date);
|
|
return String(date);
|
|
};
|
|
|
|
export const formatDate = ({
|
|
date,
|
|
format,
|
|
isUTC = false,
|
|
dateKey,
|
|
ignoreValidation = true,
|
|
convertNumberTo = null
|
|
}: {
|
|
date: string | number | Date;
|
|
format: string;
|
|
isUTC?: boolean;
|
|
dateKey?: string;
|
|
ignoreValidation?: boolean;
|
|
convertNumberTo?: ColumnLabelFormat['convertNumberTo'];
|
|
}): string => {
|
|
try {
|
|
const myDate = extractDateForFormatting(date, dateKey, convertNumberTo);
|
|
|
|
const valid = ignoreValidation
|
|
? true
|
|
: !!(myDate as dayjs.Dayjs)?.toDate ||
|
|
isDateValid({
|
|
date: myDate as string
|
|
});
|
|
|
|
if (convertNumberTo) {
|
|
if (convertNumberTo === 'day_of_week') {
|
|
const validDayFormats = ['dddd', 'ddd', 'dd', 'd'];
|
|
if (!validDayFormats.includes(format)) {
|
|
format = DEFAULT_DAY_OF_WEEK_FORMAT;
|
|
}
|
|
}
|
|
|
|
if (convertNumberTo === 'month_of_year') {
|
|
const validMonthFormats = ['MMMM', 'MMM', 'MM', 'M'];
|
|
if (!validMonthFormats.includes(format)) {
|
|
format = DEFAULT_DATE_FORMAT_MONTH_OF_YEAR;
|
|
}
|
|
}
|
|
|
|
if (convertNumberTo === 'quarter') {
|
|
const validQuarterFormats = ['YYYY [Q]Q', 'Q'];
|
|
if (!validQuarterFormats.includes(format)) {
|
|
format = DEFAULT_DATE_FORMAT_QUARTER;
|
|
}
|
|
}
|
|
}
|
|
|
|
const theDate = valid
|
|
? isUTC
|
|
? dayjs.utc(myDate).format(format)
|
|
: dayjs(myDate).format(format)
|
|
: String(date);
|
|
|
|
if (theDate === 'Invalid Date') {
|
|
return String(date);
|
|
}
|
|
return theDate;
|
|
} catch {
|
|
return String(date);
|
|
}
|
|
};
|
|
|
|
export const isDateSame = ({
|
|
date,
|
|
compareDate,
|
|
interval = 'day'
|
|
}: {
|
|
date: string | number | dayjs.Dayjs | Date;
|
|
compareDate: string | number | dayjs.Dayjs | Date;
|
|
interval?: dayjs.OpUnitType;
|
|
}) => {
|
|
return dayjs(date).isSame(compareDate, interval);
|
|
};
|
|
|
|
export const isDateBefore = ({
|
|
date,
|
|
compareDate,
|
|
interval = 'day'
|
|
}: {
|
|
date: string | number | dayjs.Dayjs | Date;
|
|
compareDate: string | number | dayjs.Dayjs | Date;
|
|
interval?: dayjs.OpUnitType;
|
|
}) => {
|
|
return dayjs(date).isBefore(compareDate, interval);
|
|
};
|
|
|
|
export const isDateAfter = ({
|
|
date,
|
|
compareDate,
|
|
interval = 'day'
|
|
}: {
|
|
date: string | number | dayjs.Dayjs | Date;
|
|
compareDate: string | number | dayjs.Dayjs | Date;
|
|
interval?: dayjs.OpUnitType;
|
|
}) => {
|
|
return dayjs(date).isAfter(compareDate, interval);
|
|
};
|
|
|
|
export const timeFromNow = (date: string | Date, relative = false) => {
|
|
return dayjs(new Date(date)).fromNow(relative);
|
|
};
|
|
|
|
export const valueIsValidMonth = (value: string | number | Date | undefined, key?: string) => {
|
|
if (value === undefined || value === null) return false;
|
|
const month = Number(value);
|
|
return (month > 0 && month <= 12) || isKeyLikelyDate(key);
|
|
};
|
|
|
|
const valueIsValidDayOfWeek = (value: string | number | Date | undefined, key?: string) => {
|
|
if (value === undefined || value === null) return false;
|
|
const day = Number(value);
|
|
return (day >= 0 && day <= 7) || isKeyLikelyDate(key);
|
|
};
|
|
|
|
const isKeyLikelyDate = (dateKey?: string) => {
|
|
return (
|
|
KEY_DATE_FORMATS.some((v) => v === dateKey) ||
|
|
KEY_DATE_INCLUDE_FORMATS.some((v) => dateKey?.toLowerCase().endsWith(v))
|
|
);
|
|
};
|
|
|
|
export const isDateValid = ({
|
|
date,
|
|
dateKey,
|
|
useNumbersAsDateKey = true
|
|
}: {
|
|
date: string | number | Date | undefined;
|
|
dateKey?: string;
|
|
useNumbersAsDateKey?: boolean;
|
|
}) => {
|
|
if (isDate(date)) return true;
|
|
if (useNumbersAsDateKey && dateKey && isNumeric(date as string) && isKeyLikelyDate(dateKey)) {
|
|
return valueIsValidMonth(date!, dateKey);
|
|
} else if (!date || isNumber(date)) return false;
|
|
const hyphenCount = (date.match(/-/g) || []).length;
|
|
const twoHyphens = hyphenCount === 2;
|
|
|
|
let filter = false;
|
|
if (date.includes('T') && twoHyphens)
|
|
filter = true; //2023-10-17T01:33:45 , YYYY-MM-DD HH:mm:ss.
|
|
else if (date.includes('Z') && twoHyphens)
|
|
filter = true; //YYYY-MM-DDTHH:mm:ss.sssZ
|
|
else if (twoHyphens && date.length === 10)
|
|
filter = true; //2023-10-17
|
|
else if (date.length === 13 && isNaN(Number(date)))
|
|
filter = true; // 1634468020000
|
|
else if (date.length === 10 && isNaN(Number(date))) filter = true; // 1634468020
|
|
|
|
if (filter) return filter && dayjs(date).isValid();
|
|
|
|
return VALID_DATE_FORMATS.some((format) => dayjs(date, format, true).isValid());
|
|
};
|
|
|
|
export const keysWithDate = (
|
|
data: Record<string, string | number | null>[] = [],
|
|
params?: {
|
|
parseable?: boolean;
|
|
includeFormats?: boolean;
|
|
absoluteFormats?: boolean;
|
|
}
|
|
): string[] => {
|
|
const uniqueKeys = Object.keys(data[0]! || {});
|
|
const { parseable, includeFormats = true, absoluteFormats = true } = params || {};
|
|
|
|
if (parseable) {
|
|
return uniqueKeys.filter((key) => {
|
|
const value = data?.[0]?.[key];
|
|
return isDateValid({
|
|
date: String(value),
|
|
dateKey: key
|
|
});
|
|
});
|
|
}
|
|
|
|
return uniqueKeys
|
|
.filter((key) => {
|
|
const value = data?.[0]?.[key];
|
|
|
|
return (
|
|
(absoluteFormats && KEY_DATE_FORMATS.some((keyDate) => keyDate === key.toLowerCase())) ||
|
|
isDateValid({
|
|
date: String(value),
|
|
dateKey: key
|
|
}) ||
|
|
(includeFormats &&
|
|
KEY_DATE_INCLUDE_FORMATS.some((format) => key.toLowerCase().endsWith(format)))
|
|
);
|
|
})
|
|
.sort((v) => {
|
|
if (v.toLowerCase().includes('year')) return 1;
|
|
return -1;
|
|
});
|
|
};
|
|
|
|
export const formatTime = (date: string | Date, format = 'h:mm A', isUTC: boolean) => {
|
|
return isUTC ? dayjs.utc(date).format(format) : dayjs(date).format(format);
|
|
};
|
|
|
|
export const millisecondsFromUnixTimestamp = (timestamp = 0) => {
|
|
return (timestamp - dayjs().unix()) * 1000;
|
|
};
|
|
|
|
export enum DEFAULT_TIME_ENCODE_FORMATS {
|
|
MONTHS = 'month',
|
|
YEARS = 'year',
|
|
DAYS = 'day',
|
|
SECONDS = 'second',
|
|
HOURS = 'hour',
|
|
MONTHS_ONLY = 'month_only',
|
|
NO_FORMAT = 'no_format',
|
|
MILLISECONDS = 'millisecond',
|
|
WEEK = 'week',
|
|
MINUTES = 'minute'
|
|
}
|
|
|
|
let loadedLocales: string[] = [];
|
|
export const setNewDateLocale = async (locale: SupportedLanguages) => {
|
|
if (!locale) return;
|
|
let _locale: string = locale;
|
|
|
|
const loadAndSet = async (locale: string) => {
|
|
try {
|
|
await import(`dayjs/locale/${_locale}.js`);
|
|
loadedLocales = [...loadedLocales, _locale as SupportedLanguages];
|
|
} catch (error) {
|
|
//
|
|
}
|
|
};
|
|
|
|
try {
|
|
if (!loadedLocales.includes(_locale as SupportedLanguages)) {
|
|
if (_locale === SupportedLanguages.EN) _locale = getBrowserLanguage(true).toLocaleLowerCase();
|
|
loadAndSet(_locale);
|
|
}
|
|
} catch {
|
|
try {
|
|
_locale = locale;
|
|
loadAndSet(_locale);
|
|
} catch (error) {
|
|
console.error(`Error loading locale ${_locale}:`, error);
|
|
}
|
|
}
|
|
|
|
dayjs.locale(_locale);
|
|
};
|
|
|
|
export const getBestDateFormat = (minDate: dayjs.Dayjs, maxDate: dayjs.Dayjs) => {
|
|
const diffInDays = maxDate.diff(minDate, 'days');
|
|
const diffInMonths = maxDate.diff(minDate, 'months');
|
|
const diffInYears = maxDate.diff(minDate, 'years');
|
|
|
|
if (diffInDays <= 1) {
|
|
return 'h:mmA'; // 1/1 2:30 PM
|
|
} else if (diffInDays <= 31) {
|
|
return 'MMM D'; // Jan 1
|
|
} else if (diffInMonths <= 12) {
|
|
return 'MMM YYYY'; // Jan 2024
|
|
} else if (diffInYears <= 1) {
|
|
return 'MMM YYYY'; // Jan 2024
|
|
} else {
|
|
return 'YYYY'; // 2024
|
|
}
|
|
};
|