Browse Source

feat: add Lunar Folk Festival

Yaavi 6 months ago
parent
commit
f7f10d08e3

+ 3 - 0
src/index.ts

@@ -1,15 +1,18 @@
 import * as HolidayUtils from "./holidays";
 import * as SolarTerm from "./solar_terms";
 import * as SolarLunar from "./solar_lunar";
+import * as LunarFolkFestival from "./lunar_folk_festival";
 
 // 单独导出这些方法和类型
 export * from "./holidays";
 export * from "./solar_terms";
 export * from "./solar_lunar";
+export * from "./lunar_folk_festival";
 
 // 默认导出所有
 export default {
   ...HolidayUtils,
   ...SolarTerm,
   ...SolarLunar,
+  ...LunarFolkFestival,
 }

+ 124 - 0
src/lunar_folk_festival/constants.ts

@@ -0,0 +1,124 @@
+import { getLunarDate, monthDays } from "../solar_lunar";
+import { getSolarTermsInRange } from "../solar_terms";
+import { type Dayjs } from "../utils/dayjs";
+
+// 特殊节日处理器类型
+export interface LunarFestival {
+  date: string;     // 节日日期(公历)
+  name: string;     // 节日名称
+  type: 'lunar' | 'solar_term' | 'special';
+}
+
+// 固定农历节日配置
+export const LUNAR_FESTIVAL_MAP: Record<number, Record<number, string[]>> = {
+  1: { // 正月
+    1: ['春节', '鸡日', '元始天尊诞辰'],
+    2: ['犬日'],
+    3: ['猪日', '小年朝'],
+    4: ['羊日', '孙天医诞辰'],
+    5: ['牛日', '破五日', '开市', '路神诞辰'],
+    6: ['马日'],
+    7: ['人日', '送火神'],
+    8: ['谷日', '阎王诞辰'],
+    9: ['天日', '玉皇诞辰'],
+    10: ['地日', '石头生日'],
+    13: ['上(试)灯日', '关公升天日'],
+    15: ['元宵节', '上元节', '正灯日', '天官诞辰'],
+    18: ['落灯日'],
+    25: ['天仓(填仓)节'],
+  },
+  2: { // 二月
+    1: ['太阳生日'],
+    2: ['春龙节', '土地公生日', '济公活佛生日'],
+    3: ['文昌帝君诞辰'],
+    12: ['百花生日(花朝节)'],
+    15: ['九天玄女诞辰', '太上老君诞辰', '精忠岳王诞辰'],
+    19: ['观音菩萨诞辰'],
+    21: ['普贤菩萨诞辰'],
+  },
+  3: {
+    3: ['上巳节'],
+    15: ['赵公元帅诞辰', '泰山老母诞辰'],
+  },
+  4: {
+    1: ['祭雹神'],
+    4: ['文殊菩萨诞辰'],
+    8: ['浴佛节(龙华会)'],
+    12: ['蛇王诞辰'],
+    14: ['吕洞宾诞辰'],
+    18: ['华佗诞辰'],
+    28: ['药王(神农)诞辰'],
+  },
+  5: {
+    5: ['端午节'],
+    13: ['雨节', '黄帝诞辰'],
+  },
+  6: {
+    1: ['半年节'],
+    6: ['晒衣节'],
+    19: ['观音菩萨得道'],
+    24: ['雷神诞辰', '荷花生日', '关公诞辰'],
+  },
+  7: {
+    1: ['祭海神'],
+    7: ['乞巧节'],
+    15: ['中元(鬼)节', '地官诞辰(孟兰盆会)'],
+    18: ['西王母诞辰'],
+    20: ['棉花生日'],
+    23: ['诸葛亮诞辰'],
+    30: ['地藏菩萨诞辰'],
+  },
+  8: {
+    1: ['天医节'],
+    3: ['灶君生日'],
+    8: ['瑶池大会'],
+    15: ['中秋节'],
+    20: ['水稻生日'],
+    28: ['孔子诞辰'],
+  },
+  9: {
+    9: ['重阳节'],
+    19: ['观音菩萨出家'],
+  },
+  10: {
+    1: ['十月朝', '寒衣节'],
+    15: ['下元节', '水官诞辰'],
+  },
+  12: {
+    8: ['腊八节'],
+    23: ['官家送灶'],
+    24: ['民间送灶'],
+    25: ['接玉皇'],
+  }
+};
+
+// 特殊节日处理器
+export const SPECIAL_FESTIVAL_HANDLERS: ((date: Dayjs, result: LunarFestival[]) => void)[] = [
+  // 处理寒食节(清明前一日)
+  (current, result) => {
+    const pureBrightnessDay = current.add(1, 'day')
+    const pureBrightness = getSolarTermsInRange(pureBrightnessDay).find(t => t.term === 'pure_brightness');
+    if (pureBrightness) {
+      result.push({
+        date: current.format('YYYY-MM-DD'),
+        name: '寒食节',
+        type: 'solar_term'
+      });
+    }
+  },
+
+  // 处理除夕(农历腊月最后一日)
+  (current, result) => {
+    const lunar = getLunarDate(current);
+    if (lunar.lunarMon === 12 && lunar.lunarDay === monthDays(lunar.lunarYear, 12)) {
+      const date = current.format('YYYY-MM-DD');
+      ['除夕', '封井', '祭井神', '贴春联', '迎财神'].forEach(name => {
+        result.push({
+          date,
+          name,
+          type: 'lunar'
+        });
+      })
+    }
+  }
+];

