ทศนิยมที่กลืนจำนวนเต็ม และการแปลงกลับที่ตันอยู่ที่ขอบ
ระบบออเดอร์ของคุณออก id ด้วยตัวสร้างที่ให้ค่า 64 bit ออเดอร์ใบหนึ่งได้ id เป็น
9007199254740993 ซึ่งเป็นค่าที่ long ของ Java และ int64 ของ Go เก็บได้สบายๆ
id ใบนั้นเดินผ่าน service ตัวหนึ่งที่รับค่าไว้ใน double ระหว่างทาง ไม่มีใครเขียนคำสั่งให้ปัด
ไม่มีการ cast ที่มองเห็นได้ในบรรทัดนั้น และไม่มี error ออกมาสักตัว
ปลายทางได้ id เป็น 9007199254740992 ซึ่งน้อยกว่าค่าที่ต้นทางส่งมาอยู่หนึ่ง และมันยังเป็น
id ที่หน้าตาถูกต้องทุกประการ ไม่มีอะไรในค่านั้นบอกว่ามันเคยเป็นค่าอื่นมาก่อน
คำถามของบทนี้จึงไม่ใช่ว่าค่าไหนถูก แต่เป็นว่า ค่าหายไปตอนไหน และมีอะไรส่งเสียงบ้างหรือเปล่า คำตอบของครึ่งหลังคือไม่มีเลย
orderctl คือเครื่องมือประมวลผลออเดอร์ที่คอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในบทนี้รันเองได้ด้วย npm run verify:integer-semantics
ซึ่งเรียก python3 scripts/integer-semantics/probe.py · ไม่ต้องมี Docker และไม่ต้องต่อเน็ต
จำนวนเต็มสองค่าที่ double แยกไม่ออก
หัวข้อที่มีชื่อว่า “จำนวนเต็มสองค่าที่ double แยกไม่ออก”double เก็บส่วนของค่าไว้ 53 bit จำนวนเต็มที่ใหญ่กว่าช่วงนั้นจึงต้องถูกปัดไปหาค่าที่แทนได้
IEEE 754 doubleIEEE 754 doubleรูปแบบทศนิยม 64 bit ที่เก็บส่วนของค่าได้ 53 bit · จำนวนเต็มที่ใหญ่กว่าช่วงนั้นถูกปัดไปยังค่าที่แทนได้ใกล้ที่สุด โดยไม่มีสัญญาณใดออกมาเลย กว้าง 64 bit และส่วนที่เก็บตัวเลขจริงของมันกว้าง 53 bit เมื่อรวม bit ที่ไม่ได้เขียนลงไปตรงๆ ด้วย จำนวนเต็มทุกค่าที่อยู่ในช่วงนั้นเก็บได้ครบทุกค่า
พ้นช่วงนั้นไปแล้ว ค่าที่ double แทนได้จะเริ่มห่างกันทีละสอง แล้วทีละสี่ ค่าที่ตกอยู่ระหว่างกลาง ต้องถูกปัดไปหาค่าที่แทนได้ใกล้ที่สุด และช่วงที่เก็บได้ครบทุกค่านี้คือสิ่งที่ JavaScript เรียกว่า safe integersafe integerช่วงของจำนวนเต็มที่ double แทนได้ตรงตัวทุกค่า · นอกช่วงนี้ค่าสองค่าที่ต่างกันกลายเป็นค่าเดียวกันได้ และ Node ตอบว่า 2**53 === 2**53+1 เป็น true
probe วัดสองทรงนี้ใน Node และทั้งสองทรงเขียนลงในนิพจน์ตรงๆ ไม่ได้อ่านค่ามาจากที่ไหน
2 ** 53 === 2 ** 53 + 1ให้trueNumber.isSafeInteger(9007199254740993)ให้false
ทรงแรกคือค่าทางคณิตศาสตร์สองค่าที่ต่างกันอยู่หนึ่ง แล้วกลายเป็น double ตัวเดียวกัน
=== ไม่ได้ทำอะไรผิด มันเทียบสองค่าที่เท่ากันจริงหลังจากที่ทั้งคู่ถูกปัดมาแล้ว
ทรงที่สองคือด่านที่ตอบคำถามนี้ได้ตรงๆ Number.isSafeInteger คืน false ให้ 9007199254740993
ซึ่งแปลว่าค่านี้อยู่นอกช่วงที่ double แทนได้ครบทุกค่า และไม่มีใครเรียกมันถ้าไม่สงสัยอยู่ก่อน
MDN อธิบายช่วงนี้ไว้ด้วยขนาดของ mantissa คือ double เก็บ mantissa ไว้ 52 bit จึงแทนจำนวนเต็ม
ได้ตรงตัวเฉพาะระหว่าง -(2**53 - 1) กับ 2**53 - 1 แล้วยกตัวอย่างว่าการบวกหนึ่งกับการบวกสอง
เข้าไปที่ขอบบนของช่วงนั้นให้ค่าที่เทียบกันแล้วเท่ากัน ซึ่งเป็นผลที่ผิดตามคณิตศาสตร์
ค่าเดียวกันเดินผ่าน double แล้วออกมาไม่เท่าเดิม
หัวข้อที่มีชื่อว่า “ค่าเดียวกันเดินผ่าน double แล้วออกมาไม่เท่าเดิม”ห้าแถวนี้มาจากสามภาษา และสามแถวล่างที่ส่ง
9007199254740993ผ่าน double ให้ค่าเดียวกันหมด
ทรงที่ probe รันจริงเขียนไว้คนละภาษา จึงต้องอ่านรายการนี้ก่อนอ่านตาราง
- Node — เขียน
2 ** 53 === 2 ** 53 + 1กับNumber.isSafeInteger(9007199254740993)ลงในนิพจน์ตรงๆ - Java — ประกาศ
long exact = 9007199254740993Lแล้วเรียก(long) (double) exactซึ่งแปลงขึ้นเป็นdoubleหนึ่งครั้งแล้วแปลงกลับลงมาหนึ่งครั้ง - Java — เรียก
(long) Double.parseDouble("9007199254740993")โดยรับค่ามาจากสตริง ไม่ได้รับมาจาก literal ที่เป็นจำนวนเต็ม - Go — เรียก
strconv.ParseFloat("9007199254740993", 64)แล้วพิมพ์ค่าที่ได้ด้วย%.0f
| ทรง | ค่าที่วัดได้ |
|---|---|
Node 2 ** 53 === 2 ** 53 + 1 | true |
Node Number.isSafeInteger(9007199254740993) | false |
Java (long) (double) 9007199254740993L | 9007199254740992 |
Java (long) Double.parseDouble("9007199254740993") | 9007199254740992 |
Go strconv.ParseFloat("9007199254740993", 64) พิมพ์ด้วย %.0f | 9007199254740992 |
สามแถวล่างให้ค่าเดียวกันทุกตัวอักษร ทั้งที่เขียนด้วยสองภาษาและมาจากสามเส้นทาง สิ่งที่ตรงกัน ไม่ใช่การตัดสินใจของภาษา แต่เป็นชนิดที่รับค่าไว้ระหว่างทาง ซึ่งเป็น double ทั้งสามเส้น
ค่าที่ได้กลับมาคือ 9007199254740992 ซึ่งเป็นค่าที่ double แทนได้ตรงตัว มันจึงเป็นจำนวนเต็ม
ที่ถูกต้องทุกประการ และไม่มีอะไรในตัวมันบอกว่ามันเคยเป็นค่าที่มากกว่านี้อยู่หนึ่ง
นี่คือรูปของ การเสียความแม่นยำprecision lossการที่ค่าเปลี่ยนไปเพราะเดินผ่านชนิดที่เก็บมันได้ไม่ครบ · ต่างจากการล้นตรงที่ผลลัพธ์ยังอ่านดูสมเหตุสมผล จึงไม่มีใครกลับไปสงสัยมัน ซึ่งต่างจากการล้นที่สามบทแรกวัดไว้ การล้นให้ค่าที่ผิดจนสังเกตได้ เช่นยอดติดลบหรือค่าสัมบูรณ์ที่ยังติดลบ ส่วนการเสียความแม่นยำ ให้ค่าที่อ่านดูสมเหตุสมผล จึงไม่มีใครกลับไปสงสัยมัน
เส้นทางที่สอง · parser ที่ไม่มีใครสั่งให้ปัด
หัวข้อที่มีชื่อว่า “เส้นทางที่สอง · parser ที่ไม่มีใครสั่งให้ปัด”สองแถวล่างไม่มีใครพิมพ์การแปลงขึ้นเป็น double ลงไปเลย ชนิดที่ตัว parse คืนมาเป็นคนปัด
แถวของ (long) (double) exact มองเห็นได้ว่ามี double อยู่กลางทาง เพราะคนเขียนพิมพ์การแปลงนั้น
ลงไปเองทั้งขาขึ้นและขาลง
อีกสองแถวไม่มีใครพิมพ์ขาขึ้นลงไป ค่าที่เข้ามาเป็นสตริง และสิ่งที่ตัดสินว่าค่าจะเหลือเท่าไร
คือชนิดที่ตัว parse คืนกลับมา ซึ่งเป็น double ทั้งคู่
Double.parseDouble คืนค่าเป็น double และ strconv.ParseFloat รับ 64 เป็นความกว้าง
ของปลายทาง ทั้งสองตัวทำงานสำเร็จ ไม่ได้โยนอะไรออกมา และ err ของฝั่ง Go ที่ probe วัดไว้
เป็นค่าว่าง คือไม่มี error
JLS §5.1.2 อธิบายแถวของ (long) (double) exact ไว้ว่าการแปลงขึ้นจาก long ไป double
เสียความแม่นยำได้ ผลที่ได้คือค่าที่ปัดมาถูกต้องตามกฎการปัดไปหาค่าที่ใกล้ที่สุด และการแปลงขึ้น
แบบนี้ไม่เคยโยน exception ออกมาเลย
สองแถวของตัว parse ไม่ได้เดินผ่านการแปลงขึ้นนั้นเลย ค่าของมันปัดเพราะชนิดที่ตัว parse
คืนออกมาเป็น double ตั้งแต่แรก ไม่ใช่เพราะมีการแปลงขึ้นเขียนอยู่ในบรรทัด ปลายทางจึงเท่ากัน
ทั้งสามแถว
บทนี้เป็นเจ้าของค่าที่ double กลืนไป ไม่ว่าค่านั้นจะเดินเข้ามาทางการ cast หรือทางตัว parse
บทที่ 6 เอา body ของ JSON ใบเดียวกันเดินไปสี่ปลายทาง แล้ววัดว่าชนิดปลายทางเป็นคนตัดสิน ส่วนบทที่ 7 วัดนโยบายของตัว parse ที่คืนค่าเป็นจำนวนเต็ม ซึ่งแยกออกได้สี่แบบ
ทิศกลับ · ทศนิยมที่ใหญ่เกินชนิดปลายทาง
หัวข้อที่มีชื่อว่า “ทิศกลับ · ทศนิยมที่ใหญ่เกินชนิดปลายทาง”ค่าตั้งต้นคือ
1e19ตัวเดียวกัน แปลงลงชนิด 32 bit แล้วได้สองคำตอบคนละทิศ
ครึ่งแรกของบทนี้วัดจำนวนเต็มที่เดินเข้าไปใน double ครึ่งหลังวัดทิศกลับ คือทศนิยมที่เดินออกมา เป็นจำนวนเต็ม โดยที่ค่าของมันใหญ่เกินช่วงของชนิดปลายทางไปแล้ว
ทรงที่ probe รันมีสามทรง และทั้งสามใช้ค่าตั้งต้นเดียวกัน
- Java — ประกาศ
double big = 1e19แล้วเรียก(int) big - Rust — ประกาศ
let big: f64 = black_box(1e19)แล้วเรียกbig as i32 - Node — เขียน
1e19 | 0ลงในนิพจน์ตรงๆ
| ภาษา | นิพจน์ | ค่าที่วัดได้ | กฎที่สเปกของภาษาเขียนไว้ |
|---|---|---|---|
| Java | (int) 1e19 | 2147483647 | ค่าที่ใหญ่เกินไปให้ผลเป็นค่าที่แทนได้มากที่สุดของ int |
| Rust | 1e19f64 as i32 | 2147483647 | ค่าที่ใหญ่กว่าค่ามากที่สุดของชนิดนั้นจะตันอยู่ที่ค่ามากที่สุด |
| Node | 1e19 | 0 | -1981284352 | ToInt32 นิยามไว้เป็นการหารเอาเศษ ไม่ใช่การตัน |
สองแถวบนตันอยู่ที่ขอบ และค่าที่ได้ยังอยู่ฝั่งบวกเหมือนค่าตั้งต้น แถวล่างวนกลับไปอีกฝั่ง แล้วเปลี่ยนเครื่องหมายของค่าไปด้วย ทั้งที่ค่าตั้งต้นเป็นค่าบวกทั้งสามแถว
JLS §5.1.3 เขียนขั้นตอนของฝั่ง Java ไว้เป็นสองขั้น ขั้นแรกปัดทศนิยมเข้าหาศูนย์ให้เป็นจำนวนเต็ม
แล้วถ้าจำนวนเต็มนั้นใหญ่เกินกว่าที่ int แทนได้ ผลของขั้นแรกคือค่าที่แทนได้มากที่สุดของ int
ส่วนขั้นที่สองไม่ได้ทำอะไรต่อเมื่อปลายทางเป็น int อยู่แล้ว
The Rust Reference เขียนกฎของ as ไว้สั้นกว่านั้น คือการ cast จากทศนิยมไปเป็นจำนวนเต็มจะปัด
เข้าหาศูนย์ · NaN ให้ 0 · ค่าที่ใหญ่กว่าค่ามากที่สุดของชนิดปลายทางจะตันอยู่ที่ค่ามากที่สุด
และค่าที่เล็กกว่าค่าน้อยที่สุดจะตันอยู่ที่ค่าน้อยที่สุด
ฝั่ง JavaScript ไม่มีการตันอยู่ในเส้นทางของ | เลย ตัวดำเนินการนี้ไปถึง ToInt32 ผ่าน
NumberBitwiseOp ที่ §6.1.6.1.16 ซึ่งเรียก ToInt32 ใส่ทั้งสองข้างก่อนจะทำอะไรกับ bit
และ ToInt32 ที่ §7.1.8 ส่งค่าต่อให้ ToFixedSizeInteger ที่ §7.1.7
ค่าที่เข้าไปถึง ToFixedSizeInteger แล้วไม่ใช่อนันต์ จะเดินผ่านสองขั้นนี้
- ขั้นแรกเอาค่ามาหารเอาเศษด้วย
2ยกกำลังความกว้าง ซึ่งกรณีนี้คือ2ยกกำลัง32 - ขั้นที่สองถ้าเศษที่ได้ไม่น้อยกว่า
2ยกกำลัง31ให้ลบ2ยกกำลัง32ออกจากมัน
ขั้นที่สองคือขั้นที่พาค่าข้ามไปฝั่งลบ และมันเป็นการย้ายการอ่าน bit ชุดเดิม ไม่ใช่การคำนวณใหม่
ปลายทางของ | จึงอยู่ในช่วง 32 bit ที่มีเครื่องหมายเสมอ ไม่ว่าค่าที่ใส่เข้าไปจะใหญ่แค่ไหน
สเปกของ Go เขียนกฎของการแปลงนี้ไว้ตรงๆ ในหัวข้อ Conversions และคำที่มันใช้คือ implementation-dependent
In all non-constant conversions involving floating-point or complex values, if the resulttype cannot represent the value the conversion succeeds but the result value isimplementation-dependent.เครื่องที่ probe รันคือ go1.27.0 บน darwin/arm64 และค่าที่วัดได้บนเครื่องนั้นคือ int32(1e19)
ได้ 2147483647 กับ int64(1e19) ได้ 9223372036854775807
probe เก็บสองค่านี้ไว้ในช่อง architectureDependent ของ measured.json แล้วไม่ผูกมันเป็นข้ออ้าง
สักข้อ สถาปัตยกรรมอื่นตอบต่างออกไปได้ตามสเปก และ CI ของไซต์นี้รันบน linux/amd64 ซึ่งเป็นคนละ
สถาปัตยกรรมกับเครื่องที่วัด
ค่าที่สเปกไม่ได้ผูกไว้จึงไม่ใช่คำตอบสากล และตารางของการแปลง 1e19 ลงชนิด 32 bit ข้างบน
เป็นตารางของคำตอบสากล Go จึงไม่มีแถวอยู่ในนั้น ไม่ใช่เพราะ probe ไม่ได้วัด แต่เพราะสิ่งที่วัดได้
เป็นของเครื่องหนึ่งเครื่อง
flowchart TD
A["จำนวนเต็มเดินเข้ามาหา double"] --> B{"อยู่ในช่วงที่ double แทนได้ครบทุกค่าไหม"}
B -->|"อยู่ในช่วง"| C["ค่าเดิมทุกบิต"]
B -->|"เกินช่วง"| D["ปัดไปหาค่าที่แทนได้ใกล้ที่สุด · ไม่มีสัญญาณ"]
D --> E{"แปลงกลับลงชนิดจำนวนเต็มไหม"}
C --> E
E -->|"ค่าอยู่ในช่วงของปลายทาง"| F["ตัดทศนิยมเข้าหาศูนย์"]
E -->|"ค่าเกินช่วงของปลายทาง"| G{"สเปกของภาษาบอกว่าอย่างไร"}
G -->|"Java และ Rust · ตันที่ขอบ"| H["2147483647"]
G -->|"JavaScript · ToInt32 วนกลับ"| I["-1981284352"]
G -->|"Go · implementation-dependent"| J["สเปกไม่ผูกค่า · ขึ้นกับเครื่อง"]
คำบรรยายภาพ: ทางเดินสองท่อนต่อกันอยู่ในภาพเดียว ท่อนบนคือการเสียความแม่นยำที่ไม่มีสัญญาณ ท่อนล่างคือการแปลงกลับที่ปลายทางแคบกว่า และช่องขวาล่างสุดคือช่องที่ไม่มีค่าเขียนอยู่ เพราะสเปก ของภาษานั้นไม่ได้ผูกค่าไว้ให้
ความเสียหาย · id ที่ชี้ไปหาออเดอร์ใบอื่น
หัวข้อที่มีชื่อว่า “ความเสียหาย · id ที่ชี้ไปหาออเดอร์ใบอื่น”ค่าที่เปลี่ยนไปยังเป็น id ที่หน้าตาถูกต้อง และมันเป็น id ของออเดอร์ใบที่มีอยู่จริงได้
กลับไปที่ id 9007199254740993 ที่เปิดบทนี้ ค่าที่ปลายทางได้รับคือ 9007199254740992
ซึ่งเป็นค่าที่ตัวสร้าง id ชุดเดียวกันออกให้ออเดอร์อีกใบได้เหมือนกัน
- ถ้าออเดอร์ใบที่ถือ id
9007199254740992มีอยู่จริง คำสั่งอัปเดตจะลงที่ออเดอร์ใบนั้นแทน - ถ้าไม่มี ปลายทางจะตอบว่าหาไม่เจอ แล้วทีมจะไปหาสาเหตุที่ฝั่งที่ส่ง ซึ่งไม่ได้ทำอะไรผิดเลย
ทั้งสองทางไม่มี error ที่ชี้กลับมาที่จุดที่ค่าเปลี่ยน เพราะจุดนั้นไม่ได้คืน error อะไรออกมาเลย
ตั้งแต่แรก และ Number.isSafeInteger ก็ตอบคำถามนี้ให้ได้เฉพาะคนที่เรียกมัน
ฝั่งยอดเงินเป็นรูปเดียวกันแต่คมกว่า เพราะยอดที่เก็บเป็นจำนวนเต็มของหน่วยย่อยจะโตขึ้นเรื่อยๆ ตามจำนวนหลักของสกุลเงินและขนาดของบิล
คนที่เขียน JavaScript แล้วอยากบังคับให้ค่าเป็นจำนวนเต็มมักเติม | 0 ต่อท้ายนิพจน์ ซึ่งสั้นและ
ได้ผลตามที่คิดตราบใดที่ค่ายังเล็ก กฎที่สเปกเขียนไว้บอกว่าปลายทางของตัวดำเนินการนี้อยู่ในช่วง
32 bit เสมอ ค่าที่วัดได้จริงจึงเป็น 1e19 | 0 ได้ -1981284352 ซึ่งเป็นยอดติดลบจากยอดบวก
ทางแก้ทุกทางมีราคา และราคาคือสิ่งที่ทำให้ทีมเลือกไม่แก้
- ส่ง id เป็นสตริง ราคาคือทุกฝั่งต้องเปลี่ยนชนิดพร้อมกัน และการเรียงลำดับกับการเทียบช่วง ของ id จะไม่ใช่การเทียบตัวเลขอีกต่อไป
- ใช้ชนิด 64 bit ให้ครบทุกฝั่ง ราคาคือฝั่ง JavaScript ต้องใช้
BigIntซึ่งผสมกับNumberในนิพจน์เดียวกันไม่ได้ และทุกจุดที่รับค่าเข้ามาต้องแปลงเอง - ตรวจที่ขอบด้วย
Number.isSafeIntegerราคาคือต้องรู้ว่าขอบอยู่ตรงไหนบ้างก่อน และด่าน ที่ตรวจหลังจากค่าเดินผ่าน double มาแล้วบอกได้แค่ว่าค่าที่อยู่ตรงหน้าไม่ปลอดภัย มันบอกไม่ได้ ว่าค่าเดิมคืออะไร เพราะค่าเดิมหายไปก่อนที่ด่านจะได้เห็นมัน
ข้อสุดท้ายคือข้อที่พลาดกันบ่อยที่สุด ด่านต้องอยู่ที่จุดที่ค่ายังเป็นสตริงหรือยังเป็นจำนวนเต็ม 64 bit
ไม่ใช่ที่จุดที่มันเป็น Number ไปแล้ว ด่านที่ย้ายมาอยู่หลัง double ไม่ได้เงียบ แต่มันเตือนสายเกินไป
จนเหลือแต่ค่าที่ปัดแล้วให้ดู
จุดที่สี่ในห้าจุด
หัวข้อที่มีชื่อว่า “จุดที่สี่ในห้าจุด”บทนี้ปิดจุดที่สี่ และครึ่งแรกของมันเป็นครั้งแรกที่ค่าเปลี่ยนทั้งที่ชนิดปลายทางกว้างพอ
ธีสิสของคอร์สนี้คือ ค่าจำนวนเต็มค่าหนึ่งเปลี่ยนได้ห้าจุดระหว่างทางผ่านระบบ และทุกจุดในห้าจุดนั้น เงียบ จุดที่หนึ่งคือตอนความกว้างหมด จุดที่สองคือตอนคำตอบที่ถูกอยู่นอกชนิดเดิม จุดที่สามคือ ตอนที่แต่ละภาษาเลือกทิศการปัดของการหารไว้ไม่ตรงกัน
จุดที่สี่คือจุดที่บทนี้เพิ่งวัด และครึ่งแรกของมันต่างจากสามจุดแรกตรงที่ปลายทางกว้างพอ
long ของ Java เก็บ 9007199254740993 ได้สบาย สิ่งที่แคบคือชนิดที่รับค่าไว้ระหว่างทาง
ครึ่งหลังกลับไปมีรูปคล้ายจุดที่หนึ่ง คือค่าเกินช่วงของปลายทางจริงๆ แต่คำตอบแยกออกเป็นสองแบบ ที่ไม่เหมือนการล้นของจำนวนเต็ม คือการตันอยู่ที่ขอบ กับการวนกลับที่สเปกเขียนไว้เป็นการหารเอาเศษ
จุดนี้ยังเพิ่มของอีกอย่างที่สามจุดแรกไม่มี คือช่องที่สเปกของภาษาไม่ผูกค่าไว้ให้เลย ค่าที่วัดได้ ในช่องแบบนั้นเป็นของเครื่องที่วัด ไม่ใช่ของภาษา และการเอามันไปวางในตารางของคำตอบสากล คือการอ้างเกินสิ่งที่พิสูจน์ได้
บทที่ 6 ย้ายไปที่จุดที่ห้า คือพรมแดนของการ parse โดยเอา body ของ JSON บรรทัดเดียวเดินไปสี่ ปลายทาง แล้ววัดว่าอะไรเป็นคนตัดสินค่าที่ออกมา
-
ECMAScript® 2027 Language Specification — §7.1.8 ToInt32 และ §7.1.7 ToFixedSizeInteger (เข้าถึง 2026-09-13) — §7.1.8 ระบุว่า
ToInt32ส่งค่าต่อให้ToFixedSizeIntegerโดยระบุsignedกับความกว้าง32· §7.1.7 เขียนสองขั้นไว้สำหรับค่าที่ไม่ใช่อนันต์ คือหารเอาเศษด้วย2ยกกำลังความกว้าง แล้วลบ2ยกกำลังความกว้างออก เมื่อเศษนั้นไม่น้อยกว่า2ยกกำลังความกว้างลบหนึ่ง -
Number.MAX_SAFE_INTEGER — MDN (เข้าถึง 2026-09-13) — อธิบายว่า double เก็บ mantissa ไว้ 52 bit จึงแทนจำนวนเต็มได้ตรงตัว เฉพาะระหว่าง
-(2**53 - 1)กับ2**53 - 1· และระบุว่าคำว่า safe ในที่นี้หมายถึงการแทนค่า ได้ตรงตัวและเทียบค่ากันได้ถูกต้อง ไม่ได้หมายถึงอย่างอื่น -
JLS SE 25 — §5.1.2 Widening Primitive Conversion และ §5.1.3 Narrowing Primitive Conversion (เข้าถึง 2026-09-13) — §5.1.2 ระบุว่าการแปลงขึ้นจาก
longไปdoubleเสียความแม่นยำได้ ผลที่ได้คือค่าที่ปัดมาถูกต้องแบบปัดไปหาค่าที่ใกล้ที่สุด และการแปลงขึ้นไม่เคยโยน exception · §5.1.3 เขียนการแปลงลงจากทศนิยมไว้เป็นสองขั้น และระบุว่าค่าที่ใหญ่เกินไปให้ผลของขั้นแรก เป็นค่าที่แทนได้มากที่สุดของintหรือlong -
The Go Programming Language Specification — Conversions (เข้าถึง 2026-09-13) — ที่มาของประโยคที่ยกมาในบทนี้ทั้งบรรทัด ซึ่งระบุว่าการแปลงที่ไม่ใช่ ค่าคงที่และเกี่ยวกับทศนิยม จะสำเร็จเสมอเมื่อชนิดปลายทางแทนค่านั้นไม่ได้ แต่ค่าที่ได้เป็น implementation-dependent
-
The Rust Reference — Type cast expressions (เข้าถึง 2026-09-13) — ระบุว่า
asจากทศนิยมไปเป็นจำนวนเต็มปัดเข้าหาศูนย์ ·NaNให้0· ค่าที่ใหญ่กว่าค่ามากที่สุดของชนิดปลายทางตันอยู่ที่ค่ามากที่สุด และค่าที่เล็กกว่าค่าน้อยที่สุด ตันอยู่ที่ค่าน้อยที่สุด -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/integer-semantics/measured.jsonซึ่งscripts/integer-semantics/probe.pyเขียนขึ้นหลังยิงทั้งชุดสามรอบต่อการเรียกหนึ่งครั้ง · วัดบนgo1.27.0openjdk 25.0.4.1rustc 1.97.1และnode v24.19.0บน darwin/arm64 เมื่อ 2026-09-12
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3Node ตอบว่า `2 ** 53 === 2 ** 53 + 1` เป็นอะไร และเพราะอะไร