修复 solarToLunar 闰月期间非初一日期算错的 bug
部署到群晖 / deploy (push) Successful in 45s

原算法在月循环外的 if (offset < 0) 分支根据 isLeap 重新判断加哪个月份天数,
但闰月期间的非初一日期会因为变量切换被错算到下一个普通月。

用 jjonline/calendar.js 的权威实现替换:循环内统一 offset -= temp,
退出循环后用保留的 temp 加回,简洁且正确。

修复验证:
- 2023-03-22 → 闰二月初一 ✓(之前也对)
- 2023-03-23 → 闰二月初二 ✓(之前错为「三月初二」)
- 2023-04-19 → 闰二月廿九 ✓(之前错为「三月廿九」)
- 2025-08-22 → 闰六月廿九 ✓(之前错为「七月廿九」)

维护手册新增踩坑 #13。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-06-02 06:10:12 +08:00
parent ddcfe3334e
commit 320209a390
3 changed files with 87 additions and 72 deletions
+1
View File
@@ -232,6 +232,7 @@ git push
10. **Gitea Actions 的 Secrets 改完不会自动触发流水线**——必须手动 re-run,或者推一个新 commit。
11. **农历 `lunarInfo` 数据表来源要可信**。早期版本数据有错位(多个年份的闰月信息错),导致 solarToLunar 算的春节日期跟实际差 1-2 个月。已替换为权威源 `github.com/jjonline/calendar.js` 的版本。如未来要扩展年份范围(>2100),务必同样从该源取。
12. **农历存字段必须含 `isLeapMonth`**。只存 `lunarMonth`/`lunarDay` 会丢闰月信息,闰月生日的人每年被错误提醒到普通月份。修复需同时改:数据库列、后端 FIELDS、前端 formData、`lunar.lunarToSolar` 第 4 参数透传。
13. **`solarToLunar` 月循环必须保留 `temp` 变量**。早期版本在闰月分支里 `offset -= leapDays`、非闰分支 `offset -= monthDays`,循环外的 `if (offset < 0)` 又根据 isLeap 重新判断要加哪个——闰月期间的"非初一日期"会被错算到下一个普通月(例如「闰二月初二」算成「三月初二」)。权威实现(jjonline/calendar.js)让循环内统一 `offset -= temp` 并把 `temp` 保留到循环外用,是更稳的写法。修改算法时一定要用 *闰月连续日期*(如 2023-03-22 到 2023-04-19)做端到端验证,不能只测闰月初一。
## 与其他记忆的关系
+43 -36
View File
@@ -74,62 +74,69 @@ function _monthDays(year, month) {
/**
* 公历转农历
* 算法移植自 github.com/jjonline/calendar.js(权威),与本地原版的差异在月循环退出时
* 必须保留 temp 保存最后一次月份天数,否则闰月非初一的日期会被错误地归到下一个普通月份。
* @param {Date} solarDate
* @returns {{ year, month, day, isLeap, lunarText }}
*/
function solarToLunar(solarDate) {
const date = new Date(solarDate.getFullYear(), solarDate.getMonth(), solarDate.getDate())
let offset = Math.round((date - BASE_DATE) / 86400000)
const y = solarDate.getFullYear()
const m = solarDate.getMonth() + 1
const d = solarDate.getDate()
let lunarYear, lunarMonth, lunarDay
let offset = (Date.UTC(y, m - 1, d) - Date.UTC(1900, 0, 31)) / 86400000
let i, leap = 0, temp = 0
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = _lunarYearDays(i)
offset -= temp
}
if (offset < 0) {
offset += temp
i--
}
const year = i
leap = _leapMonth(i)
let isLeap = false
for (lunarYear = 1900; lunarYear < 2100 && offset > 0; lunarYear++) {
const days = _lunarYearDays(lunarYear)
offset -= days
}
if (offset < 0) {
offset += _lunarYearDays(--lunarYear)
}
const leapM = _leapMonth(lunarYear)
let isLeapYear = false
for (lunarMonth = 1; lunarMonth < 13 && offset > 0; lunarMonth++) {
if (leapM > 0 && lunarMonth === leapM + 1 && !isLeapYear) {
--lunarMonth
isLeapYear = true
const leapDays = _leapDays(lunarYear)
offset -= leapDays
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i === (leap + 1) && isLeap === false) {
--i
isLeap = true
temp = _leapDays(year)
} else {
offset -= _monthDays(lunarYear, lunarMonth)
temp = _monthDays(year, i)
}
if (isLeapYear && lunarMonth === leapM + 1) isLeapYear = false
if (isLeap === true && i === (leap + 1)) {
isLeap = false
}
offset -= temp
}
if (offset === 0 && leapM > 0 && lunarMonth === leapM + 1) {
if (isLeapYear) {
isLeapYear = false
// 闰月下标重叠导致 offset 恰好为 0 时的取反
if (offset === 0 && leap > 0 && i === leap + 1) {
if (isLeap) {
isLeap = false
} else {
isLeapYear = true
--lunarMonth
isLeap = true
--i
}
}
if (offset < 0) {
offset += isLeapYear ? _leapDays(lunarYear) : _monthDays(lunarYear, lunarMonth)
if (isLeapYear) isLeapYear = false
else --lunarMonth
offset += temp
--i
}
lunarDay = offset + 1
isLeap = isLeapYear
const month = i
const day = offset + 1
return {
year: lunarYear,
month: lunarMonth,
day: lunarDay,
year,
month,
day,
isLeap,
lunarText: `${isLeap ? '闰' : ''}${getLunarMonthName(lunarMonth)}${getLunarDayName(lunarDay)}`
lunarText: `${isLeap ? '闰' : ''}${getLunarMonthName(month)}${getLunarDayName(day)}`
}
}
+43 -36
View File
@@ -74,62 +74,69 @@ function _monthDays(year, month) {
/**
* 公历转农历
* 算法移植自 github.com/jjonline/calendar.js(权威),与本地原版的差异在月循环退出时
* 必须保留 temp 保存最后一次月份天数,否则闰月非初一的日期会被错误地归到下一个普通月份。
* @param {Date} solarDate
* @returns {{ year, month, day, isLeap, lunarText }}
*/
function solarToLunar(solarDate) {
const date = new Date(solarDate.getFullYear(), solarDate.getMonth(), solarDate.getDate())
let offset = Math.round((date - BASE_DATE) / 86400000)
const y = solarDate.getFullYear()
const m = solarDate.getMonth() + 1
const d = solarDate.getDate()
let lunarYear, lunarMonth, lunarDay
let offset = (Date.UTC(y, m - 1, d) - Date.UTC(1900, 0, 31)) / 86400000
let i, leap = 0, temp = 0
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = _lunarYearDays(i)
offset -= temp
}
if (offset < 0) {
offset += temp
i--
}
const year = i
leap = _leapMonth(i)
let isLeap = false
for (lunarYear = 1900; lunarYear < 2100 && offset > 0; lunarYear++) {
const days = _lunarYearDays(lunarYear)
offset -= days
}
if (offset < 0) {
offset += _lunarYearDays(--lunarYear)
}
const leapM = _leapMonth(lunarYear)
let isLeapYear = false
for (lunarMonth = 1; lunarMonth < 13 && offset > 0; lunarMonth++) {
if (leapM > 0 && lunarMonth === leapM + 1 && !isLeapYear) {
--lunarMonth
isLeapYear = true
const leapDays = _leapDays(lunarYear)
offset -= leapDays
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i === (leap + 1) && isLeap === false) {
--i
isLeap = true
temp = _leapDays(year)
} else {
offset -= _monthDays(lunarYear, lunarMonth)
temp = _monthDays(year, i)
}
if (isLeapYear && lunarMonth === leapM + 1) isLeapYear = false
if (isLeap === true && i === (leap + 1)) {
isLeap = false
}
offset -= temp
}
if (offset === 0 && leapM > 0 && lunarMonth === leapM + 1) {
if (isLeapYear) {
isLeapYear = false
// 闰月下标重叠导致 offset 恰好为 0 时的取反
if (offset === 0 && leap > 0 && i === leap + 1) {
if (isLeap) {
isLeap = false
} else {
isLeapYear = true
--lunarMonth
isLeap = true
--i
}
}
if (offset < 0) {
offset += isLeapYear ? _leapDays(lunarYear) : _monthDays(lunarYear, lunarMonth)
if (isLeapYear) isLeapYear = false
else --lunarMonth
offset += temp
--i
}
lunarDay = offset + 1
isLeap = isLeapYear
const month = i
const day = offset + 1
return {
year: lunarYear,
month: lunarMonth,
day: lunarDay,
year,
month,
day,
isLeap,
lunarText: `${isLeap ? '闰' : ''}${getLunarMonthName(lunarMonth)}${getLunarDayName(lunarDay)}`
lunarText: `${isLeap ? '闰' : ''}${getLunarMonthName(month)}${getLunarDayName(day)}`
}
}