函数归一化 + 日期转换函数

2 阅读4分钟

函数归一化 + 日期转换函数

你提到的“函数归一化”通常指将不同格式的输入统一转换成标准格式。下面手写一个通用的日期转换/归一化函数。


1. 完整实现

/**
 * 日期归一化函数 - 将各种格式的日期输入统一转换为标准 Date 对象
 * @param {*} dateInput - 支持的输入格式:Date对象、时间戳、日期字符串、年月日对象、数组
 * @param {Object} options - 配置项
 * @returns {Date|null} - 返回 Date 对象,无效时返回 null
 */
function normalizeDate(dateInput, options = {}) {
    const { 
        strict = false,      // 严格模式:无效输入返回 null 而非抛出错误
        defaultDate = null,  // 默认返回值(当输入无效时)
        timezone = 'local'   // 时区处理:'local' | 'utc' | 'timestamp'
    } = options;

    // 1. 已经是 Date 对象且有效
    if (dateInput instanceof Date) {
        return isValidDate(dateInput) ? normalizeTimezone(dateInput, timezone) : null;
    }

    // 2. 数字类型:时间戳(毫秒或秒)
    if (typeof dateInput === 'number' && !isNaN(dateInput)) {
        // 判断是秒还是毫秒(小于 10 位数通常是秒)
        let timestamp = dateInput;
        if (String(dateInput).length === 10) {
            timestamp = dateInput * 1000;  // 秒转毫秒
        }
        const date = new Date(timestamp);
        return isValidDate(date) ? normalizeTimezone(date, timezone) : null;
    }

    // 3. 字符串类型
    if (typeof dateInput === 'string') {
        const trimmed = dateInput.trim();
        if (trimmed === '') return null;
        
        // 尝试解析各种字符串格式
        let date = tryParseString(trimmed);
        if (date && isValidDate(date)) {
            return normalizeTimezone(date, timezone);
        }
        return null;
    }

    // 4. 对象类型:{ year, month, day, hour, minute, second, millisecond }
    if (dateInput && typeof dateInput === 'object' && !Array.isArray(dateInput)) {
        const { year, y, month, mon, day, d, hour, h, minute, min, second, sec, millisecond, ms } = dateInput;
        const yVal = year || y;
        const mVal = (month !== undefined ? month : mon) - 1; // 月份需要减 1
        const dVal = day || d;
        
        if (yVal !== undefined && mVal !== undefined && dVal !== undefined) {
            const hVal = hour || h || 0;
            const minVal = minute || min || 0;
            const secVal = second || sec || 0;
            const msVal = millisecond || ms || 0;
            
            let date;
            if (timezone === 'utc') {
                date = new Date(Date.UTC(yVal, mVal, dVal, hVal, minVal, secVal, msVal));
            } else {
                date = new Date(yVal, mVal, dVal, hVal, minVal, secVal, msVal);
            }
            return isValidDate(date) ? date : null;
        }
    }

    // 5. 数组类型:[year, month, day, hour, minute, second, millisecond]
    if (Array.isArray(dateInput)) {
        const [year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0] = dateInput;
        let date;
        if (timezone === 'utc') {
            date = new Date(Date.UTC(year, (month || 1) - 1, day || 1, hour, minute, second, millisecond));
        } else {
            date = new Date(year, (month || 1) - 1, day || 1, hour, minute, second, millisecond);
        }
        return isValidDate(date) ? date : null;
    }

    // 无效输入处理
    if (strict) {
        throw new Error(`Invalid date input: ${dateInput}`);
    }
    return defaultDate;
}

/**
 * 检查 Date 对象是否有效
 */
function isValidDate(date) {
    return date instanceof Date && !isNaN(date.getTime());
}

/**
 * 时区归一化
 */
function normalizeTimezone(date, timezone) {
    if (timezone === 'utc') {
        // 返回 UTC 时间字符串对应的 Date(保持原值)
        return new Date(date.toUTCString());
    }
    if (timezone === 'timestamp') {
        // 直接返回时间戳数字
        return date.getTime();
    }
    return date; // 'local' 不做转换
}

/**
 * 尝试解析字符串(处理各种常见格式)
 */
function tryParseString(str) {
    // 常见格式列表
    const formats = [
        // ISO 8601: 2024-01-15T10:30:00.000Z
        (s) => new Date(s),
        
        // YYYY-MM-DD
        (s) => {
            const match = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            if (match) return new Date(match[1], match[2] - 1, match[3]);
            return null;
        },
        
        // YYYY/MM/DD
        (s) => {
            const match = s.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
            if (match) return new Date(match[1], match[2] - 1, match[3]);
            return null;
        },
        
        // DD/MM/YYYY 或 MM/DD/YYYY(需要指定)
        (s) => {
            const match = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
            if (match) {
                // 默认按 MM/DD/YYYY 处理
                return new Date(match[3], match[1] - 1, match[2]);
            }
            return null;
        },
        
        // 中文格式:2024年01月15日
        (s) => {
            const match = s.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
            if (match) return new Date(match[1], match[2] - 1, match[3]);
            return null;
        }
    ];
    
    for (const parser of formats) {
        const result = parser(str);
        if (result && isValidDate(result)) {
            return result;
        }
    }
    return null;
}

