Lập trình · 23/10/2026

JavaScript cộng tháng: vì sao ngày 31 có thể nhảy sang tháng kế tiếp?

Cộng một tháng vào ngày 31/01 bằng Date setter có thể cho ngày trong tháng 3. Tháng không có số ngày cố định, và JavaScript không tự chọn chính sách “ngày cuối tháng” thay cho ứng dụng. Cần xác định cách xử lý ngày không tồn tại trong tháng đích trước khi viết công thức.

JavaScript cộng tháng: vì sao ngày 31 có thể nhảy sang tháng kế tiếp?

Cộng một tháng vào ngày 31/01 bằng Date setter có thể cho ngày trong tháng 3. Tháng không có số ngày cố định, và JavaScript không tự chọn chính sách “ngày cuối tháng” thay cho ứng dụng. Cần xác định cách xử lý ngày không tồn tại trong tháng đích trước khi viết công thức.

1. Setter giữ ngày hiện tại rồi chuẩn hóa phần tràn

const date = new Date('2026-01-31T09:00:00.000Z');
const returned = date.setUTCMonth(date.getUTCMonth() + 1);
console.log(date.toISOString()); // 2026-03-03T09:00:00.000Z
console.log(typeof returned); // number

setUTCMonth sửa Date hiện tại và trả timestamp. Không truyền đối số ngày thì ngày hiện tại được dùng; ngày vượt số ngày tháng đích được chuẩn hóa. Tháng 2/2026 chỉ có 28 ngày nên ngày 31 bị đẩy sang 03/03.

Ví dụ dùng UTC để kết quả không phụ thuộc múi giờ máy chạy. Phiên bản setMonth làm việc theo giờ địa phương và còn cần xem xét chuyển đổi DST. Đổi sang UTC không tự giải quyết yêu cầu “cùng giờ địa phương” trong một múi giờ nghiệp vụ.

2. Nếu muốn chặn ở cuối tháng, hãy làm điều đó tường minh

function addUtcMonthsClamped(source, months) {
  if (!(source instanceof Date) || !Number.isFinite(source.getTime())) {
    throw new TypeError('source must be a valid Date');
  }
  if (!Number.isInteger(months)) throw new TypeError('months must be an integer');
  if (source.getUTCFullYear() < 2000 || source.getUTCFullYear() > 2099
      || months < -12 || months > 12) {
    throw new RangeError('unsupported year or month offset');
  }
  const day = source.getUTCDate();
  const target = new Date(source.getTime());
  target.setUTCDate(1);
  target.setUTCMonth(target.getUTCMonth() + months);
  const last = new Date(target.getTime());
  last.setUTCMonth(last.getUTCMonth() + 1, 0);
  target.setUTCDate(Math.min(day, last.getUTCDate()));
  return target;
}
const source = new Date('2026-01-31T09:00:00.000Z');
console.log(addUtcMonthsClamped(source, 1).toISOString()); // 2026-02-28T09:00:00.000Z
console.log(source.toISOString()); // 2026-01-31T09:00:00.000Z

Helper minh họa nhận Date chuẩn, hợp lệ, có năm UTC 2000–2099 và độ dời nguyên từ -12 đến 12 tháng. Giới hạn này là contract của ví dụ, không phải giới hạn Date. Nó tạo bản sao, chuyển về ngày 1 trước khi đổi tháng, rồi chọn ngày nhỏ hơn giữa ngày gốc và ngày cuối tháng đích.

Đặt ngày 0 của tháng sau cho ngày cuối tháng trước, theo quy tắc chuẩn hóa của Date; xem MDN setUTCDate. Helper giữ các thành phần giờ UTC và không sửa source. Nó không parse chuỗi ngày hoặc tự chuyển số tháng dạng chuỗi.

3. Cộng từng bước có thể mất ngày neo ban đầu

// Uses addUtcMonthsClamped from the previous example.
const anchor = new Date('2026-01-31T09:00:00.000Z');
const february = addUtcMonthsClamped(anchor, 1);
console.log(addUtcMonthsClamped(february, 1).toISOString()); // 2026-03-28T09:00:00.000Z
console.log(addUtcMonthsClamped(anchor, 2).toISOString()); // 2026-03-31T09:00:00.000Z

Sau khi chặn về 28/02, lần cộng tiếp chỉ nhìn thấy ngày 28. Nếu lịch cần bám ngày 31 gốc, lưu ngày neo và tính từng lần từ mốc gốc theo chính sách đã chọn. Nếu lịch cần “luôn ngày cuối tháng”, đó lại là quy tắc khác: ngày neo 28/02 không tự chứng minh nên chọn 28/03 hay 31/03.

Độ dời lớn hơn 12 tháng bị helper từ chối có chủ đích. Với lịch dài hạn, cần mở rộng contract và kiểm tra năm, miền Date cùng quy tắc lặp; đừng chỉ chạy vòng lặp nhiều lần rồi giả định vẫn giữ ngày neo.

4. Chọn kiểu dữ liệu theo ý nghĩa lịch

Date biểu diễn một thời điểm, còn lịch chỉ có ngày hoặc lịch theo múi giờ cụ thể cần thêm ngữ nghĩa. Helper UTC trong bài không phải bộ máy lập lịch đa múi giờ. Nếu yêu cầu là “09:00 giờ Việt Nam mỗi tháng”, hãy giữ múi giờ và quy tắc lịch ở tầng ứng dụng thay vì chỉ lưu một độ dời mili giây.

Cộng 30 ngày cũng không tương đương cộng một tháng. Khi thiết kế dữ liệu, phân biệt khoảng thời lượng cố định với phép dịch theo lịch, và ghi rõ cách xử lý cuối tháng, năm nhuận, dữ liệu không hợp lệ.

5. Checklist khi cộng tháng

  • Ngày không tồn tại sẽ tràn, chặn hay bị từ chối?
  • Phép tính dựa trên UTC, múi giờ máy hay múi giờ nghiệp vụ?
  • Source có được phép bị sửa không?
  • Lịch lặp dựa vào ngày neo gốc hay kết quả kỳ trước?
  • Đã thử tháng 2 năm nhuận, qua năm mới và độ dời âm?

Sáu khối mã Việt–Anh đã chạy bằng Node.js, kèm kiểm tra năm nhuận, độ dời âm, ranh giới contract và bảo toàn source. Chưa kiểm thử lịch lặp dài hạn hoặc DST theo múi giờ nghiệp vụ.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.