about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHuy Ngo <huy.ngo@eaera.com>2025-02-12 12:28:13 +0700
committerHuy Ngo <huy.ngo@eaera.com>2025-02-12 12:28:13 +0700
commit1d6e4db908554662a81d5293a148a3b879403020 (patch)
treef13f2eb20f2a7b2c48da5ec43f145c9600b5eb12
parent0fc2d2a0fe83d06f0ade85f2a60bc648d4e6c784 (diff)
downloadblog-1d6e4db908554662a81d5293a148a3b879403020.tar.gz
WIP: Add calendars
-rw-r--r--config.yaml3
-rw-r--r--content/cal/_index.html37
-rw-r--r--content/cal/lunar.js208
3 files changed, 248 insertions, 0 deletions
diff --git a/config.yaml b/config.yaml
index a6c5930..2e047a4 100644
--- a/config.yaml
+++ b/config.yaml
@@ -28,6 +28,9 @@ languages:
         - name: Articles
           pageRef: /posts
           weight: 20
+        - name: Calendar
+          pageRef: /cal
+          weight: 30
         - name: RSS
           pageRef: /about/rss
           weight: 90
diff --git a/content/cal/_index.html b/content/cal/_index.html
new file mode 100644
index 0000000..1d078d6
--- /dev/null
+++ b/content/cal/_index.html
@@ -0,0 +1,37 @@
+---
+title: Calendar
+disable_feed: true
+---
+
+<div id="lunar-solar-cal">
+	<noscript>
+		This calendar works locally in your browser and requires JavaScript to remain serverless
+	</noscript>
+	<div>
+		<label>
+			Solar date:
+			<input id="solar-date" type="date" />
+		</label>
+		<label>
+			Timezone offset:
+			<select id="timezone-offset">
+				<option value=7>UTC+7 (Vietnam)</option>
+				<option value=8>UTC+8 (China)</option>
+				<option value=9>UTC+9 (Japan/Korea)</option>
+			</select>
+		</label>
+	</div>
+	<div id="solar-cal">
+		<div id="solar-year"></div>
+		<div id="solar-month"></div>
+		<div id="solar-day"></div>
+		<div id="solar-weekday"></div>
+	</div>
+	<div id="lunar-cal">
+		<div id="lunar-year"></div>
+		<div id="lunar-month"></div>
+		<div id="lunar-day"></div>
+	</div>
+</div>
+<script src="lunar.js">
+</script>
diff --git a/content/cal/lunar.js b/content/cal/lunar.js
new file mode 100644
index 0000000..ce4434f
--- /dev/null
+++ b/content/cal/lunar.js
@@ -0,0 +1,208 @@
+// 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
+	}
+}
+
+// Part 2: Update DOM
+
+// DOM
+const dateInput = document.querySelector('#solar-date')
+const tzOffsetInput = document.querySelector('#timezone-offset')
+const solarYearOutput = document.querySelector('#solar-year')
+const solarMonthOutput = document.querySelector('#solar-month')
+const solarDayOutput = document.querySelector('#solar-day')
+const solarWeekdayOutput = document.querySelector('#solar-weekday')
+const lunarYearOutput = document.querySelector('#lunar-year')
+const lunarMonthOutput = document.querySelector('#lunar-month')
+const lunarDayOutput = document.querySelector('#lunar-day')
+
+const monthFormat = new Intl.DateTimeFormat("en-US", {month: "long"})
+const weekdayFormat = new Intl.DateTimeFormat("en-US", {weekday: "long"})
+
+function updateOutputs() {
+	if (dateInput.valueAsDate === null) {
+		dateInput.valueAsDate = new Date()
+	}
+	const date = dateInput.valueAsDate
+	const tzOffset = parseInt(tzOffsetInput.value)
+	solarYearOutput.innerText = date.getFullYear()
+	solarMonthOutput.innerText = monthFormat.format(date)
+	solarDayOutput.innerText = date.getDate()
+	solarWeekdayOutput.innerText = weekdayFormat.format(date)
+
+	const lunarDate = convertSolarToLunar(date, tzOffset)
+	lunarYearOutput.innerText = lunarDate.year
+	lunarMonthOutput.innerText = lunarDate.month
+	if (lunarDate.isLeap) {
+		lunarMonthOutput.innerText += " (leap)"
+	}
+	lunarDayOutput.innerText = lunarDate.day
+}
+
+document.addEventListener("DOMContentLoaded", updateOutputs)
+
+dateInput.onchange = updateOutputs
+tzOffsetInput.onchange = updateOutputs