├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── leafee98 │ │ └── CSTI │ │ └── core │ │ ├── utils │ │ ├── LocalTimeRange.java │ │ ├── LocalDateTimeRange.java │ │ ├── WeekUtilsNoException.java │ │ ├── PairNumber.java │ │ ├── CharacterEscape.java │ │ ├── TimeFormatter.java │ │ ├── VariableSubstitution.java │ │ ├── LessonDateTimeCalculator.java │ │ ├── RangeNumber.java │ │ └── WeekUtils.java │ │ ├── ics │ │ ├── Component.java │ │ ├── IcsWords.java │ │ ├── model │ │ │ ├── VAlarm.java │ │ │ ├── VTimezone.java │ │ │ ├── Daylight.java │ │ │ ├── Standard.java │ │ │ ├── VCalendar.java │ │ │ └── VEvent.java │ │ ├── Value.java │ │ └── Property.java │ │ ├── exceptions │ │ ├── InvalidConfigure.java │ │ ├── InvalidLessonRange.java │ │ ├── TimeZoneException.java │ │ ├── InvalidLessonSchedule.java │ │ ├── InvalidComponentParams.java │ │ └── InvalidScheduleFileStruct.java │ │ ├── bean │ │ ├── loader │ │ │ ├── builder │ │ │ │ ├── GenericScheduleObjectBuilder.java │ │ │ │ ├── GenericLessonBuilder.java │ │ │ │ └── GenericConfigureBuilder.java │ │ │ └── JSONLoader.java │ │ ├── LessonSchedule.java │ │ ├── ScheduleObject.java │ │ ├── LessonRanges.java │ │ ├── Lesson.java │ │ └── Configure.java │ │ ├── configure │ │ └── KeyWords.java │ │ ├── App.java │ │ ├── wrapper │ │ └── schedule │ │ │ └── BreakLine.java │ │ └── generate │ │ └── Generator.java └── test │ ├── java │ └── com │ │ └── github │ │ └── leafee98 │ │ └── CSTI │ │ └── core │ │ ├── CharacterEscapeTest.java │ │ ├── TimeFormatterTest.java │ │ ├── RangeNumberTest.java │ │ ├── TestIcs.java │ │ ├── LocalTimeRangeTest.java │ │ ├── BreakLineTest.java │ │ ├── GeneratorTest.java │ │ └── bean │ │ └── loader │ │ └── JSONLoaderTest.java │ └── resources │ └── csti-example.json ├── README.md ├── doc ├── example │ ├── e1(deprecated) │ │ ├── e1.csti │ │ └── e1.ics │ └── e2 │ │ └── e2.json ├── csti-json.md └── csti-define(deprecated).md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.vscode/ 3 | /.settings/ 4 | /.idea/ 5 | /*.iml 6 | /.project 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/LocalTimeRange.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import java.time.LocalTime; 4 | 5 | public class LocalTimeRange { 6 | 7 | public LocalTime from; 8 | public LocalTime to; 9 | 10 | public LocalTimeRange(LocalTime from, LocalTime to) { 11 | this.from = from; 12 | this.to = to; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/Component.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public abstract class Component { 7 | 8 | private final String name; 9 | 10 | public Component(String name) { 11 | this.name = name; 12 | } 13 | 14 | public String getName() { 15 | return name; 16 | } 17 | 18 | public abstract String toString(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/LocalDateTimeRange.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.time.LocalTime; 6 | 7 | public class LocalDateTimeRange { 8 | 9 | public LocalDateTime from; 10 | public LocalDateTime to; 11 | 12 | public LocalDateTimeRange(LocalDateTime from, LocalDateTime to) { 13 | this.from = from; 14 | this.to = to; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/CharacterEscapeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.utils.CharacterEscape; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class CharacterEscapeTest { 8 | 9 | @Test 10 | void testEscape() { 11 | String input = "abc,\n" + "def:\n" + "ghi;\n" + "\"hello\"\n" + "\\"; 12 | String actual = CharacterEscape.escape(input); 13 | String expect = "abc\\,\\ndef:\\nghi\\;\\n\"hello\"\\n\\\\"; 14 | Assertions.assertEquals(expect, actual); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/WeekUtilsNoException.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import com.github.leafee98.CSTI.core.exceptions.TimeZoneException; 4 | 5 | import java.time.Month; 6 | 7 | public class WeekUtilsNoException extends WeekUtils { 8 | 9 | @Override 10 | public int byIndicator(int dayOfMonthIndicator, Month month) { 11 | int result = 0; 12 | try { 13 | result = super.byIndicator(dayOfMonthIndicator, month); 14 | } catch (TimeZoneException e) { 15 | result = super.ordinalOfWeek(dayOfMonthIndicator); 16 | } 17 | return result; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/TimeFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.utils.TimeFormatter; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | public class TimeFormatterTest { 10 | 11 | @Test 12 | public void testFormat() { 13 | LocalDateTime dateTime = LocalDateTime.of(2021, 1, 2, 12, 30); 14 | TimeFormatter formatter = new TimeFormatter("Asia/Shanghai"); 15 | 16 | Assertions.assertEquals("20210102T123000", formatter.local(dateTime)); 17 | Assertions.assertEquals("20210102T043000Z", formatter.utc(dateTime)); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/InvalidConfigure.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class InvalidConfigure extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -5132496754132654987L; 6 | 7 | public InvalidConfigure() { 8 | super(); 9 | } 10 | 11 | public InvalidConfigure(String message) { 12 | super(message); 13 | } 14 | 15 | public InvalidConfigure(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public InvalidConfigure(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected InvalidConfigure(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/InvalidLessonRange.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class InvalidLessonRange extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -3784919202339592450L; 6 | 7 | public InvalidLessonRange() { 8 | super(); 9 | } 10 | 11 | public InvalidLessonRange(String message) { 12 | super(message); 13 | } 14 | 15 | public InvalidLessonRange(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public InvalidLessonRange(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected InvalidLessonRange(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/TimeZoneException.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class TimeZoneException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 546781747196321695L; 6 | 7 | public TimeZoneException() { 8 | super(); 9 | } 10 | 11 | public TimeZoneException(String message) { 12 | super(message); 13 | } 14 | 15 | public TimeZoneException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public TimeZoneException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected TimeZoneException(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/InvalidLessonSchedule.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class InvalidLessonSchedule extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -8237491283000239416L; 6 | 7 | public InvalidLessonSchedule() { 8 | super(); 9 | } 10 | 11 | public InvalidLessonSchedule(String message) { 12 | super(message); 13 | } 14 | 15 | public InvalidLessonSchedule(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public InvalidLessonSchedule(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected InvalidLessonSchedule(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/InvalidComponentParams.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class InvalidComponentParams extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -995228257258251568L; 6 | 7 | public InvalidComponentParams() { 8 | super(); 9 | } 10 | 11 | public InvalidComponentParams(String message) { 12 | super(message); 13 | } 14 | 15 | public InvalidComponentParams(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public InvalidComponentParams(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected InvalidComponentParams(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Class Schedule To Icalendar (Core) 2 | 3 | 潜在的毕业设计项目, 在此之前会限制源码级别的使用. 4 | 5 | 根据指定格式的课程表定义文件的信息, 生成可供日历软件导入的`ics`文件. 本项目是逻辑实现的核心部分. 6 | 7 | ## 课程表定义文件(试验阶段, 未来有可能会进行格式上的修改) 8 | 9 | 见[csti-define.md](/doc/csti-define(csti-define(deprecated).md))(Deprecated), 预计在 0.1.0 版本移除. 10 | 11 | 新加入的课程表描述格式为 JSON , 其格式描述见 [csti-json](doc/csti-json.md), 在直接运行编译后的应用时, 此描述格式尚未起作用. 12 | 13 | ## 特征 14 | 15 | - 项目生成结果符合[rfc 5545](https://tools.ietf.org/html/rfc5545)标准 16 | - 生成结果中, 事件包括事件前的提醒 17 | - 生成结果中, 支持事件名称和事件描述的自定义 18 | - 支持Java库内已有的所有时区 19 | 20 | ## 使用 21 | 22 | 编译好的主程序有为命令行程序, 有两个参数可以使用. 23 | 24 | ``` 25 | -i 26 | -o 27 | ``` 28 | 29 | 如果 `` 或 `` 值为短横线或未设置命令行参数, 则表示采用标准输入/输出流. 30 | 31 | ## 示例 32 | 33 | 见目录[`/doc/example/`](!https://github.com/leafee98/class-schedule-to-icalendar-core/tree/master/doc/example) 34 | 35 | ## 待办 36 | 37 | - [ ] 修正转义不完善的问题(针对`\$`等转义字符) 38 | - [x] 对于可选的留空的配置选项, 使用默认值而不是空字符串作为配置结果 39 | 40 | ## 许可 41 | 42 | 允许编译和使用, 但暂时不开放对源码的直接复制使用 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/exceptions/InvalidScheduleFileStruct.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.exceptions; 2 | 3 | public class InvalidScheduleFileStruct extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -1553216499745121336L; 6 | 7 | public InvalidScheduleFileStruct() { 8 | super(); 9 | } 10 | 11 | public InvalidScheduleFileStruct(String message) { 12 | super(message); 13 | } 14 | 15 | public InvalidScheduleFileStruct(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public InvalidScheduleFileStruct(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | protected InvalidScheduleFileStruct(String message, Throwable cause, 24 | boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/PairNumber.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | public class PairNumber { 4 | 5 | int first; 6 | int second; 7 | 8 | public PairNumber(int n) { 9 | this(n, n); 10 | } 11 | 12 | public PairNumber(int first, int second) { 13 | this.first = first; 14 | this.second = second; 15 | } 16 | 17 | boolean isPair() { 18 | return first != second; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | if (isPair()) { 24 | return String.format("{%d,%d}", first, second); 25 | } else { 26 | return String.format("{%d}", first); 27 | } 28 | } 29 | 30 | public int getFirst() { 31 | return first; 32 | } 33 | 34 | public void setFirst(int first) { 35 | this.first = first; 36 | } 37 | 38 | public int getSecond() { 39 | return second; 40 | } 41 | 42 | public void setSecond(int second) { 43 | this.second = second; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/CharacterEscape.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public class CharacterEscape { 7 | 8 | private static class CharPair { 9 | public CharPair(String from, String to) { 10 | this.from = from; 11 | this.to = to; 12 | } 13 | public String from; 14 | public String to; 15 | } 16 | 17 | private static final List escapeList = Arrays.asList( 18 | // the holy shit escape in java, regex. why isn't there a raw string? 19 | new CharPair("\\\\(?![;,n])", "\\\\\\\\"), 20 | new CharPair(";", "\\\\;"), 21 | new CharPair(",", "\\\\,"), 22 | new CharPair("\\n", "\\\\n") 23 | // the char ":" is not needed to escape 24 | ); 25 | 26 | public static String escape(String str) { 27 | for (CharPair x : escapeList) { 28 | str = str.replaceAll(x.from, x.to); 29 | } 30 | return str; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/loader/builder/GenericScheduleObjectBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean.loader.builder; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Configure; 4 | import com.github.leafee98.CSTI.core.bean.Lesson; 5 | import com.github.leafee98.CSTI.core.bean.ScheduleObject; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class GenericScheduleObjectBuilder { 11 | private final ScheduleObject scheduleObject; 12 | 13 | public GenericScheduleObjectBuilder() { 14 | scheduleObject = new ScheduleObject(); 15 | } 16 | 17 | public ScheduleObject build() { 18 | return scheduleObject; 19 | } 20 | 21 | public void setConfigure(Configure conf) { 22 | scheduleObject.setConfigure(conf); 23 | } 24 | 25 | public void addLesson(Lesson lesson) { 26 | if (scheduleObject.getLessons() == null) 27 | scheduleObject.setLessons(new ArrayList<>()); 28 | scheduleObject.getLessons().add(lesson); 29 | } 30 | 31 | public void setLessons(List lessons) { 32 | scheduleObject.setLessons(lessons); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/IcsWords.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics; 2 | 3 | public class IcsWords { 4 | 5 | public static final String vevent = "VEVENT"; 6 | public static final String valarm = "VALARM"; 7 | public static final String created = "CREATED"; 8 | public static final String description = "DESCRIPTION"; 9 | public static final String dtend = "DTEND"; 10 | public static final String dtstamp = "DTSTAMP"; 11 | public static final String dtstart = "DTSTART"; 12 | public static final String lastModified = "LAST-MODIFIED"; 13 | public static final String location = "LOCATION"; 14 | public static final String rrule = "RRULE"; 15 | public static final String sequence = "SEQUENCE"; 16 | public static final String summary = "SUMMARY"; 17 | public static final String transp = "TRANSP"; 18 | public static final String uid = "UID"; 19 | public static final String action = "ACTION"; 20 | public static final String trigger = "TRIGGER"; 21 | public static final String DURATION = "DURATION"; 22 | public static final String BEGIN = "BEGIN"; 23 | public static final String END = "END"; 24 | public static final String RELATED = "RELATED"; 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/configure/KeyWords.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.configure; 2 | 3 | public class KeyWords { 4 | 5 | public static final String lessonName = "name"; 6 | public static final String lessonType = "type"; 7 | public static final String lessonTeacher = "teacher"; 8 | public static final String lessonLocation = "location"; 9 | public static final String lessonRemark = "remark"; 10 | public static final String lessonSchedule = "schedule"; 11 | 12 | public static final String eventSummaryFormat = "event-summary-format"; 13 | public static final String eventDescriptionFormat = "event-description-format"; 14 | public static final String timezone = "timezone"; 15 | public static final String firstDayOfWeek = "first-day-of-week"; 16 | public static final String semesterStartDate = "semester-start-date"; 17 | public static final String lessonRanges = "lesson-ranges"; 18 | public static final String reminderTime = "reminder-time"; 19 | 20 | public static final String global = "global"; 21 | public static final String lessons = "lessons"; 22 | 23 | public static final String confBegin = "[[["; 24 | public static final String confEnd = "]]]"; 25 | public static final String lessonBegin = "<<<"; 26 | public static final String lessonEnd = ">>>"; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /doc/example/e1(deprecated)/e1.csti: -------------------------------------------------------------------------------- 1 | [[[ 2 | event-summary-format:${lessonName}-${location} 3 | event-description-format:name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull} 4 | timezone:Asia/Shanghai 5 | first-day-of-week:1 6 | semester-start-date:2020-02-24 7 | reminder-time:-15m 8 | lesson-ranges: 9 | 1=08:00:00-08:45:00, 10 | 2=08:50:00-09:35:00, 11 | 3=09:50:00-10:35:00, 12 | 4=10:40:00-11:25:00, 13 | 5=11:30:00-12:15:00, 14 | 6=13:30:00-14:15:00, 15 | 7=14:20:00-15:05:00, 16 | 8=15:20:00-16:05:00, 17 | 9=16:10:00-16:55:00, 18 | 10=18:30:00-19:15:00, 19 | 11=19:20:00-20:05:00, 20 | 12=20:10:00-20:55:00 21 | ]]] 22 | 23 | <<< 24 | name:软件测试技术 25 | type:专业必修 26 | teacher:某教师a 27 | location:某位置a 28 | remark:暂无 29 | schedule: 30 | 1-14|1|6-9; 31 | >>> 32 | 33 | <<< 34 | name:软件项目管理 35 | type:专业必修 36 | teacher:某教师b 37 | location:某位置b 38 | remark:没有 39 | schedule: 40 | 1-11|2|3-5; 41 | >>> 42 | 43 | <<< 44 | name:软件测试实践 45 | type:实践选修 46 | teacher:某教师c 47 | location:某位置c 48 | remark:无 49 | schedule: 50 | 1,3,5,7,9,11,13,15|2|8-9; 51 | >>> 52 | 53 | <<< 54 | name:高级软件工程 55 | type:专业必修 56 | teacher:某教师d 57 | location:某位置d 58 | remark:无 59 | schedule: 60 | 1-16|3|3-5; 61 | >>> 62 | 63 | <<< 64 | name:编译原理 65 | type:专业必修 66 | teacher:某教师e 67 | location:某位置e 68 | remark:无 69 | schedule: 70 | 1-12|4|3-5; 71 | 13-14|4|3-5; 72 | >>> 73 | 74 | <<< 75 | name:软件开发实践 76 | type:专业实践 77 | teacher:某教师f 78 | location:某位置f 79 | remark:无 80 | schedule: 81 | 1-4|0|6-7; 82 | 1-4|0|8-9; 83 | >>> 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/TimeFormatter.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import java.time.*; 4 | import java.time.format.DateTimeFormatter; 5 | 6 | public class TimeFormatter { 7 | 8 | private final ZoneId zoneId; 9 | 10 | public TimeFormatter(String zoneId) { 11 | this.zoneId = ZoneId.of(zoneId); 12 | } 13 | 14 | public TimeFormatter(ZoneId zoneId) { 15 | this.zoneId = zoneId; 16 | } 17 | 18 | /** 19 | * get string "19700101T000000Z" from local datetime 19700101T080000 at timezone 20 | * @param dateTime local datetime 21 | * @return string formatted as "yyyyMMddThhmmssZ" 22 | */ 23 | public String utc(LocalDateTime dateTime) { 24 | ZonedDateTime local = dateTime.atZone(zoneId); 25 | ZonedDateTime utc= local.withZoneSameInstant(ZoneId.of("UTC")); 26 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"); 27 | return formatter.format(utc); 28 | } 29 | 30 | /** 31 | * get string "19700101T080000" from datetime 19700101T080000 at Asia/Shanghai 32 | * @param dateTime local datetime 33 | * @return string formatted as "yyyyMMddThhmmss" 34 | */ 35 | public String local(LocalDateTime dateTime) { 36 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"); 37 | return formatter.format(dateTime); 38 | } 39 | 40 | public String offset(ZoneOffset offset) { 41 | return offset.toString().replace(":", ""); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/RangeNumberTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.utils.RangeNumber; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.IntStream; 10 | 11 | public class RangeNumberTest { 12 | @Test 13 | public void testParse() { 14 | String input = "1,2,3-9,10,11,12-20"; 15 | String expected = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20"; 16 | 17 | List list = RangeNumber.parse(input); 18 | 19 | StringBuilder builder = new StringBuilder(); 20 | for (Integer i : list) { 21 | builder.append(i); 22 | builder.append(','); 23 | } 24 | 25 | Assertions.assertEquals(expected, builder.substring(0, builder.length() - 1)); 26 | } 27 | 28 | @Test 29 | public void testRender() { 30 | String expected = "1-20"; 31 | List input = IntStream.range(1, 21).boxed().collect(Collectors.toList()); 32 | 33 | String actual = RangeNumber.render(input); 34 | 35 | Assertions.assertEquals(expected, actual); 36 | } 37 | 38 | @Test 39 | public void testRenderToPair() { 40 | String expected = "[{1,3}, {5}, {7,10}, {19}]"; 41 | String input = "1,2,3,5,7,8,9,10,19"; 42 | 43 | String actual = RangeNumber.renderToPair(RangeNumber.parse(input)).toString(); 44 | Assertions.assertEquals(expected, actual); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/TestIcs.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | import com.github.leafee98.CSTI.core.ics.Value; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class TestIcs { 10 | 11 | String expected1 = "BEGIN:VEVENT\n" + 12 | "CREATED:20200809T013446Z\n" + 13 | "DESCRIPTION:重复事件\\n地点\\n2021年1月1日每周重复直到2021年" + 14 | "3月1日\\n(2021年1月1日为周五)\\n提醒时间为15分钟前\n" + 15 | "DTEND;TZID=Asia/Shanghai:20210101T090000\n" + 16 | "DTSTAMP:20200809T013819Z\n" + 17 | "DTSTART;TZID=Asia/Shanghai:20210101T080000\n" + 18 | "LAST-MODIFIED:20200809T013819Z\n" + 19 | "LOCATION:地点\n" + 20 | "RRULE:FREQ=WEEKLY;UNTIL=20210301T000000Z\n" + 21 | "SEQUENCE:2\n" + 22 | "SUMMARY:重复事件\n" + 23 | "TRANSP:OPAQUE\n" + 24 | "UID:fa3aa1a0-6209-4b71-aa87-a3ac9a32ec1c\n" + 25 | "X-MOZ-GENERATION:2\n" + 26 | 27 | "BEGIN:VALARM\n" + 28 | "ACTION:DISPLAY\n" + 29 | "DESCRIPTION:Default Mozilla Description\n" + 30 | "TRIGGER;VALUE=DURATION:-PT15M\n" + 31 | "END:VALARM\n" + 32 | "END:VEVENT"; 33 | 34 | String expected2 = 35 | "BEGIN:VALARM\n" + 36 | "ACTION:DISPLAY\n" + 37 | "DESCRIPTION:Default Mozilla Description\n" + 38 | "TRIGGER;VALUE=DATE-TIME:20210103T080000Z\n" + 39 | "END:VALARM"; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/VAlarm.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | 6 | public class VAlarm extends Component { 7 | 8 | public static final String ACTION = "ACTION"; 9 | public static final String TRIGGER = "TRIGGER"; 10 | public static final String DESCRIPTION = "DESCRIPTION"; 11 | 12 | private final Property action = new Property(ACTION); 13 | private final Property trigger = new Property(TRIGGER); 14 | private final Property description = new Property(DESCRIPTION); 15 | 16 | public VAlarm() { 17 | super("VALARM"); 18 | } 19 | 20 | public Property getAction() { 21 | return action; 22 | } 23 | 24 | public Property getTrigger() { 25 | return trigger; 26 | } 27 | 28 | public Property getDescription() { 29 | return description; 30 | } 31 | 32 | public boolean isEmpty() { 33 | return action.isEmpty() && trigger.isEmpty() && description.isEmpty(); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | if (this.isEmpty()) return ""; 39 | 40 | StringBuilder builder = new StringBuilder(); 41 | builder.append("BEGIN:").append(getName()).append('\n'); 42 | 43 | if (! action.isEmpty()) builder.append(action.toString()).append('\n'); 44 | if (! trigger.isEmpty()) builder.append(trigger.toString()).append('\n'); 45 | if (! description.isEmpty()) builder.append(description.toString()).append('\n'); 46 | 47 | builder.append("END:").append(getName()); 48 | return builder.toString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /doc/example/e2/e2.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "event-summary-format": "${lessonName}-${location}", 4 | "event-description-format": "name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull}", 5 | "timezone": "Asia/Shanghai", 6 | "first-day-of-week": 1, 7 | "semester-start-date": "2020-02-24", 8 | "reminder-time": ["-15m"], 9 | "lesson-ranges": [ 10 | "1=08:00:00-08:45:00", 11 | "2=08:50:00-09:35:00", 12 | "3=09:50:00-10:35:00", 13 | "4=10:40:00-11:25:00", 14 | "5=11:30:00-12:15:00", 15 | "6=13:30:00-14:15:00", 16 | "7=14:20:00-15:05:00", 17 | "8=15:20:00-16:05:00", 18 | "9=16:10:00-16:55:00", 19 | "10=18:30:00-19:15:00", 20 | "11=19:20:00-20:05:00", 21 | "12=20:10:00-20:55:00" 22 | ] 23 | }, 24 | "lessons": [ { 25 | "name": "软件测试技术", 26 | "type": "专业必修", 27 | "teacher": "某教师", 28 | "location": "某位置", 29 | "remark": "暂无", 30 | "schedule": [ 31 | "1-14|1|6-9" 32 | ] 33 | }, 34 | { 35 | "name": "软件项目管理", 36 | "type": "专业必修", 37 | "teacher": "某教师b", 38 | "location": "某位置b", 39 | "remark": "没有", 40 | "schedule": [ 41 | "1-11|2|3-5" 42 | ] 43 | }, 44 | { 45 | "name": "软件测试实践", 46 | "type": "实践选修", 47 | "teacher": "某教师c", 48 | "location": "某位置c", 49 | "remark": "无", 50 | "schedule":[ 51 | "1,3,5,7,9,11,13,15|2|8-9" 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/test/resources/csti-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "event-summary-format": "${lessonName}-${location}", 4 | "event-description-format": "name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull}", 5 | "timezone": "Asia/Shanghai", 6 | "first-day-of-week": 1, 7 | "semester-start-date": "2020-02-24", 8 | "reminder-time": ["-15m"], 9 | "lesson-ranges": [ 10 | "1=08:00:00-08:45:00", 11 | "2=08:50:00-09:35:00", 12 | "3=09:50:00-10:35:00", 13 | "4=10:40:00-11:25:00", 14 | "5=11:30:00-12:15:00", 15 | "6=13:30:00-14:15:00", 16 | "7=14:20:00-15:05:00", 17 | "8=15:20:00-16:05:00", 18 | "9=16:10:00-16:55:00", 19 | "10=18:30:00-19:15:00", 20 | "11=19:20:00-20:05:00", 21 | "12=20:10:00-20:55:00" 22 | ] 23 | }, 24 | "lessons": [ { 25 | "name": "软件测试技术", 26 | "type": "专业必修", 27 | "teacher": "某教师", 28 | "location": "某位置", 29 | "remark": "暂无", 30 | "schedule": [ 31 | "1-14|1|6-9" 32 | ] 33 | }, 34 | { 35 | "name": "软件项目管理", 36 | "type": "专业必修", 37 | "teacher": "某教师b", 38 | "location": "某位置b", 39 | "remark": "没有", 40 | "schedule": [ 41 | "1-11|2|3-5" 42 | ] 43 | }, 44 | { 45 | "name": "软件测试实践", 46 | "type": "实践选修", 47 | "teacher": "某教师c", 48 | "location": "某位置c", 49 | "remark": "无", 50 | "schedule":[ 51 | "1,3,5,7,9,11,13,15|2|8-9" 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/VTimezone.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | 6 | public class VTimezone extends Component { 7 | 8 | public static final String TZID = "TZID"; 9 | public static final String STANDARD = "STANDARD"; 10 | public static final String DAYLIGHT = "DAYLIGHT"; 11 | 12 | private final Property tzid = new Property(TZID); 13 | private Standard standard = new Standard(); 14 | private Daylight daylight = new Daylight(); 15 | 16 | public VTimezone() { 17 | super("VTIMEZONE"); 18 | } 19 | 20 | public Property getTzid() { 21 | return tzid; 22 | } 23 | 24 | public Standard getStandard() { 25 | return standard; 26 | } 27 | 28 | public void setStandard(Standard standard) { 29 | this.standard = standard; 30 | } 31 | 32 | public Daylight getDaylight() { 33 | return daylight; 34 | } 35 | 36 | public void setDaylight(Daylight daylight) { 37 | this.daylight = daylight; 38 | } 39 | 40 | public boolean isEmpty() { 41 | return tzid.isEmpty() 42 | && standard.isEmpty() 43 | && daylight.isEmpty(); 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | if (this.isEmpty()) return ""; 49 | 50 | StringBuilder builder = new StringBuilder(); 51 | builder.append("BEGIN:").append(getName()).append('\n'); 52 | 53 | if (! tzid.isEmpty()) builder.append(tzid.toString()).append('\n'); 54 | if (! standard.isEmpty()) builder.append(standard.toString()).append('\n'); 55 | if (! daylight.isEmpty()) builder.append(daylight.toString()).append('\n'); 56 | 57 | builder.append("END:").append(getName()); 58 | return builder.toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/LocalTimeRangeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import java.time.LocalTime; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.Assertions; 6 | 7 | import com.github.leafee98.CSTI.core.bean.LessonRanges; 8 | import com.github.leafee98.CSTI.core.utils.LocalTimeRange; 9 | 10 | public class LocalTimeRangeTest { 11 | 12 | @Test 13 | public void testFormatOfToString() { 14 | LocalTimeRange range1 = new LocalTimeRange(LocalTime.of(1, 0, 0), LocalTime.of(1, 40, 0)); 15 | LocalTimeRange range2 = new LocalTimeRange(LocalTime.of(2, 2, 30), LocalTime.of(2, 50, 25)); 16 | LocalTimeRange range3 = new LocalTimeRange(LocalTime.of(22, 59, 59), LocalTime.of(23, 0, 0)); 17 | 18 | LessonRanges lessonRanges = new LessonRanges(); 19 | lessonRanges.addRange(range1); 20 | lessonRanges.addRange(range2); 21 | lessonRanges.addRange(range3); 22 | 23 | String generated = lessonRanges.toString(); 24 | String expected = "1=01:00:00-01:40:00,2=02:02:30-02:50:25,3=22:59:59-23:00:00"; 25 | 26 | Assertions.assertEquals(expected, generated); 27 | } 28 | 29 | @Test 30 | public void testLoad() { 31 | String lessonRangeStr = "1=08:00:00-08:45:00," 32 | + "2=08:50:00-09:35:00," 33 | + "3=09:50:00-10:35:00," 34 | + "4=10:40:00-11:25:00," 35 | + "5=11:30:00-12:15:00," 36 | + "6=13:30:00-14:15:00," 37 | + "7=14:20:00-15:05:00," 38 | + "8=15:20:00-16:05:00," 39 | + "9=16:10:00-16:55:00," 40 | + "10=18:30:00-19:15:00," 41 | + "11=19:20:00-20:05:00," 42 | + "12=20:10:00-20:55:00"; 43 | 44 | LessonRanges lr = LessonRanges.load(lessonRangeStr); 45 | 46 | Assertions.assertEquals(lessonRangeStr, lr.toString()); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/App.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import java.io.*; 4 | import java.nio.charset.Charset; 5 | import java.nio.charset.StandardCharsets; 6 | 7 | import com.github.leafee98.CSTI.core.bean.ScheduleObject; 8 | import com.github.leafee98.CSTI.core.bean.loader.JSONLoader; 9 | import com.github.leafee98.CSTI.core.generate.Generator; 10 | import com.github.leafee98.CSTI.core.wrapper.schedule.BreakLine; 11 | 12 | public class App { 13 | 14 | private static final String paramIn = "-i"; 15 | private static final String paramOut = "-o"; 16 | 17 | private static String inputPath = "-"; 18 | private static String outputPath = "-"; 19 | 20 | private static InputStream input; 21 | private static OutputStream output; 22 | 23 | private static void parseParameter(String[] args) { 24 | for (int i = 0; i < args.length; ++i) { 25 | if (args[i].equals(paramIn)) { 26 | inputPath = args[++i]; 27 | } else if (args[i].equals(paramOut)) { 28 | outputPath = args[++i]; 29 | } 30 | } 31 | } 32 | 33 | private static void initIO() throws FileNotFoundException { 34 | if (inputPath.equals("-")) { 35 | input = System.in; 36 | } else { 37 | input = new FileInputStream(inputPath); 38 | } 39 | 40 | if (outputPath.equals("-")) { 41 | output = System.out; 42 | } else { 43 | output = new FileOutputStream(outputPath); 44 | } 45 | } 46 | 47 | public static void main(String[] args) throws IOException { 48 | parseParameter(args); 49 | initIO(); 50 | 51 | String confContent = new String(input.readAllBytes(), StandardCharsets.UTF_8); 52 | 53 | JSONLoader loader = new JSONLoader(); 54 | ScheduleObject scheduleObj = loader.load(confContent); 55 | 56 | Generator generator = new Generator(scheduleObj); 57 | String result = generator.generate().toString(); 58 | 59 | output.write(result.getBytes()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/Daylight.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | 6 | public class Daylight extends Component { 7 | 8 | public static final String TZOFFSETFROM = "TZOFFSETFROM"; 9 | public static final String TZOFFSETTO = "TZOFFSETTO"; 10 | public static final String TZNAME = "TZNAME"; 11 | public static final String DTSTART = "DTSTART"; 12 | public static final String RRULE = "RRULE"; 13 | 14 | private final Property tzOffsetFrom = new Property(TZOFFSETFROM); 15 | private final Property tzOffsetTo = new Property(TZOFFSETTO); 16 | private final Property tzName = new Property(TZNAME); 17 | private final Property dtStart = new Property(DTSTART); 18 | private final Property rRule = new Property(RRULE); 19 | 20 | public Daylight() { 21 | super("DAYLIGHT"); 22 | } 23 | 24 | public Property getTzOffsetFrom() { 25 | return tzOffsetFrom; 26 | } 27 | 28 | public Property getTzOffsetTo() { 29 | return tzOffsetTo; 30 | } 31 | 32 | public Property getTzName() { 33 | return tzName; 34 | } 35 | 36 | public Property getDtStart() { 37 | return dtStart; 38 | } 39 | 40 | public Property getRRule() { 41 | return rRule; 42 | } 43 | 44 | boolean isEmpty() { 45 | return tzOffsetFrom.isEmpty() 46 | && tzOffsetTo.isEmpty() 47 | && tzName.isEmpty() 48 | && dtStart.isEmpty() 49 | && rRule.isEmpty(); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | if (this.isEmpty()) return ""; 55 | 56 | StringBuilder builder = new StringBuilder(); 57 | builder.append("BEGIN:").append(getName()).append('\n'); 58 | 59 | if (! tzOffsetFrom.isEmpty()) builder.append(tzOffsetFrom.toString()).append('\n'); 60 | if (! tzOffsetTo.isEmpty()) builder.append(tzOffsetTo.toString()).append('\n'); 61 | if (! tzName.isEmpty()) builder.append(tzName.toString()).append('\n'); 62 | if (! dtStart.isEmpty()) builder.append(dtStart.toString()).append('\n'); 63 | if (! rRule.isEmpty()) builder.append(rRule.toString()).append('\n'); 64 | 65 | builder.append("END:").append(getName()); 66 | return builder.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/Standard.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | 6 | public class Standard extends Component { 7 | 8 | public static final String TZOFFSETFROM = "TZOFFSETFROM"; 9 | public static final String TZOFFSETTO = "TZOFFSETTO"; 10 | public static final String TZNAME = "TZNAME"; 11 | public static final String DTSTART = "DTSTART"; 12 | public static final String RRULE = "RRULE"; 13 | 14 | private final Property tzOffsetFrom = new Property(TZOFFSETFROM); 15 | private final Property tzOffsetTo = new Property(TZOFFSETTO); 16 | private final Property tzName = new Property(TZNAME); 17 | private final Property dtStart = new Property(DTSTART); 18 | private final Property rRule = new Property(RRULE); 19 | 20 | public Standard() { 21 | super("STANDARD"); 22 | } 23 | 24 | public Property getTzOffsetFrom() { 25 | return tzOffsetFrom; 26 | } 27 | 28 | public Property getTzOffsetTo() { 29 | return tzOffsetTo; 30 | } 31 | 32 | public Property getTzName() { 33 | return tzName; 34 | } 35 | 36 | public Property getDtStart() { 37 | return dtStart; 38 | } 39 | 40 | public Property getRRule() { 41 | return rRule; 42 | } 43 | 44 | boolean isEmpty() { 45 | return tzOffsetFrom.isEmpty() 46 | && tzOffsetTo.isEmpty() 47 | && tzName.isEmpty() 48 | && dtStart.isEmpty() 49 | && rRule.isEmpty(); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | if (this.isEmpty()) return ""; 55 | 56 | StringBuilder builder = new StringBuilder(); 57 | builder.append("BEGIN:").append(getName()).append('\n'); 58 | 59 | if (! tzOffsetFrom.isEmpty()) builder.append(tzOffsetFrom.toString()).append('\n'); 60 | if (! tzOffsetTo.isEmpty()) builder.append(tzOffsetTo.toString()).append('\n'); 61 | if (! tzName.isEmpty()) builder.append(tzName.toString()).append('\n'); 62 | if (! dtStart.isEmpty()) builder.append(dtStart.toString()).append('\n'); 63 | if (! rRule.isEmpty()) builder.append(rRule.toString()).append('\n'); 64 | 65 | builder.append("END:").append(getName()); 66 | return builder.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/VCalendar.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | import com.github.leafee98.CSTI.core.ics.Value; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class VCalendar extends Component { 11 | 12 | public static final String VCALENDAR = "VCALENDAR"; 13 | public static final String VTIMEZONE = "VTIMEZONE"; 14 | public static final String VEVENT = "VEVENT"; 15 | public static final String PRODID = "PRODID"; 16 | public static final String VERSION = "VERSION"; 17 | 18 | private final Property prodId = new Property(PRODID); 19 | private final Property version = new Property(VERSION); 20 | private VTimezone timeZone = new VTimezone(); 21 | private final List vEvents = new ArrayList<>(); 22 | 23 | public VCalendar() { 24 | super(VCALENDAR); 25 | this.getVersion().setValue(new Value("2.0")); 26 | this.getProdId().setValue(new Value("class-schedule-to-icalendar")); 27 | } 28 | 29 | public Property getProdId() { 30 | return prodId; 31 | } 32 | 33 | public Property getVersion() { 34 | return version; 35 | } 36 | 37 | public VTimezone getTimeZone() { 38 | return timeZone; 39 | } 40 | 41 | public void setTimeZone(VTimezone timeZone) { 42 | this.timeZone = timeZone; 43 | } 44 | 45 | public List getVEvents() { 46 | return vEvents; 47 | } 48 | 49 | public boolean isEmpty() { 50 | return prodId.isEmpty() 51 | && version.isEmpty() 52 | && timeZone.isEmpty() 53 | && vEvents.isEmpty(); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | if (this.isEmpty()) return ""; 59 | 60 | StringBuilder builder = new StringBuilder(); 61 | builder.append("BEGIN:").append(getName()).append('\n'); 62 | 63 | if (! prodId.isEmpty()) builder.append(prodId.toString()).append('\n'); 64 | if (! version.isEmpty()) builder.append(version.toString()).append('\n'); 65 | if (! timeZone.isEmpty()) builder.append(timeZone.toString()).append('\n'); 66 | 67 | for (VEvent event : vEvents) 68 | builder.append(event).append('\n'); 69 | 70 | builder.append("END:").append(getName()); 71 | return builder.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/BreakLineTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.wrapper.schedule.BreakLine; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class BreakLineTest { 8 | 9 | String done = "event-prefix:课-\n" 10 | + "timezone:+08:00\n" 11 | + "first-day-of-week:0\n" 12 | + "semester-start-date:2020-02-21\n" 13 | + "lesson-ranges:\n" 14 | + " 1=08:00:00-08:45:00,\n" 15 | + " 2=08:50:00-09:35:00,\n" 16 | + " 3=09:50:00-10:35:00,\n" 17 | + " 4=10:40:00-11:25:00,\n" 18 | + " 5=11:30:00-12:15:00,\n" 19 | + " 6=13:30:00-14:15:00,\n" 20 | + " 7=14:20:00-15:05:00,\n" 21 | + " 8=15:20:00-16:05:00,\n" 22 | + " 9=16:10:00-16:55:00,\n" 23 | + " 10=18:30:00-19:15:00,\n" 24 | + " 11=19:20:00-20:05:00,\n" 25 | + " 12=20:10:00-20:55:00\n" 26 | + "<<<\n" 27 | + "name:C语言课\n" 28 | + "type:专业必修课\n" 29 | + "teacher:某某大师\n" 30 | + "location:某校区某楼某层某室\n" 31 | + "remark:其实暂时没什么其他信息的啦\n" 32 | + "schedule:\n" 33 | + " x4,x5,x6,x7-x8,x9,x10|1|1,2,3;\n" 34 | + " x4,x5,x6,x7-x8,x9,x10|1|1,2,3;\n" 35 | + ">>>\n"; 36 | 37 | String origin = 38 | "event-prefix:课-\n" 39 | + "timezone:+08:00\n" 40 | + "first-day-of-week:0\n" 41 | + "semester-start-date:2020-02-21\n" 42 | + "lesson-ranges:1=08:00:00-08:45:00,2=08:50:00-09:35:00,3=09:50:00-10:35:00,4=10:40:00-11:25:00," + 43 | "5=11:30:00-12:15:00,6=13:30:00-14:15:00,7=14:20:00-15:05:00,8=15:20:00-16:05:00," + 44 | "9=16:10:00-16:55:00,10=18:30:00-19:15:00,11=19:20:00-20:05:00,12=20:10:00-20:55:00\n" 45 | + "<<<\n" 46 | + "name:C语言课\n" 47 | + "type:专业必修课\n" 48 | + "teacher:某某大师\n" 49 | + "location:某校区某楼某层某室\n" 50 | + "remark:其实暂时没什么其他信息的啦\n" 51 | + "schedule:x4,x5,x6,x7-x8,x9,x10|1|1,2,3;x4,x5,x6,x7-x8,x9,x10|1|1,2,3;\n" 52 | + ">>>\n"; 53 | 54 | @Test 55 | public void testDoBreak() { 56 | Assertions.assertEquals(done, BreakLine.doBreak(origin)); 57 | } 58 | 59 | @Test 60 | public void testRecovery() { 61 | Assertions.assertEquals(origin, BreakLine.recovery(done)); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/VariableSubstitution.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Lesson; 4 | import com.github.leafee98.CSTI.core.bean.LessonSchedule; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class VariableSubstitution { 10 | 11 | public static final String lessonName = "lessonName"; 12 | public static final String location = "location"; 13 | public static final String teacher = "teacher"; 14 | public static final String lessonType = "lessonType"; 15 | public static final String remark = "remark"; 16 | public static final String scheduleFull = "scheduleFull"; 17 | 18 | public static String doSubstitute(String input, Lesson lesson) { 19 | Matcher matcher1 = generatePattern(lessonName).matcher(input); 20 | String res1 = matcher1.replaceAll(lesson.getName()); 21 | 22 | Matcher matcher2 = generatePattern(location).matcher(res1); 23 | String res2 = matcher2.replaceAll(lesson.getLocation()); 24 | 25 | Matcher matcher3 = generatePattern(teacher).matcher(res2); 26 | String res3 = matcher3.replaceAll(lesson.getTeacher()); 27 | 28 | Matcher matcher4 = generatePattern(lessonType).matcher(res3); 29 | String res4 = matcher4.replaceAll(lesson.getType()); 30 | 31 | Matcher matcher5 = generatePattern(remark).matcher(res4); 32 | String res5 = matcher5.replaceAll(lesson.getRemark()); 33 | 34 | Matcher matcher6 = generatePattern(scheduleFull).matcher(res5); 35 | StringBuilder builder = new StringBuilder(); 36 | for (LessonSchedule s : lesson.getSchedule()) { 37 | // there is escape in methods replaceAll of matcher 38 | builder.append("\\\\n ").append(s); 39 | } 40 | String res6 = matcher6.replaceAll(builder.toString()); 41 | 42 | return suitEscape(res6); 43 | } 44 | 45 | private static Pattern generatePattern(String variableName) { 46 | String regVariableStart = "\\$\\{"; 47 | String regVariableEnd = "}"; 48 | 49 | return Pattern.compile("(? cal(Lesson lesson) { 32 | return cal(lesson.getSchedule()); 33 | } 34 | 35 | public List cal(List schedules) { 36 | List result = new ArrayList<>(); 37 | 38 | for (LessonSchedule schedule : schedules) { 39 | for (int week : schedule.getWeeks()) { 40 | LocalDate firstDay = semesterStartDate.plusDays((week - 1) * 7L); 41 | for (int day : schedule.getDayOfWeek()) { 42 | LocalDate lessonDay = firstDay.plusDays(calDayOffset(day)); 43 | 44 | for (PairNumber pair : RangeNumber.renderToPair(schedule.getLessons())) { 45 | LocalTime from = ranges.getRange(pair.getFirst()).from; 46 | LocalTime to = ranges.getRange(pair.getSecond()).to; 47 | 48 | LocalDateTimeRange dateTimeRange = new LocalDateTimeRange( 49 | LocalDateTime.of(lessonDay, from), 50 | LocalDateTime.of(lessonDay, to) 51 | ); 52 | result.add(dateTimeRange); 53 | } 54 | } 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | private int calDayOffset(int day) { 61 | return (day + 7 - firstDayOfWeek) % 7; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/loader/builder/GenericLessonBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean.loader.builder; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Lesson; 4 | import com.github.leafee98.CSTI.core.bean.LessonSchedule; 5 | import com.github.leafee98.CSTI.core.exceptions.InvalidLessonSchedule; 6 | import com.github.leafee98.CSTI.core.utils.RangeNumber; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class GenericLessonBuilder { 12 | private final Lesson lesson; 13 | 14 | public GenericLessonBuilder() { 15 | lesson = new Lesson(); 16 | 17 | // assign default value 18 | lesson.setName("default name"); 19 | lesson.setType("default type"); 20 | lesson.setTeacher("default teacher"); 21 | lesson.setLocation("default location"); 22 | lesson.setRemark("no remark"); 23 | lesson.setSchedule(new ArrayList<>()); 24 | } 25 | 26 | public Lesson build() { 27 | return lesson; 28 | } 29 | 30 | public void setName(String str) { 31 | lesson.setName(str); 32 | } 33 | 34 | public void setType(String str) { 35 | lesson.setType(str); 36 | } 37 | 38 | public void setTeacher(String str) { 39 | lesson.setTeacher(str); 40 | } 41 | 42 | public void setLocation(String str) { 43 | lesson.setLocation(str); 44 | } 45 | 46 | public void setRemark(String str) { 47 | lesson.setRemark(str); 48 | } 49 | 50 | public void setSchedule(List strList) { 51 | lesson.setSchedule(loadSchedule(strList)); 52 | } 53 | 54 | private List loadSchedule(List schedules) { 55 | List result = new ArrayList<>(); 56 | 57 | for (String scheduleStr : schedules) { 58 | scheduleStr = scheduleStr.trim(); 59 | if (scheduleStr.length() == 0) 60 | continue; 61 | 62 | String[] components = scheduleStr.split("\\|"); 63 | 64 | if (components.length != 3) 65 | throw new InvalidLessonSchedule("there should be 3 components (weeks, dayOfWeek, lessons)" + 66 | " in a schedule description: " + scheduleStr); 67 | 68 | for (int i = 0; i < 3; ++i) 69 | components[i] = components[i].trim(); 70 | 71 | LessonSchedule schedule = new LessonSchedule(); 72 | schedule.setWeeks(RangeNumber.parse(components[0])); 73 | schedule.setDayOfWeek(RangeNumber.parse(components[1])); 74 | schedule.setLessons(RangeNumber.parse(components[2])); 75 | result.add(schedule); 76 | } 77 | 78 | return result; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/LessonSchedule.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean; 2 | 3 | import com.github.leafee98.CSTI.core.exceptions.InvalidLessonSchedule; 4 | import com.github.leafee98.CSTI.core.utils.RangeNumber; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | public class LessonSchedule { 11 | 12 | private List weeks; 13 | private List dayOfWeek; 14 | private List lessons; 15 | 16 | public static List load(String str) { 17 | List result = new ArrayList<>(); 18 | String[] schedules = str.split(";"); 19 | 20 | for (String scheduleStr : schedules) { 21 | scheduleStr = scheduleStr.trim(); 22 | if (scheduleStr.length() == 0) 23 | continue; 24 | 25 | String[] components = scheduleStr.split("\\|"); 26 | 27 | if (components.length != 3) 28 | throw new InvalidLessonSchedule("there should be 3 components (weeks, dayOfWeek, lessons)" + 29 | " in a schedule description: " + scheduleStr); 30 | 31 | for (int i = 0; i < 3; ++i) 32 | components[i] = components[i].trim(); 33 | 34 | LessonSchedule schedule = new LessonSchedule(); 35 | schedule.setWeeks(RangeNumber.parse(components[0])); 36 | schedule.setDayOfWeek(RangeNumber.parse(components[1])); 37 | schedule.setLessons(RangeNumber.parse(components[2])); 38 | result.add(schedule); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return RangeNumber.render(this.getWeeks()) + 47 | '|' + 48 | RangeNumber.render(this.getDayOfWeek()) + 49 | '|' + 50 | RangeNumber.render(this.getLessons()) + 51 | ';'; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (!(o instanceof LessonSchedule)) return false; 58 | LessonSchedule schedule = (LessonSchedule) o; 59 | return Objects.equals(getWeeks(), schedule.getWeeks()) && 60 | Objects.equals(getDayOfWeek(), schedule.getDayOfWeek()) && 61 | Objects.equals(getLessons(), schedule.getLessons()); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(getWeeks(), getDayOfWeek(), getLessons()); 67 | } 68 | 69 | public List getWeeks() { 70 | return weeks; 71 | } 72 | 73 | public void setWeeks(List weeks) { 74 | this.weeks = weeks; 75 | } 76 | 77 | public List getDayOfWeek() { 78 | return dayOfWeek; 79 | } 80 | 81 | public void setDayOfWeek(List dayOfWeek) { 82 | this.dayOfWeek = dayOfWeek; 83 | } 84 | 85 | public List getLessons() { 86 | return lessons; 87 | } 88 | 89 | public void setLessons(List lessons) { 90 | this.lessons = lessons; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/Value.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics; 2 | 3 | import com.github.leafee98.CSTI.core.exceptions.InvalidComponentParams; 4 | import com.github.leafee98.CSTI.core.utils.CharacterEscape; 5 | 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Parameters put in this object will keep its origin order. 11 | * Take care of order while putting if you mind it. 12 | */ 13 | public class Value { 14 | 15 | private String value; 16 | private final Map parameters = new LinkedHashMap<>(); 17 | 18 | public Value() { 19 | this(""); 20 | } 21 | 22 | public Value(String value) { 23 | this.value = value; 24 | } 25 | 26 | public Value(String value, String... params) { 27 | this.value = value; 28 | if ((params.length & 1) != 1) { 29 | for (int i = 0; i < params.length; i += 2) 30 | this.putParameter(params[i], params[i + 1]); 31 | } else { 32 | throw new InvalidComponentParams("params must be \"Key1, Value1, Key2, Value2\" " + 33 | "and its length must be even."); 34 | } 35 | } 36 | 37 | public boolean isEmpty() { 38 | return value.length() == 0 && parameters.isEmpty(); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | StringBuilder builder = new StringBuilder(); 44 | for (Map.Entry entry : parameters.entrySet()) { 45 | builder.append(entry.getKey()); 46 | builder.append('='); 47 | builder.append(entry.getValue()); 48 | builder.append(';'); 49 | } 50 | if (value == null || value.length() == 0) { 51 | if (builder.length() > 0) 52 | return builder.substring(0, builder.length() - 1); 53 | else 54 | return ""; 55 | } else { 56 | builder.append(value); 57 | return builder.toString(); 58 | } 59 | } 60 | 61 | public String toStringEscape() { 62 | StringBuilder builder = new StringBuilder(); 63 | for (Map.Entry entry : parameters.entrySet()) { 64 | builder.append(entry.getKey()); 65 | builder.append('='); 66 | builder.append(entry.getValue()); 67 | builder.append(';'); 68 | } 69 | if (value == null || value.length() == 0) { 70 | if (builder.length() > 0) 71 | return builder.substring(0, builder.length() - 1); 72 | else 73 | return ""; 74 | } else { 75 | builder.append(CharacterEscape.escape(value)); 76 | return builder.toString(); 77 | } 78 | } 79 | 80 | public void putParameter(String param, String value) { 81 | parameters.put(param, value); 82 | } 83 | 84 | public String getParameter(String param) { 85 | return parameters.get(param); 86 | } 87 | 88 | public String getValue() { 89 | return value; 90 | } 91 | 92 | public void setValue(String value) { 93 | this.value = value; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/GeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core; 2 | 3 | import com.github.leafee98.CSTI.core.bean.ScheduleObject; 4 | import com.github.leafee98.CSTI.core.generate.Generator; 5 | import com.github.leafee98.CSTI.core.ics.model.VTimezone; 6 | import com.github.leafee98.CSTI.core.wrapper.schedule.BreakLine; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.time.ZoneId; 10 | 11 | public class GeneratorTest { 12 | 13 | private static String scheduleStr = 14 | "[[[\n" + 15 | "event-prefix:课-\n" + 16 | "reminder-time:-15m\n" + 17 | "timezone:Asia/Shanghai\n" + 18 | "first-day-of-week:5\n" + 19 | "semester-start-date:2020-02-21\n" + 20 | "lesson-ranges:\n" + 21 | " 1=08:00:00-08:45:00,\n" + 22 | " 2=08:50:00-09:35:00,\n" + 23 | " 3=09:50:00-10:35:00,\n" + 24 | " 4=10:40:00-11:25:00,\n" + 25 | " 5=11:30:00-12:15:00,\n" + 26 | " 6=13:30:00-14:15:00,\n" + 27 | " 7=14:20:00-15:05:00,\n" + 28 | " 8=15:20:00-16:05:00,\n" + 29 | " 9=16:10:00-16:55:00,\n" + 30 | " 10=18:30:00-19:15:00,\n" + 31 | " 11=19:20:00-20:05:00,\n" + 32 | " 12=20:10:00-20:55:00\n" + 33 | "]]]\n" + 34 | "\n" + 35 | "<<<\n" + 36 | "name:C语言课\n" + 37 | "type:专业必修课\n" + 38 | "teacher:某某大师\n" + 39 | "location:某校区某楼某层某室\n" + 40 | "remark:其实暂时没什么其他信息的啦\n" + 41 | "schedule:\n" + 42 | " 4,6-8,10|1|1-3;\n" + 43 | " 4-5,7-8,9,10|2|10-12;\n" + 44 | ">>>\n"; 45 | 46 | ScheduleObject schedule; 47 | 48 | public GeneratorTest() { 49 | this.schedule = ScheduleObject.load(BreakLine.recovery(scheduleStr)); 50 | } 51 | 52 | @Test 53 | public void testTimezone() { 54 | Generator generator = new Generator(schedule); 55 | VTimezone timezone = generator.generateVTimeZone(); 56 | System.out.println(timezone); 57 | } 58 | 59 | @Test 60 | public void testTimeZoneWithDST1() { 61 | Generator generator = new Generator(schedule); 62 | // schedule.getConfigure().setTimezone(ZoneId.of("Pacific/Easter")); 63 | schedule.getConfigure().setTimezone(ZoneId.of("America/Los_Angeles")); 64 | VTimezone timezone = generator.generateVTimeZone(); 65 | System.out.println(timezone); 66 | } 67 | 68 | @Test 69 | public void testTimeZoneWithDST2() { 70 | Generator generator = new Generator(schedule); 71 | schedule.getConfigure().setTimezone(ZoneId.of("Pacific/Fiji")); 72 | // schedule.getConfigure().setTimezone(ZoneId.of("America/Los_Angeles")); 73 | VTimezone timezone = generator.generateVTimeZone(); 74 | System.out.println(timezone); 75 | } 76 | @Test 77 | public void testFinalGenerate() { 78 | Generator generator = new Generator(schedule); 79 | schedule.getConfigure().setTimezone(ZoneId.of("Pacific/Fiji")); 80 | // schedule.getConfigure().setTimezone(ZoneId.of("America/Los_Angeles")); 81 | System.out.println(generator.generate()); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/ScheduleObject.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean; 2 | 3 | import com.github.leafee98.CSTI.core.configure.KeyWords; 4 | import com.github.leafee98.CSTI.core.exceptions.InvalidScheduleFileStruct; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class ScheduleObject { 10 | 11 | private List lessons; 12 | private Configure configure; 13 | 14 | public ScheduleObject() { 15 | } 16 | 17 | public static ScheduleObject load(String str) { 18 | ScheduleObject result = new ScheduleObject(); 19 | 20 | int confBegin = str.indexOf(KeyWords.confBegin); 21 | int confEnd = str.indexOf(KeyWords.confEnd); 22 | 23 | if( confBegin < 0 || confEnd < 0 || confBegin > confEnd 24 | || confBegin != str.lastIndexOf(KeyWords.confBegin) 25 | || confEnd != str.lastIndexOf(KeyWords.confEnd)) { 26 | throw new InvalidScheduleFileStruct(String.format("'%s' and '%s' must occur in order.", 27 | KeyWords.confBegin, KeyWords.confEnd)); 28 | } 29 | 30 | 31 | List lessons = new ArrayList<>(); 32 | int lessonBegin = str.indexOf(KeyWords.lessonBegin); 33 | int lessonEnd = str.indexOf(KeyWords.lessonEnd); 34 | while (lessonBegin > 0) { 35 | if (lessonBegin > lessonEnd) { 36 | throw new InvalidScheduleFileStruct(String.format("No '%s' after '%s', or '%s' is in front of '%s'\n" + 37 | "Take care that they must correspond with each other.", 38 | KeyWords.lessonEnd, KeyWords.lessonBegin, 39 | KeyWords.lessonBegin, KeyWords.lessonEnd)); 40 | } 41 | 42 | lessons.add(Lesson.load(str.substring(lessonBegin + KeyWords.lessonBegin.length() + 1, lessonEnd))); 43 | 44 | lessonBegin = str.indexOf(KeyWords.lessonBegin, lessonBegin + KeyWords.lessonBegin.length()); 45 | lessonEnd = str.indexOf(KeyWords.lessonEnd, lessonEnd + KeyWords.lessonEnd.length()); 46 | } 47 | 48 | result.setConfigure(Configure.load(str.substring(confBegin + KeyWords.confBegin.length() + 1, confEnd))); 49 | result.setLessons(lessons); 50 | return result; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | StringBuilder builder = new StringBuilder(KeyWords.confBegin + '\n' 56 | + this.configure.toString() + '\n' 57 | + KeyWords.confEnd + '\n'); 58 | for (Lesson lesson : lessons) { 59 | builder.append(KeyWords.lessonBegin); 60 | builder.append('\n'); 61 | builder.append(lesson.toString()); 62 | builder.append('\n'); 63 | builder.append(KeyWords.lessonEnd); 64 | builder.append('\n'); 65 | } 66 | 67 | return builder.toString(); 68 | } 69 | 70 | public List getLessons() { 71 | return lessons; 72 | } 73 | 74 | public void setLessons(List lessons) { 75 | this.lessons = lessons; 76 | } 77 | 78 | public Configure getConfigure() { 79 | return configure; 80 | } 81 | 82 | public void setConfigure(Configure configure) { 83 | this.configure = configure; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/Property.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics; 2 | 3 | import com.github.leafee98.CSTI.core.exceptions.InvalidComponentParams; 4 | 5 | import java.util.Map; 6 | import java.util.TreeMap; 7 | 8 | public class Property { 9 | public static final String SUMMARY = "SUMMARY"; 10 | public static final String DESCRIPTION = "DESCRIPTION"; 11 | public static final String LOCATION = "LOCATION"; 12 | public static final String ACTION = "ACTION"; 13 | public static final String TRANSP = "TRANSP"; 14 | public static final String PRODID = "PRODID"; 15 | public static final String TZID = "TZID"; 16 | public static final String TZNAME = "TZNAME"; 17 | public static final String UID = "UID"; 18 | 19 | private final String name; 20 | private final Map parameters = new TreeMap<>(); 21 | private Value value; 22 | 23 | public Property(String name, Value value) { 24 | this.name = name; 25 | this.value = value; 26 | } 27 | 28 | public Property(String name) { 29 | this(name, new Value("")); 30 | } 31 | 32 | public Property(String name, Value value, String... params) { 33 | this(name, value); 34 | if ((params.length & 1) != 1) { 35 | for (int i = 0; i < params.length; i += 2) 36 | this.putParameter(params[i], params[i + 1]); 37 | } else { 38 | throw new InvalidComponentParams("params must be \"Key1, Value1, Key2, Value2\" " + 39 | "and its length must be even."); 40 | } 41 | } 42 | 43 | public boolean isEmpty() { 44 | return value.isEmpty(); 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | 50 | 51 | 52 | StringBuilder builder = new StringBuilder(name); 53 | 54 | for (Map.Entry entry : parameters.entrySet()) { 55 | builder.append(';'); 56 | builder.append(entry.getKey()); 57 | builder.append('='); 58 | builder.append(entry.getValue()); 59 | } 60 | 61 | builder.append(':'); 62 | 63 | // escape value or not 64 | if (this.name.equals(SUMMARY) 65 | || this.name.equals(DESCRIPTION) 66 | || this.name.equals(LOCATION) 67 | || this.name.equals(ACTION) 68 | || this.name.equals(TRANSP) 69 | || this.name.equals(PRODID) 70 | || this.name.equals(TZID) 71 | || this.name.equals(TZNAME) 72 | || this.name.equals(UID)) 73 | builder.append(value.toStringEscape()); 74 | else 75 | builder.append(value); 76 | 77 | return builder.toString(); 78 | } 79 | 80 | public void putParameter(String name, String value) { 81 | this.parameters.put(name, value); 82 | } 83 | 84 | public String getParameter(String name) { 85 | return parameters.get(name); 86 | } 87 | 88 | public String getName() { 89 | return name; 90 | } 91 | 92 | public Value getValue() { 93 | return value; 94 | } 95 | 96 | public void setValue(String value) { 97 | this.setValue(new Value(value)); 98 | } 99 | 100 | public void setValue(Value value) { 101 | this.value = value; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/RangeNumber.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | import java.util.stream.IntStream; 6 | 7 | public class RangeNumber { 8 | 9 | /** 10 | * convert "1,2,5-7" to [1,2,5,6,7]. 11 | * (will sort elements in Set). 12 | * @param str string like "1,2,5-7" 13 | * @return List of Integer like [1,2,5,6,7] 14 | */ 15 | public static List parse(String str) { 16 | Set result = new TreeSet<>(); 17 | 18 | String[] numbs = str.split(","); 19 | for (String n : numbs) { 20 | int ind = n.indexOf('-'); 21 | if (ind > 0) { 22 | int from = Integer.parseInt(n.substring(0, ind)); 23 | int to = Integer.parseInt(n.substring(ind + 1)); 24 | result.addAll(IntStream.range(from, to + 1).boxed().collect(Collectors.toList())); 25 | } else { 26 | result.add(Integer.parseInt(n)); 27 | } 28 | } 29 | 30 | return new ArrayList<>(result); 31 | } 32 | 33 | /** 34 | * convert [1,2,5,6,7] to "1,2,5-7". 35 | * @param arr List of Integer like [1,2,5,6,7] 36 | * @return string like "1,2,5-7" 37 | */ 38 | public static String render(List arr) { 39 | int front = arr.get(0); 40 | int back = front; 41 | 42 | StringBuilder builder = new StringBuilder(); 43 | 44 | for (int i = 1; i < arr.size(); ++i) { 45 | if (arr.get(i - 1).equals(arr.get(i) - 1)) { 46 | back = arr.get(i); 47 | } else { 48 | if (front != back) { 49 | builder.append(front); 50 | builder.append('-'); 51 | } 52 | builder.append(back); 53 | builder.append(','); 54 | back = front = arr.get(i); 55 | } 56 | } 57 | 58 | if (front != back) { 59 | builder.append(front); 60 | builder.append('-'); 61 | } 62 | builder.append(back); 63 | builder.append(','); 64 | 65 | return builder.substring(0, builder.length() - 1); 66 | } 67 | 68 | /** 69 | * convert [1,2,5,6,7] to [{1},{2},{5,7}]. 70 | * @param arr List of Integer like [1,2,5,6,7] 71 | * @return string like [{1},{2},{5,7}] 72 | */ 73 | public static List renderToPair(List arr) { 74 | List result = new ArrayList<>(); 75 | int front, back; 76 | back = front = arr.get(0); 77 | for (int i = 1; i < arr.size(); ++i) { 78 | if (arr.get(i - 1).equals(arr.get(i) - 1)) { 79 | back = arr.get(i); 80 | } else { 81 | if (front != back) { 82 | result.add(new PairNumber(front, back)); 83 | } else { 84 | result.add(new PairNumber(front)); 85 | } 86 | back = front = arr.get(i); 87 | } 88 | } 89 | 90 | if (front != back) { 91 | result.add(new PairNumber(front, back)); 92 | } else { 93 | result.add(new PairNumber(front, front)); 94 | } 95 | 96 | return result; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/wrapper/schedule/BreakLine.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.wrapper.schedule; 2 | 3 | import com.github.leafee98.CSTI.core.configure.KeyWords; 4 | 5 | public class BreakLine { 6 | 7 | /** 8 | * Make lessonSchedule and lessonRanges output more friendly. 9 | * @param input Cschedule configuration without break line 10 | * @return formatted configuration 11 | */ 12 | 13 | public static String doBreak(String input) { 14 | return doBreak(input, true); 15 | } 16 | 17 | public static String doBreak(String input, boolean eightyCharPerLine) { 18 | StringBuilder builder = new StringBuilder(); 19 | String[] lines = input.split("\n"); 20 | for (String line : lines) { 21 | if (line.startsWith(KeyWords.lessonRanges)) { 22 | builder.append(handleLessonRanges(line)); 23 | } else if (line.startsWith(KeyWords.lessonSchedule)) { 24 | builder.append(handleLessonSchedule(line)); 25 | } else { 26 | if (eightyCharPerLine) { 27 | while (line.length() > 80) { 28 | // break line and start new line with a white space 29 | builder.append(line, 0, 79).append("\n "); 30 | line = line.substring(79); 31 | } 32 | } 33 | builder.append(line); 34 | } 35 | builder.append('\n'); 36 | } 37 | return builder.toString(); 38 | } 39 | 40 | private static String BreakLineByStr(String line, String c) { 41 | int colonIndex = line.indexOf(':'); 42 | StringBuilder builder = new StringBuilder(line.substring(0, colonIndex + 1)); 43 | String[] contents = line.substring(colonIndex + 1).split(c); 44 | for (String content : contents) { 45 | builder.append("\n "); 46 | builder.append(content); 47 | builder.append(c); 48 | } 49 | return builder.substring(0, builder.length()); 50 | } 51 | 52 | private static String handleLessonRanges(String line) { 53 | String res = BreakLineByStr(line, ","); 54 | return res.substring(0, res.length() - 1); 55 | } 56 | 57 | private static String handleLessonSchedule(String line) { 58 | return BreakLineByStr(line, ";"); 59 | } 60 | 61 | /** 62 | * Undo the break line to simplify parsing Cschedule. 63 | * The first space character will be ignored is it is at the first line. 64 | * (The trailing space characters will be kept, for those configuration options like event-name) 65 | * (Will append a blank line at the end.) 66 | * @param input Cschedule configuration with break line 67 | * @return string without break line. 68 | */ 69 | public static String recovery(String input) { 70 | StringBuilder builder = new StringBuilder(); 71 | String[] lines = input.split("\n"); 72 | for (String line : lines) { 73 | // remove the trailing spaces 74 | String verifyBlankLine = line.replaceAll("\\s+$", ""); 75 | if (verifyBlankLine.length() == 0) 76 | continue; 77 | 78 | if (line.startsWith(" ")) { 79 | builder.append(line.substring(1)); 80 | } else { 81 | builder.append('\n'); 82 | builder.append(line); 83 | } 84 | } 85 | builder.append('\n'); 86 | return builder.substring(1); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /doc/csti-json.md: -------------------------------------------------------------------------------- 1 | # JSON 格式课程表描述文件 2 | 3 | 描述文件遵守 JSON 文件格式, 完整的例子见 [例子2](/doc/example/e2/e2.json) 4 | 5 | ## 文件形式 6 | 7 | ``` 8 | { 9 | "global": {}, 10 | "lessons": [] 11 | } 12 | ``` 13 | 14 | global: [global 对象](#global-对象) 15 | 16 | lessons: [lessons 数组](#lessons-数组) 17 | 18 | ### global 对象 19 | 20 | ``` 21 | { 22 | "event-summary-format": "${lessonName}-${location}", 23 | "event-description-format": "name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull}", 24 | "timezone": "Asia/Shanghai", 25 | "first-day-of-week": 1, 26 | "semester-start-date": "2020-02-24", 27 | "reminder-time": ["-15m"], 28 | "lesson-ranges": [ 29 | "1=08:00:00-08:45:00", 30 | "2=08:50:00-09:35:00", 31 | ] 32 | } 33 | ``` 34 | 35 | 描述课程表的全局配置, 包括生成日历的事件描述格式和时区以及课程时间相关的配置. 36 | 37 | |属性|描述| 38 | |---|---| 39 | | `event-summary-format` | 字符串, 描述生成的事件的名称, 支持[变量代换](#变量代换).| 40 | | `event-description-format` | 字符串, 描述生成的事件的详细描述, 支持[变量代换](#变量代换). 41 | | `timezone` | 字符串, 描述时区, 要求是能够被 Java 识别的字符串.| 42 | | `first-day-of-week` | 整数, 范围 `[0-7]`, `0` 和 `7` 都表示星期日.| 43 | | `semester-start-date` | 字符串, 以 `yyyy-mm-dd` 的格式描述开学的那一天, 这一天会所在的周会被作为学期的第一周并开始安排课程.| 44 | | `reminder-time` | 事件的提醒时间, 使用 `+` 或 `-` 号表示在**事件开始**之后或之前的一段时间提醒, `-` 号表示之前.| 45 | | `lesson-ranges` | JSON 数组, 每一个元素均为字符串, 描述每一节课程的起止时间, 格式为 `课程节号=上课时间-下课时间`, 每天有多节课程则需要多个字符串描述.| 46 | 47 | > `lesson-ranges` 中一个字符串的样例: 48 | > 49 | > `1=08:00:00-08:45:00`, 表示每天的第 `1` 节课自 `08:00:00` 开始, 直到 `08:45:00` 时下课. 50 | 51 | 各个属性的默认值如下 52 | 53 | |属性|默认值| 54 | |---|---| 55 | |`event-summary-format`|`"${lessonName}-${location}"`| 56 | |`event-description-format`|`"name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull}"`| 57 | |`timezone`|`Asia/Shanghai`| 58 | |`first-day-of-week`|`1`| 59 | |`semester-start-date`|无(必填)| 60 | |`reminder-time`|`-15m`| 61 | |`lesson-ranges`|无(必填)| 62 | 63 | ### lessons 数组 64 | 65 | 数组中每一个元素均为一个描述课程信息的对象, 该对象格式如下: 66 | 67 | ``` 68 | { 69 | "name": "软件测试技术", 70 | "type": "专业必修", 71 | "teacher": "某教师", 72 | "location": "某位置", 73 | "remark": "暂无", 74 | "schedule": [ 75 | "1-14|1|6-9" 76 | ] 77 | } 78 | ``` 79 | 80 | |属性|描述|默认值| 81 | |---|---|---| 82 | | `name` | 字符串, 描述课程名称.|无(必填)| 83 | | `type` | 字符串, 描述课程的类型, 如专业选修课/专业必修课.|`""`| 84 | | `teacher` | 字符串, 该课程的任课教师.|`""`| 85 | | `location` | 字符串, 描述上课位置.|`""`| 86 | | `remark` | 字符串, 备注, 用来记录一些其他的信息.|`""`| 87 | | `schedule` | JSON 数组, 每个元素为一个字符串, 描述上课的周次/星期几/课程的起止节数. 具体格式为 `周次|星期几|课程起止节数` , 三个区域分别可以使用逗号分隔符表示单独添加, 或使用连字符表示范围. 字符串可以有多个, 描述多个不同的规则.|无(必填) 88 | 89 | > 字符串中使用逗号或连字符的例子: 90 | > 91 | > `1,3-5|2,4|1-3,9` 表示上课周次为第 1 周和第 3,4,5 周, 每周的周二和周四, 上课的时间为第 1,2,3 节, 还有第 9 节. 92 | > 93 | > 整个数组有多个字符串的例子: 94 | > 95 | > `["1-3,6-9|1|1-3", "4,5|2|4-5"]` 表示第 1 到第 3 周, 第 6 周到第 9 周, 每周一在第 1 到 3 节上课, 还有第 4 到第 5 周, 每周二在第 4 到第 5节上课 96 | 97 | ## 变量代换 98 | 99 | ``` 100 | ${lessonName} 课程的 name 属性 101 | ${location} 课程的 location 属性 102 | ${teacher} 课程的 teacher 属性 103 | ${lessonType} 课程的 type 属性 104 | ${remark} 课程的 remark 属性 105 | ${scheduleFull} 课程的 schedule 完整属性 106 | ``` 107 | 108 | 暂未实现的变量替换 109 | 110 | ``` 111 | ${weekInfo} 本节课程发生时的当前周次 112 | ${schedule} 本节课程的起止课程节 113 | ```` 114 | 115 | ## 格式定义中特殊字符转义 116 | 117 | 建议遵守以下转义规则, 否则可能会出现意料之外的结果 118 | 119 | 除开换行有 JSON 进行转义以外, 其他几个需要额外使用一次斜杠来将斜杠成功传达到变量代换阶段 120 | 121 | - 换行符号需要使用 `\n` 进行转义 (由 JSON 进行处理) 122 | - `$` 符号需要使用 `\\$` 进行转义 123 | - `{` 符号需要使用 `\\{` 进行转义 124 | - `}` 符号需要使用 `\\}` 进行转义 [^1] 125 | -------------------------------------------------------------------------------- /src/test/java/com/github/leafee98/CSTI/core/bean/loader/JSONLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean.loader; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Configure; 4 | import com.github.leafee98.CSTI.core.bean.Lesson; 5 | import com.github.leafee98.CSTI.core.bean.LessonSchedule; 6 | import com.github.leafee98.CSTI.core.bean.ScheduleObject; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.io.*; 11 | import java.nio.charset.Charset; 12 | import java.nio.charset.StandardCharsets; 13 | import java.time.LocalDate; 14 | import java.time.LocalTime; 15 | import java.time.ZoneId; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.stream.Collectors; 19 | import java.util.stream.IntStream; 20 | 21 | public class JSONLoaderTest { 22 | private final String jsonResource = "csti-example.json"; 23 | 24 | @Test 25 | public void testLoad() throws IOException { 26 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 27 | InputStream inputStream = classLoader.getResourceAsStream(jsonResource); 28 | assert inputStream != null; 29 | InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); 30 | String inputText = new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n")); 31 | 32 | JSONLoader loader = new JSONLoader(); 33 | ScheduleObject scheduleObject = loader.load(inputText); 34 | Configure configure = scheduleObject.getConfigure(); 35 | List lessons = scheduleObject.getLessons(); 36 | Lesson lesson = lessons.get(0); 37 | 38 | // Assert global configuration 39 | 40 | Assertions.assertEquals("${lessonName}-${location}", configure.getEventSummaryFormat()); 41 | Assertions.assertEquals("name:${lessonName}\n" 42 | + "location:${location}\n" 43 | + "teacher:${teacher}\n" 44 | + "type:${lessonType}\n" 45 | + "remark:${remark}\n" 46 | + "schedule:${scheduleFull}", configure.getEventDescriptionFormat()); 47 | Assertions.assertEquals(ZoneId.of("Asia/Shanghai"), configure.getTimezone()); 48 | Assertions.assertEquals(1, configure.getFirstDayOfWeek()); 49 | Assertions.assertEquals(LocalDate.of(2020, 2, 24), configure.getSemesterStartDate()); 50 | 51 | Assertions.assertEquals(1, configure.getReminderTime().size()); 52 | Assertions.assertEquals("-15m", configure.getReminderTime().get(0)); 53 | 54 | Assertions.assertEquals(12, configure.getLessonRanges().size()); 55 | Assertions.assertEquals(LocalTime.of(8, 0, 0), configure.getLessonRanges().getRange(1).from); 56 | Assertions.assertEquals(LocalTime.of(8, 45, 0), configure.getLessonRanges().getRange(1).to); 57 | Assertions.assertEquals(LocalTime.of(20, 10, 0), configure.getLessonRanges().getRange(12).from); 58 | Assertions.assertEquals(LocalTime.of(20, 55, 0), configure.getLessonRanges().getRange(12).to); 59 | 60 | // Assert lessons 61 | 62 | Assertions.assertEquals(3, lessons.size()); 63 | Assertions.assertEquals("软件测试技术", lesson.getName()); 64 | Assertions.assertEquals("专业必修", lesson.getType()); 65 | Assertions.assertEquals("某教师", lesson.getTeacher()); 66 | Assertions.assertEquals("某位置", lesson.getLocation()); 67 | Assertions.assertEquals("暂无", lesson.getRemark()); 68 | 69 | LessonSchedule schedule = lesson.getSchedule().get(0); 70 | Assertions.assertEquals(1, lesson.getSchedule().size()); 71 | Assertions.assertEquals(IntStream.range(1, 15).boxed().collect(Collectors.toList()), schedule.getWeeks()); 72 | Assertions.assertEquals(Collections.singletonList(1), schedule.getDayOfWeek()); 73 | Assertions.assertEquals(IntStream.range(6, 10).boxed().collect(Collectors.toList()), schedule.getLessons()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/LessonRanges.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean; 2 | 3 | import java.time.LocalTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.time.format.DateTimeParseException; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.IntStream; 11 | 12 | import com.github.leafee98.CSTI.core.exceptions.InvalidLessonRange; 13 | import com.github.leafee98.CSTI.core.utils.LocalTimeRange; 14 | 15 | public class LessonRanges { 16 | 17 | private final ArrayList ranges; 18 | 19 | /** 20 | * @param scheduleStr should like the follows, the whitespaces will not be 21 | * checked there: 22 | * 1=01:00:00-01:40:00,2=02:02:30-02:50:25,3=22:59:59-23:00:00 23 | * 24 | * @return a LessonRanges object generated from {@code scheduleStr} 25 | * 26 | * @throws InvalidLessonRange 27 | */ 28 | public static LessonRanges load(String scheduleStr) { 29 | LessonRanges lr = new LessonRanges(); 30 | 31 | HashMap map = new HashMap<>(); 32 | String[] list = scheduleStr.split(","); 33 | 34 | for (String str : list) { 35 | str = str.trim(); 36 | 37 | if (str.length() <= 0) 38 | continue; 39 | 40 | // str: 1=01:00:00-01:40:00 41 | String[] components = str.split("[=-]"); 42 | 43 | if (components.length != 3) { 44 | throw new InvalidLessonRange("invalid description of lesson range: " + str); 45 | } 46 | 47 | for (int i = 0; i < 3; ++i) 48 | components[i] = components[i].trim(); 49 | 50 | try { 51 | int lessonIndex = Integer.parseInt(components[0]); 52 | LocalTime from = LocalTime.parse(components[1]); 53 | LocalTime to = LocalTime.parse(components[2]); 54 | 55 | map.put(lessonIndex, new LocalTimeRange(from, to)); 56 | } catch (NumberFormatException e) { 57 | throw new InvalidLessonRange("lesson index is not a valid number: " + components[0], e); 58 | } catch (DateTimeParseException e) { 59 | throw new InvalidLessonRange("lesson range time is not a valid time: " 60 | + components[1] + '-' + components[2], e); 61 | } 62 | } 63 | 64 | List lessonNumbers = IntStream.range(1, map.keySet().size()).boxed().collect(Collectors.toList()); 65 | if (! map.keySet().containsAll(lessonNumbers)) { 66 | throw new InvalidLessonRange("the lesson indexes are not from 1 to n"); 67 | } 68 | 69 | for (int lessonIndex : map.keySet()) 70 | lr.addRange(map.get(lessonIndex)); 71 | 72 | return lr; 73 | } 74 | 75 | public LessonRanges() { 76 | this.ranges = new ArrayList<>(); 77 | } 78 | 79 | public void addRange(LocalTimeRange t) { 80 | ranges.add(t); 81 | } 82 | 83 | public void reset() { 84 | ranges.clear(); 85 | } 86 | 87 | public LocalTimeRange getRange(int i) { 88 | return ranges.get(i - 1); 89 | } 90 | 91 | public int size() { 92 | return ranges.size(); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); 98 | StringBuilder builder = new StringBuilder(); 99 | 100 | boolean comma = false; 101 | for (int i = 0; i < ranges.size(); ++i) { 102 | if (comma) { 103 | builder.append(','); 104 | } else { 105 | comma = true; 106 | } 107 | 108 | LocalTimeRange range = ranges.get(i); 109 | builder.append(i + 1); 110 | builder.append('='); 111 | builder.append(range.from.format(formatter)); 112 | builder.append('-'); 113 | builder.append(range.to.format(formatter)); 114 | } 115 | 116 | return builder.toString(); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/Lesson.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean; 2 | 3 | import com.github.leafee98.CSTI.core.configure.KeyWords; 4 | 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | public class Lesson { 9 | 10 | private String name; 11 | private String type; 12 | private String teacher; 13 | private String location; 14 | private String remark; 15 | private List schedule; 16 | 17 | public static Lesson load(String scheduleStr) { 18 | Lesson result = new Lesson(); 19 | String[] lines = scheduleStr.split("\n"); 20 | 21 | for (String line : lines) { 22 | if (line.startsWith(KeyWords.lessonName)) { 23 | result.setName(line.substring(KeyWords.lessonName.length() + 1).trim()); 24 | } else if (line.startsWith(KeyWords.lessonType)) { 25 | result.setType(line.substring(KeyWords.lessonType.length() + 1).trim()); 26 | } else if (line.startsWith(KeyWords.lessonTeacher)) { 27 | result.setTeacher(line.substring(KeyWords.lessonTeacher.length() + 1).trim()); 28 | } else if (line.startsWith(KeyWords.lessonLocation)) { 29 | result.setLocation(line.substring(KeyWords.lessonLocation.length() + 1).trim()); 30 | } else if (line.startsWith(KeyWords.lessonRemark)) { 31 | result.setRemark(line.substring(KeyWords.lessonRemark.length() + 1).trim()); 32 | } else if (line.startsWith(KeyWords.lessonSchedule)) { 33 | result.setSchedule(LessonSchedule.load(line.substring(KeyWords.lessonSchedule.length() + 1))); 34 | } 35 | } 36 | 37 | return result; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | StringBuilder builder = new StringBuilder( 43 | KeyWords.lessonName + ':' + name + '\n' 44 | + KeyWords.lessonType + ':' + type + '\n' 45 | + KeyWords.lessonTeacher + ':' + teacher + '\n' 46 | + KeyWords.lessonLocation + ':' + location + '\n' 47 | + KeyWords.lessonRemark + ':' + remark + '\n' 48 | + KeyWords.lessonSchedule + ':' 49 | ); 50 | 51 | for (LessonSchedule s : schedule) 52 | builder.append(s); 53 | 54 | return builder.toString(); 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if (this == o) return true; 60 | if (!(o instanceof Lesson)) return false; 61 | Lesson lesson = (Lesson) o; 62 | return Objects.equals(getName(), lesson.getName()) && 63 | Objects.equals(getType(), lesson.getType()) && 64 | Objects.equals(getTeacher(), lesson.getTeacher()) && 65 | Objects.equals(getRemark(), lesson.getRemark()) && 66 | Objects.equals(getSchedule(), lesson.getSchedule()); 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | return Objects.hash(getName(), getType(), getTeacher(), getRemark(), getSchedule()); 72 | } 73 | 74 | public String getName() { 75 | return name; 76 | } 77 | 78 | public void setName(String name) { 79 | this.name = name; 80 | } 81 | 82 | public String getType() { 83 | return type; 84 | } 85 | 86 | public void setType(String type) { 87 | this.type = type; 88 | } 89 | 90 | public String getTeacher() { 91 | return teacher; 92 | } 93 | 94 | public void setTeacher(String teacher) { 95 | this.teacher = teacher; 96 | } 97 | 98 | public String getLocation() { 99 | return location; 100 | } 101 | 102 | public void setLocation(String location) { 103 | this.location = location; 104 | } 105 | 106 | public String getRemark() { 107 | return remark; 108 | } 109 | 110 | public void setRemark(String remark) { 111 | this.remark = remark; 112 | } 113 | 114 | public List getSchedule() { 115 | return schedule; 116 | } 117 | 118 | public void setSchedule(List schedule) { 119 | this.schedule = schedule; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /doc/csti-define(deprecated).md: -------------------------------------------------------------------------------- 1 | # 课程表定义文件 格式描述 (deprecated) 2 | 3 | ## 格式要求 4 | 5 | 每一个以单个空格为起始的行都被认作时上一行的继续, 在处理时会先移除行首的一个空格,再进行行的合并. 6 | 7 | > 如果需要进行断行, 只需要在新的一行以一个空格作为起始即可. 8 | > 9 | > 如果新的一行以多个空格起始, 则在合并上一行时会保留除第一个空格以外的空格 10 | > 11 | > `tab` 起始的行**不**被视作上一行的继续 12 | 13 | 全局配置的选项必须以 `[[[` 和 `]]]` 进行包括, 且两个三括号必须位于单独的一行, 不得紧接其他配置内容. 14 | 15 | 每节课程的配置必须以 `<<<` 和 `>>>` 进行包括, 且两个三括号必须位于单独的一行, 不得紧接其他配置内容. 16 | 17 | 在 `[[[` 和 `]]]` 或 `<<<` 和 `>>>` 之外的内容会被忽略, 即可以将其他说明性文字写在三括号之外 18 | 19 | 如果需要使用配置选项的默认值, 则需要直接在配置文件中移除该选项的配置, 或将此项配置留空 20 | 21 | ## 全局配置 22 | 23 | ### `event-summary-format` 24 | 25 | 定义生成事件的名称格式, 支持进行[变量替换](#变量替换), 同是也要注意[格式定义中特殊字符转义](#格式定义中特殊字符转义), 默认值为 `${lessonName}-${location}` 26 | 27 | 28 | ### `event-description-format` 29 | 30 | 定义生成事件的描述格式, 格式要求同[事件名称格式的定义](#event-summary-format), 默认值为 `name:${lessonName}\nlocation:${location}\nteacher:${teacher}\ntype:${lessonType}\nremark:${remark}\nschedule:${scheduleFull}` 31 | 32 | ### `timezone` 33 | 34 | 时区设置, 值为一串字符串, 内部实现是借助于 `Java.time` 来获取夏令时信息, 所以要求可以被Java的 `ZoneId` 解析到正确的时区 35 | 36 | ### `first-day-of-week` 37 | 38 | 定义每周的第一天为周几, 通常西方使用周日作为每周的第一天, 但是东方则是一般使用周一作为每周的第一天. 39 | 40 | 此处的值为一个整数, 范围是 `0-7`, 其中 `0` 和 `7` 都代表周日 41 | 42 | 默认会使用与 `semester-start-date` 相吻合的值. 43 | 44 | ### `semester-start-date` 45 | 46 | 定义学期的第一天, 格式定为 `yyyy-MM-dd`, 注意这一天的与 `first-day-of-week` 应该相吻合 47 | 48 | ### `reminder-time` 49 | 50 | 定义课程的提醒时间, 整数表示延后, 负数表示提前, 单位支持 `s`\\`m` 和 `h`, 分别表示提前或者延后的秒\\分\\时. 51 | 52 | 可以填写多个值, 其间使用逗号分隔, 表示对于同一节课提醒多次. 53 | 54 | > 比如 `-1h,-15m` 表示在上课前1小时和前15分钟提醒两次. 55 | 56 | ### `lesson-ranges` 57 | 58 | 定义每节课的起止时间, 格式为 `1=08:00:00-08:45:00`, 表示每天的第1节课自08:00:00开始, 直到08:45:00时下课. 59 | 60 | 可以写多个定义, 每个定义之间通过逗号分隔, 格式中 `=` 前的数字可以不按照递增规律进行编写, 但是所有定义最后一定要是从1到n不重不漏. 比如下面两个定义就是可以被接受并且等价的, 注意这里为了可读性套用了以空格起始的行视为上一行的延续的规则. 61 | 62 | ``` 63 | lesson-ranges: 64 | 1=08:00:00-08:45:00, 65 | 3=09:50:00-10:35:00, 66 | 2=08:50:00-09:35:00, 67 | 4=10:40:00-11:25:00, 68 | 5=11:30:00-12:15:00 69 | ``` 70 | 71 | ``` 72 | lesson-ranges: 73 | 1=08:00:00-08:45:00, 74 | 2=08:50:00-09:35:00, 75 | 3=09:50:00-10:35:00, 76 | 4=10:40:00-11:25:00, 77 | 5=11:30:00-12:15:00 78 | ``` 79 | 80 | ## 单个课程配置 81 | 82 | ### `name` 83 | 84 | 课程的名称 85 | 86 | ### `type` 87 | 88 | 课程的类型 89 | 90 | ### `teacher` 91 | 92 | 课程的授课教师 93 | 94 | ### `location` 95 | 96 | 上课的地点, 此项属性会被填充到 `ics` 文件的事件中 `LOCATION` 的位置 97 | 98 | ### `remark` 99 | 100 | 备注, 可以记录其他一些信息 101 | 102 | ### `schedule` 103 | 104 | 课程的安排时间定义. 105 | 106 | 每一条安排时间定义被划分为三个部分, 即**周次定义**, **周天定义**, **课程节数安排**. 中间以竖线(`|`)分隔, 每个课程安排定义以分号(`;`)结尾. 107 | 108 | 三个部分中每一个部分都是一个列表, 可以填写数字范围 `1-15` 表示从 `1` 到 `15` 的每一个数字, 也可以直接写一个数字如 `4`, 数字和数字范围可以混用并多用, 比如 `1,3,5-9` 和 `1,3,5,6,7,8,9` 等价. 109 | 110 | 假设全局配置的每节课程起止时间和课程安排时间分别如下(仅提出关键配置, 未遵守文件结构), 则表示此课程安排在在 `第4\6\7\8\10周的周1的第1到3节课程`, 和 `第4\5\7\8\9\10周的周2的第10到第12节`. 生成结果中的课程实际起止时间为 `08:00:00` 到 `10:35:00`, 和 `18:30:00` 到 `20:55:00`. 111 | 112 | ``` 113 | lesson-ranges: 114 | 1=08:00:00-08:45:00, 115 | 2=08:50:00-09:35:00, 116 | 3=09:50:00-10:35:00, 117 | 4=10:40:00-11:25:00, 118 | 5=11:30:00-12:15:00 119 | 6=13:30:00-14:15:00, 120 | 7=14:20:00-15:05:00, 121 | 8=15:20:00-16:05:00, 122 | 9=16:10:00-16:55:00, 123 | 10=18:30:00-19:15:00, 124 | 11=19:20:00-20:05:00, 125 | 12=20:10:00-20:55:00 126 | 127 | ... 128 | 129 | schedule: 130 | 4,6-8,10|1|1-3; 131 | 4-5,7-8,9,10|2|10-12; 132 | 133 | ``` 134 | 135 | 在生成结果中的课程实际起止时间为全局配置中对应此课程的第一节课的开始时间到全局配置中对应此课程的最后一节课的结束时间. 即**同一定义中连续安排的课程会被生成为一个事件**. 136 | 137 | > 注意三个部分中的列表即便写作 `3-5,6-8`, 其效果也是和 `3-8`是等价的, 最终会生成为一个事件 138 | > 139 | > 如果在第三个部分课次定义中需要强调是先上 `3-5` 再上 `6-8`, 将其分为两个事件, 则需要将其分开在两个课程安排定义中, 如下: 140 | > 141 | > ``` 142 | > schedule: 143 | > 1-3|1|3-8; 144 | > 145 | > 改为 146 | > 147 | > schedule: 148 | > 1-3|1|3-5; 149 | > 1-3|1|6-8; 150 | > ``` 151 | 152 | ## 变量替换 153 | 154 | ``` 155 | ${lessonName} 课程的 name 属性 156 | ${location} 课程的 location 属性 157 | ${teacher} 课程的 teacher 属性 158 | ${lessonType} 课程的 type 属性 159 | ${remark} 课程的 remark 属性 160 | ${scheduleFull} 课程的 schedule 完整属性 161 | ``` 162 | 163 | 暂未实现的变量替换 164 | 165 | ``` 166 | ${weekInfo} 本节课程发生时的当前周次 167 | ${schedule} 本节课程的起止课程节 168 | ```` 169 | 170 | ## 格式定义中特殊字符转义 171 | 172 | 建议遵守以下转义规则, 否则可能会出现意料之外的结果 173 | 174 | - 换行符号需要使用 `\n` 进行转义 175 | - `$` 符号需要使用 `\$` 进行转义 176 | - `{` 符号需要使用 `\{` 进行转义 177 | - `}` 符号需要使用 `\}` 进行转义 178 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.github.leafee98 8 | class-schedule-to-icalendar-core 9 | v0.3.0 10 | 11 | class schedule to icalendar core 12 | 13 | 14 | UTF-8 15 | 11 16 | 11 17 | 18 | 19 | 20 | 21 | 22 | org.junit.jupiter 23 | junit-jupiter-engine 24 | 5.6.2 25 | test 26 | 27 | 28 | 29 | 30 | org.json 31 | json 32 | 20230227 33 | 34 | 35 | 36 | 37 | 38 | 39 | maven-assembly-plugin 40 | 3.3.0 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | maven-clean-plugin 50 | 3.1.0 51 | 52 | 53 | 54 | 55 | maven-resources-plugin 56 | 3.0.2 57 | 58 | 59 | 60 | maven-compiler-plugin 61 | 3.8.0 62 | 63 | 64 | 65 | maven-surefire-plugin 66 | 2.22.1 67 | 68 | 69 | 70 | maven-jar-plugin 71 | 3.0.2 72 | 73 | 74 | 75 | com.github.leafee98.CSTI.core.App 76 | 77 | 78 | 79 | 80 | 81 | 82 | maven-assembly-plugin 83 | 84 | 85 | 86 | com.github.leafee98.CSTI.core.App 87 | 88 | 89 | 90 | jar-with-dependencies 91 | 92 | 93 | 94 | 95 | make-assembly 96 | package 97 | 98 | single 99 | 100 | 101 | 102 | 103 | 104 | 105 | maven-install-plugin 106 | 2.5.2 107 | 108 | 109 | 110 | maven-deploy-plugin 111 | 2.8.2 112 | 113 | 114 | 115 | 116 | maven-site-plugin 117 | 3.7.1 118 | 119 | 120 | maven-project-info-reports-plugin 121 | 3.0.0 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/loader/builder/GenericConfigureBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean.loader.builder; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Configure; 4 | import com.github.leafee98.CSTI.core.bean.LessonRanges; 5 | import com.github.leafee98.CSTI.core.exceptions.InvalidLessonRange; 6 | import com.github.leafee98.CSTI.core.utils.LocalTimeRange; 7 | 8 | import java.time.LocalDate; 9 | import java.time.LocalTime; 10 | import java.time.ZoneId; 11 | import java.time.format.DateTimeParseException; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.TreeMap; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.IntStream; 18 | 19 | public class GenericConfigureBuilder { 20 | private final Configure configure; 21 | 22 | /** 23 | * create new Configure and assign default value of Configure 24 | */ 25 | public GenericConfigureBuilder() { 26 | configure = new Configure(); 27 | 28 | // two properties have no default value 29 | configure.setEventSummaryFormat("${lessonName}-${location}"); 30 | configure.setEventDescriptionFormat( 31 | "name:${lessonName}\n" + 32 | "location:${location}\n" + 33 | "teacher:${teacher}\n" + 34 | "type:${lessonType}\n" + 35 | "remark:${remark}\n" + 36 | "schedule:${scheduleFull}"); 37 | configure.setTimezone(ZoneId.of("Asia/Shanghai")); 38 | configure.setFirstDayOfWeek(1); 39 | // configure.setSemesterStartDate(); 40 | configure.setReminderTime(Collections.singletonList("-15m")); 41 | // configure.setLessonRanges(); 42 | } 43 | 44 | /** 45 | * @return the result built 46 | */ 47 | public Configure build() { 48 | return configure; 49 | } 50 | 51 | public void setEventSummaryFormat(String str) { 52 | configure.setEventSummaryFormat(str); 53 | } 54 | 55 | public void setEventDescriptionFormat(String str) { 56 | configure.setEventDescriptionFormat(str); 57 | } 58 | 59 | public void setTimezone(String str) { 60 | configure.setTimezone(ZoneId.of(str)); 61 | } 62 | 63 | public void setFirstDayOfWeek(String str) { 64 | this.setFirstDayOfWeek(Integer.parseInt(str) % 7); 65 | } 66 | 67 | public void setFirstDayOfWeek(int n) { 68 | configure.setFirstDayOfWeek(n); 69 | } 70 | 71 | public void setSemesterStartDate(String str) { 72 | configure.setSemesterStartDate(LocalDate.parse(str)); 73 | } 74 | 75 | public void setReminderTime(List reminderTimes) { 76 | configure.setReminderTime(reminderTimes); 77 | } 78 | 79 | public void setLessonRanges(List ranges) { 80 | configure.setLessonRanges(loadLessonRanges(ranges)); 81 | } 82 | 83 | private LessonRanges loadLessonRanges(List strList) { 84 | LessonRanges result = new LessonRanges(); 85 | Map map = new TreeMap<>(); 86 | 87 | for (String str : strList) { 88 | // str: 1=01:00:00-01:40:00 89 | String[] components = str.split("[=-]"); 90 | 91 | if (components.length != 3) { 92 | throw new InvalidLessonRange("invalid description of lesson range: " + str); 93 | } 94 | 95 | for (int j = 0; j < 3; ++j) 96 | components[j] = components[j].trim(); 97 | 98 | try { 99 | int lessonIndex = Integer.parseInt(components[0]); 100 | LocalTime from = LocalTime.parse(components[1]); 101 | LocalTime to = LocalTime.parse(components[2]); 102 | 103 | map.put(lessonIndex, new LocalTimeRange(from, to)); 104 | } catch (NumberFormatException e) { 105 | throw new InvalidLessonRange("lesson index is not a valid number: " + components[0], e); 106 | } catch (DateTimeParseException e) { 107 | throw new InvalidLessonRange("lesson range time is not a valid time: " + components[1] + '-' + components[2], e); 108 | } 109 | } 110 | 111 | List lessonNumbers = IntStream.range(1, map.keySet().size()).boxed().collect(Collectors.toList()); 112 | if (! map.keySet().containsAll(lessonNumbers)) { 113 | throw new InvalidLessonRange("the lesson indexes are not from 1 to n"); 114 | } 115 | 116 | for (int lessonIndex : map.keySet()) 117 | result.addRange(map.get(lessonIndex)); 118 | 119 | return result; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/ics/model/VEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.ics.model; 2 | 3 | import com.github.leafee98.CSTI.core.ics.Component; 4 | import com.github.leafee98.CSTI.core.ics.Property; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class VEvent extends Component { 10 | 11 | public static final String CREATED = "CREATED"; 12 | public static final String LAST_MODIFIED= "LAST-MODIFIED"; 13 | public static final String DTSTAMP = "DTSTAMP"; 14 | public static final String UID = "UID"; 15 | public static final String SUMMARY = "SUMMARY"; 16 | public static final String DTSTART = "DTSTART"; 17 | public static final String DTEND = "DTEND"; 18 | public static final String RRULE = "RRULE"; 19 | public static final String TRANSP = "TRANSP"; 20 | public static final String LOCATION = "LOCATION"; 21 | public static final String DESCRIPTION = "DESCRIPTION"; 22 | 23 | private final Property created = new Property(CREATED); 24 | private final Property lastModified = new Property(LAST_MODIFIED); 25 | private final Property dtStamp = new Property(DTSTAMP); 26 | private final Property uid = new Property(UID); 27 | private final Property summary = new Property(SUMMARY); 28 | private final Property dtStart = new Property(DTSTART); 29 | private final Property dtEnd = new Property(DTEND); 30 | private final Property rRule = new Property(RRULE); 31 | private final Property transp = new Property(TRANSP); 32 | private final Property location = new Property(LOCATION); 33 | private final Property description = new Property(DESCRIPTION); 34 | 35 | private final List vAlarms = new ArrayList<>(); 36 | 37 | public VEvent() { 38 | super("VEVENT"); 39 | } 40 | 41 | public Property getCreated() { 42 | return created; 43 | } 44 | 45 | public Property getLastModified() { 46 | return lastModified; 47 | } 48 | 49 | public Property getDtStamp() { 50 | return dtStamp; 51 | } 52 | 53 | public Property getUid() { 54 | return uid; 55 | } 56 | 57 | public Property getSummary() { 58 | return summary; 59 | } 60 | 61 | public Property getDtStart() { 62 | return dtStart; 63 | } 64 | 65 | public Property getDtEnd() { 66 | return dtEnd; 67 | } 68 | 69 | public Property getRRule() { 70 | return rRule; 71 | } 72 | 73 | public Property getTransp() { 74 | return transp; 75 | } 76 | 77 | public Property getLocation() { 78 | return location; 79 | } 80 | 81 | public Property getDescription() { 82 | return description; 83 | } 84 | 85 | public List getVAlarms() { 86 | return vAlarms; 87 | } 88 | 89 | public boolean isEmpty() { 90 | return created.isEmpty() 91 | && lastModified.isEmpty() 92 | && dtStamp.isEmpty() 93 | && uid.isEmpty() 94 | && summary.isEmpty() 95 | && dtStart.isEmpty() 96 | && dtEnd.isEmpty() 97 | && rRule.isEmpty() 98 | && transp.isEmpty() 99 | && location.isEmpty() 100 | && description.isEmpty() 101 | && vAlarms.isEmpty(); 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | if (this.isEmpty()) return ""; 107 | 108 | StringBuilder builder = new StringBuilder(); 109 | builder.append("BEGIN:").append(getName()).append('\n'); 110 | 111 | if (! created.isEmpty()) builder.append(created.toString()).append('\n'); 112 | if (! lastModified.isEmpty()) builder.append(lastModified.toString()).append('\n'); 113 | if (! dtStamp.isEmpty()) builder.append(dtStamp.toString()).append('\n'); 114 | if (! uid.isEmpty()) builder.append(uid.toString()).append('\n'); 115 | if (! summary.isEmpty()) builder.append(summary.toString()).append('\n'); 116 | if (! dtStart.isEmpty()) builder.append(dtStart.toString()).append('\n'); 117 | if (! dtEnd.isEmpty()) builder.append(dtEnd.toString()).append('\n'); 118 | if (! rRule.isEmpty()) builder.append(rRule.toString()).append('\n'); 119 | if (! location.isEmpty()) builder.append(location.toString()).append('\n'); 120 | if (! description.isEmpty()) builder.append(description.toString()).append('\n'); 121 | if (! transp.isEmpty()) builder.append(transp.toString()).append('\n'); 122 | 123 | for (VAlarm alarm : vAlarms) { 124 | builder.append(alarm).append('\n'); 125 | } 126 | 127 | builder.append("END:").append(getName()); 128 | return builder.toString(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/loader/JSONLoader.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean.loader; 2 | 3 | import com.github.leafee98.CSTI.core.bean.*; 4 | import com.github.leafee98.CSTI.core.bean.loader.builder.GenericConfigureBuilder; 5 | import com.github.leafee98.CSTI.core.bean.loader.builder.GenericLessonBuilder; 6 | import com.github.leafee98.CSTI.core.bean.loader.builder.GenericScheduleObjectBuilder; 7 | import com.github.leafee98.CSTI.core.configure.KeyWords; 8 | import org.json.*; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class JSONLoader { 14 | public ScheduleObject load(String str) { 15 | JSONObject jsonObject = new JSONObject(str); 16 | 17 | Configure configure = this.loadConfigure(jsonObject.getJSONObject(KeyWords.global)); 18 | List lessons = this.loadLessons(jsonObject.getJSONArray(KeyWords.lessons)); 19 | 20 | GenericScheduleObjectBuilder builder = new GenericScheduleObjectBuilder(); 21 | builder.setConfigure(configure); 22 | builder.setLessons(lessons); 23 | 24 | return builder.build(); 25 | } 26 | 27 | public Configure loadConfigure(String str) { 28 | JSONObject configure = new JSONObject(str); 29 | return this.loadConfigure(configure); 30 | } 31 | 32 | public Configure loadConfigure(JSONObject configure) { 33 | GenericConfigureBuilder builder = new GenericConfigureBuilder(); 34 | 35 | // load value from json 36 | if (configure.has(KeyWords.eventSummaryFormat)) 37 | builder.setEventSummaryFormat(configure.getString(KeyWords.eventSummaryFormat)); 38 | 39 | if (configure.has(KeyWords.eventDescriptionFormat)) 40 | builder.setEventDescriptionFormat(configure.getString(KeyWords.eventDescriptionFormat)); 41 | 42 | if (configure.has(KeyWords.timezone)) 43 | builder.setTimezone(configure.getString(KeyWords.timezone)); 44 | 45 | if (configure.has(KeyWords.firstDayOfWeek)) 46 | builder.setFirstDayOfWeek(configure.getInt(KeyWords.firstDayOfWeek)); 47 | 48 | if (configure.has(KeyWords.semesterStartDate)) 49 | builder.setSemesterStartDate(configure.getString(KeyWords.semesterStartDate)); 50 | 51 | // load reminder-time 52 | if (configure.has(KeyWords.reminderTime)) { 53 | JSONArray jsonArray = configure.getJSONArray(KeyWords.reminderTime); 54 | ArrayList reminderTime = new ArrayList<>(jsonArray.length()); 55 | for (int i = 0; i < jsonArray.length(); ++i) 56 | reminderTime.add(jsonArray.getString(i)); 57 | builder.setReminderTime(reminderTime); 58 | } 59 | 60 | // load lesson-ranges by other methods. 61 | if (configure.has(KeyWords.lessonRanges)) { 62 | JSONArray jsonArray = configure.getJSONArray(KeyWords.lessonRanges); 63 | ArrayList strList = new ArrayList<>(jsonArray.length()); 64 | for (int i = 0; i < jsonArray.length(); ++i) 65 | strList.add(i, jsonArray.getString(i)); 66 | builder.setLessonRanges(strList); 67 | } 68 | 69 | return builder.build(); 70 | } 71 | 72 | public List loadLessons(JSONArray jsonArray) { 73 | ArrayList result = new ArrayList<>(jsonArray.length()); 74 | 75 | for (int i = 0; i < jsonArray.length(); ++i) { 76 | result.add(this.loadLesson(jsonArray.getJSONObject(i))); 77 | } 78 | 79 | return result; 80 | } 81 | 82 | public Lesson loadLesson(JSONObject jsonObject) { 83 | GenericLessonBuilder builder = new GenericLessonBuilder(); 84 | 85 | if (jsonObject.has(KeyWords.lessonName)) 86 | builder.setName(jsonObject.getString(KeyWords.lessonName)); 87 | 88 | if (jsonObject.has(KeyWords.lessonType)) 89 | builder.setType(jsonObject.getString(KeyWords.lessonType)); 90 | 91 | if (jsonObject.has(KeyWords.lessonTeacher)) 92 | builder.setTeacher(jsonObject.getString(KeyWords.lessonTeacher)); 93 | 94 | if (jsonObject.has(KeyWords.lessonLocation)) 95 | builder.setLocation(jsonObject.getString(KeyWords.lessonLocation)); 96 | 97 | if (jsonObject.has(KeyWords.lessonRemark)) 98 | builder.setRemark(jsonObject.getString(KeyWords.lessonRemark)); 99 | 100 | if (jsonObject.has(KeyWords.lessonSchedule)) { 101 | JSONArray jsonArray = jsonObject.getJSONArray(KeyWords.lessonSchedule); 102 | 103 | List stringList = new ArrayList<>(jsonArray.length()); 104 | for (int i = 0; i < jsonArray.length(); ++i) { 105 | stringList.add(jsonArray.getString(i)); 106 | } 107 | 108 | builder.setSchedule(stringList); 109 | } 110 | 111 | return builder.build(); 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/utils/WeekUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.utils; 2 | 3 | import com.github.leafee98.CSTI.core.exceptions.TimeZoneException; 4 | 5 | import java.time.Month; 6 | 7 | /** 8 | * 9 | SU MO TU WE TH FA SA 10 | 1 2 3 4 5 6 7 11 | 8 9 10 11 12 13 14 12 | 15 16 17 18 19 20 21 13 | 22 23 24 25 26 27 28 14 | 29 30 31 15 | 16 | SU MO TU WE TH FA SA 17 | 1 2 3 4 5 6 18 | 7 8 9 10 11 12 13 19 | 14 15 16 17 18 19 20 20 | 21 22 23 24 25 26 27 21 | 28 29 30 31 22 | 23 | SU MO TU WE TH FA SA 24 | 1 2 3 4 5 25 | 6 7 8 9 10 11 12 26 | 13 14 15 16 17 18 19 27 | 20 21 22 23 24 25 26 28 | 27 28 29 30 31 29 | 30 | SU MO TU WE TH FA SA 31 | 1 2 3 4 32 | 5 6 7 8 9 10 11 33 | 12 13 14 15 16 17 18 34 | 19 20 21 22 23 24 25 35 | 26 27 28 29 30 31 36 | 37 | SU MO TU WE TH FA SA 38 | 1 2 3 39 | 4 5 6 7 8 9 10 40 | 11 12 13 14 15 16 17 41 | 18 19 20 21 22 23 24 42 | 25 26 27 28 29 30 31 43 | 44 | SU MO TU WE TH FA SA 45 | 1 2 46 | 3 4 5 6 7 8 9 47 | 10 11 12 13 14 15 16 48 | 17 18 19 20 21 22 23 49 | 24 25 26 27 28 29 30 50 | 31 51 | 52 | SU MO TU WE TH FA SA 53 | 1 54 | 2 3 4 5 6 7 8 55 | 9 10 11 12 13 14 15 56 | 16 17 18 19 20 21 22 57 | 23 24 25 26 27 28 29 58 | 30 31 59 | 60 | 61 | 假设周X 62 | 第一次一定在当月1-7日之间 [1+7*0 ~ 7*1] == [1 + 7*(y-1), 1 + 7*y) ==> y=(d-1)/7+1 63 | 第二次一点在挡雨8-14日之间 [1+7*1 ~ 7*2] 64 | 第三次一定在当月15-21日之间[1+7*2 ~ 7*3] 65 | 第四次一定在当月22-28日之间[1+7*3 ~ 7*4] 66 | 第五次一定在当月25-31日之间[1+7*4 ~ Z] 67 | 68 | 1+7(y-1) <= d < 1+7y 69 | ===> y <= (d-1)/7+1 70 | ===> y > (d-1)/7 71 | ======> y=floor((d-1)/7+1) 72 | 73 | 每正数周次起始日d = 1 + 7(y-1) 74 | 每正数周次结束如d = 7y 75 | 76 | 假设周X, 当月最大日为Z 77 | 倒数第一次一定在当月[Z-7*1+1 ~ Z-7*0](25 ~ 31) 78 | 倒数第二次一定在当月[Z-7*2+1 ~ Z-7*1](18 ~ 24) 79 | 倒数第三次一定在当月[Z-7*3+1 ~ Z-7*2](11 ~ 17) 80 | 倒数第四次一定在当月[Z-7*4+1 ~ Z-7*3](4 ~ 10) 81 | 倒数第五次一定在当月[Z-7*5+1 ~ Z-7*4](1 ~ 3) 82 | 83 | z-7y < d <= z-7(y-1) 84 | ===> y <= (z+7-d)/7 85 | ===> y > (z-d)/7 86 | ======> y = floor((z-d)/7+1) 87 | 88 | 每倒数周次起始日d = z - 7y + 1 89 | 每倒数周次结束如d = z - 7(y-1) 90 | 91 | 92 | z=28, start: 22 15 8 1 -6 | 1 8 15 22 29 93 | z=28, end: 28 21 14 7 0 | 7 14 21 28 35 94 | 95 | z=29, start: 23 16 9 2 -5 | 1 8 15 22 29 96 | z=29, end: 29 22 15 8 1 | 7 14 21 28 35 97 | 98 | z=30, start: 24 17 10 3 -4 | 1 8 15 22 29 99 | z=30, end: 30 23 16 9 2 | 7 14 21 28 35 100 | 101 | z=31, start: 25 18 11 4 -3 | 1 8 15 22 29 102 | z=31, end: 31 24 17 10 3 | 7 14 21 28 35 103 | */ 104 | 105 | /** 106 | * member function to make it easy to rewrite by inherit 107 | */ 108 | public class WeekUtils { 109 | 110 | // private static final Set afterPositive = 111 | // new TreeSet<>(Stream.of(1, 8, 15, 22, 29).collect(Collectors.toSet())); 112 | // private static final Set beforePositive = 113 | // new TreeSet<>(Stream.of(7, 14, 21, 28, 35).collect(Collectors.toSet())); 114 | 115 | // private static Map> afterNegative; 116 | // private static Map> beforeNegative; 117 | 118 | // static { 119 | // for (int z = 28; z <= 31; ++z) { 120 | // Set set = new TreeSet<>(); 121 | // for (int i = 1; i <= 5; ++i) { 122 | // int day = z - 7 * i + 1; 123 | // if (day > 0 && day <= z) { 124 | // set.add(day); 125 | // } 126 | // } 127 | // afterNegative.put(z, set); 128 | // } 129 | 130 | // for (int z = 28; z <= 31; ++z) { 131 | // Set set = new TreeSet<>(); 132 | // for (int i = 1; i <= 5; ++i) { 133 | // int day = z - 7 * (i - 1); 134 | // if (day > 0 && day <= z) { 135 | // set.add(day); 136 | // } 137 | // } 138 | // beforeNegative.put(z, set); 139 | // } 140 | 141 | // } 142 | 143 | 144 | 145 | public int onOrAfter(int dayOfMonth, int monthLength) { 146 | // // on or after, positive 147 | // // 每正数周次起始日 d = 1 + 7(y-1) --> y = (d - 1) / 7 + 1 148 | // if ((dayOfMonth - 1) % 7 != 0 || dayOfMonth > monthLength) { 149 | // throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 150 | // } else { 151 | // return (dayOfMonth - 1) / 7 + 1; 152 | // } 153 | 154 | // // on or after, negative 155 | // // 每倒数周次起始日 d = z - 7y + 1 --> y = (z - d + 1) / 7 156 | // if ((monthLength - dayOfMonth + 1) % 7 != 0 || dayOfMonth < 1) { 157 | // throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 158 | // } else { 159 | // return -(monthLength - dayOfMonth + 1) / 7; 160 | // } 161 | 162 | if (dayOfMonth < 1 || dayOfMonth > monthLength) { 163 | throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 164 | } 165 | 166 | if ((dayOfMonth - 1) % 7 == 0) { 167 | return (dayOfMonth - 1) / 7 + 1; 168 | } else if ((monthLength - dayOfMonth + 1) % 7 == 0) { 169 | return -1 * ((monthLength - dayOfMonth + 1) / 7); 170 | } else { 171 | throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 172 | } 173 | } 174 | 175 | public int byIndicator(int dayOfMonthIndicator, int monthLength) { 176 | // if (dayOfMonthIndicator > 0) { 177 | // if ((dayOfMonthIndicator - 1) % 7 == 0) { 178 | // return (dayOfMonthIndicator - 1) / 7 + 1; 179 | // } 180 | // } else { 181 | // dayOfMonthIndicator = -dayOfMonthIndicator; 182 | // if ((dayOfMonthIndicator - 1) % 7 == 0) { 183 | // return -((dayOfMonthIndicator - 1) / 7 + 1); 184 | // } 185 | // } 186 | // throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonthIndicator); 187 | 188 | if (dayOfMonthIndicator > 0) { 189 | return onOrAfter(dayOfMonthIndicator, monthLength); 190 | } else { 191 | int day = monthLength + dayOfMonthIndicator + 1; 192 | return onOrBefore(day, monthLength); 193 | } 194 | } 195 | 196 | /** 197 | * February not occurred in Java Timezone, so treat month's length as non-leap year 198 | */ 199 | public int byIndicator(int dayOfMonthIndicator, Month month) { 200 | return byIndicator(dayOfMonthIndicator, month.length(false)); 201 | } 202 | 203 | 204 | public int onOrBefore(int dayOfMonth, int monthLength) { 205 | if (dayOfMonth < 1 || dayOfMonth > monthLength) { 206 | throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 207 | } 208 | 209 | // 每正数周次结束日d = 7y -> y = d / 7 210 | // 每倒数周次结束日d = z - 7(y-1) -> y = (z - d) / 7 + 1 211 | if (dayOfMonth % 7 == 0) { 212 | // positive 213 | return dayOfMonth / 7; 214 | } else if ((monthLength - dayOfMonth) % 7 == 0) { 215 | // negative 216 | return -1 * ((monthLength - dayOfMonth) / 7 + 1); 217 | } else { 218 | throw new TimeZoneException("failed to determine the ordinal of week with dayOfMonth: " + dayOfMonth); 219 | } 220 | } 221 | 222 | public int ordinalOfWeek(int dayOfMonth) { 223 | return (dayOfMonth - 1) / 7 + 1; 224 | } 225 | 226 | public int reversedOrdinalOfWeek(int dayOfMonth, int monthLength) { 227 | return (monthLength - dayOfMonth) / 7 + 1; 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/bean/Configure.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.bean; 2 | 3 | import java.time.LocalDate; 4 | import java.time.ZoneId; 5 | import java.time.format.DateTimeFormatter; 6 | import java.time.zone.ZoneRulesException; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import com.github.leafee98.CSTI.core.configure.KeyWords; 12 | import com.github.leafee98.CSTI.core.exceptions.InvalidConfigure; 13 | 14 | public class Configure { 15 | 16 | // default event-summary-format is ``${lessonName}-${location}`` 17 | private String eventSummaryFormat = "${lessonName}-${location}"; 18 | 19 | // default value of event-description-format 20 | private String eventDescriptionFormat = 21 | "name:${lessonName}\\n" + 22 | "location:${location}\\n" + 23 | "teacher:${teacher}\\n" + 24 | "type:${lessonType}\\n" + 25 | "remark:${remark}\\n" + 26 | "schedule:${scheduleFull}"; 27 | 28 | // default is system default time zone 29 | private ZoneId timezone = ZoneId.systemDefault(); 30 | 31 | // default unset, init when loading from configure file 32 | // use day of semesterStartDate if not present in configure file 33 | private int firstDayOfWeek = -1; 34 | 35 | // no default, and must occur in config file 36 | private LocalDate semesterStartDate; 37 | 38 | // reminder time default is before 15 minutes class start 39 | private List reminderTime = new ArrayList<>(Collections.singleton("-15m")); 40 | 41 | // no default, and must occur in config file 42 | private LessonRanges lessonRanges; 43 | 44 | public static Configure load(String str) { 45 | Configure result = new Configure(); 46 | 47 | String[] lines = str.split("\n"); 48 | for (int i = 0; i < lines.length; ++i) 49 | lines[i] = lines[i].trim(); 50 | 51 | for (String line : lines) { 52 | if (line.startsWith(KeyWords.eventSummaryFormat)) { 53 | // keep white spaces of format description 54 | String tmp = line.substring(KeyWords.eventSummaryFormat.length() + 1); 55 | if (tmp.trim().length() != 0) { 56 | result.setEventSummaryFormat(tmp); 57 | } 58 | // result.setEventSummaryFormat(line.substring(KeyWords.eventSummaryFormat.length() + 1)); 59 | } else if (line.startsWith(KeyWords.eventDescriptionFormat)) { 60 | String tmp = line.substring(KeyWords.eventDescriptionFormat.length() + 1); 61 | if (tmp.trim().length() != 0) { 62 | result.setEventDescriptionFormat(tmp); 63 | } 64 | // result.setEventDescriptionFormat(line.substring(KeyWords.eventDescriptionFormat.length() + 1)); 65 | } else if (line.startsWith(KeyWords.firstDayOfWeek)) { 66 | // take 0 as Sunday 67 | String tmp = line.substring(KeyWords.firstDayOfWeek.length() + 1).trim(); 68 | if (tmp.length() != 0) { 69 | int dow = Integer.parseInt(tmp); 70 | result.setFirstDayOfWeek(dow % 7); 71 | } 72 | } else if (line.startsWith(KeyWords.semesterStartDate)) { 73 | result.setSemesterStartDate( 74 | LocalDate.parse(line.substring(KeyWords.semesterStartDate.length() + 1).trim())); 75 | if (result.getFirstDayOfWeek() < 0) { 76 | result.setFirstDayOfWeek(result.getSemesterStartDate().getDayOfWeek().getValue()); 77 | } 78 | } else if (line.startsWith(KeyWords.reminderTime)) { 79 | // get reminder description and remove redundant spaces 80 | List reminder = new ArrayList<>(); 81 | String reminderStr = line.substring(KeyWords.reminderTime.length() + 1).trim(); 82 | String[] reminderArr = reminderStr.split(","); 83 | for (String s : reminderArr) { 84 | String tmp = s.trim(); 85 | if (tmp.length() > 0) 86 | reminder.add(tmp); 87 | } 88 | result.setReminderTime(reminder); 89 | } else if (line.startsWith(KeyWords.timezone)) { 90 | try { 91 | ZoneId id = ZoneId.of(line.substring(KeyWords.timezone.length() + 1).trim()); 92 | result.setTimezone(id); 93 | } catch (ZoneRulesException e) { 94 | throw new InvalidConfigure("timezone must be region format, follows are available:\n" 95 | + ZoneId.getAvailableZoneIds()); 96 | } 97 | } else if (line.startsWith(KeyWords.lessonRanges)) { 98 | String remain = line.substring(KeyWords.lessonRanges.length() + 1).trim(); 99 | result.setLessonRanges(LessonRanges.load(remain)); 100 | } 101 | } 102 | 103 | // check, throw exception if failed 104 | result.checkSemesterStartDate(); 105 | 106 | return result; 107 | } 108 | 109 | private void checkSemesterStartDate() { 110 | if (semesterStartDate.getDayOfWeek().getValue() % 7 != firstDayOfWeek) { 111 | throw new InvalidConfigure("semester-start-date is not compatible with first-day-of-week\n" + 112 | "it's " + getSemesterStartDate().getDayOfWeek().name() + " on " + 113 | getSemesterStartDate()); 114 | } 115 | } 116 | 117 | @Override 118 | public String toString() { 119 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 120 | 121 | StringBuilder builder = new StringBuilder(); 122 | 123 | builder.append(KeyWords.eventSummaryFormat).append(':'); 124 | builder.append(eventSummaryFormat).append('\n'); 125 | 126 | builder.append(KeyWords.eventDescriptionFormat).append(':'); 127 | builder.append(eventDescriptionFormat).append('\n'); 128 | 129 | builder.append(KeyWords.timezone).append(':'); 130 | builder.append(timezone).append('\n'); 131 | 132 | builder.append(KeyWords.firstDayOfWeek).append(':'); 133 | builder.append(firstDayOfWeek).append('\n'); 134 | 135 | builder.append(KeyWords.semesterStartDate).append(':'); 136 | builder.append(semesterStartDate.format(formatter)).append('\n'); 137 | 138 | // append list of reminder time with comma split 139 | builder.append(KeyWords.reminderTime).append(':'); 140 | for (String s : reminderTime) { 141 | builder.append(s).append(','); 142 | } 143 | builder.setLength(builder.length() - 1); 144 | builder.append('\n'); 145 | 146 | // no newline at the end of a string description 147 | builder.append(KeyWords.lessonRanges).append(':'); 148 | builder.append(lessonRanges.toString()); 149 | 150 | return builder.toString(); 151 | } 152 | 153 | public String getEventSummaryFormat() { 154 | return eventSummaryFormat; 155 | } 156 | 157 | public void setEventSummaryFormat(String eventSummaryFormat) { 158 | this.eventSummaryFormat = eventSummaryFormat; 159 | } 160 | 161 | public String getEventDescriptionFormat() { 162 | return eventDescriptionFormat; 163 | } 164 | 165 | public void setEventDescriptionFormat(String eventDescriptionFormat) { 166 | this.eventDescriptionFormat = eventDescriptionFormat; 167 | } 168 | 169 | public ZoneId getTimezone() { 170 | return timezone; 171 | } 172 | 173 | public void setTimezone(ZoneId timezone) { 174 | this.timezone = timezone; 175 | } 176 | 177 | public int getFirstDayOfWeek() { 178 | return firstDayOfWeek; 179 | } 180 | 181 | public void setFirstDayOfWeek(int firstDayOfWeek) { 182 | this.firstDayOfWeek = firstDayOfWeek; 183 | } 184 | 185 | public LocalDate getSemesterStartDate() { 186 | return semesterStartDate; 187 | } 188 | 189 | public void setSemesterStartDate(LocalDate semesterStartDate) { 190 | this.semesterStartDate = semesterStartDate; 191 | } 192 | 193 | public List getReminderTime() { 194 | return reminderTime; 195 | } 196 | 197 | public void setReminderTime(List reminderTime) { 198 | this.reminderTime = reminderTime; 199 | } 200 | 201 | public LessonRanges getLessonRanges() { 202 | return lessonRanges; 203 | } 204 | 205 | public void setLessonRanges(LessonRanges lessonRanges) { 206 | this.lessonRanges = lessonRanges; 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/github/leafee98/CSTI/core/generate/Generator.java: -------------------------------------------------------------------------------- 1 | package com.github.leafee98.CSTI.core.generate; 2 | 3 | import com.github.leafee98.CSTI.core.bean.Lesson; 4 | import com.github.leafee98.CSTI.core.bean.ScheduleObject; 5 | import com.github.leafee98.CSTI.core.configure.KeyWords; 6 | import com.github.leafee98.CSTI.core.exceptions.InvalidConfigure; 7 | import com.github.leafee98.CSTI.core.exceptions.TimeZoneException; 8 | import com.github.leafee98.CSTI.core.ics.Value; 9 | import com.github.leafee98.CSTI.core.ics.model.*; 10 | import com.github.leafee98.CSTI.core.utils.*; 11 | 12 | import java.time.LocalDateTime; 13 | import java.time.ZoneId; 14 | import java.time.ZoneOffset; 15 | import java.time.zone.ZoneOffsetTransitionRule; 16 | import java.time.zone.ZoneRules; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.UUID; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | public class Generator { 24 | 25 | public static final String FREQ = "FREQ"; 26 | public static final String RRULE = "RRULE"; 27 | public static final String YEARLY = "YEARLY"; 28 | public static final String BYDAY = "BYDAY"; 29 | public static final String BYMONTHDAY = "BYMONTHDAY"; 30 | public static final String BYMONTH = "BYMONTH"; 31 | public static final String OPAQUE = "OPAQUE"; 32 | public static final String VALUE = "VALUE"; 33 | public static final String DURATION = "DURATION"; 34 | public static final String DISPLAY = "DISPLAY"; 35 | public static final String TZID = "TZID"; 36 | 37 | private final ScheduleObject scheduleObj; 38 | private final TimeFormatter formatter; 39 | 40 | private final WeekUtils weekUtil = new WeekUtils(); 41 | private final LessonDateTimeCalculator calculator; 42 | 43 | public Generator(ScheduleObject obj) { 44 | this.scheduleObj = obj; 45 | this.formatter = new TimeFormatter(scheduleObj.getConfigure().getTimezone()); 46 | this.calculator = new LessonDateTimeCalculator(scheduleObj.getConfigure()); 47 | } 48 | 49 | public VCalendar generate() { 50 | VCalendar result = new VCalendar(); 51 | result.setTimeZone(generateVTimeZone()); 52 | for (Lesson l : scheduleObj.getLessons()) { 53 | result.getVEvents().addAll(generateVEvent(l)); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | /** 60 | * there is RRULE with generated DAYLIGHT and STANDARD 61 | * @return VTIMEZONE generate from timezone 62 | */ 63 | public VTimezone generateVTimeZone() { 64 | VTimezone timezone = new VTimezone(); 65 | ZoneId zoneId = this.scheduleObj.getConfigure().getTimezone(); 66 | ZoneRules zoneRules = zoneId.getRules(); 67 | 68 | timezone.getTzid().setValue(zoneId.getId()); 69 | 70 | LocalDateTime dateStart = LocalDateTime.of(1970, 1, 1, 0, 0); 71 | 72 | if (zoneRules.getTransitionRules().isEmpty()) { 73 | Standard standard = new Standard(); 74 | 75 | ZoneOffset offset = zoneRules.getOffset(LocalDateTime.now()); 76 | // ZoneOffset offset = zoneRules.getStandardOffset(Instant.from(dateStart)); 77 | 78 | standard.getTzName().setValue("STD(irregular)"); 79 | standard.getDtStart().setValue(formatter.local(dateStart)); 80 | standard.getTzOffsetFrom().setValue("+0000"); 81 | standard.getTzOffsetTo().setValue(formatter.offset(offset)); 82 | 83 | timezone.setStandard(standard); 84 | } else { 85 | for (ZoneOffsetTransitionRule rule : zoneRules.getTransitionRules()) { 86 | if (rule.getOffsetAfter().equals(rule.getStandardOffset())) { 87 | Standard standard = new Standard(); 88 | 89 | standard.getTzName().setValue("STD(irregular)"); 90 | standard.getDtStart().setValue(formatter.local(dateStart)); 91 | standard.getTzOffsetFrom().setValue(formatter.offset(rule.getOffsetBefore())); 92 | standard.getTzOffsetTo().setValue(formatter.offset(rule.getOffsetAfter())); 93 | 94 | assignTimeZoneRrule(standard.getRRule().getValue(), rule); 95 | 96 | timezone.setStandard(standard); 97 | } else { 98 | Daylight daylight = new Daylight(); 99 | 100 | daylight.getTzName().setValue("DST(irregular)"); 101 | daylight.getDtStart().setValue(formatter.local(dateStart)); 102 | daylight.getTzOffsetFrom().setValue(formatter.offset(rule.getOffsetBefore())); 103 | daylight.getTzOffsetTo().setValue(formatter.offset(rule.getOffsetAfter())); 104 | 105 | assignTimeZoneRrule(daylight.getRRule().getValue(), rule); 106 | 107 | timezone.setDaylight(daylight); 108 | } 109 | } 110 | } 111 | 112 | return timezone; 113 | } 114 | 115 | private void assignTimeZoneRrule(Value value, ZoneOffsetTransitionRule rule) { 116 | String monthValue = String.valueOf(rule.getMonth().getValue()); 117 | 118 | try { 119 | int week = weekUtil.byIndicator(rule.getDayOfMonthIndicator(), rule.getMonth()); 120 | 121 | String weekValue = week + rule.getDayOfWeek().name().substring(0, 2); 122 | value.putParameter(FREQ, YEARLY); 123 | value.putParameter(BYDAY, weekValue); 124 | value.putParameter(BYMONTH, monthValue); 125 | } catch (TimeZoneException e) { 126 | StringBuilder byMonthDayValue = new StringBuilder(); 127 | 128 | // no February appeared in rules of java tz database, treat as no-leap year. 129 | int monthLength = rule.getMonth().length(false); 130 | int indicator = rule.getDayOfMonthIndicator(); 131 | 132 | if (indicator > 0) { 133 | byMonthDayValue.append(indicator++); 134 | for (int i = 1; i < 7 && indicator <= monthLength; ++i, ++indicator) 135 | byMonthDayValue.append(",").append(indicator); 136 | } else { 137 | indicator = monthLength + 1 + indicator; 138 | byMonthDayValue.append(indicator--); 139 | for (int i = 0; i < 6 && indicator >= 1; ++i, --indicator) { 140 | byMonthDayValue.append(",").append(indicator); 141 | } 142 | } 143 | 144 | value.putParameter(FREQ, YEARLY); 145 | value.putParameter(BYDAY, rule.getDayOfWeek().name().substring(0, 2)); 146 | value.putParameter(BYMONTHDAY, byMonthDayValue.toString()); 147 | value.putParameter(BYMONTH, monthValue); 148 | } 149 | } 150 | 151 | public List generateVEvent(Lesson lesson) { 152 | List result = new ArrayList<>(); 153 | 154 | // constant properties in different events of the same lesson 155 | String created = formatter.utc(LocalDateTime.now()); // created, dtstamp, lastModified 156 | String location = lesson.getLocation(); 157 | 158 | List alarms = generateVAlarm(); 159 | String summary = generateEventSummary(lesson); 160 | String description = generateEventDescription(lesson); 161 | 162 | // variable properties: UID, DTSTART, DTEND 163 | List lessonDateTimes = calculator.cal(lesson); 164 | 165 | // as parameter of dtStart 166 | String tzId = scheduleObj.getConfigure().getTimezone().getId(); 167 | 168 | for (LocalDateTimeRange dateTimeRange : lessonDateTimes) { 169 | VEvent event = new VEvent(); 170 | 171 | // datetime start and end 172 | event.getDtStart().setValue(formatter.local(dateTimeRange.from)); 173 | event.getDtStart().putParameter(TZID, tzId); 174 | 175 | event.getDtEnd().setValue(formatter.local(dateTimeRange.to)); 176 | event.getDtEnd().putParameter(TZID, tzId); 177 | 178 | // uid 179 | event.getUid().setValue(UUID.randomUUID().toString()); 180 | 181 | // constant property 182 | event.getCreated().setValue(created); 183 | event.getDtStamp().setValue(created); 184 | event.getLastModified().setValue(created); 185 | event.getTransp().setValue(OPAQUE); 186 | event.getLocation().setValue(location); 187 | 188 | event.getSummary().setValue(summary); 189 | event.getDescription().setValue(description); 190 | 191 | // VALARM 192 | for (VAlarm alarm : alarms) { 193 | event.getVAlarms().add(alarm); 194 | } 195 | 196 | // save to result 197 | result.add(event); 198 | } 199 | 200 | return result; 201 | } 202 | 203 | // event title and description is custom, which may contain special prefix or other remarks. 204 | private String generateEventSummary(Lesson lesson) { 205 | return VariableSubstitution.doSubstitute(scheduleObj.getConfigure().getEventSummaryFormat(), lesson); 206 | } 207 | 208 | private String generateEventDescription(Lesson lesson) { 209 | return VariableSubstitution.doSubstitute(scheduleObj.getConfigure().getEventDescriptionFormat(), lesson); 210 | } 211 | 212 | private List generateVAlarm() { 213 | Pattern reminderFormat = Pattern.compile("([+-]?\\d+)([HMS])"); 214 | 215 | List result = new ArrayList<>(); 216 | 217 | List reminders = scheduleObj.getConfigure().getReminderTime(); 218 | for (String reminderTime : reminders) { 219 | VAlarm alarm = new VAlarm(); 220 | alarm.getAction().setValue(DISPLAY); 221 | alarm.getDescription().setValue("generated by class-schedule-to-icalendar"); 222 | 223 | reminderTime = reminderTime.toUpperCase(); 224 | Matcher matcher = reminderFormat.matcher(reminderTime); 225 | if (matcher.matches()) { 226 | int timeValue = Integer.parseInt(matcher.group(1)); 227 | String timeUnit = matcher.group(2); 228 | 229 | alarm.getTrigger().putParameter(VALUE, DURATION); 230 | if (timeValue < 0) { 231 | timeValue *= -1; 232 | alarm.getTrigger().setValue("-PT" + timeValue + timeUnit); 233 | } else { 234 | alarm.getTrigger().setValue("PT" + timeValue + timeUnit); 235 | } 236 | } else { 237 | throw new InvalidConfigure("cannot parse " + KeyWords.reminderTime + "'s value: " + reminderTime); 238 | } 239 | 240 | result.add(alarm); 241 | } 242 | 243 | return result; 244 | } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /doc/example/e1(deprecated)/e1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:class-schedule-to-icalendar 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Asia/Shanghai 6 | BEGIN:STANDARD 7 | TZOFFSETFROM:+0000 8 | TZOFFSETTO:+0800 9 | TZNAME:STD(irregular) 10 | DTSTART:19700101T000000 11 | END:STANDARD 12 | END:VTIMEZONE 13 | BEGIN:VEVENT 14 | CREATED:20200825T054930Z 15 | LAST-MODIFIED:20200825T054930Z 16 | DTSTAMP:20200825T054930Z 17 | UID:b18a8380-841b-44c2-8f19-66a4735c1d22 18 | SUMMARY:软件测试技术-某位置a 19 | DTSTART;TZID=Asia/Shanghai:20200224T133000 20 | DTEND;TZID=Asia/Shanghai:20200224T165500 21 | LOCATION:某位置a 22 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 23 | TRANSP:OPAQUE 24 | BEGIN:VALARM 25 | ACTION:DISPLAY 26 | TRIGGER;VALUE=DURATION:-PT15M 27 | DESCRIPTION:generated by class-schedule-to-icalendar 28 | END:VALARM 29 | END:VEVENT 30 | BEGIN:VEVENT 31 | CREATED:20200825T054930Z 32 | LAST-MODIFIED:20200825T054930Z 33 | DTSTAMP:20200825T054930Z 34 | UID:31594f75-fd0e-4220-97a4-297b1c90db8e 35 | SUMMARY:软件测试技术-某位置a 36 | DTSTART;TZID=Asia/Shanghai:20200302T133000 37 | DTEND;TZID=Asia/Shanghai:20200302T165500 38 | LOCATION:某位置a 39 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 40 | TRANSP:OPAQUE 41 | BEGIN:VALARM 42 | ACTION:DISPLAY 43 | TRIGGER;VALUE=DURATION:-PT15M 44 | DESCRIPTION:generated by class-schedule-to-icalendar 45 | END:VALARM 46 | END:VEVENT 47 | BEGIN:VEVENT 48 | CREATED:20200825T054930Z 49 | LAST-MODIFIED:20200825T054930Z 50 | DTSTAMP:20200825T054930Z 51 | UID:fa59e315-49b7-4d0f-aa92-20cfd7455171 52 | SUMMARY:软件测试技术-某位置a 53 | DTSTART;TZID=Asia/Shanghai:20200309T133000 54 | DTEND;TZID=Asia/Shanghai:20200309T165500 55 | LOCATION:某位置a 56 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 57 | TRANSP:OPAQUE 58 | BEGIN:VALARM 59 | ACTION:DISPLAY 60 | TRIGGER;VALUE=DURATION:-PT15M 61 | DESCRIPTION:generated by class-schedule-to-icalendar 62 | END:VALARM 63 | END:VEVENT 64 | BEGIN:VEVENT 65 | CREATED:20200825T054930Z 66 | LAST-MODIFIED:20200825T054930Z 67 | DTSTAMP:20200825T054930Z 68 | UID:cd05a2db-4a4b-4824-9ed0-f37de75db9fc 69 | SUMMARY:软件测试技术-某位置a 70 | DTSTART;TZID=Asia/Shanghai:20200316T133000 71 | DTEND;TZID=Asia/Shanghai:20200316T165500 72 | LOCATION:某位置a 73 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 74 | TRANSP:OPAQUE 75 | BEGIN:VALARM 76 | ACTION:DISPLAY 77 | TRIGGER;VALUE=DURATION:-PT15M 78 | DESCRIPTION:generated by class-schedule-to-icalendar 79 | END:VALARM 80 | END:VEVENT 81 | BEGIN:VEVENT 82 | CREATED:20200825T054930Z 83 | LAST-MODIFIED:20200825T054930Z 84 | DTSTAMP:20200825T054930Z 85 | UID:ac31397e-bda0-4f6a-b993-3f85b025a302 86 | SUMMARY:软件测试技术-某位置a 87 | DTSTART;TZID=Asia/Shanghai:20200323T133000 88 | DTEND;TZID=Asia/Shanghai:20200323T165500 89 | LOCATION:某位置a 90 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 91 | TRANSP:OPAQUE 92 | BEGIN:VALARM 93 | ACTION:DISPLAY 94 | TRIGGER;VALUE=DURATION:-PT15M 95 | DESCRIPTION:generated by class-schedule-to-icalendar 96 | END:VALARM 97 | END:VEVENT 98 | BEGIN:VEVENT 99 | CREATED:20200825T054930Z 100 | LAST-MODIFIED:20200825T054930Z 101 | DTSTAMP:20200825T054930Z 102 | UID:b8291ce8-25f9-4d3b-bb04-9642e874a746 103 | SUMMARY:软件测试技术-某位置a 104 | DTSTART;TZID=Asia/Shanghai:20200330T133000 105 | DTEND;TZID=Asia/Shanghai:20200330T165500 106 | LOCATION:某位置a 107 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 108 | TRANSP:OPAQUE 109 | BEGIN:VALARM 110 | ACTION:DISPLAY 111 | TRIGGER;VALUE=DURATION:-PT15M 112 | DESCRIPTION:generated by class-schedule-to-icalendar 113 | END:VALARM 114 | END:VEVENT 115 | BEGIN:VEVENT 116 | CREATED:20200825T054930Z 117 | LAST-MODIFIED:20200825T054930Z 118 | DTSTAMP:20200825T054930Z 119 | UID:76c3b5fe-43d6-45f9-812a-951c39343ea7 120 | SUMMARY:软件测试技术-某位置a 121 | DTSTART;TZID=Asia/Shanghai:20200406T133000 122 | DTEND;TZID=Asia/Shanghai:20200406T165500 123 | LOCATION:某位置a 124 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 125 | TRANSP:OPAQUE 126 | BEGIN:VALARM 127 | ACTION:DISPLAY 128 | TRIGGER;VALUE=DURATION:-PT15M 129 | DESCRIPTION:generated by class-schedule-to-icalendar 130 | END:VALARM 131 | END:VEVENT 132 | BEGIN:VEVENT 133 | CREATED:20200825T054930Z 134 | LAST-MODIFIED:20200825T054930Z 135 | DTSTAMP:20200825T054930Z 136 | UID:020b055a-a9b8-4f53-ae3b-9890239b7b0f 137 | SUMMARY:软件测试技术-某位置a 138 | DTSTART;TZID=Asia/Shanghai:20200413T133000 139 | DTEND;TZID=Asia/Shanghai:20200413T165500 140 | LOCATION:某位置a 141 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 142 | TRANSP:OPAQUE 143 | BEGIN:VALARM 144 | ACTION:DISPLAY 145 | TRIGGER;VALUE=DURATION:-PT15M 146 | DESCRIPTION:generated by class-schedule-to-icalendar 147 | END:VALARM 148 | END:VEVENT 149 | BEGIN:VEVENT 150 | CREATED:20200825T054930Z 151 | LAST-MODIFIED:20200825T054930Z 152 | DTSTAMP:20200825T054930Z 153 | UID:d2462dd9-b740-4bb8-a9f2-a9a768f8c772 154 | SUMMARY:软件测试技术-某位置a 155 | DTSTART;TZID=Asia/Shanghai:20200420T133000 156 | DTEND;TZID=Asia/Shanghai:20200420T165500 157 | LOCATION:某位置a 158 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 159 | TRANSP:OPAQUE 160 | BEGIN:VALARM 161 | ACTION:DISPLAY 162 | TRIGGER;VALUE=DURATION:-PT15M 163 | DESCRIPTION:generated by class-schedule-to-icalendar 164 | END:VALARM 165 | END:VEVENT 166 | BEGIN:VEVENT 167 | CREATED:20200825T054930Z 168 | LAST-MODIFIED:20200825T054930Z 169 | DTSTAMP:20200825T054930Z 170 | UID:1f3b81b5-019a-464d-a852-373142450d7f 171 | SUMMARY:软件测试技术-某位置a 172 | DTSTART;TZID=Asia/Shanghai:20200427T133000 173 | DTEND;TZID=Asia/Shanghai:20200427T165500 174 | LOCATION:某位置a 175 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 176 | TRANSP:OPAQUE 177 | BEGIN:VALARM 178 | ACTION:DISPLAY 179 | TRIGGER;VALUE=DURATION:-PT15M 180 | DESCRIPTION:generated by class-schedule-to-icalendar 181 | END:VALARM 182 | END:VEVENT 183 | BEGIN:VEVENT 184 | CREATED:20200825T054930Z 185 | LAST-MODIFIED:20200825T054930Z 186 | DTSTAMP:20200825T054930Z 187 | UID:38376010-06fb-4259-8ea1-5344a613ac3f 188 | SUMMARY:软件测试技术-某位置a 189 | DTSTART;TZID=Asia/Shanghai:20200504T133000 190 | DTEND;TZID=Asia/Shanghai:20200504T165500 191 | LOCATION:某位置a 192 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 193 | TRANSP:OPAQUE 194 | BEGIN:VALARM 195 | ACTION:DISPLAY 196 | TRIGGER;VALUE=DURATION:-PT15M 197 | DESCRIPTION:generated by class-schedule-to-icalendar 198 | END:VALARM 199 | END:VEVENT 200 | BEGIN:VEVENT 201 | CREATED:20200825T054930Z 202 | LAST-MODIFIED:20200825T054930Z 203 | DTSTAMP:20200825T054930Z 204 | UID:d56bb3c2-c90d-4a6a-af5d-c66205ba11ad 205 | SUMMARY:软件测试技术-某位置a 206 | DTSTART;TZID=Asia/Shanghai:20200511T133000 207 | DTEND;TZID=Asia/Shanghai:20200511T165500 208 | LOCATION:某位置a 209 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 210 | TRANSP:OPAQUE 211 | BEGIN:VALARM 212 | ACTION:DISPLAY 213 | TRIGGER;VALUE=DURATION:-PT15M 214 | DESCRIPTION:generated by class-schedule-to-icalendar 215 | END:VALARM 216 | END:VEVENT 217 | BEGIN:VEVENT 218 | CREATED:20200825T054930Z 219 | LAST-MODIFIED:20200825T054930Z 220 | DTSTAMP:20200825T054930Z 221 | UID:d68446b0-5cd1-4e89-8633-9d85e51d48c7 222 | SUMMARY:软件测试技术-某位置a 223 | DTSTART;TZID=Asia/Shanghai:20200518T133000 224 | DTEND;TZID=Asia/Shanghai:20200518T165500 225 | LOCATION:某位置a 226 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 227 | TRANSP:OPAQUE 228 | BEGIN:VALARM 229 | ACTION:DISPLAY 230 | TRIGGER;VALUE=DURATION:-PT15M 231 | DESCRIPTION:generated by class-schedule-to-icalendar 232 | END:VALARM 233 | END:VEVENT 234 | BEGIN:VEVENT 235 | CREATED:20200825T054930Z 236 | LAST-MODIFIED:20200825T054930Z 237 | DTSTAMP:20200825T054930Z 238 | UID:426bc143-688b-4773-be5e-5dffa3a30e62 239 | SUMMARY:软件测试技术-某位置a 240 | DTSTART;TZID=Asia/Shanghai:20200525T133000 241 | DTEND;TZID=Asia/Shanghai:20200525T165500 242 | LOCATION:某位置a 243 | DESCRIPTION:name:软件测试技术\nlocation:某位置a\nteacher:某教师a\ntype:专业必修\nremark:暂无\nschedule:\n 1-14|1|6-9; 244 | TRANSP:OPAQUE 245 | BEGIN:VALARM 246 | ACTION:DISPLAY 247 | TRIGGER;VALUE=DURATION:-PT15M 248 | DESCRIPTION:generated by class-schedule-to-icalendar 249 | END:VALARM 250 | END:VEVENT 251 | BEGIN:VEVENT 252 | CREATED:20200825T054930Z 253 | LAST-MODIFIED:20200825T054930Z 254 | DTSTAMP:20200825T054930Z 255 | UID:f6939659-6a0f-4cca-84b8-3589dd9d35a9 256 | SUMMARY:软件项目管理-某位置b 257 | DTSTART;TZID=Asia/Shanghai:20200225T095000 258 | DTEND;TZID=Asia/Shanghai:20200225T121500 259 | LOCATION:某位置b 260 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 261 | TRANSP:OPAQUE 262 | BEGIN:VALARM 263 | ACTION:DISPLAY 264 | TRIGGER;VALUE=DURATION:-PT15M 265 | DESCRIPTION:generated by class-schedule-to-icalendar 266 | END:VALARM 267 | END:VEVENT 268 | BEGIN:VEVENT 269 | CREATED:20200825T054930Z 270 | LAST-MODIFIED:20200825T054930Z 271 | DTSTAMP:20200825T054930Z 272 | UID:8000367a-2842-4dff-a1f8-efbce0162abd 273 | SUMMARY:软件项目管理-某位置b 274 | DTSTART;TZID=Asia/Shanghai:20200303T095000 275 | DTEND;TZID=Asia/Shanghai:20200303T121500 276 | LOCATION:某位置b 277 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 278 | TRANSP:OPAQUE 279 | BEGIN:VALARM 280 | ACTION:DISPLAY 281 | TRIGGER;VALUE=DURATION:-PT15M 282 | DESCRIPTION:generated by class-schedule-to-icalendar 283 | END:VALARM 284 | END:VEVENT 285 | BEGIN:VEVENT 286 | CREATED:20200825T054930Z 287 | LAST-MODIFIED:20200825T054930Z 288 | DTSTAMP:20200825T054930Z 289 | UID:a9945008-4a15-4639-83cd-c772673d7186 290 | SUMMARY:软件项目管理-某位置b 291 | DTSTART;TZID=Asia/Shanghai:20200310T095000 292 | DTEND;TZID=Asia/Shanghai:20200310T121500 293 | LOCATION:某位置b 294 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 295 | TRANSP:OPAQUE 296 | BEGIN:VALARM 297 | ACTION:DISPLAY 298 | TRIGGER;VALUE=DURATION:-PT15M 299 | DESCRIPTION:generated by class-schedule-to-icalendar 300 | END:VALARM 301 | END:VEVENT 302 | BEGIN:VEVENT 303 | CREATED:20200825T054930Z 304 | LAST-MODIFIED:20200825T054930Z 305 | DTSTAMP:20200825T054930Z 306 | UID:d3581bc3-98c6-4563-85fb-868b666d3338 307 | SUMMARY:软件项目管理-某位置b 308 | DTSTART;TZID=Asia/Shanghai:20200317T095000 309 | DTEND;TZID=Asia/Shanghai:20200317T121500 310 | LOCATION:某位置b 311 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 312 | TRANSP:OPAQUE 313 | BEGIN:VALARM 314 | ACTION:DISPLAY 315 | TRIGGER;VALUE=DURATION:-PT15M 316 | DESCRIPTION:generated by class-schedule-to-icalendar 317 | END:VALARM 318 | END:VEVENT 319 | BEGIN:VEVENT 320 | CREATED:20200825T054930Z 321 | LAST-MODIFIED:20200825T054930Z 322 | DTSTAMP:20200825T054930Z 323 | UID:69d9850a-9dbc-42f3-8958-8a3d41681624 324 | SUMMARY:软件项目管理-某位置b 325 | DTSTART;TZID=Asia/Shanghai:20200324T095000 326 | DTEND;TZID=Asia/Shanghai:20200324T121500 327 | LOCATION:某位置b 328 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 329 | TRANSP:OPAQUE 330 | BEGIN:VALARM 331 | ACTION:DISPLAY 332 | TRIGGER;VALUE=DURATION:-PT15M 333 | DESCRIPTION:generated by class-schedule-to-icalendar 334 | END:VALARM 335 | END:VEVENT 336 | BEGIN:VEVENT 337 | CREATED:20200825T054930Z 338 | LAST-MODIFIED:20200825T054930Z 339 | DTSTAMP:20200825T054930Z 340 | UID:cff7ea8f-cfd3-4ef6-bc2e-4d3120e8c1a6 341 | SUMMARY:软件项目管理-某位置b 342 | DTSTART;TZID=Asia/Shanghai:20200331T095000 343 | DTEND;TZID=Asia/Shanghai:20200331T121500 344 | LOCATION:某位置b 345 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 346 | TRANSP:OPAQUE 347 | BEGIN:VALARM 348 | ACTION:DISPLAY 349 | TRIGGER;VALUE=DURATION:-PT15M 350 | DESCRIPTION:generated by class-schedule-to-icalendar 351 | END:VALARM 352 | END:VEVENT 353 | BEGIN:VEVENT 354 | CREATED:20200825T054930Z 355 | LAST-MODIFIED:20200825T054930Z 356 | DTSTAMP:20200825T054930Z 357 | UID:92a65980-bffe-4a30-a105-ef23b8cb82a1 358 | SUMMARY:软件项目管理-某位置b 359 | DTSTART;TZID=Asia/Shanghai:20200407T095000 360 | DTEND;TZID=Asia/Shanghai:20200407T121500 361 | LOCATION:某位置b 362 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 363 | TRANSP:OPAQUE 364 | BEGIN:VALARM 365 | ACTION:DISPLAY 366 | TRIGGER;VALUE=DURATION:-PT15M 367 | DESCRIPTION:generated by class-schedule-to-icalendar 368 | END:VALARM 369 | END:VEVENT 370 | BEGIN:VEVENT 371 | CREATED:20200825T054930Z 372 | LAST-MODIFIED:20200825T054930Z 373 | DTSTAMP:20200825T054930Z 374 | UID:a95bacc2-a135-4935-aed6-98d5a8275554 375 | SUMMARY:软件项目管理-某位置b 376 | DTSTART;TZID=Asia/Shanghai:20200414T095000 377 | DTEND;TZID=Asia/Shanghai:20200414T121500 378 | LOCATION:某位置b 379 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 380 | TRANSP:OPAQUE 381 | BEGIN:VALARM 382 | ACTION:DISPLAY 383 | TRIGGER;VALUE=DURATION:-PT15M 384 | DESCRIPTION:generated by class-schedule-to-icalendar 385 | END:VALARM 386 | END:VEVENT 387 | BEGIN:VEVENT 388 | CREATED:20200825T054930Z 389 | LAST-MODIFIED:20200825T054930Z 390 | DTSTAMP:20200825T054930Z 391 | UID:2cc80edc-2805-4148-b873-3c9665e6f0e6 392 | SUMMARY:软件项目管理-某位置b 393 | DTSTART;TZID=Asia/Shanghai:20200421T095000 394 | DTEND;TZID=Asia/Shanghai:20200421T121500 395 | LOCATION:某位置b 396 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 397 | TRANSP:OPAQUE 398 | BEGIN:VALARM 399 | ACTION:DISPLAY 400 | TRIGGER;VALUE=DURATION:-PT15M 401 | DESCRIPTION:generated by class-schedule-to-icalendar 402 | END:VALARM 403 | END:VEVENT 404 | BEGIN:VEVENT 405 | CREATED:20200825T054930Z 406 | LAST-MODIFIED:20200825T054930Z 407 | DTSTAMP:20200825T054930Z 408 | UID:056db1e4-8ce5-474b-83f5-203e8c5d7fa1 409 | SUMMARY:软件项目管理-某位置b 410 | DTSTART;TZID=Asia/Shanghai:20200428T095000 411 | DTEND;TZID=Asia/Shanghai:20200428T121500 412 | LOCATION:某位置b 413 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 414 | TRANSP:OPAQUE 415 | BEGIN:VALARM 416 | ACTION:DISPLAY 417 | TRIGGER;VALUE=DURATION:-PT15M 418 | DESCRIPTION:generated by class-schedule-to-icalendar 419 | END:VALARM 420 | END:VEVENT 421 | BEGIN:VEVENT 422 | CREATED:20200825T054930Z 423 | LAST-MODIFIED:20200825T054930Z 424 | DTSTAMP:20200825T054930Z 425 | UID:3ac7c157-5287-413b-a52b-fe9c5db3b983 426 | SUMMARY:软件项目管理-某位置b 427 | DTSTART;TZID=Asia/Shanghai:20200505T095000 428 | DTEND;TZID=Asia/Shanghai:20200505T121500 429 | LOCATION:某位置b 430 | DESCRIPTION:name:软件项目管理\nlocation:某位置b\nteacher:某教师b\ntype:专业必修\nremark:没有\nschedule:\n 1-11|2|3-5; 431 | TRANSP:OPAQUE 432 | BEGIN:VALARM 433 | ACTION:DISPLAY 434 | TRIGGER;VALUE=DURATION:-PT15M 435 | DESCRIPTION:generated by class-schedule-to-icalendar 436 | END:VALARM 437 | END:VEVENT 438 | BEGIN:VEVENT 439 | CREATED:20200825T054930Z 440 | LAST-MODIFIED:20200825T054930Z 441 | DTSTAMP:20200825T054930Z 442 | UID:6a34a894-9a15-408c-bbb0-36d0efd9e6d6 443 | SUMMARY:软件测试实践-某位置c 444 | DTSTART;TZID=Asia/Shanghai:20200225T152000 445 | DTEND;TZID=Asia/Shanghai:20200225T165500 446 | LOCATION:某位置c 447 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 448 | TRANSP:OPAQUE 449 | BEGIN:VALARM 450 | ACTION:DISPLAY 451 | TRIGGER;VALUE=DURATION:-PT15M 452 | DESCRIPTION:generated by class-schedule-to-icalendar 453 | END:VALARM 454 | END:VEVENT 455 | BEGIN:VEVENT 456 | CREATED:20200825T054930Z 457 | LAST-MODIFIED:20200825T054930Z 458 | DTSTAMP:20200825T054930Z 459 | UID:b5a65b41-1d1e-4b3c-b396-6aeae510d6b9 460 | SUMMARY:软件测试实践-某位置c 461 | DTSTART;TZID=Asia/Shanghai:20200310T152000 462 | DTEND;TZID=Asia/Shanghai:20200310T165500 463 | LOCATION:某位置c 464 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 465 | TRANSP:OPAQUE 466 | BEGIN:VALARM 467 | ACTION:DISPLAY 468 | TRIGGER;VALUE=DURATION:-PT15M 469 | DESCRIPTION:generated by class-schedule-to-icalendar 470 | END:VALARM 471 | END:VEVENT 472 | BEGIN:VEVENT 473 | CREATED:20200825T054930Z 474 | LAST-MODIFIED:20200825T054930Z 475 | DTSTAMP:20200825T054930Z 476 | UID:90a0505b-1e31-4f3a-8e72-0c360900c308 477 | SUMMARY:软件测试实践-某位置c 478 | DTSTART;TZID=Asia/Shanghai:20200324T152000 479 | DTEND;TZID=Asia/Shanghai:20200324T165500 480 | LOCATION:某位置c 481 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 482 | TRANSP:OPAQUE 483 | BEGIN:VALARM 484 | ACTION:DISPLAY 485 | TRIGGER;VALUE=DURATION:-PT15M 486 | DESCRIPTION:generated by class-schedule-to-icalendar 487 | END:VALARM 488 | END:VEVENT 489 | BEGIN:VEVENT 490 | CREATED:20200825T054930Z 491 | LAST-MODIFIED:20200825T054930Z 492 | DTSTAMP:20200825T054930Z 493 | UID:89349575-0948-4f62-8a47-1659ce8c4565 494 | SUMMARY:软件测试实践-某位置c 495 | DTSTART;TZID=Asia/Shanghai:20200407T152000 496 | DTEND;TZID=Asia/Shanghai:20200407T165500 497 | LOCATION:某位置c 498 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 499 | TRANSP:OPAQUE 500 | BEGIN:VALARM 501 | ACTION:DISPLAY 502 | TRIGGER;VALUE=DURATION:-PT15M 503 | DESCRIPTION:generated by class-schedule-to-icalendar 504 | END:VALARM 505 | END:VEVENT 506 | BEGIN:VEVENT 507 | CREATED:20200825T054930Z 508 | LAST-MODIFIED:20200825T054930Z 509 | DTSTAMP:20200825T054930Z 510 | UID:fea7f062-5d4d-4e09-9283-8d49f7bcaea4 511 | SUMMARY:软件测试实践-某位置c 512 | DTSTART;TZID=Asia/Shanghai:20200421T152000 513 | DTEND;TZID=Asia/Shanghai:20200421T165500 514 | LOCATION:某位置c 515 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 516 | TRANSP:OPAQUE 517 | BEGIN:VALARM 518 | ACTION:DISPLAY 519 | TRIGGER;VALUE=DURATION:-PT15M 520 | DESCRIPTION:generated by class-schedule-to-icalendar 521 | END:VALARM 522 | END:VEVENT 523 | BEGIN:VEVENT 524 | CREATED:20200825T054930Z 525 | LAST-MODIFIED:20200825T054930Z 526 | DTSTAMP:20200825T054930Z 527 | UID:5cef982f-3958-42e4-a2b3-db4cb3642620 528 | SUMMARY:软件测试实践-某位置c 529 | DTSTART;TZID=Asia/Shanghai:20200505T152000 530 | DTEND;TZID=Asia/Shanghai:20200505T165500 531 | LOCATION:某位置c 532 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 533 | TRANSP:OPAQUE 534 | BEGIN:VALARM 535 | ACTION:DISPLAY 536 | TRIGGER;VALUE=DURATION:-PT15M 537 | DESCRIPTION:generated by class-schedule-to-icalendar 538 | END:VALARM 539 | END:VEVENT 540 | BEGIN:VEVENT 541 | CREATED:20200825T054930Z 542 | LAST-MODIFIED:20200825T054930Z 543 | DTSTAMP:20200825T054930Z 544 | UID:36fb93ba-5eb7-40b6-8cf9-dfe801bf052d 545 | SUMMARY:软件测试实践-某位置c 546 | DTSTART;TZID=Asia/Shanghai:20200519T152000 547 | DTEND;TZID=Asia/Shanghai:20200519T165500 548 | LOCATION:某位置c 549 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 550 | TRANSP:OPAQUE 551 | BEGIN:VALARM 552 | ACTION:DISPLAY 553 | TRIGGER;VALUE=DURATION:-PT15M 554 | DESCRIPTION:generated by class-schedule-to-icalendar 555 | END:VALARM 556 | END:VEVENT 557 | BEGIN:VEVENT 558 | CREATED:20200825T054930Z 559 | LAST-MODIFIED:20200825T054930Z 560 | DTSTAMP:20200825T054930Z 561 | UID:d4e08cad-88c9-4bc7-ad58-2e0572f7671f 562 | SUMMARY:软件测试实践-某位置c 563 | DTSTART;TZID=Asia/Shanghai:20200602T152000 564 | DTEND;TZID=Asia/Shanghai:20200602T165500 565 | LOCATION:某位置c 566 | DESCRIPTION:name:软件测试实践\nlocation:某位置c\nteacher:某教师c\ntype:实践选修\nremark:无\nschedule:\n 1,3,5,7,9,11,13,15|2|8-9; 567 | TRANSP:OPAQUE 568 | BEGIN:VALARM 569 | ACTION:DISPLAY 570 | TRIGGER;VALUE=DURATION:-PT15M 571 | DESCRIPTION:generated by class-schedule-to-icalendar 572 | END:VALARM 573 | END:VEVENT 574 | BEGIN:VEVENT 575 | CREATED:20200825T054930Z 576 | LAST-MODIFIED:20200825T054930Z 577 | DTSTAMP:20200825T054930Z 578 | UID:9f5c149f-e4fd-4cba-b5d2-dafeb9dceb9f 579 | SUMMARY:高级软件工程-某位置d 580 | DTSTART;TZID=Asia/Shanghai:20200226T095000 581 | DTEND;TZID=Asia/Shanghai:20200226T121500 582 | LOCATION:某位置d 583 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 584 | TRANSP:OPAQUE 585 | BEGIN:VALARM 586 | ACTION:DISPLAY 587 | TRIGGER;VALUE=DURATION:-PT15M 588 | DESCRIPTION:generated by class-schedule-to-icalendar 589 | END:VALARM 590 | END:VEVENT 591 | BEGIN:VEVENT 592 | CREATED:20200825T054930Z 593 | LAST-MODIFIED:20200825T054930Z 594 | DTSTAMP:20200825T054930Z 595 | UID:1f90e33b-0d47-41cd-ba1e-c58791bc3151 596 | SUMMARY:高级软件工程-某位置d 597 | DTSTART;TZID=Asia/Shanghai:20200304T095000 598 | DTEND;TZID=Asia/Shanghai:20200304T121500 599 | LOCATION:某位置d 600 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 601 | TRANSP:OPAQUE 602 | BEGIN:VALARM 603 | ACTION:DISPLAY 604 | TRIGGER;VALUE=DURATION:-PT15M 605 | DESCRIPTION:generated by class-schedule-to-icalendar 606 | END:VALARM 607 | END:VEVENT 608 | BEGIN:VEVENT 609 | CREATED:20200825T054930Z 610 | LAST-MODIFIED:20200825T054930Z 611 | DTSTAMP:20200825T054930Z 612 | UID:0ee22857-57bf-4c87-ab42-256a38747a4a 613 | SUMMARY:高级软件工程-某位置d 614 | DTSTART;TZID=Asia/Shanghai:20200311T095000 615 | DTEND;TZID=Asia/Shanghai:20200311T121500 616 | LOCATION:某位置d 617 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 618 | TRANSP:OPAQUE 619 | BEGIN:VALARM 620 | ACTION:DISPLAY 621 | TRIGGER;VALUE=DURATION:-PT15M 622 | DESCRIPTION:generated by class-schedule-to-icalendar 623 | END:VALARM 624 | END:VEVENT 625 | BEGIN:VEVENT 626 | CREATED:20200825T054930Z 627 | LAST-MODIFIED:20200825T054930Z 628 | DTSTAMP:20200825T054930Z 629 | UID:53598602-b361-4527-8e1d-fe5308956871 630 | SUMMARY:高级软件工程-某位置d 631 | DTSTART;TZID=Asia/Shanghai:20200318T095000 632 | DTEND;TZID=Asia/Shanghai:20200318T121500 633 | LOCATION:某位置d 634 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 635 | TRANSP:OPAQUE 636 | BEGIN:VALARM 637 | ACTION:DISPLAY 638 | TRIGGER;VALUE=DURATION:-PT15M 639 | DESCRIPTION:generated by class-schedule-to-icalendar 640 | END:VALARM 641 | END:VEVENT 642 | BEGIN:VEVENT 643 | CREATED:20200825T054930Z 644 | LAST-MODIFIED:20200825T054930Z 645 | DTSTAMP:20200825T054930Z 646 | UID:6ad4fb37-2df7-4a2a-a0f3-165237fbecc9 647 | SUMMARY:高级软件工程-某位置d 648 | DTSTART;TZID=Asia/Shanghai:20200325T095000 649 | DTEND;TZID=Asia/Shanghai:20200325T121500 650 | LOCATION:某位置d 651 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 652 | TRANSP:OPAQUE 653 | BEGIN:VALARM 654 | ACTION:DISPLAY 655 | TRIGGER;VALUE=DURATION:-PT15M 656 | DESCRIPTION:generated by class-schedule-to-icalendar 657 | END:VALARM 658 | END:VEVENT 659 | BEGIN:VEVENT 660 | CREATED:20200825T054930Z 661 | LAST-MODIFIED:20200825T054930Z 662 | DTSTAMP:20200825T054930Z 663 | UID:f9d2cf83-eb41-4790-b3a3-98d5c9ecd26b 664 | SUMMARY:高级软件工程-某位置d 665 | DTSTART;TZID=Asia/Shanghai:20200401T095000 666 | DTEND;TZID=Asia/Shanghai:20200401T121500 667 | LOCATION:某位置d 668 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 669 | TRANSP:OPAQUE 670 | BEGIN:VALARM 671 | ACTION:DISPLAY 672 | TRIGGER;VALUE=DURATION:-PT15M 673 | DESCRIPTION:generated by class-schedule-to-icalendar 674 | END:VALARM 675 | END:VEVENT 676 | BEGIN:VEVENT 677 | CREATED:20200825T054930Z 678 | LAST-MODIFIED:20200825T054930Z 679 | DTSTAMP:20200825T054930Z 680 | UID:cba6c14c-80b6-42a1-beeb-e5edeab0c7a0 681 | SUMMARY:高级软件工程-某位置d 682 | DTSTART;TZID=Asia/Shanghai:20200408T095000 683 | DTEND;TZID=Asia/Shanghai:20200408T121500 684 | LOCATION:某位置d 685 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 686 | TRANSP:OPAQUE 687 | BEGIN:VALARM 688 | ACTION:DISPLAY 689 | TRIGGER;VALUE=DURATION:-PT15M 690 | DESCRIPTION:generated by class-schedule-to-icalendar 691 | END:VALARM 692 | END:VEVENT 693 | BEGIN:VEVENT 694 | CREATED:20200825T054930Z 695 | LAST-MODIFIED:20200825T054930Z 696 | DTSTAMP:20200825T054930Z 697 | UID:7a8d51b0-176a-4fee-9e3e-2efaaebd362c 698 | SUMMARY:高级软件工程-某位置d 699 | DTSTART;TZID=Asia/Shanghai:20200415T095000 700 | DTEND;TZID=Asia/Shanghai:20200415T121500 701 | LOCATION:某位置d 702 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 703 | TRANSP:OPAQUE 704 | BEGIN:VALARM 705 | ACTION:DISPLAY 706 | TRIGGER;VALUE=DURATION:-PT15M 707 | DESCRIPTION:generated by class-schedule-to-icalendar 708 | END:VALARM 709 | END:VEVENT 710 | BEGIN:VEVENT 711 | CREATED:20200825T054930Z 712 | LAST-MODIFIED:20200825T054930Z 713 | DTSTAMP:20200825T054930Z 714 | UID:14989bec-6f6a-4736-a0ea-efcfafee7651 715 | SUMMARY:高级软件工程-某位置d 716 | DTSTART;TZID=Asia/Shanghai:20200422T095000 717 | DTEND;TZID=Asia/Shanghai:20200422T121500 718 | LOCATION:某位置d 719 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 720 | TRANSP:OPAQUE 721 | BEGIN:VALARM 722 | ACTION:DISPLAY 723 | TRIGGER;VALUE=DURATION:-PT15M 724 | DESCRIPTION:generated by class-schedule-to-icalendar 725 | END:VALARM 726 | END:VEVENT 727 | BEGIN:VEVENT 728 | CREATED:20200825T054930Z 729 | LAST-MODIFIED:20200825T054930Z 730 | DTSTAMP:20200825T054930Z 731 | UID:eec53e5e-b0e9-4f6b-9f40-0e0906c15950 732 | SUMMARY:高级软件工程-某位置d 733 | DTSTART;TZID=Asia/Shanghai:20200429T095000 734 | DTEND;TZID=Asia/Shanghai:20200429T121500 735 | LOCATION:某位置d 736 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 737 | TRANSP:OPAQUE 738 | BEGIN:VALARM 739 | ACTION:DISPLAY 740 | TRIGGER;VALUE=DURATION:-PT15M 741 | DESCRIPTION:generated by class-schedule-to-icalendar 742 | END:VALARM 743 | END:VEVENT 744 | BEGIN:VEVENT 745 | CREATED:20200825T054930Z 746 | LAST-MODIFIED:20200825T054930Z 747 | DTSTAMP:20200825T054930Z 748 | UID:9072ff28-2cdd-4221-b3b5-d0827ca46c69 749 | SUMMARY:高级软件工程-某位置d 750 | DTSTART;TZID=Asia/Shanghai:20200506T095000 751 | DTEND;TZID=Asia/Shanghai:20200506T121500 752 | LOCATION:某位置d 753 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 754 | TRANSP:OPAQUE 755 | BEGIN:VALARM 756 | ACTION:DISPLAY 757 | TRIGGER;VALUE=DURATION:-PT15M 758 | DESCRIPTION:generated by class-schedule-to-icalendar 759 | END:VALARM 760 | END:VEVENT 761 | BEGIN:VEVENT 762 | CREATED:20200825T054930Z 763 | LAST-MODIFIED:20200825T054930Z 764 | DTSTAMP:20200825T054930Z 765 | UID:d9bfd4b4-5ce6-4a4e-9f5d-e99ede07e81b 766 | SUMMARY:高级软件工程-某位置d 767 | DTSTART;TZID=Asia/Shanghai:20200513T095000 768 | DTEND;TZID=Asia/Shanghai:20200513T121500 769 | LOCATION:某位置d 770 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 771 | TRANSP:OPAQUE 772 | BEGIN:VALARM 773 | ACTION:DISPLAY 774 | TRIGGER;VALUE=DURATION:-PT15M 775 | DESCRIPTION:generated by class-schedule-to-icalendar 776 | END:VALARM 777 | END:VEVENT 778 | BEGIN:VEVENT 779 | CREATED:20200825T054930Z 780 | LAST-MODIFIED:20200825T054930Z 781 | DTSTAMP:20200825T054930Z 782 | UID:ff5749b8-69b7-4ea9-a44e-4ac5a1ac451d 783 | SUMMARY:高级软件工程-某位置d 784 | DTSTART;TZID=Asia/Shanghai:20200520T095000 785 | DTEND;TZID=Asia/Shanghai:20200520T121500 786 | LOCATION:某位置d 787 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 788 | TRANSP:OPAQUE 789 | BEGIN:VALARM 790 | ACTION:DISPLAY 791 | TRIGGER;VALUE=DURATION:-PT15M 792 | DESCRIPTION:generated by class-schedule-to-icalendar 793 | END:VALARM 794 | END:VEVENT 795 | BEGIN:VEVENT 796 | CREATED:20200825T054930Z 797 | LAST-MODIFIED:20200825T054930Z 798 | DTSTAMP:20200825T054930Z 799 | UID:bedff8a5-3cf2-41a5-ae12-81b41d0788c9 800 | SUMMARY:高级软件工程-某位置d 801 | DTSTART;TZID=Asia/Shanghai:20200527T095000 802 | DTEND;TZID=Asia/Shanghai:20200527T121500 803 | LOCATION:某位置d 804 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 805 | TRANSP:OPAQUE 806 | BEGIN:VALARM 807 | ACTION:DISPLAY 808 | TRIGGER;VALUE=DURATION:-PT15M 809 | DESCRIPTION:generated by class-schedule-to-icalendar 810 | END:VALARM 811 | END:VEVENT 812 | BEGIN:VEVENT 813 | CREATED:20200825T054930Z 814 | LAST-MODIFIED:20200825T054930Z 815 | DTSTAMP:20200825T054930Z 816 | UID:33102261-656a-46be-9ac6-9b3ff963a739 817 | SUMMARY:高级软件工程-某位置d 818 | DTSTART;TZID=Asia/Shanghai:20200603T095000 819 | DTEND;TZID=Asia/Shanghai:20200603T121500 820 | LOCATION:某位置d 821 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 822 | TRANSP:OPAQUE 823 | BEGIN:VALARM 824 | ACTION:DISPLAY 825 | TRIGGER;VALUE=DURATION:-PT15M 826 | DESCRIPTION:generated by class-schedule-to-icalendar 827 | END:VALARM 828 | END:VEVENT 829 | BEGIN:VEVENT 830 | CREATED:20200825T054930Z 831 | LAST-MODIFIED:20200825T054930Z 832 | DTSTAMP:20200825T054930Z 833 | UID:68e7978b-a983-4460-8236-73974edaa45d 834 | SUMMARY:高级软件工程-某位置d 835 | DTSTART;TZID=Asia/Shanghai:20200610T095000 836 | DTEND;TZID=Asia/Shanghai:20200610T121500 837 | LOCATION:某位置d 838 | DESCRIPTION:name:高级软件工程\nlocation:某位置d\nteacher:某教师d\ntype:专业必修\nremark:无\nschedule:\n 1-16|3|3-5; 839 | TRANSP:OPAQUE 840 | BEGIN:VALARM 841 | ACTION:DISPLAY 842 | TRIGGER;VALUE=DURATION:-PT15M 843 | DESCRIPTION:generated by class-schedule-to-icalendar 844 | END:VALARM 845 | END:VEVENT 846 | BEGIN:VEVENT 847 | CREATED:20200825T054930Z 848 | LAST-MODIFIED:20200825T054930Z 849 | DTSTAMP:20200825T054930Z 850 | UID:b01e4d46-e39c-4416-868e-bbc03adfc9ca 851 | SUMMARY:编译原理-某位置e 852 | DTSTART;TZID=Asia/Shanghai:20200227T095000 853 | DTEND;TZID=Asia/Shanghai:20200227T121500 854 | LOCATION:某位置e 855 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 856 | TRANSP:OPAQUE 857 | BEGIN:VALARM 858 | ACTION:DISPLAY 859 | TRIGGER;VALUE=DURATION:-PT15M 860 | DESCRIPTION:generated by class-schedule-to-icalendar 861 | END:VALARM 862 | END:VEVENT 863 | BEGIN:VEVENT 864 | CREATED:20200825T054930Z 865 | LAST-MODIFIED:20200825T054930Z 866 | DTSTAMP:20200825T054930Z 867 | UID:70956d17-30cd-47a6-82e3-fc333bfee675 868 | SUMMARY:编译原理-某位置e 869 | DTSTART;TZID=Asia/Shanghai:20200305T095000 870 | DTEND;TZID=Asia/Shanghai:20200305T121500 871 | LOCATION:某位置e 872 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 873 | TRANSP:OPAQUE 874 | BEGIN:VALARM 875 | ACTION:DISPLAY 876 | TRIGGER;VALUE=DURATION:-PT15M 877 | DESCRIPTION:generated by class-schedule-to-icalendar 878 | END:VALARM 879 | END:VEVENT 880 | BEGIN:VEVENT 881 | CREATED:20200825T054930Z 882 | LAST-MODIFIED:20200825T054930Z 883 | DTSTAMP:20200825T054930Z 884 | UID:d9b69f6c-1101-4e06-b540-cc056ec7b078 885 | SUMMARY:编译原理-某位置e 886 | DTSTART;TZID=Asia/Shanghai:20200312T095000 887 | DTEND;TZID=Asia/Shanghai:20200312T121500 888 | LOCATION:某位置e 889 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 890 | TRANSP:OPAQUE 891 | BEGIN:VALARM 892 | ACTION:DISPLAY 893 | TRIGGER;VALUE=DURATION:-PT15M 894 | DESCRIPTION:generated by class-schedule-to-icalendar 895 | END:VALARM 896 | END:VEVENT 897 | BEGIN:VEVENT 898 | CREATED:20200825T054930Z 899 | LAST-MODIFIED:20200825T054930Z 900 | DTSTAMP:20200825T054930Z 901 | UID:7b956f87-5be3-4198-b16e-e7e7a271b510 902 | SUMMARY:编译原理-某位置e 903 | DTSTART;TZID=Asia/Shanghai:20200319T095000 904 | DTEND;TZID=Asia/Shanghai:20200319T121500 905 | LOCATION:某位置e 906 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 907 | TRANSP:OPAQUE 908 | BEGIN:VALARM 909 | ACTION:DISPLAY 910 | TRIGGER;VALUE=DURATION:-PT15M 911 | DESCRIPTION:generated by class-schedule-to-icalendar 912 | END:VALARM 913 | END:VEVENT 914 | BEGIN:VEVENT 915 | CREATED:20200825T054930Z 916 | LAST-MODIFIED:20200825T054930Z 917 | DTSTAMP:20200825T054930Z 918 | UID:096adc0a-147e-46c8-910d-736cd276491b 919 | SUMMARY:编译原理-某位置e 920 | DTSTART;TZID=Asia/Shanghai:20200326T095000 921 | DTEND;TZID=Asia/Shanghai:20200326T121500 922 | LOCATION:某位置e 923 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 924 | TRANSP:OPAQUE 925 | BEGIN:VALARM 926 | ACTION:DISPLAY 927 | TRIGGER;VALUE=DURATION:-PT15M 928 | DESCRIPTION:generated by class-schedule-to-icalendar 929 | END:VALARM 930 | END:VEVENT 931 | BEGIN:VEVENT 932 | CREATED:20200825T054930Z 933 | LAST-MODIFIED:20200825T054930Z 934 | DTSTAMP:20200825T054930Z 935 | UID:d32c210c-0a42-4e81-a0f1-ed29ed0eab65 936 | SUMMARY:编译原理-某位置e 937 | DTSTART;TZID=Asia/Shanghai:20200402T095000 938 | DTEND;TZID=Asia/Shanghai:20200402T121500 939 | LOCATION:某位置e 940 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 941 | TRANSP:OPAQUE 942 | BEGIN:VALARM 943 | ACTION:DISPLAY 944 | TRIGGER;VALUE=DURATION:-PT15M 945 | DESCRIPTION:generated by class-schedule-to-icalendar 946 | END:VALARM 947 | END:VEVENT 948 | BEGIN:VEVENT 949 | CREATED:20200825T054930Z 950 | LAST-MODIFIED:20200825T054930Z 951 | DTSTAMP:20200825T054930Z 952 | UID:34d2bfd4-44b0-4436-b0aa-c7cdd759f2ad 953 | SUMMARY:编译原理-某位置e 954 | DTSTART;TZID=Asia/Shanghai:20200409T095000 955 | DTEND;TZID=Asia/Shanghai:20200409T121500 956 | LOCATION:某位置e 957 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 958 | TRANSP:OPAQUE 959 | BEGIN:VALARM 960 | ACTION:DISPLAY 961 | TRIGGER;VALUE=DURATION:-PT15M 962 | DESCRIPTION:generated by class-schedule-to-icalendar 963 | END:VALARM 964 | END:VEVENT 965 | BEGIN:VEVENT 966 | CREATED:20200825T054930Z 967 | LAST-MODIFIED:20200825T054930Z 968 | DTSTAMP:20200825T054930Z 969 | UID:081566c4-27c1-4cd1-b40c-6274c25cdd65 970 | SUMMARY:编译原理-某位置e 971 | DTSTART;TZID=Asia/Shanghai:20200416T095000 972 | DTEND;TZID=Asia/Shanghai:20200416T121500 973 | LOCATION:某位置e 974 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 975 | TRANSP:OPAQUE 976 | BEGIN:VALARM 977 | ACTION:DISPLAY 978 | TRIGGER;VALUE=DURATION:-PT15M 979 | DESCRIPTION:generated by class-schedule-to-icalendar 980 | END:VALARM 981 | END:VEVENT 982 | BEGIN:VEVENT 983 | CREATED:20200825T054930Z 984 | LAST-MODIFIED:20200825T054930Z 985 | DTSTAMP:20200825T054930Z 986 | UID:2492d6d6-79b4-421c-8431-3a6b9a73d69f 987 | SUMMARY:编译原理-某位置e 988 | DTSTART;TZID=Asia/Shanghai:20200423T095000 989 | DTEND;TZID=Asia/Shanghai:20200423T121500 990 | LOCATION:某位置e 991 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 992 | TRANSP:OPAQUE 993 | BEGIN:VALARM 994 | ACTION:DISPLAY 995 | TRIGGER;VALUE=DURATION:-PT15M 996 | DESCRIPTION:generated by class-schedule-to-icalendar 997 | END:VALARM 998 | END:VEVENT 999 | BEGIN:VEVENT 1000 | CREATED:20200825T054930Z 1001 | LAST-MODIFIED:20200825T054930Z 1002 | DTSTAMP:20200825T054930Z 1003 | UID:42b44393-fcf2-4c33-bdb4-a28edd4d730d 1004 | SUMMARY:编译原理-某位置e 1005 | DTSTART;TZID=Asia/Shanghai:20200430T095000 1006 | DTEND;TZID=Asia/Shanghai:20200430T121500 1007 | LOCATION:某位置e 1008 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 1009 | TRANSP:OPAQUE 1010 | BEGIN:VALARM 1011 | ACTION:DISPLAY 1012 | TRIGGER;VALUE=DURATION:-PT15M 1013 | DESCRIPTION:generated by class-schedule-to-icalendar 1014 | END:VALARM 1015 | END:VEVENT 1016 | BEGIN:VEVENT 1017 | CREATED:20200825T054930Z 1018 | LAST-MODIFIED:20200825T054930Z 1019 | DTSTAMP:20200825T054930Z 1020 | UID:449cd05b-ffc9-4c21-be0d-91e201974a5f 1021 | SUMMARY:编译原理-某位置e 1022 | DTSTART;TZID=Asia/Shanghai:20200507T095000 1023 | DTEND;TZID=Asia/Shanghai:20200507T121500 1024 | LOCATION:某位置e 1025 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 1026 | TRANSP:OPAQUE 1027 | BEGIN:VALARM 1028 | ACTION:DISPLAY 1029 | TRIGGER;VALUE=DURATION:-PT15M 1030 | DESCRIPTION:generated by class-schedule-to-icalendar 1031 | END:VALARM 1032 | END:VEVENT 1033 | BEGIN:VEVENT 1034 | CREATED:20200825T054930Z 1035 | LAST-MODIFIED:20200825T054930Z 1036 | DTSTAMP:20200825T054930Z 1037 | UID:c8fec0f4-9948-41d5-a73a-ba5da6d6e2a1 1038 | SUMMARY:编译原理-某位置e 1039 | DTSTART;TZID=Asia/Shanghai:20200514T095000 1040 | DTEND;TZID=Asia/Shanghai:20200514T121500 1041 | LOCATION:某位置e 1042 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 1043 | TRANSP:OPAQUE 1044 | BEGIN:VALARM 1045 | ACTION:DISPLAY 1046 | TRIGGER;VALUE=DURATION:-PT15M 1047 | DESCRIPTION:generated by class-schedule-to-icalendar 1048 | END:VALARM 1049 | END:VEVENT 1050 | BEGIN:VEVENT 1051 | CREATED:20200825T054930Z 1052 | LAST-MODIFIED:20200825T054930Z 1053 | DTSTAMP:20200825T054930Z 1054 | UID:1320497c-6a2e-4742-8e40-2ec75e44e23b 1055 | SUMMARY:编译原理-某位置e 1056 | DTSTART;TZID=Asia/Shanghai:20200521T095000 1057 | DTEND;TZID=Asia/Shanghai:20200521T121500 1058 | LOCATION:某位置e 1059 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 1060 | TRANSP:OPAQUE 1061 | BEGIN:VALARM 1062 | ACTION:DISPLAY 1063 | TRIGGER;VALUE=DURATION:-PT15M 1064 | DESCRIPTION:generated by class-schedule-to-icalendar 1065 | END:VALARM 1066 | END:VEVENT 1067 | BEGIN:VEVENT 1068 | CREATED:20200825T054930Z 1069 | LAST-MODIFIED:20200825T054930Z 1070 | DTSTAMP:20200825T054930Z 1071 | UID:ef69f253-37c5-4d32-91bf-737a7c72463e 1072 | SUMMARY:编译原理-某位置e 1073 | DTSTART;TZID=Asia/Shanghai:20200528T095000 1074 | DTEND;TZID=Asia/Shanghai:20200528T121500 1075 | LOCATION:某位置e 1076 | DESCRIPTION:name:编译原理\nlocation:某位置e\nteacher:某教师e\ntype:专业必修\nremark:无\nschedule:\n 1-12|4|3-5;\n 13-14|4|3-5; 1077 | TRANSP:OPAQUE 1078 | BEGIN:VALARM 1079 | ACTION:DISPLAY 1080 | TRIGGER;VALUE=DURATION:-PT15M 1081 | DESCRIPTION:generated by class-schedule-to-icalendar 1082 | END:VALARM 1083 | END:VEVENT 1084 | BEGIN:VEVENT 1085 | CREATED:20200825T054930Z 1086 | LAST-MODIFIED:20200825T054930Z 1087 | DTSTAMP:20200825T054930Z 1088 | UID:f3b200bb-dbf6-4c21-ba81-ad4782fdbd7a 1089 | SUMMARY:软件开发实践-某位置f 1090 | DTSTART;TZID=Asia/Shanghai:20200301T133000 1091 | DTEND;TZID=Asia/Shanghai:20200301T150500 1092 | LOCATION:某位置f 1093 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1094 | TRANSP:OPAQUE 1095 | BEGIN:VALARM 1096 | ACTION:DISPLAY 1097 | TRIGGER;VALUE=DURATION:-PT15M 1098 | DESCRIPTION:generated by class-schedule-to-icalendar 1099 | END:VALARM 1100 | END:VEVENT 1101 | BEGIN:VEVENT 1102 | CREATED:20200825T054930Z 1103 | LAST-MODIFIED:20200825T054930Z 1104 | DTSTAMP:20200825T054930Z 1105 | UID:e59a600f-d014-4ed7-a871-04056980c0f0 1106 | SUMMARY:软件开发实践-某位置f 1107 | DTSTART;TZID=Asia/Shanghai:20200308T133000 1108 | DTEND;TZID=Asia/Shanghai:20200308T150500 1109 | LOCATION:某位置f 1110 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1111 | TRANSP:OPAQUE 1112 | BEGIN:VALARM 1113 | ACTION:DISPLAY 1114 | TRIGGER;VALUE=DURATION:-PT15M 1115 | DESCRIPTION:generated by class-schedule-to-icalendar 1116 | END:VALARM 1117 | END:VEVENT 1118 | BEGIN:VEVENT 1119 | CREATED:20200825T054930Z 1120 | LAST-MODIFIED:20200825T054930Z 1121 | DTSTAMP:20200825T054930Z 1122 | UID:9b0ba147-caf0-472b-9702-62095659828a 1123 | SUMMARY:软件开发实践-某位置f 1124 | DTSTART;TZID=Asia/Shanghai:20200315T133000 1125 | DTEND;TZID=Asia/Shanghai:20200315T150500 1126 | LOCATION:某位置f 1127 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1128 | TRANSP:OPAQUE 1129 | BEGIN:VALARM 1130 | ACTION:DISPLAY 1131 | TRIGGER;VALUE=DURATION:-PT15M 1132 | DESCRIPTION:generated by class-schedule-to-icalendar 1133 | END:VALARM 1134 | END:VEVENT 1135 | BEGIN:VEVENT 1136 | CREATED:20200825T054930Z 1137 | LAST-MODIFIED:20200825T054930Z 1138 | DTSTAMP:20200825T054930Z 1139 | UID:11e8ff0b-8959-473f-a0f6-3cd8705a6163 1140 | SUMMARY:软件开发实践-某位置f 1141 | DTSTART;TZID=Asia/Shanghai:20200322T133000 1142 | DTEND;TZID=Asia/Shanghai:20200322T150500 1143 | LOCATION:某位置f 1144 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1145 | TRANSP:OPAQUE 1146 | BEGIN:VALARM 1147 | ACTION:DISPLAY 1148 | TRIGGER;VALUE=DURATION:-PT15M 1149 | DESCRIPTION:generated by class-schedule-to-icalendar 1150 | END:VALARM 1151 | END:VEVENT 1152 | BEGIN:VEVENT 1153 | CREATED:20200825T054930Z 1154 | LAST-MODIFIED:20200825T054930Z 1155 | DTSTAMP:20200825T054930Z 1156 | UID:be8b2025-5109-4da0-95ac-474128ed80bf 1157 | SUMMARY:软件开发实践-某位置f 1158 | DTSTART;TZID=Asia/Shanghai:20200301T152000 1159 | DTEND;TZID=Asia/Shanghai:20200301T165500 1160 | LOCATION:某位置f 1161 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1162 | TRANSP:OPAQUE 1163 | BEGIN:VALARM 1164 | ACTION:DISPLAY 1165 | TRIGGER;VALUE=DURATION:-PT15M 1166 | DESCRIPTION:generated by class-schedule-to-icalendar 1167 | END:VALARM 1168 | END:VEVENT 1169 | BEGIN:VEVENT 1170 | CREATED:20200825T054930Z 1171 | LAST-MODIFIED:20200825T054930Z 1172 | DTSTAMP:20200825T054930Z 1173 | UID:26b96bb3-5268-4101-83c6-3dd14d810e16 1174 | SUMMARY:软件开发实践-某位置f 1175 | DTSTART;TZID=Asia/Shanghai:20200308T152000 1176 | DTEND;TZID=Asia/Shanghai:20200308T165500 1177 | LOCATION:某位置f 1178 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1179 | TRANSP:OPAQUE 1180 | BEGIN:VALARM 1181 | ACTION:DISPLAY 1182 | TRIGGER;VALUE=DURATION:-PT15M 1183 | DESCRIPTION:generated by class-schedule-to-icalendar 1184 | END:VALARM 1185 | END:VEVENT 1186 | BEGIN:VEVENT 1187 | CREATED:20200825T054930Z 1188 | LAST-MODIFIED:20200825T054930Z 1189 | DTSTAMP:20200825T054930Z 1190 | UID:18b20157-1e64-4624-8602-530b39deabf6 1191 | SUMMARY:软件开发实践-某位置f 1192 | DTSTART;TZID=Asia/Shanghai:20200315T152000 1193 | DTEND;TZID=Asia/Shanghai:20200315T165500 1194 | LOCATION:某位置f 1195 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1196 | TRANSP:OPAQUE 1197 | BEGIN:VALARM 1198 | ACTION:DISPLAY 1199 | TRIGGER;VALUE=DURATION:-PT15M 1200 | DESCRIPTION:generated by class-schedule-to-icalendar 1201 | END:VALARM 1202 | END:VEVENT 1203 | BEGIN:VEVENT 1204 | CREATED:20200825T054930Z 1205 | LAST-MODIFIED:20200825T054930Z 1206 | DTSTAMP:20200825T054930Z 1207 | UID:b3de3297-c547-4055-9bd6-f676657ae0fc 1208 | SUMMARY:软件开发实践-某位置f 1209 | DTSTART;TZID=Asia/Shanghai:20200322T152000 1210 | DTEND;TZID=Asia/Shanghai:20200322T165500 1211 | LOCATION:某位置f 1212 | DESCRIPTION:name:软件开发实践\nlocation:某位置f\nteacher:某教师f\ntype:专业实践\nremark:无\nschedule:\n 1-4|0|6-7;\n 1-4|0|8-9; 1213 | TRANSP:OPAQUE 1214 | BEGIN:VALARM 1215 | ACTION:DISPLAY 1216 | TRIGGER;VALUE=DURATION:-PT15M 1217 | DESCRIPTION:generated by class-schedule-to-icalendar 1218 | END:VALARM 1219 | END:VEVENT 1220 | END:VCALENDAR --------------------------------------------------------------------------------