Chương trình dựa trên Thuật toán tính lịch Âm Dương đề xuất bởi Hồ Ngọc Đức.

Mã nguồn chương trình được viết bằng ngôn ngữ C. Chương trình này được dùng làm backend cho nhiều script khác của tôi.

lunisolar.c

/*! \file lunisolar.c
    \brief Convert Solar Calendar to Lunisolar Calendar
           and vice versa.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <math.h>
 
#define FALSE           0
#define TRUE            1
#define SOLAR2LUNAR     0
#define LUNAR2SOLAR     1
#define DATE_DELIMITER  "/-"
 
static char *dow_list[] = {
        "Chủ nhật", "Thứ hai", "Thứ ba", "Thứ tư",
        "Thứ năm", "Thứ sáu", "Thứ bảy"
    };
 
static char *stems_list[] = {
        "Giáp", "Ất", "Bính", "Đinh", "Mậu",
        "Kỷ", "Canh", "Tân", "Nhâm", "Quý"
    };
 
static char *branches_list[] = {
        "Tý", "Sửu", "Dần", "Mão", "Thìn",
        "Tỵ", "Ngọ", "Mùi", "Thân", "Dậu",
        "Tuất", "Hợi"
    };
 
static char *terms_list[] = {
        "Xuân Phân", "Thanh Minh", "Cốc Vũ", "Lập Hạ", "Tiểu Mãn",
        "Mang Chủng", "Hạ Chí", "Tiểu Thử", "Đại Thử", "Lập Thu"
        "Xử Thử", "Bạch Lộ", "Thu Phân", "Hàn Lộ", "Sương Giáng",
        "Lập Đông", "Tiểu Tuyết", "Đại Tuyết", "Đông Chí", "Tiểu Hàn",
        "Đại Hàn", "Lập Xuân", "Vũ Thủy", "Kinh Trập"
    };
 
static int golden_hour_masks[] = { 0x034B, 0x0D2C, 0x04B3, 0x02CD, 0x0B34, 0x0CD2 };
 
static void usage(int argc, char *argv[])
{
    fprintf(stderr, "Usage: %s [-r] [-s [-l]] [-t time_zone] [-y YEAR] DATE\n", argv[0]);
    fprintf(stderr, "   -r: raw format. (default: FALSE)\n");
    fprintf(stderr, "   -s: convert lunar to solar date. (default: solar to lunar)\n");
    fprintf(stderr, "   -l: is a leap lunar date. (default: FALSE)\n");
    fprintf(stderr, "   -t: time zone in float format. (default: +7.0)\n");
    exit(EXIT_FAILURE);
}
 
static int is_leap_year(int solar_year)
{
    if ((solar_year % 4) != 0)
        return FALSE;
 
    if ((solar_year % 400) == 0)
        return TRUE;
 
    if ((solar_year % 100) == 0)
        return FALSE;
 
    return TRUE;
}
 
static int is_valid_solar_date(int solar_day, int solar_month, int solar_year)
{
    int month_length[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
 
    if (is_leap_year(solar_year))
        month_length[2] = 29;
 
    if (solar_month < 1 || solar_month > 12)
        return FALSE;
 
    if (solar_day < 1 || solar_day > month_length[solar_month])
        return FALSE;
 
    return TRUE;
}
 
static long jd_from_date(int solar_day, int solar_month, int solar_year)
{
    int a, y, m, jd;
 
    a = (14 - solar_month) / 12;
    y = solar_year + 4800 - a;
    m = solar_month + 12 * a - 3;
    jd = solar_day + ((153 * m + 2) / 5) + (365 * y) + (y / 4) - (y / 100) + (y / 400) - 32045;
    if (jd < 2299161)
        jd = solar_day + ((153 * m + 2) / 5) + (365 * y) + (y / 4) - 32083;
 
    return jd;
}
 
static void jd_to_date(int jd, int *solar_day, int *solar_month, int *solar_year)
{
    int a, b, c, d, e, m;
 
    if (jd > 2299160) { /* after 5/10/1582, Gregorian calendar */
        a = jd + 32044;
        b = (4 * a + 3) / 146097;
        c = a - (b * 146097) / 4;
    } else {
        b = 0;
        c = jd + 32082;
    }
    d = (4 * c + 3) / 1461;
    e = c - (1461 * d) / 4;
    m = (5 * e + 2) / 153;
    *solar_day = e - (153 * m + 2) / 5 + 1;
    *solar_month = m + 3 - 12 * (m / 10);
    *solar_year = b * 100 + d - 4800 + (m / 10);
}
 
