|
| 1 | +// This script generates lookup tables for the calendar library. |
| 2 | +// Generated files: |
| 3 | +// lib/曆表.wy |
| 4 | +// The LUTs used by the standard calendar library. |
| 5 | +// tools/calendar.html |
| 6 | +// A table showing the first day of each month for checking. |
| 7 | + |
| 8 | +try { |
| 9 | + process.chdir("./tools"); |
| 10 | +} catch (e) { } //make sure we're in tools directory |
| 11 | + |
| 12 | +// totalDay of 1st of month #n = ceil(n * 25101 / 850 - 1) + monthFix(n) |
| 13 | +// monthFix is tabulated below |
| 14 | + |
| 15 | +// start index of the month start correction table |
| 16 | +// update this value if the table is extended further into the past |
| 17 | +const MONTH_FIX_START = 0; |
| 18 | + |
| 19 | +// month start correction table |
| 20 | +// lists correction to the totalDay of 1st of each month |
| 21 | +const MONTH_FIX_TABLE = []; |
| 22 | + |
| 23 | + |
| 24 | +// totalMonth of leap month #n = ceil(n * 3157 / 94 - 27) + leapFix(n) |
| 25 | +// leapFix is tabulated below |
| 26 | + |
| 27 | +// start index of the leap month correction table |
| 28 | +// update this value if the table is extended further into the past |
| 29 | +const LEAP_FIX_START = 0; |
| 30 | + |
| 31 | +// leap month correction table |
| 32 | +// lists correction to the totalMonth of each leap month |
| 33 | +const LEAP_FIX_TABLE = []; |
| 34 | + |
| 35 | + |
| 36 | +const WENYAN_HEAD = `注曰「「此文程式之作文也。勿施以修訂。」」 |
| 37 | +注曰「「程式書於文 tools/make_calendar.js 」」 |
| 38 | +
|
| 39 | +`; |
| 40 | + |
| 41 | +const getNumWord = function (n) { |
| 42 | + const NUM_CHARS = "〇一二三四五六七八九十"; |
| 43 | + if (n < 0) { |
| 44 | + return "負" + getNumWord(-n); |
| 45 | + } |
| 46 | + let str = ""; |
| 47 | + do { |
| 48 | + str = NUM_CHARS.charAt(n % 10) + str; |
| 49 | + n = Math.trunc(n / 10); |
| 50 | + } while (n > 0); |
| 51 | + return str; |
| 52 | +}; |
| 53 | + |
| 54 | +const generateWenyan = function () { |
| 55 | + const ELEMS_PER_LINE = 20; |
| 56 | + let wy = WENYAN_HEAD; |
| 57 | + wy += `今有一數。曰${getNumWord(MONTH_FIX_START)}。名之曰「始曆月」。\n`; |
| 58 | + wy += "今有一列。名之曰「曆月表」。\n"; |
| 59 | + wy += `今有一數。曰${getNumWord(LEAP_FIX_START)}。名之曰「始閏月」。\n`; |
| 60 | + wy += "今有一列。名之曰「閏月表」。\n"; |
| 61 | + wy += "\n充「曆月表」。\n"; |
| 62 | + for (let i = 0; i < MONTH_FIX_TABLE.length; ++i) { |
| 63 | + if (i === 0 || (i + MONTH_FIX_START) % ELEMS_PER_LINE === 0) { |
| 64 | + wy += "\t"; |
| 65 | + } |
| 66 | + wy += `以${getNumWord(MONTH_FIX_TABLE[i])}`; |
| 67 | + if (i + 1 === MONTH_FIX_TABLE.length || (i + 1 + MONTH_FIX_START) % ELEMS_PER_LINE === 0) { |
| 68 | + wy += "。\n"; |
| 69 | + } |
| 70 | + } |
| 71 | + wy += "\n充「閏月表」。\n"; |
| 72 | + for (let i = 0; i < LEAP_FIX_TABLE.length; ++i) { |
| 73 | + if (i === 0 || (i + LEAP_FIX_START) % ELEMS_PER_LINE === 0) { |
| 74 | + wy += "\t"; |
| 75 | + } |
| 76 | + wy += `以${getNumWord(LEAP_FIX_TABLE[i])}`; |
| 77 | + if (i + 1 === LEAP_FIX_TABLE.length || (i + 1 + LEAP_FIX_START) % ELEMS_PER_LINE === 0) { |
| 78 | + wy += "。\n"; |
| 79 | + } |
| 80 | + } |
| 81 | + return wy; |
| 82 | +}; |
| 83 | + |
| 84 | + |
| 85 | +const HTML_HEAD = `<!-- DOCTYPE html --> |
| 86 | +<!-- Generated by make_calendar.js --> |
| 87 | +<html> |
| 88 | +<head> |
| 89 | + <meta charset="UTF-8"> |
| 90 | + <title>Calendar</title> |
| 91 | + <style> |
| 92 | + table, th, td { |
| 93 | + border: solid 1px black; |
| 94 | + border-collapse: collapse; |
| 95 | + } |
| 96 | + th, td { |
| 97 | + padding: 0.25em; |
| 98 | + text-align: center; |
| 99 | + } |
| 100 | + th { |
| 101 | + background-color: #ccc; |
| 102 | + } |
| 103 | + .even { |
| 104 | + background-color: #eee; |
| 105 | + } |
| 106 | + .odd { |
| 107 | + background-color: #fff; |
| 108 | + } |
| 109 | + .leap { |
| 110 | + background-color: #bdf; |
| 111 | + } |
| 112 | + </style> |
| 113 | +</head> |
| 114 | +<body> |
| 115 | + <table> |
| 116 | +`; |
| 117 | + |
| 118 | +const TABLE_HEAD = ` <tr> |
| 119 | + <th>年</th> |
| 120 | + <th>正月</th> |
| 121 | + <th>二月</th> |
| 122 | + <th>三月</th> |
| 123 | + <th>四月</th> |
| 124 | + <th>五月</th> |
| 125 | + <th>六月</th> |
| 126 | + <th>七月</th> |
| 127 | + <th>八月</th> |
| 128 | + <th>九月</th> |
| 129 | + <th>十月</th> |
| 130 | + <th>十一月</th> |
| 131 | + <th>十二月</th> |
| 132 | + </tr> |
| 133 | +`; |
| 134 | + |
| 135 | +const HTML_TAIL = ` </table> |
| 136 | +</body> |
| 137 | +</html> |
| 138 | +`; |
| 139 | + |
| 140 | +const CE_YEAR_OFFSET = 2697; |
| 141 | + |
| 142 | +const getSexagenaryWord = function (n) { |
| 143 | + return "甲乙丙丁戊己庚辛壬癸".charAt((n - 1) % 10) + "子丑寅卯辰巳午未申酉戌亥".charAt((n - 1) % 12); |
| 144 | +}; |
| 145 | + |
| 146 | +const getMonthBeginDay = function (totalMonth) { |
| 147 | + const index = totalMonth - MONTH_FIX_START; |
| 148 | + if (index < 0) { |
| 149 | + return -Infinity; |
| 150 | + } else if (index >= MONTH_FIX_TABLE.length) { |
| 151 | + return Infinity; |
| 152 | + } else { |
| 153 | + return Math.ceil(totalMonth * 25101 / 850 - 1) + MONTH_FIX_TABLE[index]; |
| 154 | + } |
| 155 | +}; |
| 156 | + |
| 157 | +const getLeapMonth = function (totalLeap) { |
| 158 | + const index = totalLeap - LEAP_FIX_START; |
| 159 | + if (index < 0) { |
| 160 | + return -Infinity; |
| 161 | + } else if (index >= LEAP_FIX_TABLE.length) { |
| 162 | + return Infinity; |
| 163 | + } else { |
| 164 | + return Math.ceil(totalLeap * 3157 / 94 - 27) + LEAP_FIX_TABLE[index]; |
| 165 | + } |
| 166 | +}; |
| 167 | + |
| 168 | +const getYearMonth = function (totalMonth) { |
| 169 | + let totalLeap = Math.round((totalMonth + 27) * 94 / 3157); |
| 170 | + const leapMonth = getLeapMonth(totalLeap); |
| 171 | + if (totalMonth < leapMonth) { |
| 172 | + --totalLeap; |
| 173 | + } |
| 174 | + const year = Math.floor((totalMonth - totalLeap) / 12); |
| 175 | + const month = (totalMonth - totalLeap) % 12 + 1; |
| 176 | + const isLeapMonth = totalMonth === leapMonth; |
| 177 | + return { year: year, month: month, isLeapMonth: isLeapMonth }; |
| 178 | +}; |
| 179 | + |
| 180 | +const getGregorianDay = function (totalDay) { |
| 181 | + const d = new Date((totalDay - 1704558) * 8.64e+7); |
| 182 | + return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() }; |
| 183 | +}; |
| 184 | + |
| 185 | +const printYear = function (totalYear) { |
| 186 | + return `${totalYear - CE_YEAR_OFFSET}<br>${getSexagenaryWord(totalYear)}`; |
| 187 | +}; |
| 188 | + |
| 189 | +const printMonth = function (totalMonth) { |
| 190 | + const fillZero = function (n, digits) { |
| 191 | + const s = n.toString(); |
| 192 | + if (s.length >= digits) { |
| 193 | + return s; |
| 194 | + } else { |
| 195 | + return new Array(digits - s.length).fill("0") + s; |
| 196 | + } |
| 197 | + }; |
| 198 | + const d = getMonthBeginDay(totalMonth); |
| 199 | + const g = getGregorianDay(d); |
| 200 | + return `${fillZero(g.month, 2)}-${fillZero(g.day, 2)} ${getSexagenaryWord(d)}`; |
| 201 | +}; |
| 202 | + |
| 203 | +const getRowClass = function (totalYear) { |
| 204 | + return totalYear % 2 === 0 ? "odd" : "even"; |
| 205 | +} |
| 206 | + |
| 207 | +const generateCalendar = function () { |
| 208 | + let prevMonth = getYearMonth(MONTH_FIX_START - 1); |
| 209 | + let currMonth = getYearMonth(MONTH_FIX_START); |
| 210 | + let prevLeapTotalMonth = null; |
| 211 | + let html = HTML_HEAD; |
| 212 | + html += TABLE_HEAD; |
| 213 | + html += ` <tr class="${getRowClass(currMonth.year)}">\n <th rowspan="2">`; |
| 214 | + html += printYear(currMonth.year); |
| 215 | + html += '</th>\n'; |
| 216 | + for (let i = 1; i < currMonth.month; ++i) { |
| 217 | + html += ' <td rowspan="2"></td>\n'; |
| 218 | + } |
| 219 | + if (currMonth.isLeapMonth) { |
| 220 | + html += ' <td rowspan="1"></td>\n'; |
| 221 | + } |
| 222 | + for (let i = 0; i < MONTH_FIX_TABLE.length; ++i) { |
| 223 | + const totalMonth = MONTH_FIX_START + i; |
| 224 | + const nextMonth = getYearMonth(totalMonth + 1); |
| 225 | + if (i !== 0 && currMonth.year !== prevMonth.year) { |
| 226 | + if (currMonth.year % 10 === 7) { |
| 227 | + html += TABLE_HEAD; |
| 228 | + } |
| 229 | + html += ` <tr class="${getRowClass(currMonth.year)}">\n <th rowspan="2">`; |
| 230 | + html += printYear(currMonth.year); |
| 231 | + html += '</th>\n'; |
| 232 | + } |
| 233 | + if (currMonth.isLeapMonth) { |
| 234 | + prevLeapTotalMonth = totalMonth; |
| 235 | + } else { |
| 236 | + if (nextMonth.isLeapMonth) { |
| 237 | + html += ' <td rowspan="1">'; |
| 238 | + } else { |
| 239 | + html += ' <td rowspan="2">'; |
| 240 | + } |
| 241 | + html += printMonth(totalMonth); |
| 242 | + html += '</td>\n'; |
| 243 | + } |
| 244 | + if (nextMonth.year !== currMonth.year) { |
| 245 | + if (prevLeapTotalMonth !== null) { |
| 246 | + html += ' </tr>\n <tr>\n <td rowspan="1" class="leap">'; |
| 247 | + html += printMonth(prevLeapTotalMonth); |
| 248 | + html += '</td>\n </tr>\n'; |
| 249 | + prevLeapTotalMonth = null; |
| 250 | + } else { |
| 251 | + html += ' </tr>\n <tr></tr>\n'; |
| 252 | + } |
| 253 | + } |
| 254 | + prevMonth = currMonth; |
| 255 | + currMonth = nextMonth; |
| 256 | + } |
| 257 | + if (currMonth.year === prevMonth.year) { |
| 258 | + for (let i = prevMonth.month + 1; i <= 12; ++i) { |
| 259 | + html += ' <td rowspan="2"></td>\n'; |
| 260 | + } |
| 261 | + if (prevLeapTotalMonth !== null) { |
| 262 | + html += ' </tr>\n <tr>\n <td rowspan="1" class="leap">'; |
| 263 | + html += printMonth(prevLeapTotalMonth); |
| 264 | + html += '</td>\n </tr>\n'; |
| 265 | + } else { |
| 266 | + html += ' </tr>\n <tr></tr>\n'; |
| 267 | + } |
| 268 | + html += ' </tr>\n'; |
| 269 | + } |
| 270 | + html += HTML_TAIL; |
| 271 | + return html; |
| 272 | +}; |
| 273 | + |
| 274 | +const fs = require("fs"); |
| 275 | +fs.writeFileSync("../lib/曆表.wy", generateWenyan()); |
| 276 | +fs.writeFileSync("./calendar.html", generateCalendar()); |
0 commit comments