فهرست مقاله
در عیبیابی لاگ، دیدن دو سطر با ساعت یکسان لزوماً به معنی رخدادِ همزمان نیست. هنگام عقبرفتنِ ساعت محلی، یک بازه دوبار دیده میشود؛ حتی تفاضل دو datetime آگاه در پایتون ممکن است برای هدف ما گمراهکننده باشد. این درس با چهار رویدادِ تألیفی، برچسب محلی را به لحظهٔ UTC تبدیل میکند و ترتیب و مدت را میسنجد. دادهها تاریخی و ساختگیاند؛ لاگ سرور، ساعت میزبان و زمانسنج واقعی خوانده نمیشوند.
۱. دو رویداد میتوانند ساعتِ محلیِ یکسان داشته باشند

منطقهٔ ثابتِ مثال America/New_York و سال ورودی ۲۰۲۳ است. در دادهٔ منطقهٔ استفادهشده، ساعت ۰۱:۳۰ روز پنجم نوامبر دو خوانش دارد: با fold صفر، offset منفی چهار ساعت و لحظهٔ ۰۵:۳۰ UTC؛ با fold یک، offset منفی پنج ساعت و لحظهٔ ۰۶:۳۰ UTC. فاصله یک ساعت است، هرچند ساعت و دقیقهٔ روی برچسب برابرند. این مشاهده، توصیه دربارهٔ قوانین جاری یا آیندهٔ یک کشور نیست.
طبق راهنمای ZoneInfo، fold خوانش زودتر و دیرترِ بازهٔ تکراری را جدا میکند. fold همان offset نیست و از روی شمارهٔ سطر حدس زده نمیشود. اگر منبع فقط ساعت محلیِ مبهم داشته باشد و اطلاعات دیگری ندهد، برنامه نمیتواند خوانش درست را کشف کند؛ باید قرارداد ورودی یا دادهٔ ثبتشده کاملتر شود.
هر رکوردِ ما یک تاپلِ شناسه، datetime بدون منطقه و fold صریح است. شناسه در همین دسته یکتا و از حروف و ارقام محدودِ ASCII است. خودِ datetime ورودی باید fold صفر داشته باشد تا انتخابِ خوانش فقط در فیلد جداگانه باشد؛ این سیاستِ رابط آموزشی است. حداکثر شانزده رکورد میپذیریم. قالبخوان عمومیِ فایل، بازیابی خودکارِ منطقه و رفع ابهام از متن آزاد پیاده نشدهاند.
۲. مقایسه و تفاضل را پس از تبدیل به UTC انجام دهیم

یک شیء ZoneInfo مشترک را عمداً برای همهٔ رویدادها نگه میداریم. قرارداد عملیات datetime میگوید وقتی دو مقدار آگاه همان شیء tzinfo را دارند، مقایسهٔ محلی fold را نادیده میگیرد و تفاضل هم تعدیل منطقه انجام نمیدهد. بنابراین دو ۰۱:۳۰ مثال، در آن مقایسه برابر و تفاضلشان صفر میشود. این رفتار مستند است؛ خرابشدنِ پایگاه دادهٔ منطقه نیست.
برای هدفِ ترتیبِ لحظهها، ابتدا هر دو طرف را با astimezone به UTC تبدیل میکنیم. سپس دو لحظه جدا میمانند و مدت یک ساعت به دست میآید. از همین رو datetime محلیِ مبهم را بهتنهایی کلید یکتای رویداد نمیگذاریم: در شاهد برنامه، دو کلید محلی به یک مدخل تبدیل میشوند. شناسهٔ رویداد را حفظ میکنیم و لحظهٔ UTC را معیار زمانی قرار میدهیم.
احتیاط دیگری هم هست: طبق قواعد PEP495 دربارهٔ محاسبه و برابری زمان، مقدار محلی در بازهٔ تکراری ممکن است با نمایش UTC همان لحظه برابر مقایسه نشود. پس راهحل ما مقایسهٔ «محلی با UTC» نیست؛ هر دو طرف را به UTC میبریم. برای لحظههای کاملاً برابر، شناسه ترتیب نمایش را میشکند؛ این تساویشکنی، علتومعلول یا تقدم واقعی را اثبات نمیکند.
۳. برچسبِ ناموجود را با رفتوبرگشت آشکار کنیم

