程式開發日期處理完整指南|JavaScript、Python、Java 實戰與最佳實踐【2025】

程式開發日期處理完整指南|JavaScript、Python、Java 實戰與最佳實踐【2025】

日期處理常見陷阱圖解,包含時區、閏年、夏令時等問題的視覺化說明
日期處理常見陷阱圖解,包含時區、閏年、夏令時等問題的視覺化說明

引言:為什麼日期處理是開發者的惡夢?

「為什麼我的程式在本地正常,部署後日期就錯了?」

「時區轉換為什麼這麼複雜?」

「Date 物件的 getMonth() 為什麼從 0 開始?」

根據 Stack Overflow 2024 年統計,日期處理相關問題佔所有程式問題的 8%,是最常見的 Bug 來源之一。從 JavaScript 的反直覺 API,到 Python 的時區處理,再到跨語言的數據交換,日期處理充滿陷阱。

本文將用最實戰的方式,解析 6 種主流程式語言(JavaScript、Python、Java、PHP、C#、Go)的日期處理方法,搭配 30 個實際範例、時區處理技巧、API 設計最佳實踐,幫助開發者徹底掌握日期處理,寫出正確、高效、可維護的程式碼。

插圖 1:開發者編寫日期處理程式情境

場景描述: 一位軟體工程師(28-35 歲亞洲男性)坐在辦公桌前,面對雙螢幕設置,左邊螢幕顯示程式碼編輯器(VS Code 或類似 IDE)正在撰寫日期處理函數,右邊螢幕顯示瀏覽器開發者工具的 Console,顯示日期輸出結果。桌上有機械鍵盤、滑鼠、咖啡杯、筆記本,背景是典型的開發者工作環境,有程式語言參考書和公仔。自然光與螢幕光照明。

視覺重點:
- 前景:雙螢幕(程式碼清晰可見)、機械鍵盤
- 中景:工程師正在打字、咖啡杯、筆記本
- 背景:辦公桌環境、書架、裝飾(虛化)

必須出現的元素:
- 28-35 歲亞洲男性軟體工程師(短髮或中短髮,穿 T-shirt 或襯衫)
- 雙螢幕設置(兩個 24-27 吋螢幕,橫向排列)
- 左螢幕:程式碼編輯器(VS Code 或 IntelliJ IDEA 風格)
- 程式碼內容(JavaScript 或 Python 日期處理函數)
- 右螢幕:瀏覽器或終端機視窗
- Console 輸出(顯示日期字串或物件)
- 機械鍵盤(Cherry MX 或類似,RGB 燈光可選)
- 滑鼠(電競或工作用滑鼠)
- 咖啡杯(深色或黑色杯子)
- 筆記本(A5 或 B5 尺寸,攤開,有手寫筆記)
- 辦公桌(深色木質或白色桌面)
- 辦公椅(電競椅或人體工學椅,部分可見)
- 書架或置物架(背景,有程式語言書籍)
- 技術公仔或裝飾品(如 GitHub Octocat、Python logo 等)
- 窗戶或燈光(自然光或暖色燈光)

需要顯示的中文字:
- 左螢幕程式碼編輯器標題列:「date_calculator.js - Visual Studio Code」(使用系統標準字體,12pt,深色主題)
- 程式碼內容範例(使用 Consolas 或 Fira Code 字體,14pt,語法高亮顏色):
javascript function calculateDaysBetween(date1, date2) { const diffTime = Math.abs(date2 - date1); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }
- 函數名稱:calculateDaysBetween(黃色或金色,函數語法顏色)
- 變數名稱:diffTimediffDays(藍色或青色,變數語法顏色)
- 註解(若有):// 計算兩日期間隔天數(綠色或灰色,註解顏色)
- 右螢幕 Console 輸出:
- 提示符號:>(白色或灰色)
- 輸出結果:calculateDaysBetween(new Date('2025-01-01'), new Date('2025-12-31'))(白色)
- 結果值:364(藍色或綠色,數字顏色)
- 筆記本上的手寫文字:「Date API」、「時區處理」、「UTC」(手寫風格,黑色原子筆)

顯示圖片/人物風格: 真實攝影風格,專業的軟體工程師工作場景,展現真實的程式開發環境和日期處理編程情境。光線自然,螢幕內容清晰可辨。

顏色調性: 開發者深色主題(VS Code Dark)、螢幕藍光、溫暖辦公照明

避免元素: 卡通圖示、對話框、箭頭特效、過度誇張的表情、過於明亮的背景、多人出現、會議室場景

Slug: developer-coding-date-processing-scene


各程式語言日期處理庫對比信息圖,包含JavaScript、Python、Java等的最佳實踐庫
各程式語言日期處理庫對比信息圖,包含JavaScript、Python、Java等的最佳實踐庫

要點 1-JavaScript 日期處理(最常用語言)

基礎:Date 物件與陷阱

創建 Date 物件的 5 種方法

// 方法 1:當前時間
const now = new Date();

// 方法 2:時間戳(毫秒)
const fromTimestamp = new Date(1706342400000);

// 方法 3:日期字串(ISO 8601)
const fromString = new Date('2025-01-27');

// 方法 4:指定年月日
const fromNumbers = new Date(2025, 0, 27); // 注意:月份從 0 開始!

// 方法 5:UTC 時間
const fromUTC = new Date(Date.UTC(2025, 0, 27, 12, 0, 0));

⚠️ 最常見陷阱:月份從 0 開始

// ❌ 錯誤:以為是 2025 年 1 月 27 日
new Date(2025, 1, 27); // 實際是 2 月 27 日!

// ✅ 正確:1 月要用 0
new Date(2025, 0, 27); // 1 月 27 日

// 記憶口訣:
// 0=Jan, 1=Feb, 2=Mar, ..., 11=Dec

核心操作:日期計算實戰

範例 1:計算兩日期間隔天數

function daysBetween(date1, date2) {
  // 轉換為時間戳(毫秒)
  const time1 = date1.getTime();
  const time2 = date2.getTime();

  // 計算差異
  const diffMs = Math.abs(time2 - time1);

  // 轉換為天數(1 天 = 24*60*60*1000 毫秒)
  const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));

  return diffDays;
}

