Browse Source

feat: English iCal

yaavi 1 year ago
parent
commit
bb976d4735
5 changed files with 133 additions and 93 deletions
  1. 5 1
      CHANGELOG.md
  2. 3 1
      README.en.md
  3. 2 0
      README.md
  4. 1 1
      package.json
  5. 122 90
      scripts/build-ics.ts

+ 5 - 1
CHANGELOG.md

@@ -1,8 +1,12 @@
 # CHANGELOG
 
+## [1.3.1](https://github.com/vsme/chinese-days) (2024-06-15)
+
+- 增加 `iCal` 英文版本订阅
+
 ## [1.3.0](https://github.com/vsme/chinese-days) (2024-06-15)
 
-- 支持 `ics` 文件订阅节假日,可供 Google Calendar、Apple Calendar、Microsoft Outlook 等客户端订阅
+- 支持 `iCal` 文件订阅节假日,可供 Google Calendar、Apple Calendar、Microsoft Outlook 等客户端订阅
 
 ## [1.2.4](https://github.com/vsme/chinese-days) (2024-06-03)
 

+ 3 - 1
README.en.md

@@ -16,7 +16,9 @@ This project provides a series of functions for querying Chinese holidays, adjus
 
 The subscribed calendar includes holidays and adjusted working days for the past three years (2022-2024).
 
-Subscription URL: [https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics](https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics)
+Subscription URL: [https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics](https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics) (default language is Chinese)
+
+For English: [https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.en.ics](https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.en.ics)
 
 ## For non-JS projects, you can use the JSON file
 

+ 2 - 0
README.md

@@ -14,6 +14,8 @@
 
 在 Google Calendar、Apple Calendar、Microsoft Outlook 等客户端中,可以设置订阅地址:[https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics](https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.ics) 来获取日历订阅。
 
