// 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 specialDayOutputEN = document.querySelector('#special-day-en') 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 datePermalinkA = document.querySelector('#date-permalink') 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: "元旦", en: "New Year", 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", en: "Saint Valentine's Day", isHoliday: false }, "03-08": { vi: "Ngày Quốc tế Phụ nữ", en: "International Women's Day", isHoliday: false }, "04-30": { vi: "Ngày Thống nhất", en: "Reunification Day", isHoliday: true }, "05-01": { vi: "Ngày Quốc tế Lao động", en: "International Worker's Day", zh: "劳动节", isHoliday: true }, "06-01": { vi: "Ngày Quốc tế thiếu nhi", en: "International Children's Day", 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", en: "National Day", 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", en: "Vietnamese Women's Day", isHoliday: false }, "10-31": { vi: "", en: "Halloween", isHoliday: false }, "11-19": { vi: "Ngày Quốc tế Nam giới", en: "International Men's Day", isHoliday: false }, "11-20": { vi: "Ngày Nhà giáo Việt Nam", en: "Vietnamese Teacher's Day", isHoliday: false }, "12-24": { vi: "Đêm Giáng sinh", en: "Christmas Eve", isHoliday: false }, "12-25": { vi: "Giáng sinh", en: "Christmas", isHoliday: false }, "12-31": { vi: "Tất niên Dương Lịch", en: "New Year's Eve", isHoliday: false } } const specialDaysLunar = { "1-1": { vi: "Tết Nguyên Đán", en: "Lunar New Year", zh: "春节", isHoliday: true }, "1-2": { vi: "Mồng Hai Tết Nguyên Đán", isHoliday: true }, "1-3": { vi: "Mồng Ba Tết Nguyên Đán", 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", en: "Mid-Autumn Festival", zh: "中秋节", isHoliday: false }, "12-23": { vi: "Tết ông Công ông Táo", isHoliday: false }, "12-30": { vi: "Tất niên Tết Nguyên Đán", en: "Lunar New Year's Eve", zh: "除夕", isHoliday: true } } function getSpecialDays(date, lunarDate) { const dateStr = date.toISOString().substring(5, 10) const lunarStr = lunarDate.month + "-" + lunarDate.day tomorrow = new Date(date.valueOf() + 24 * 3600 * 1000) tomorrowLunar = convertSolarToLunar(tomorrow, tzOffset) if (tomorrowLunar.month === 1 && tomorrowLunar.day === 1) { return [specialDaysSolar[dateStr], specialDaysLunar["12-30"]] } return [specialDaysSolar[dateStr], specialDaysLunar[lunarStr]] } function updateOutputs() { const url = new URL(document.location) if (dateInput.valueAsDate === null) { const dateStr = url.searchParams.get('date') if (dateStr) { dateInput.valueAsDate = new Date(dateStr) } else { dateInput.valueAsDate = new Date() } } url.searchParams.set('date', dateInput.value) datePermalinkA.innerText = url datePermalinkA.href = url 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 = "" specialDayOutputEN.innerText = "" let isHoliday = false for (let sday of specialDays) { if (sday === undefined) { continue } specialDayOutputVI.innerText += "\n" + sday.vi if (sday.en) { specialDayOutputEN.innerText += "\n" + sday.en } 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