// 使用範例
const start = new Date('2025-01-01');
const end = new Date('2025-12-31');
console.log(daysBetween(start, end)); // 364

範例 2:日期加減運算

// 加 N 天
function addDays(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

// 加 N 個月(處理月底邊界)
function addMonths(date, months) {
  const result = new Date(date);
  const day = result.getDate();

  result.setMonth(result.getMonth() + months);

  // 處理月底邊界(如 1/31 + 1 月 = 2/28)
  if (result.getDate() !== day) {
    result.setDate(0); // 設為上個月最後一天
  }

  return result;
}

// 使用範例
const today = new Date('2025-01-27');
console.log(addDays(today, 10));   // 2025-02-06
console.log(addMonths(today, 2));  // 2025-03-27

範例 3:判斷是否為工作日

function isWorkday(date) {
  const day = date.getDay(); // 0=週日, 6=週六
  return day !== 0 && day !== 6;
}

// 計算工作日數量(不考慮國定假日)
function countWorkdays(startDate, endDate) {
  let count = 0;
  const current = new Date(startDate);

  while (current <= endDate) {
    if (isWorkday(current)) {
      count++;
    }
    current.setDate(current.getDate() + 1);
  }

  return count;
}

// 使用範例
const start = new Date('2025-02-03');
const end = new Date('2025-02-14');
console.log(countWorkdays(start, end)); // 10(不含週末)

進階:時區處理與格式化

時區處理(最大痛點)

// 問題:Date 物件總是本地時區
const date = new Date('2025-01-27T12:00:00');
console.log(date.toString());
// 台灣:Mon Jan 27 2025 12:00:00 GMT+0800
// 美國:Mon Jan 27 2025 12:00:00 GMT-0500

// 解決方案 1:統一使用 UTC
const utcDate = new Date('2025-01-27T12:00:00Z'); // Z = UTC
console.log(utcDate.toISOString()); // 2025-01-27T12:00:00.000Z

// 解決方案 2:使用第三方函式庫
// 推薦:date-fns-tz, Luxon, Day.js

// date-fns-tz 範例
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';

const utc = new Date('2025-01-27T12:00:00Z');
const taipei = utcToZonedTime(utc, 'Asia/Taipei');
console.log(taipei); // 2025-01-27 20:00:00(台北時間)

日期格式化

// 方法 1:原生 Intl.DateTimeFormat
const date = new Date('2025-01-27');
const formatter = new Intl.DateTimeFormat('zh-TW', {
  year: 'numeric',
  month: '2-digit',
  day: '2-digit'
});
console.log(formatter.format(date)); // 2025/01/27

// 方法 2:使用 date-fns(推薦)
import { format } from 'date-fns';
import { zhTW } from 'date-fns/locale';

console.log(format(date, 'yyyy年MM月dd日', { locale: zhTW }));
// 2025年01月27日

console.log(format(date, 'EEEE', { locale: zhTW }));
// 星期一

推薦函式庫比較

函式庫 大小 特色 適用場景
date-fns ~70KB 模組化、樹搖優化 通用推薦 ⭐
Day.js ~2KB 極輕量、類 Moment API 輕量專案
Luxon ~70KB 強大時區支援 國際化專案
Moment.js ~300KB 老牌但已停止維護 不推薦(legacy)

安裝與使用

# date-fns(推薦)
npm install date-fns

# Day.js(輕量)
npm install dayjs

# Luxon(時區強)
npm install luxon

時區處理最佳實踐流程圖,從UTC存儲到本地顯示的完整流程
時區處理最佳實踐流程圖,從UTC存儲到本地顯示的完整流程

要點 2-Python 日期處理(後端首選)

基礎:datetime 模組

核心類別介紹

from datetime import datetime, date, time, timedelta

# 1. date:只有日期(年月日)
today = date.today()
print(today)  # 2025-01-27

# 2. time:只有時間(時分秒)
now_time = time(14, 30, 0)
print(now_time)  # 14:30:00

# 3. datetime:日期 + 時間
now = datetime.now()
print(now)  # 2025-01-27 14:30:00.123456

# 4. timedelta:時間差
diff = timedelta(days=10, hours=5)
future = now + diff
print(future)  # 2025-02-06 19:30:00.123456

核心操作:實戰範例

範例 1:日期解析與格式化

from datetime import datetime

# 解析字串為 datetime
date_str = '2025-01-27 14:30:00'
dt = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
print(dt)  # 2025-01-27 14:30:00

# 格式化 datetime 為字串
formatted = dt.strftime('%Y年%m月%d日 %H:%M')
print(formatted)  # 2025年01月27日 14:30

# 常用格式代碼:
# %Y:4 位數年份(2025)
# %m:2 位數月份(01-12)
# %d:2 位數日期(01-31)
# %H:24 小時制小時(00-23)
# %M:分鐘(00-59)
# %S:秒(00-59)
# %A:星期全名(Monday)
# %B:月份全名(January)

範例 2:日期計算與比較

from datetime import datetime, timedelta

# 計算兩日期間隔
start = datetime(2025, 1, 1)
end = datetime(2025, 12, 31)
diff = end - start
print(f"相差 {diff.days} 天")  # 364

# 日期加減
today = datetime.now()
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
next_month = today + timedelta(days=30)  # 注意:不是精確月份

# 日期比較
if end > start:
    print("end 在 start 之後")  # True

範例 3:工作日計算

from datetime import datetime, timedelta
import numpy as np

def count_workdays(start_date, end_date, holidays=None):
    """計算工作日數量(排除週末和國定假日)"""
    if holidays is None:
        holidays = []

    # 使用 numpy 的 busday_count
    workdays = np.busday_count(
        start_date.date(),
        end_date.date(),
        holidays=[h.date() if isinstance(h, datetime) else h
                  for h in holidays]
    )

    return workdays

# 使用範例
start = datetime(2025, 2, 3)
end = datetime(2025, 2, 14)
holidays = [datetime(2025, 2, 10)]  # 假設 2/10 是假日

workdays = count_workdays(start, end, holidays)
print(f"工作日:{workdays} 天")

進階:時區處理(pytz)

安裝與基本使用

pip install pytz
from datetime import datetime
import pytz

# 創建時區感知的 datetime
taipei_tz = pytz.timezone('Asia/Taipei')
now_taipei = datetime.now(taipei_tz)
print(now_taipei)  # 2025-01-27 14:30:00+08:00

# UTC 與本地時區轉換
utc_tz = pytz.utc
now_utc = datetime.now(utc_tz)
print(now_utc)  # 2025-01-27 06:30:00+00:00

# 時區轉換
ny_tz = pytz.timezone('America/New_York')
now_ny = now_taipei.astimezone(ny_tz)
print(now_ny)  # 2025-01-27 01:30:00-05:00

# 常見時區:
# Asia/Taipei:台北
# Asia/Tokyo:東京
# Asia/Shanghai:上海
# America/New_York:紐約
# America/Los_Angeles:洛杉磯
# Europe/London:倫敦

避免常見錯誤

import pytz
from datetime import datetime

# ❌ 錯誤:直接使用 replace
tz = pytz.timezone('Asia/Taipei')
dt = datetime(2025, 1, 27, 14, 30, 0)
wrong = dt.replace(tzinfo=tz)  # 不會正確處理夏令時間

# ✅ 正確:使用 localize
correct = tz.localize(dt)

# ✅ 或使用 datetime.now(tz)
now = datetime.now(tz)

要點 3-Java 日期處理(企業級應用)

現代 API:java.time(Java 8+)

核心類別

import java.time.*;
import java.time.format.DateTimeFormatter;

// LocalDate:只有日期
LocalDate today = LocalDate.now();
LocalDate specificDate = LocalDate.of(2025, 1, 27);

// LocalTime:只有時間
LocalTime now = LocalTime.now();
LocalTime specificTime = LocalTime.of(14, 30, 0);

// LocalDateTime:日期 + 時間(無時區)
LocalDateTime dateTime = LocalDateTime.now();

// ZonedDateTime:日期 + 時間 + 時區
ZonedDateTime taipeiTime = ZonedDateTime.now(ZoneId.of("Asia/Taipei"));

實戰範例

範例 1:日期計算

import java.time.*;
import java.time.temporal.ChronoUnit;

public class DateCalculator {
    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2025, 1, 1);
        LocalDate end = LocalDate.of(2025, 12, 31);

        // 計算間隔天數
        long days = ChronoUnit.DAYS.between(start, end);
        System.out.println("相差 " + days + " 天");  // 364

        // 日期加減
        LocalDate nextWeek = start.plusDays(7);
        LocalDate nextMonth = start.plusMonths(1);
        LocalDate nextYear = start.plusYears(1);

        // 判斷是否為閏年
        boolean isLeap = start.isLeapYear();
        System.out.println("2025 是閏年:" + isLeap);  // false
    }
}

