程式開發日期處理完整指南|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(黃色或金色,函數語法顏色)
- 變數名稱:diffTime、diffDays(藍色或青色,變數語法顏色)
- 註解(若有):// 計算兩日期間隔天數(綠色或灰色,註解顏色)
- 右螢幕 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
要點 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
要點 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);
});
});
- 函數名稱:describe、test(紫色或粉紅色,關鍵字顏色)
- 字串:'日期計算測試'(橘色或棕色,字串顏色)
- 右螢幕終端機提示:$ 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
參考資料
- MDN Web Docs「Date」官方文檔(2025 年版)
- Python 官方文檔「datetime」模組說明
- Oracle Java SE 文檔「java.time」套件
- IETF RFC 3339「Date and Time on the Internet」標準
- ISO 8601「Data elements and interchange formats」國際標準
- Stack Overflow 日期處理問題統計分析(2024)
- date-fns、Luxon、Day.js 官方技術文檔
- 各語言官方時區資料庫(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)
- 不同國家實施時間不同
- 有些國家不實施
- 時區邊界變動
- 政治因素調整
-
例如:北韓 2015 年改時區
-
歷史時區資料
- 過去的時區規則不同
- 需要 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)
- 類型安全
- 易於測試