ข้าม​ไป​ยัง​เนื้อหา

วัน​ที่​ที่​ไม่มี​เวลา

ร้าน​ของ​คุณ​อยู่​ใน Bangkok ใบ​เสร็จ​ขอ​งอ​อเด​อร์​ใบ​หนึ่ง​พิมพ์​วัน​ส่ง​ของ​ไว้​ว่า 2026-10-02 ไม่มี​เวลา​และ​ไม่มี​โซน เพราะ​ร้าน​สัญญา​เป็น​วัน ไม่​ได้​สัญญา​เป็น​ชั่วโมง

ฟอร์ม​ส่ง​ค่า​นี้​เป็น​สตริง​ไป​ที่ service หลาย​ตัว ตัว​หนึ่ง​คัด​ออ​เด​อร์​ของ​แต่ละ​วัน​ให้​ทีม​จัด​รถ อีก​ตัว​ส่ง​ข้อความ​เตือน​ลูกค้า ทุก​ตัว​ต้อง​แปลง​สตริง​นี้​เป็น​เวลา​ก่อน​จะ​เปรียบ​เทียบ​อะไร​ได้

คำถาม​ของ​บท​นี้​คือ วัน​ส่ง​ของ 2026-10-02 กลาย​เป็น​วัน​อื่น​ได้​อย่างไร คำ​ตอบ​อยู่​ที่​ว่า​ใคร​เติม​เวลา และ​โซน​ที่​สตริง​ไม่​ได้​ให้​มา และ​ใน​ทรง​ที่ probe วัด ห้า​ภาษา​จัดการ​ช่อง​ว่าง​นั้น​ต่าง​กัน

📦 orderctl — ของ​ชิ้น​เดียวกัน​กับ​คอร์ส​ภาษา​อื่น​ใน​เสา​นี้

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ

ระหว่าง​นี้​ทุก​ค่า​ใน​บท​นี้​รัน​เอง​ได้​ด้วย npm run verify:time-semantics ซึ่ง​เรียก python3 scripts/time-semantics/probe.py · ไม่​ต้อง​มี Docker แต่​การ​รัน​ครั้ง​แรก ต้อง​ต่อ​เน็ต​เพื่อ​ดึง chrono กับ chrono-tz จาก crates.io

วัน​ส่ง​ของ​บน​ใบ​เสร็จ​บอก​วัน​บน​ปฏิทิน แต่​ยัง​ไม่​ได้​บอก​ว่า​วัน​นั้น​เริ่ม​ที่​จุด​ไหน​บน​เส้น​เวลา

2026-10-02 บน​ใบ​เสร็จ​คือ date-only valuedate-only valueค่าที่​มี​แค่​วัน​ที่ ไม่มี​เวลา​และ​ไม่มี​โซน เช่น​วัน​ส่ง​ของ · พอ​แปลง​เป็น instant แต่ละ​ภาษา​เติม​เวลา​กับ​โซน​ให้​เอง​คนละ​แบบ วัน​ที่​เดียวกัน​จึง​กลาย​เป็น​คนละ​วัน​ใน UTC ได้ (ค่าที่​มี​แค่​วัน​ที่) มัน​บอก​ว่า​เป็น​วัน​ไหน บน​ปฏิทิน แต่​ไม่มี​เวลา​และ​ไม่มี​โซน​ติด​มา​ด้วย

ระบบ​จัด​รถ​ต้องการ instant เพราะ​ต้อง​เทียบ​ว่า​ออ​เด​อร์​ไหน​อยู่​ใน​วัน​ไหน วัน​เดียวกัน​บน​ปฏิทิน​เริ่ม​คนละ instant ใน​แต่ละ​โซน การ​แปลง​วัน​ที่​เป็น instant จึง​ต้อง​มี​โซน​มา​ตอบ​ว่า​วัน​นั้น​เริ่ม​เมื่อไร