範例 2:格式化與解析

import java.time.*;
import java.time.format.DateTimeFormatter;

public class DateFormatting {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2025, 1, 27);

        // 格式化為字串
        DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String formatted1 = date.format(formatter1);
        System.out.println(formatted1);  // 2025-01-27

        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
        String formatted2 = date.format(formatter2);
        System.out.println(formatted2);  // 2025年01月27日

        // 解析字串為日期
        String dateStr = "2025-01-27";
        LocalDate parsed = LocalDate.parse(dateStr, formatter1);
        System.out.println(parsed);
    }
}

範例 3:工作日計算

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.HashSet;

public class WorkdayCalculator {
    public static long countWorkdays(LocalDate start, LocalDate end,
                                     Set<LocalDate> holidays) {
        long workdays = 0;
        LocalDate current = start;

        while (!current.isAfter(end)) {
            DayOfWeek day = current.getDayOfWeek();

            // 排除週末和假日
            if (day != DayOfWeek.SATURDAY &&
                day != DayOfWeek.SUNDAY &&
                !holidays.contains(current)) {
                workdays++;
            }

            current = current.plusDays(1);
        }

        return workdays;
    }

    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2025, 2, 3);
        LocalDate end = LocalDate.of(2025, 2, 14);

        Set<LocalDate> holidays = new HashSet<>();
        holidays.add(LocalDate.of(2025, 2, 10)); // 假日

        long workdays = countWorkdays(start, end, holidays);
        System.out.println("工作日:" + workdays + " 天");
    }
}

要點 4-其他語言快速參考

PHP 日期處理

基本操作

<?php
// 創建日期
$now = new DateTime();
$specific = new DateTime('2025-01-27');

// 日期計算
$start = new DateTime('2025-01-01');
$end = new DateTime('2025-12-31');
$diff = $start->diff($end);
echo $diff->days . " 天\n";  // 364

// 日期加減
$start->add(new DateInterval('P10D'));  // 加 10 天
$start->sub(new DateInterval('P1M'));   // 減 1 個月

// 格式化
echo $now->format('Y-m-d H:i:s');  // 2025-01-27 14:30:00

// 時區處理
$taipei = new DateTimeZone('Asia/Taipei');
$date = new DateTime('now', $taipei);
echo $date->format('Y-m-d H:i:s T');  // 2025-01-27 14:30:00 CST
?>

C# 日期處理

基本操作

using System;

class DateExample {
    static void Main() {
        // 創建日期
        DateTime now = DateTime.Now;
        DateTime specific = new DateTime(2025, 1, 27);

        // 日期計算
        DateTime start = new DateTime(2025, 1, 1);
        DateTime end = new DateTime(2025, 12, 31);
        TimeSpan diff = end - start;
        Console.WriteLine($"相差 {diff.Days} 天");  // 364

        // 日期加減
        DateTime nextWeek = start.AddDays(7);
        DateTime nextMonth = start.AddMonths(1);

        // 格式化
        string formatted = now.ToString("yyyy-MM-dd HH:mm:ss");
        Console.WriteLine(formatted);  // 2025-01-27 14:30:00

        // 時區處理
        TimeZoneInfo taipeiZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
        DateTime taipeiTime = TimeZoneInfo.ConvertTime(now, taipeiZone);
    }
}

