Skip to content

บทที่ 08 — String, Regular Expression และวันเวลา

← บทที่ 07 | สารบัญ | บทที่ 09: Async →

💡 เคยเห็น regex มาก่อนหรือยัง? ใน บทที่ 07 Lab 3 เราใช้ /\s+/ กับ split แบบไม่ได้อธิบาย — บทนี้คือคำอธิบายเต็มของ regex ตัวนั้น

บทนี้รวม 3 เรื่องที่เจอในงานจริงตลอด — จัดการข้อความ, ค้นหา/ตรวจสอบ pattern ด้วย regex, และทำงานกับวันเวลา

อ่านจบบทนี้คุณจะ:

  • จัดการ string ได้คล่อง
  • เข้าใจ Regular Expression — เครื่องมือค้นหา pattern ที่ทรงพลัง
  • ทำงานกับวันเวลาได้ พร้อมรู้กับดักของ Date
  • แสดงผลตัวเลข/วันที่ตาม locale (ภาษาไทย พ.ศ.)

ใช้เวลา 2-3 ชั่วโมง


Part 1: String Methods

ทบทวนจากบทที่ 02 — เราเรียน string มาบ้างแล้ว บทนี้ลงลึกขึ้น

1.1 Methods ที่ใช้บ่อย

string มี method สำเร็จรูปให้จัดการข้อความมากมาย — ตัดช่องว่าง (trim), เปลี่ยนตัวพิมพ์ (toUpperCase), ค้นหา (includes, indexOf), แทนที่ (replace, replaceAll) รวมตัวที่ใช้บ่อยไว้ด้านล่าง:

javascript
const s = "  Hello, World!  ";

s.length                  // 17 — รวมช่องว่าง
s.trim()                  // "Hello, World!" — ตัดช่องว่างหน้า-หลัง
s.toUpperCase()           // "  HELLO, WORLD!  "
s.toLowerCase()
s.includes("World")       // true
s.startsWith("  Hello")   // true
s.endsWith("!  ")         // true
s.indexOf("World")        // 9 — ตำแหน่งที่เจอ (ไม่เจอคืน -1)
s.replace("World", "JS")  // แทนที่ครั้งแรก
s.replaceAll("l", "L")    // แทนที่ทุกตัว
s.repeat(3)               // ทำซ้ำ 3 รอบ

1.2 ตัด string — slice

slice ตัดเอาบางส่วนของ string ตามตำแหน่ง index (รองรับเลขติดลบเพื่อนับจากท้าย) — คืน string ใหม่ ไม่แก้ของเดิม:

javascript
const s = "JavaScript";
s.slice(0, 4)             // "Java" — index 0 ถึง 4 (ไม่รวม 4)
s.slice(4)                // "Script" — จาก index 4 ถึงท้าย
s.slice(-6)               // "Script" — 6 ตัวท้าย

1.3 แยก string — split

split แยก string ออกเป็น array ตามตัวคั่นที่กำหนด (เช่น comma, ช่องว่าง, หรือ "" เพื่อแยกทีละตัวอักษร) — ตรงข้ามกับ join ของ array:

javascript
"a,b,c".split(",")        // ["a", "b", "c"]
"hello".split("")          // ["h", "e", "l", "l", "o"]
"one two three".split(" ") // ["one", "two", "three"]

1.4 เติมให้ครบความยาว — padStart / padEnd

javascript
"5".padStart(3, "0")      // "005" — เติม 0 ข้างหน้าให้ครบ 3 ตัว
"5".padEnd(3, "-")        // "5--"

ใช้บ่อยตอนจัดรูปแบบ — เช่นแสดงเวลา 09:05:

