build-ics.ts 4.6 KB

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