Calendar — nó là gì, lịch Date và tính toán trong Swift

Tác giả: IT Sectr Đã đăng: 2026-07-12 Thời gian đọc: 7 phút

Calendar là một lớp Foundation định nghĩa hệ thống lịch và cung cấp các phương thức cho tính toán lịch: trích xuất thành phần ngày, tính hiệu giữa các ngày, tìm ranh giới khoảng thời gian và dời chuyển ngày. Lịch kết nối thời gian tuyệt đối (Date) với các thành phần có thể đọc được bởi con người và tính đến các đặc điểm khu vực: ngày đầu tuần, múi giờ và giờ mùa hè. Theo Tài liệu Nhà phát triển Apple (2025), Foundation hỗ trợ 17 hệ thống lịch — từ Gregory đến Phật lịch và Nhật lịch — làm cho Calendar trở thành công cụ phổ quát cho các ứng dụng quốc tế hóa.

Các điểm chính

  • Calendar — một lớp Foundation cho tính toán lịch: trích xuất thành phần, so sánh và dời chuyển ngày.
  • Calendar.current — lịch hệ thống của người dùng, tự động bao gồm cài đặt khu vực.
  • 17 hệ thống lịch — Foundation hỗ trợ Gregory, Phật lịch, Nhật lịch, Do Thái, Hồi giáo và các loại khác.
  • Calendar.dateComponents trích xuất các thành phần (năm, tháng, ngày) từ Date có tính đến múi giờ.
  • Calendar.dateInterval trả về ngày bắt đầu và kết thúc của một khoảng thời gian chỉ định (ngày, tuần, tháng).

Calendar trong Foundation là gì?

Calendar là một lớp Foundation thực hiện các tính toán lịch dựa trên ICU (International Components for Unicode). Lịch xác định cách thời gian tuyệt đối (Date) ánh xạ tới các thành phần lịch: năm, tháng, ngày, giờ, phút, giây. Không có Calendar thì không thể biết được hôm nay là năm nào, tháng nào, ngày nào — bản thân Date không chứa thông tin này.

Lịch tính đến ba nhóm tham số: hệ thống lịch (Gregory, Phật lịch, Nhật lịch), múi giờkhu vực. Calendar.current kết hợp cả ba từ cài đặt hệ thống của người dùng. Calendar.autoupdatingCurrent là một phiên bản đặc biệt tự động cập nhật khi thay đổi cài đặt mà không cần khởi động lại ứng dụng thông qua NotificationCenter.

Calendar là một kiểu giá trị (value type) trong Foundation. Calendar(identifier:) tạo một thực thể mới với các tham số cố định. Calendar có thể được sao chép, so sánh qua == và sử dụng làm khóa trong từ điển. Điều này cho phép tạo các lịch với cài đặt timeZone và locale cụ thể để thử nghiệm.

Calendar và NSCalendar

Calendar là phiên bản Swift của NSCalendar trong Objective-C, được cầu nối qua as Calendar / as NSCalendar. Trong Swift hiện đại, Calendar được sử dụng ở khắp mọi nơi. NSCalendar vẫn tồn tại để tương thích ngược với các API Objective-C. Calendar có một bộ phương thức đầy đủ không có tiền tố NS, với các đối số type-safe và tùy chọn Swift.

An toàn luồng — Calendar an toàn cho việc đọc từ nhiều luồng. Một thực thể đã tạo có thể được đọc an toàn từ nhiều luồng. Việc sửa đổi thuộc tính (timeZone, locale) không an toàn cho luồng — hãy tạo các thực thể Calendar riêng biệt cho các cấu hình khác nhau.

Các loại lịch trong Foundation

Foundation hỗ trợ 17 hệ thống lịch thông qua liệt kê Calendar.Identifier. Mỗi hệ thống có các quy tắc riêng về năm nhuận, số tháng và bắt đầu kỷ nguyên. Việc chọn lịch ảnh hưởng đến tất cả các tính toán: dateComponents, dateInterval, nextDate.

Các hệ thống lịch chính:

  • .gregorian — tiêu chuẩn quốc tế, 12 tháng, 365/366 ngày, kỷ nguyên từ khi Chúa Giêngsu ra đời.
  • .buddhist — Phật lịch, được sử dụng ở Thái Lan, Campuchia, Lào, kỷ nguyên trước Gregory 543 năm.
  • .japanese — Nhật lịch theo kỷ nguyên trị vì của Thiên hoàng, 12 tháng như Gregory.
  • .hebrew — lịch Do Thái, 12/13 tháng, tháng nhuận Adar II.
  • .islamic — lịch Hồi giáo, 12 tháng âm lịch, 354/355 ngày.
  • .indian — lịch quốc gia Ấn Độ (Saka), 12 tháng.