جلو رفتنِ ساعت، مسئلهٔ دیگری میسازد: ساعت ۰۲:۳۰ دوازدهم مارس ۲۰۲۳ در این منطقه در شکاف قرار میگیرد. سازندهٔ datetime و replace الزاماً چنین برچسبی را رد نمیکنند. آگاهبودنِ شیء بهتنهایی، اثباتِ وجودِ برچسب محلی نیست. چسباندنِ منطقه با replace نیز تبدیل یک لحظهٔ قبلاً شناختهشده محسوب نمیشود؛ اینجا هنوز در حال تفسیرِ یک برچسب هستیم.
تابع local_to_utc انتخاب صریحِ fold را میچسباند، به UTC میرود و دوباره به همان منطقه برمیگردد. هم فیلدهای محلی و هم fold باید حفظ شوند. هر دو خوانشِ ۰۲:۳۰ شکاف رد میشوند. همچنین fold یک را خارج از ساعت تکراری رد میکنیم؛ چون برگشتِ متعارف آنجا fold صفر دارد. این قراردادِ سختگیرانهٔ خودمان است و ادعا نمیکند همهٔ نمونههای قابلساختِ پایتون یا همهٔ سیاستهای معتبرِ برنامهها را میپذیرد.
ورودی آگاه، زیرکلاس datetime، سال دیگر، fold بولی یا اعشاری و شناسهٔ تکراری نیز خطا میدهند. سکوت و جایگزینی با ساعت سیستم نداریم. در سامانهٔ واقعی میتوان رکورد نامعتبر را برای بازبینی نگه داشت یا اطلاعات بیشتری خواست؛ نمونه فقط استثنای مشخص میدهد و خطمشی نگهداری یا حذف لاگ را تعیین نمیکند.
۴. کدِ کامل، دادهٔ منطقه و مرجعِ مستقل

