Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"vitepress": "^1.6.3"
},
"dependencies": {
"dayjs": "^1.10.6",
"dayjs": "^1.11.13",
"idb": "^8.0.0",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/formatDateTime/__test__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dayjs from 'dayjs';

import formatDateTime, { DateTimeFormat } from '..';
import formatDateTime, { DateTimeFormat } from '../';

describe('formatDateTime', () => {
const testDate = new Date('2023-05-15T14:30:45');
Expand Down Expand Up @@ -92,6 +92,9 @@ describe('formatDateTime', () => {
// Year boundaries
const yearMin = new Date('0001-01-01T00:00:00');
expect(formatDateTime(yearMin, DateTimeFormat.YEAR)).toBe('0001');
expect(formatDateTime(yearMin, DateTimeFormat.DATE)).toBe('0001-01-01');
expect(formatDateTime(yearMin, DateTimeFormat.DATE_TIME_12)).toBe('0001-01-01 12:00 AM');
expect(formatDateTime(yearMin, DateTimeFormat.STANDARD)).toBe('0001-01-01 00:00:00');

const yearMax = new Date('9999-12-31T23:59:59');
expect(formatDateTime(yearMax, DateTimeFormat.YEAR)).toBe('9999');
Expand Down
110 changes: 107 additions & 3 deletions src/formatDateTime/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone'; // ES 2015
import utc from 'dayjs/plugin/utc'; // ES 2015

dayjs.extend(utc);
dayjs.extend(timezone);
// 默认时区:东八时区 (Asia/Shanghai)
const DEFAULT_TIMEZONE = 'Asia/Shanghai';
/**
* 将时间转换为默认时区
* @param {string | number | Date} time - 输入时间
* @return {dayjs.Dayjs} 带时区的Dayjs对象
*/
dayjs.tz.setDefault(DEFAULT_TIMEZONE);
/**
* @category 枚举
* 日期和时间格式模式的枚举
Expand Down Expand Up @@ -117,16 +130,107 @@ type FormatPattern = DateTimeFormat | string;
* formatDateTime(new Date(), "dddd, MMMM D, YYYY") // dayjs.Dayjs
* ```
*/
/**
* Helper function to convert date to default timezone
*/
const toDefaultTz = (date: DateTimeInput): dayjs.Dayjs => {
// 如果已经是 dayjs 对象且支持 tz,直接转换到默认时区
if ((date as any) && typeof (date as any).tz === 'function') {
return (date as any).tz(DEFAULT_TIMEZONE);
}
// 否则使用 dayjs.tz 将输入解析为默认时区的 dayjs 对象
// 对于无效日期,dayjs.tz 可能会抛出错误,此时降级到普通 dayjs
try {
return (dayjs as any).tz(date, DEFAULT_TIMEZONE);
} catch {
return dayjs(date);
}
};

/**
* Normalize input to native Date object
*/
const getNativeDate = (date: DateTimeInput): Date | null => {
if ((date as any) && typeof (date as any).toDate === 'function') {
return (date as any).toDate();
} else if (date instanceof Date) {
return date as Date;
} else {
const d = new Date(date as any);
return Number.isNaN(d.getTime()) ? null : d;
}
};

/**
* Check if a date is an edge-case year (year < 1900)
* Edge-case years may have parsing inconsistencies in dayjs/timezone
*/
const isEdgeCaseYear = (date: DateTimeInput): boolean => {
const nativeDate = getNativeDate(date);
if (!nativeDate) return false;
return nativeDate.getFullYear() < 1900;
};

/**
* Format native Date object using dayjs-like format string
* Leverages dayjs for standard tokens, only customizing where needed
*/
const formatNativeDate = (nativeDate: Date, format: string): string => {
const pad = (num: number, len = 2) => String(num).padStart(len, '0');
const d = nativeDate;
// 使用dayjs进行标准格式化
const dayjsInstance = dayjs(d);
// 只维护自定义tokens,主要用于处理时区格式,因为原生Date对象不支持dayjs的时区功能
const customTokens: Record<string, () => string> = {
// 时区 Z 替换成 +08:00 格式
Z: () => {
const offset = d.getTimezoneOffset();
const sign = offset <= 0 ? '+' : '-';
const h = Math.abs(Math.floor(offset / 60));
const m = Math.abs(offset % 60);
return `${sign}${pad(h)}:${pad(m)}`;
},
// 时区 ZZ 替换成 +0800 格式
ZZ: () => {
const offset = d.getTimezoneOffset();
const sign = offset <= 0 ? '+' : '-';
const h = Math.abs(Math.floor(offset / 60));
const m = Math.abs(offset % 60);
return `${sign}${pad(h)}${pad(m)}`;
},
};

// 通过长度将 tokens 排序,避免 Z 早于 ZZ 这种情况
const sortedCustomTokens = Object.keys(customTokens).sort((a, b) => b.length - a.length);
let result = format;
for (const token of sortedCustomTokens) {
const regex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
result = result.replace(regex, customTokens[token]());
}
result = dayjsInstance.format(result);

return result;
};

export const formatDateTime = (
date: DateTimeInput,
format: FormatPattern = DateTimeFormat.STANDARD
): string | dayjs.Dayjs => {
const isValidFormat = Object.values<string>(DateTimeFormat).includes(format);

if (!isValidFormat) {
return dayjs(date);
return toDefaultTz(date);
}
return dayjs(date).format(format);

// 检测是否为极早年份(< 1900):边缘情况年份可能存在解析问题,因此需要特殊处理
if (isEdgeCaseYear(date)) {
const nativeDate = getNativeDate(date);
if (!nativeDate) {
return 'Invalid Date';
}
return formatNativeDate(nativeDate, format as string);
}

return toDefaultTz(date).format(format);
};

export default formatDateTime;