Go 日期處理

基本操作

package main

import (
    "fmt"
    "time"
)

func main() {
    // 創建日期
    now := time.Now()
    specific := time.Date(2025, 1, 27, 14, 30, 0, 0, time.UTC)

    // 日期計算
    start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
    end := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)
    diff := end.Sub(start)
    fmt.Printf("相差 %.0f 天\n", diff.Hours()/24)  // 364

    // 日期加減
    nextWeek := start.AddDate(0, 0, 7)
    nextMonth := start.AddDate(0, 1, 0)

    // 格式化(使用 Go 獨特的參考時間)
    formatted := now.Format("2006-01-02 15:04:05")
    fmt.Println(formatted)  // 2025-01-27 14:30:00

    // 時區處理
    taipei, _ := time.LoadLocation("Asia/Taipei")
    taipeiTime := now.In(taipei)
    fmt.Println(taipeiTime)
}

要點 5-API 設計最佳實踐

原則 1:統一使用 ISO 8601 格式

推薦格式

日期:2025-01-27
日期時間:2025-01-27T14:30:00Z
日期時間(時區):2025-01-27T14:30:00+08:00

API 範例

// ✅ 正確:使用 ISO 8601
{
  "startDate": "2025-01-27",
  "endDate": "2025-12-31",
  "createdAt": "2025-01-27T14:30:00Z"
}

// ❌ 錯誤:使用不明確格式
{
  "startDate": "01/27/2025",  // 月/日/年?日/月/年?
  "endDate": "2025.12.31",
  "createdAt": "1706342400"   // 時間戳?秒還是毫秒?
}

原則 2:明確指定時區

後端 API 設計

from flask import Flask, jsonify
from datetime import datetime
import pytz

app = Flask(__name__)

@app.route('/api/events')
def get_events():
    # ✅ 正確:明確使用 UTC
    utc_now = datetime.now(pytz.utc)

    return jsonify({
        'timestamp': utc_now.isoformat(),  # 2025-01-27T06:30:00+00:00
        'timezone': 'UTC'
    })

@app.route('/api/local-time')
def get_local_time():
    # ✅ 正確:明確標示時區
    taipei_tz = pytz.timezone('Asia/Taipei')
    local_now = datetime.now(taipei_tz)

    return jsonify({
        'timestamp': local_now.isoformat(),  # 2025-01-27T14:30:00+08:00
        'timezone': 'Asia/Taipei'
    })

前端處理

// ✅ 正確:接收 UTC,轉換為本地顯示
fetch('/api/events')
  .then(res => res.json())
  .then(data => {
    const utcDate = new Date(data.timestamp);
    const localString = utcDate.toLocaleString('zh-TW', {
      timeZone: 'Asia/Taipei'
    });
    console.log(localString);  // 2025/1/27 下午2:30:00
  });

原則 3:提供多種查詢方式

靈活的 API 設計

@app.route('/api/date-range', methods=['GET'])
def date_range():
    # 支援多種輸入方式
    start = request.args.get('start')  # 2025-01-27
    end = request.args.get('end')      # 2025-12-31

    # 或使用相對時間
    days = request.args.get('days')    # last_7_days, next_30_days

    if days:
        if days == 'last_7_days':
            end = datetime.now()
            start = end - timedelta(days=7)
        elif days == 'next_30_days':
            start = datetime.now()
            end = start + timedelta(days=30)

    # 返回計算結果
    diff = (end - start).days

    return jsonify({
        'start': start.isoformat(),
        'end': end.isoformat(),
        'days': diff
    })

原則 4:錯誤處理與驗證

完整的輸入驗證

function validateDateInput(dateStr) {
  // 檢查格式(YYYY-MM-DD)
  const regex = /^\d{4}-\d{2}-\d{2}$/;
  if (!regex.test(dateStr)) {
    throw new Error('日期格式錯誤,應為 YYYY-MM-DD');
  }

  // 檢查日期有效性
  const date = new Date(dateStr);
  if (isNaN(date.getTime())) {
    throw new Error('無效的日期');
  }

  // 檢查合理範圍(例如:1900-2100)
  const year = date.getFullYear();
  if (year < 1900 || year > 2100) {
    throw new Error('日期超出合理範圍');
  }

  return date;
}

// API 使用
app.post('/api/calculate', (req, res) => {
  try {
    const start = validateDateInput(req.body.startDate);
    const end = validateDateInput(req.body.endDate);

    if (end < start) {
      return res.status(400).json({
        error: '結束日期不能早於開始日期'
      });
    }

    // 處理計算...

  } catch (error) {
    return res.status(400).json({
      error: error.message
    });
  }
});

要點 6-常見陷阱與解決方案

陷阱 1:時區混亂

問題描述

// 本地開發(台北時間 UTC+8)
const date = new Date('2025-01-27T00:00:00');
console.log(date.toISOString());
// 2025-01-26T16:00:00.000Z(前一天!)

// 部署到美國伺服器(UTC-5)
// 同樣的程式碼,結果變成 2025-01-27T05:00:00.000Z

解決方案

// ✅ 方案 1:統一使用 UTC
const utcDate = new Date('2025-01-27T00:00:00Z'); // Z 表示 UTC
console.log(utcDate.toISOString());
// 2025-01-27T00:00:00.000Z(一致)

// ✅ 方案 2:使用 date-fns-tz
import { utcToZonedTime } from 'date-fns-tz';
const utc = new Date('2025-01-27T00:00:00Z');
const taipei = utcToZonedTime(utc, 'Asia/Taipei');

// ✅ 方案 3:後端統一 UTC,前端顯示時轉換
// 資料庫存 UTC → API 回傳 UTC → 前端轉本地時區

陷阱 2:夏令時間(DST)

問題描述

