// 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