สตริง 2026-10-02 เขียน​ตาม ISO 8601ISO 8601มาตรฐาน​สากล​ของ​การ​เขียน​วัน​ที่​และ​เวลา​เป็น​สตริง เช่น 2026-10-02 หรือ 2026-10-02T09:00:00+07:00 · มัน​บอก​ได้​แค่ offset หรือ UTC ไม่มี​ช่อง​สำหรับ​ชื่อ​โซน (มาตรฐาน​การ​เขียน​วัน​ที่​และ​เวลา​เป็น​สตริง) สเปก ECMAScript เขียน​ว่า​รูป​แบบ​สตริง​ของ Date “is adapted from the ISO 8601 calendar date extended format”

สเปก​เดียวกัน​แบ่ง​รูป​แบบ​นั้น​เป็น​สอง​กลุ่ม คือ date-only forms กับ date-time forms และ date-time forms มี​เวลา​ต่อ​ท้าย “with an optional UTC offset representation appended” คือ offset เป็น​ของ​ที่​ไม่​บังคับ

Node อ่าน​สตริง​ที่​มี​แค่​วัน​ที่​เป็น UTC แต่​อ่าน​สตริง​ที่​มี​เวลา​ต่อ​ท้าย​ตาม TZ ของ process

ทุก​การ​เรียก​ใน​บท​นี้​ตัด​มา​จาก runner ของ probe ใน scripts/time-semantics/runners/ และ​ค่า​ใน​คอม​เมน​ต์ หรือ​ใน​ตาราง​คือ​ค่าที่ probe วัด​ได้

scripts/time-semantics/runners/shapes.mjs (เฉพาะ​ทรง D)
new Date("2026-10-02").toISOString();
new Date("2026-10-02T00:00").toISOString();

probe รัน file เดียวกัน​สอง​ครั้ง ต่าง​กัน​ที่ TZ อย่าง​เดียว

TZnew Date("2026-10-02")new Date("2026-10-02T00:00")
America/New_York2026-10-02T00:00:00.000Z2026-10-02T04:00:00.000Z
Asia/Bangkok2026-10-02T00:00:00.000Z2026-10-01T17:00:00.000Z

column ของ​สตริง​ที่​มี​แค่​วัน​ที่​ได้​ค่า​เดียวกัน​ทั้ง​สอง TZ คือ​เที่ยง​คืน​ของ UTC ส่วน column ของ​สตริง​ที่​มี T00:00 ได้​คนละ​ค่า เพราะ Node อ่าน​มัน​เป็น​เที่ยง​คืน​ใน​โซน​ของ process

กฎ​นี้​ไม่ใช่​ความ​บังเอิญ​ของ Node สเปก ECMAScript หัวข้อ Date.parse เขียน​ไว้​ว่า “When the UTC offset representation is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.”

new Date ที่​รับ​สตริง​ตัว​เดียว​ก็​ใช้​กฎ​เดียวกัน เพราะ​สเปก​ของ Date ( ...values ) เขียน​ว่า​มัน parse สตริง “in exactly the same manner as for the parse method”

flowchart LR
  A["วันส่งของ · 2026-10-02"] --> B{"สตริงมีเวลาต่อท้ายไหม"}
  B -->|"ไม่มี · date-only form"| C["Node อ่านเป็น UTC"]
  C --> D["2026-10-02T00:00:00.000Z · ทั้งสอง TZ ที่วัด"]
  B -->|"มี · ต่อ T00:00"| E{"TZ ของ process"}
  E -->|"America/New_York"| F["2026-10-02T04:00:00.000Z"]
  E -->|"Asia/Bangkok"| G["2026-10-01T17:00:00.000Z"]

คำ​บรรยาย​ภาพ: T00:00 ที่​ต่อ​ท้าย​สตริง​ไม่​ได้​เพิ่ม​แค่​เวลา มัน​เปลี่ยน​โซน​ที่ Node ใช้​อ่าน​ทั้ง​ค่า จาก UTC ที่​ตายตัว ไป​เป็น​โซน​ของ​เครื่อง​ที่​รัน

T00:00 ดู​เหมือน​วิธี​พูด​ว่า “เริ่ม​วัน” ให้​ชัด​ขึ้น แต่​สิ่ง​ที่​มัน​ทำ​จริง​คือ​ย้าย​การ​ตัดสิน​ใจ​เรื่อง​โซน จาก code ไป​อยู่​ที่​การ​ตั้ง​ค่า​ของ​เครื่อง

