about summary refs log tree commit diff
path: root/content/cal/lunar.js
diff options
context:
space:
mode:
Diffstat (limited to 'content/cal/lunar.js')
-rw-r--r--content/cal/lunar.js522
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