// 美國 2025 年 3 月 9 日凌晨 2:00 進入夏令時間
const before = new Date('2025-03-09T01:00:00'); // 1:00 AM
const after = new Date('2025-03-09T03:00:00');  // 3:00 AM(跳過 2:00)

const diff = (after - before) / (1000 * 60 * 60);
console.log(diff); // 1 小時(不是 2 小時!)

解決方案

// ✅ 使用時區感知的函式庫
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';

const tz = 'America/New_York';
const date1 = zonedTimeToUtc('2025-03-09 01:00:00', tz);
const date2 = zonedTimeToUtc('2025-03-09 03:00:00', tz);

// 函式庫會自動處理夏令時間

陷阱 3:月份越界

問題描述

// 1 月 31 日加 1 個月
const jan31 = new Date(2025, 0, 31);
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31);
// 2025-03-03(跳到 3 月 3 日!)

// 原因:2 月只有 28 天,31 天溢出到 3 月

解決方案

// ✅ 方案 1:使用 date-fns 的 addMonths
import { addMonths } from 'date-fns';
const jan31 = new Date(2025, 0, 31);
const result = addMonths(jan31, 1);
console.log(result); // 2025-02-28(月底)

// ✅ 方案 2:手動處理邊界
function addMonthsSafe(date, months) {
  const result = new Date(date);
  const day = result.getDate();
  result.setMonth(result.getMonth() + months);

  // 如果日期改變(溢出),設為該月最後一天
  if (result.getDate() !== day) {
    result.setDate(0); // 上個月最後一天
  }

  return result;
}

陷阱 4:閏秒(Leap Second)

問題描述

UTC 時間偶爾會插入閏秒來校正地球自轉
2016-12-31 23:59:60 UTC(多出 1 秒)

影響
- 大部分程式語言忽略閏秒
- 高精度應用需注意(金融、科學計算)

解決方案

# Python:datetime 不處理閏秒
# 需要使用專門函式庫:astropy

from astropy.time import Time

# 包含閏秒的時間計算
t1 = Time('2016-12-31T23:59:59', format='isot', scale='utc')
t2 = Time('2017-01-01T00:00:01', format='isot', scale='utc')
diff = (t2 - t1).sec
print(diff)  # 3 秒(包含閏秒)

陷阱 5:資料庫日期類型

問題描述

-- MySQL: DATETIME vs TIMESTAMP
CREATE TABLE events (
  created_datetime DATETIME,    -- 不帶時區,按字面值儲存
  created_timestamp TIMESTAMP   -- 帶時區,自動轉換 UTC
);

-- 插入同樣的時間
INSERT INTO events VALUES
  ('2025-01-27 14:30:00', '2025-01-27 14:30:00');

-- 查詢時可能不同(取決於伺服器時區設定)

最佳實踐

-- ✅ 推薦:統一使用 TIMESTAMP 或 TIMESTAMPTZ(PostgreSQL)
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ✅ 或明確儲存 UTC
INSERT INTO events (created_at)
VALUES (CURRENT_TIMESTAMP AT TIME ZONE 'UTC');

要點 7-效能優化技巧

技巧 1:避免重複創建 Date 物件

❌ 低效寫法

function processEvents(events) {
  return events.map(event => {
    // 每次迭代都創建新物件
    const now = new Date();
    const eventDate = new Date(event.date);
    return eventDate < now;
  });
}

✅ 優化寫法

function processEvents(events) {
  // 只創建一次
  const now = new Date();

  return events.map(event => {
    const eventDate = new Date(event.date);
    return eventDate < now;
  });
}

技巧 2:使用時間戳加速比較

✅ 高效比較

// 預先轉換為時間戳(數字)
const events = rawEvents.map(e => ({
  ...e,
  timestamp: new Date(e.date).getTime()
}));

// 比較時使用數字(更快)
const now = Date.now();
const futureEvents = events.filter(e => e.timestamp > now);

技巧 3:批次處理日期運算

❌ 低效:逐一計算

dates = []
for i in range(365):
    dates.append(start_date + timedelta(days=i))

✅ 高效:使用 pandas

import pandas as pd

# 使用 pandas 生成日期範圍(C 語言加速)
dates = pd.date_range(start='2025-01-01', end='2025-12-31', freq='D')
# 比 Python 迴圈快 10-100 倍

技巧 4:快取假日資料

✅ 快取策略

// 假日資料快取(避免重複計算)
class HolidayCache {
  constructor() {
    this.cache = new Map();
  }

  getHolidays(year) {
    if (!this.cache.has(year)) {
      // 只計算一次
      const holidays = this.calculateHolidays(year);
      this.cache.set(year, holidays);
    }
    return this.cache.get(year);
  }

  calculateHolidays(year) {
    // 計算該年度所有假日
    return [
      new Date(year, 0, 1),  // 元旦
      // ... 其他假日
    ];
  }
}

const cache = new HolidayCache();
const holidays2025 = cache.getHolidays(2025); // 第一次計算
const holidays2025Again = cache.getHolidays(2025); // 直接返回快取

要點 8-測試策略

單元測試範例

JavaScript(Jest)

import { daysBetween, addMonths } from './dateUtils';

describe('日期工具函數', () => {
  test('計算兩日期間隔', () => {
    const start = new Date('2025-01-01');
    const end = new Date('2025-12-31');
    expect(daysBetween(start, end)).toBe(364);
  });

  test('日期加月份', () => {
    const date = new Date('2025-01-31');
    const result = addMonths(date, 1);
    expect(result.getMonth()).toBe(1); // 2 月(0-based)
    expect(result.getDate()).toBe(28); // 2 月最後一天
  });

  test('跨年計算', () => {
    const start = new Date('2024-12-25');
    const end = new Date('2025-01-05');
    expect(daysBetween(start, end)).toBe(11);
  });
});

邊界測試案例

必須測試的邊界