+ 54 - 0
src/lunar_folk_festival/index.ts

@@ -0,0 +1,54 @@
+import { type ConfigType } from "../utils/dayjs";
+import dayjs from '../utils/dayjs';
+import { getLunarDate } from "../solar_lunar";
+import { LUNAR_FESTIVAL_MAP, SPECIAL_FESTIVAL_HANDLERS, type LunarFestival } from './constants'
+
+/**
+ * 获取农历节日(包含固定节日和特殊计算节日)
+ * @param start 开始日期
+ * @param end 结束日期
+ */
+export const getLunarFestivals = (
+  start?: ConfigType,
+  end?: ConfigType
+): { date: string, name: string[] }[] => {
+  const results: LunarFestival[] = [];
+  let current = dayjs(start);
+  const endDate = dayjs(end || start);
+
+  // 遍历日期范围
+  while (current.isBefore(endDate) || current.isSame(endDate)) {
+    // 处理固定农历节日
+    const lunar = getLunarDate(current);
+    if (!lunar.isLeap) {
+      const festivals = LUNAR_FESTIVAL_MAP[lunar.lunarMon]?.[lunar.lunarDay] || [];
+      festivals.forEach(name => {
+        results.push({
+          date: current.format('YYYY-MM-DD'),
+          name,
+          type: 'lunar'
+        });
+      });
+    }
+
+    // 运行特殊节日处理器
+    SPECIAL_FESTIVAL_HANDLERS.forEach(handler => handler(current, results));
+
+    current = current.add(1, 'day');
+  }
+
+  // 去重并排序
+  return results.reduce((acc: { date: string; name: string[] }[], curr) => {
+    const existing = acc.find(item => item.date === curr.date)
+    if (existing) {
+      existing.name.push(curr.name)
+    } else {
+      acc.push({ date: curr.date, name: [curr.name] })
+    }
+    return acc
+  }, [])
+};
+
+export default {
+  getLunarFestivals
+}

+ 1 - 1
src/solar_lunar/index.ts

@@ -41,7 +41,7 @@ const cyclicalm = (num: number): string => NUMBER_1[num % 10] + NUMBER_2[num % 1
  * @param m 农历月份
  * @returns 月份天数
  */
-const monthDays = (y: number, m: number): number => (LUNAR_INFO[y - 1900] & (0x10000 >> m)) === 0 ? 29 : 30;
+export const monthDays = (y: number, m: number): number => (LUNAR_INFO[y - 1900] & (0x10000 >> m)) === 0 ? 29 : 30;
 
 /**
  * 获取指定年份的生肖

+ 84 - 0
test/lunar_folk_festival/index.test.ts

@@ -0,0 +1,84 @@
+import { getLunarFestivals } from "../../src";
+
+describe("lunarFestivals", () => {
+  test("getLunarFestivals should return fixed lunar festivals", () => {
+    // 测试常规固定节日
+    const result = getLunarFestivals("2025-01-29");
+    expect(result).toEqual([
+      {
+        date: "2025-01-29",
+        name: ["春节", "鸡日", "元始天尊诞辰"],
+      }
+    ]);
+  });
+
+  test("should handle solar term related festivals", () => {
+    // 测试寒食节(清明前一日)
+    const result = getLunarFestivals("2025-04-03");
+    expect(result).toEqual([{
+      date: "2025-04-03",
+      name: ["寒食节"],
+    }]);
+  });
+
+  test("should handle special festivals", () => {
+    // 测试除夕(农历腊月最后一日)
+    const result = getLunarFestivals("2025-01-28");
+    expect(result).toEqual([{
+      date: "2025-01-28",
+      name: ["除夕", "封井", "祭井神", "贴春联", "迎财神"],
+    }]);
+  });
+
+  test("should filter leap month festivals", () => {
+    const result1 = getLunarFestivals("2025-06-30")
+    expect(result1).toEqual([{
+      date: "2025-06-30",
+      name: ["晒衣节"],
+    }]);
+
+    // 测试闰月不返回节日
+    const result2 = getLunarFestivals("2025-07-30")
+    expect(result2).toEqual([]);
+  });
+
+  test("should handle cross-year scenarios", () => {
+    // 测试多天与跨年场景
+    const result = getLunarFestivals("2024-11-15", "2025-01-30");
+
+    expect(result).toEqual([
+      {
+        date: "2024-11-15",
+        name: ["下元节", "水官诞辰"],
+      },
+      {
+        date: "2025-01-07",
+        name: ["腊八节"],
+      },
+      {
+        date: "2025-01-22",
+        name: ["官家送灶"],
+      },
+      {
+        date: "2025-01-23",
+        name: ["民间送灶"],
+      },
+      {
+        date: "2025-01-24",
+        name: ["接玉皇"],
+      },
+      {
+        date: "2025-01-28",
+        name: ["除夕", "封井", "祭井神", "贴春联", "迎财神"],
+      },
+      {
+        date: "2025-01-29",
+        name: ["春节", "鸡日", "元始天尊诞辰"],
+      },
+      {
+        date: "2025-01-30",
+        name: ["犬日"],
+      },
+    ]);
+  });
+});