فایل log_utc_fold.py را ذخیره کنید. ZoneInfo از کتابخانهٔ استاندارد است، اما دادهٔ IANA را خودش همراه ندارد. این برنامه فایل منطقه را صریحاً از بستهٔ از قبل نصبشدهٔ tzdata میخواند؛ نسخهٔ بسته، نسخهٔ IANA و SHA-256 همان بایتها در خروجیاند. نصب بسته، تغییر TZPATH و دستکاری کش سراسری نداریم. نسخهٔ اجرای ثبتشده با نسخهٔ مستنداتِ خواندهشده جدا گزارش میشود.
ترتیبِ ورود چهار شناسه end، start، repeat-early و repeat-late است. ترتیبِ UTC باید repeat-early در ۰۵:۳۰، start در ۰۵:۵۰، end در ۰۶:۱۰ و repeat-late در ۰۶:۳۰ باشد. ساعت محلیِ start برابر ۰۱:۵۰ِ خوانش اول و end برابر ۰۱:۱۰ِ خوانش دوم است: برچسب چهل دقیقه عقب میرود، اما فاصلهٔ دو لحظه بیست دقیقه است. این پاسخ دستی از ترتیب ورود نتیجه گرفته نشده است.
مدت از مؤلفههای صحیحِ timedelta با واحد میکروثانیه ساخته میشود؛ تفاضل timestamp شناور نداریم. درخواستِ پایانِ پیش از آغاز خطا و دو شناسهٔ متفاوت با لحظهٔ یکسان، مدت صفر میدهد. برای روش نگهداری فیلدهای رویداد، درس گزارش ساختاریافتهٔ حسگر مکمل این مثال است؛ آن درس مدتِ از پیش دادهشده را ثبت میکند و مسئلهٔ تفسیر ساعت محلی را حل نمیکند.
"""Supplied historical civil log labels, explicit fold, UTC ordering and duration.
No system clock, server, filesystem log parser or clock synchronization.
Uses the already installed first-party tzdata package as an explicit resource.
"""
from datetime import datetime, timedelta, timezone
from importlib.resources import files
from io import BytesIO
import hashlib
import json
import platform
import re
from itertools import permutations
from zoneinfo import ZoneInfo
import tzdata
UTC = timezone.utc
ZONE_KEY = "America/New_York"
TZIF = files("tzdata.zoneinfo").joinpath(ZONE_KEY).read_bytes()
# One object is reused deliberately; no global TZPATH/cache modification.
ZONE = ZoneInfo.from_file(BytesIO(TZIF), key=ZONE_KEY)
ID_PATTERN = re.compile(r"[A-Za-z][A-Za-z0-9_-]{0,23}")
def checked_id(value):
if type(value) is not str or ID_PATTERN.fullmatch(value) is None:
raise ValueError("event_id")
return value
def local_to_utc(wall, fold):
"""Own strict policy: exact naive2023 label, explicit canonical fold."""
if type(wall) is not datetime or wall.tzinfo is not None:
raise ValueError("naive_datetime_label")
if wall.year != 2023 or wall.fold != 0:
raise ValueError("year2023_and_separate_fold_required")
if type(fold) is not int or fold not in (0, 1):
raise ValueError("exact_fold_zero_or_one")
local = wall.replace(tzinfo=ZONE, fold=fold)
instant = local.astimezone(UTC)
back = instant.astimezone(ZONE)
# Naive comparison ignores fold, so compare the fold field separately.
if back.replace(tzinfo=None) != wall or back.fold != fold:
raise ValueError("missing_or_noncanonical_local_label")
return instant
def canonical_events(records):
"""Return UTC events sorted by instant, then supplied ID for exact ties."""
if type(records) is not tuple or len(records) > 16:
raise ValueError("bounded_record_tuple")
ids, converted = set(), []
for row in records:
if type(row) is not tuple or len(row) != 3:
raise ValueError("record_shape")
event_id, wall, fold = row
checked_id(event_id)
if event_id in ids:
raise ValueError("duplicate_event_id")
ids.add(event_id)
converted.append((event_id, local_to_utc(wall, fold)))
return tuple(sorted(converted, key=lambda row: (row[1], row[0])))
def microseconds(delta):
return ((delta.days * 86400 + delta.seconds) * 1_000_000
+ delta.microseconds)
def duration_us(records, start_id, end_id):
checked_id(start_id)
checked_id(end_id)
by_id = dict(canonical_events(records))
if start_id not in by_id or end_id not in by_id:
raise ValueError("unknown_event_id")
delta = by_id[end_id] - by_id[start_id]
if delta < timedelta(0):
raise ValueError("end_precedes_start_in_UTC")
return microseconds(delta)
def historical_reference(wall, fold):
"""Independent fixed2023 transition table; no ZoneInfo/main resolver calls.
Enumerate both candidate UTC offsets, then verify the declared UTC rule.
This is a fixture oracle, not a general implementation of IANA rules.
"""
spring = datetime(2023, 3, 12, 7)
autumn = datetime(2023, 11, 5, 6)
found = []
for offset in (-240, -300):
candidate = wall - timedelta(minutes=offset)
actual_offset = -240 if spring <= candidate < autumn else -300
actual_fold = int(autumn <= candidate < autumn + timedelta(hours=1))
returned_wall = candidate + timedelta(minutes=actual_offset)
if returned_wall == wall and actual_fold == fold:
found.append(candidate.replace(tzinfo=UTC))
assert len(found) <= 1
return found[0] if found else None
def rejects(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def run_example():
label = datetime(2023, 11, 5, 1, 30)
early = label.replace(tzinfo=ZONE, fold=0)
late = label.replace(tzinfo=ZONE, fold=1)
rows = (
("end", datetime(2023, 11, 5, 1, 10), 1),
("start", datetime(2023, 11, 5, 1, 50), 0),
("repeat-early", label, 0),
("repeat-late", label, 1),
)
original = rows
ordered = canonical_events(rows)
expected = (
("repeat-early", datetime(2023, 11, 5, 5, 30, tzinfo=UTC)),
("start", datetime(2023, 11, 5, 5, 50, tzinfo=UTC)),
("end", datetime(2023, 11, 5, 6, 10, tzinfo=UTC)),
("repeat-late", datetime(2023, 11, 5, 6, 30, tzinfo=UTC)),
)
gap = datetime(2023, 3, 12, 2, 30)
spring_pair = (("a", datetime(2023, 3, 12, 1, 59, 59), 0),
("b", datetime(2023, 3, 12, 3), 0))
equal_instant = (("Z", datetime(2023, 7, 1, 12), 0),
("A", datetime(2023, 7, 1, 12), 0))
precise = (("a", datetime(2023, 1, 1, 0, 0, 0, 1), 0),
("b", datetime(2023, 12, 31, 23, 59, 59, 999999), 0))
precise_expected = microseconds(datetime(2024, 1, 1) - datetime(2023, 1, 1)) - 2
oracle_cases, oracle_ok = 0, True
for hour in range(365 * 24):
wall = datetime(2023, 1, 1) + timedelta(hours=hour)
for fold in (0, 1):
answer = historical_reference(wall, fold)
if answer is None:
actual = rejects(lambda w=wall, f=fold: local_to_utc(w, f),
"missing_or_noncanonical_local_label")
else:
actual = local_to_utc(wall, fold) == answer
oracle_ok = oracle_ok and actual
oracle_cases += 1
transitions = [
(datetime(2023, 3, 12, 1, 59, 59, 999999), 0,
datetime(2023, 3, 12, 6, 59, 59, 999999, tzinfo=UTC)),
(datetime(2023, 3, 12, 3), 0, datetime(2023, 3, 12, 7, tzinfo=UTC)),
(datetime(2023, 11, 5, 1, 59, 59, 999999), 0,
datetime(2023, 11, 5, 5, 59, 59, 999999, tzinfo=UTC)),
(datetime(2023, 11, 5, 1), 1, datetime(2023, 11, 5, 6, tzinfo=UTC)),
(datetime(2023, 11, 5, 2), 0, datetime(2023, 11, 5, 7, tzinfo=UTC)),
]
class DateSubclass(datetime):
pass
class IntSubclass(int):
pass
class StrSubclass(str):
pass
bad_labels = [label.replace(tzinfo=UTC), DateSubclass(2023, 1, 1), "2023-11-05T01:30:00", None]
invalid_ids = ["", "1bad", "A" * 25, "event with space", StrSubclass("A"), 1]
checks = {
"four_hand_events_order_by_UTC_and_repeated_labels_are_one_hour_apart":
ordered == expected and duration_us(rows, "repeat-early", "repeat-late") == 3_600_000_000,
"same_zone_local_equality_subtraction_and_dict_key_collapse_are_explicit":
early == late and microseconds(late - early) == 0 and len({early: "first", late: "second"}) == 1
and len({early.astimezone(UTC), late.astimezone(UTC)}) == 2,
"convert_both_sides_before_comparing_ambiguous_local_to_UTC":
early != early.astimezone(UTC)
and local_to_utc(label, 0) == datetime(2023, 11, 5, 5, 30, tzinfo=UTC),
"wall_clock_moves_back_but_UTC_elapsed_start_to_end_is_twenty_minutes":
microseconds(rows[0][1] - rows[1][1]) == -2_400_000_000
and duration_us(rows, "start", "end") == 1_200_000_000,
"spring_gap_both_constructible_labels_are_rejected_by_roundtrip":
all(rejects(lambda f=f: local_to_utc(gap, f), "missing_or_noncanonical_local_label") for f in (0, 1)),
"strict_policy_rejects_fold_one_outside_repeated_hour":
rejects(lambda: local_to_utc(datetime(2023, 7, 1, 12), 1), "missing_or_noncanonical_local_label"),
"spring_elapsed_one_second_is_not_wall_label_difference3601_seconds":
duration_us(spring_pair, "a", "b") == 1_000_000
and microseconds(spring_pair[1][1] - spring_pair[0][1]) == 3_601_000_000,
"exact_transition_endpoints_and_last_microseconds_match_hand_UTC":
all(local_to_utc(wall, fold) == answer for wall, fold, answer in transitions),
"equal_instants_keep_distinct_ids_with_explicit_lexical_tie_rule":
[row[0] for row in canonical_events(equal_instant)] == ["A", "Z"]
and duration_us(equal_instant, "Z", "A") == 0,
"all_twenty_four_arrival_permutations_preserve_same_canonical_order":
all(canonical_events(tuple(p)) == expected for p in permutations(rows)),
"exact_integer_microseconds_across_year_no_float_timestamp_subtraction":
duration_us(precise, "a", "b") == precise_expected == 31_535_999_999_998,
"one_whole_year_hour_grid_matches_independent_fixed_transition_preimage_oracle":
oracle_cases == 17520 and oracle_ok,
"duration_zero_unknown_id_and_reverse_order_are_explicit":
duration_us(rows, "start", "start") == 0
and rejects(lambda: duration_us(rows, "missing", "end"), "unknown_event_id")
and rejects(lambda: duration_us(rows, "end", "start"), "end_precedes_start_in_UTC"),
"exact_types_separate_fold_year_and_naive_only_are_checked":
all(rejects(lambda w=w: local_to_utc(w, 0), "naive_datetime_label") for w in bad_labels)
and all(rejects(lambda f=f: local_to_utc(label, f), "exact_fold_zero_or_one")
for f in (True, False, 0.0, "0", -1, 2, IntSubclass(0)))
and rejects(lambda: local_to_utc(datetime(2024, 1, 1), 0), "year2023_and_separate_fold_required")
and rejects(lambda: local_to_utc(label.replace(fold=1), 1), "year2023_and_separate_fold_required"),
"profile_shape_unique_IDs_and_sixteen_record_bound_are_checked":
all(rejects(lambda i=i: canonical_events(((i, label, 0),)), "event_id") for i in invalid_ids)
and rejects(lambda: canonical_events(list(rows)), "bounded_record_tuple")
and rejects(lambda: canonical_events((list(rows[0]),)), "record_shape")
and rejects(lambda: canonical_events((rows[0], rows[0])), "duplicate_event_id")
and rejects(lambda: canonical_events(tuple(("E" + str(i), label, 0) for i in range(17))), "bounded_record_tuple")
and len(canonical_events(tuple(("E" + str(i), label, 0) for i in range(16)))) == 16,
"empty_profile_repeatability_input_preservation_and_UTC_year_rollover":
canonical_events(()) == () and canonical_events(rows) == ordered and rows == original
and local_to_utc(datetime(2023, 12, 31, 23), 0) == datetime(2024, 1, 1, 4, tzinfo=UTC),
}
assert all(type(value) is bool and value for value in checks.values()), checks
return {
"python": platform.python_version(),
"dependencies": "standard library plus already installed first-party tzdata resource",
"timezone_data": {"key": ZONE_KEY, "tzdata_package_version": tzdata.__version__,
"iana_version": tzdata.IANA_VERSION,
"tzif_sha256": hashlib.sha256(TZIF).hexdigest(),
"loaded_explicitly_from_package": True},
"scope": "Synthetic supplied2023 local log labels only; no real server, system clock, synchronization, monotonic timer or causality guarantee",
"input_arrival_order": [{"event_id": event_id, "wall": wall.isoformat(), "fold": fold}
for event_id, wall, fold in rows],
"canonical_UTC_order": [{"event_id": event_id, "UTC": instant.isoformat()}
for event_id, instant in ordered],
"repeated_hour_witness": {"earlier_local": early.isoformat(), "later_local": late.isoformat(),
"same_zone_equality": early == late,
"local_subtraction_us": microseconds(late - early),
"UTC_duration_us": duration_us(rows, "repeat-early", "repeat-late")},
"backward_wall_label_pair": {"start_to_end_wall_us": -2_400_000_000,
"start_to_end_UTC_us": duration_us(rows, "start", "end")},
"missing_label": {"wall": gap.isoformat(), "both_folds_rejected": True},
"strict_fold_policy": "roundtrip civil fields AND fold must match; fold1 outside repeated interval rejected",
"independent_oracle_cases_inside_ONE_group": oracle_cases,
"meaningful_named_groups": len(checks),
"checks": checks,
}
if __name__ == "__main__":
print(json.dumps(run_example(), ensure_ascii=True, indent=2, allow_nan=False))
مرجع مستقل دو offset فرضشده و مرزهای UTC تغییر ساعتِ همین سال را دارد. برای هر برچسب، نامزدهای UTC را میسازد و با قواعدِ قطعهایِ ازپیشنوشتهشده به زمان محلی برمیگرداند؛ ZoneInfo یا تابع اصلی را صدا نمیزند. شبکهٔ همهٔ ساعتهای سال با هر دو fold، ۱۷۵۲۰ حالت را در یک گروه بررسی پوشش میدهد؛ این تعداد را آزمونهای مستقلِ نامدار نمینامیم.
{
"python": "3.14.5",
"dependencies": "standard library plus already installed first-party tzdata resource",
"timezone_data": {
"key": "America/New_York",
"tzdata_package_version": "2026.2",
"iana_version": "2026b",
"tzif_sha256": "d7f2206b3a45989fc9ad63d558922532fa7352280d5f87176bf1db79cb1d1fa9",
"loaded_explicitly_from_package": true
},
"scope": "Synthetic supplied2023 local log labels only; no real server, system clock, synchronization, monotonic timer or causality guarantee",
"input_arrival_order": [
{
"event_id": "end",
"wall": "2023-11-05T01:10:00",
"fold": 1
},
{
"event_id": "start",
"wall": "2023-11-05T01:50:00",
"fold": 0
},
{
"event_id": "repeat-early",
"wall": "2023-11-05T01:30:00",
"fold": 0
},
{
"event_id": "repeat-late",
"wall": "2023-11-05T01:30:00",
"fold": 1
}
],
"canonical_UTC_order": [
{
"event_id": "repeat-early",
"UTC": "2023-11-05T05:30:00+00:00"
},
{
"event_id": "start",
"UTC": "2023-11-05T05:50:00+00:00"
},
{
"event_id": "end",
"UTC": "2023-11-05T06:10:00+00:00"
},
{
"event_id": "repeat-late",
"UTC": "2023-11-05T06:30:00+00:00"
}
],
"repeated_hour_witness": {
"earlier_local": "2023-11-05T01:30:00-04:00",
"later_local": "2023-11-05T01:30:00-05:00",
"same_zone_equality": true,
"local_subtraction_us": 0,
"UTC_duration_us": 3600000000
},
"backward_wall_label_pair": {
"start_to_end_wall_us": -2400000000,
"start_to_end_UTC_us": 1200000000
},
"missing_label": {
"wall": "2023-03-12T02:30:00",
"both_folds_rejected": true
},
"strict_fold_policy": "roundtrip civil fields AND fold must match; fold1 outside repeated interval rejected",
"independent_oracle_cases_inside_ONE_group": 17520,
"meaningful_named_groups": 16,
"checks": {
"four_hand_events_order_by_UTC_and_repeated_labels_are_one_hour_apart": true,
"same_zone_local_equality_subtraction_and_dict_key_collapse_are_explicit": true,
"convert_both_sides_before_comparing_ambiguous_local_to_UTC": true,
"wall_clock_moves_back_but_UTC_elapsed_start_to_end_is_twenty_minutes": true,
"spring_gap_both_constructible_labels_are_rejected_by_roundtrip": true,
"strict_policy_rejects_fold_one_outside_repeated_hour": true,
"spring_elapsed_one_second_is_not_wall_label_difference3601_seconds": true,
"exact_transition_endpoints_and_last_microseconds_match_hand_UTC": true,
"equal_instants_keep_distinct_ids_with_explicit_lexical_tie_rule": true,
"all_twenty_four_arrival_permutations_preserve_same_canonical_order": true,
"exact_integer_microseconds_across_year_no_float_timestamp_subtraction": true,
"one_whole_year_hour_grid_matches_independent_fixed_transition_preimage_oracle": true,
"duration_zero_unknown_id_and_reverse_order_are_explicit": true,
"exact_types_separate_fold_year_and_naive_only_are_checked": true,
"profile_shape_unique_IDs_and_sixteen_record_bound_are_checked": true,
"empty_profile_repeatability_input_preservation_and_UTC_year_rollover": true
}
}
شانزده گروه نامدار شامل مرزِ تغییر ساعت تا میکروثانیه، شکاف، برابری و کلیدِ محلی، بیستوچهار جایگشتِ ورودی، مدتِ دقیقِ طولانی، تساوی لحظهها و ورودیهای نامعتبرند. مرجعِ سال ۲۰۲۳ جایگزینِ عمومیِ IANA نیست. این محاسبه نیز خطای ساعتِ دو میزبان، عقبکشیدنِ ساعت بهعلت تنظیمات، ثانیهٔ کبیسه یا صحتِ مدتِ واقعیِ درخواست را نمیسنجد؛ فقط لحظههای گزارششده را طبق فرضها مقایسه میکند.
۵. چهار پرسش دربارهٔ ترتیب و مدتِ لاگ

پرسش اول: اگر فقط ۰۱:۳۰ و نام منطقه را داشته باشیم، کدام خوانش درست است؟ پاسخ: در بازهٔ تکراری، این اطلاعات کافی نیست. نمونه fold را صریح میخواهد؛ انتخابِ خودکارِ صفر، دانشی دربارهٔ رخداد واقعی ایجاد نمیکند.
پرسش دوم: چرا به تفاضلِ دو مقدار آگاهِ هممنطقه اعتماد نکردیم؟ پاسخ: برای همان شیء tzinfo، حسابِ محلی تعدیلِ offset انجام نمیدهد. در شاهد ۰۱:۳۰، خروجی صفر است؛ پس از تبدیلِ هر دو طرف به UTC، مدت ۳۶۰۰ ثانیه میشود.
پرسش سوم: آیا ردشدنِ fold یک در تیرماه، قانونِ سازندهٔ datetime است؟ پاسخ: خیر؛ سیاستِ صریحِ این اعتبارسنج است. ما بازگشتِ فیلدها و fold را شرط کردهایم. سازنده میتواند نمونهای بسازد که این رابط نمیپذیرد.
پرسش چهارم: آیا مرتبسازی UTC همان ترتیبِ رسیدن یا تضمینِ علیت است؟ پاسخ: خیر؛ ساعتِ نادرست یا نامتقارنِ میزبانها هنوز میتواند نتیجه را منحرف کند. زمانبندِ رویدادِ قطعی ترتیبِ یک ساعتِ مجازیِ فرضشده را جدا بررسی میکند. اینجا همگامسازی و زمانسنج یکنواخت پیاده نشدهاند.
منابع و مطالعهٔ بیشتر
- Python3.14؛ ZoneInfo و منبعِ دادهٔ منطقه
- Python3.14؛ عملیاتِ datetime و تبدیلِ یک لحظه
- PEP495؛ ساعتِ تکراری، شکاف و محاسبهٔ زمان
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

