diff options
Diffstat (limited to 'content/cal/lunar.js')
-rw-r--r-- | content/cal/lunar.js | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/content/cal/lunar.js b/content/cal/lunar.js new file mode 100644 index 0000000..4ab5e26 --- /dev/null +++ b/content/cal/lunar.js @@ -0,0 +1,522 @@ +// Part 1: Converter functions + +// Sin function with degree instead of radian +function sind(degree) { + return Math.sin(degree * Math.PI / 180) +} + +// Convert Gregorian date to Julian day +function getJulianDay(date) { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + + let a = parseInt((14 - month) / 12) + let y = year + 4800 - a + y = y * 365 + parseInt(y / 4) - parseInt(y / 100) + parseInt(y / 400) + let m = month + 12 * a - 3 + return parseInt(day + (153 * m + 2) / 5) + y - 32045 +} + +// Convert Julian day to Gregorian date +function getDateFromJulianDay(jd) { + const a = jd + 32044 + const b = parseInt((4 * a + 3) / 146097) + const c = a - parseInt((b * 146097) / 4) + const d = parseInt((4 * c + 3) / 1461) + const e = c - parseInt((1461 * d) / 4) + const m = parseInt((5 * e + 2) / 153) + const dd = e - parseInt((153 * m + 2) / 5) + 1 + const mm = m + 2 - 12 * parseInt(m / 10) + const yy = b * 100 + d - 4800 + parseInt(m / 10) + return new Date(yy, mm, dd) +} + +// Get kth new moon day since 1900-01-01 in Julian day +function getNewMoonDay(k, tzOffset) { + const t = k / 1236.85 + const t2 = t * t + const t3 = t2 * t + + let jd1 = 2415020.75933 + 29.53058868 * k + 0.0001178 * t2 - 0.000000155 * t3 + jd1 += 0.00033 * sind(166.56 + 132.87 * t - 0.009173 * t2) + + const m = 359.2242 + 29.10535608 * k - 0.0000333 * t2 - 0.00000347 * t3 + const mpr = 306.0253 + 385.81691806 * k + 0.0107306 * t2 + 0.00001236 * t3 + const f = 21.2964 + 390.67050646 * k - 0.0016528 * t2 - 0.00000239 * t3 + + let c1 = (0.1734 - 0.000393 * t) * sind(m) + 0.0021 * sind(2 * m) + c1 -= 0.4068 * sind(mpr) + 0.0161 * sind(2 * mpr) + c1 -= 0.0004 * sind(3 * mpr) + c1 += 0.0104 * sind(2 * f) - 0.0051 * sind(m + mpr) + c1 -= 0.0074 * sind(m - mpr) + 0.0004 * sind(2 * f + m) + c1 -= 0.0004 * sind(2 * f - m) - 0.0006 * sind(2 * f + mpr) + c1 += 0.0010 * sind(2 * f - mpr) + 0.0005 * sind(2 * mpr + m) + + let dt + if (t < -11) { + dt = 0.001 + 0.000839 * t + 0.0002261 * t2 - 0.00000845 * t3 - 0.000000081 * t * t3 + } else { + dt = -0.000278 + 0.000265 * t + 0.000262 * t2 + } + let jdNew = jd1 + c1 - dt + return parseInt(jdNew + 0.5 + tzOffset / 24) +} + +// Get solar term from Julian day, from 0 to 23, corresponding to Chunfen to Jingzhe. +function getSolarTerm(jd, tzOffset) { + t = (jd - 2451545.5 - tzOffset / 24) / 36525 + t2 = t * t + m = 357.52910 + 35999.05030 * t - 0.0001559 * t2 - 0.00000048 * t * t2 + l0 = 280.46645 + 36000.76983 * t + 0.0003032 * t2 + dl = (1.914600 - 0.004817 * t - 0.000014 * t2) * sind(m) + dl = dl + (0.019993 - 0.000101 * t) * sind(2 * m) + 0.000290 * sind(3 * m) + l = l0 + dl + l %= 360 // normalize to (0, 360) + return parseInt(l * 24 / 360) +} + +// Get the solar longitude of the day, from 0 to 11. +// 0 is corresponding to 0 degree to 30 degree and solar term Chunfen (spring equinox) - Guyu +// 1 is corresponding to 30 degree to 60 degree and solar term Guyu - Xiaoman +// 2 is corresponding to 60 degree to 90 degree and solar term Xiaoman - Xiazhi (summer solstince) +// etc +function getSunLongitude(jd, tzOffset) { + t = (jd - 2451545.5 - tzOffset / 24) / 36525 + t2 = t * t + m = 357.52910 + 35999.05030 * t - 0.0001559 * t2 - 0.00000048 * t * t2 + l0 = 280.46645 + 36000.76983 * t + 0.0003032 * t2 + dl = (1.914600 - 0.004817 * t - 0.000014 * t2) * sind(m) + dl = dl + (0.019993 - 0.000101 * t) * sind(2 * m) + 0.000290 * sind(3 * m) + l = l0 + dl + l %= 360 // normalize to (0, 360 deg) + return parseInt(l * 12 / 360) +} + +// Get Julian day for the first day of 11th lunar month of the year. +function getMonth11(year, tzOffset) { + off = getJulianDay(new Date(year, 11, 31)) - 2415021 + k = parseInt(off / 29.530588853) + nm = getNewMoonDay(k, tzOffset) + sun_long = getSunLongitude(nm, tzOffset) + if (sun_long >= 9) { + nm = getNewMoonDay(k-1, tzOffset) + } + return nm +} + +// Get the leap month offset from the 11th month before it. +// +// a11: Julian day of the first day of the 11th month +function getLeapMonthOffset(a11, tzOffset) { + k = parseInt((a11 - 2415021.076998695) / 29.530588853 + 0.5) + i = 1 + arc = getSunLongitude(getNewMoonDay(k + i, tzOffset), tzOffset) + last = 0 + while (arc != last && i < 14) { + last = arc + i++ + arc = getSunLongitude(getNewMoonDay(k + i, tzOffset), tzOffset) + } + return i - 1 +} + +function convertSolarToLunar(date, tzOffset) { + const jd = getJulianDay(date) + + let k = parseInt((jd - 2415021.076998695) / 29.530588853) + let monthStart = getNewMoonDay(k + 1, tzOffset) + if (monthStart > jd) { + monthStart = getNewMoonDay(k, tzOffset) + } + let a11 = getMonth11(date.getFullYear(), tzOffset) + let b11 = a11 + let lunarYear + if (a11 >= monthStart) { + lunarYear = date.getFullYear() + a11 = getMonth11(lunarYear - 1, tzOffset) + } else { + lunarYear = date.getFullYear() + 1 + b11 = getMonth11(lunarYear, tzOffset) + } + let lunarDay = jd - monthStart + 1 + let diff = parseInt((monthStart - a11) / 29) + let isLeapMonth = false + let lunarMonth = diff + 11 + if (b11 - a11 > 365) { + leapMonthDiff = getLeapMonthOffset(a11, tzOffset) + if (diff >= leapMonthDiff) { + lunarMonth = diff + 10 + if (diff == leapMonthDiff) { + isLeapMonth = true + } + } + } + if (lunarMonth > 12) { + lunarMonth -= 12 + } + if (lunarMonth >= 11 && diff < 4) { + lunarYear -= 1 + } + return { + year: lunarYear, + month: lunarMonth, + day: lunarDay, + isLeap: isLeapMonth, + jd: jd + } +} + +const CAN_VI = ['Giáp', 'Ất', 'Bính', 'Đinh', 'Mậu', 'Kỷ', 'Canh', 'Tân', 'Nhâm', 'Quý'] +const CAN_ZH = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'] +const CHI_VI = ['Tý', 'Sửu', 'Dần', 'Mão', 'Thìn', 'Tỵ', 'Ngọ', 'Mùi', 'Thân', 'Dậu', 'Tuất', 'Hợi'] +const CHI_ZH = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'] +const TERM_VI = [ + '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ũ thuỷ', + 'Kinh trập', +] +const TERM_ZH = [ + '春分', + '清明', + '穀雨', + '立夏', + '小滿', + '芒種', + '夏至', + '小暑', + '大暑', + '立秋', + '處暑', + '白露', + '秋分', + '寒露', + '霜降', + '立冬', + '小雪', + '大雪', + '冬至', + '小寒', + '大寒', + '立春', + '雨水', + '驚蟄' +] + +// Get can/chi of a lunar date +function getZodiac({year, month, day, jd}) { + let yearZodiac = { + can: (year + 6) % 10, + chi: (year + 8) % 12 + } + let monthZodiac = { + can: (year * 12 + month + 3) % 10, + chi: (month + 1) % 12 + } + let dayZodiac = { + can: (jd + 9) % 10, + chi: (jd + 1) % 12 + } + return {yearZodiac, monthZodiac, dayZodiac} +} + +function getZodiacText(zodiac, lang) { + switch (lang) { + case "vi": + return CAN_VI[zodiac.can] + " " + CHI_VI[zodiac.chi] + case "zh": + return CAN_ZH[zodiac.can] + CHI_ZH[zodiac.chi] + default: + throw("Unsupported") + } +} + +// Part 2: Update DOM + +// DOM +const dateInput = document.querySelector('#solar-date') +const calendar = document.querySelector('#lunar-solar-cal') +const solarYearOutput = document.querySelector('#solar-year') +const solarMonthOutputEN = document.querySelector('#solar-month-en') +const solarMonthOutputVI = document.querySelector('#solar-month-vi') + +const solarDayOutput = document.querySelector('#solar-day') +const specialDayOutputVI = document.querySelector('#special-day-vi') +const specialDayOutputZH = document.querySelector('#special-day-zh') + +const solarWeekdayOutputEN = document.querySelector('#solar-weekday-en') +const solarWeekdayOutputVI = document.querySelector('#solar-weekday-vi') +const solarWeekdayOutputZH = document.querySelector('#solar-weekday-zh') + +const lunarYearOutputVI = document.querySelector('#lunar-year-vi') +const lunarMonthOutputVI = document.querySelector('#lunar-month-vi') +const lunarMonthZodiacOutputVI = document.querySelector('#lunar-month-zodiac-vi') +const lunarDayZodiacOutputVI = document.querySelector('#lunar-day-zodiac-vi') +const solarTermVI = document.querySelector('#solar-term-vi') + +const lunarDayOutput = document.querySelector('#lunar-day') + +const lunarYearOutputZH = document.querySelector('#lunar-year-zh') +const lunarMonthOutputZH = document.querySelector('#lunar-month-zh') +const lunarMonthZodiacOutputZH = document.querySelector('#lunar-month-zodiac-zh') +const lunarDayOutputZH = document.querySelector('#lunar-day-zh') +const lunarDayZodiacOutputZH = document.querySelector('#lunar-day-zodiac-zh') +const solarTermZH = document.querySelector('#solar-term-zh') + +const dayBeforeBtn = document.querySelector('#nav-day-before') +const dayAfterBtn = document.querySelector('#nav-day-after') + +const monthFormatEN = new Intl.DateTimeFormat("en-US", {month: "long"}) +const monthFormatVI = new Intl.DateTimeFormat("vi-VN", {month: "long"}) +const weekdayFormatEN = new Intl.DateTimeFormat("en-US", {weekday: "long"}) +const weekdayFormatVI = new Intl.DateTimeFormat("vi-VN", {weekday: "long"}) +const weekdayFormatZH = new Intl.DateTimeFormat("zh-CN", {weekday: "long"}) + +const SOLAR_MONTH_VI = ["Một", "Hai", "Ba", "Tư", "Năm", "Sáu", "Bảy", "Tám", "Chín", "Mười", "Mười Một", "Mười Hai"] +const LUNAR_MONTH_VI = ["Giêng", "Hai", "Ba", "Tư", "Năm", "Sáu", "Bảy", "Tám", "Chín", "Mười", "Mười Một", "Chạp"] +const LUNAR_MONTH_ZH = ["元", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "臘", ] +const tzOffset = 7 + +const CHINESE_NUMBERS = "一二三四五六七八九十" + +function getChineseDay(day) { + if (day <= 10) { + return "初" + CHINESE_NUMBERS[day - 1] + } else if (day < 20) { + return "十" + CHINESE_NUMBERS[(day - 1) % 10] + } else if (day == 20) { + return "二十" + } else if (day < 30) { + return "廿" + CHINESE_NUMBERS[(day - 1) % 10] + } else { + return "三十" + } +} + +function isFullMonth(date, lunarDate) { + const daysTill30 = 30 - lunarDate.day + date.setDate(date.getDate() + daysTill30) + let day30 = convertSolarToLunar(date, tzOffset) + return day30.month === lunarDate.month +} + +const specialDaysSolar = { + "01-01": { + vi: "Tết Dương Lịch", + zh: "元旦", + isHoliday: true + }, + "02-03": { + vi: "Ngày thành lập Đảng Cộng Sản Việt Nam", + isHoliday: false + }, + "02-14": { + vi: "Lễ tình nhân (Valentine)", + isHoliday: false + }, + "03-08": { + vi: "Ngày Quốc tế Phụ nữ", + isHoliday: false + }, + "04-30": { + vi: "Ngày Thống nhất", + isHoliday: true + }, + "05-01": { + vi: "Ngày Quốc tế Lao động", + zh: "劳动节", + isHoliday: true + }, + "06-01": { + vi: "Ngày Quốc tế thiếu nhi", + isHoliday: false + }, + "08-19": { + vi: "Ngày kỷ niệm Cách mạng Tháng 8 thành công", + isHoliday: false + }, + "09-02": { + vi: "Ngày Quốc Khánh", + isHoliday: true + }, + "10-01": { + vi: "Ngày quốc tế người cao tuổi", + isHoliday: false + }, + "10-20": { + vi: "Ngày Phụ nữ Việt Nam", + isHoliday: false + }, + "10-31": { + vi: "Halloween", + isHoliday: false + }, + "11-19": { + vi: "Ngày Quốc tế Nam giới", + isHoliday: false + }, + "11-20": { + vi: "Ngày Nhà giáo Việt Nam", + isHoliday: false + }, + "12-24": { + vi: "Đêm Giáng sinh", + isHoliday: false + }, + "12-25": { + vi: "Giáng sinh", + isHoliday: false + } +} +const specialDaysLunar = { + "1-1": { + vi: "Tết Nguyên Đán", + zh: "春节", + isHoliday: true + }, + "1-15": { + vi: "Tết Nguyên Tiêu", + zh: "元宵节", + isHoliday: false + }, + "3-3": { + vi: "Tết Hàn thực", + isHoliday: false + }, + "3-10": { + vi: "Giỗ tổ Hùng Vương", + isHoliday: true + }, + "5-5": { + vi: "Tết Đoan ngọ", + isHoliday: false + }, + "7-7": { + vi: "Lễ Thất tịch", + zh: "七夕", + isHoliday: false + }, + "7-15": { + vi: "Lễ Vu Lan", + isHoliday: false + }, + "8-15": { + vi: "Tết Trung Thu", + zh: "中秋节", + isHoliday: false + }, + "12-23": { + vi: "Tết ông Công ông Táo", + isHoliday: false + } +} + +function getSpecialDays(date, lunarDate) { + const dateStr = date.toISOString().substring(5, 10) + const lunarStr = lunarDate.month + "-" + lunarDate.day + return [specialDaysSolar[dateStr], specialDaysLunar[lunarStr]] +} + +function updateOutputs() { + if (dateInput.valueAsDate === null) { + dateInput.valueAsDate = new Date() + } + const date = dateInput.valueAsDate + solarYearOutput.innerText = date.getFullYear() + solarMonthOutputEN.innerText = monthFormatEN.format(date) + solarMonthOutputVI.innerText = monthFormatVI.format(date) + solarDayOutput.innerText = date.getDate() + solarWeekdayOutputEN.innerText = weekdayFormatEN.format(date) + solarWeekdayOutputVI.innerText = weekdayFormatVI.format(date) + solarWeekdayOutputZH.innerText = weekdayFormatZH.format(date) + + const lunarDate = convertSolarToLunar(date, tzOffset) + const zodiac = getZodiac(lunarDate) + lunarYearOutputVI.innerText = "Năm " + getZodiacText(zodiac.yearZodiac, "vi") + lunarYearOutputZH.innerText = getZodiacText(zodiac.yearZodiac, "zh") + "年" + lunarMonthOutputVI.innerText = "Tháng " + LUNAR_MONTH_VI[lunarDate.month - 1] + lunarMonthOutputZH.innerText = LUNAR_MONTH_ZH[lunarDate.month - 1] + "月" + if (lunarDate.isLeap) { + lunarMonthOutputVI.innerText += " nhuận" + } + if (isFullMonth(structuredClone(date), lunarDate)) { + lunarMonthOutputVI.innerText += " (đủ)" + lunarMonthOutputZH.innerText += "大" + } else { + lunarMonthOutputVI.innerText += " (thiếu)" + lunarMonthOutputZH.innerText += "小" + } + lunarDayOutput.innerText = lunarDate.day + lunarMonthZodiacOutputVI.innerText = "Tháng " + getZodiacText(zodiac.monthZodiac, "vi") + lunarMonthZodiacOutputZH.innerText = getZodiacText(zodiac.monthZodiac, "zh") + "月" + lunarDayZodiacOutputVI.innerText = "Ngày " + getZodiacText(zodiac.dayZodiac, "vi") + lunarDayOutputZH.innerText = getChineseDay(lunarDate.day) + lunarDayZodiacOutputZH.innerText = getZodiacText(zodiac.dayZodiac, "zh") + "曰" + + const solarTerm = getSolarTerm(lunarDate.jd, tzOffset) + solarTermVI.innerText = "Tiết " + TERM_VI[solarTerm] + solarTermZH.innerText = TERM_ZH[solarTerm] + + const specialDays = getSpecialDays(date, lunarDate) + specialDayOutputVI.innerText = "" + specialDayOutputZH.innerText = "" + let isHoliday = false + for (let sday of specialDays) { + if (sday === undefined) { + continue + } + specialDayOutputVI.innerText += "\n" + sday.vi + if (sday.zh) { + specialDayOutputZH.innerText += " · " + sday.zh + } + if (sday.isHoliday) { + isHoliday = true + } + } + if (isHoliday || date.getDay() === 0) { + calendar.className = "holiday" + } else { + calendar.className = "" + } + +} + +dayBeforeBtn.onclick = () => { + let date = new Date(dateInput.valueAsDate.setDate(dateInput.valueAsDate.getDate() - 1)) + dateInput.valueAsDate = date + updateOutputs() +} +dayAfterBtn.onclick = () => { + let date = new Date(dateInput.valueAsDate.setDate(dateInput.valueAsDate.getDate() + 1)) + dateInput.valueAsDate = date + updateOutputs() +} +document.querySelector("#nav-today").onclick = () => { + console.log("a") + dateInput.valueAsDate = new Date() + updateOutputs() +} +document.addEventListener("DOMContentLoaded", updateOutputs) + +dateInput.onchange = updateOutputs |