Go เติม UTC ให้ Python ไม่​เติม​โซน ส่วน Java กับ Rust ได้ instant เมื่อ​ผู้​เรียก​ส่ง​โซน​เข้าไป​เอง

scripts/time-semantics/runners/go/main.go (เฉพาะ​ทรง D)
d, err := time.Parse("2006-01-02", "2026-10-02")
// ...
d.Format(time.RFC3339) // 2026-10-02T00:00:00Z
d.Location().String() // UTC

layout 2006-01-02 ไม่มี​ช่อง​ของ​โซน Go จึง​คืน​เที่ยง​คืน​ที่ location UTC เอกสาร​ของ time.Parse เขียน​ไว้​ว่า “In the absence of a time zone indicator, Parse returns a time in UTC.”

ผล​นี้​ตรง​กับ Node ที่​อ่าน​สตริง​ที่​มี​แค่​วัน​ที่​ทั้ง​สอง TZ ที่​วัด ทั้ง​สอง​ภาษา​เลือก UTC ให้​โดย​ไม่​ถาม และ​ไม่มี​อะไร​ใน​ค่าที่​คืน​มา บอก​ว่า UTC เป็น​ค่าที่​ภาษา​เติม​ให้ ไม่ใช่​ค่าที่​สตริง​ให้​มา

scripts/time-semantics/runners/shapes.py (เฉพาะ​ทรง D)
from datetime import datetime
d = datetime.fromisoformat("2026-10-02")
str(d) # 2026-10-02 00:00:00
repr(d.tzinfo) # None

Python เติม​เวลา​เป็น​เที่ยง​คืนให้ แต่​ไม่​เติม​โซน ค่าที่​ได้​คือ naive datetimenaive datetimedatetime ที่​ไม่มี​โซน​หรือ offset ติด​มา · ใน Python คือ​ค่าที่ tzinfo เป็น None ค่า​นี้​บอก​เวลา​บน​หน้า​ปัด​ได้ แต่​บอก​ไม่​ได้​ว่า​เป็น​จุด​ไหน​บน​เส้น​เวลา (datetime ที่​ไม่มี​โซน​หรือ offset ติด​มา) คือ tzinfo เป็น None

เอกสาร​ของ datetime เขียน​ว่า “A naive object does not contain enough information to unambiguously locate itself relative to other date/time objects.” ค่า​นี้​จึง​ยัง​ไม่ใช่ instant มัน​รอ​ให้​ส่วน​อื่น​ของ​โปรแกรม ตัดสิน​ว่า​มัน​อยู่​ใน​โซน​ไหน

ข้อดี​คือ Python ไม่​ได้​เลือก​โซน​ผิด​ให้ ราคา​คือ​มัน​ไม่​ได้​เลือก​โซน​ให้​เลย และ​การ​ตัดสิน​นั้น​จะ​ไป​เกิด​ที่ไหน​สัก​แห่ง ใน code ที่​อาจ​ไม่มี​ใคร​ตั้งใจ​เขียน

scripts/time-semantics/runners/Shapes.java (เฉพาะ​ทรง D)
ZoneId bkk = ZoneId.of("Asia/Bangkok");
LocalDate.parse("2026-10-02").atStartOfDay(bkk).toInstant().toString(); // 2026-10-01T17:00:00Z

Javadoc นิยาม LocalDate ว่า “A date without a time-zone in the ISO-8601 calendar system” ชนิด​ของ Java จึง​แยก​วัน​ที่​ออก​จาก​เวลา​ตั้งแต่​ชื่อ

ใน​การ​เรียก​ที่ probe ใช้ ทาง​จาก LocalDate ไป​ถึง Instant ผ่าน atStartOfDay(bkk) ซึ่ง​รับ​โซน​เป็น argument Javadoc เขียน​ว่า method นี้ “Returns a zoned date-time from this date at the earliest valid time according to the rules in the time-zone.” คน​อ่าน code จึง​เห็น​ว่า​วัน​นั้น​เริ่ม​ใน​โซน​ไหน

Rust ได้ instant เมื่อ​ได้​โซน แต่ parse_from_rfc3339 ไม่​รับ​วัน​ที่​ล้วน

