Viết chương trình nhập vào một năm (> 1582), in lịch của năm đó (với ngày đầu tuần là Chủ nhật). Tính thứ cho ngày đầu năm bằng công thức Zeller.
Kết quả chạy chương trình
Input year: 2020
January
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
February
S M T W T F S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
March
S M T W T F S
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
April
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
May
S M T W T F S
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
June
S M T W T F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
July
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
August
S M T W T F S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
September
S M T W T F S
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
October
S M T W T F S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
November
S M T W T F S
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
December
S M T W T F S
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Lịch Gregorian
Trước khi bắt tay vào giải bài tập, chúng ta cần hiểu đây là loại lịch nào. Với thông tin năm nhập vào > 1582, có thể đoán đây là lịch Gregorian (Gregorian Calendar). Vậy lịch Gregorian là gì?
Theo Wikpedia định nghĩa:
Lịch Gregorius, còn gọi là Tây lịch, Công lịch, Dương lịch, là một bộ lịch do Giáo hoàng Grêgôriô XIII đưa ra vào năm 1582. Lịch Gregorius chia thành 12 tháng với 365 ngày, cứ 4 năm thì thêm một ngày vào cuối tháng 2 tạo thành năm nhuận (366 ngày). Trước đó lịch Julius quy ước một năm có 365,25 ngày, song độ dài của năm mặt trời là 365,242216 ngày cho nên một năm theo lịch Julius dài hơn khoảng 0,0078 ngày so với năm mặt trời (tức là khoảng 11 phút 14 giây).
Vậy, lịch Gregorian chính là Dương lịch mà chúng ta đang sử dụng phổ biến, khác với Âm lịch hay Phật lịch.
Công thức Zeller
Công thức Zeller là một giải thuật được sáng chế bởi Christian Zeller để tính thứ cho bất kỳ ngày nào theo lịch Julian hoặc Gregorian.
Dưới đây là công thức Zeller cho lịch Gregorian:
Còn đây là công thức Zeller cho lịch Julian:
Trong đó:
- h là thứ trong tuần (day-of-week) (0 = Thứ bảy, 1 = Chủ nhật, 2 = Thứ hai, ..., 6 = Thứ sáu).
- q là ngày trong tháng.
- m là tháng (3 = Tháng ba, 4 = Tháng tư, 5 = tháng 5, ..., 14 = Tháng hai).
- K là năm của thế kỷ (year mod 100).
- J là thế kỷ (year div 100).
Chú thích:
- div: chia lấy nguyên. Vd: 5 / 2 = 2.
- mod: chia lấy dư. Vd: 5 % 2 = 1.
- Trong giải thuật này thì Tháng một và Tháng hai là được tính là tháng 13 và 14 của năm trước đó. Ví dụ như ngày 2 tháng 2 năm 2010 đối với giải thuật này sẽ được xem là ngày thứ hai của tháng 14 của năm 2009 (02/14/2009, định dạng DD/MM/YYYY).
- Đối với ISO day-of-week (1 = Thứ hai, 7 = Thứ bảy), có thể sử dụng công thức này để chuyển đổi: d = ((h + 5) mod 7) + 1
- Theo cách tính modulus thông thường cho số âm thì -2 mod 7 sẽ được tính bằng cách như sau: Gọi -2 là số bị chia, 7 là số chia, lấy số chia nhân với một số nguyên âm bất kỳ sao cho kết quả là một số âm x lớn nhất nhưng vẫn nhỏ hơn số bị chia. Trong trường hợp này, ta có: 7 * (-1) = -7. -7 là số âm lớn nhất có thể nhưng vẫn nhỏ hơn -2. Giả sử lấy 7 * (-2) = -14. -14 dù < -2 nhưng không phải là số âm lớn nhất so với -7. Sau đó lấy số bị chia cộng với trị tuyệt đối của kết quả trên ta được: -2 + |7 * (-1)| = 5. Vậy -2 mod 7 = 5. Tương tự -2 mod 9 sẽ bằng - 2 + |9 * (-1)| = 7. Hoặc -11 mod 9 = -11 + |9 * (-2)| = -11 + 18 = 7.
- Tuy nhiên, Phần lớn các ngôn ngữ máy tính sẽ trả về -2 cho phép tính -2 mod 7, điều này có thể khiến cho kết quả của cuối cùng được trả về từ công thức Zeller là một số âm. Do đó, để đảm bảo kết quả luôn dương, chúng ta phải điều chỉnh công thức từ -2 mod 7 sang 5 mod 7 vì 5 mod 7 cũng bằng 5.
Vậy cuối cùng, cài đặt C của công thức Zeller cho lịch Gregorian như sau:
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
Theo công thức Zeller gốc thì h = 0 là Thứ bảy. Nhưng vì tôi muốn lịch của tôi bắt đầu từ Chủ Nhật nên khi trả về h tôi kiểm tra nếu h = 0 thì tôi cho h = 6, với các giá trị khác của h thì tôi giảm đi 1 đơn vị để khớp với thứ tự {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}.
Lời giải
- Tôi sử dụng một vòng lặp để quét qua từng tháng một.
- Với mỗi tháng, tôi sẽ dùng công thức Zeller để tính thứ (day of week) của ngày 1 của tháng đó.
- Đồng thời tôi cũng cần xác định xem tháng đó có bao nhiêu ngày. Như chúng ta đã biết: Tháng 1,3,5,7,8,10,12 có 31 ngày; Tháng 4,6,9,11 có 30 ngày; Tháng 2 có thể có 28 hoặc 29 ngày tuỳ thuộc vào năm đó có phải là năm nhuận hay không.
- Một năm được gọi là năm nhuận nếu năm đó chia hết cho 4 và không chia hết cho 100 hoặc nếu năm đó chia hết cho 400.
#include <stdio.h>
int isLeapYear(int y) {
return (!(y % 4) && y % 100) || !(y % 400);
}
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
int main()
{
char months[12][10] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
int y;
printf("Input year: ");
scanf("%d", &y);
putchar('\n');
for (int m = 1; m <= 12; m++, printf("\n\n")) {
printf("%s\n", months[m - 1]);
int top;
switch(m) {
case 2: top = isLeapYear(y) ? 29 : 28; break;
case 4: case 6: case 9: case 11: top = 30; break;
default: top = 31; break;
}
int dow = getDow(1, m, y);
printf(" S M T W T F S\n");
for (int i = 0; i < dow; i++)
printf("%3s", " ");
for (int i = 1; i <= top; i++) {
printf("%3d", i);
if (i < top && (i + dow) % 7 == 0)
printf("\n");
}
}
return 0;
}
Sử dụng mảng
- Tôi sử dụng một mảng 2 chiều có kích thước cố định 6 x 7 để lưu trữ lịch. Khi in, những phần tử có giá trị '0' sẽ được thay bằng khoảng trắng. Lý do của cách làm này là để bài nâng cao bên dưới được dễ hơn, bỏ đi các logic phức tạp của việc in lịch theo chiều dọc.
- Như các bạn thấy tôi sử dụng con trỏ int* d trỏ vào phần tử đầu tiên của mảng 2 chiều days. Tại sao lại như vậy? Mảng 2 chiều int days[6][7] với kích thước cố định thực chất là 42 ô nhớ 4 bytes liền kề nhau. int days[6][7] có thể tạm coi là tương đương với int**. Do đó khi viết days[0] tức tương đương với int*. Vậy, khi tôi viết int* d = days[0] tức là tôi đang gán địa chỉ của ô nhớ đầu tiên trong mảng days cho con trỏ d, hay nói cách khác, d đang trỏ vào phần tử đầu tiên của mảng 2 chiều days. Như vậy, tôi sử dụng phương pháp duyệt mảng sử dụng con trỏ trung gian.
- Khi bạn sử dụng toán tử ++ đối với một con trỏ, thì điều đó có nghĩa là con trỏ sẽ trỏ qua ô nhớ kế tiếp.
- Và để truy xuất vào giá trị của ô nhớ mà con trỏ d đang trỏ tới, chúng ta dùng *d (ý nghĩa khác với int* d dùng để khai báo con trỏ). Còn khi chúng ta viết d không, tức là đang nói tới địa chỉ của ô nhớ mà con trỏ d đang trỏ tới.
#include <stdio.h>
#include <stdlib.h>
int isLeapYear(int y) {
return (!(y % 4) && y % 100) || !(y % 400);
}
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
int main()
{
char months[12][10] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
char daysow[7][4] = {"S", "M", "T", "W", "T", "F", "S"};
int y;
printf("Input year: ");
scanf("%d", &y);
putchar('\n');
for (int m = 1; m <= 12; m++, printf("\n")) {
printf("%s\n", months[m - 1]);
int top;
switch(m) {
case 2: top = isLeapYear(y) ? 29 : 28; break;
case 4: case 6: case 9: case 11: top = 30; break;
default: top = 31; break;
}
int dow = getDow(1, m, y);
int days[6][7] = {0};
int* d = days[0];
for (int i = 0; i < dow; i++) d++;
for (int i = 0; i < top; i++) *(d++) = i + 1;
for (int i = 0; i < 7; i++) printf("%3s", daysow[i]);
printf("\n");
int row = dow + top > 35 ? 6 : 5;
for (int i = 0; i < row; i++, putchar('\n'))
for (int j = 0; j < 7; j++)
days[i][j] == 0 ? printf("%3s", " ") : printf("%3d", days[i][j]);
}
return 0;
}
Bài nâng cao
Tương tự như bài trên nhưng lịch của mỗi tháng được in theo chiều dọc.
Input year: 1975
January
S 5 12 19 26
M 6 13 20 27
T 7 14 21 28
W 1 8 15 22 29
T 2 9 16 23 30
F 3 10 17 24 31
S 4 11 18 25
February
S 2 9 16 23
M 3 10 17 24
T 4 11 18 25
W 5 12 19 26
T 6 13 20 27
F 7 14 21 28
S 1 8 15 22
March
S 2 9 16 23 30
M 3 10 17 24 31
T 4 11 18 25
W 5 12 19 26
T 6 13 20 27
F 7 14 21 28
S 1 8 15 22 29
April
S 6 13 20 27
M 7 14 21 28
T 1 8 15 22 29
W 2 9 16 23 30
T 3 10 17 24
F 4 11 18 25
S 5 12 19 26
May
S 4 11 18 25
M 5 12 19 26
T 6 13 20 27
W 7 14 21 28
T 1 8 15 22 29
F 2 9 16 23 30
S 3 10 17 24 31
June
S 1 8 15 22 29
M 2 9 16 23 30
T 3 10 17 24
W 4 11 18 25
T 5 12 19 26
F 6 13 20 27
S 7 14 21 28
July
S 6 13 20 27
M 7 14 21 28
T 1 8 15 22 29
W 2 9 16 23 30
T 3 10 17 24 31
F 4 11 18 25
S 5 12 19 26
August
S 3 10 17 24 31
M 4 11 18 25
T 5 12 19 26
W 6 13 20 27
T 7 14 21 28
F 1 8 15 22 29
S 2 9 16 23 30
September
S 7 14 21 28
M 1 8 15 22 29
T 2 9 16 23 30
W 3 10 17 24
T 4 11 18 25
F 5 12 19 26
S 6 13 20 27
October
S 5 12 19 26
M 6 13 20 27
T 7 14 21 28
W 1 8 15 22 29
T 2 9 16 23 30
F 3 10 17 24 31
S 4 11 18 25
November
S 2 9 16 23 30
M 3 10 17 24
T 4 11 18 25
W 5 12 19 26
T 6 13 20 27
F 7 14 21 28
S 1 8 15 22 29
December
S 7 14 21 28
M 1 8 15 22 29
T 2 9 16 23 30
W 3 10 17 24 31
T 4 11 18 25
F 5 12 19 26
S 6 13 20 27
Dựa vào lời giải bằng phương pháp sử dụng mảng ở trên thì việc in lịch theo chiều dọc trở nên dễ dàng hơn bao giờ hết, chỉ cần hoán đổi trị số cột i và j là xong:
#include <stdio.h>
#include <stdlib.h>
int isLeapYear(int y) {
return (!(y % 4) && y % 100) || !(y % 400);
}
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
int main()
{
char months[12][10] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
char daysow[7][4] = {"S", "M", "T", "W", "T", "F", "S"};
int y;
printf("Input year: ");
scanf("%d", &y);
putchar('\n');
for (int m = 1; m <= 12; m++, printf("\n")) {
printf("%s\n", months[m - 1]);
int top;
switch(m) {
case 2: top = isLeapYear(y) ? 29 : 28; break;
case 4: case 6: case 9: case 11: top = 30; break;
default: top = 31; break;
}
int dow = getDow(1, m, y);
int days[6][7] = {0};
int* d = days[0];
for (int i = 0; i < dow; i++) d++;
for (int i = 0; i < top; i++) *(d++) = i + 1;
int col = dow + top > 35 ? 6 : 5;
for (int j = 0; j < 7; j++, putchar('\n')) {
printf("%2s ", daysow[j]);
for (int i = 0; i < col; i++)
days[i][j] == 0 ? printf("%3s", " ") : printf("%3d", days[i][j]);
}
}
return 0;
}
Sử dụng mảng cấp phát động
Cũng tương tự như trên nhưng thay vì dùng mảng 2 chiều có kích thước cố định thì tôi dùng mảng cấp phát động với kích thước hàng thay đổi (5 hoặc 6) tuỳ theo số lượng ngày trong tháng. Thực ra cũng không cần thiết lắm nhưng tôi xem đây như một cách để luyện tập thực hành với mảng cấp phát động trong C.
Lần này tôi tách riêng phần tạo mảng thành một hàm riêng biệt initCal(dow, top) với dow (day of week) là thứ của ngày đầu tiên của tháng và top là số ngày tối đa của tháng đó. Phần khởi tạo các phần tử của mảng bạn sẽ thấy hơi phức tạp hơn so với mảng có kích thước cố định bởi vì khi tạo mảng kích thước động, tôi nhận thấy các phần tử trong mảng không phải lúc nào cũng nằm liền kề nhau. Do đó không thể sử dụng phương pháp dùng con trỏ trỏ vào phần tử đầu tiên rồi quét hết qua mảng được. Lúc này, tôi buộc phải dùng phương pháp duyệt mảng truyền thống bằng trị số dòng và cột.
In lịch theo chiều ngang:
#include <stdio.h>
#include <stdlib.h>
int isLeapYear(int y) {
return (!(y % 4) && y % 100) || !(y % 400);
}
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
int getRows(int dow, int top) {
return dow + top > 35 ? 6 : 5;
}
int** initCal(int dow, int top) {
int rows = getRows(dow, top);
int cols = 7;
int** cal = (int**) calloc(rows, sizeof(int*));
for (int r = 0; r < rows; r++)
cal[r] = (int*) calloc(cols, sizeof(int));
int day = 1;
for (int i = 0; i < rows; i++) {
int* d = cal[i];
if (i == 0) {
for (int j = 0; j < dow; j++) d++;
for (int j = dow; j < cols; j++) *(d++) = day++;
} else {
for (int j = 0; j < cols; j++) {
*(d++) = day++;
if (day > top) break;
}
}
if (day > top) break;
}
return cal;
}
int main()
{
char months[12][10] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
char daysow[7][4] = {"S", "M", "T", "W", "T", "F", "S"};
int y;
printf("Input year: ");
scanf("%d", &y);
putchar('\n');
for (int m = 1; m <= 12; m++, printf("\n")) {
printf("%s\n", months[m - 1]);
int top;
switch(m) {
case 2: top = isLeapYear(y) ? 29 : 28; break;
case 4: case 6: case 9: case 11: top = 30; break;
default: top = 31; break;
}
int dow = getDow(1, m, y);
int rows = getRows(dow, top), cols = 7;
int** cal = initCal(dow, top);
for (int i = 0; i < cols; i++) printf("%3s", daysow[i]);
printf("\n");
for (int i = 0; i < rows; i++, putchar('\n'))
for (int j = 0; j < cols; j++)
cal[i][j] == 0 ? printf("%3s", " ") : printf("%3d", cal[i][j]);
free(cal);
}
return 0;
}
In lịch theo chiều dọc:
#include <stdio.h>
#include <stdlib.h>
int isLeapYear(int y) {
return (!(y % 4) && y % 100) || !(y % 400);
}
int getDow(int q, int m, int y) {
if (m == 1) { m = 13; y--; }
if (m == 2) { m = 14; y--; }
int k = y % 100;
int j = y / 100;
int h = q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j;
h = h % 7;
return h == 0 ? 6 : h - 1;
}
int getRows(int dow, int top) {
return dow + top > 35 ? 6 : 5;
}
int** initCal(int dow, int top) {
int rows = getRows(dow, top);
int cols = 7;
int** cal = (int**) calloc(rows, sizeof(int*));
for (int r = 0; r < rows; r++)
cal[r] = (int*) calloc(cols, sizeof(int));
int day = 1;
for (int i = 0; i < rows; i++) {
int* d = cal[i];
if (i == 0) {
for (int j = 0; j < dow; j++) d++;
for (int j = dow; j < cols; j++) *(d++) = day++;
} else {
for (int j = 0; j < cols; j++) {
*(d++) = day++;
if (day > top) break;
}
}
if (day > top) break;
}
return cal;
}
int main()
{
char months[12][10] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
char daysow[7][4] = {"S", "M", "T", "W", "T", "F", "S"};
int y;
printf("Input year: ");
scanf("%d", &y);
putchar('\n');
for (int m = 1; m <= 12; m++, printf("\n")) {
printf("%s\n", months[m - 1]);
int top;
switch(m) {
case 2: top = isLeapYear(y) ? 29 : 28; break;
case 4: case 6: case 9: case 11: top = 30; break;
default: top = 31; break;
}
int dow = getDow(1, m, y);
int rows = getRows(dow, top), cols = 7;
int** cal = initCal(dow, top);
for (int j = 0; j < cols; j++, putchar('\n')) {
printf("%s ", daysow[j]);
for (int i = 0; i < rows; i++)
cal[i][j] == 0 ? printf("%3s", " ") : printf("%3d", cal[i][j]);
}
free(cal);
}
return 0;
}