Calendar(identifier: .gregorian) — được sử dụng nhiều nhất. Nó tuân thủ tiêu chuẩn quốc tế ISO 8601 và là lịch mặc định ở hầu hết các quốc gia. Đối với các ứng dụng có khán giả quốc tế, hãy sử dụng Calendar.current — nó tự động khớp với lịch hệ thống của người dùng.

Mã nhận dạngLoạiKhu vực sử dụng
.gregorianDương lịchQuốc tế
.buddhistDương lịchThái Lan, Campuchia
.japaneseDương lịchNhật Bản
.hebrewÂm dương lịchIsrael
.islamicÂm lịchCác nước Hồi giáo
.chineseÂm dương lịchTrung Quốc

Calendar và DateComponents

DateComponentsCalendar là một cặp không thể tách rời. Calendar.dateComponents(_:from:) trích xuất các thành phần từ Date tôn trọng múi giờ của lịch. Calendar.date(from:) tổng hợp Date từ DateComponents, điền các trường thiếu bằng giá trị mặc định: ngày = 1, giờ = 0, phút = 0, giây = 0.

Phương thức Calendar.component trích xuất một thành phần duy nhất, thuận tiện cho việc kiểm tra nhanh. Calendar.dateComponents trích xuất nhiều thành phần trong một lần gọi — điều này hiệu quả hơn vì Calendar thực hiện tính toán lịch một lần thay vì cho từng thành phần riêng lẻ. Đối với danh sách 3+ thành phần, luôn sử dụng dateComponents.

Calendar.compare so sánh hai Date với độ chính xác xác định. Tham số toGranularity xác định độ chính xác của thành phần: .year chỉ so sánh năm, .month — năm và tháng, .day — năm, tháng, ngày. Điều này hữu ích để kiểm tra xem hai ngày có cùng một ngày không, bỏ qua thời gian.

swift
let calendar = Calendar.current
let now = Date()

// Trích xuất một thành phần
let year = calendar.component(.year, from: now)

// Trích xuất một tập hợp thành phần
let comps = calendar.dateComponents(
    [.year, .month, .day], from: now
)

// So sánh với độ chính xác ngày
let isSameDay = calendar.compare(date1, to: date2,
                                  toGranularity: .day) == .orderedSame

// Kiểm tra ngày có phải hôm nay không
let isToday = calendar.isDateInToday(someDate)

Calendar.isDateInToday, isDateInTomorrow, isDateInYesterday — các phương thức cho kiểm tra tương đối. Calendar.isDate(_:inSameDayAs:) kiểm tra xem hai ngày có rơi vào cùng một ngày lịch hay không, có tính đến múi giờ của lịch. Các phương thức này sử dụng Calendar.compare bên trong và được tối ưu cho các lần gọi thường xuyên.

Tính toán Calendar

Calendar.dateInterval là một trong những phương thức hữu ích nhất cho phân tích và giao diện người dùng. Nó trả về DateInterval cho thành phần chỉ định: bắt đầu và kết thúc của một ngày, tuần, tháng, năm. DateInterval chứa start (Date) và end (Date) — ranh giới của khoảng thời gian. Ví dụ, dateInterval(of: .weekOfYear, for: Date()) trả về ngày bắt đầu thứ Hai và kết thúc Chủ nhật của tuần hiện tại.

Calendar.date với byAdding — một phương thức để dời chuyển ngày. Calendar.date(byAdding: .day, value: 7, to: Date()) trả về ngày sau một tuần. Calendar.date(byAdding: DateComponents) là một phiên bản linh hoạt hơn cho phép dời chuyển nhiều thành phần cùng một lúc: +1 tháng +3 ngày. Calendar tự động tính đến độ dài tháng khác nhau và năm nhuận.

Calendar.nextDate tìm ngày tiếp theo khớp với DateComponents chỉ định. Tham số matchingPolicy xác định hành vi khi không khớp: .nextTime — lần khớp tiếp theo về thời gian, .nextTimePreservingSmallerComponents — giữ nguyên phút và giây từ ngày gốc, .strict — yêu cầu khớp chính xác.

swift
let calendar = Calendar.current
let today = Date()

// Bắt đầu và kết thúc tuần
let weekInterval = calendar.dateInterval(
    of: .weekOfYear, for: today
)!

// Dời chuyển 1 tháng
let nextMonth = calendar.date(
    byAdding: .month, value: 1, to: today
)!

// Dời chuyển qua DateComponents
var delta = DateComponents()
delta.month = 1
delta.day = 3
let shifted = calendar.date(byAdding: delta, to: today)!

