“Mốc thời gian” trong xử lý ngày tháng trên máy tính

Kể từ khi sự kiện Y2K xảy ra, người ta mới bắt đầu nhận thấy sự quan trọng trong việc xử lý ngày tháng trên máy tính. Qui luật biến đổi ngày tháng không khó, nhưng không phải lúc nào người ta cũng có thể nhớ và dự liệu trước được những khó khăn khi tính toán với kiểu số liệu này. Số lượng hàm xử lý cho kiểu số liệu ngày tháng không nhiều và tương đối thiếu linh hoạt, trong nhiều trường hợp người ta phải chuyển sang số liệu dạng số với định dạng YYYYMMDD để có thể xử lý như một số nguyên bình thường.

Trong nhiều dự án liên quan đến lịch thanh toán hay lịch kế hoạch, người ta vấp phải một vấn đề khá khó chịu khi xử lý số liệu ngày tháng. Số lượng ngày biến đổi trong một tháng và sự xuất hiện năm nhuận (tăng số ngày trong tháng 2) vô cùng quen thuộc với mỗi chúng ta hóa ra lại rất khó thể hiện trên máy tính. Dữ liệu dạng ngày tháng vốn được coi như một loại kiểu dữ liệu chuẩn lại đòi hỏi phải xử lý một cách đặc biệt và trong nhiều trường hợp rất dễ dẫn đến sai lầm. Để có thể tính toán chính xác, đôi khi người ta chấp nhận can thiệp một cách kỹ thuật thông qua một hàm hay thủ tục thêm vào (thậm chí là nhập liệu bằng tay) nhằm có được kết quả mong muốn. Tuy nhiên điều này dễ dẫn đến sự thiếu tương thích và vô tình biến ngày tháng thành một kiểu dữ liệu đặc biệt không thể xử lý theo cách thông thường.

Sau đây chúng ta sẽ nghiên cứu một hàm dùng để xử lý ngày tháng vô cùng quen thuộc với những ai làm cơ sở dữ liệu, để từ đó có thể thấy được sự khó khăn trong xử lý ngày tháng trong các trường hợp đặc biệt, cũng như phương pháp xử lý để có thể thực hiện biến đổi ngày tháng như những kiểu dữ liệu "thô" truyền thống khác. Hàm được nói đến ở đây thực hiện tính ngày tương ứng với ngày hiện tại của tháng kế tiếp, đó là hàm Add_Months của Oracle hay DateAdd('m',,) của SQL Sever.

Để vấn đề trực quan và dễ hiểu hơn, ta hãy xét bài toán cụ thể: định lịch hạch toán chi phí cho các hợp đồng phát sinh của một doanh nghiệp. Đương nhiên, các kỳ hạch toán thường rơi vào một ngày cố định trong tháng và độ dài của kỳ hạch toán phải phù hợp với các sinh hoạt bình thường của con người như theo ngày, tuần, tháng quý hay năm... Các kỳ theo ngày hay tuần không xét đến ở đây do nó hoàn toàn tuân theo quy luật thông thường với chu kỳ không đổi là 1 và 7 ngày. Do đó, ta giả thiết người ta định thời gian thanh toán theo ngày ký hợp đồng, ví dụ vào ngày 30 hàng tháng chẳng hạn.

Ta hãy thử thực hiện một vài phép tính sau:

Add_Months('30/11/2008',1)='31/12/2008'
Add_Months('31/12/2008',1)='31/01/2009'
Add_Months('31/01/2008',1)='28/02/2009'
Add_Months('28/02/2009',1)='31/03/2009'
Add_Months('31/03/2009',2)='30/04/2009'
Add_Months('30/04/2009',2)='31/05/2009'
...
Chúng ta nhận thấy các chỉ số ngày thay đổi liên tục do ngày của kỳ thanh toán đầu tiên được hiểu là ngày cuối cùng của tháng, điều này kéo theo tất cả các kỳ thanh toán tiếp theo đều vào cuối tháng. Chỉ số ngày thay đổi dẫn đến việc định chính xác ngày thanh toán chung cho mọi tháng là không thể trong trường hợp đặc biệt này.