javascript
const hour = 9, minute = 5;
`${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;  // "09:05"

1.5 ⚠️ String แก้ไม่ได้

ทบทวน — string เปลี่ยนแปลงไม่ได้ (immutable) ทุก method สร้าง string ใหม่ ไม่แก้ของเดิม:

javascript
let s = "hello";
s.toUpperCase();          // คืน "HELLO" แต่ s ยังเป็น "hello"
s = s.toUpperCase();      // ต้อง assign กลับ

1.6 Template Literal — ทบทวน

จากบทที่ 02 — `...` แทรกตัวแปรด้วย ${ } และเขียนหลายบรรทัดได้

ความสามารถเพิ่มเติม — tagged template (เทคนิคที่ใส่ชื่อฟังก์ชันหน้า backtick เพื่อให้ฟังก์ชันนั้นประมวลผล template literal แทนเรา):

🚀 โซนขั้นสูง — ข้ามได้: tagged template เป็นเทคนิคที่มือใหม่แทบไม่ได้ใช้เอง ข้ามได้เลย แค่รู้ว่ามีไว้ตอนเจอใน library โค้ดด้านล่างใช้ .reduce() ที่เรียนในบทที่ 07 — ถ้ายังไม่แม่นไม่ต้องพยายามอ่านตอนนี้

javascript
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? `**${values[i]}**` : "");
  }, "");
}

const name = "Alice";
highlight`สวัสดี ${name} ยินดีต้อนรับ`;
// "สวัสดี **Alice** ยินดีต้อนรับ"

ใช้น้อยสำหรับมือใหม่ — แต่ library บางตัว (เช่น styled-components) ใช้เทคนิคนี้


Part 2: Unicode — เรื่องที่ต้องรู้

2.1 ปัญหาความยาว emoji

javascript
"hello".length            // 5 — ปกติ
"😀".length               // 2 — ⚠️ อ้าว ทำไม 2?

JavaScript เก็บ string เป็น "หน่วยเล็ก ๆ" ที่เรียกว่า code unit ขนาด 16 บิต (bit คือหน่วยข้อมูลที่เล็กที่สุดในคอมพิวเตอร์ — ไม่ต้องเข้าใจลึกก็ได้) ตัวอักษรปกติใช้ 1 code unit แต่ emoji (และอักขระพิเศษบางตัว) ใช้ 2 code unit — เพราะแบบนี้ .length ของ "😀" ถึงนับเป็น 2

วิธีนับให้ถูก — ใช้ spread หรือ for...of:

javascript
[..."😀"].length          // 1 — ถูกต้อง
[..."hello😀"].length     // 6 — ถูกต้อง

⚠️ ยังไม่จบ — emoji รวม (ZWJ) และธงประเทศ: วิธี spread ใช้ได้กับ emoji เดี่ยว ๆ — แต่ emoji ที่เป็น "การรวมกัน" เช่น 👨‍👩‍👧 (ครอบครัว — รวมจาก 3 ตัวด้วยตัวเชื่อม ZWJ) หรือ 🇹🇭 (ธงไทย — รวมจาก 2 ตัว regional indicator) ยังถูกนับเกิน 1:

javascript
[..."👨‍👩‍👧"].length      // 5 — ❌ จริง ๆ ควรเป็น 1
[..."🇹🇭"].length          // 2 — ❌ จริง ๆ ควรเป็น 1

ถ้าต้องการนับ "ตัวอักษรที่ผู้ใช้เห็น" (grapheme cluster) จริง ๆ ใช้ Intl.Segmenter (ES2022):

javascript
const seg = new Intl.Segmenter("th", { granularity: "grapheme" });
[...seg.segment("👨‍👩‍👧")].length    // 1 — ถูกต้อง
[...seg.segment("🇹🇭")].length        // 1 — ถูกต้อง

→ ส่วนใหญ่งานทั่วไปไม่ต้องกังวล — แต่ถ้าทำงานกับ emoji/ภาษาที่ซับซ้อน ระวังเรื่องนี้


Part 3: Regular Expression — เครื่องมือค้นหา pattern

Regular Expression (Regex) = ชุดสัญลักษณ์สำหรับอธิบาย "รูปแบบ (pattern)" ของข้อความ — ใช้ค้นหา ตรวจสอบ แทนที่

ตัวอย่างที่ regex แก้ได้:

  • "ข้อความนี้เป็น email ที่ถูกต้องไหม"
  • "ดึงตัวเลขทั้งหมดออกจากข้อความ"
  • "แทนที่ช่องว่างซ้ำ ๆ ด้วยช่องว่างเดียว"

3.1 สร้าง Regex

สร้าง regex (รูปแบบสำหรับค้นหา/ตรวจข้อความ) ได้ 2 วิธี — เขียนตรง ๆ ระหว่าง / / (นิยมสุด) หรือสร้างจาก string ด้วย new RegExp() (ใช้เมื่อ pattern มาจากตัวแปร):

javascript
const re1 = /abc/;                    // เขียนตรง ๆ ระหว่าง / /
const re2 = new RegExp("abc");        // สร้างจาก string

3.2 Pattern พื้นฐาน — มาทำความรู้จักทีละตัว

ตัวอักษรพิเศษสำหรับ "ชนิดของตัวอักษร":

text
\d    = ตัวเลข 0-9
\w    = ตัวอักษร a-z, A-Z, 0-9, _ (word character)
\s    = ช่องว่าง (space, tab, ขึ้นบรรทัด)
.     = ตัวอักษรอะไรก็ได้ (ยกเว้นขึ้นบรรทัด)

\D    = ไม่ใช่ตัวเลข
\W    = ไม่ใช่ word character
\S    = ไม่ใช่ช่องว่าง

กำหนดกลุ่มตัวอักษรเอง:

text
[abc]    = a หรือ b หรือ c
[a-z]    = ตัวเล็ก a ถึง z
[0-9]    = ตัวเลข 0 ถึง 9 (เหมือน \d)
[^abc]   = อะไรก็ได้ที่ไม่ใช่ a, b, c

จำนวนครั้ง (quantifier):

text
*        = 0 ครั้งขึ้นไป
+        = 1 ครั้งขึ้นไป
?        = 0 หรือ 1 ครั้ง
{3}      = 3 ครั้งพอดี
{3,}     = 3 ครั้งขึ้นไป
{3,5}    = 3 ถึง 5 ครั้ง

ตำแหน่ง:

text
^        = ต้นข้อความ
$        = ท้ายข้อความ

อื่น ๆ:

text
|        = หรือ (เช่น cat|dog = "cat" หรือ "dog")
( )      = จับกลุ่ม

3.3 อ่าน pattern เป็นตัวอย่าง

วิธีเก่ง regex คือฝึก "อ่าน" ทีละส่วน — ลองดูตัวอย่าง pattern จริงด้านล่างแล้วแปลว่ามันหาอะไร โดยอาศัยตารางสัญลักษณ์จากหัวข้อก่อน:

มาฝึกอ่าน regex:

javascript
/\d+/              // ตัวเลข 1 ตัวขึ้นไป
/^\d{4}$/          // ตัวเลข 4 ตัวพอดี ตั้งแต่ต้นถึงท้าย (เช่น "2026")
/[a-z]+/           // ตัวอักษรเล็ก 1 ตัวขึ้นไป
/\s+/              // ช่องว่าง 1 ตัวขึ้นไป
/^0\d{8,9}$/       // เริ่มด้วย 0 ตามด้วยตัวเลข 8-9 ตัว (เบอร์โทรไทย)

3.4 Flags — ตัวเลือกท้าย regex

Flag คือ "ตัวเลือก" ที่เปิด/ปิดพฤติกรรมการค้นหา — ใส่ตัวอักษรหลัง / ปิดท้าย regex: g (หาทุกจุด ไม่ใช่แค่จุดแรก), i (ไม่สนตัวเล็ก-ใหญ่), m (ทำงานข้ามบรรทัด) ใส่หลายตัวพร้อมกันได้:

ใส่ตัวอักษรหลัง / ปิดท้าย:

javascript
/abc/g             // g = global — หาทุกจุด (ไม่ใช่แค่จุดแรก)
/abc/i             // i = ignore case — ไม่สนตัวเล็ก-ใหญ่
/abc/m             // m = multiline — ^ $ ทำงานต่อบรรทัด
/abc/gi            // ใส่หลายตัวได้

3.5 ใช้ Regex — test, match, replace

test — เช็คว่า match ไหม (คืน true/false):

javascript
/\d+/.test("abc123")      // true — มีตัวเลขไหม
/^\d+$/.test("abc123")    // false — เป็นตัวเลขล้วนไหม → ไม่
/^\d+$/.test("12345")     // true

match — ดึงสิ่งที่ match ออกมา:

javascript
"abc 123 def 456".match(/\d+/g)        // ["123", "456"] — ดึงตัวเลขทุกชุด

replace — แทนที่:

javascript
"hello   world".replace(/\s+/g, " ")   // "hello world" — ยุบช่องว่างซ้ำ
"abc123".replace(/\d/g, "*")            // "abc***" — แทนตัวเลขด้วย *

split ด้วย regex:

javascript
"a1b2c3".split(/\d/)       // ["a", "b", "c", ""] — แยกที่ตัวเลข
// หมายเหตุ: "" ท้ายสุดเกิดเพราะ string จบด้วย "3" (ตัวเลข) → split แล้วได้ string ว่างหลังตัวสุดท้าย

3.6 Capturing Group — ดึงส่วนย่อย

( ) จับกลุ่ม — ดึงส่วนย่อยของ match ออกมาได้:

javascript
const date = "2026-05-20";
const match = date.match(/(\d{4})-(\d{2})-(\d{2})/);
// match[0] = "2026-05-20" — ทั้งหมด
// match[1] = "2026" — กลุ่มแรก
// match[2] = "05" — กลุ่มสอง
// match[3] = "20" — กลุ่มสาม

⚠️ ระวัง: ถ้า string ไม่ match pattern เลย — match() คืน null ไม่ใช่ array ว่าง → ถ้าเข้าถึง null[0] จะ error วิธีป้องกัน — ใช้ optional chaining ?.:

javascript
const date = someString.match(/(\d{4})-(\d{2})-(\d{2})/)?.[0];  // undefined ถ้าไม่เจอ

Named group — ตั้งชื่อกลุ่ม อ่านง่ายขึ้น:

javascript
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups } = "2026-05-20".match(re);
console.log(groups.year);     // "2026"
console.log(groups.month);    // "05"

3.7 Pattern ที่ใช้บ่อยในงานจริง

รวม regex สำเร็จรูปที่ใช้บ่อยในงานจริง — ตรวจ email, เบอร์โทรไทย, ยุบช่องว่าง, ดึงตัวเลข ก๊อปไปปรับใช้ได้เลย:

javascript
// เช็ค email (แบบง่าย — สำหรับ feedback ทันทีฝั่ง frontend เท่านั้น)
/^[^\s@]+@[^\s@]+\.[^\s@]+$/

// เบอร์โทรไทย
/^0\d{8,9}$/

// ยุบช่องว่างหลายตัวเป็นช่องเดียว
text.replace(/\s+/g, " ")

// ตัดตัวอักษรที่ไม่ใช่ตัวเลขออก
"abc123def456".replace(/\D/g, "")     // "123456"

// ดึงตัวเลขทั้งหมด
"ราคา 250 บาท ลด 50".match(/\d+/g)    // ["250", "50"]

⚠️ อย่าใช้ regex ตรวจ email ใน production:

  • ไม่ครอบคลุมจริง — มาตรฐาน email (RFC 5322) ซับซ้อนมาก regex สั้น ๆ ปฏิเสธ email ถูก ๆ หลายแบบ และผ่าน email ผิด ๆ หลายแบบ
  • ReDoS — regex email ที่ "ดูครอบคลุม" หลายแบบใน internet (มักมี quantifier ซ้อนกัน เช่น (a+)+) มีปัญหา catastrophic backtracking — ผู้ใช้ส่ง input ที่จงใจมาให้ → server ค้างหลายวินาที (เป็น Denial of Service) — pattern ง่าย ๆ อย่าง /^[^\s@]+@[^\s@]+\.[^\s@]+$/ ที่ใช้ด้านบนไม่มี quantifier ซ้อนแบบนั้น จึงไม่เสี่ยง ReDoS แต่ยังไม่ครอบคลุม email ที่ถูกต้องตามมาตรฐานอยู่ดี
  • ถ้าจะใช้ฝั่ง frontend ให้ตรวจความยาวก่อนเสมอ (ป้องกัน input ที่ยาวผิดปกติ): if (email.length > 254) return false
  • ทางที่ดีกว่า:
    • HTML5: <input type="email"> ให้ browser ตรวจให้
    • Library ที่ผ่านการทดสอบมาแล้ว เช่น validator.js (isEmail)
    • ฝั่ง backend: ส่ง email ยืนยันจริง ๆ — เป็นวิธีเดียวที่พิสูจน์ว่า email ใช้งานได้

3.8 ⚠️ ข้อควรระวังของ Regex

  • อย่าใช้ regex แยก HTML/JSON — มันซับซ้อนเกินกว่าที่ regex จัดการได้ ใช้เครื่องมือเฉพาะ
  • regex ที่ซับซ้อนอ่านยาก — ใส่ comment อธิบาย หรือแยกเป็นขั้นตอน
  • เวลาสงสัย — ลองที่ regex101.com (เว็บทดสอบ regex ที่อธิบายแต่ละส่วนให้)

Part 4: Date — วันเวลา

Date คือ object สำหรับจัดการวันเวลา — และมันมี "กับดัก" เยอะ ต้องรู้

4.1 สร้าง Date

สร้าง Date ได้หลายวิธี — เวลาปัจจุบัน (new Date()), จาก string ISO, จากปี-เดือน-วัน, หรือจาก timestamp ส่วน Date.now() คืนเวลาปัจจุบันเป็นตัวเลข timestamp

💡 timestamp = จำนวนมิลลิวินาที (1/1000 วินาที) ที่นับตั้งแต่วันที่ 1 มกราคม 1970 UTC (เป็นมาตรฐานสากล) — ใช้เป็นตัวแทนของเวลาที่คอมพิวเตอร์จัดการได้ง่าย เช่น 1716163200000 คือตัวอย่างเลข timestamp หนึ่งค่า (ตัวเลขนี้เป็นแค่ตัวอย่างประกอบ ไม่ผูกกับปีปัจจุบันของหนังสือเล่มนี้)

javascript
new Date()                          // ตอนนี้
new Date("2026-05-20")              // จาก ISO string (⚠️ ดูกับดักด้านล่าง)
new Date("2026-05-20T10:30:00")     // มีเวลาด้วย (เวลาท้องถิ่น)
new Date(2026, 4, 20)               // ปี 2026, เดือน 4, วันที่ 20 (เวลาท้องถิ่น)
new Date(1716163200000)             // จาก timestamp (มิลลิวินาที)

Date.now()                           // timestamp ปัจจุบัน (เลขมิลลิวินาที)

⚠️ กับดัก ISO string: กฎนี้ตามมาตรฐาน ES2015+ (ไม่ใช่ quirk ของ engine):

  • "2026-05-20" (วันที่อย่างเดียว — ไม่มีเวลา) → อ่านเป็น UTC เที่ยงคืน
  • "2026-05-20T10:30:00" (มีเวลาแต่ไม่ระบุ TZ) → อ่านเป็น เวลาท้องถิ่น

สองแบบนี้ต่างกันคนละเรื่อง! ที่ไทย (+07): new Date("2026-05-20") จะเป็น 07:00 น. วันที่ 20 พ.ค. ตามเวลาท้องถิ่น — ดูแสดงผลก็เหมือนถูก แต่ถ้าทำ "วันที่ลบกัน" หรือเช็ค getDate() อาจเลื่อนไปวันก่อนหน้าได้ (โดยเฉพาะใน timezone ตะวันตกของ UTC)

ทางที่ปลอดภัยกว่า — ใช้รูปแบบที่ "บอก TZ ชัด" หรือใช้ตัวเลข:

javascript
new Date(2026, 4, 20);                          // เวลาท้องถิ่น เที่ยงคืน 20 พ.ค.
new Date("2026-05-20T00:00:00+07:00");          // ระบุ TZ ไทยชัด ๆ
new Date("2026-05-20T00:00:00Z");               // ระบุ UTC ชัด ๆ

4.2 ⚠️ กับดักที่ 1 — เดือนเริ่มนับจาก 0

javascript
new Date(2026, 4, 20)

4 ในที่นี้ ไม่ใช่เดือนเมษายน — แต่เป็นเดือนพฤษภาคม!

เพราะ Date นับเดือนจาก 0:

text
0 = มกราคม
1 = กุมภาพันธ์
...
4 = พฤษภาคม
11 = ธันวาคม

นี่คือกับดักที่ทำให้มือใหม่ (และมือเก่า) พลาดบ่อยมาก — จำให้ขึ้นใจ: เดือนใน Date เริ่มจาก 0

4.3 อ่านค่าจาก Date

ดึงส่วนต่าง ๆ ของวันเวลาออกมาด้วย method get... — ปี เดือน วัน ชั่วโมง ฯลฯ (ย้ำกับดัก: getMonth() คืนเดือนที่เริ่มนับจาก 0):

javascript
const d = new Date("2026-05-20T14:30:45");
// ⚠️ string นี้ไม่ระบุ TZ → ถูกอ่านเป็นเวลาท้องถิ่น
// getHours() คืนชั่วโมงตาม timezone ของเครื่องที่รัน
// ถ้ารันในไทย (UTC+7) จะได้ 14 — แต่ถ้ารันใน UTC+0 จะได้ 14 เช่นกัน (เพราะ string นี้ local)

d.getFullYear()      // 2026
d.getMonth()         // 4 — ⚠️ พฤษภาคม (เริ่มจาก 0)
d.getDate()          // 20 — วันที่
d.getDay()           // วันในสัปดาห์ (0=อาทิตย์, 1=จันทร์, ...)
d.getHours()         // 14 — ชั่วโมงตามเวลาท้องถิ่น (ค่านี้เปลี่ยนตาม timezone)
d.getMinutes()       // 30
d.getSeconds()       // 45
d.getTime()          // timestamp (มิลลิวินาที)

4.4 ⚠️ กับดักที่ 2 — Date แก้ของเดิม

javascript
const a = new Date("2026-01-01");
const b = a;                      // b ชี้ Date ตัวเดียวกับ a (จำเรื่อง reference)
b.setFullYear(2030);              // แก้ผ่าน b
console.log(a.getFullYear());     // 2030 — ⚠️ a เปลี่ยนด้วย!

Date เป็น object → copy แบบ reference → ระวังการแก้

ถ้าอยากได้ Date ใหม่ที่ไม่กระทบเดิม:

javascript
const copy = new Date(original);

4.5 แสดงผลวันที่ — ใช้ Intl ดีกว่าทำเอง

อย่าจัดรูปแบบวันที่เอง (d.getDate() + "/" + d.getMonth()) — มันยุ่งและไม่รองรับหลายภาษา

ใช้ Intl.DateTimeFormat — มันจัดรูปแบบตาม "ภาษา/ประเทศ" (เรียกเป็นทางการว่า locale) ให้:

javascript
// ใช้ตัวเลขแทน ISO string เพื่อหลีกเลี่ยงกับดัก UTC vs local (ดู 4.1)
const d = new Date(2026, 4, 20);  // เวลาท้องถิ่น เที่ยงคืน 20 พ.ค. 2026

// ภาษาไทย — ได้ พ.ศ. ด้วย!
new Intl.DateTimeFormat("th-TH", {
  year: "numeric",
  month: "long",
  day: "numeric",
  timeZone: "Asia/Bangkok",       // ⚠️ ระบุ timezone ชัด ๆ เพื่อผลลัพธ์คงที่ทุก environment
}).format(d);
// "20 พฤษภาคม 2569"

// ภาษาอังกฤษ
new Intl.DateTimeFormat("en-US", { timeZone: "Asia/Bangkok" }).format(d);
// "5/20/2026"

// ญี่ปุ่น
new Intl.DateTimeFormat("ja-JP", { timeZone: "Asia/Bangkok" }).format(d);
// "2026/5/20"

💡 ถ้าไม่ระบุ timeZone — ผลลัพธ์จะเปลี่ยนตาม timezone ของ runtime (เครื่องผู้ใช้หรือ server) ซึ่งอาจทำให้แสดงวันที่ผิดได้ในบาง timezone

Intl ฉลาด — ภาษาไทยมันแปลงเป็น พ.ศ. ให้อัตโนมัติ (2569 = 2026 + 543)

4.6 คำนวณวันเวลา

คำนวณวันเวลาทำได้ด้วย set... (บวก/ลบวัน) และการลบ Date สองตัวได้ผลเป็นมิลลิวินาที (หารเพื่อแปลงเป็นวัน) — แต่งานจริงที่ซับซ้อนแนะนำใช้ library (ดูหัวข้อ 4.7):

javascript
const now = new Date();

// บวก 7 วัน
const nextWeek = new Date(now);
nextWeek.setDate(now.getDate() + 7);

// หาความต่างเป็นวัน
const start = new Date("2026-01-01");
const end = new Date("2026-12-31");
const diffMs = end - start;                          // ความต่างเป็นมิลลิวินาที
const diffDays = diffMs / (1000 * 60 * 60 * 24);     // แปลงเป็นวัน
console.log(diffDays);     // 364

💡 ทำไม 364 ไม่ใช่ 365? เพราะ 1 ม.ค. ถึง 31 ธ.ค. มี "ช่วงห่าง" = 364 วัน (ไม่นับ 1 ม.ค. เองเป็น 1 วันของความต่าง) — ปี 2026 มี 365 วันในปฏิทิน แต่ความต่างระหว่างวันแรกกับวันสุดท้ายของปีคือ 364 วัน เหมือนเดิน 365 ก้าว แต่ระยะห่างระหว่างก้าวแรกกับก้าวสุดท้ายคือ 364 ช่วง

ในบางประเทศที่มี DST (เปลี่ยนเวลาฤดูร้อน/หนาว) ค่านี้อาจกลายเป็นเลขทศนิยม .999... หรือ .041... เพราะวันนั้นมี 23 หรือ 25 ชั่วโมง — ไทยไม่มี DST ก็เลยไม่เจอกับดักนี้ แต่ถ้าทำงานข้ามประเทศต้องระวัง

ถ้าทำงานกับวันเวลาข้ามประเทศบ่อย ๆ ใช้ library อย่าง date-fns ช่วยคำนวณจะปลอดภัยกว่า

4.7 ในงานจริง — ใช้ library

Date ของ JavaScript จัดการยาก — งานจริงมักใช้ library:

  • date-fns — ชุดฟังก์ชันจัดการวันเวลา (แนะนำ)
  • Day.js — เล็ก เบา

💡 import คืออะไร? — คำสั่งดึง library มาใช้ จะเรียนละเอียดในบทที่ 10 ตอนนี้แค่รู้ว่า date-fns ต้องติดตั้งก่อน (npm install date-fns) แล้วนำเข้าแบบนี้

javascript
import { format, addDays, differenceInDays } from "date-fns";

format(new Date("2026-05-20"), "yyyy-MM-dd");  // "2026-05-20" — ใช้วันที่แน่นอนให้ผลคงที่
addDays(new Date(), 7);                         // บวก 7 วัน
differenceInDays(end, start);                   // หาความต่าง

→ มี Temporal ที่เป็น API ใหม่ของ JS สำหรับวันเวลาโดยเฉพาะ — ออกแบบมาแก้กับดักของ Date ทั้งหมด (เดือนเริ่มจาก 1, immutable, แยก "วันที่อย่างเดียว" กับ "วันที่+เวลา" ชัดเจน, จัดการ timezone ตรง ๆ)

สถานะ Temporal ใน TC39 และการรองรับใน engine ต่าง ๆ (Node.js, Chrome, Safari) เปลี่ยนแปลงเร็ว — ตรวจสอบสถานะและเวอร์ชันล่าสุดก่อนใช้งานจริง ที่ เพจ Temporal proposal หรือ MDN — ถ้ายังไม่แน่ใจว่า environment ที่ใช้รองรับหรือยัง ให้ใช้ polyfill (@js-temporal/polyfill) หรือ date-fns ไปก่อนเพื่อความปลอดภัย


Part 5: จัดรูปแบบตัวเลขด้วย Intl

นอกจากวันที่ — Intl จัดรูปแบบตัวเลขได้ด้วย

javascript
const n = 1234567.89;

// ใส่ comma คั่นหลักพัน
new Intl.NumberFormat("en-US").format(n);
// "1,234,567.89"

// แสดงเป็นเงินบาท
new Intl.NumberFormat("th-TH", {
  style: "currency",
  currency: "THB",
}).format(n);
// "฿1,234,567.89"

// แสดงเป็นเปอร์เซ็นต์
new Intl.NumberFormat("th-TH", { style: "percent" }).format(0.25);
// "25%"

// แบบย่อ
new Intl.NumberFormat("en", { notation: "compact" }).format(1234567);
// "1.2M"

→ ใช้ Intl.NumberFormat ทุกครั้งที่แสดงตัวเลขให้ผู้ใช้เห็น — มันจัดการ comma, สกุลเงิน, locale ให้หมด


Part 6: Lab — ลงมือทำ

Lab 1: จัดการ String

ฝึกรวม string method หลายตัวเป็นสายเดียวเพื่อทำ "title case" (ขึ้นต้นแต่ละคำด้วยตัวพิมพ์ใหญ่) — toLowerCasesplitmapjoin:

javascript
function titleCase(str) {
  return str
    .toLowerCase()
    .split(" ")
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(" ");
}

console.log(titleCase("hello WORLD from js"));
// "Hello World From Js"

Lab 2: Validate ด้วย Regex

ฝึกใช้ regex ตรวจความถูกต้องของข้อมูล (email, เบอร์โทร, รหัสไปรษณีย์) โดยเก็บ pattern ไว้ใน object แล้วเลือกใช้ตามชนิด:

javascript
function validate(value, type) {
  const patterns = {
    email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    phone: /^0\d{8,9}$/,
    zipcode: /^\d{5}$/,
  };
  return patterns[type]?.test(value) ?? false;
}

console.log(validate("a@example.com", "email"));   // true
console.log(validate("0812345678", "phone"));       // true
console.log(validate("123", "zipcode"));             // false

Lab 3: ดึงข้อมูลด้วย Regex

ฝึกใช้ regex ดึงข้อมูลเฉพาะส่วน (วันที่, user id) ออกจากข้อความ log — รวม match กับ capturing group และ optional chaining:

javascript
const log = "Error at 2026-05-20 10:30:45 - User 12345 failed";

const date = log.match(/\d{4}-\d{2}-\d{2}/)?.[0];
const userId = log.match(/User (\d+)/)?.[1];

console.log("วันที่:", date);      // "2026-05-20"
console.log("User ID:", userId);   // "12345"

Lab 4: นับเวลาที่ผ่านมา

💡 Intl.RelativeTimeFormat เป็น API เพิ่มเติมของ Intl (เหมือน Intl.DateTimeFormat ที่เรียนใน 4.5) ใช้แปลงตัวเลขเวลาเป็นข้อความเปรียบเทียบ — เช่น -5 กับ "minute" → ไทยได้ "5 นาทีที่แล้ว", อังกฤษได้ "5 minutes ago" — ใส่ค่าลบ = อดีต, ค่าบวก = อนาคต

Lab นี้ใช้ Intl.RelativeTimeFormat เพื่อแปลงเวลาที่ผ่านมาเป็นข้อความที่ผู้ใช้อ่านออก:

javascript
function timeAgo(date) {
  const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
  const rtf = new Intl.RelativeTimeFormat("th", { numeric: "auto" });

  // กรณี edge: ถ้าวันที่อยู่ในอนาคต (seconds < 0) หรือเพิ่งเกิดขึ้น (= 0)
  if (seconds <= 0) return "เมื่อสักครู่";

  if (seconds < 60) return rtf.format(-seconds, "second");
  if (seconds < 3600) return rtf.format(-Math.floor(seconds / 60), "minute");
  if (seconds < 86400) return rtf.format(-Math.floor(seconds / 3600), "hour");
  return rtf.format(-Math.floor(seconds / 86400), "day");
}

const past = new Date(Date.now() - 5 * 60 * 1000);   // 5 นาทีที่แล้ว
console.log(timeAgo(past));     // "5 นาทีที่แล้ว"

const future = new Date(Date.now() + 60_000);        // 1 นาทีในอนาคต
console.log(timeAgo(future));   // "เมื่อสักครู่" — กัน edge case

💡 ขยายเพิ่ม: ถ้าอยากบอก "อีก 5 นาที" สำหรับอนาคต — เปลี่ยนเงื่อนไขให้ใช้ Math.abs(seconds) แล้วเช็คทิศทาง (ถ้า seconds < 0 ส่งค่าบวกเข้า rtf.format แทนค่าลบ)


Part 7: Checkpoint

  1. ทำไม "😀".length เป็น 2? นับให้ถูกยังไง?
  2. String แก้ของเดิมได้ไหม?
  3. Regex \d, \w, \s แทนอะไร?
  4. Quantifier *, +, ? ต่างกันยังไง?
  5. flag g กับ i ทำอะไร?
  6. test ต่าง match ยังไง?
  7. Capturing group ( ) ใช้ทำอะไร?
  8. ทำไม new Date(2026, 4, 20) คือพฤษภาคม ไม่ใช่เมษายน?
  9. Date เป็น object — กระทบเรื่องการ copy ยังไง?
  10. ทำไมควรใช้ Intl แทนจัดรูปแบบวันที่/ตัวเลขเอง?

Part 8: สรุปบทนี้

  • String methods: trim, slice, split, replace, padStart — ทุกตัวสร้าง string ใหม่ (ไม่แก้เดิม)
  • Unicode: emoji ใช้ 2 code unit — "😀".length เป็น 2 — นับด้วย [...str]
  • Regex = ภาษาอธิบาย pattern — \d \w \s, quantifier * + ?, flags g i m
  • ใช้ regex: test (เช็ค), match (ดึง), replace (แทนที่)
  • Date กับดัก: เดือนเริ่มจาก 0, เป็น object (copy by reference)
  • ใช้ Intl จัดรูปแบบวันที่/ตัวเลขตาม locale — อย่าทำเอง
  • งานจริงใช้ library (date-fns) จัดการวันเวลา

บทต่อไป — Async: เรื่องที่ยากที่สุดและสำคัญที่สุดของ JavaScript


← บทที่ 07 | สารบัญ | บทที่ 09: Async →