// Thứ Sáu ngày 13 tiếp theo
let friday13Components = DateComponents(
    weekday: 6, day: 13
)
let nextFriday13 = calendar.nextDate(
    after: today, matching: friday13Components,
    matchingPolicy: .nextTime
)

EnumerateDates — một phương thức mạnh mẽ để lặp qua các ngày theo mẫu. Calendar.enumerateDates(startingAfter:matching:matchingPolicy:using:) gọi một khối cho mỗi lần khớp cho đến khi khối trả về stop = true. Được sử dụng để tạo các sự kiện lặp lại trong lịch và lịch trình. Phương thức này hiệu quả hơn so vòng lặp thủ công với nextDate vì nó được ICU tối ưu hóa.

TimeZone và Locale

TimeZone là một phần không thể thiếu của Calendar. Múi giờ xác định thời gian lịch tương ứng với một Date tuyệt đối. Cùng một Date ở UTC và ở Moscow cho các thành phần khác nhau: Date() ở UTC có thể hiển thị 10:00, trong khi ở MSK — 13:00. Calendar.timeZone mặc định là TimeZone.current.

Locale ảnh hưởng đến ngày đầu tuần, số ngày tối thiểu trong tuần đầu tiên của năm (minDaysInFirstWeek) và tên tháng/ngày trong tuần (khi chuyển đổi qua DateFormatter). Calendar.locale mặc định là Locale.current. Trong khu vực tiếng Nga, tuần bắt đầu vào thứ Hai, trong khu vực Mỹ — vào Chủ nhật.

Calendar.availableIdentifiers trả về danh sách tất cả các mã nhận dạng lịch được hỗ trợ. Thuộc tính tĩnh Calendar.availableCalendarIdentifiers là một mảng các chuỗi với các mã nhận dạng giống nhau. Được sử dụng để xây dựng giao diện chọn lịch và kiểm tra tính khả dụng của một hệ thống lịch cụ thể trên thiết bị.

swift
// Calendar với múi giờ cụ thể
var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(identifier: "UTC")!

// Calendar với khu vực tiếng Nga
var russianCalendar = Calendar(identifier: .gregorian)
russianCalendar.locale = Locale(identifier: "ru_RU")

// Ngày làm việc đầu tiên phụ thuộc vào khu vực
let firstWeekday = russianCalendar.firstWeekday
// 2 = Thứ Hai (trong ru_RU)

// Danh sách lịch có sẵn
for identifier in Calendar.availableIdentifiers {
    print(identifier)
}

firstWeekday — một thuộc tính Calendar xác định ngày nào trong tuần được coi là ngày đầu tiên. Trong khu vực tiếng Nga, Sunday = 2 (thứ Hai là đầu tiên). Trong khu vực Mỹ, Sunday = 1. Điều này ảnh hưởng đến weekOfMonth và weekOfYear: cùng một ngày có thể thuộc về số tuần khác nhau trong các khu vực khác nhau. Đối với các ứng dụng làm việc với ngày tháng, hãy sử dụng Calendar.current hoặc đặt firstWeekday một cách rõ ràng.

Ví dụ làm việc với Calendar

Hãy xem xét các kịch bản thực tế minh họa khả năng của Calendar. Mỗi ví dụ giải quyết một tác vụ phát triển iOS cụ thể và cho thấy cách sử dụng đúng các tính toán lịch.

Kiểm tra: ngày có trong tháng này không?

Calendar.dateInterval(of: .month, for:) trả về ranh giới của tháng hiện tại. Kiểm tra xem một Date có nằm trong khoảng này không là cách nhanh nhất để xác định một ngày có thuộc tháng hiện tại hay không. Một cách khác là Calendar.compare với độ chính xác .month: nếu kết quả là .orderedSame thì tháng khớp.

swift
func isInCurrentMonth(_ date: Date) -> Bool {
    let calendar = Calendar.current
    let monthInterval = calendar.dateInterval(
        of: .month, for: Date()
    )!
    return monthInterval.contains(date)
}

// Số ngày trong một tháng
func daysInMonth(for date: Date) -> Int {
    let calendar = Calendar.current
    return calendar.range(
        of: .day, in: .month, for: date
    )?.count ?? 0
}

// Thêm tháng với điều chỉnh chính xác
func addMonths(_ months: Int, to date: Date) -> Date {
    let calendar = Calendar.current
    return calendar.date(
        byAdding: .month, value: months, to: date
    )!
}

Calendar.range(of:in:for:) trả về phạm vi các giá trị hợp lệ cho một thành phần được chỉ định trong bối cảnh của một thành phần khác. Ví dụ, range(of: .day, in: .month, for: date) trả về 1..<32 cho các tháng có 31 ngày hoặc 1..<29 cho tháng Hai của năm không nhuận. Đây là cách đúng để biết số ngày trong một tháng, thay vì sử dụng các giá trị cứng.