static int get_new_moon_day(int k, double time_zone)
{
    double t, t2, t3, dr, jd1, m, mpr, f, c1, delta_t, jd_new;
 
    t = k / 1236.85; /* time in Julian centuries from 1900 January 0.5 */
    t2 = t * t;
    t3 = t2 * t;
    dr = M_PI / 180.0;
    jd1 = 2415020.75933 + 29.53058868 * k + 0.0001178 * t2 - 0.000000155 * t3;
    jd1 = jd1 + 0.00033 * sin((166.56 + 132.87 * t - 0.009173 * t2) * dr); /* mean new moon */
    m = 359.2242 + 29.10535608 * k - 0.0000333 * t2 - 0.00000347 * t3; /* sun's mean anomaly */
    mpr = 306.0253 + 385.81691806 * k + 0.0107306 * t2 + 0.00001236 * t3; /* moon's mean anomaly */
    f = 21.2964 + 390.67050646 * k - 0.0016528 * t2 - 0.00000239 * t3; /* moon's argument of latitude */
    c1 = (0.1734 - 0.000393 * t) * sin(m * dr) + 0.0021 * sin(2 * dr * m);
    c1 = c1 - 0.4068 * sin(mpr * dr) + 0.0161 * sin(dr * 2 * mpr);
    c1 = c1 - 0.0004 * sin(dr * 3 * mpr);
    c1 = c1 + 0.0104 * sin(dr * 2 * f) - 0.0051 * sin(dr * (m + mpr));
    c1 = c1 - 0.0074 * sin(dr * (m - mpr)) + 0.0004 * sin(dr * (2 * f + m));
    c1 = c1 - 0.0004 * sin(dr * (2 * f - m)) - 0.0006 * sin(dr * (2 * f + mpr));
    c1 = c1 + 0.0010 * sin(dr * (2 * f - mpr)) + 0.0005 * sin(dr * (2 * mpr + m));
 
    if (t < -11)
        delta_t = 0.001 + 0.000839 * t + 0.0002261 * t2 - 0.00000845 * t3 - 0.000000081 * t * t3;
    else
        delta_t = -0.000278 + 0.000265 * t + 0.000262 * t2;
 
    jd_new = jd1 + c1 - delta_t;
 
    return (int) (jd_new + 0.5 + time_zone / 24);
}
 
static double get_sun_longitude(int jdn, double time_zone)
{
    double t, t2, dr, m, l0, dl, l;
 
    t = (jdn - 2451545.5 - time_zone / 24) / 36525; /* time in Julian centuries from 2000-01-01 12:00:00 GMT */
    t2 = t * t;
    dr = M_PI / 180.0; /* degree to radian */
    m = 357.52910 + 35999.05030 * t - 0.0001559 * t2 - 0.00000048 * t * t2; /* mean anomaly, degree */
    l0 = 280.46645 + 36000.76983 * t + 0.0003032 * t2; /* mean longitude, degree */
    dl = (1.914600 - 0.004817 * t - 0.000014 * t2) * sin(dr * m);
    dl = dl + (0.019993 - 0.000101 * t) * sin(dr * 2 * m) + 0.000290 * sin(dr * 3 * m);
    l = l0 + dl; /* true longitude, degree */
    l = l * dr;
    l = l - M_PI * 2 * (int) (l / (M_PI * 2)); /* normalize to (0, 2*PI) */
 
    return (l / M_PI * 6);
}
 