describe('邊界測試', () => {
  test('閏年 2 月 29 日', () => {
    const leap = new Date('2024-02-29');
    expect(leap.getDate()).toBe(29);
  });

  test('月份邊界(1 月 31 日 + 1 個月)', () => {
    const jan31 = new Date('2025-01-31');
    const result = addMonths(jan31, 1);
    expect(result.getMonth()).toBe(1); // 2 月
  });

  test('年份邊界(12 月 31 日 + 1 天)', () => {
    const dec31 = new Date('2025-12-31');
    const result = addDays(dec31, 1);
    expect(result.getFullYear()).toBe(2026);
    expect(result.getMonth()).toBe(0); // 1 月
  });

  test('負數天數(往回算)', () => {
    const date = new Date('2025-01-10');
    const result = addDays(date, -15);
    expect(result.getFullYear()).toBe(2024);
    expect(result.getMonth()).toBe(11); // 12 月
  });
});

要點 9-實戰專案範例

專案:日期計算 API 服務

完整 Node.js 範例

const express = require('express');
const { addDays, differenceInDays, format } = require('date-fns');

const app = express();
app.use(express.json());

// API 1:計算兩日期間隔
app.post('/api/date-diff', (req, res) => {
  try {
    const { startDate, endDate } = req.body;

    // 驗證輸入
    if (!startDate || !endDate) {
      return res.status(400).json({
        error: '缺少必要參數'
      });
    }

    const start = new Date(startDate);
    const end = new Date(endDate);

    if (isNaN(start) || isNaN(end)) {
      return res.status(400).json({
        error: '無效的日期格式'
      });
    }

    // 計算間隔
    const days = differenceInDays(end, start);

    res.json({
      startDate,
      endDate,
      days,
      weeks: Math.floor(days / 7),
      months: Math.floor(days / 30)
    });

  } catch (error) {
    res.status(500).json({
      error: error.message
    });
  }
});

// API 2:日期加減
app.post('/api/date-add', (req, res) => {
  try {
    const { date, days } = req.body;

    if (!date || days === undefined) {
      return res.status(400).json({
        error: '缺少必要參數'
      });
    }

    const baseDate = new Date(date);
    const result = addDays(baseDate, parseInt(days));

    res.json({
      original: date,
      days: parseInt(days),
      result: format(result, 'yyyy-MM-dd'),
      dayOfWeek: format(result, 'EEEE')
    });

  } catch (error) {
    res.status(500).json({
      error: error.message
    });
  }
});

app.listen(3000, () => {
  console.log('API 服務運行於 http://localhost:3000');
});

要點 10-開發者檢查清單

設計階段

日期處理設計檢查
- [ ] 是否需要支援多時區?
- [ ] 資料庫儲存格式(UTC 或本地)?
- [ ] API 輸入/輸出格式(ISO 8601)?
- [ ] 是否需要處理歷史日期(< 1970)?
- [ ] 是否需要處理未來日期(> 2100)?
- [ ] 精確度需求(秒?毫秒?)
- [ ] 是否需要處理夏令時間?
- [ ] 國際化需求(多語言日期格式)?


實作階段

程式碼品質檢查
- [ ] 使用現代 API(不使用已棄用方法)
- [ ] 統一使用 UTC 或明確標示時區
- [ ] 輸入驗證完整(格式、範圍)
- [ ] 邊界案例處理(閏年、月底)
- [ ] 錯誤處理清楚(try-catch)
- [ ] 使用成熟函式庫(date-fns、Luxon)
- [ ] 避免魔術數字(10006060*24)
- [ ] 註解清楚說明時區假設


測試階段

測試覆蓋檢查
- [ ] 基本功能測試
- [ ] 邊界測試(閏年、月底、年底)
- [ ] 時區測試(至少 2-3 個時區)
- [ ] 夏令時間測試(如適用)
- [ ] 負數測試(往回計算)
- [ ] 大數值測試(100 年後)
- [ ] 效能測試(大量計算)
- [ ] 整合測試(API 端到端)


結論:掌握日期處理,寫出可靠程式碼

3 個核心原則

原則 1:統一使用標準格式
- ISO 8601(YYYY-MM-DD)
- 資料庫統一 UTC
- API 明確時區

原則 2:選擇合適工具
- JavaScript:date-fns(推薦)
- Python:datetime + pytz
- Java:java.time(Java 8+)
- 避免自己造輪子

原則 3:完整測試邊界
- 閏年、月底、年底
- 時區轉換
- 夏令時間
- 負數和大數值

推薦資源

官方文檔
- MDN Date
- Python datetime
- Java Time API

函式庫
- date-fns - JavaScript 推薦
- Luxon - 強大時區支援
- pytz - Python 時區

工具
- 日期計算器 - 快速驗證
- 工作日計算 - 工作日處理
- 時區轉換器 - 時區測試

延伸閱讀
- 日期計算器完整指南 - 線上工具、Excel 公式與 12 種實用場景
- Excel 日期計算公式大全 - DATEDIF、EDATE、NETWORKDAYS 完整教學
- 工作日計算完整攻略 - 排除假日、NETWORKDAYS 函數進階應用
- 線上日期計算工具完整評比 - 10 款熱門工具實測與選擇指南

插圖 2:開發環境多螢幕設置

場景描述: 特寫開發者的工作桌面,三個螢幕並排(超寬螢幕設置),左螢幕顯示 API 文檔頁面,中間螢幕顯示程式碼編輯器(正在編寫日期處理測試案例),右螢幕顯示終端機正在執行測試。桌面前方有機械鍵盤、滑鼠、咖啡杯,桌面乾淨整齊。俯視 30 度角拍攝,強調專業開發環境。

視覺重點:
- 前景:三個螢幕(內容清晰可見)
- 中景:鍵盤、滑鼠、咖啡杯
- 背景:桌面邊緣、部分可見的辦公環境