Chúng ta sẽ tiếp tục thử tính toán đối với các ngày sát cuối tháng:

Add_Months('31/01/2009',1)='28/02/2009'
Add_Months('30/01/2009',1)='28/02/2009'
Add_Months('29/01/2009',1)='28/02/2009'
Add_Months('28/01/2009',1)='28/02/2009'
Add_Months('27/01/2009',1)='27/02/2009'
Add_Months('26/01/2009',1)='26/02/2009'
...

Thoạt nhìn ai cũng có thể thấy bất ngờ, kết quả của 4 ngày liên tiếp cuối tháng 1 khi tăng thêm một tháng đều cho một đáp án duy nhất. Tất cả các hợp đồng ký trong 4 ngày này có ngày hạch toán trùng nhau ở kỳ hạch toán của tháng kế tiếp!

Để tránh sự rắc rối, người ta thường định lịch theo một ngày nhất định trong tháng và thực hiện thay đổi tháng và năm để tính ra ngày của kỳ tiếp theo (sử dụng các hàm Date(), Month() và Year() để có được ngày, tháng, năm cần tính).

Ví dụ: Ngày mùng sáu hàng tháng được ấn định là ngày hạch toán:

Kỳ 1: '06/03/2007'
Kỳ 2: '06/04/2007'
Kỳ 3: '06/05/2007'
...
Cách sử dụng theo kiểu ngày "áp đặt" này khiến việc xử lý ngày tháng rất đơn giản (và thực tế cũng được sử dụng rất nhiều, đặc biệt là cho các bài toán kế toán đơn giản sử dụng Excel) nhưng hóa ra nó lại vô cùng nguy hiểm! Hãy thử hình dung khi ta gửi tiền vào ngân hàng với hạn mức 3 năm vào ngày 29/2/2008, sau 3 năm lượng tiền lãi có thể bằng 0 chỉ vì năm thứ 3 không nhuận tháng 2 và ngân hàng hoàn toàn có thể "quên" tính lãi vì không có ngày 29/02/2011 trên lịch (Điều này trên thực tế đã từng xảy ra tuy nhiên nó đã bị phát hiện ngay sau đó như một lỗi bất khả kháng do sự xuất hiện của một ngày vào năm nhuận với tần xuất khá khiêm tốn: chỉ 1 lần trong 4 năm).

Sự bất cập này khiến cho việc định lịch tự động trên máy tính gặp phải trở ngại không nhỏ. Hoàn toàn có thể nói rằng một hệ thống lịch quen thuộc trong hàng ngàn năm vô tình tạo ra lỗ hổng tính toán trên máy tính, một giá trị ngày truyền vào hệ thống sai (truyền giá trị Text '31/04/2008' mà quên bắt lỗi chẳng hạn) có thể khiến giá trị ngày tháng mà hệ thống tính ra thành một giá trị của cả chục năm sau đó. Trong một số dự án kế toán mà tôi có điều kiện tham gia, thậm chí người ta còn tránh sử dụng 3 ngày 29, 30, 31 để nhập hợp đồng vào hệ thống nhằm tránh gặp phải những tình huống đặc biệt với một sự giải thích đơn giản đến nực cười: Đó là những ngày cuối tháng đặc biệt!

Điều này dĩ nhiên không đúng, bởi với tháng 2, 3 ngày cuối tháng phải tính từ ngày 26 (27 đối với năm nhuận) thay vì ngày 29. Những tháng có 30 ngày cũng không tính giống tháng có 31 ngày.

Trên thực tế, không phải ngẫu nhiên mà một số ngân hàng tính lãi hàng tháng vào ngày 25 chứ không phải ngày cuối tháng. (Điều này không giống như sự kết thúc của năm tài chính vào tháng 11 hàng năm thay vì tháng 12, đây là việc cần thiết cho việc lên kế hoạch tài chính cho năm tiếp theo)

