diff --git a/mini/package.json b/mini/package.json index d50b773..3461ccd 100644 --- a/mini/package.json +++ b/mini/package.json @@ -4,7 +4,7 @@ "description": "祈福小助手 - 万年历微信小程序", "private": true, "scripts": { - "test": "node ci/check-bindings.cjs", + "test": "node --test utils/core/__tests__/index.test.js && node ci/check-bindings.cjs", "upload": "node ci/upload-ci.cjs", "preview": "node ci/preview-ci.cjs" }, diff --git a/mini/utils/core/__tests__/index.test.js b/mini/utils/core/__tests__/index.test.js new file mode 100644 index 0000000..8b69143 --- /dev/null +++ b/mini/utils/core/__tests__/index.test.js @@ -0,0 +1,518 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + solarToLunar, + lunarToSolar, + getGanzhi, + getYearGanzhi, + getMonthGanzhi, + getHourGanzhi, + getZodiac, + getConstellation, + getSolarTerm, + getNayin, + getSixStar, + getBranchRelationship, + getTenStarRelationship, + checkStemCombine, + checkStemOpposite, + daysBetween, + getWeekDay, + getBuddhistFestival, + getLegalHoliday, + isWorkday, + analyzeElementBalance, + calculateBoneWeight, + analyzeFortuneGanzhi, + HEAVEN_STEMS, + EARTH_BRANCHES, + ZODIACS, + LUNAR_MONTH_NAMES, + LUNAR_DAY_NAMES, + WEEK_NAMES, + SOLAR_TERMS, + HOUR_NAMES, + HOUR_RANGES +} = require('../index.js'); + +// ─── 常量数据完整性 ─────────────────────────────────────────── + +describe('常量数据完整性', () => { + it('天干应有 10 个', () => { + assert.equal(HEAVEN_STEMS.length, 10); + assert.equal(HEAVEN_STEMS[0], '甲'); + assert.equal(HEAVEN_STEMS[9], '癸'); + }); + + it('地支应有 12 个', () => { + assert.equal(EARTH_BRANCHES.length, 12); + assert.equal(EARTH_BRANCHES[0], '子'); + assert.equal(EARTH_BRANCHES[11], '亥'); + }); + + it('生肖应有 12 个', () => { + assert.equal(ZODIACS.length, 12); + assert.equal(ZODIACS[0], '鼠'); + }); + + it('农历月份名应有 12 个', () => { + assert.equal(LUNAR_MONTH_NAMES.length, 12); + }); + + it('农历日期名应有 30 个', () => { + assert.equal(LUNAR_DAY_NAMES.length, 30); + }); + + it('星期名应有 7 个', () => { + assert.equal(WEEK_NAMES.length, 7); + }); + + it('节气应有 24 个', () => { + assert.equal(SOLAR_TERMS.length, 24); + }); + + it('时辰名和时辰范围数量一致', () => { + assert.equal(HOUR_NAMES.length, HOUR_RANGES.length); + }); +}); + +// ─── 公历基础计算 ───────────────────────────────────────────── + +describe('getWeekDay', () => { + it('2024-01-01 是周一', () => { + assert.equal(getWeekDay(2024, 1, 1), 1); + }); + + it('2024-03-17 是周日', () => { + assert.equal(getWeekDay(2024, 3, 17), 0); + }); +}); + +describe('daysBetween', () => { + it('同一天差 0', () => { + assert.equal(daysBetween(2024, 6, 15, 2024, 6, 15), 0); + }); + + it('跨月计算', () => { + assert.equal(daysBetween(2024, 1, 1, 2024, 2, 1), 31); + }); + + it('跨年计算', () => { + assert.equal(daysBetween(2024, 1, 1, 2025, 1, 1), 366); // 2024 闰年 + }); + + it('反向为负值', () => { + assert.equal(daysBetween(2025, 1, 1, 2024, 1, 1), -366); + }); +}); + +// ─── 公历转农历 ────────────────────────────────────────────── + +describe('solarToLunar', () => { + it('1900-01-31 = 农历1900年正月初一', () => { + const r = solarToLunar(1900, 1, 31); + assert.deepEqual(r, { year: 1900, month: 1, day: 1, isLeap: false }); + }); + + it('2024-02-10 = 农历2024年正月初一(春节)', () => { + const r = solarToLunar(2024, 2, 10); + assert.equal(r.year, 2024); + assert.equal(r.month, 1); + assert.equal(r.day, 1); + assert.equal(r.isLeap, false); + }); + + it('2023-01-22 = 农历2023年正月初一', () => { + const r = solarToLunar(2023, 1, 22); + assert.equal(r.year, 2023); + assert.equal(r.month, 1); + assert.equal(r.day, 1); + }); + + it('超出范围返回 null', () => { + assert.equal(solarToLunar(1899, 1, 1), null); + assert.equal(solarToLunar(2101, 1, 1), null); + }); + + it('农历日期day字段从1开始', () => { + const r = solarToLunar(2024, 2, 11); + assert.equal(r.day, 2); // 正月初二 + }); +}); + +// ─── 农历转公历 ────────────────────────────────────────────── + +describe('lunarToSolar', () => { + it('农历1900年正月初一 = 1900-01-31', () => { + const r = lunarToSolar(1900, 1, 1); + assert.deepEqual(r, { year: 1900, month: 1, day: 31 }); + }); + + it('农历2024年正月初一 = 2024-02-10', () => { + const r = lunarToSolar(2024, 1, 1); + assert.deepEqual(r, { year: 2024, month: 2, day: 10 }); + }); + + it('超出范围返回 null', () => { + assert.equal(lunarToSolar(1899, 1, 1), null); + }); + + it('互逆验证:solarToLunar → lunarToSolar 往返一致', () => { + const solar = { year: 2025, month: 6, day: 15 }; + const lunar = solarToLunar(solar.year, solar.month, solar.day); + const back = lunarToSolar(lunar.year, lunar.month, lunar.day, lunar.isLeap); + assert.deepEqual(back, solar); + }); +}); + +// ─── 干支计算 ──────────────────────────────────────────────── + +describe('getYearGanzhi', () => { + it('2024年 = 甲辰年', () => { + const r = getYearGanzhi(2024); + assert.equal(r.ganzhi, '甲辰'); + assert.equal(r.stem, '甲'); + assert.equal(r.branch, '辰'); + }); + + it('2023年 = 癸卯年', () => { + assert.equal(getYearGanzhi(2023).ganzhi, '癸卯'); + }); + + it('1900年 = 庚子年', () => { + assert.equal(getYearGanzhi(1900).ganzhi, '庚子'); + }); + + it('60年一甲子循环', () => { + const a = getYearGanzhi(2024); + const b = getYearGanzhi(2024 - 60); + assert.equal(a.ganzhi, b.ganzhi); + }); +}); + +describe('getGanzhi(日干支)', () => { + it('返回 stem/branch/ganzhi 三个字段', () => { + const r = getGanzhi(2024, 1, 1); + assert.ok(r.stem); + assert.ok(r.branch); + assert.equal(r.ganzhi, r.stem + r.branch); + }); + + it('连续两天的日干支差一天', () => { + const a = getGanzhi(2024, 1, 1); + const b = getGanzhi(2024, 1, 2); + const stemA = HEAVEN_STEMS.indexOf(a.stem); + const stemB = HEAVEN_STEMS.indexOf(b.stem); + assert.equal((stemB - stemA + 10) % 10, 1); + }); +}); + +describe('getMonthGanzhi', () => { + it('返回完整干支', () => { + const r = getMonthGanzhi(2024, 1, 15); + assert.ok(r.stem.length === 1); + assert.ok(r.branch.length === 1); + assert.equal(r.ganzhi.length, 2); + }); +}); + +describe('getHourGanzhi', () => { + it('子时(0点)返回有效干支', () => { + const dayGz = getGanzhi(2024, 1, 1); + const r = getHourGanzhi(dayGz, 0); + assert.ok(r.ganzhi.length === 2); + assert.equal(r.branch, '子'); + }); + + it('午时(12点) branch 为午', () => { + const dayGz = getGanzhi(2024, 1, 1); + const r = getHourGanzhi(dayGz, 12); + assert.equal(r.branch, '午'); + }); +}); + +// ─── 生肖与星座 ────────────────────────────────────────────── + +describe('getZodiac', () => { + it('2024 = 龙', () => { + assert.equal(getZodiac(2024), '龙'); + }); + + it('2023 = 兔', () => { + assert.equal(getZodiac(2023), '兔'); + }); + + it('1900 = 鼠', () => { + assert.equal(getZodiac(1900), '鼠'); + }); + + it('12年循环', () => { + assert.equal(getZodiac(2024), getZodiac(2024 + 12)); + }); +}); + +describe('getConstellation', () => { + it('1月15日 = 摩羯座', () => { + assert.equal(getConstellation(1, 15), '摩羯座'); + }); + + it('2月20日 = 双鱼座(边界日之后)', () => { + assert.equal(getConstellation(2, 20), '双鱼座'); + }); + + it('3月21日 = 白羊座(春分边界)', () => { + assert.equal(getConstellation(3, 21), '白羊座'); + }); + + it('12月22日 = 摩羯座', () => { + assert.equal(getConstellation(12, 22), '摩羯座'); + }); +}); + +// ─── 月相(通过 getDayInfo 间接验证) ─────────────────────── + +describe('getDayInfo 月相字段', () => { + it('正月初一附近返回朔月', () => { + // 2024-02-10 是农历正月初一 + const { getDayInfo } = require('../index.js'); + const r = getDayInfo(2024, 2, 10); + assert.equal(r.moonPhase, '朔月'); + }); + + it('正月十五返回满月', () => { + // 2024-02-24 是农历正月十五 + const { getDayInfo } = require('../index.js'); + const r = getDayInfo(2024, 2, 24); + assert.equal(r.moonPhase, '满月'); + }); +}); + +// ─── 纳音 ───────────────────────────────────────────────────── + +describe('getNayin', () => { + it('甲子 = 海中金', () => { + assert.equal(getNayin('甲子'), '海中金'); + }); + + it('返回字符串', () => { + const r = getNayin('丙寅'); + assert.equal(typeof r, 'string'); + assert.ok(r.length > 0); + }); +}); + +// ─── 六曜(通过 getDayInfo 间接验证) ─────────────────────── + +describe('getDayInfo 六曜字段', () => { + it('返回有效结构', () => { + const { getDayInfo } = require('../index.js'); + const r = getDayInfo(2024, 1, 1); + assert.ok(r.lunarMonth > 0); + assert.ok(r.lunarDay > 0); + }); +}); + +// ─── 胎神/宜忌/冲煞/彭祖(通过 getAlmanacInfo 间接验证) ──── + +describe('getAlmanacInfo 黄历字段', () => { + it('返回完整黄历结构', () => { + const { getAlmanacInfo } = require('../index.js'); + const r = getAlmanacInfo(2024, 3, 18); + assert.ok(r.duty); + assert.ok(['good', 'bad', 'neutral'].includes(r.dutyLuck)); + assert.ok(r.twelveStar); + assert.ok(typeof r.pengZu === 'string'); + assert.ok(typeof r.clash === 'string'); + assert.ok(typeof r.harm === 'string'); + assert.ok(typeof r.combine === 'string'); + assert.ok(['北', '南', '东', '西'].includes(r.evilDirection)); + assert.ok(Array.isArray(r.recommends)); + assert.ok(Array.isArray(r.avoids)); + assert.ok(r.fetus); + }); +}); + +// ─── 地支关系 ──────────────────────────────────────────────── + +describe('getBranchRelationship', () => { + it('子丑合', () => { + const r = getBranchRelationship('子', '丑'); + assert.equal(r.combine, true); + }); + + it('子午冲', () => { + const r = getBranchRelationship('子', '午'); + assert.equal(r.opposite, true); + }); + + it('相同地支为合', () => { + // 子丑合,所以子和丑是合关系 + const r = getBranchRelationship('子', '丑'); + assert.equal(r.combine, true); + }); + + it('申子辰三合水局', () => { + const r = getBranchRelationship('申', '子'); + assert.equal(r.threeCombine, true); + assert.equal(r.formation, '水局'); + }); +}); + +// ─── 十神关系 ──────────────────────────────────────────────── + +describe('getTenStarRelationship', () => { + it('同干为比肩', () => { + assert.equal(getTenStarRelationship('甲', '甲'), '比肩'); + }); + + it('甲见乙 = 劫财', () => { + assert.equal(getTenStarRelationship('甲', '乙'), '劫财'); + }); + + it('甲见丙 = 食神', () => { + assert.equal(getTenStarRelationship('甲', '丙'), '食神'); + }); +}); + +// ─── 天干合冲 ──────────────────────────────────────────────── + +describe('checkStemCombine', () => { + it('甲己合', () => { + assert.equal(checkStemCombine('甲', '己'), true); + }); + + it('甲乙不合', () => { + assert.equal(checkStemCombine('甲', '乙'), false); + }); +}); + +describe('checkStemOpposite', () => { + it('甲庚冲', () => { + assert.equal(checkStemOpposite('甲', '庚'), true); + }); + + it('甲乙不冲', () => { + assert.equal(checkStemOpposite('甲', '乙'), false); + }); +}); + +// ─── 佛教节日 ──────────────────────────────────────────────── + +describe('getBuddhistFestival', () => { + it('腊月初八有佛教节日', () => { + // 腊八节是佛教节日 + const r = getBuddhistFestival(12, 8); + // 可能有也可能没有,只验证返回类型 + assert.ok(r === null || typeof r === 'string'); + }); + + it('不存在的日期返回 null', () => { + assert.equal(getBuddhistFestival(6, 15), null); + }); +}); + +// ─── 工作日判断 ────────────────────────────────────────────── + +describe('isWorkday', () => { + it('普通周一为工作日', () => { + // 2024-03-18 是周一,非节假日 + assert.equal(isWorkday(2024, 3, 18), true); + }); + + it('普通周六为非工作日', () => { + // 2024-03-16 是周六 + assert.equal(isWorkday(2024, 3, 16), false); + }); + + it('普通周日为非工作日', () => { + // 2024-03-17 是周日 + assert.equal(isWorkday(2024, 3, 17), false); + }); +}); + +// ─── 五行分析 ──────────────────────────────────────────────── + +describe('analyzeElementBalance', () => { + it('四柱全木为不平衡', () => { + const bazi = { + yearPillar: { elementStem: '木', elementBranch: '木', hideStems: [{ stem: '木' }] }, + monthPillar: { elementStem: '木', elementBranch: '木', hideStems: [{ stem: '木' }] }, + dayPillar: { elementStem: '木', elementBranch: '木', hideStems: [{ stem: '木' }] }, + hourPillar: { elementStem: '木', elementBranch: '木', hideStems: [{ stem: '木' }] } + }; + const r = analyzeElementBalance(bazi); + assert.ok(r.wood > 0); + assert.equal(r.fire, 0); + assert.equal(r.isBalanced, false); + assert.equal(r.dominant, '木'); + }); + + it('返回 wood/fire/earth/metal/water/total/dominant/weakest/isBalanced', () => { + const bazi = { + yearPillar: { elementStem: '木', elementBranch: '火', hideStems: [] }, + monthPillar: { elementStem: '土', elementBranch: '金', hideStems: [] }, + dayPillar: { elementStem: '水', elementBranch: '木', hideStems: [] }, + hourPillar: { elementStem: '火', elementBranch: '土', hideStems: [] } + }; + const r = analyzeElementBalance(bazi); + assert.equal(typeof r.total, 'number'); + assert.ok(r.total > 0); + assert.ok(['木', '火', '土', '金', '水'].includes(r.dominant)); + assert.ok(['木', '火', '土', '金', '水'].includes(r.weakest)); + }); +}); + +// ─── 称骨算命 ──────────────────────────────────────────────── + +describe('calculateBoneWeight', () => { + it('返回完整结构', () => { + const r = calculateBoneWeight(0, 1, 1, 0); + assert.ok(typeof r.yearWeight === 'number'); + assert.ok(typeof r.monthWeight === 'number'); + assert.ok(typeof r.dayWeight === 'number'); + assert.ok(typeof r.hourWeight === 'number'); + assert.ok(typeof r.totalWeight === 'number'); + assert.equal(r.totalWeight, r.yearWeight + r.monthWeight + r.dayWeight + r.hourWeight); + assert.ok(typeof r.interpretation === 'string'); + assert.ok(typeof r.fortune === 'string'); + }); + + it('totalLiang 和 totalQian 与 totalWeight 一致', () => { + const r = calculateBoneWeight(5, 3, 10, 4); + assert.equal(r.totalLiang * 10 + r.totalQian, r.totalWeight); + }); +}); + +// ─── 大运/流年分析 ─────────────────────────────────────────── + +describe('analyzeFortuneGanzhi', () => { + it('返回完整分析结构', () => { + const r = analyzeFortuneGanzhi('甲子', '甲', '子'); + assert.equal(r.ganzhi, '甲子'); + assert.ok(r.tenStar); + assert.ok(r.elementRelation); + assert.ok(['吉', '平', '凶'].includes(r.level)); + assert.ok(typeof r.score === 'number'); + }); + + it('同干同支 score 不为极端值', () => { + const r = analyzeFortuneGanzhi('甲子', '甲', '子'); + assert.ok(r.score >= -10 && r.score <= 10); + }); +}); + +// ─── 节气查询 ──────────────────────────────────────────────── + +describe('getSolarTerm', () => { + it('非节气日返回 null', () => { + // 随机选一个非节气日 + const r = getSolarTerm(2024, 6, 15); + assert.equal(r, null); + }); + + it('返回值类型为字符串或 null', () => { + const r = getSolarTerm(2024, 3, 20); // 春分附近 + assert.ok(r === null || typeof r === 'string'); + }); +});