必須出現的元素:
- 三個螢幕(24-27 吋,橫向排列或一個超寬螢幕)
- 左螢幕:瀏覽器顯示技術文檔(date-fns 或 MDN)
- 中間螢幕:程式碼編輯器(VS Code 深色主題)
- 測試檔案內容(Jest 或 Mocha 測試語法)
- 右螢幕:終端機視窗(黑底綠字或黑底白字)
- 測試執行輸出(通過的測試顯示綠色勾選符號)
- 機械鍵盤(全尺寸或 TKL,RGB 燈光)
- 無線或有線滑鼠
- 咖啡杯(深色或黑色,簡約設計)
- 木質或白色桌面
- 桌面乾淨整齊(無雜物)
- 顯示器支架或底座

需要顯示的中文字:
- 左螢幕文檔頁面標題:「date-fns 官方文檔」(使用 Sans-serif 字體,18pt,深藍色或黑色)
- 文檔內容標題:「format - 日期格式化函數」(14pt,黑色)
- 中間螢幕檔案名稱:「dateUtils.test.js」(標籤列,12pt)
- 測試程式碼範例(Consolas 或 Fira Code,14pt,語法高亮):
javascript describe('日期計算測試', () => { test('計算兩日期間隔', () => { expect(daysBetween(start, end)).toBe(364); }); });
- 函數名稱:describetest(紫色或粉紅色,關鍵字顏色)
- 字串:'日期計算測試'(橘色或棕色,字串顏色)
- 右螢幕終端機提示:$ npm test(綠色或白色)
- 測試輸出:
- PASS dateUtils.test.js(綠色,粗體)
- ✓ 計算兩日期間隔 (3ms)(綠色,勾選符號)
- Test Suites: 1 passed, 1 total(綠色)
- Tests: 10 passed, 10 total(綠色)

顯示圖片/人物風格: 真實攝影風格,專業開發者的多螢幕工作環境,俯視角度展現完整桌面布局,螢幕內容清晰可讀,展現真實的測試驅動開發(TDD)工作流程。

顏色調性: 開發者深色主題、終端機綠、測試通過綠

避免元素: 人物出現、手部特寫、卡通圖示、過多雜物、混亂的桌面、過度鮮豔的 RGB 燈光

Slug: developer-multi-monitor-testing-environment


參考資料

  1. MDN Web Docs「Date」官方文檔(2025 年版)
  2. Python 官方文檔「datetime」模組說明
  3. Oracle Java SE 文檔「java.time」套件
  4. IETF RFC 3339「Date and Time on the Internet」標準
  5. ISO 8601「Data elements and interchange formats」國際標準
  6. Stack Overflow 日期處理問題統計分析(2024)
  7. date-fns、Luxon、Day.js 官方技術文檔
  8. 各語言官方時區資料庫(IANA Time Zone Database)

延伸閱讀

完整指南系列
- 日期計算器完整指南 - 線上工具、Excel 公式與 12 種實用場景
- Excel 日期計算公式大全 - DATEDIF、EDATE、NETWORKDAYS 完整教學與 50+ 範例
- 工作日計算完整攻略 - 排除假日、NETWORKDAYS 函數進階與專案排程實戰

實務應用場景
- 離職預告期日期計算全攻略 - 勞基法規定、預告期對照表與實務案例
- 育嬰留停日期計算完整攻略 - 2025 最新法規、Excel 公式與系統開發參考

工具評測
- 線上日期計算工具完整評比 - 10 款熱門工具實測、功能比較與隱私分析


推薦工具

開發輔助工具
- 日期計算器 - 快速驗證計算結果
- 工作日計算器 - 測試工作日邏輯
- 日期間隔計算 - 驗證複雜區間

技術文檔
- 日期計算器技術實作 - 前端實作範例
- 工作日計算技術實作 - 後端演算法

相關工具分類
- 更多開發工具 - 開發者必備工具集
- 更多生活工具 - 終端使用者工具


常見問題(FAQ)

Q1:JavaScript 的 Date 為什麼這麼難用?

:歷史包袱。JavaScript Date 設計於 1995 年,參考 Java 1.0 的 Date 類別(後來 Java 也棄用了)。

主要問題
- 月份從 0 開始(0=1月,11=12月)
- getYear() 回傳「年份 - 1900」
- 沒有時區支援
- API 不一致

建議
- 使用 date-fns 或 Luxon 取代原生 API
- 或等待 Temporal API(提案階段)


Q2:如何選擇 JavaScript 日期函式庫?

:根據需求選擇:

date-fns(推薦大多數情況):
- ✅ 模組化,樹搖優化
- ✅ 函數式風格,不可變
- ✅ TypeScript 支援完整
- 適合:通用專案

Day.js(輕量專案):
- ✅ 僅 2KB
- ✅ API 類似 Moment.js
- 適合:對大小敏感的專案

Luxon(國際化專案):
- ✅ 強大時區支援
- ✅ 區間計算完整
- 適合:多時區應用


Q3:Python datetime 和 date 有什麼差別?

:精確度不同:

date:只有日期

from datetime import date
d = date(2025, 1, 27)
print(d)  # 2025-01-27

datetime:日期 + 時間

from datetime import datetime
dt = datetime(2025, 1, 27, 14, 30, 0)
print(dt)  # 2025-01-27 14:30:00

選擇建議
- 只需要日期(生日、截止日)→ date
- 需要精確時間(事件記錄)→ datetime
- 大多數情況用 datetime 更安全


Q4:時區處理為什麼這麼複雜?

:因為現實世界複雜:

複雜因素
1. 夏令時間(DST)
- 不同國家實施時間不同
- 有些國家不實施

  1. 時區邊界變動
  2. 政治因素調整
  3. 例如:北韓 2015 年改時區

  4. 歷史時區資料

  5. 過去的時區規則不同
  6. 需要 IANA 時區資料庫

最佳實踐
- 使用成熟的時區函式庫
- 資料庫統一儲存 UTC
- 顯示時才轉本地時區


Q5:如何在前後端之間傳遞日期?

:統一使用 ISO 8601 格式:

後端(Python)

from datetime import datetime
import json

dt = datetime.now()
# 轉換為 ISO 8601
iso_string = dt.isoformat()
return json.dumps({'date': iso_string})
# {"date": "2025-01-27T14:30:00"}

前端(JavaScript)

