build-ics.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import fs from "fs";
  2. import ical, {
  3. ICalEventClass,
  4. ICalEventStatus,
  5. ICalEventTransparency,
  6. } from "ical-generator";
  7. import generate from "../src/holidays/generate";
  8. import dayjs, { Dayjs } from "../src/utils/dayjs";
  9. import { Holiday } from "../src/holidays/arrangement";
  10. import { createHash } from 'crypto';
  11. enum DayType {
  12. Workday = 1,
  13. Holiday = 2,
  14. }
  15. const { holidays, workdays } = generate();
  16. const endYear = Number(Object.keys(holidays)[0].slice(0, 4))
  17. const buildIcal = (language: 'CN' | 'EN') => {
  18. const info = language == 'CN' ? {
  19. name: '中国节假日',
  20. desc: `${endYear - 2}~${endYear}年中国节假日日历`,
  21. location: '北京',
  22. categories: '节假日',
  23. holiday: '休',
  24. workday: '班'
  25. } : {
  26. name: 'Chinese Public Holidays',
  27. desc: `Calendar of Chinese Public Holidays for ${endYear - 2}~${endYear} Years`,
  28. location: 'Beijing',
  29. categories: 'Holidays',
  30. holiday: 'Holiday',
  31. workday: 'Workday',
  32. }
  33. // 创建一个新的日历
  34. const cal = ical({
  35. name: info.name,
  36. timezone: "Asia/Shanghai",
  37. prodId: { company: "yaavi.me", product: "Chinese Days", language },
  38. });
  39. // 设置日历描述
  40. cal.description(info.desc);
  41. // 添加时区信息
  42. cal.timezone({
  43. name: "Asia/Shanghai",
  44. generator: (tzid) => `
  45. BEGIN:VTIMEZONE
  46. TZID:${tzid}
  47. X-LIC-LOCATION:${tzid}
  48. BEGIN:STANDARD
  49. TZOFFSETFROM:+0800
  50. TZOFFSETTO:+0800
  51. TZNAME:CST
  52. DTSTART:19700101T000000
  53. END:STANDARD
  54. END:VTIMEZONE`,
  55. });
  56. const calAddDays = (startDate: Dayjs, endDate: Dayjs, name: string, mark: DayType) => {
  57. // 基于事件生成稳定的哈希值
  58. const generateStableUUID = () => {
  59. const hash = createHash('sha256');
  60. hash.update(name + startDate.toDate().toISOString() + endDate.toDate().toISOString() + mark);
  61. return hash.digest('hex');
  62. };
  63. cal.createEvent({
  64. start: startDate.toDate(),
  65. end: endDate.add(1, "day").toDate(),
  66. description: `${mark === DayType.Holiday ? info.holiday : info.workday}`,
  67. status: ICalEventStatus.CONFIRMED,
  68. summary: `${name}(${mark === DayType.Holiday ? info.holiday : info.workday})`,
  69. location: info.location,
  70. transparency: ICalEventTransparency.TRANSPARENT,
  71. allDay: true,
  72. class: ICalEventClass.PUBLIC,
  73. categories: [{ name: info.categories }],
  74. x: [
  75. { key: 'X-MICROSOFT-CDO-ALLDAYEVENT', value: 'TRUE' },
  76. { key: 'X-MICROSOFT-MSNCALENDAR-ALLDAYEVENT', value: 'TRUE' },
  77. ...(
  78. mark == DayType.Holiday
  79. ? [{ key: 'X-APPLE-SPECIAL-DAY', value: 'WORK-HOLIDAY' }]
  80. : mark == DayType.Workday
  81. ? [{ key: 'X-APPLE-SPECIAL-DAY', value: 'ALTERNATE-WORKDAY' }]
  82. : []
  83. ),
  84. { key: 'X-APPLE-UNIVERSAL-ID', value: generateStableUUID() }
  85. ]
  86. });
  87. }
  88. const buildHolidays = (
  89. years: number[],
  90. days: Record<string, Holiday>,
  91. mark: DayType
  92. ) => {
  93. // 合并相同节日的日期
  94. const mergedHolidays: Record<string, { chineseName: string; dates: string[] }> = {};
  95. for (const [date, info] of Object.entries(days)) {
  96. if (years.includes(Number(date.slice(0, 4)))) {
  97. const [name, chineseName] = info.split(",");
  98. if (!mergedHolidays[name]) {
  99. mergedHolidays[name] = {
  100. chineseName,
  101. dates: [],
  102. };
  103. }
  104. mergedHolidays[name].dates.push(date);
  105. }
  106. }
  107. // 检查日期是否连续的函数
  108. const areDatesContinuous = (date1: Dayjs, date2: Dayjs) => {
  109. return dayjs(date2).diff(date1, "day") === 1;
  110. };
  111. for (const [name, { chineseName, dates }] of Object.entries(mergedHolidays)) {
  112. dates.sort(); // 确保日期按顺序排列
  113. let startDate = dayjs(dates[0]);
  114. let endDate = startDate;
  115. for (let i = 1; i < dates.length; i++) {
  116. const currentDate = dayjs(dates[i]);
  117. if (areDatesContinuous(endDate, currentDate)) {
  118. endDate = currentDate;
  119. } else {
  120. // 添加当前事件
  121. calAddDays(startDate, endDate, language == 'CN' ? chineseName : name, mark);
  122. // 重置开始和结束日期
  123. startDate = currentDate;
  124. endDate = currentDate;
  125. }
  126. }
  127. // 添加最后一个事件
  128. calAddDays(startDate, endDate, language == 'CN' ? chineseName : name, mark);
  129. }
  130. };
  131. buildHolidays([endYear, endYear - 1, endYear - 2], holidays, DayType.Holiday);
  132. buildHolidays([endYear, endYear - 1, endYear - 2], workdays, DayType.Workday);
  133. // 将日历保存到 ./dist/holidays.ics 文件
  134. fs.writeFile(`./dist/holidays${language == 'CN' ? '' : '.en'}.ics`, cal.toString(), "utf8", (err) => {
  135. if (err) throw err;
  136. console.log(`The ${language} ICS file has been saved!`);
  137. });
  138. }
  139. buildIcal('CN')
  140. buildIcal('EN')