+For English: [https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.en.ics](https://cdn.jsdelivr.net/npm/chinese-days/dist/holidays.en.ics)
+
 订阅的日历包含近三年(2022-2024年)的节假日和调休日。
 
 ## 非 `JS` 语言

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "chinese-days",
-  "version": "1.3.0",
+  "version": "1.3.1",
   "description": "中国节假日、调休日、工作日、24节气查询,农历阳历互转,支持 TS、CommonJS、UMD 模块化使用,提供 ics 日历格式,可供 Google Calendar、Apple Calendar、Microsoft Outlook 等客户端订阅。",
   "main": "dist/index.min.js",
   "module": "dist/index.es.js",

+ 122 - 90
scripts/build-ics.ts

@@ -7,25 +7,46 @@ import ical, {
 import generate from "../src/holidays/generate";
 import dayjs, { Dayjs } from "../src/utils/dayjs";
 import { Holiday } from "../src/holidays/arrangement";
+import { createHash } from 'crypto';
 
-const { holidays, workdays, inLieuDays } = generate();
+enum DayType {
+  Workday = 1,
+  Holiday = 2,
+}
 
+const { holidays, workdays } = generate();
 const endYear = Number(Object.keys(holidays)[0].slice(0, 4))
 
-// 创建一个新的日历
-const cal = ical({
-  name: "中国节假日",
-  timezone: "Asia/Shanghai",
-  prodId: { company: "yaavi.me", product: "Chinese Days", language: "CN" },
-});
-
-// 设置日历描述
-cal.description(`${endYear - 2}~${endYear}年中国节假日日历`);
-
-// 添加时区信息
-cal.timezone({
-  name: "Asia/Shanghai",
-  generator: (tzid) => `
+const buildIcal = (language: 'CN' | 'EN') => {
+  const info = language == 'CN' ? {
+    name: '中国节假日',
+    desc: `${endYear - 2}~${endYear}年中国节假日日历`,
+    location: '北京',
+    categories: '节假日',
+    holiday: '休',
+    workday: '班'
+  } : {
+    name: 'Chinese Public Holidays',
+    desc: `Calendar of Chinese Public Holidays for ${endYear - 2}~${endYear} Years`,
+    location: 'Beijing',
+    categories: 'Holidays',
+    holiday: 'Holiday',
+    workday: 'Workday',
+  }
+  // 创建一个新的日历
+  const cal = ical({
+    name: info.name,
+    timezone: "Asia/Shanghai",
+    prodId: { company: "yaavi.me", product: "Chinese Days", language },
+  });
+
+  // 设置日历描述
+  cal.description(info.desc);
+
+  // 添加时区信息
+  cal.timezone({
+    name: "Asia/Shanghai",
+    generator: (tzid) => `
 BEGIN:VTIMEZONE
 TZID:${tzid}
 X-LIC-LOCATION:${tzid}
@@ -36,92 +57,103 @@ TZNAME:CST
 DTSTART:19700101T000000
 END:STANDARD
 END:VTIMEZONE`,
-});
-
-const buildHolidays = (
-  year: number,
-  days: Record<string, Holiday>,
-  mark: "(休)" | "(班)" | string
-) => {
-  // 合并相同节日的日期
-  const mergedHolidays: Record<
-    string,
-    {
-      chineseName: string;
-      dates: string[];
-    }
-  > = {};
-
-  for (const [date, info] of Object.entries(days)) {
-    if (date.startsWith(String(year))) {
-      const [name, chineseName] = info.split(",");
-      if (!mergedHolidays[name]) {
-        mergedHolidays[name] = {
-          chineseName,
-          dates: [],
-        };
-      }
-      mergedHolidays[name].dates.push(date);
-    }
-  }
-
-  // 检查日期是否连续的函数
-  const areDatesContinuous = (date1: Dayjs, date2: Dayjs) => {
-    return dayjs(date2).diff(dayjs(date1), "day") === 1;
-  };
+  });
 
-  for (const [name, details] of Object.entries(mergedHolidays)) {
-    const { chineseName, dates } = details;
-    dates.sort(); // 确保日期按顺序排列
-
-    let startDate = dayjs(dates[0]);
-    let endDate = startDate;
-
-    for (let i = 1; i < dates.length; i++) {
-      const currentDate = dayjs(dates[i]);
-
-      if (areDatesContinuous(endDate, currentDate)) {
-        endDate = currentDate;
-      } else {
-        // 添加当前事件
-        cal.createEvent({
-          start: startDate.toDate(),
-          end: endDate.add(1, "day").toDate(),
-          description: `${chineseName}放假: 共${mark === '(休)' ? '休息' : '需补班'} ${dates.length} 天`,
-          status: ICalEventStatus.CONFIRMED,
-          summary: `${chineseName}${mark}`,
-          transparency: ICalEventTransparency.TRANSPARENT,
-          allDay: true,
-          class: ICalEventClass.PUBLIC,
-        });
-
-        // 重置开始和结束日期
-        startDate = currentDate;
-        endDate = currentDate;
-      }
-    }
+  const calAddDays = (startDate: Dayjs, endDate: Dayjs, name: string, mark: DayType) => {
+    // 基于事件生成稳定的哈希值
+    const generateStableUUID = () => {
+      const hash = createHash('sha256');
+      hash.update(name + startDate.toDate().toISOString() + endDate.toDate().toISOString() + mark);
+      return hash.digest('hex');
+    };
 
-    // 添加最后一个事件
     cal.createEvent({
       start: startDate.toDate(),
       end: endDate.add(1, "day").toDate(),
-      description: `${chineseName}放假: 共${mark === '(休)' ? '休息' : '需补班'} ${dates.length} 天`,
+      description: `${mark === DayType.Holiday ? info.holiday : info.workday}`,
       status: ICalEventStatus.CONFIRMED,
-      summary: `${chineseName}${mark}`,
+      summary: `${name}(${mark === DayType.Holiday ? info.holiday : info.workday})`,
+      location: info.location,
       transparency: ICalEventTransparency.TRANSPARENT,
       allDay: true,
       class: ICalEventClass.PUBLIC,
+      categories: [{ name: info.categories }],
+      x: [
+        { key: 'X-MICROSOFT-CDO-ALLDAYEVENT', value: 'TRUE' },
+        { key: 'X-MICROSOFT-MSNCALENDAR-ALLDAYEVENT', value: 'TRUE' },
+        ...(
+          mark == DayType.Holiday
+            ? [{ key: 'X-APPLE-SPECIAL-DAY', value: 'WORK-HOLIDAY' }]
+            : mark == DayType.Workday
+              ? [{ key: 'X-APPLE-SPECIAL-DAY', value: 'ALTERNATE-WORKDAY' }]
+              : []
+        ),
+        { key: 'X-APPLE-UNIVERSAL-ID', value: generateStableUUID() }
+      ]
     });
   }
-};
 
-for (let i = endYear; i > endYear - 3; i--) {
-  buildHolidays(i, holidays, "(休)");
-  buildHolidays(i, workdays, "(班)");
+  const buildHolidays = (
+    years: number[],
+    days: Record<string, Holiday>,
+    mark: DayType
+  ) => {
+    // 合并相同节日的日期
+    const mergedHolidays: Record<string, { chineseName: string; dates: string[] }> = {};
+
+    for (const [date, info] of Object.entries(days)) {
+      if (years.includes(Number(date.slice(0, 4)))) {
+        const [name, chineseName] = info.split(",");
+        if (!mergedHolidays[name]) {
+          mergedHolidays[name] = {
+            chineseName,
+            dates: [],
+          };
+        }
+        mergedHolidays[name].dates.push(date);
+      }
+    }
+
+    // 检查日期是否连续的函数
+    const areDatesContinuous = (date1: Dayjs, date2: Dayjs) => {
+      return dayjs(date2).diff(date1, "day") === 1;
+    };
+
+    for (const [name, { chineseName, dates }] of Object.entries(mergedHolidays)) {
+      dates.sort(); // 确保日期按顺序排列
+
+      let startDate = dayjs(dates[0]);
+      let endDate = startDate;
+
+      for (let i = 1; i < dates.length; i++) {
+        const currentDate = dayjs(dates[i]);
+
+        if (areDatesContinuous(endDate, currentDate)) {
+          endDate = currentDate;
+        } else {
+          // 添加当前事件
+          calAddDays(startDate, endDate, language == 'CN' ? chineseName : name, mark);
+
+          // 重置开始和结束日期
+          startDate = currentDate;
+          endDate = currentDate;
+        }
+      }
+
+      // 添加最后一个事件
+      calAddDays(startDate, endDate, language == 'CN' ? chineseName : name, mark);
+    }
+  };
+
+  buildHolidays([endYear, endYear - 1, endYear - 2], holidays, DayType.Holiday);
+  buildHolidays([endYear, endYear - 1, endYear - 2], workdays, DayType.Workday);
+
+  // 将日历保存到 ./dist/holidays.ics 文件
+  fs.writeFile(`./dist/holidays${language == 'CN' ? '' : '.en'}.ics`, cal.toString(), "utf8", (err) => {
+    if (err) throw err;
+    console.log(`The ${language} ICS file has been saved!`);
+  });
 }
 
-// 将日历保存到 ./dist/holidays.ics 文件
-fs.writeFile("./dist/holidays.ics", cal.toString(), "utf8", (err) => {
-  if (err) throw err;
-  console.log("The ICS file has been saved!");
-});
+buildIcal('CN')
+buildIcal('EN')