ครึ่งชั่วโมงที่ไม่มีอยู่จริง
ร้านของคุณอยู่ใน Bangkok และส่งของให้ลูกค้าใน New York ด้วย ลูกค้าของออเดอร์ A-1042
เลือก slot ส่งของเป็น 2026-03-08 02:30 ตามนาฬิกาของบ้านตัวเอง ซึ่งอยู่ในโซน America/New_York
ระบบรับค่านี้ แปลงมันเป็นเวลาที่ใช้คำนวณต่อ แล้วส่งให้ระบบจัดรถ ไม่มีบรรทัดไหนแดง ไม่มี exception และไม่มีคำเตือน
คำถามของบทนี้คือ เวลาที่ลูกค้าเลือกมีอยู่จริงหรือเปล่า คำตอบคือไม่มี และในห้าภาษาที่วัด มีภาษาเดียวที่บอกคุณ
orderctl คือเครื่องมือประมวลผลออเดอร์ที่คอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ
ระหว่างนี้ทุกค่าในบทนี้รันเองได้ด้วย npm run verify:time-semantics
ซึ่งเรียก python3 scripts/time-semantics/probe.py · ไม่ต้องมี Docker แต่การรันครั้งแรก
ต้องต่อเน็ตเพื่อดึง chrono กับ chrono-tz จาก crates.io
นาฬิกาที่กระโดดข้ามไปหนึ่งช่วง
หัวข้อที่มีชื่อว่า “นาฬิกาที่กระโดดข้ามไปหนึ่งช่วง”คืนที่ DST เริ่ม นาฬิกาของ New York ข้ามช่วงหนึ่งไปทั้งช่วง และ
02:30อยู่ในช่วงนั้น
ประเทศไทยใช้ +07:00 ตลอดปี จึงไม่มี DST (daylight saving time หรือเวลาออมแสง)
แต่ America/New_York มี และวันที่ 2026-03-08 คือวันที่ DST ของปีนั้นเริ่ม
คืนนั้น offset ของโซนเปลี่ยนจาก -05:00 เป็น -04:00 นาฬิกาจึงต้องกระโดดไปข้างหน้าเท่าส่วนต่างนั้น
เวลาช่วงหนึ่งบนหน้าปัดของคืนนั้นจึงไม่เคยเกิดขึ้น
ค่าที่ลูกค้ากรอกคือ wall-clock timewall-clock timeเวลาที่อ่านจากหน้าปัดนาฬิกาของที่ใดที่หนึ่ง คือวันที่ ชั่วโมง และนาที โดยไม่มี offset หรือโซนกำกับ · ค่าหนึ่งค่าอาจไม่ตรงกับจุดไหนบนเส้นเวลาเลย ตรงกับสองจุด หรือตรงกับจุดเดียว (เวลาบนหน้าปัด) คือวันที่ ชั่วโมง และนาที ตามนาฬิกาของที่ใดที่หนึ่ง ค่าแบบนี้ยังไม่ได้ระบุว่าเป็นจุดไหนบนเส้นเวลา จนกว่าจะมีโซนมากำกับ และโซนนั้นตอบได้ว่าใช้ offset อะไร
จุดบนเส้นเวลาที่ไม่ขึ้นกับโซนของใครเรียกว่า instantinstantจุดหนึ่งจุดบนเส้นเวลาที่ไม่ขึ้นกับโซนของใคร · เขียนเป็น UTC ได้ค่าเดียวเสมอ สองระบบที่เก็บ instant เดียวกันจึงหมายถึงเวลาเดียวกัน ต่อให้หน้าปัดของแต่ละที่แสดงคนละค่า (จุดหนึ่งบนเส้นเวลา) เขียนเป็น UTC ได้ค่าเดียว ระบบจัดรถต้องการ instant ไม่ใช่ wall-clock time เพราะรถวิ่งตามเวลาจริง ไม่ได้วิ่งตามตัวเลขบนฟอร์ม
ช่วงของ wall-clock time ที่นาฬิกากระโดดข้ามไปเรียกว่า DST gapDST gapช่วงเวลาบนหน้าปัดที่ไม่มีอยู่จริง เพราะนาฬิกากระโดดข้ามไปตอนเริ่ม DST · ที่ America/New_York วันที่ 2026-03-08 offset เปลี่ยนจาก -05:00 เป็น -04:00 เวลา 02:30 ของวันนั้นจึงไม่มีอยู่ในโซนนั้น
(ช่วงเวลาบนหน้าปัดที่ไม่มีอยู่จริง) 02:30 ของวันที่ 2026-03-08 ที่ America/New_York
อยู่ใน gap นั้น จึงไม่ตรงกับ instant ไหนเลย
flowchart LR
A["ฟอร์มของลูกค้า · 2026-03-08 02:30"] --> B{"America/New_York มี 02:30 ของวันนั้นไหม"}
B -->|"ไม่มี · นาฬิกากระโดดข้ามช่วงนั้นไป"| C["ไม่มี instant ให้แปลงไปหา"]
C --> D["ภาษาต้องเลือกเองว่าจะทำอะไร"]
D --> E["คืนเวลาค่าหนึ่งให้เงียบๆ"]
D --> F["บอกผู้เรียกว่าไม่มีเวลานี้"]
คำบรรยายภาพ: ปัญหาไม่ได้อยู่ที่การแปลงผิด แต่อยู่ที่ไม่มีอะไรให้แปลงไปหา ทุกภาษาจึงต้องตัดสินใจแทนผู้เรียก และสิ่งที่แยกห้าภาษาออกจากกันคือกิ่งที่มันเลือก
ห้าภาษา กับเวลาค่าเดียว
หัวข้อที่มีชื่อว่า “ห้าภาษา กับเวลาค่าเดียว”แต่ละภาษาได้รับ
2026-03-08 02:30ที่America/New_Yorkเหมือนกัน แล้วคืนของกลับมาคนละแบบ
ทุกการเรียกข้างล่างตัดมาจาก runner ของ probe ใน scripts/time-semantics/runners/ และค่าในคอมเมนต์
คือค่าที่ probe วัดได้ ไม่ใช่ค่าที่เขียนจากความจำ
Java เลื่อนไปข้างหน้า
หัวข้อที่มีชื่อว่า “Java เลื่อนไปข้างหน้า”ZonedDateTime.of รับเวลาที่ไม่มีอยู่จริงโดยไม่โยน exception แล้วคืนเวลาที่เลื่อนไปแล้ว
ZoneId ny = ZoneId.of("America/New_York");ZonedDateTime gap = ZonedDateTime.of(2026, 3, 8, 2, 30, 0, 0, ny);gap.toString(); // 2026-03-08T03:30-04:00[America/New_York]gap.toInstant(); // 2026-03-08T07:30:00ZJavadoc ของ ZonedDateTime.of เขียนกฎนี้ไว้ตรงๆ ว่าในกรณีของ gap “the local date-time is adjusted
to be later by the length of the gap” ค่า 02:30 จึงกลายเป็น 03:30 และ offset กลายเป็น -04:00
Node เลื่อนไปข้างหน้า แต่เฉพาะเมื่อ process อยู่ใน New York
หัวข้อที่มีชื่อว่า “Node เลื่อนไปข้างหน้า แต่เฉพาะเมื่อ process อยู่ใน New York”Date สร้างเวลาจากวันที่และชั่วโมงได้แค่ในโซนของ process ซึ่งมาจาก TZ ค่าของ Node
ทุกค่าในบทนี้จึงบอก TZ ที่ใช้รันกำกับไว้เสมอ
// เดือนของ Date เริ่มที่ 0 · เลข 2 คือเดือนมีนาคมconst gap = new Date(2026, 2, 8, 2, 30);gap.toString();gap.toISOString();probe รัน file เดียวกันสองครั้ง ต่างกันที่ TZ อย่างเดียว
TZ | toString() | toISOString() |
|---|---|---|
America/New_York | Sun Mar 08 2026 03:30:00 GMT-0400 (Eastern Daylight Time) | 2026-03-08T07:30:00.000Z |
Asia/Bangkok | Sun Mar 08 2026 02:30:00 GMT+0700 (Indochina Time) | 2026-03-07T19:30:00.000Z |
แถวแรกตรงกับ Java ทั้ง wall-clock time และ instant ส่วนแถวที่สองไม่ได้ตอบคำถามของบทนี้เลย
มันสร้าง 02:30 ของ Bangkok ซึ่งมีอยู่จริง เพราะ Bangkok ไม่มี gap ในคืนนั้น
ผลของแถวที่สองคือ server ที่ตั้ง TZ=Asia/Bangkok สร้าง slot ของลูกค้าใน New York ด้วย Date
ไม่ได้ตั้งแต่แรก code บรรทัดเดียวกันได้คนละ instant ตามเครื่องที่รัน
สเปก ECMAScript หัวข้อ UTC ( t ) เขียนกฎของแถวแรกไว้ว่าเวลาที่ถูกข้ามไป “t is interpreted
using the time zone offset before the transition” คือใช้ -05:00 ซึ่งเป็น offset ก่อนการเปลี่ยน
แล้วจุดที่ได้ก็แสดงผลเป็น 03:30 ของ offset ใหม่
Python เก็บเวลาที่ไม่มีอยู่จริงไว้ตามเดิม
หัวข้อที่มีชื่อว่า “Python เก็บเวลาที่ไม่มีอยู่จริงไว้ตามเดิม”datetime ของ Python ไม่ได้เลื่อนอะไรเลย มันเก็บ 02:30 ไว้ แล้วแปะ offset ให้
from datetime import datetime, timezonefrom zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")g = datetime(2026, 3, 8, 2, 30, tzinfo=ny)g.isoformat() # 2026-03-08T02:30:00-05:002026-03-08T02:30:00-05:00 เป็นสตริงที่อ่านแล้วดูถูกต้องทุกตัวอักษร แต่ไม่มีนาฬิกาเรือนไหน
ใน New York เคยแสดง 02:30 คู่กับ -05:00 ในวันนั้น
PEP 495 ซึ่งเป็นที่มาของช่อง fold เขียนกฎไว้ว่าเมื่อเวลาตกอยู่ใน gap “the rules in effect
before the transition should be used if fold=0” ค่าตั้งต้นของ fold คือ 0 Python จึงแปะ
-05:00 ซึ่งเป็น offset ก่อนการเปลี่ยน
อ่านประโยคนี้ก่อนอ่านค่าของ Go · go doc time.Date เขียนไว้ว่า
“Date returns a time that is correct in one of the two zones involved in the transition, but it does not guarantee which.”
ค่าที่ Go คืนให้ในเวลาที่ไม่มีอยู่จริงจึงไม่ใช่คำตอบของ Go ทั้งภาษา มันเป็นค่าของเครื่องที่วัด และเครื่องอื่นตอบอีกโซนได้โดยไม่ผิดเอกสาร
Go คืนค่าที่เอกสารไม่รับปาก
หัวข้อที่มีชื่อว่า “Go คืนค่าที่เอกสารไม่รับปาก”ny, err := time.LoadLocation("America/New_York")// ...time.Date(2026, 3, 8, 2, 30, 0, 0, ny).Format(time.RFC3339)เครื่องที่ probe รันคือ go1.27.0 บน darwin/arm64 และค่าที่วัดได้บนเครื่องนั้นคือ
2026-03-08T01:30:00-05:00
probe เก็บค่านี้ไว้ในช่อง unguaranteed ของ measured.json แล้วไม่ผูกมันเป็นข้ออ้างสักข้อ
ด่านของ CI รันบน linux/amd64 ซึ่งเป็นคนละเครื่องกับที่วัด
สิ่งที่บทนี้พูดได้เกี่ยวกับ Go จึงมีข้อเดียว คือ time.Date คืนเวลาค่าหนึ่งให้เงียบๆ ไม่มี error
และไม่มีช่องให้รู้ว่าเวลาที่ส่งเข้าไปไม่มีอยู่จริง
Rust บอกว่าไม่มีเวลานี้
หัวข้อที่มีชื่อว่า “Rust บอกว่าไม่มีเวลานี้”chrono-tz ไม่คืนเวลาค่าเดียว มันคืน enum ที่บังคับให้ผู้เรียกจัดการทุกกรณี
use chrono::{LocalResult, NaiveDate, TimeZone};use chrono_tz::America::New_York;
let gap = NaiveDate::from_ymd_opt(2026, 3, 8).unwrap().and_hms_opt(2, 30, 0).unwrap();match New_York.from_local_datetime(&gap) { LocalResult::None => { /* ค่าที่วัดได้ตกกิ่งนี้ */ } LocalResult::Single(a) => { /* ... */ } LocalResult::Ambiguous(a, b) => { /* ... */ }}ค่าที่วัดได้คือ LocalResult::None · เอกสารของ chrono เขียนความหมายของ variant นี้ว่า
“The local time does not exist because there is a gap in the local time.”
match ข้างบนไม่ compile ถ้าขาดกิ่งใดกิ่งหนึ่งในสามกิ่ง ผู้เรียกจึงหนีคำถามว่า “ถ้าเวลานี้ไม่มีจะทำอะไร”
ไม่ได้ นี่คือราคาของ Rust ด้วย code ที่แค่อยากได้เวลาค่าเดียวต้องเขียนกิ่งเพิ่มอีกสองกิ่ง
ห้าภาษา สี่คำตอบ บนเครื่องที่วัด
หัวข้อที่มีชื่อว่า “ห้าภาษา สี่คำตอบ บนเครื่องที่วัด”รวมห้าคำตอบไว้ในตารางเดียว พร้อมรุ่นของ tzdata ที่แต่ละภาษาอ่านจริง
| ภาษา | การเรียก | ค่าที่วัดได้ | tzdata ที่ใช้ |
|---|---|---|---|
Java openjdk 25.0.4.1 | ZonedDateTime.of(...) | 2026-03-08T03:30-04:00[America/New_York] | 2026b · tzdb.dat ของ JDK |
Node v24.19.0 ที่ TZ=America/New_York | new Date(2026, 2, 8, 2, 30) | Sun Mar 08 2026 03:30:00 GMT-0400 (Eastern Daylight Time) | 2026b · ICU ที่ฝังใน node |
Python 3.14.6 | datetime(..., tzinfo=ny) | 2026-03-08T02:30:00-05:00 | 2026c · /usr/share/zoneinfo ของระบบ |
Go go1.27.0 · darwin/arm64 เท่านั้น | time.Date(...) | 2026-03-08T01:30:00-05:00 · เอกสารไม่รับปาก | 2026c · /usr/share/zoneinfo ของระบบ |
Rust chrono-tz 0.10.4 | New_York.from_local_datetime(...) | LocalResult::None | 2025b · ฝังใน chrono-tz |
แถวของ Java กับ Node ตรงกันทั้ง wall-clock time และ instant ที่เหลือต่างกันหมด บนเครื่องที่วัด ห้าภาษาจึงให้สี่คำตอบ
ตัวเลขสี่นี้นับรวมค่าของ Go ด้วย ถ้าตัดแถวที่เอกสารไม่รับปากออก สิ่งที่รับปากได้คือสี่ภาษาให้ สามคำตอบ และมีภาษาเดียวที่บอกว่าเวลานี้ไม่มีอยู่จริง
ห้าภาษาบนเครื่องเดียวกันอ่าน tzdata สามรุ่น จากสี่แหล่ง ตารางที่รวมหลายภาษาจึงต้องพิมพ์รุ่นของทุกภาษา เพราะคำตอบของคอร์สนี้ขึ้นกับข้อมูลของโซนด้วย ไม่ได้ขึ้นกับ code อย่างเดียว
หลักฐานว่า 02:30 ไม่มีอยู่จริง
หัวข้อที่มีชื่อว่า “หลักฐานว่า 02:30 ไม่มีอยู่จริง”Python ไม่รู้ว่าค่าที่มันเก็บไม่มีอยู่จริง จนกว่าจะส่งค่านั้นไป UTC แล้วรับกลับมา
2026-03-08T02:30:00-05:00 ของ Python ดูเหมือนเวลาปกติค่าหนึ่ง วิธีที่ทำให้มันเผยตัวคือ
แปลงไป UTC แล้วแปลงกลับมาที่โซนเดิม
g.astimezone(timezone.utc).astimezone(ny).isoformat() # 2026-03-08T03:30:00-04:00เวลาที่ไปแล้วกลับมาไม่ใช่เวลาเดิม 02:30 หายไป และ 03:30-04:00 มาแทนที่ ซึ่งเป็น
wall-clock time เดียวกับที่ Java คืนให้ตั้งแต่ต้น
ค่าที่มีอยู่จริงจะไปแล้วกลับมาเป็นค่าเดิมเสมอ เพราะการแปลงจาก UTC กลับมาที่โซนไม่มีทางได้เวลาใน gap PEP 495 เขียนข้อนี้ไว้ว่า “The fromutc() method should never produce a time in the gap.”
Java ก็ตอบคำถามนี้ได้ ถ้าถามมันตรงๆ แทนที่จะสร้าง ZonedDateTime
ny.getRules().getValidOffsets(LocalDateTime.of(2026, 3, 8, 2, 30)).size(); // 0offset ที่ใช้ได้กับ 02:30 ของวันนั้นมี 0 ตัว คือไม่มีเลย Java รู้ความจริงข้อนี้มาตลอด
แต่ ZonedDateTime.of ไม่ได้บอกมัน ผู้เรียกต้องรู้เองว่าต้องถามอีก method หนึ่ง
slot ที่ลูกค้าไม่ได้เลือก
หัวข้อที่มีชื่อว่า “slot ที่ลูกค้าไม่ได้เลือก”ทุกภาษาที่คืนเวลาให้เงียบๆ ส่งรถไปตามเวลาที่ลูกค้าไม่ได้กรอก
กลับไปที่ออเดอร์ A-1042 ระบบที่เขียนด้วย Java หรือ Node ที่ TZ=America/New_York
จะบันทึก slot เป็น 03:30 ลูกค้ากรอก 02:30 และไม่มีใครบอกลูกค้าว่าเวลาถูกเลื่อน
ระบบที่เขียนด้วย Python จะเก็บ 02:30-05:00 ไว้ และแสดง 02:30 ให้ลูกค้าเห็นตามเดิม
พอค่านั้นผ่าน UTC ไปที่ระบบจัดรถแล้วกลับมา หน้าจอของคนขับจะแสดง 03:30-04:00
ถ้าสอง service ในเส้นทางเดียวกันเขียนคนละภาษา slot เดียวกันจะแสดงคนละเวลาบนสองหน้าจอ
และ server ที่ตั้ง TZ=Asia/Bangkok สร้าง slot ของ New York ด้วย Date ไม่ได้
มันได้ 02:30 ของ Bangkok แทน ซึ่งคือ 2026-03-07T19:30:00.000Z คนละจุดกับ slot ที่ลูกค้าเลือก
ทางออกไม่ใช่การเลือกภาษาที่เลื่อนเวลาได้ถูกใจ แต่คือการถามก่อนว่าเวลานี้มีอยู่จริงไหม แล้วตัดสินเองว่าจะปฏิเสธ หรือจะถามลูกค้าใหม่
ราคาคือ code ทุกจุดที่รับเวลาจากผู้ใช้ต้องมีกิ่งของเวลาที่ไม่มีอยู่จริง ใน Rust คอมไพเลอร์บังคับกิ่งนั้นให้ ในอีกสี่ภาษา ไม่มีใครบังคับ และกิ่งนั้นจะไม่ถูกเขียนจนกว่าจะมีลูกค้าร้องเรียน
จุดแรกในหกจุด
หัวข้อที่มีชื่อว่า “จุดแรกในหกจุด”บทนี้ปิดจุดที่หนึ่ง อีกห้าจุดอยู่ในบทถัดไป และทุกจุดเงียบเหมือนกัน
ธีสิสของคอร์สนี้เป็นประโยคเดียว คือ wall-clock time ค่าหนึ่งไม่ได้ระบุจุดเดียวบนเส้นเวลา มันอาจไม่มีจุดเลย มีสองจุด หรือมีจุดเดียวที่แต่ละภาษาเลือกต่างกัน และในทรงที่ probe วัด มีภาษาเดียวจากห้าภาษาที่บอกคุณ
จุดที่หนึ่งคือจุดที่บทนี้เพิ่งวัด คือ wall-clock time ที่ไม่มีจุดบนเส้นเวลาเลย สี่ภาษาตอบมันกลับมา เป็นเวลาค่าหนึ่ง และ Rust เป็นภาษาเดียวที่ตอบว่าไม่มี
บทที่ 2 ถอยไปที่ค่าที่สั้นกว่านี้อีก คือวันส่งของที่มีแค่วันที่ ไม่มีเวลาและไม่มีโซน แล้วถามว่ามันกลายเป็น คนละวันได้อย่างไร
-
Go — package time, func Date (เข้าถึง 2026-10-02) — ที่มาของประโยค “Date returns a time that is correct in one of the two zones involved in the transition, but it does not guarantee which.” · ตรงกับผลของ
go doc time.Dateบนgo1.27.0 -
Java SE 25 — ZonedDateTime (เข้าถึง 2026-10-02) — ที่มาของกฎของ
ZonedDateTime.ofที่เลื่อนเวลาใน gap ไปข้างหน้าเท่าความยาวของ gap -
ECMAScript — UTC ( t ) (เข้าถึง 2026-10-02) — ที่มาของกฎที่
Dateตีความเวลาที่ถูกข้ามไปด้วย offset ก่อนการเปลี่ยน -
PEP 495 — Local Time Disambiguation (เข้าถึง 2026-10-02) — ที่มาของกฎที่
fold=0ใช้ offset ก่อนการเปลี่ยนสำหรับเวลาใน gap และของข้อที่fromutc()ไม่มีทางคืนเวลาใน gap -
Python — zoneinfo (เข้าถึง 2026-10-02) — ที่มาของ
ZoneInfoที่บทนี้ใช้ และของข้อที่โซนของมันรองรับช่องfoldตาม PEP 495 -
chrono 0.4.45 — LocalResult (เข้าถึง 2026-10-02) — ที่มาของความหมายของ
LocalResult::None -
ค่าทุกค่าในบทนี้มาจาก
scripts/time-semantics/measured.jsonซึ่งscripts/time-semantics/probe.pyเขียนเมื่อ 2026-10-02 บน darwin/arm64 · probe วัดทั้งชุดสามรอบต่อการเรียกหนึ่งครั้ง และ Node รันด้วยTZที่ probe ตั้งเองทุกครั้ง
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3ส่ง `2026-03-08 02:30` ที่ `America/New_York` เข้าห้าภาษา ภาษาไหนบอกผู้เรียกว่าเวลานี้ไม่มีอยู่จริง