diff --git a/js/date-picker/format.ts b/js/date-picker/format.ts index 3b264a4a3e..3187caf1ca 100644 --- a/js/date-picker/format.ts +++ b/js/date-picker/format.ts @@ -3,6 +3,9 @@ import dayjs from 'dayjs'; import isoWeeksInYear from 'dayjs/plugin/isoWeeksInYear'; import isLeapYear from 'dayjs/plugin/isLeapYear'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import weekOfYear from 'dayjs/plugin/weekOfYear'; +import quarterOfYear from 'dayjs/plugin/quarterOfYear'; import log from '../log'; @@ -11,12 +14,21 @@ type DateValue = string | number | Date; dayjs.extend(isoWeeksInYear); dayjs.extend(isLeapYear); dayjs.extend(customParseFormat); +dayjs.extend(advancedFormat); +dayjs.extend(weekOfYear); +dayjs.extend(quarterOfYear); export const TIME_FORMAT = 'HH:mm:ss'; // extract time format from a completed date format 'YYYY-MM-DD HH:mm' -> 'HH:mm' export function extractTimeFormat(dateFormat: string = '') { - return dateFormat.replace(/\W?Y{2,4}|\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?y{2,4}/g, '').trim(); + return ( + dateFormat + // 匹配:可选的非单词字符 + 日期占位符 + 可选的年月日中文单位 + .replace(/\W?(Y{2,4}|y{2,4}|M{1,4}|D{1,2}|d{1,4}|Do|Q{1,2}|w{1,2}|W{1,2}|E{1,4})[年月日]?/g, '') + .replace(/^[^\w]+/, '') // 移除开头的非单词字符 + .trim() + ); } // 统一解析日期格式字符串成 Dayjs 对象 @@ -38,9 +50,26 @@ export function parseToDayjs( .format(format) as string; } - const yearStr = dateText.split(/[-/.\s]/)[0]; - const weekStr = dateText.split(/[-/.\s]/)[1]; - const weekFormatStr = format.split(/[-/.\s]/)[1]; + let yearStr = dateText.split(/[-/.\s]/)[0]; + let weekStr = dateText.split(/[-/.\s]/)[1]; + let weekFormatStr = format.split(/[-/.\s]/)[1]; + + if (weekStr === undefined) { + const yearIndex = format.search(/Y{2,4}/); + const weekIndex = format.search(/w{1,2}|W{1,2}/); + if (yearIndex !== -1 && weekIndex !== -1) { + const yearLen = format.match(/Y{2,4}/)[0].length; + if (yearIndex < weekIndex) { + yearStr = dateText.substring(0, yearLen); + weekStr = dateText.substring(yearLen); + weekFormatStr = format.substring(yearLen); + } else { + weekStr = dateText.substring(0, dateText.length - yearLen); + yearStr = dateText.substring(dateText.length - yearLen); + weekFormatStr = format.substring(0, format.length - yearLen); + } + } + } let firstWeek = dayjs(yearStr, 'YYYY') .locale(dayjsLocale || 'zh-cn') @@ -71,6 +100,7 @@ export function parseToDayjs( return nextWeek; } } + if (weekStr && weekFormatStr) return dayjs(); } // format quarter @@ -81,9 +111,26 @@ export function parseToDayjs( .format(format) as string; } - const yearStr = dateText.split(/[-/.\s]/)[0]; - const quarterStr = dateText.split(/[-/.\s]/)[1]; - const quarterFormatStr = format.split(/[-/.\s]/)[1]; + let yearStr = dateText.split(/[-/.\s]/)[0]; + let quarterStr = dateText.split(/[-/.\s]/)[1]; + let quarterFormatStr = format.split(/[-/.\s]/)[1]; + + if (quarterStr === undefined) { + const yearIndex = format.search(/Y{2,4}/); + const quarterIndex = format.search(/Q/); + if (yearIndex !== -1 && quarterIndex !== -1) { + const yearLen = format.match(/Y{2,4}/)[0].length; + if (yearIndex < quarterIndex) { + yearStr = dateText.substring(0, yearLen); + quarterStr = dateText.substring(yearLen); + quarterFormatStr = format.substring(yearLen); + } else { + quarterStr = dateText.substring(0, dateText.length - yearLen); + yearStr = dateText.substring(dateText.length - yearLen); + quarterFormatStr = format.substring(0, format.length - yearLen); + } + } + } const firstQuarter = dayjs(yearStr, 'YYYY').startOf('year'); for (let i = 0; i < 4; i += 1) { const nextQuarter = firstQuarter.add(i, 'quarter'); @@ -117,15 +164,13 @@ export function parseToDayjs( try { const timeFormatFromFormat = extractTimeFormat(format || ''); if (defaultTime && (!timeFormatFromFormat || timeFormatFromFormat.trim() === '')) { - if (defaultTime) { - const parts = defaultTime.split(':').map((p) => Number(p)); - // 注意:dayjs 的 hour/minute/second 返回新的 dayjs 对象(可链式调用) - const withTime = result - .hour(parts[0] || 0) - .minute(parts[1] || 0) - .second(parts[2] || 0); - return withTime; - } + const parts = defaultTime.split(':').map((p) => Number(p)); + // 注意:dayjs 的 hour/minute/second 返回新的 dayjs 对象(可链式调用) + const withTime = result + .hour(parts[0] || 0) + .minute(parts[1] || 0) + .second(parts[2] || 0); + return withTime; } } catch (e) { // 保守处理:若设置时间出错,仍返回原始结果并记录日志 diff --git a/js/date-picker/utils.ts b/js/date-picker/utils.ts index 1b2e66d2e1..8fb0a57c4d 100644 --- a/js/date-picker/utils.ts +++ b/js/date-picker/utils.ts @@ -158,7 +158,12 @@ export function getDateObj(date: Date) { * @returns {Date} 一个新的date */ export function setDateTime(date: Date, hours: number, minutes: number, seconds: number, milliseconds?: number): Date { - return dayjs(date).hour(hours).minute(minutes).second(seconds).millisecond(milliseconds).toDate(); + return dayjs(date) + .hour(hours) + .minute(minutes) + .second(seconds) + .millisecond(milliseconds || 0) + .toDate(); } /** diff --git a/test/unit/date-picker/format.test.js b/test/unit/date-picker/format.test.js new file mode 100644 index 0000000000..d199854384 --- /dev/null +++ b/test/unit/date-picker/format.test.js @@ -0,0 +1,763 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; +import { + calcFormatTime, + extractTimeFormat, + formatDate, + formatTime, + getDefaultFormat, + initYearMonthTime, + isValidDate, + parseToDayjs, +} from '../../../js/date-picker/format'; + +describe('format', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + describe('calcFormatTime', () => { + it('time format shorter than default time format', () => { + const res = calcFormatTime('12:30:45', 'HH:mm'); + expect(res).toBe('12:30'); + }); + + it('time format same as default time format', () => { + const res = calcFormatTime('12:30:45', 'HH:mm:ss'); + expect(res).toBe('12:30:45'); + }); + + it('time format longer than provided time', () => { + const res = calcFormatTime('12:30', 'HH:mm:ss'); + expect(res).toBe('12:30'); + }); + + it('empty time returns empty', () => { + const res = calcFormatTime('', 'HH:mm:ss'); + expect(res).toBe(''); + }); + + it('empty time format returns time', () => { + const res = calcFormatTime('12:30:45', ''); + expect(res).toBe('12:30:45'); + }); + }); + + describe('extractTimeFormat', () => { + it('YYYY-MM-DD HH:mm:ss', () => { + const res = extractTimeFormat('YYYY-MM-DD HH:mm:ss'); + expect(res).toBe('HH:mm:ss'); + }); + + it('YYYY-MM-DD HH时mm分ss秒', () => { + const res = extractTimeFormat('YYYY-MM-DD HH时mm分ss秒'); + expect(res).toBe('HH时mm分ss秒'); + }); + + it('YYYY-MM-DD HH时mm分ss秒SSS毫秒', () => { + const res = extractTimeFormat('YYYY-MM-DD HH时mm分ss秒SSS毫秒'); + expect(res).toBe('HH时mm分ss秒SSS毫秒'); + }); + + it('YYYYMMDDHHmmss', () => { + const res = extractTimeFormat('YYYYMMDDHHmmss'); + expect(res).toBe('HHmmss'); + }); + + it('YYYY年MM月DD日HH时mm分ss秒', () => { + const res = extractTimeFormat('YYYY年MM月DD日HH时mm分ss秒'); + expect(res).toBe('HH时mm分ss秒'); + }); + + it('YYYY/MM/DD HH:mm:ss', () => { + const res = extractTimeFormat('YYYY/MM/DD HH:mm:ss'); + expect(res).toBe('HH:mm:ss'); + }); + + it('YYYY.MM.DD HH.mm.ss', () => { + const res = extractTimeFormat('YYYY.MM.DD HH.mm.ss'); + expect(res).toBe('HH.mm.ss'); + }); + + it('YYYY-MM-DD', () => { + const res = extractTimeFormat('YYYY-MM-DD'); + expect(res).toBe(''); + }); + + it('empty string', () => { + const res = extractTimeFormat(''); + expect(res).toBe(''); + }); + + it('YYYY-MM-DD HH:mm:ss.SSS', () => { + const res = extractTimeFormat('YYYY-MM-DD HH:mm:ss.SSS'); + expect(res).toBe('HH:mm:ss.SSS'); + }); + + it('YYYY-MM-DD h:mm A', () => { + const res = extractTimeFormat('YYYY-MM-DD h:mm A'); + expect(res).toBe('h:mm A'); + }); + + it('YYYY-MM-DD HH:mm', () => { + const res = extractTimeFormat('YYYY-MM-DD HH:mm'); + expect(res).toBe('HH:mm'); + }); + }); + + describe('formatDate', () => { + it('single date with default format', () => { + const result = formatDate('2025-08-26', { format: 'YYYY-MM-DD' }); + expect(result).toBe('2025-08-26'); + }); + + it('single date with targetFormat', () => { + const result = formatDate('2025-08-26', { format: 'YYYY-MM-DD', targetFormat: 'YYYY/MM/DD' }); + expect(result).toBe('2025/08/26'); + }); + + it('single date with time-stamp valueType', () => { + const date = '2025-08-26 10:24:30'; + const result = formatDate(date, { format: 'YYYY-MM-DD HH:mm:ss', targetFormat: 'time-stamp' }); + const expected = new Date(date).getTime(); + expect(result).toBe(expected); + }); + + it('single date with Date valueType', () => { + const date = '2025-08-26 10:24:30'; + const result = formatDate(date, { format: 'YYYY-MM-DD HH:mm:ss', targetFormat: 'Date' }); + expect(result).toBeInstanceOf(Date); + expect(result.getFullYear()).toBe(2025); + }); + + it('date range with autoSwap false', () => { + const result = formatDate(['2025-08-26', '2025-08-20'], { + format: 'YYYY-MM-DD', + autoSwap: false, + }); + expect(result).toEqual(['2025-08-26', '2025-08-20']); + }); + + it('date range with autoSwap true', () => { + const result = formatDate(['2025-08-26', '2025-08-20'], { + format: 'YYYY-MM-DD', + autoSwap: true, + }); + expect(result).toEqual(['2025-08-20', '2025-08-26']); + }); + + it('date range with targetFormat', () => { + const result = formatDate(['2025-08-20', '2025-08-26'], { + format: 'YYYY-MM-DD', + targetFormat: 'YYYY/MM/DD', + }); + expect(result).toEqual(['2025/08/20', '2025/08/26']); + }); + + it('date range with time-stamp valueType', () => { + const result = formatDate(['2025-08-20', '2025-08-26'], { + format: 'YYYY-MM-DD', + targetFormat: 'time-stamp', + }); + expect(Array.isArray(result)).toBe(true); + expect(result[0]).toBeTypeOf('number'); + expect(result[1]).toBeTypeOf('number'); + }); + + it('date range with Date valueType', () => { + const result = formatDate(['2025-08-20', '2025-08-26'], { + format: 'YYYY-MM-DD', + targetFormat: 'Date', + }); + expect(Array.isArray(result)).toBe(true); + expect(result[0]).toBeInstanceOf(Date); + expect(result[1]).toBeInstanceOf(Date); + }); + + it('date range with defaultTime', () => { + const result = formatDate(['2025-08-20', '2025-08-26'], { + format: 'YYYY-MM-DD', + targetFormat: 'YYYY-MM-DD HH:mm:ss', + defaultTime: ['09:00:00', '18:00:00'], + }); + expect(result).toEqual(['2025-08-20 09:00:00', '2025-08-26 18:00:00']); + }); + + it('empty date returns empty string', () => { + const result = formatDate('', { format: 'YYYY-MM-DD' }); + expect(result).toBe(''); + }); + + it('null date returns empty string', () => { + const result = formatDate(null, { format: 'YYYY-MM-DD' }); + expect(result).toBe(''); + }); + + it('date with custom locale zh-cn', () => { + const result = formatDate('2025-08-26', { format: 'YYYY-MM-DD', dayjsLocale: 'zh-cn' }); + expect(result).toBe('2025-08-26'); + }); + + it('date with custom locale en-us', () => { + const result = formatDate('2025-08-26', { format: 'YYYY-MM-DD', dayjsLocale: 'en-us' }); + expect(result).toBe('2025-08-26'); + }); + + it('single date with string defaultTime', () => { + const result = formatDate('2025-08-26', { + format: 'YYYY-MM-DD', + targetFormat: 'YYYY-MM-DD HH:mm:ss', + defaultTime: '12:30:00', + }); + expect(result).toBe('2025-08-26 12:30:00'); + }); + + it('handles single date with array defaultTime', () => { + const res = formatDate('2023-01-01', { + format: 'YYYY-MM-DD', + defaultTime: ['00:00:00', '12:00:00'], + }); + expect(res).toBe('2023-01-01'); + }); + }); + + describe('formatTime', () => { + it('valid date time value, return time value of datetime', () => { + const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); + expect(res).toBe('10:24:24'); + }); + + it('valid date time value, format and defaultTime, return time value of datetime', () => { + const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); + expect(res).toBe('10:24:24'); + }); + + it('valid array type date time value and format, return time value of datetime', () => { + const res = formatTime(['2025-08-26 10:24:24', '2025-08-26 10:24:24'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', [ + '00:00:00', + '23:59:59', + ]); + expect(res).toEqual(['10:24:24', '10:24:24']); + }); + + it('invalid date time value and defaultTime, return defaultTime', () => { + const res = formatTime('2025-08-26', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); + expect(res).toBe('00:00:00'); + }); + + it('invalid array type date time value, return time value of datetime', () => { + const res = formatTime(['2025-08-26', '2025-08-26'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', ['00:00:00', '23:59:59']); + expect(res).toEqual(['00:00:00', '23:59:59']); + }); + + it('valid date time value with no separator format', () => { + const res = formatTime('20250826102424', 'YYYYMMDDHHmmss', 'HHmmss'); + expect(res).toBe('102424'); + }); + + it('valid array type date time value with no separator format', () => { + const res = formatTime(['20250826102424', '20250826153059'], 'YYYYMMDDHHmmss', 'HHmmss'); + expect(res).toEqual(['102424', '153059']); + }); + + it('invalid date time value with no separator format and defaultTime', () => { + const res = formatTime('20250826', 'YYYYMMDDHHmmss', 'HHmmss', '000000'); + expect(res).toBe('000000'); + }); + + it('valid date time value with slash separator', () => { + const res = formatTime('2025/08/26 10:24:24', 'YYYY/MM/DD HH:mm:ss', 'HH:mm:ss'); + expect(res).toBe('10:24:24'); + }); + + it('valid date time value with dot separator', () => { + const res = formatTime('2025.08.26 10.24.24', 'YYYY.MM.DD HH.mm.ss', 'HH.mm.ss'); + expect(res).toBe('10.24.24'); + }); + + it('valid date time value with chinese separator', () => { + const res = formatTime('2025年08月26日 10时24分24秒', 'YYYY年MM月DD日HH时mm分ss秒', 'HH时mm分ss秒'); + expect(res).toBe('10时24分24秒'); + }); + + it('empty value returns formatted default time', () => { + const res = formatTime('', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '12:00:00'); + expect(res).toBe('12:00:00'); + }); + + it('undefined value returns formatted default time', () => { + const res = formatTime(undefined, 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '12:00:00'); + expect(res).toBe('12:00:00'); + }); + + it('Date object type value', () => { + const date = new Date('2025-08-26T10:24:30'); + const res = formatTime(date, 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); + expect(res).toBe('10:24:30'); + }); + + it('number timestamp type value', () => { + const timestamp = new Date('2025-08-26T10:24:30').getTime(); + const res = formatTime(timestamp, 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); + expect(res).toBe('10:24:30'); + }); + + it('handles empty array value', () => { + const res = formatTime([], 'YYYY-MM-DD', 'HH:mm:ss', ['00:00:00']); + expect(res).toEqual(['00:00:00']); + }); + }); + + describe('getDefaultFormat', () => { + it('year mode', () => { + const result = getDefaultFormat({ mode: 'year' }); + expect(result.format).toBe('YYYY'); + expect(result.valueType).toBe('YYYY'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('year mode with custom format', () => { + const result = getDefaultFormat({ mode: 'year', format: 'YYYY年' }); + expect(result.format).toBe('YYYY年'); + expect(result.valueType).toBe('YYYY年'); + }); + + it('month mode', () => { + const result = getDefaultFormat({ mode: 'month' }); + expect(result.format).toBe('YYYY-MM'); + expect(result.valueType).toBe('YYYY-MM'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('month mode with custom format', () => { + const result = getDefaultFormat({ mode: 'month', format: 'YYYY年MM月' }); + expect(result.format).toBe('YYYY年MM月'); + expect(result.valueType).toBe('YYYY年MM月'); + }); + + it('quarter mode', () => { + const result = getDefaultFormat({ mode: 'quarter' }); + expect(result.format).toBe('YYYY-[Q]Q'); + expect(result.valueType).toBe('YYYY-[Q]Q'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('week mode', () => { + const result = getDefaultFormat({ mode: 'week' }); + expect(result.format).toBe('gggg-wo'); + expect(result.valueType).toBe('gggg-wo'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('date mode', () => { + const result = getDefaultFormat({ mode: 'date' }); + expect(result.format).toBe('YYYY-MM-DD'); + expect(result.valueType).toBe('YYYY-MM-DD'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('date mode with time picker', () => { + const result = getDefaultFormat({ mode: 'date', enableTimePicker: true }); + expect(result.format).toBe('YYYY-MM-DD HH:mm:ss'); + expect(result.valueType).toBe('YYYY-MM-DD HH:mm:ss'); + expect(result.timeFormat).toBe('HH:mm:ss'); + }); + + it('date mode with custom format', () => { + const result = getDefaultFormat({ mode: 'date', format: 'YYYY/MM/DD', valueType: 'time-stamp' }); + expect(result.format).toBe('YYYY/MM/DD'); + expect(result.valueType).toBe('time-stamp'); + }); + + it('extract time format from date mode with time', () => { + const result = getDefaultFormat({ mode: 'date', format: 'YYYY-MM-DD HH:mm' }); + expect(result.timeFormat).toBe('HH:mm'); + }); + }); + + describe('initYearMonthTime', () => { + it('empty value returns default year month time', () => { + const result = initYearMonthTime({ + value: [], + mode: 'date', + format: 'YYYY-MM-DD', + }); + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth(); + expect(result.year[0]).toBe(currentYear); + expect(result.month[0]).toBe(currentMonth); + }); + + it('year mode increments second year by 10', () => { + const result = initYearMonthTime({ + value: [], + mode: 'year', + format: 'YYYY', + }); + const currentYear = new Date().getFullYear(); + expect(result.year[0]).toBe(currentYear); + expect(result.year[1]).toBe(currentYear + 10); + }); + + it('month mode increments second year by 1', () => { + const result = initYearMonthTime({ + value: [], + mode: 'month', + format: 'YYYY-MM', + }); + const currentYear = new Date().getFullYear(); + expect(result.year[0]).toBe(currentYear); + expect(result.year[1]).toBe(currentYear + 1); + }); + + it('quarter mode increments second year by 1', () => { + const result = initYearMonthTime({ + value: [], + mode: 'quarter', + format: 'YYYY-[Q]Q', + }); + const currentYear = new Date().getFullYear(); + expect(result.year[0]).toBe(currentYear); + expect(result.year[1]).toBe(currentYear + 1); + }); + + it('date mode without time picker increments month', () => { + const result = initYearMonthTime({ + value: [], + mode: 'date', + format: 'YYYY-MM-DD', + }); + const currentMonth = new Date().getMonth(); + if (currentMonth === 11) { + expect(result.month[1]).toBe(0); + expect(result.year[1]).toBe(result.year[0] + 1); + } else { + expect(result.month[1]).toBe(currentMonth + 1); + } + }); + + it('date mode with December increments year', () => { + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + const result = initYearMonthTime({ + value: [], + mode: 'date', + format: 'YYYY-MM-DD', + }); + if (currentMonth === 11) { + expect(result.year[1]).toBe(currentYear + 1); + expect(result.month[1]).toBe(0); + } + }); + + it('date mode with valid value', () => { + const result = initYearMonthTime({ + value: ['2025-08-26', '2025-09-15'], + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result.year).toEqual([2025, 2025]); + expect(result.month).toEqual([7, 8]); + }); + + it('date mode with custom timeFormat', () => { + const result = initYearMonthTime({ + value: ['2025-08-26 10:30', '2025-08-26 15:45'], + mode: 'date', + format: 'YYYY-MM-DD HH:mm', + timeFormat: 'HH:mm', + }); + expect(result.time).toEqual(['10:30', '15:45']); + }); + + it('week mode with valid value', () => { + const result = initYearMonthTime({ + value: ['2025-35', '2025-40'], + mode: 'week', + format: 'YYYY-ww', + }); + expect(result.year).toEqual([2025, 2025]); + expect(Array.isArray(result.time)).toBe(true); + }); + + it('week mode without time picker', () => { + const result = initYearMonthTime({ + value: [], + mode: 'week', + format: 'YYYY-ww', + }); + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + if (currentMonth === 11) { + expect(result.year[1]).toBe(currentYear + 1); + expect(result.month[1]).toBe(0); + } else { + expect(result.month[1]).toBe(currentMonth + 1); + } + }); + + it('handles December rollover correctly', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2023-12-15')); + + const result = initYearMonthTime({ + value: [], + mode: 'date', + format: 'YYYY-MM-DD', + }); + + expect(result.month[0]).toBe(11); + expect(result.month[1]).toBe(0); + expect(result.year[1]).toBe(result.year[0] + 1); + }); + }); + + describe('isValidDate', () => { + it('valid single date string', () => { + const result = isValidDate('2025-08-26', 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('invalid single date string', () => { + const result = isValidDate('invalid-date', 'YYYY-MM-DD'); + expect(result).toBe(false); + }); + + it('empty string is valid', () => { + const result = isValidDate('', 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('valid date array', () => { + const result = isValidDate(['2025-08-26', '2025-08-27'], 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('invalid date array with one invalid', () => { + const result = isValidDate(['2025-08-26', 'invalid'], 'YYYY-MM-DD'); + expect(result).toBe(false); + }); + + it('date array with empty string', () => { + const result = isValidDate(['2025-08-26', ''], 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('date array with null', () => { + const result = isValidDate(['2025-08-26', ''], 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('valid date object', () => { + const result = isValidDate(new Date('2025-08-26'), 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + + it('valid timestamp', () => { + const timestamp = new Date('2025-08-26').getTime(); + const result = isValidDate(timestamp, 'YYYY-MM-DD'); + expect(result).toBe(true); + }); + }); + + describe('parseToDayjs', () => { + it('basic date format YYYY-MM-DD', () => { + const result = parseToDayjs('2025-08-26', 'YYYY-MM-DD'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD')).toBe('2025-08-26'); + }); + + it('date time format YYYY-MM-DD HH:mm:ss', () => { + const result = parseToDayjs('2025-08-26 10:24:30', 'YYYY-MM-DD HH:mm:ss'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD HH:mm:ss')).toBe('2025-08-26 10:24:30'); + }); + + it('date object type', () => { + const date = new Date('2025-08-26T10:24:30'); + const result = parseToDayjs(date, 'YYYY-MM-DD HH:mm:ss'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD HH:mm:ss')).toBe('2025-08-26 10:24:30'); + }); + + it('number timestamp type', () => { + const timestamp = new Date('2025-08-26T10:24:30').getTime(); + const result = parseToDayjs(timestamp, 'YYYY-MM-DD HH:mm:ss'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD HH:mm:ss')).toBe('2025-08-26 10:24:30'); + }); + + it('empty string return current date', () => { + const result = parseToDayjs('', 'YYYY-MM-DD'); + expect(result.isValid()).toBe(true); + }); + + it('null return current date', () => { + const result = parseToDayjs(null, 'YYYY-MM-DD'); + expect(result.isValid()).toBe(true); + }); + + it('week format with separator YYYY-ww', () => { + const result = parseToDayjs('2025-35', 'YYYY-ww'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('week format without separator YYYYww', () => { + const result = parseToDayjs('202535', 'YYYYww'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('quarter format with separator YYYY-[Q]Q', () => { + const result = parseToDayjs('2025-Q1', 'YYYY-[Q]Q'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(0); + }); + + it('quarter format without separator YYYYQ', () => { + const result = parseToDayjs('2025Q1', 'YYYYQ'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(0); + }); + + it('year format YYYY', () => { + const result = parseToDayjs('2025', 'YYYY'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('month format YYYY-MM', () => { + const result = parseToDayjs('2025-08', 'YYYY-MM'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(7); + }); + + it('chinese format YYYY年MM月', () => { + const result = parseToDayjs('2025年08月', 'YYYY年MM月'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(7); + }); + + it('custom locale zh-cn', () => { + const result = parseToDayjs('2025-08-26', 'YYYY-MM-DD', undefined, 'zh-cn'); + expect(result.isValid()).toBe(true); + expect(result.locale('zh-cn').format('YYYY年MM月DD日')).toBe('2025年08月26日'); + }); + + it('custom locale en-us', () => { + const result = parseToDayjs('2025-08-26', 'YYYY-MM-DD', undefined, 'en-us'); + expect(result.isValid()).toBe(true); + expect(result.locale('en-us').format('MM/DD/YYYY')).toBe('08/26/2025'); + }); + + it('date format with defaultTime', () => { + const result = parseToDayjs('2025-08-26', 'YYYY-MM-DD', undefined, 'zh-cn', '12:30:00'); + expect(result.isValid()).toBe(true); + expect(result.hour()).toBe(12); + expect(result.minute()).toBe(30); + expect(result.second()).toBe(0); + }); + + it('date format without time and with defaultTime', () => { + const result = parseToDayjs('2025-08-26', 'YYYY-MM-DD'); + expect(result.isValid()).toBe(true); + expect(result.hour()).toBe(0); + expect(result.minute()).toBe(0); + expect(result.second()).toBe(0); + }); + + it('no separator format YYYYMMDD', () => { + const result = parseToDayjs('20250826', 'YYYYMMDD'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD')).toBe('2025-08-26'); + }); + + it('slash separator format YYYY/MM/DD', () => { + const result = parseToDayjs('2025/08/26', 'YYYY/MM/DD'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD')).toBe('2025-08-26'); + }); + + it('dot separator format YYYY.MM.DD', () => { + const result = parseToDayjs('2025.08.26', 'YYYY.MM.DD'); + expect(result.isValid()).toBe(true); + expect(result.format('YYYY-MM-DD')).toBe('2025-08-26'); + }); + + it('format mismatch return parsed date', () => { + const result = parseToDayjs('2025/08/26', 'YYYY-MM-DD'); + expect(result.isValid()).toBe(true); + }); + + it('week format with timeOfDay start', () => { + const result = parseToDayjs('2025-35', 'YYYY-ww', 'start'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('quarter format with defaultTime', () => { + const result = parseToDayjs('2025Q1', 'YYYYQ', undefined, 'zh-cn', '15:30:45'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(0); + expect(result.hour()).toBe(15); + expect(result.minute()).toBe(30); + expect(result.second()).toBe(45); + }); + + it('week format with separator and defaultTime', () => { + const result = parseToDayjs('2025-35', 'YYYY-ww', undefined, 'zh-cn', '08:20:00'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.hour()).toBe(8); + expect(result.minute()).toBe(20); + expect(result.second()).toBe(0); + }); + + it('handles non-string input for week format', () => { + const date = new Date('2023-01-02'); + const res = parseToDayjs(date, 'YYYY-wo'); + expect(res.isValid()).toBe(true); + expect(res.format('YYYY-MM-DD')).toBe('2023-01-08'); + }); + + it('returns default dayjs for invalid weekNum', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-01-01')); + const res = parseToDayjs('2025-54', 'YYYY-ww'); + expect(res.format('YYYY-MM-DD')).toBe('2025-01-01'); + }); + + it('logs error when setting defaultTime fails', () => { + const res = parseToDayjs('2023-01-01', 'YYYY-MM-DD', undefined, undefined, 123); + expect(res.isValid()).toBe(true); + expect(res.year()).toBe(2023); + }); + + it('parses YYYYwo correctly', () => { + // Use 'en' locale to match '1st' format + const result = parseToDayjs('20251st', 'YYYYwo', undefined, 'en'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('parses wwYYYY correctly', () => { + const result = parseToDayjs('352025', 'wwYYYY'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + }); + + it('parses QYYYY correctly', () => { + const result = parseToDayjs('32025', 'QYYYY'); + expect(result.isValid()).toBe(true); + expect(result.year()).toBe(2025); + expect(result.month()).toBe(6); // Q3 starts in July (index 6) + }); + }); +}); diff --git a/test/unit/date-picker/utils.test.js b/test/unit/date-picker/utils.test.js index 2f9eebdab0..6f3b7ab283 100644 --- a/test/unit/date-picker/utils.test.js +++ b/test/unit/date-picker/utils.test.js @@ -1,50 +1,794 @@ import { describe, it, expect } from 'vitest'; -import { extractTimeFormat, formatTime } from '../../../js/date-picker/format'; +import { + addMonth, + covertToDate, + extractTimeObj, + firstUpperCase, + flagActive, + getDateObj, + getMonths, + getQuarters, + getToday, + getWeeks, + getYears, + isEnabledDate, + isSame, + outOfRanges, + setDateTime, + subtractMonth, +} from '../../../js/date-picker/utils'; describe('utils', () => { - describe(' extractTimeFormat', () => { - it('YYYY-MM-DD HH:mm:ss', () => { - const res = extractTimeFormat('YYYY-MM-DD HH:mm:ss'); - expect(res).toBe('HH:mm:ss'); + describe('addMonth', () => { + it('should add months', () => { + const date = new Date('2025-08-26'); + const result = addMonth(date, 3); + expect(result.getMonth()).toBe(10); + expect(result.getFullYear()).toBe(2025); }); - it('YYYY-MM-DD HH时mm分ss秒', () => { - const res = extractTimeFormat('YYYY-MM-DD HH时mm分ss秒'); - expect(res).toBe('HH时mm分ss秒'); + it('should handle year rollover', () => { + const date = new Date('2025-11-26'); + const result = addMonth(date, 5); + expect(result.getMonth()).toBe(3); + expect(result.getFullYear()).toBe(2026); }); - it('YYYY-MM-DD HH时mm分ss秒SSS毫秒', () => { - const res = extractTimeFormat('YYYY-MM-DD HH时mm分ss秒SSS毫秒'); - expect(res).toBe('HH时mm分ss秒SSS毫秒'); + it('should add one month', () => { + const date = new Date('2025-08-26'); + const result = addMonth(date, 1); + expect(result.getMonth()).toBe(8); }); }); - describe('formatTime', () => { - it('valid date time value, return time value of datetime', () => { - const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); - expect(res).toBe('10:24:24'); + + describe('covertToDate', () => { + it('should convert time-stamp to Date', () => { + const result = covertToDate('1756166400000', 'time-stamp'); + expect(result).toBeInstanceOf(Date); + }); + + it('should convert formatted string to Date', () => { + const result = covertToDate('2025-08-26', 'YYYY-MM-DD'); + expect(result).toBeInstanceOf(Date); + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(7); + expect(result.getDate()).toBe(26); + }); + + it('should handle datetime string', () => { + const result = covertToDate('2025-08-26 10:30:45', 'YYYY-MM-DD HH:mm:ss'); + expect(result).toBeInstanceOf(Date); + expect(result.getHours()).toBe(10); + expect(result.getMinutes()).toBe(30); + expect(result.getSeconds()).toBe(45); + }); + }); + + describe('extractTimeObj', () => { + it('should extract time from PM format', () => { + const result = extractTimeObj('pm 20:11:11:333'); + expect(result.hours).toBe(20); + expect(result.minutes).toBe(11); + expect(result.seconds).toBe(11); + expect(result.milliseconds).toBe(333); + expect(result.meridiem).toBe('pm'); + }); + + it('should extract time from AM format', () => { + const result = extractTimeObj('am 08:30:45'); + expect(result.hours).toBe(8); + expect(result.minutes).toBe(30); + expect(result.seconds).toBe(45); + expect(result.meridiem).toBe('am'); + }); + + it('should handle empty string', () => { + const result = extractTimeObj(''); + expect(result.hours).toBe(0); + expect(result.minutes).toBe(0); + expect(result.seconds).toBe(0); + expect(result.milliseconds).toBe(0); + }); + + it('should handle time without meridiem', () => { + const result = extractTimeObj('15:30:45'); + expect(result.hours).toBe(15); + expect(result.minutes).toBe(30); + expect(result.seconds).toBe(45); + }); + + it('should handle hours and minutes only', () => { + const result = extractTimeObj('15:30'); + expect(result.hours).toBe(15); + expect(result.minutes).toBe(30); + expect(result.seconds).toBe(0); + expect(result.milliseconds).toBe(0); + }); + + it('should handle hours only', () => { + const result = extractTimeObj('15'); + expect(result.hours).toBe(15); + expect(result.minutes).toBe(0); + expect(result.seconds).toBe(0); + }); + + it('should handle milliseconds', () => { + const result = extractTimeObj('15:30:45:999'); + expect(result.milliseconds).toBe(999); + }); + }); + + describe('firstUpperCase', () => { + it('should capitalize first letter', () => { + expect(firstUpperCase('hello')).toBe('Hello'); + }); + + it('should handle single character', () => { + expect(firstUpperCase('a')).toBe('A'); + }); + + it('should handle empty string', () => { + expect(firstUpperCase('')).toBe(''); + }); + + it('should handle string with special characters', () => { + expect(firstUpperCase('123abc')).toBe('123abc'); + }); + }); + + describe('flagActive', () => { + it('should flag active date for single selection', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const start = new Date('2025-08-15'); + const result = flagActive(data, { + start, + end: null, + hoverStart: null, + hoverEnd: null, + type: 'date', + isRange: false, + value: start, + }); + expect(result.flat().some((d) => d.active && d.text === 15)).toBe(true); + }); + + it('should flag active dates for range selection', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const start = new Date('2025-08-10'); + const end = new Date('2025-08-20'); + const result = flagActive(data, { start, end, hoverStart: null, hoverEnd: null, type: 'date', isRange: true }); + expect(result.flat().some((d) => d.active && d.text === 10)).toBe(true); + expect(result.flat().some((d) => d.active && d.text === 20)).toBe(true); + }); + + it('should highlight dates in range', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const start = new Date('2025-08-10'); + const end = new Date('2025-08-20'); + const result = flagActive(data, { start, end, hoverStart: null, hoverEnd: null, type: 'date', isRange: true }); + expect(result.flat().some((d) => d.highlight)).toBe(true); + }); + + it('should not flag additional dates', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const start = new Date('2025-08-15'); + const result = flagActive(data, { + start, + end: null, + hoverStart: null, + hoverEnd: null, + type: 'date', + isRange: false, + value: start, + }); + expect(result.flat().filter((d) => d.active && d.additional).length).toBe(0); + }); + + it('should handle hover highlight for range', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const hoverStart = new Date('2025-08-10'); + const hoverEnd = new Date('2025-08-15'); + const result = flagActive(data, { start: null, end: null, hoverStart, hoverEnd, type: 'date', isRange: true }); + expect(result.flat().some((d) => d.hoverHighlight)).toBe(true); + }); + + it('should return data unchanged for week type', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const result = flagActive(data, { + start: null, + end: null, + hoverStart: null, + hoverEnd: null, + type: 'week', + isRange: false, + }); + expect(result).toEqual(data); + }); + + it('should handle multiple selection', () => { + const data = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + const value = [new Date('2025-08-10'), new Date('2025-08-15'), new Date('2025-08-20')]; + const result = flagActive(data, { + start: null, + end: null, + hoverStart: null, + hoverEnd: null, + type: 'date', + isRange: false, + value, + multiple: true, + }); + const activeCount = result.flat().filter((d) => d.active).length; + expect(activeCount).toBeGreaterThanOrEqual(3); + }); + }); + + describe('getDateObj', () => { + it('should return date object properties', () => { + const date = new Date('2025-08-26T10:30:45'); + const obj = getDateObj(date); + expect(obj.year).toBe(2025); + expect(obj.month).toBe(7); + expect(obj.date).toBe(26); + expect(obj.hours).toBe(10); + expect(obj.minutes).toBe(30); + expect(obj.seconds).toBe(45); + expect(obj.milliseconds).toBe(0); + expect(obj.meridiem).toBe('AM'); + }); + + it('should handle PM time', () => { + const date = new Date('2025-08-26T14:30:45'); + const obj = getDateObj(date); + expect(obj.meridiem).toBe('PM'); + }); + + it('should handle non-date input', () => { + const obj = getDateObj(null); + expect(obj.year).toBeTypeOf('number'); + expect(obj.month).toBeTypeOf('number'); + expect(obj.date).toBeTypeOf('number'); + }); + + it('should handle milliseconds', () => { + const date = new Date('2025-08-26T10:30:45.123'); + const obj = getDateObj(date); + expect(obj.milliseconds).toBe(123); + }); + }); + + describe('getMonths', () => { + it('should return 12 months', () => { + const result = getMonths(2025, { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + monthLocal: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + }); + expect(result.length).toBe(4); + expect(result.flat().length).toBe(12); + }); + + it('should mark current month', () => { + const today = new Date(); + const result = getMonths(today.getFullYear(), { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + monthLocal: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + }); + expect(result.flat().some((d) => d.now)).toBe(true); + }); + + it('should disable dates within range', () => { + const result = getMonths(2025, { + disableDate: () => false, + minDate: new Date('2025-03-01'), + maxDate: new Date('2025-08-31'), + monthLocal: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + }); + expect(result.flat().some((d) => d.disabled)).toBe(true); + }); + + it('should handle disableDate function', () => { + const result = getMonths(2025, { + disableDate: () => true, + minDate: undefined, + maxDate: undefined, + monthLocal: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + }); + expect(result.flat().every((d) => d.disabled)).toBe(true); + }); + }); + + describe('getQuarters', () => { + it('should return 4 quarters', () => { + const result = getQuarters(2025, { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + quarterLocal: ['Q1', 'Q2', 'Q3', 'Q4'], + }); + expect(result.length).toBe(1); + expect(result[0].length).toBe(4); + expect(result[0][0].text).toBe('Q1'); + expect(result[0][3].text).toBe('Q4'); + }); + + it('should mark current quarter', () => { + const today = new Date(); + const result = getQuarters(today.getFullYear(), { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + quarterLocal: ['Q1', 'Q2', 'Q3', 'Q4'], + }); + expect(result.flat().some((d) => d.now)).toBe(true); + }); + + it('should disable dates within range', () => { + const result = getQuarters(2025, { + disableDate: () => false, + minDate: new Date('2025-04-01'), + maxDate: new Date('2025-09-30'), + quarterLocal: ['Q1', 'Q2', 'Q3', 'Q4'], + }); + expect(result.flat().some((d) => d.disabled)).toBe(true); + }); + + it('should handle disableDate function', () => { + const result = getQuarters(2025, { + disableDate: () => true, + minDate: undefined, + maxDate: undefined, + quarterLocal: ['Q1', 'Q2', 'Q3', 'Q4'], + }); + expect(result.flat().every((d) => d.disabled)).toBe(true); + }); + }); + + describe('getToday', () => { + it('should return today with time set to 00:00:00', () => { + const today = getToday(); + const now = new Date(); + expect(today.getFullYear()).toBe(now.getFullYear()); + expect(today.getMonth()).toBe(now.getMonth()); + expect(today.getDate()).toBe(now.getDate()); + expect(today.getHours()).toBe(0); + expect(today.getMinutes()).toBe(0); + expect(today.getSeconds()).toBe(0); + }); + }); + + describe('getWeeks', () => { + it('should return 6 weeks of days', () => { + const result = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + expect(result.length).toBe(6); + expect(result[0].length).toBe(7); + }); + + it('should show week of year when enabled', () => { + const result = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + showWeekOfYear: true, + } + ); + expect(result[0].length).toBe(8); }); - it('valid date time value, format and defaultTime, return time value of datetime', () => { - const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); - expect(res).toBe('10:24:24'); + it('should disable dates within range', () => { + const result = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: new Date('2025-08-15'), + maxDate: new Date('2025-08-20'), + } + ); + expect(result.flat().some((d) => d.disabled && !d.additional)).toBe(true); }); - it('valid array type date time value and format, return time value of datetime', () => { - const res = formatTime(['2025-08-26 10:24:24', '2025-08-26 10:24:24'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', [ - '00:00:00', - '23:59:59', - ]); - expect(res).toEqual(['10:24:24', '10:24:24']); + it('should handle disableDate function', () => { + const result = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 1, + disableDate: (date) => date.getDate() === 10, + minDate: undefined, + maxDate: undefined, + } + ); + expect(result.flat().some((d) => d.disabled && d.text === 10)).toBe(true); + }); + + it('should include additional days from prev and next month', () => { + const result = getWeeks( + { year: 2025, month: 7 }, + { + firstDayOfWeek: 0, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + expect(result.flat().some((d) => d.additional && d.type === 'prev-month')).toBe(true); + expect(result.flat().some((d) => d.additional && d.type === 'next-month')).toBe(true); + }); + + it('should mark today', () => { + const today = new Date(); + const result = getWeeks( + { year: today.getFullYear(), month: today.getMonth() }, + { + firstDayOfWeek: 1, + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + } + ); + expect(result.flat().some((d) => d.now)).toBe(true); + }); + }); + + describe('getYears', () => { + it('should return 10 years', () => { + const result = getYears(2025, { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + }); + expect(result.length).toBe(4); + expect(result.flat().length).toBe(10); + expect(result[0][0].text).toBe('2020'); + expect(result[0][2].text).toBe('2022'); + }); + + it('should mark current year', () => { + const currentYear = new Date().getFullYear(); + const result = getYears(currentYear, { + disableDate: () => false, + minDate: undefined, + maxDate: undefined, + }); + expect(result.flat().some((d) => d.now)).toBe(true); + }); + + it('should disable dates within range', () => { + const result = getYears(2025, { + disableDate: () => false, + minDate: new Date('2025-01-01'), + maxDate: new Date('2028-12-31'), + }); + expect(result.flat().some((d) => d.disabled)).toBe(true); + }); + + it('should handle disableDate function', () => { + const result = getYears(2025, { + disableDate: () => true, + minDate: undefined, + maxDate: undefined, + }); + expect(result.flat().every((d) => d.disabled)).toBe(true); + }); + }); + + describe('isEnabledDate', () => { + it('should return true when no disableDate', () => { + const result = isEnabledDate({ + value: new Date('2025-08-26'), + disableDate: null, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(true); + }); + + it('should handle disableDate function', () => { + const result = isEnabledDate({ + value: new Date('2025-08-26'), + disableDate: (date) => date.getDate() === 26, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(false); + }); + + it('should handle disableDate array', () => { + const date = new Date('2025-08-26T00:00:00'); + const result = isEnabledDate({ + value: date, + disableDate: ['2025-08-26 00:00:00', '2025-08-27 00:00:00'], + mode: 'date', + format: 'YYYY-MM-DD HH:mm:ss', + }); + expect(result).toBe(false); + }); + + it('should return true for date not in disableDate array', () => { + const date = new Date('2025-08-25T00:00:00'); + const result = isEnabledDate({ + value: date, + disableDate: ['2025-08-26 00:00:00', '2025-08-27 00:00:00'], + mode: 'date', + format: 'YYYY-MM-DD HH:mm:ss', + }); + expect(result).toBe(true); + }); + + it('should handle disableDate object with from and to', () => { + const result = isEnabledDate({ + value: new Date('2025-08-15'), + disableDate: { from: '2025-08-10', to: '2025-08-20' }, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(false); + }); + + it('should return true for date outside from-to range', () => { + const result = isEnabledDate({ + value: new Date('2025-08-25'), + disableDate: { from: '2025-08-10', to: '2025-08-20' }, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(true); + }); + + it('should handle disableDate object with before', () => { + const result = isEnabledDate({ + value: new Date('2025-08-05'), + disableDate: { before: '2025-08-10' }, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(false); + }); + + it('should handle disableDate object with after', () => { + const result = isEnabledDate({ + value: new Date('2025-08-25'), + disableDate: { after: '2025-08-20' }, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(false); + }); + + it('should handle disableDate object with both before and after', () => { + const result = isEnabledDate({ + value: new Date('2025-08-15'), + disableDate: { before: '2025-08-10', after: '2025-08-20' }, + mode: 'date', + format: 'YYYY-MM-DD', + }); + expect(result).toBe(true); + }); + + it('should handle quarter mode with date mode checking', () => { + const result = isEnabledDate({ + value: new Date('2025-04-15'), + disableDate: (date) => date.getDate() === 15, + mode: 'quarter', + format: 'YYYY-[Q]Q', + }); + expect(result).toBe(false); + }); + }); + + describe('isSame', () => { + it('same date', () => { + const date1 = new Date('2025-08-26'); + const date2 = new Date('2025-08-26'); + expect(isSame(date1, date2, 'date')).toBe(true); + }); + + it('different date', () => { + const date1 = new Date('2025-08-26'); + const date2 = new Date('2025-08-27'); + expect(isSame(date1, date2, 'date')).toBe(false); + }); + + it('same month', () => { + const date1 = new Date('2025-08-10'); + const date2 = new Date('2025-08-20'); + expect(isSame(date1, date2, 'month')).toBe(true); + }); + + it('different month', () => { + const date1 = new Date('2025-08-10'); + const date2 = new Date('2025-09-20'); + expect(isSame(date1, date2, 'month')).toBe(false); + }); + + it('same year', () => { + const date1 = new Date('2025-01-10'); + const date2 = new Date('2025-12-20'); + expect(isSame(date1, date2, 'year')).toBe(true); + }); + + it('different year', () => { + const date1 = new Date('2025-08-10'); + const date2 = new Date('2026-08-20'); + expect(isSame(date1, date2, 'year')).toBe(false); + }); + + it('same quarter', () => { + const date1 = new Date('2025-08-10'); + const date2 = new Date('2025-09-20'); + expect(isSame(date1, date2, 'quarter')).toBe(true); + }); + + it('different quarter', () => { + const date1 = new Date('2025-08-10'); + const date2 = new Date('2025-10-20'); + expect(isSame(date1, date2, 'quarter')).toBe(false); + }); + + it('same week', () => { + const date1 = new Date('2025-08-25'); + const date2 = new Date('2025-08-27'); + expect(isSame(date1, date2, 'week')).toBe(true); + }); + + it('different week', () => { + const date1 = new Date('2025-08-25'); + const date2 = new Date('2025-09-01'); + expect(isSame(date1, date2, 'week')).toBe(false); + }); + + it('default type is date', () => { + const date1 = new Date('2025-08-26'); + const date2 = new Date('2025-08-26'); + expect(isSame(date1, date2)).toBe(true); + }); + }); + + describe('outOfRanges', () => { + it('date before min', () => { + const date = new Date('2025-08-20'); + const min = new Date('2025-08-25'); + expect(outOfRanges(date, min, null)).toBe(true); + }); + + it('date after max', () => { + const date = new Date('2025-08-30'); + const max = new Date('2025-08-25'); + expect(outOfRanges(date, null, max)).toBe(true); + }); + + it('date within range', () => { + const date = new Date('2025-08-25'); + const min = new Date('2025-08-20'); + const max = new Date('2025-08-30'); + expect(outOfRanges(date, min, max)).toBe(false); + }); + + it('date equal to min', () => { + const date = new Date('2025-08-25'); + const min = new Date('2025-08-25'); + expect(outOfRanges(date, min, null)).toBeFalsy(); + }); + + it('date equal to max', () => { + const date = new Date('2025-08-25'); + const max = new Date('2025-08-25'); + expect(outOfRanges(date, null, max)).toBeFalsy(); + }); + + it('no min and max', () => { + const date = new Date('2025-08-25'); + expect(outOfRanges(date, null, null)).toBeFalsy(); + }); + }); + + describe('setDateTime', () => { + it('should return new date object', () => { + const date = new Date('2025-08-26'); + const result = setDateTime(date, 10, 30, 45); + expect(result).toBeInstanceOf(Date); + expect(result).not.toBe(date); + }); + + it('should set time on date', () => { + const date = new Date('2025-08-26'); + const result = setDateTime(date, 10, 30, 45); + expect(result).toBeInstanceOf(Date); + expect(result.getHours()).toBe(10); + expect(result.getMinutes()).toBe(30); + expect(result.getSeconds()).toBe(45); + }); + }); + + describe('subtractMonth', () => { + it('should subtract months', () => { + const date = new Date('2025-08-26'); + const result = subtractMonth(date, 3); + expect(result.getMonth()).toBe(4); + expect(result.getFullYear()).toBe(2025); }); - it('invalid date time value and defaultTime, return defaultTime', () => { - const res = formatTime('2025-08-26', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); - expect(res).toBe('00:00:00'); + it('should handle year rollover', () => { + const date = new Date('2025-02-26'); + const result = subtractMonth(date, 5); + expect(result.getMonth()).toBe(8); + expect(result.getFullYear()).toBe(2024); }); - it('invalid array type date time value, return time value of datetime', () => { - const res = formatTime(['2025-08-26', '2025-08-26'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', ['00:00:00', '23:59:59']); - expect(res).toEqual(['00:00:00', '23:59:59']); + it('should subtract one month', () => { + const date = new Date('2025-08-26'); + const result = subtractMonth(date, 1); + expect(result.getMonth()).toBe(6); }); }); });