หัวข้อ​ที่​มีชื่อ​ว่า “Rust ได้ instant เมื่อ​ได้​โซน แต่ parse_from_rfc3339 ไม่​รับ​วัน​ที่​ล้วน”
scripts/time-semantics/runners/rs/src/main.rs (เฉพาะ​ทรง D)
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
use chrono_tz::Asia::Bangkok;
let d: NaiveDate = "2026-10-02".parse().unwrap();
let start = Bangkok.from_local_datetime(&d.and_hms_opt(0, 0, 0).unwrap()).single().unwrap();
start.with_timezone(&Utc).to_rfc3339(); // 2026-10-01T17:00:00+00:00
match DateTime::parse_from_rfc3339("2026-10-02") {
Ok(v) => format!("Ok({})", v.to_rfc3339()),
Err(e) => format!("Err({})", e), // Err(premature end of input)
};

ทาง​แรก​เริ่ม​จาก NaiveDate ซึ่ง​เป็น​วัน​ที่​ล้วน แล้ว​ต้อง​เติม​เวลา​เอง และ​ต้อง​เลือก​โซน​เอง​ด้วย Bangkok.from_local_datetime ซึ่ง​คืน LocalResult แบบ​เดียว​กับบท​ที่ 1

ทาง​ที่​สอง​ขอ date-time จาก​สตริง​ที่​มี​แค่​วัน​ที่ ค่าที่ runner บันทึก​คือ Err(premature end of input) เอกสาร​ของ chrono อธิบาย parse_from_rfc3339 ว่า “Parses an RFC 3339 date-and-time string” สตริง​ที่​ไม่มี​เวลา​จึง​ไม่ใช่​สิ่ง​ที่​มัน​รับ

รวม​ผล​ของ​ห้า​ภาษา​ไว้​ใน​ตาราง​เดียว พร้อม​โซน​ที่​ผู้​เรียก​ระบุ และ​รุ่น​ของ tzdata ที่​แต่ละ​ภาษา​อ่าน​จริง

ภาษาการ​เรียกโซน​ที่​ผู้​เรียก​ระบุค่าที่​วัด​ได้tzdata ที่​ใช้
Node v24.19.0 ที่ TZ=America/New_Yorknew Date("2026-10-02")ไม่มี2026-10-02T00:00:00.000Z2026b · ICU ที่​ฝัง​ใน node
Node v24.19.0 ที่ TZ=Asia/Bangkoknew Date("2026-10-02")ไม่มี2026-10-02T00:00:00.000Z2026b · ICU ที่​ฝัง​ใน node
Go go1.27.0time.Parse("2006-01-02", ...)ไม่มี2026-10-02T00:00:00Z · location UTC2026c · /usr/share/zoneinfo ของ​ระบบ
Python 3.14.6datetime.fromisoformat(...)ไม่มี2026-10-02 00:00:00 · tzinfo เป็น None2026c · /usr/share/zoneinfo ของ​ระบบ
Java openjdk 25.0.4.1LocalDate.parse(...).atStartOfDay(bkk)Asia/Bangkok2026-10-01T17:00:00Z2026b · tzdb.dat ของ JDK
Rust chrono-tz 0.10.4Bangkok.from_local_datetime(...)Asia/Bangkok2026-10-01T17:00:00+00:002025b · ฝัง​ใน chrono-tz

ทุก​แถว​ที่​ผู้​เรียก​ไม่​ได้​ระบุ​โซน ไม่​ได้ instant ที่​เป็น​จุด​เริ่ม​วัน​ของ​ร้าน​ใน Bangkok แถว​ของ Node กับ Go ได้​เที่ยง​คืน​ของ UTC และ​แถว​ของ Python ไม่​ได้ instant เลย

สอง​แถว​ที่​ระบุ Asia/Bangkok ได้ instant เดียวกัน เขียน​ต่าง​กัน​แค่​ท้าย​สตริง คือ Z ของ Java กับ +00:00 ของ Rust ซึ่ง​เป็น​จุด​เดียว​กับ​ที่ Node ให้​เมื่อ​รัน​ที่ TZ=Asia/Bangkok แล้ว​อ่าน 2026-10-02T00:00