static int get_lunar_month_11(int year, double time_zone)
{
    long off;
    int k, nm, sun_long;
 
    off = jd_from_date(31, 12, year) - 2415021;
    k = (int) (off / 29.530588853);
    nm = get_new_moon_day(k, time_zone);
    sun_long = (int) get_sun_longitude(nm, time_zone); /* sun longitude at local midnight */
    if (sun_long >= 9)
        nm = get_new_moon_day(k - 1, time_zone);
 
    return nm;
}
 
static int get_leap_month_offset(int a11, double time_zone)
{
    double last, arc;
    int k, i;
    k = (int) (((double) a11 - 2415021.076998695) / 29.530588853 + 0.5);
    last = 0;
    i = 1; /* we start with the month following lunar month 11 */
    arc = (int) get_sun_longitude(get_new_moon_day(k + i, time_zone), time_zone);
    do {
        last = arc;
        i++;
        arc = (int) get_sun_longitude(get_new_moon_day(k + i, time_zone), time_zone);
    } while (arc != last && i < 14);
 
    return (i - 1);
}
 
void convert_solar_to_lunar(int solar_day, int solar_month, int solar_year,
        double time_zone, int *lunar_day, int *lunar_month, int *lunar_year,
        int *lunar_leap, int *jd)
{
    int day_number, month_start, a11, b11;
    int k, diff;
 
    day_number = jd_from_date(solar_day, solar_month, solar_year);
    *jd = day_number;
 
    k = (int) ((day_number - 2415021.076998695) / 29.530588853);
    month_start = get_new_moon_day(k + 1, time_zone);
    if (month_start > day_number)
        month_start = get_new_moon_day(k, time_zone);
    a11 = get_lunar_month_11(solar_year, time_zone);
    b11 = a11;
    if (a11 >= month_start) {
        *lunar_year = solar_year;
        a11 = get_lunar_month_11(solar_year - 1, time_zone);
    } else {
        *lunar_year = solar_year + 1;
        b11 = get_lunar_month_11(solar_year + 1, time_zone);
    }
    *lunar_day = day_number - month_start + 1;
    diff = (int) ((month_start - a11) / 29);
    *lunar_leap = FALSE;
    *lunar_month = diff + 11;
    if (b11 - a11 > 365) {
        int leap_month_diff = get_leap_month_offset(a11, time_zone);
        if (diff >= leap_month_diff) {
            *lunar_month = diff + 10;
            if (diff == leap_month_diff)
                *lunar_leap = TRUE;
        }
    }
    if (*lunar_month > 12)
        *lunar_month = *lunar_month - 12;
 
    if (*lunar_month >= 11 && diff < 4)
        *lunar_year -= 1;
}
 
void convert_lunar_to_solar(int lunar_day, int lunar_month,
        int lunar_year, int lunar_leap, double time_zone,
        int *solar_day, int *solar_month, int *solar_year, int *jd)
{
    int k, a11, b11, off, leap_ofs, leap_month, month_start;
 
    if (lunar_month < 11) {
        a11 = get_lunar_month_11(lunar_year - 1, time_zone);
        b11 = get_lunar_month_11(lunar_year, time_zone);
    } else {
        a11 = get_lunar_month_11(lunar_year, time_zone);
        b11 = get_lunar_month_11(lunar_year + 1, time_zone);
    }
 
    off = lunar_month - 11;
    if (off < 0)
        off += 12;
 
    if (b11 - a11 > 365) {
        leap_ofs = get_leap_month_offset(a11, time_zone);
        leap_month = leap_ofs - 2;
        if (leap_month < 0)
            leap_month += 12;
 
        if (lunar_leap == TRUE && lunar_month != leap_month) {
            *solar_day = 0;
            *solar_month = 0;
            *solar_year = 0;
 
            return;
        }
        else if (lunar_leap == TRUE || off >= leap_ofs)
            off += 1;
    }
 
    k = (int) (0.5 + (a11 - 2415021.076998695) / 29.530588853);
    month_start = get_new_moon_day(k + off, time_zone);
    *jd = month_start + lunar_day - 1;
    jd_to_date(*jd, solar_day, solar_month, solar_year);
}
 