fetch('/api/data')
  .then(res => res.json())
  .then(data => {
    // 解析 ISO 8601
    const date = new Date(data.date);
    console.log(date);
  });

優點
- 標準格式,所有語言都支援
- 包含時區資訊
- 無歧義


Q6:閏年怎麼判斷?

:規則有三個條件:

判斷規則
1. 能被 4 整除
2. 能被 100 整除的不是(例如:1900)
3. 除非能被 400 整除(例如:2000)

實作

function isLeapYear(year) {
  return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}

console.log(isLeapYear(2024)); // true(能被 4 整除)
console.log(isLeapYear(1900)); // false(能被 100 整除但不能被 400 整除)
console.log(isLeapYear(2000)); // true(能被 400 整除)

直接使用函式庫

import { isLeapYear } from 'date-fns';
console.log(isLeapYear(new Date(2024, 0, 1))); // true

Q7:為什麼不建議使用 Moment.js?

:官方已宣布停止開發(進入維護模式):

問題
- ❌ 體積大(~300KB)
- ❌ 可變物件(容易出 Bug)
- ❌ 不支援樹搖優化
- ❌ 停止新功能開發

替代方案
- date-fns(推薦)
- Luxon(Moment 團隊開發)
- Day.js(最輕量)

遷移建議

# 使用官方遷移工具
npx moment-fns-migration

Q8:如何處理不同語言的日期格式?

:使用 Intl.DateTimeFormat:

const date = new Date('2025-01-27');

// 台灣格式
const zhTW = new Intl.DateTimeFormat('zh-TW').format(date);
console.log(zhTW); // 2025/1/27

// 美國格式
const enUS = new Intl.DateTimeFormat('en-US').format(date);
console.log(enUS); // 1/27/2025

// 日本格式
const jaJP = new Intl.DateTimeFormat('ja-JP').format(date);
console.log(jaJP); // 2025/1/27

// 完整格式
const full = new Intl.DateTimeFormat('zh-TW', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  weekday: 'long'
}).format(date);
console.log(full); // 2025年1月27日 星期一

Q9:資料庫應該儲存 UTC 還是本地時間?

:強烈建議儲存 UTC:

儲存 UTC 的優點
- ✅ 無時區混亂
- ✅ 夏令時間自動處理
- ✅ 跨時區查詢容易
- ✅ 國際化友善

實作方式

PostgreSQL

CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW() -- 自動 UTC
);

MySQL

CREATE TABLE events (
  id INT PRIMARY KEY,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- UTC
);

顯示時轉換

// 後端回傳 UTC
const utcDate = '2025-01-27T06:30:00Z';

// 前端轉本地時區
const local = new Date(utcDate);
console.log(local.toLocaleString('zh-TW', {
  timeZone: 'Asia/Taipei'
}));
// 2025/1/27 下午2:30:00

Q10:如何測試日期相關的程式碼?

:使用時間模擬函式庫:

Jest 範例

// 模擬固定時間
const mockDate = new Date('2025-01-27T00:00:00Z');
jest.useFakeTimers();
jest.setSystemTime(mockDate);

// 測試程式碼
test('應該返回今天日期', () => {
  expect(getTodayDate()).toBe('2025-01-27');
});

// 恢復真實時間
jest.useRealTimers();

Sinon 範例

import sinon from 'sinon';

// 模擬時間
const clock = sinon.useFakeTimers(new Date('2025-01-27'));

// 測試
assert.equal(getToday(), '2025-01-27');

// 時間前進
clock.tick(24 * 60 * 60 * 1000); // 前進 1 天
assert.equal(getToday(), '2025-01-28');

// 恢復
clock.restore();

Q11:如何處理使用者輸入的日期?

:嚴格驗證和標準化:

function parseUserDate(input) {
  // 支援多種格式
  const formats = [
    /^\d{4}-\d{2}-\d{2}$/,        // 2025-01-27
    /^\d{4}\/\d{2}\/\d{2}$/,      // 2025/01/27
    /^\d{2}\/\d{2}\/\d{4}$/,      // 27/01/2025
  ];

  // 驗證格式
  const isValid = formats.some(format => format.test(input));
  if (!isValid) {
    throw new Error('無效的日期格式');
  }

  // 標準化為 ISO 8601
  let normalized = input;
  if (/^\d{2}\/\d{2}\/\d{4}$/.test(input)) {
    // 將 DD/MM/YYYY 轉為 YYYY-MM-DD
    const [day, month, year] = input.split('/');
    normalized = `${year}-${month}-${day}`;
  } else {
    normalized = input.replace(/\//g, '-');
  }

  // 驗證日期有效性
  const date = new Date(normalized);
  if (isNaN(date.getTime())) {
    throw new Error('無效的日期');
  }

  return normalized;
}

// 使用
try {
  const date = parseUserDate('27/01/2025');
  console.log(date); // 2025-01-27
} catch (error) {
  console.error(error.message);
}

Q12:有推薦的日期處理設計模式嗎?

:Value Object 模式(領域驅動設計):

class DateRange {
  constructor(start, end) {
    this.start = new Date(start);
    this.end = new Date(end);

    if (this.end < this.start) {
      throw new Error('結束日期不能早於開始日期');
    }
  }

  // 封裝業務邏輯
  getDays() {
    const diff = this.end - this.start;
    return Math.ceil(diff / (1000 * 60 * 60 * 24));
  }

  includes(date) {
    const d = new Date(date);
    return d >= this.start && d <= this.end;
  }

  overlaps(other) {
    return this.start <= other.end && this.end >= other.start;
  }

  // 不可變操作
  extend(days) {
    return new DateRange(
      this.start,
      new Date(this.end.getTime() + days * 24 * 60 * 60 * 1000)
    );
  }
}

// 使用
const range = new DateRange('2025-01-01', '2025-01-31');
console.log(range.getDays()); // 30
console.log(range.includes('2025-01-15')); // true

const extended = range.extend(7);
console.log(extended.getDays()); // 37

優點
- 封裝業務邏輯
- 不可變物件(避免 Bug)
- 類型安全
- 易於測試