Trên thực tế, việc giải quyết những ngày được coi là đặc biệt này hóa ra lại vô cùng đơn giản với hàm Add_Months có sử dụng "Mốc thời gian" với bước nhảy theo đơn vị tròn tháng. Những bước nhảy quen thuộc thường thấy là: một tháng, hai tháng, một quý hay nửa năm....

Cách tính tôi sử dụng sau đây sử dụng phép biến đổi kỹ thuật với một số nét khá tương đồng với các phép xử lý kỹ thuật mà chúng ta thường thấy khi xử lý trên máy tính như việc tăng các chỉ số lên 1 đối với các vòng lặp trong một số ngôn ngữ sử dụng biến đếm tính từ Mốc 1 như Pascal thay vì tính từ Mốc 0 như C chẳng hạn, chỉ có điều việc sử dụng Mốc đối với ngày tháng đặc biệt hơn một chút.

Phương pháp tính toán sử dụng "Mốc thời gian" được mô tả như sau:

Nếu ngày đầu tiên của năm là ngày thứ nhất (1) thì ngày 31/12 của năm trước chính là ngày thứ 0 – tức là "ngày mốc" (và kéo theo tháng 12 năm trước như là "tháng mốc"). Việc sử dụng hệ ngày, tháng Mốc này có một số đặc điểm sau:

- "Ngày mốc" không ảnh hưởng đến các ngày trong năm vì nó là ngày của năm trước.

- Việc tính toán các tháng kế tiếp hoàn toàn bình thường theo chỉ số tháng.

- Tất cả các tháng có số ngày không vượt quá số ngày của "tháng mốc".

- Có thể sử dụng 31 ngày trong tháng Mốc như là hệ thống ngày cơ sở để tính tất cả các ngày khác trong các năm tiếp theo thông qua hàm Add_Months.

- Chỉ số Mốc có thể được Reset về 0 khi một kỳ nào đó rơi vào tháng 12 để tránh sự tràn số do chính chỉ số chạy gây ra.

Thật vậy, giả sử để tính liên tiếp lịch kỳ hạn 1 tháng bắt đầu là ngày 30/07/2008 ta tính với chỉ số chạy bắt đầu từ chỉ số tháng của kỳ đầu tiên (07) như sau:

Mốc tính thời gian: '30/12/2007'

Kỳ thứ nhất 30/07/2008 = Add_Months('30/12/2007', 7+0)
Kỳ thứ hai 30/08/2008 = Add_Months('30/12/2007', 7 + 1)
Kỳ thứ ba 30/09/2008 = Add_Months('30/12/2007', 7 + 2)
Kỳ thứ tư 30/10/2008 = Add_Months('30/12/2007', 7 + 3)
Kỳ thứ năm 30/11/2008 = Add_Months('30/12/2007', 7+4)
Kỳ thứ sáu 30/12/2008 = Add_Months('30/12/2007', 7 + 5)

Có thể Reset chỉ số Mốc ở đây để "ngày mốc" là: 30/12/2008 và chỉ số cộng lại bắt đầu từ 0 và trùng với chỉ số tháng

Kỳ thứ bảy 30/01/2008 = Add_Months('30/12/2007', 7 + 6)
Kỳ thứ tám 28/02/2008 = Add_Months('30/12/2007',7 + 7)
Kỳ thứ chín 30/03/2008 = Add_Months('30/12/2007',7 + 8)
...
Kỳ thứ n: ##/##/#### = Add_Months('30/12/2007', 7 + n - 1)

