diff --git a/documents/Principle_zh-CN.md b/documents/Principle_zh-CN.md index b778c80..451e0b1 100644 --- a/documents/Principle_zh-CN.md +++ b/documents/Principle_zh-CN.md @@ -580,7 +580,7 @@ Admin类的操作不涉及任何客户端存储,因此不需要lastChange来 #### 按年 -格式:`Y[R|F][span]` +格式:`Y[S|R][span]` 每间隔`[span]`年在同样的月份和日期进行循环。`[S|R]`则表示在严格模式(Strict mode)和粗略模式(Rough mode)中的选择。 假设在某个闰年,在2月29日设置3年循环一次,若选择严格模式,则实际上是12年循环一次(不考虑400年非闰),也就是不存在的日子则无视。而选择粗略模式,则将会在不存在的日子将事件设置在2月28日。 @@ -589,10 +589,10 @@ Admin类的操作不涉及任何客户端存储,因此不需要lastChange来 按月有4种格式 -* 每月第`x`天:`M[R|F]A[span]` -* 每月倒数第`x`天:`M[R|F]B[span]` -* 每月第`x`个星期`y`:`M[R|F]C[span]` -* 每月倒数第`x`个星期`y`:`M[R|F]D[span]` +* 每月第`x`天:`M[S|R]A[span]` +* 每月倒数第`x`天:`M[S|R]B[span]` +* 每月第`x`个星期`y`:`M[S|R]C[span]` +* 每月倒数第`x`个星期`y`:`M[S|R]D[span]` `[span]`表示每隔多少个月处理一次此类事件。 需要注意相关数字的钳制,此种类型的事件循环也是算力消耗最大的。 `[S|R]`则表示在严格模式(Strict mode)和粗略模式(Rough mode)中的选择。 diff --git a/src/database.py b/src/database.py index bb3e955..0d45e6b 100644 --- a/src/database.py +++ b/src/database.py @@ -278,8 +278,15 @@ class CalendarDatabase(object): analyseData[7] = cache if reAnalyseLoop: - pass - # todo: finish this, re-compute loop data and upload it into list + # re-compute loop data and upload it into list + sqlList.append('[ccn_loopDateTimeStart] = ?') + argumentsList.append(analyseData[5]) + sqlList.append('[ccn_loopDateTimeEnd] = ?') + argumentsList.append(dt.ResolveLoopStr( + analyseData[8], + analyseData[5], + analyseData[7] + )) # execute argumentsList.append(uuid) @@ -296,9 +303,9 @@ class CalendarDatabase(object): newuuid = utils.GenerateUUID() lastupdate = utils.GenerateUUID() - # todo: analyse loopRules and output following 2 fileds. + # analyse loopRules and output following 2 fileds. loopDateTimeStart = eventDateTimeStart - loopDateTimeEnd = eventDateTimeEnd + loopDateTimeEnd = dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset) self.cursor.execute('INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', (newuuid, diff --git a/src/dt.py b/src/dt.py index 7e6058a..d6ee2d0 100644 --- a/src/dt.py +++ b/src/dt.py @@ -2,9 +2,14 @@ import datetime import time import re from functools import reduce +import utils -MIN_TIMESTAMP = int(datetime.datetime(1950, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc).timestamp() / 60) -MAX_TIMESTAMP = int(datetime.datetime(2200, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc).timestamp() / 60) +MonthDayCount = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + +MIN_DATETIME = datetime.datetime(1950, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) +MAX_DATETIME = datetime.datetime(2200, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) +MIN_TIMESTAMP = int(MIN_DATETIME.timestamp() / 60) +MAX_TIMESTAMP = int(MAX_DATETIME.timestamp() / 60) DAY1_SPAN = 60 * 24 DAY7_SPAN = 7 * DAY1_SPAN YEAR400_SPAN = DAY1_SPAN * 400 * 365 @@ -12,7 +17,7 @@ YEAR400_SPAN = DAY1_SPAN * 400 * 365 def ResolveLoopStr(strl, starttime, tzoffset): # check no loop if strl == '': - return starttime + 1 + return starttime # try compute from loopStop (loopRules, loopStopRules) = strl.split('-') @@ -37,10 +42,96 @@ def ResolveLoopStr(strl, starttime, tzoffset): def LoopHandle_Year(searchResult, starttime, times, tzoffset): - pass + clientDate = datetime.datetime.fromtimestamp(starttime, UTCTimezone(tzoffset)) + isStrict = searchResult.group(1) == 'S' + yearSpan = int(searchResult.group(2)) + + newYear = clientYear = clientDate.year + newMonth = clientMonth = clientDate.month + newDay = clientDay = clientDate.day + if clientMonth == 2 and clientDay == 29: + if isStrict: + realSpan = utils.LCM(yearSpan, 4) + valCache = starttime + timesCache = times - 1 + while valCache < MAX_TIMESTAMP and timesCache > 0: + newYear += realSpan + if not IsLeapYear(newYear): + continue + valCache = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) + timesCache -= 1 + else: + newYear = 0 if times == 1 else (times * yearSpan) + if not IsLeapYear(newYear): + newDay = 28 # migrate to 28 + else: + # if times == 1, no extra datetime need to be added + newYear += 0 if times == 1 else (times * yearSpan) + + val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) + return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP def LoopHandle_Month(searchResult, starttime, times, tzoffset): - pass + isStrict = searchResult.group(1) == 'S' + loopType = searchResult.group(2) + monthSpan = int(searchResult.group(3)) + + # we should get original data in each method + times -= 1 + clientDate = datetime.datetime.fromtimestamp(starttime, UTCTimezone(tzoffset)) + newYear = clientYear = clientDate.year + newMonth = clientMonth = clientDate.month + newDay = clientDay = clientDate.day + # data struct + # dayStatistics = + # (dayForwards, dayBackwards, weeksForward, dayOfWeekForward, weeksBackwards, dayOfWeekBackward) + dayStatistics = GetDayInMonth(clientYear, clientMonth, clientDay) + + if isStrict: + if loopType == 'A': + while times > 0: + newMonth += 1 + if newMonth > 12 + newMonth = 1 + newYear += 1 + maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) + if dayStatistics[0] <= maxDays: + times -= 1 + elif loopType == 'B': + while times > 0: + newMonth += 1 + if newMonth > 12 + newMonth = 1 + newYear += 1 + maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) + if dayStatistics[1] <= maxDays: + times -= 1 + elif loopType == 'C': + while times > 0: + newMonth += 1 + if newMonth > 12 + newMonth = 1 + newYear += 1 + monthStatistics = GetMonthWeekStatistics(newYear, newMonth) + if dayStatistics[2] <= monthStatistics[dayStatistics[3]]: + times -= 1 + elif loopType == 'D': + while times > 0: + newMonth += 1 + if newMonth > 12 + newMonth = 1 + newYear += 1 + monthStatistics = GetMonthWeekStatistics(newYear, newMonth) + if dayStatistics[4] <= monthStatistics[dayStatistics[5]]: + times -= 1 + else: + newMonth += times * monthSpan + newYear += int(newMonth - 1 / 12) + newMonth = (newMonth % 12) + 1 + newDay = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) + + val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) + return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP def LoopHandle_Week(searchResult, starttime, times, tzoffset): weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1))) @@ -65,15 +156,17 @@ def LoopHandle_Week(searchResult, starttime, times, tzoffset): remainEvent -= 1 nowDayOfWeek += 1 + val -= 1 return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP def LoopHandle_Day(searchResult, starttime, times, tzoffset): val = starttime + DAY1_SPAN * times * int(searchResult.group(1)) + val -= 1 return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP precompiledLoopRules = ( - (re.compile(r'^Y([RF]{1})([1-9]\d*)$'), LoopHandle_Year), - (re.compile(r'^M([RF]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month), + (re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year), + (re.compile(r'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month), (re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week), (re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day) ) @@ -84,6 +177,92 @@ precompiledLoopStopRules = { 'times': re.compile(r'^T([1-9]\d*)$') } +def LeapYearCountEx(endYear, includeThis = False, baseYear = 1, includeBase = True): + if not includeThis: + endYear -= 1 + if includeBase: + baseYear -= 1 + + endly = int(endYear / 4) + endly -= int(endYear / 100) + endly += int(endYear / 400) + + basely = int(baseYear / 4) + basely -= int(baseYear / 100) + basely += int(baseYear / 400) + + return (endly - basely) + +def LeapYearCount(year): + return LeapYearCountEx(year, False, 1, True) + +def IsLeapYear(year): + isLeap = False + if year % 4 == 0: + isLeap = True + if year % 100 == 0: + isLeap = False + if year % 400 == 0: + isLeap = True + return isLeap + +def DaysCount(year, month, day): + ly = LeapYearCountEx(year, False, 1, True) + days = 365 * (year - 1) + days += ly + + for index in range(1, month, 1): + days += MonthDayCount[index - 1] + + if (month > 2) and IsLeapYear(year): + days += 1 + + days += day - 1 + return days + +def DayOfWeek(year, month, day): + # as we know, 1/1/1900 is Monday. + # via this method, we can got 1/1/1 is Monday + # compute day span + days=DaysCount(year, month, day) + + # return day of week (from 0 - 6, corresponding with python) + return days % 7 + +def GetDayInMonth(year, month, day): + days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) + firstDayOfWeek = DayOfWeek(year, month, 1) + lastDayOfWeek = (firstDayOfWeek + days - 1) % 7 + dayOfWeek = (firstDayOfWeek + day - 1) % 7 + + dayForwards = day + dayBackwards = days - day + 1 + + weeksForward = (dayForwards - 1) / 7 + weeksBackwards = (dayBackwards - 1) / 7 + + dayOfWeekForward = (firstDayOfWeek + ((dayForwards - 1) % 7)) % 7 + # 7 don't change week + # # just keep this is the positive number and prevent pretential minus number calc problem + dayOfWeekBackward = (7 + lastDayOfWeek - ((dayBackwards - 1) % 7)) % 7 + + return (dayForwards, dayBackwards, weeksForward, dayOfWeekForward, weeksBackwards, dayOfWeekBackward) + +def GetMonthWeekStatistics(year, month): + days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) + firstDayOfWeek = DayOfWeek(year, month, 1) + lastDayOfWeek = (firstDayOfWeek + days - 1) % 7 + + result = [4, 4, 4, 4, 4, 4, 4] + remain = (days - 1) % 7 + week = firstDayOfWeek + while remain > 0: + result[week % 7] += 1 + week += 1 + remain -= 1 + + return tuple(result) + class UTCTimezone(datetime.tzinfo): def __init__(self, offset = 0): self._offset = offset diff --git a/src/utils.py b/src/utils.py index 9d9cee7..e0e502a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -42,4 +42,10 @@ def GetTokenExpireOn(): def Str2Bool(strl): return strl.lower() == 'true' + +def GCD(a, b): + return math.gcd(a, b) + +def LCM(a, b): + return a * b / GCD(a, b) \ No newline at end of file