2. 辅助格式化函数

/**
 * 将日期格式化为指定字符串
 * @param {Date|string|number} dateInput - 日期输入
 * @param {string} format - 格式模板,如 'YYYY-MM-DD HH:mm:ss'
 */
function formatDate(dateInput, format = 'YYYY-MM-DD HH:mm:ss') {
    const date = normalizeDate(dateInput);
    if (!date) return '';
    
    const pad = (n) => String(n).padStart(2, '0');
    
    const replacements = {
        'YYYY': date.getFullYear(),
        'YY': String(date.getFullYear()).slice(-2),
        'MM': pad(date.getMonth() + 1),
        'M': date.getMonth() + 1,
        'DD': pad(date.getDate()),
        'D': date.getDate(),
        'HH': pad(date.getHours()),
        'H': date.getHours(),
        'hh': pad(date.getHours() % 12 || 12),
        'h': date.getHours() % 12 || 12,
        'mm': pad(date.getMinutes()),
        'm': date.getMinutes(),
        'ss': pad(date.getSeconds()),
        's': date.getSeconds(),
        'SSS': String(date.getMilliseconds()).padStart(3, '0'),
        'A': date.getHours() < 12 ? 'AM' : 'PM',
        'a': date.getHours() < 12 ? 'am' : 'pm'
    };
    
    return format.replace(/YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a/g, (match) => replacements[match]);
}

/**
 * 获取相对时间描述(如“3分钟前”)
 */
function timeAgo(dateInput, lang = 'zh') {
    const date = normalizeDate(dateInput);
    if (!date) return '';
    
    const now = new Date();
    const diff = (now - date) / 1000; // 秒差
    
    const intervals = {
        zh: [
            { limit: 60, unit: '秒前', divisor: 1 },
            { limit: 3600, unit: '分钟前', divisor: 60 },
            { limit: 86400, unit: '小时前', divisor: 3600 },
            { limit: 2592000, unit: '天前', divisor: 86400 },
            { limit: 31536000, unit: '个月前', divisor: 2592000 },
            { limit: Infinity, unit: '年前', divisor: 31536000 }
        ],
        en: [
            { limit: 60, unit: 'seconds ago', divisor: 1 },
            { limit: 3600, unit: 'minutes ago', divisor: 60 },
            { limit: 86400, unit: 'hours ago', divisor: 3600 },
            { limit: 2592000, unit: 'days ago', divisor: 86400 },
            { limit: 31536000, unit: 'months ago', divisor: 2592000 },
            { limit: Infinity, unit: 'years ago', divisor: 31536000 }
        ]
    };
    
    const rules = intervals[lang] || intervals.zh;
    for (const rule of rules) {
        if (diff < rule.limit) {
            const value = Math.floor(diff / rule.divisor);
            return value <= 0 ? (lang === 'zh' ? '刚刚' : 'just now') : `${value} ${rule.unit}`;
        }
    }
    return formatDate(date, lang === 'zh' ? 'YYYY-MM-DD' : 'YYYY-MM-DD');
}

3. 测试用例

// 测试归一化
console.log(normalizeDate('2024-01-15'));           // Date: 2024-01-15
console.log(normalizeDate(1705305600000));          // 时间戳毫秒
console.log(normalizeDate(1705305600));             // 时间戳秒(自动识别)
console.log(normalizeDate({ year: 2024, month: 1, day: 15 }));  // 对象
console.log(normalizeDate([2024, 1, 15, 10, 30]));  // 数组

// 测试格式化
console.log(formatDate('2024-01-15', 'YYYY年MM月DD日'));        // 2024年01月15日
console.log(formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS')); // 当前时间
console.log(formatDate(1705305600000, 'hh:mm A'));              // 12:00 AM

// 测试相对时间
console.log(timeAgo(new Date(Date.now() - 120000)));  // 2分钟前
console.log(timeAgo(new Date(Date.now() - 86400000))); // 1天前

核心设计要点

功能实现方式
类型判断依次检测 Date、number、string、object、array
时间戳识别根据位数(10位秒/13位毫秒)自动转换
字符串解析支持 ISO、YYYY-MM-DD、中文格式等
时区处理local / utc / timestamp 三种模式
错误处理严格模式抛错 / 宽松模式返回 null/默认值
格式模板正则替换实现自定义格式