ห้า​ภาษา​บน​เครื่อง​เดียวกัน​อ่าน tzdata สาม​รุ่น ตาราง​นี้​จึง​พิมพ์​รุ่น​ของ​ทุก​ภาษา เพราะ​คำ​ตอบ​ของ​คอร์ส​นี้​ขึ้น​กับ​ข้อมูล​ของ​โซน​ด้วย ไม่​ได้​ขึ้น​กับ code อย่าง​เดียว

สอง service ที่​เก็บ​วัน​ส่ง​ของ​ค่า​เดียวกัน ได้​คนละ instant และ service ที่​สาม​อ่าน​มัน​เป็น​คนละ​วัน

กลับ​ไป​ที่​ใบ​เสร็จ สมมุติ service ที่​ร้าน​เขียน​ด้วย Java แปลง​วัน​ส่ง​ของ​ด้วย​โซน​ของ​ร้าน มัน​เก็บ 2026-10-01T17:00:00Z เป็น​จุด​เริ่ม​วัน

service ของ​ทีม​จัด​รถ​เขียน​ด้วย Node แล้ว​ส่ง "2026-10-02" เข้า new Date ตรงๆ มัน​เก็บ 2026-10-02T00:00:00.000Z ไม่​ว่า​จะ​รัน​ที่ TZ=America/New_York หรือ TZ=Asia/Bangkok

วัน​ส่ง​ของ​ค่า​เดียวกัน​จึง​กลาย​เป็น​สอง instant สอง service ที่​เทียบ​วัน​ส่ง​ของ​ด้วย instant เห็น​ค่าที่​ไม่​เท่า​กัน ทั้ง​ที่​ใบ​เสร็จ​พิมพ์​วัน​เดียวกัน และ​ไม่มี service ไหน​แดง

ส่วน service ที่​สาม​รับ​ค่า​ของ Java ไป​แล้ว​ตัด​ส่วน​วัน​ที่​ของ 2026-10-01T17:00:00Z มา​แสดง สิ่ง​ที่​ได้​ไม่ใช่ 2026-10-02 ลูกค้า​เห็น​วัน​ส่ง​ของ​เป็น​วัน​ก่อน​หน้า​บน​หน้า​จอ​หนึ่ง และ​เห็น​วัน​ที่​ถูก​บน​ใบ​เสร็จ

ทาง​แก้​ที่​ดู​ง่าย​ที่สุด​คือ​ต่อ T00:00 ท้าย​สตริง​ก่อน​ส่ง​เข้า Node ผล​ที่​วัด​ได้​ขึ้น​กับ TZ ของ process:

  • ที่ TZ=Asia/Bangkok ได้ 2026-10-01T17:00:00.000Z ตรง​กับ​จุด​เริ่ม​วัน​ของ​ร้าน
  • ที่ TZ=America/New_York ได้ 2026-10-02T04:00:00.000Z ซึ่ง​เป็น​จุด​ที่​สาม

code บรรทัด​เดียวกัน​จึง​ถูก​บน​เครื่อง​หนึ่ง​และ​ผิด​บน​อีก​เครื่อง​หนึ่ง โดยที่ code ไม่​ได้​บอก​เลย​ว่า​มัน​พึ่ง TZ

ราคา​ของ​การ​เก็บ​วัน​ที่​ให้​เป็น​วัน​ที่

ทางออก​คือ​เก็บ​วัน​ส่ง​ของ​เป็น​วัน​ที่​ล้วน​คู่​กับ​ชื่อ​โซน​ของ​ร้าน แล้ว​แปลง​เป็น instant เฉพาะ​จุด​ที่​ต้อง​เทียบ​กับ​เวลา​จริง โดย​ส่ง​โซน​เข้าไป​เอง​ทุก​ครั้ง แบบ​ที่ atStartOfDay(bkk) ของ Java กับ Bangkok.from_local_datetime ของ Rust ทำ

ราคา​คือ​ทุก​จุด​ที่​แปลง​ต้อง​รู้​ชื่อ​โซน และ Date ของ Node ไม่มี​รูป​ที่​เป็น​วัน​ที่​ล้วน สตริง 2026-10-02 จึง​ต้อง​อยู่​เป็น​สตริง​จนถึง​จุด​ที่​รู้​โซน ห้าม​ส่ง​มัน​เข้า new Date ก่อน​หน้า​นั้น