void get_day_info(int lunar_month, int lunar_year, int jd, double time_zone,
        int *day_of_week, int *day_stem, int *day_branch, int *month_stem,
        int *month_branch, int *year_stem, int *year_branch, int *term,
        int *hour0_stem, int golden_hours[])
{
    int hours;
    int i, cnt;
 
    *day_of_week = (jd + 1) % 7;
 
    /* day stem-branch */
    *day_stem = (jd + 9) % 10;
    *day_branch = (jd + 1) % 12;
 
    /* day stem-branch */
    *month_stem = (lunar_year * 12 + lunar_month + 3) % 10;
    *month_branch = (lunar_month + 1) % 12;
 
    /* year stem-branch */
    *year_stem = (lunar_year + 6) % 10;
    *year_branch = (lunar_year + 8) % 12;
 
    /* solar term */
    *term = (int) get_sun_longitude((jd - 0.5 - time_zone / 24.0) + 1, time_zone) * 2;
 
    *hour0_stem = (jd - 1) * 2 % 10;
 
    /* golden hours */
    hours = golden_hour_masks[*day_branch % 6];
    cnt = 0;
    for (i = 0; i <= 12; i++)
        if (hours & (1 << i))
            golden_hours[cnt++] = i;
}
 
int main(int argc, char *argv[])
{
    double time_zone = 7.0;
    int lunar_leap = FALSE;
    int mode = SOLAR2LUNAR;
    int human_readable = TRUE;
    int jd;
    int solar_day, solar_month, solar_year;
    int lunar_day, lunar_month, lunar_year;
    int count;
    time_t t;
    struct tm tm;
    char opt;
    char *str = NULL;
    char *pch;
    int day_of_week;
    int day_stem, day_branch, month_stem, month_branch, year_stem, year_branch, term;
    int hour0_stem, golden_hours[6];
 
    while (((opt = getopt(argc, argv, "hslrt:")) != -1) && (opt != 255)) {
        switch (opt) {
        case 's':
            mode = LUNAR2SOLAR;
            break;
 
        case 'l':
            lunar_leap = TRUE;
            break;
 
        case 'r':
            human_readable = FALSE;
            break;
 
        case 't':
            time_zone = atof(optarg);
            break;

      case 'h':
            usage(argc, argv);
            break;

        default:
            usage(argc, argv);
        }
    }
 
    /* the arguments after the command-line options */
    for (; optind < argc; optind++) {
        str = argv[optind];
    }
 
    if (str != NULL) {
        count = 0;
        pch = strtok(str, DATE_DELIMITER);
        while (pch != NULL && count < 3) {
            switch (count) {
            case 0:
                solar_day = atoi(pch);
                break;
 
            case 1:
                solar_month = atoi(pch);
                break;
 
            case 2:
                solar_year = atoi(pch);
                break;
 
            default:
                break;
            }
            pch = strtok(NULL, DATE_DELIMITER); /* next token */
            count++;
        }
    } else {
        t = time(NULL);
        tm = *localtime(&t);
 
        solar_day = tm.tm_mday;
        solar_month = tm.tm_mon + 1;
        solar_year = tm.tm_year + 1900;
    }
 
    if (mode == SOLAR2LUNAR) {
        if (!is_valid_solar_date(solar_day, solar_month, solar_year)) {
            fprintf(stderr, "invalid input solar date\n");
            return EXIT_FAILURE;
        }
        convert_solar_to_lunar(solar_day, solar_month, solar_year, time_zone,
                &lunar_day, &lunar_month, &lunar_year, &lunar_leap, &jd);
 
    } else {
        if (str != NULL) {
            lunar_day = solar_day;
            lunar_month = solar_month;
            lunar_year = solar_year;
            convert_lunar_to_solar(lunar_day, lunar_month, lunar_year, lunar_leap, time_zone,
                    &solar_day, &solar_month, &solar_year, &jd);
            if (solar_day == 0) {
                fprintf(stderr, "invalid input lunar date\n");
                return EXIT_FAILURE;
            }
        }
    }
    get_day_info(lunar_month, lunar_year, jd, time_zone, &day_of_week,
                &day_stem, &day_branch, &month_stem, &month_branch,
                &year_stem, &year_branch,
                &term, &hour0_stem, golden_hours);
 
    /***********************************************************/
    if (human_readable) {
        printf("DL    : %02d/%02d/%04d (%s)\n", solar_day, solar_month, solar_year, dow_list[day_of_week]);
        printf("AL    : %02d/%02d %s\n", lunar_day, lunar_month, (lunar_leap) ? "(N)" : "");
        printf("Giờ   : %s %s\nNgày  : %s %s\nTháng : %s %s\nNăm   : %s %s\nTiết  : %s\nGiờ Hoàng Đạo:\n",
                    stems_list[hour0_stem], branches_list[0],
                    stems_list[day_stem],   branches_list[day_branch],
                    stems_list[month_stem], branches_list[month_branch],
                    stems_list[year_stem],  branches_list[year_branch],
                    terms_list[term]);
        for (count = 0; count < 6; count++)
            printf("  %s (%02dh ~ %02dh)\n", branches_list[golden_hours[count]],
                    (golden_hours[count] * 2 + 23) % 24,
                    (golden_hours[count] * 2 +  1) % 24);
    } else {
        printf("%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n",
                day_of_week, solar_day, solar_month, solar_year,
                lunar_day, lunar_month, lunar_year, lunar_leap,
                hour0_stem, 0, day_stem, day_branch,
                month_stem, month_branch, year_stem,  year_branch, term,
                golden_hours[0],golden_hours[1],golden_hours[2],
                golden_hours[3],golden_hours[4],golden_hours[5]);
    }
 
    return EXIT_SUCCESS;
}