Như vậy, có một sự tương ứng chỉ số với toàn bộ các kỳ kế tiếp với các chỉ số tăng tuần tự một đơn vị. Điểm mấu chốt ở đây chính là sự quy chuẩn bằng cách lùi về Mốc trước khi thực hiện các phép tính chỉ số. Kết quả là ngày của Mốc không bao giờ được hiểu là ngày cuối tháng nếu chỉ số ngày của Mốc < 31 và kết quả nhận được từ hàm Add_months không bao giờ tạo ra kết quả không mong muốn bởi sự nhầm lẫn ngày cuối tháng. Tất nhiên, các chỉ số ngày được hiểu là khả năng đáp ứng tối đa có thể được (ngày 30 chỉ có thể xuất hiện trong kỳ tương ứng với tháng có >= 30 ngày). Hoàn toàn có thể nhận thấy sự hợp lý trong cách tính này cho tất cả các ngày trong năm đối với các bước nhảy tròn tháng (một số nguyên lần tháng) tính từ Mốc.

Dưới đây là bảng so sánh trực quan kết quả sau khi sử dụng hàm Add_Month sử dụng "Mốc thời gian" với kết quả của chính hàm đó khi không sử dụng Mốc như sau:

Bắt đầu Kỳ 0: 30/05/2008, Mốc tương ứng là 30/12/2007

Ta có thể dễ dàng nhận thấy kết quả có sự khác biệt khi kỳ thanh toán đầu tiên rơi vào những ngày sát ngày cuối tháng (từ ngày 28 đến ngày 30). Kết quả của hàm không sử dụng Mốc sau 1 năm kỳ hạch toán ngày 30/05 đã biến thành ngày 31/05. Đương nhiên đó là kết quả có vấn đề!

Nếu quen dùng SQL Sever bạn hoàn toàn có thể kiểm tra lại kết quả tính toán ở bảng trên với hàm DateAdd('m',,). Mặc dù hàm này không bị rơi vào cái bẫy "cuối tháng" chết người nhưng có thể nhận ra sau 12 kỳ kết quả thậm chí còn tồi tệ hơn thế. Nếu kỳ đầu tiên rơi vào ngày 29 cho đến 31 thì sau kỳ tính của tháng 2 (28 ngày), tất cả các kỳ sau đó đều rơi vào ngày 28. Chính sự không thống nhất cách xử lý giữa các hệ CSDL khác nhau cũng cho thấy sự khó khăn trong xử lý số liệu ngày tháng trong các trường hợp đặc biệt.

Dễ dàng có thể nhận thấy việc lấy Mốc không nhất thiết phải bắt đầu từ tháng 12 năm trước mà có thể ở bất cứ một tháng nào có số ngày là 31 (bằng số ngày lớn nhất có thể có trong một tháng). Tuy nhiên, nếu sử dụng "Mốc thời gian" không vào tháng 12 năm trước thì chỉ số tháng cộng thêm hoàn toàn có thể âm:

'30/05/2008' = Add_Months('30/08/2008', - 3), điều này có vẻ không được tự nhiên cho lắm (thay vì các chỉ số phải trùng hoàn toàn với chỉ số tháng nếu Reset Mốc một cách hợp lý khi chuyển năm).

Như vậy, đối với mỗi hợp đồng được tạo ra ta cần có thêm 1 trường Datetime lưu Mốc và 1 trường lưu tháng bắt đầu của kỳ thanh toán.

SysDateID

Các ngày thanh toán hoàn toàn có thể được xác định bởi 1 lệnh Update như sau:

Update DataTable A
 Set TermDate = Add_Months(
        (Select DateStart From SysDateID B Where B.ID=A.ID),
        TermID + (Select InitTerm From SysDateID C Where C.ID=A.ID) -1)

Và đây là bảng kết quả:

DataTable

Lê Công Đài
Email: LecongdaiR577@yahoo.com

In trang [In trang]    Đóng trang [Đóng trang]

© Tạp chí Thế Giới Vi Tính - PC World VN. CQ chủ quản: Sở Khoa Học và Công Nghệ TP.HCM
Giấy phép số 196/GP-BVHTT do Bộ Văn Hóa Thông Tin cấp ngày 27-06-2003
Tòa soạn: 126 Nguyễn Thị Minh Khai, Q.3 TP.HCM - ĐT: 84.8.39304324 - FAX: 84.8.39304338
Bản quyền của Thế Giới Vi Tính - PC World VN