aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--src/soon/conditions.nim186
-rw-r--r--src/soon/config.nim53
-rw-r--r--src/soon/typechecker.nim36
4 files changed, 276 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 792058f..7de46c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)