บท​นี้​ปิด​จุด​ที่​สอง คือ​วัน​ที่​ที่​ไม่มี​เวลา​ไม่ใช่​จุด​บน​เส้น​เวลา จนกว่า​จะ​มี​ใคร​เลือก​โซน​ให้​มัน

บท​ที่ 1 วัด​เวลา​บน​หน้า​ปัด​ที่​ไม่มี​จุด​บน​เส้น​เวลา​เลย บท​นี้​วัด​ค่าที่​สั้น​กว่า​นั้น คือ​วัน​ที่​ที่​ไม่มี​ทั้ง​เวลา​และ​โซน และ​มัน​ก็​ยัง​ไม่ใช่​จุด​บน​เส้น​เวลา

ใน​ทรง​ที่ probe วัด สิ่ง​ที่​แยก​ห้า​ภาษา​ออก​จาก​กัน​คือ​ใคร​เป็น​คน​เลือก​โซน:

  • Node กับ Go เลือก UTC ให้​โดย​ไม่​ถาม และ Node เปลี่ยน​ไป​ใช้​โซน​ของ process ทันที​ที่​สตริง​มี T00:00
  • Python ไม่​เลือก​ให้ และ​คืน naive datetime ที่​ยัง​ไม่ใช่ instant
  • Java กับ Rust ได้ instant จาก​โซน​ที่​ผู้​เรียก​ส่ง​เข้าไป​เอง

การ​เรียก​เดียว​ที่​คืน error ใน​ทรง​นี้​คือ parse_from_rfc3339 ของ Rust ซึ่ง​เอกสาร​ของ chrono อธิบาย​ว่า​มัน parse RFC 3339 date-and-time string การ​เรียก​อื่น​ทุก​ตัว​คืน​ค่า​กลับ​มา​ให้​เงียบๆ

บท​ที่ 3 ถาม​ต่อ​จาก​ตรง​นี้ ถ้า​สตริง​ที่มา​กับ​สาย​มี offset ติด​มา​แล้ว offset นั้น​แทน​โซน​ได้​หรือ​ไม่


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • ECMAScript — Date Time String Format (เข้า​ถึง 2026-10-03) — ที่มา​ของ​ข้อ​ที่​รูป​แบบ​สตริง​ของ Date “is adapted from the ISO 8601 calendar date extended format” และ​ของ​การ​แบ่ง date-only forms กับ date-time forms

  • ECMAScript — Date.parse ( string ) (เข้า​ถึง 2026-10-03) — ที่มา​ของ​กฎ “When the UTC offset representation is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.”

  • ECMAScript — Date ( …values ) (เข้า​ถึง 2026-10-03) — ที่มา​ของ​ข้อ​ที่ new Date parse สตริง “in exactly the same manner as for the parse method”

  • Go — package time, func Parse (เข้า​ถึง 2026-10-03) — ที่มา​ของ​ประโยค “In the absence of a time zone indicator, Parse returns a time in UTC.”

  • Python — datetime, Aware and Naive Objects (เข้า​ถึง 2026-10-03) — ที่มา​ของ​นิยาม​ของ naive object

  • Java SE 25 — LocalDate (เข้า​ถึง 2026-10-03) — ที่มา​ของ​นิยาม​ของ LocalDate และ​ของ atStartOfDay(ZoneId)

  • chrono 0.4.45 — DateTime::parse_from_rfc3339 (เข้า​ถึง 2026-10-03) — ที่มา​ของ​ข้อ​ที่ parse_from_rfc3339 อ่าน date-and-time string

  • ค่า​ทุก​ค่า​ใน​บท​นี้​มา​จาก scripts/time-semantics/measured.json ซึ่ง scripts/time-semantics/probe.py เขียน​เมื่อ 2026-10-02 บน darwin/arm64 · probe วัด​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง และ Node รัน​ด้วย TZ ที่ probe ตั้ง​เอง​ทุก​ครั้ง

เช็กความเข้าใจ — บทที่ 2

ข้อ 1 / 3

Node ที่ `TZ=Asia/Bangkok` รับสตริง `2026-10-02` เข้า `new Date` แล้วสั่ง `toISOString()` ได้อะไร