Compile mã nguồn với GCC, chương trình yêu cầu thư viện libmath.

$ gcc -lm -o ~/bin/lunisolar lunisolar.c

Truyền tham số -h để xem ý nghĩa các tham số.

$ lunisolar.exe -h

Usage: lunisolar [-r] [-s [-l]] [-t time_zone] [-y YEAR] DATE
   -r: raw format. (default: FALSE)
   -s: convert lunar to solar date. (default: solar to lunar)
   -l: is a leap lunar date. (default: FALSE)
   -t: time zone in float format. (default: +7.0)

Chương trình có 2 kiểu đầu ra.

Một là kiểu human-readable, ví dụ:

$ lunisolar 1/1/2000

DL    : 01/01/2000 (Thứ bảy)
AL    : 25/11
Giờ   : Nhâm Tý
Ngày  : Mậu Ngọ
Tháng : Bính Tý
Năm   : Kỷ Mão
Tiết  : Tiểu Hàn
Giờ Hoàng Đạo:
  Tý (23h ~ 01h)
  Sửu (01h ~ 03h)
  Mão (05h ~ 07h)
  Ngọ (11h ~ 13h)
  Thân (15h ~ 17h)
  Dậu (17h ~ 19h)

Hai là kiểu hiển thị cho Shell Script xử lý khi truyền tham số -r với các giá trị ngăn cách bằng dấu cách như sau:

Thứ _ Ngày DL _ Tháng DL _ Năm DL _ Ngày ÂL _ Tháng ÂL _ Năm ÂL _ Nhuận _ Can Giờ Tý _ 0 _ Can Ngày _ Chi Ngày _ Can Tháng _ Chi Tháng _ Can Năm _ Chi Năm _ Tiết khí _ Giờ Hoàng Đạo [1-6]

Ví dụ:

$ lunisolar.exe -r 1/1/2000

6 1 1 2000 25 11 1999 0 8 0 4 6 2 0 5 3 18 0 1 3 6 8 9