Thêm tháng qua Calendar.date(byAdding:value:to:) xử lý đúng các ngày biên. Nếu bạn thêm 1 tháng vào ngày 31 tháng 1, Calendar trả về ngày 28 tháng 2 (hoặc 29 trong năm nhuận), thay vì ngày 3 tháng 3 sẽ xảy ra nếu chỉ thêm 30 ngày qua TimeInterval. Đây là một lý do nữa để không sử dụng TimeInterval cho các tính toán lịch.

Phương thức CalendarMục đíchVí dụ
dateIntervalRanh giới khoảng thời gianBắt đầu và kết thúc tháng
range(of:in:for:)Phạm vi thành phầnNgày trong tháng hiện tại
date(byAdding:)Dời chuyển ngày+1 tháng từ hôm nay
isDateInTodayKiểm tra hôm nayNgày có phải hôm nay không?
compare(toGranularity:)So sánh với độ chính xácCùng ngày bỏ qua thời gian

Câu hỏi thường gặp

Sự khác biệt giữa Calendar.current và Calendar(identifier: .gregorian) là gì?

Calendar.current trả về lịch từ cài đặt hệ thống của người dùng — nó có thể không phải Gregory (ví dụ, Phật lịch ở Thái Lan). Calendar(identifier: .gregorian) luôn tạo lịch Gregory bất kể cài đặt. Sử dụng Calendar.current để hiển thị ngày và mã nhận dạng được chọn rõ ràng cho logic nghiệp vụ.

Tại sao Calendar.date(byAdding: .month, value: 1) đôi khi trả về cùng một ngày?

Điều này liên quan đến độ dài tháng khác nhau. Nếu ngày hiện tại là 31 tháng 1, thêm 1 tháng sẽ cho ngày 28 tháng 2, vì tháng 2 không có ngày 31. Calendar tự động điều chỉnh ngày đến ngày hợp lệ cuối cùng của tháng. Để kiểm soát chính xác, hãy sử dụng DateComponents với day: 1 để chuyển đến ngày đầu tiên của tháng.

DateFormatter sử dụng lịch nào theo mặc định?

DateFormatter sử dụng Calendar.current — lịch hệ thống của người dùng. Nếu một ứng dụng luôn hiển thị ngày theo lịch Gregory bất kể cài đặt, hãy đặt formatter.calendar = Calendar(identifier: .gregorian). Điều này đảm bảo hiển thị thống nhất cho tất cả người dùng.

Làm thế nào để kiểm tra xem một năm có phải năm nhuận qua Calendar?

Calendar.range(of: .day, in: .year, for: date) trả về 365 hoặc 366 ngày. Đơn giản hơn: Calendar.date(from: DateComponents(year: year, month: 2, day: 29)) != nil — nếu ngày 29 tháng 2 tồn tại, năm đó là năm nhuận. Calendar tự động xử lý các quy tắc cho hệ thống lịch cụ thể.

Có thể thay đổi firstWeekday sau khi tạo Calendar không?

, thuộc tính firstWeekday có thể ghi. Thay đổi ảnh hưởng đến weekOfMonth, weekOfYear và tất cả các tính toán liên quan đến số tuần. Khi đặt locale = Locale(identifier: “ru_RU”), firstWeekday tự động trở thành 2 (thứ Hai). Cài đặt thủ công ghi đè giá trị từ locale.

Tổng kết

  • Calendar — một lớp Foundation cho tính toán lịch, kết nối Date với các thành phần có thể đọc được qua DateComponents.
  • 17 hệ thống lịch được Foundation hỗ trợ — từ Gregory đến Phật lịch và Nhật lịch, với xử lý quy tắc khu vực tự động.
  • Calendar.current khớp với lịch hệ thống của người dùng — sử dụng nó cho UI và DateFormatter.
  • Calendar.dateIntervalCalendar.range là các phương thức chính để làm việc với ranh giới khoảng thời gian và phạm vi thành phần.
  • Calendar.date(byAdding:) xử lý đúng độ dài tháng khác nhau và năm nhuận — đừng thay thế nó bằng số học TimeInterval.
  • TimeZone và Locale là thuộc tính Calendar ảnh hưởng đến tất cả tính toán lịch — hãy đặt chúng rõ ràng để có hành vi có thể dự đoán trước.
  • firstWeekday được xác định bởi khu vực và ảnh hưởng đến số tuần — đối với ứng dụng toàn cầu, hãy đặt nó rõ ràng hoặc sử dụng Calendar.current.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm