diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | src/soon/conditions.nim | 186 | ||||
| -rw-r--r-- | src/soon/config.nim | 53 | ||||
| -rw-r--r-- | src/soon/typechecker.nim | 36 |
4 files changed, 276 insertions, 0 deletions
@@ -1,4 +1,5 @@ soon /tests/* +!/src/soon/ !/tests/*.nim !/tests/*.nims diff --git a/src/soon/conditions.nim b/src/soon/conditions.nim new file mode 100644 index 0000000..1880d42 --- /dev/null +++ b/src/soon/conditions.nim @@ -0,0 +1,186 @@ +import std/[times, strutils, tables, re, strtabs] +import typechecker + +# Forward declarations +proc checkCond*(cond: string, date: DateTime): bool + +proc fixCase(s: string): string = + return s.toLower.capitalizeAscii + +proc expandWeekday(w: string): string = + let weekday = w.fixCase + if weekday.len > 3: return weekday + const weekdays = {"Mon": "Monday", "Tue": "Tuesday", "Wed": "Wednesday", "Thu": "Thursday", "Fri": "Friday", "Sat": "Saturday", "Sun": "Sunday"}.toTable + return weekdays[weekday] + +proc expandMonth(m: string): string = + let month = m.fixcase + if month.len > 3: return month + return parse(month, "MMM").format("MMMM") + +proc monthToInt(m: string): int = + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + let month = m.expandMonth + for i in 0..11: + if month == months[i]: + return i+1 + return -1 + +proc weekdayToInt(w: string): int = + const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + let weekday = w.expandWeekday + for i in 0..6: + if weekday == weekdays[i]: + return i+1 + return -1 + +# Returns MJD as if date was UTC +proc modifiedJulianDay*(date: DateTime): int = + let dayZero = dateTime(1858, mNov, 17, 0, 0, 0, 0, utc()) + let dateUTC = dateTime(date.year, date.month, date.monthday, 0, 0, 0, 0, utc()) + return (dateUTC - dayZero).inDays + +proc getWeekFromEnd(date: DateTime): int = + let daysInMonth = getDaysInMonth(date.month, date.year) + return int((daysInMonth - date.monthday) / 7) + 1 + +proc getWeek(date: DateTime): int = + return int((date.monthDay - 1) / 7) + 1 + +# Generically checks forwards (Mon-Fri) and backwards (Dec-Feb) ranges +proc isInLoopingRange(left: int, right: int, date: int): bool = + if left <= right: + return left <= date and right >= date + else: + return left <= date or right >= date + +proc checkTime(condition: string, date: DateTime): bool = + return true + +proc checkYear(condition: string, date: DateTime): bool = + return parseInt(condition) == date.year + +proc checkMonth(condition: string, date: DateTime): bool = + return condition == date.format("MMM") or condition == date.format("MMMM") + +proc checkDay(condition: string, date: DateTime): bool = + return condition == date.format("d") or condition == date.format("dd") + +proc checkWeekday(condition: string, date: DateTime): bool = + return condition == date.format("ddd") or condition == date.format("dddd") + +proc checkFirstWeek(condition: string, date: DateTime): bool = + return parseInt(condition[0..1]) == getWeek(date) + +proc checkLastWeek(condition: string, date: DateTime): bool = + return parseInt(condition[0..1]) == getWeekFromEnd(date) + +proc checkJulian(condition: string, date: DateTime): bool= + var jDate: int = date.modifiedJulianDay + var remainder: int + var matches: array[2, string] + discard find(condition, re"[Jj]\%?(\d+)?([\-+]\d+)?", matches) + if matches[0] != "": + remainder = jDate mod parseInt(matches[0]) + if matches[1] != "": + remainder = remainder + parseInt(matches[1]) + return remainder == 0 + +proc checkRangeYear(s: string, date: DateTime): bool = + let years = s.split('-') + return date.year >= parseInt(years[0]) and date.year <= parseInt(years[1]) + +proc checkRangeMonth(s: string, date: DateTime): bool = + let months = s.split('-') + let firstMonth = months[0].monthToInt + let lastMonth = months[1].monthToInt + let dateMonth = ord(date.month) + return isInLoopingRange(firstMonth, lastMonth, dateMonth) + +proc checkRangeDay(s: string, date: DateTime): bool = + let days = s.split('-') + return date.monthday >= parseInt(days[0]) and date.monthday <= parseInt(days[1]) + +proc checkRangeWeekday(s: string, date: DateTime): bool = + let wdays = s.split('-') + let firstWeekday = wdays[0].weekdayToInt + let lastWeekday = wdays[1].weekdayToInt + let dateWeekday = ord(date.weekday) + return isInLoopingRange(firstWeekday, lastWeekday, dateWeekday) + +proc conditionToInt(c: string): int = + case checkType(c): + of "year": + return parseInt(c) + of "month": + return monthToInt(c) + of "day": + return parseInt(c) + +proc dateGroupStringToTable(s: string): Table[string, int] = + let conditions = s[1..s.len-2].splitWhitespace + var t = initTable[string, int]() + for c in conditions: + t[checkType(c)] = conditionToInt(c) + return t + +proc isValidDateGroup(left: Table, right: Table): bool = + for c in ["year", "month", "day"]: + if left.hasKey(c) != right.hasKey(c): + echo "Error: Left group conditions don't match right group: ", string + return false + if c != "year" and not left.hasKey(c) or not right.hasKey(c): + echo "Error: Missing ", c, " in groups: ", string + return false + return true + +# Check that date falls in a range like (Jan 30 2025)-(Feb 2 2025) +proc checkRangeDateGroup(s: string, date: DateTime): bool = + let dates = s.split('-') + var left = dateGroupStringToTable(dates[0]) + var right = dateGroupStringToTable(dates[1]) + if not isValidDateGroup(left, right): + return false + if left["year"] > date.year or right["year"] < date.year: + return false + if left["day"] > date.monthday or right["day"] < date.monthday: + return false + return isInLoopingRange(left["month"], right["month"], ord(date.month)) + +# Check each condition in a date group like (Jan 1 2025) +proc checkDateGroup(s: string, date: DateTime): bool = + var conditions = s + let cs = conditions[1..conditions.len-2].splitWhitespace + for c in cs: + if checkCond(c, date) == false: + return false + return true + +proc checkCond*(cond: string, date: DateTime): bool = + var condition = cond + var invert = false + var found = false + if condition[0] == '!': + condition.delete(0..0) + invert = true + case checkType(condition): + of "time": found = checkTime(condition, date) + of "year": found = checkYear(condition, date) + of "month": found = checkMonth(condition, date) + of "day": found = checkDay(condition, date) + of "weekday": found = checkWeekday(condition, date) + of "firstweek": found = checkFirstWeek(condition, date) + of "lastweek": found = checkLastWeek(condition, date) + of "julian": found = checkJulian(condition, date) + of "dategroup": found = checkDateGroup(condition, date) + of "rangeyear": found = checkRangeYear(condition, date) + of "rangemonth": found = checkRangeMonth(condition, date) + of "rangeday": found = checkRangeDay(condition, date) + of "rangeweekday": found = checkRangeWeekday(condition, date) + of "rangedategroup": found = checkRangeDateGroup(condition, date) + else: + echo "ERROR: Unknown condition: ", condition + if invert == true: + return not found + return found + diff --git a/src/soon/config.nim b/src/soon/config.nim new file mode 100644 index 0000000..6421baa --- /dev/null +++ b/src/soon/config.nim @@ -0,0 +1,53 @@ +import std/[parsecfg, os, times, strtabs] + + #Creates a default configuration file if it doesn't exist +proc createDefaultConfigFile(path: string) = + createDir(path) + var config = newConfig() + config.setSectionKey("Calendar", "path", path) + config.setSectionKey("Calendar", "defaultFile", "calendar.soon") + config.setSectionKey("Calendar", "past", "0") + config.setSectionKey("Calendar", "future", "14") + config.setSectionKey("Calendar", "editor", getEnv("EDITOR")) + config.setSectionKey("Calendar", "", "") + config.setSectionKey("Todo List", "alwaysPrint", "true") + config.setSectionKey("Todo List", "saveCompletedTodos", "false") + config.setSectionKey("Todo List", "completedTodoFile", path / "done") + config.setSectionKey("Todo List", "", "") + config.setSectionKey("Archive", "path", path / "archive") + config.setSectionKey("Archive", "showFutureArchivedEvents", "true") + config.setSectionKey("Archive", "showPastArchivedEvents", "false") + config.setSectionKey("Archive", "alwaysShowUnarchivedEvents", "false") + config.setSectionKey("Archive", "startDate", now().format("yyyy-MM-dd")) + config.setSectionKey("Archive", "deleteFromCalendarFile", "true") + config.setSectionKey("Archive", "autoPurge", "true") + config.writeConfig(path / "soon.conf") + +proc loadConfigFromFile(file: string): StringTableRef = + var config = newStringTable() + let c = loadConfig(file) + config["calendarPath"] = c.getSectionValue("Calendar", "path") + config["defaultFile"] = c.getSectionValue("Calendar", "defaultFile") + config["past"] = c.getSectionValue("Calendar", "past") + config["future"] = c.getSectionValue("Calendar", "future") + config["editor"] = c.getSectionValue("Calendar", "editor") + config["printtodos"] = c.getSectionValue("Todo List", "alwaysPrint") + config["saveCompletedTodos"] = c.getSectionValue("Todo List", "saveCompletedTodos") + config["completedFile"] = c.getSectionValue("Todo List", "completedFile") + config["archiveFile"] = c.getSectionValue("Archive", "file") + config["showFutureArchived"] = c.getSectionValue("Archive", "showFutureArchived") + config["showPastArchived"] = c.getSectionValue("Archive", "showPastArchived") + config["alwaysShowUnarchived"] = c.getSectionValue("Archive", "alwaysShowUnarchived") + config["startDate"] = c.getSectionValue("Archive", "startDate") + config["deleteArchivedEvents"] = c.getSectionValue("Archive", "deleteArchivedEvents") + config["autoPurge"] = c.getSectionValue("Archive", "autoPurge") + return config + +proc loadConfig*(): StringTableRef = + let path = expandTilde(getEnv("XDG_CONFIG_HOME", "~/.config")) / "soon" + let file = path / "soon.conf" + if not fileExists(file): + createDefaultConfigFile(path) + result = loadConfigFromFile(file) + result["path"] = path + diff --git a/src/soon/typechecker.nim b/src/soon/typechecker.nim new file mode 100644 index 0000000..a408d10 --- /dev/null +++ b/src/soon/typechecker.nim @@ -0,0 +1,36 @@ +import std/[times, re, strutils, sets] +const MONTHS = DefaultLocale.MMM.toHashSet + DefaultLocale.MMMM.toHashSet +const WEEKDAYS = DefaultLocale.ddd.toHashSet + DefaultLocale.dddd.toHashSet + +proc checkForRanges(c: string): string = + let cs = c.split("-", maxsplit = 1) + if cs.len != 2: + return "unknown" + let startRange = cs[0].toLower.capitalizeAscii + let endRange = cs[1].toLower.capitalizeAscii + if (startRange in MONTHS) and (endRange in MONTHS): + return "rangemonth" + if (startRange in WEEKDAYS) and (endRange in WEEKDAYS): + return "rangeweekday" + if find(startRange, re"^\([A-Za-z0-9 ]+\)$") > -1 and find(endRange, re"^\([A-Za-z0-9 ]+\)$") > -1: + return "rangedategroup" + return "unknown" + +# Check an individual condition (Jan) and return the condition type (month) +proc checkType*(c: string): string = + var cond = c + if find(cond, re"^\d{4}$") > -1: return "year" + if find(cond, re"^\d{1,2}$") > -1: return "day" + if find(cond, re"^\d[Ww]$") > -1: return "firstweek" + if find(cond, re"^\d[Ll]$") > -1: return "lastweek" + if find(cond, re"^\([A-Za-z0-9 ]+\)$") > -1: return "dategroup" + if find(cond, re"^\d{4}-\d{4}$") > -1: return "rangeyear" + if find(cond, re"^\d{1,2}-\d{1,2}$") > -1: return "rangeday" + if find(cond, re"^\d{1,2}:\d{2}(-\d{1,2}:\d{2})?$") > -1: return "time" + if find(cond, re"^\d{1,2}(:\d{2})?[AaPp]\.?[Mm](-\d{1,2}(:\d{2})?[AaPp]\.?[Mm])?") > -1: return "time" + if find(cond, re"^\(?\s*[Jj]\s*%\s*\d+\s*([-+]\s*\d+)?\s*\)?$") > -1: return "julian" + cond = cond.toLower.capitalizeAscii + if cond in MONTHS: return "month" + if cond in WEEKDAYS: return "weekday" + + return checkForRanges(cond) |
