Tutorial
夏時間変更の処理: 完全なチュートリアル
はじめに
夏時間 (DST) への移行は、タイムスタンプを扱う際に最も困難な側面の 1 つです。年に 2 回、時計が「前進」または「後退」し、バグ、データ損失、誤った計算を引き起こす可能性のあるエッジ ケースが発生します。このチュートリアルでは、アプリケーションで DST 移行を正しく処理する方法を説明します。
DST 移行について
DST 中には何が起こりますか?
春の前進 (DST の開始)
- 時計が 1 時間進みます (通常、午前 2:00 → 午前 3:00)。
- 1 時間が「欠落」しています - 午前 2 時 30 分のような時間は存在しません
- 期間: 1 日の長さはわずか 23 時間です
フォールバック (DST の終了)
- 時計が 1 時間進みます (通常、午前 2:00 → 午前 1:00)。
- 1 時間が「重複」します - 午前 1 時 30 分が 2 回発生します
- 期間: 1 日の長さは 25 時間です
現実世界への影響
// Spring Forward - March 10, 2024 (US)
// Problem: Scheduled task at 2:30 AM doesn't execute
const scheduled = new Date('2024-03-10T02:30:00');
// This time doesn't exist! JavaScript may interpret it as 3:30 AM
// Fall Back - November 3, 2024 (US)
// Problem: Log entries at 1:30 AM appear twice
const firstOccurrence = new Date('2024-11-03T01:30:00-04:00'); // EDT
const secondOccurrence = new Date('2024-11-03T01:30:00-05:00'); // EST
// Same wall clock time, different actual times!
DST の一般的な問題
問題 1: 時間の欠落 (Spring Forward)
時計が早く進んでいる場合、欠落している時間にタイムスタンプを作成しようとすると、予期しない動作が発生する可能性があります。
JavaScript の動作
// March 10, 2024, 2:30 AM in New York doesn't exist
const date = new Date('2024-03-10T02:30:00');
console.log(date.toLocaleString('en-US', {
timeZone: 'America/New_York',
hour12: false
}));
// Output varies by browser/environment
// Most will interpret as 3:30 AM or 1:30 AM
Python の動作
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Naive datetime in missing hour
try:
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30))
print(dt)
except pytz.exceptions.NonExistentTimeError as e:
print(f"Error: {e}")
# Error: 2024-03-10 02:30:00
解決策: 存在しない時間を明示的に処理します
# Option 1: Use is_dst parameter
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30), is_dst=None)
# Raises exception for ambiguous time
# Option 2: Normalize after creation
naive_dt = datetime(2024, 3, 10, 2, 30)
dt = ny_tz.normalize(ny_tz.localize(naive_dt, is_dst=False))
print(dt)
# 2024-03-10 03:30:00 EDT (adjusted forward)
問題 2: 時間の重複 (フォールバック)
クロックが後退すると、同じ実時間が 2 回発生し、曖昧さが生じます。
JavaScript の例
// November 3, 2024, 1:30 AM in New York occurs twice
// First occurrence (before fall back)
const first = new Date('2024-11-03T01:30:00-04:00'); // EDT
// Second occurrence (after fall back)
const second = new Date('2024-11-03T01:30:00-05:00'); // EST
console.log(first.getTime()); // 1730616600000
console.log(second.getTime()); // 1730620200000
console.log(second - first); // 3600000 (1 hour difference)
Python の例
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Ambiguous time - which occurrence?
try:
dt = ny_tz.localize(datetime(2024, 11, 3, 1, 30))
except pytz.exceptions.AmbiguousTimeError as e:
print(f"Ambiguous: {e}")
# Specify which occurrence
first = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=True) # Before fall back
second = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=False) # After fall back
print(first) # 2024-11-03 01:30:00 EDT-0400
print(second) # 2024-11-03 01:30:00 EST-0500
問題 3: 間違った期間の計算
DST の移行は、カレンダーベースの演算を使用する場合の期間の計算に影響します。
// Calculate hours between midnight on DST transition days
// Spring Forward Day (23 hours)
const springStart = new Date('2024-03-10T00:00:00');
const springEnd = new Date('2024-03-10T23:59:59');
const springHours = (springEnd - springStart) / 3600000;
console.log(springHours); // 23.999... hours (not 24!)
// Fall Back Day (25 hours)
const fallStart = new Date('2024-11-03T00:00:00');
const fallEnd = new Date('2024-11-03T23:59:59');
const fallHours = (fallEnd - fallStart) / 3600000;
console.log(fallHours); // 24.999... hours (appears normal but day is 25 hours)
DST を処理するためのベスト プラクティス
1. 常にタイムゾーンを認識した日付時刻を使用する
date-fns-tz を使用した JavaScript
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz';
// Always work in UTC internally
const utcDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Convert to local only for display
const nyDate = utcToZonedTime(utcDate, 'America/New_York');
console.log(format(nyDate, 'yyyy-MM-dd HH:mm:ss zzz', { timeZone: 'America/New_York' }));
Python と pytz
from datetime import datetime
import pytz
# Always work with timezone-aware datetimes
utc = pytz.UTC
ny_tz = pytz.timezone('America/New_York')
# Create timezone-aware datetime
dt_utc = datetime(2024, 3, 10, 7, 30, tzinfo=utc) # UTC time
dt_ny = dt_utc.astimezone(ny_tz) # Convert to NY time
print(dt_ny) # 2024-03-10 03:30:00 EDT (automatically adjusted for DST)
2. DST 移行の検出
JavaScript: 日付が DST かどうかを確認します
function isDST(date, timezone) {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = jan.getTimezoneOffset();
const julOffset = jul.getTimezoneOffset();
const stdOffset = Math.max(janOffset, julOffset);
const currentOffset = date.getTimezoneOffset();
return currentOffset < stdOffset;
}
const winterDate = new Date('2024-01-15T12:00:00');
const summerDate = new Date('2024-07-15T12:00:00');
console.log(isDST(winterDate)); // false
console.log(isDST(summerDate)); // true
Python: DST 移行日の検索
from datetime import datetime, timedelta
import pytz
def find_dst_transitions(year, timezone_name):
"""Find DST transition dates for a given year and timezone."""
tz = pytz.timezone(timezone_name)
transitions = []
# Check each day of the year
for day in range(365):
date = datetime(year, 1, 1) + timedelta(days=day)
today = tz.localize(datetime(year, 1, 1) + timedelta(days=day), is_dst=None)
tomorrow = tz.localize(datetime(year, 1, 1) + timedelta(days=day + 1), is_dst=None)
# Check if UTC offset changed
if today.utcoffset() != tomorrow.utcoffset():
transitions.append({
'date': date.strftime('%Y-%m-%d'),
'from_offset': str(today.utcoffset()),
'to_offset': str(tomorrow.utcoffset()),
'type': 'Spring Forward' if today.utcoffset() < tomorrow.utcoffset() else 'Fall Back'
})
return transitions
# Find 2024 DST transitions in New York
transitions = find_dst_transitions(2024, 'America/New_York')
for t in transitions:
print(f"{t['date']}: {t['type']} ({t['from_offset']} → {t['to_offset']})")
# Output:
# 2024-03-10: Spring Forward (-5:00:00 → -4:00:00)
# 2024-11-03: Fall Back (-4:00:00 → -5:00:00)
3. 欠勤時間は適切に処理する
JavaScript: 有効な時刻に正規化します
function normalizeToValidTime(dateString, timezone) {
try {
// Attempt to create date
const date = new Date(dateString);
// Check if time exists by comparing round-trip conversion
const formatted = date.toLocaleString('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
// If times don't match, time was adjusted
return {
original: dateString,
normalized: date.toISOString(),
wasAdjusted: !dateString.includes(formatted.split(',')[1].trim())
};
} catch (error) {
return { error: error.message };
}
}
const result = normalizeToValidTime('2024-03-10T02:30:00', 'America/New_York');
console.log(result);
// { original: '2024-03-10T02:30:00', normalized: '2024-03-10T07:30:00.000Z', wasAdjusted: true }
Python: 明示的な DST 処理
def safe_localize(tz, dt, prefer_dst=True):
"""
Safely localize datetime, handling DST transitions.
Args:
tz: pytz timezone
dt: naive datetime
prefer_dst: If True, prefer DST time during ambiguous hour
Returns:
Localized datetime
"""
try:
# Try to localize normally
return tz.localize(dt, is_dst=None)
except pytz.exceptions.AmbiguousTimeError:
# Ambiguous time (fall back) - specify preference
return tz.localize(dt, is_dst=prefer_dst)
except pytz.exceptions.NonExistentTimeError:
# Non-existent time (spring forward) - normalize forward
return tz.normalize(tz.localize(dt, is_dst=False))
# Usage
ny_tz = pytz.timezone('America/New_York')
# Missing hour
missing = safe_localize(ny_tz, datetime(2024, 3, 10, 2, 30))
print(missing) # 2024-03-10 03:30:00 EDT (adjusted forward)
# Duplicate hour
duplicate = safe_localize(ny_tz, datetime(2024, 11, 3, 1, 30), prefer_dst=True)
print(duplicate) # 2024-11-03 01:30:00 EDT (first occurrence)
4. タイムスタンプを UTC で保存する
タイムスタンプは常に UTC で保存され、表示の場合のみ現地時間に変換されます。
// Database storage pattern
class EventScheduler {
// Store in UTC
scheduleEvent(localDateString, timezone) {
const localDate = new Date(localDateString);
const utcTimestamp = localDate.getTime();
// Save to database
return {
utc_timestamp: utcTimestamp,
utc_iso: new Date(utcTimestamp).toISOString(),
original_timezone: timezone
};
}
// Retrieve and display in local time
getEventInTimezone(utcTimestamp, timezone) {
const date = new Date(utcTimestamp);
return date.toLocaleString('en-US', {
timeZone: timezone,
dateStyle: 'full',
timeStyle: 'long'
});
}
}
const scheduler = new EventScheduler();
// Schedule event
const event = scheduler.scheduleEvent('2024-03-10T02:30:00', 'America/New_York');
console.log(event);
// { utc_timestamp: 1710054600000, utc_iso: '2024-03-10T07:30:00.000Z', ... }
// Display in different timezones
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'America/New_York'));
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'Europe/London'));
5. DST のエッジケースをテストする
常に DST 移行日を使用してコードをテストしてください。
// Test suite for DST handling
describe('DST Transition Tests', () => {
const dstDates = {
springForward: '2024-03-10',
fallBack: '2024-11-03',
missingHour: '2024-03-10T02:30:00',
duplicateHour: '2024-11-03T01:30:00'
};
test('handles missing hour in spring forward', () => {
const result = normalizeToValidTime(
dstDates.missingHour,
'America/New_York'
);
expect(result.wasAdjusted).toBe(true);
});
test('duration calculation on spring forward day', () => {
const start = new Date(`${dstDates.springForward}T00:00:00`);
const end = new Date(`${dstDates.springForward}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(23, 0); // 23 hours, not 24
});
test('duration calculation on fall back day', () => {
const start = new Date(`${dstDates.fallBack}T00:00:00`);
const end = new Date(`${dstDates.fallBack}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(24, 0); // Appears as 24 but day is 25 hours
});
});
実践的なシナリオ
シナリオ 1: 定期的なイベントのスケジュール設定
from datetime import datetime, timedelta
import pytz
def schedule_daily_task(start_date, local_time, timezone_name, days=30):
"""
Schedule a task at the same local time each day, accounting for DST.
"""
tz = pytz.timezone(timezone_name)
events = []
for day in range(days):
# Create naive datetime for each day
date = start_date + timedelta(days=day)
naive_dt = datetime.combine(date, local_time)
# Safely localize (handles DST transitions)
try:
localized = tz.localize(naive_dt, is_dst=None)
except pytz.exceptions.NonExistentTimeError:
# Time doesn't exist (spring forward), adjust forward
localized = tz.normalize(tz.localize(naive_dt, is_dst=False))
except pytz.exceptions.AmbiguousTimeError:
# Time occurs twice (fall back), use first occurrence
localized = tz.localize(naive_dt, is_dst=True)
events.append({
'local_time': localized.strftime('%Y-%m-%d %H:%M:%S %Z'),
'utc_time': localized.astimezone(pytz.UTC).strftime('%Y-%m-%d %H:%M:%S UTC'),
'timestamp': int(localized.timestamp())
})
return events
# Schedule daily task at 2:00 AM, crossing DST boundary
from datetime import time, date
events = schedule_daily_task(
start_date=date(2024, 3, 8),
local_time=time(2, 0, 0),
timezone_name='America/New_York',
days=5
)
for event in events:
print(f"{event['local_time']} → {event['utc_time']}")
# Output shows how UTC time shifts on DST transition day
シナリオ 2: DST をまたいだ営業時間の計算
function calculateBusinessHours(startDate, endDate, timezone) {
const businessHoursPerDay = 8; // 9 AM - 5 PM
const businessStart = 9;
const businessEnd = 17;
let totalHours = 0;
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dayOfWeek = currentDate.getDay();
// Skip weekends
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
// Check if it's a DST transition day
const dayStart = new Date(currentDate);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(currentDate);
dayEnd.setHours(23, 59, 59, 999);
const dayLength = (dayEnd - dayStart) / 3600000;
if (dayLength < 24) {
// Spring forward - missing hour might affect business hours
console.log(`Spring forward on ${currentDate.toDateString()}`);
totalHours += Math.min(businessHoursPerDay, dayLength - (24 - dayLength));
} else if (dayLength > 24) {
// Fall back - extra hour
console.log(`Fall back on ${currentDate.toDateString()}`);
totalHours += businessHoursPerDay; // Business hours unchanged
} else {
totalHours += businessHoursPerDay;
}
}
currentDate.setDate(currentDate.getDate() + 1);
}
return totalHours;
}
// Calculate business hours March 1-15, 2024 (includes DST transition)
const hours = calculateBusinessHours(
new Date('2024-03-01'),
new Date('2024-03-15'),
'America/New_York'
);
console.log(`Total business hours: ${hours}`);
よくある落とし穴
❌ これを行わないでください
// Bad: Assuming all days are 24 hours
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const hours = (tomorrow - today) / 3600000; // Won't be exactly 24 on DST days!
// Bad: Using string concatenation for times
const timeString = `${year}-${month}-${day} 02:30:00`;
const date = new Date(timeString); // Might not exist on spring forward day!
// Bad: Ignoring timezone in calculations
const event1 = new Date('2024-11-03T01:30:00'); // Which occurrence?
✅ 代わりにこれを実行してください
// Good: Use UTC for calculations
const tomorrow = new Date(today.getTime() + 86400000); // Always exactly 24 hours
// Good: Use timezone-aware libraries
import { zonedTimeToUtc } from 'date-fns-tz';
const safeDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Good: Always include timezone offset
const event1 = new Date('2024-11-03T01:30:00-04:00'); // First occurrence (EDT)
const event2 = new Date('2024-11-03T01:30:00-05:00'); // Second occurrence (EST)
概要とベストプラクティス
重要なポイント
- UTC で保存 - タイムスタンプは常に UTC で保存され、表示の場合のみローカルに変換されます。
- タイムゾーン対応ライブラリを使用します - DST を手動で処理しないでください
- エッジケースを検証 - スプリングフォワード日付とフォールバック日付を使用してテストします。
- 曖昧さを明示的に処理 - 重複する時間のどの発生を意味するのかを指定します
- 欠落している時間を正規化 - 存在しない時間を調整するか、最も近い有効時間を使用します
推奨ライブラリ
JavaScript:
date-fns-tz- date-fns のタイムゾーンのサポートluxon- 優れた DST 処理を備えた最新の日時ライブラリmoment-timezone- 包括的なタイムゾーン データベース (メンテナンス モード)
パイソン:
pytz- Python の標準タイムゾーン ライブラリdateutil- 優れた DST サポートを備えた代替手段zoneinfo(Python 3.9+) - 組み込みのタイムゾーンのサポート
クイックリファレンス
| 問題 | ソリューション |
|---|---|
| ミッシングアワー(春先) | 次の有効な時間に正規化します (1 時間進める) |
| 重複した時間 (フォールバック) | タイムゾーン オフセットを使用して、どの発生が発生するかを指定します。 |
| 期間の計算 | 現地時間ではなく UTC タイムスタンプを使用する |
| 定期的なイベント | 各出現箇所を個別にローカライズする |
| テスト | 常に DST 移行日を使用してテストする |