RelativeDateTimeFormatter เป็นคลาส Foundation ใน iOS และ macOS ที่แปลงวันที่แบบสัมบูรณ์เป็นวลีสัมพัทธ์ที่มนุษย์อ่านได้: "5 นาทีที่แล้ว", "เมื่อวานนี้", "ใน 3 วัน"> ตาม Apple Developer Documentation, 2024, RelativeDateTimeFormatter จะเลือกหน่วยที่เหมาะสม (วินาที นาที ชั่วโมง วัน) โดยอัตโนมัติและแปลผลลัพธ์เป็นภาษาของสถานที่ปัจจุบันของอุปกรณ์ แตกต่างจากการคำนวณความแตกต่างระหว่างวันที่ด้วยตนเองผ่าน Calendar คลาสนี้คำนึงถึงลักษณะทางภาษาของแต่ละภาษา: สำหรับบางภาษา ตัวเลขจะผันรูป สำหรับภาษาอื่น จะใช้รูปแบบพิเศษสำหรับคำว่า "เมื่อวานนี้" คลาสนี้พร้อมใช้งานตั้งแต่ iOS 13 และ macOS 10.15
ประเด็นสำคัญ
RelativeDateTimeFormatter เป็นคลาสย่อยของ Formatter ใน Foundation ที่รับ Date (หรือความแตกต่างเป็นวินาที) และส่งคืนสตริงที่แปลแล้วพร้อมเวลาสัมพัทธ์ ตัวอย่างเช่น สำหรับวันที่ 5 นาทีก่อนวันที่ปัจจุบัน จะส่งคืน "5 นาทีที่แล้ว" คลาสรองรับสามบริบทเวลา: อดีต อนาคต และปัจจุบัน
ตรรกะภายในของ RelativeDateTimeFormatter ใช้ Calendar และ Locale เพื่อคำนวณความแตกต่างระหว่างวันที่และเลือกรูปแบบไวยากรณ์ที่ถูกต้อง สำหรับภาษาไทย จะเลือกระหว่าง "1 นาทีที่แล้ว" และ "5 นาทีที่แล้ว" ฟังก์ชันการทำงานนี้ขึ้นอยู่กับข้อมูล ICU (International Components for Unicode) และไม่ต้องการการกำหนดค่าเพิ่มเติมจากนักพัฒนา
ตาม Apple WWDC 2019 RelativeDateTimeFormatter ได้กลายเป็นส่วนหนึ่งของเฟรมเวิร์กเพื่อทำให้การแปลง่ายขึ้น — ก่อนที่จะมีการนำเสนอ นักพัฒนาต้องคำนวณความแตกต่างของวันที่ด้วยตนเองและแทนที่สตริงที่แปลแล้วผ่าน String.localizedStringWithFormat สิ่งนี้นำไปสู่ข้อผิดพลาดในการผันคำ (โดยเฉพาะสำหรับภาษาสลาฟและอาหรับ) และการเลือกหน่วยวัดที่ไม่ถูกต้อง
อัลกอริทึม ของ RelativeDateTimeFormatter ประกอบด้วยสามขั้นตอน: การคำนวณความแตกต่างระหว่างวันที่ที่ส่งผ่านและช่วงเวลาปัจจุบัน การเลือกหน่วยที่เหมาะสม (ใหญ่ที่สุดที่ไม่ให้ค่าศูนย์) และการจัดรูปแบบตามสถานที่ ตัวอย่างเช่น สำหรับความแตกต่าง 3720 วินาที (1 ชั่วโมง 2 นาที) จะเลือกหน่วย "ชั่วโมง" และผลลัพธ์คือ "1 ชั่วโมงที่แล้ว" ไม่ใช่ "62 นาทีที่แล้ว"
หน่วยถูกเลือกตามหลักการ "ใหญ่ที่สุดที่ไม่เป็นศูนย์": หากความแตกต่างมากกว่า 86400 วินาที (1 วัน) จะใช้วัน หากมากกว่า 604800 (1 สัปดาห์) — จะใช้สัปดาห์ และอื่นๆ อัลกอริทึมนี้รับประกันว่าผลลัพธ์จะอ่านได้อย่างเป็นธรรมชาติเสมอ: แทนที่จะเป็น "518400 วินาทีที่แล้ว" ผู้ใช้จะเห็น "6 วันที่แล้ว" ขอบเขตที่แน่นอนของหน่วยถูกกำหนดโดยปฏิทินของสถานที่ปัจจุบัน
| ช่วงความแตกต่าง | หน่วย | ตัวอย่างสำหรับ th_TH |
|---|---|---|
| 0–59 วินาที | Seconds | 30 วินาทีที่แล้ว |
| 1–59 นาที | Minutes | 5 นาทีที่แล้ว |
| 1–23 ชั่วโมง | Hours | 3 ชั่วโมงที่แล้ว |
| 1–6 วัน | Days | 2 วันที่แล้ว |
| 7–27 วัน | Weeks | 1 สัปดาห์ที่แล้ว |
| 28 วัน–11 เดือน | Months | 3 เดือนที่แล้ว |
| 12+ เดือน | Years | 1 ปีที่แล้ว |
บริบทการจัดรูปแบบ กำหนดจุดสิ้นสุดของวลี สำหรับอดีต: "ที่แล้ว" (ภาษาไทย) สำหรับอนาคต: "ใน 3 วัน" (ภาษาไทย) สำหรับปัจจุบัน: "ตอนนี้" (ภาษาไทย) บริบทถูกกำหนดผ่านเมธอด localizeString(fromTimeInterval:) หรือโดยตรงผ่าน string(from: Date)
RelativeDateTimeFormatter มีการตั้งค่าหลายอย่างเพื่อควบคุมผลลัพธ์: คุณสมบัติ unitsStyle กำหนดสไตล์การจัดรูปแบบ (numeric, abbreviated, full, spellOut) และ maximumUnitCount จำกัดจำนวนหน่วยที่แสดง ตัวอย่างเช่น ด้วย maximumUnitCount = 1 ความแตกต่าง 1 ชั่วโมง 30 นาทีจะแสดงเป็น "1 ชั่วโมงที่แล้ว" แทนที่จะเป็น "1 ชั่วโมง 30 นาทีที่แล้ว"
การจำกัดหน่วย: โดยค่าเริ่มต้น RelativeDateTimeFormatter จะแสดงเพียงหนึ่งหน่วย (ใหญ่ที่สุด) การตั้งค่า maximumUnitCount = 2 จะรวมหน่วยถัดไปสำหรับคำอธิบายที่แม่นยำยิ่งขึ้น: "1 ชั่วโมง 30 นาทีที่แล้ว" อย่างไรก็ตาม สิ่งนี้อาจทำให้สตริงยาวเกินไปสำหรับข้อความสั้น (การแจ้งเตือนแบบพุช การแจ้งเตือน) สำหรับ UI แนะนำให้คง maximumUnitCount = 1
import Foundation
let formatter = RelativeDateTimeFormatter()
// Configure styles
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Examples with different dates
let fiveMinAgo = Date().addingTimeInterval(-300)
print("5 min ago: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")
let twoDaysLater = Date().addingTimeInterval(172800)
print("2 days later: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")
// Abbreviated style
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Abbreviated: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")
// Full style (spelled out)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Full: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")
การเลือกสไตล์สำหรับบริบทต่างๆ: สำหรับฟีดข่าว ให้ใช้ .numeric ด้วย maximumUnitCount = 1 — นี่เป็นมาตรฐานสำหรับ Twitter, Instagram และ Facebook สำหรับการเข้าถึง (VoiceOver) ให้ใช้ .full — ตัวเลขที่สะกดจะถูกอ่านอย่างเป็นธรรมชาติมากกว่า สำหรับองค์ประกอบขนาดกะทัดรัด (ป้ายการแจ้งเตือน แถบสถานะ) ให้ใช้ .abbreviated เพื่อประหยัดพื้นที่
การใช้งานพื้นฐาน ของ RelativeDateTimeFormatter ประกอบด้วยการสร้างอินสแตนซ์ การกำหนดค่าคุณสมบัติ และการเรียกหนึ่งในเมธอดการจัดรูปแบบ เมธอดหลักคือ: localizedString(for:relativeTo:) — สำหรับคู่วันที่ localizedString(fromTimeInterval:) — สำหรับความแตกต่างเป็นวินาที และ string(for:) — สำหรับ Date ด้วยบริบทอัตโนมัติ (อดีต/อนาคต)
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Social network UI examples
let postDates: [(title: String, date: Date)] = [
("Just now", Date().addingTimeInterval(-30)),
("5 min ago", Date().addingTimeInterval(-300)),
("Yesterday", Date().addingTimeInterval(-90000)),
("Last week", Date().addingTimeInterval(-700000)),
("Last year", Date().addingTimeInterval(-32000000))
]
for (title, postDate) in postDates {
let relative = formatter.localizedString(
for: postDate,
relativeTo: Date()
)
print("\(title): \(relative)")
}
// Future dates
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
for: inOneHour,
relativeTo: Date()
)
print("Reminder: \(reminderText)")
การจัดการสถานการณ์ "เมื่อกี้" — RelativeDateTimeFormatter ไม่มีการสนับสนุนในตัวสำหรับวลี "เมื่อกี้" สำหรับช่วงเวลาที่น้อยมาก สำหรับความแตกต่างน้อยกว่า 5 วินาที จะส่งคืน "0 วินาทีที่แล้ว" ซึ่งดูไม่ดีใน UI แนะนำให้ห่อการเรียกฟอร์แมตเตอร์ในตรรกะแบบมีเงื่อนไข: หากความแตกต่างน้อยกว่าเกณฑ์ที่กำหนด (เช่น 5 วินาที) — แสดง "เมื่อกี้" ด้วยตนเอง มิฉะนั้นให้ส่งวันที่ไปยังฟอร์แมตเตอร์
import Foundation
func relativeTimeString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
// "Just now" threshold
if interval < 5 {
return "just now"
}
// "Today" threshold
if interval < 60 {
return "just now"
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Display without "ago" suffix
return formatter.localizedString(
for: date,
relativeTo: Date()
)
}
print(relativeTimeString(from: Date().addingTimeInterval(-3)))
print(relativeTimeString(from: Date().addingTimeInterval(-120)))
print(relativeTimeString(from: Date().addingTimeInterval(-3600)))
เมธอด string(fromTimeInterval:) รับความแตกต่างเป็นวินาทีและกำหนดบริบทโดยอัตโนมัติ (ค่าบวก — อนาคต ค่าลบ — อดีต) สะดวกเมื่อทราบความแตกต่างอยู่แล้ว (เช่น ได้รับจากเซิร์ฟเวอร์เป็น timestamp unix) ในกรณีนี้ ไม่จำเป็นต้องสร้าง Date — ความแตกต่างจะถูกส่งโดยตรง
RelativeDateTimeFormatter จะแปลผลลัพธ์โดยอัตโนมัติตาม Locale.current ในการเปลี่ยนภาษาการจัดรูปแบบ ให้ตั้งค่าคุณสมบัติ locale — แตกต่างจาก DateFormatter สำหรับ RelativeDateTimeFormatter สถานที่ไม่ได้รับการแก้ไขและสามารถเปลี่ยนแปลงได้ในแต่ละการเรียก ซึ่งช่วยให้แสดงวันที่สัมพัทธ์ในภาษาที่แตกต่างจากภาษาอินเทอร์เฟซ (เช่น เนื้อหาในภาษาต้นฉบับ)
ความซับซ้อนของการแปลวันที่สัมพัทธ์อยู่ที่ลักษณะทางไวยากรณ์ของภาษาต่างๆ ภาษาไทยต้องการรูปแบบตัวเลขที่แตกต่างกัน: "1 นาที", "2 นาที" ภาษาอาหรับใช้รูปพหูพจน์สำหรับตัวเลขตั้งแต่ 3 ถึง 10 และรูปแบบพิเศษสำหรับ 11+ ภาษาจีนไม่มีการผันคำเลย ซึ่งทำให้งานง่ายขึ้น RelativeDateTimeFormatter ครอบคลุมทุกกรณีเหล่านี้ผ่านกฎ ICU โดยไม่ต้องใช้โค้ดเพิ่มเติม
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
let targetDate = Date().addingTimeInterval(-7200) // 2 hours ago
// Different locales
let locales: [String] = ["ru_RU", "en_US", "de_DE", "fr_FR", "ja_JP", "ar_SA"]
for identifier in locales {
formatter.locale = Locale(identifier: identifier)
let result = formatter.localizedString(
for: targetDate,
relativeTo: Date()
)
print("\(identifier): \(result)")
}
// Check Thai pluralization
formatter.locale = Locale(identifier: "ru_RU")
let intervals: [TimeInterval] = [-60, -120, -180, -300]
for interval in intervals {
let date = Date().addingTimeInterval(interval)
print("\(-Int(interval / 60)) min: \(formatter.localizedString(for: date, relativeTo: Date()))")
}
ความแตกต่างที่สำคัญ: RelativeDateTimeFormatter ไม่สนใจ TimeZone เมื่อคำนวณความแตกต่างสำหรับการตั้งค่า .numeric — ใช้ความแตกต่างสัมบูรณ์เป็นวินาที อย่างไรก็ตาม สำหรับสไตล์ .full (ด้วยตัวเลขที่สะกด) และกรณีพิเศษ (เมื่อวานนี้ วันนี้) จะพิจารณา TimeZone ตั้งค่า TimeZone อย่างชัดเจนเสมอเพื่อความสอดคล้อง โดยเฉพาะอย่างยิ่งหากแอปพลิเคชันทำงานกับวันที่ของเซิร์ฟเวอร์ใน UTC
การไม่สนใจ TimeZone เมื่อคำนวณวันที่สัมพัทธ์ — ข้อผิดพลาดทั่วไปเมื่อทำงานกับวันที่ของเซิร์ฟเวอร์ หากเซิร์ฟเวอร์ส่ง Date ใน UTC และ RelativeDateTimeFormatter ใช้ TimeZone.current ความแตกต่างอาจคำนวณไม่ถูกต้องสำหรับวันที่ใกล้กับช่วงเวลาปัจจุบัน แนะนำให้ตั้งค่า formatter.timeZone = TimeZone(secondsFromGMT: 0) สำหรับข้อมูลเซิร์ฟเวอร์เสมอ
การเลือกหน่วยที่ไม่ถูกต้องสำหรับช่วงเวลาสั้น — RelativeDateTimeFormatter จะปัดเศษความแตกต่างเป็นหน่วยที่ใหญ่ที่สุด สำหรับ 25 ชั่วโมง ผลลัพธ์จะเป็น "1 วันที่แล้ว" ซึ่งอาจทำให้ผู้ใช้เข้าใจผิด หากต้องการความแม่นยำสูง (เช่น สำหรับตัวจับเวลาถอยหลัง) ให้ใช้ DateComponentsFormatter แทน RelativeDateTimeFormatter — สามารถแสดงหลายหน่วยพร้อมกันได้
ขาดการตรวจสอบ TimeInterval ติดลบ — หากวันที่ในอนาคตถูกส่งผ่านเป็นอดีต (ค่าลบใน string(fromTimeInterval:)) ฟอร์แมตเตอร์อาจส่งคืนสตริงที่ไม่ถูกต้อง ตรวจสอบเครื่องหมายของช่วงเวลาก่อนส่งไปยังฟอร์แมตเตอร์เสมอ โดยเฉพาะเมื่อทำงานกับข้อมูลเซิร์ฟเวอร์ที่เขตเวลาอาจบิดเบือนการคำนวณ
ตาม Hacker News (2024) หนึ่งในปัญหาที่ถูกพูดถึงมากที่สุดของ RelativeDateTimeFormatter คือการขาดการสนับสนุนในตัวสำหรับ "เมื่อวานนี้" และ "วันนี้" สำหรับภาษาอังกฤษ แทนที่จะเป็น "เมื่อวานนี้" ฟอร์แมตเตอร์สำหรับความแตกต่าง 90000 วินาทีจะส่งคืน "1 วันที่แล้ว" สำหรับภาษาไทยไม่มีปัญหาดังกล่าว — "1 วันที่แล้ว" ฟังดูเป็นธรรมชาติ แต่สำหรับ UI ภาษาอังกฤษ "yesterday" เป็นที่ต้องการมากกว่า ฟังก์ชันการทำงานนี้ไม่ได้รับการสนับสนุนและต้องการการตรวจสอบด้วยตนเองผ่าน Calendar.isDateInToday/Yesterday
คำถามที่พบบ่อย
RelativeDateTimeFormatter เป็นคลาส Foundation สำหรับแสดงวันที่ในรูปแบบสัมพัทธ์: "5 นาทีที่แล้ว", "ใน 2 วัน" พร้อมใช้งานตั้งแต่ iOS 13 และ macOS 10.15
โดยหลักการหน่วยที่ใหญ่ที่สุดที่ไม่เป็นศูนย์ — วินาที นาที ชั่วโมง วัน สัปดาห์ เดือน หรือปี ตัวอย่างเช่น สำหรับความแตกต่าง 3720 วินาที (1 ชั่วโมง 2 นาที) จะเลือกหน่วย "ชั่วโมง" ไม่ใช่ "นาที"
ตั้งค่าคุณสมบัติ locale เป็นอินสแตนซ์ Locale ที่ต้องการ โดยค่าเริ่มต้นจะใช้ Locale.current ตัวอย่าง: formatter.locale = Locale(identifier: "de_DE") สำหรับภาษาเยอรมัน
.numeric — รูปแบบเต็ม ("3 วันที่แล้ว"), .abbreviated — รูปแบบย่อ ("3 ว. ที่แล้ว") การเลือกขึ้นอยู่กับบริบท: numeric สำหรับ UI หลัก abbreviated สำหรับองค์ประกอบขนาดกะทัดรัด
เพิ่มการตรวจสอบด้วยตนเอง สำหรับช่วงเวลาน้อยกว่า 5-10 วินาที RelativeDateTimeFormatter ไม่รองรับ "เมื่อกี้" — สำหรับช่วงเวลาเล็กจะส่งคืน "0 วินาทีที่แล้ว" ใช้ตรรกะแบบมีเงื่อนไขพร้อมเกณฑ์
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม