From 63e113de4afe54c47789d4ce20b26a1665880c2a Mon Sep 17 00:00:00 2001 From: historia Date: Fri, 9 Aug 2024 04:30:05 -0400 Subject: Feature: Parsing todos, attachments, and deadlines --- src/soon.nim | 84 ++----------------- src/soon/agenda.nim | 72 ++++++++++++++++ src/soon/conditionchecker.nim | 186 ++++++++++++++++++++++++++++++++++++++++++ src/soon/conditions.nim | 186 ------------------------------------------ src/soon/todos.nim | 49 +++++++++++ 5 files changed, 312 insertions(+), 265 deletions(-) create mode 100644 src/soon/agenda.nim create mode 100644 src/soon/conditionchecker.nim delete mode 100644 src/soon/conditions.nim create mode 100644 src/soon/todos.nim (limited to 'src') diff --git a/src/soon.nim b/src/soon.nim index d154846..cede38c 100644 --- a/src/soon.nim +++ b/src/soon.nim @@ -1,13 +1,7 @@ -import std/[times, os, strutils, strbasics, sequtils, strscans, sets, strtabs, tables], parseopt -import soon/[conditions, config, typechecker] +import std/[os, strutils, strtabs, sequtils, parseopt] +import soon/[config, agenda, todos] const VERSION = "0.1" -const configDir = expandTilde("~/.config/soon/") -const defaultConfigFile = configDir & "soon.toml" -const defaultCalendarFile = configDir & "calendar.soon" - -const MONTHS = DefaultLocale.MMM.toHashSet + DefaultLocale.MMMM.toHashSet -const WEEKDAYS = DefaultLocale.ddd.toHashSet + DefaultLocale.dddd.toHashSet proc printHelp(badCmd: string) = if badCmd != "": @@ -41,73 +35,6 @@ proc createDefaultCalendarFile(file: string) = if not fileExists(file): writeFile(file, "# This is your first calendar file. Put events and todos in here.") -# Splits a string on whitespace, but maintains whitespace in date groups -proc splitWhitespaceExceptParens(str:string): seq[string] = - var output: seq[string] = newSeq[string]() - var inParens = false - for token in str.splitWhitespace: - if inParens: - output.add(output.pop & ' ' & token) - else: - output.add(token) - for c in token: - if c == '(': inParens = true - if c == ')': inParens = false - return output - -# Returns Table[conditionType, seq of conditions] -proc parseDateToTable(date:string): Table[string, seq[string]] = - var table = initTable[string,seq[string]]() - let conditions = splitWhitespaceExceptParens(date) - for c in conditions: - let condType = checkType(c) - if not table.hasKey(condType): - table[condType] = @[] - table[condType].add(c) - return table - -proc checkAllConditions(dateConditions: string, date: DateTime): bool = - let conditions = parseDateToTable(dateConditions.strip) - for k,cs in conditions: - var keyMatched = false - for c in cs: - if checkCond(c, date) == true: - keyMatched = true - break - if not keyMatched: - return false - return true - -# Returns Table[eventName, eventAttachedLines] -proc getEventsForDate(cal: seq[string], date: DateTime): Table[string, seq[string]] = - var lastInsertion = "" - for line in cal: - var (success, dateConditions, event) = scanTuple(line, "$*,$*") - if success: - if checkAllConditions(dateConditions, date) == true: - event.strip - result[event] = @[] - lastInsertion = event - elif line.strip[0] == '+': - if result.hasKey(lastInsertion): - result[lastInsertion].add(line) - else: - echo "WARNING: Cannot parse line: ", line - -proc printEvents(cal: seq[string], date: DateTime, format: string) = - let events = getEventsForDate(cal, date) - if events.len > 0: - for e in events.keys: - echo date.format(format), " ", e - for attachment in events[e]: - echo attachment.indent(date.format(format).len + 2) - -proc printAgenda(calendar: seq[string], past: int, future: int) = - let now = now() - for i in past..future: - let curDay = now + i.days - printEvents(calendar, curDay, "ddd yyyy MMM dd") - proc getCalendarFileLines(file: string): seq[string] = let f = open(file) for line in f.lines: @@ -121,7 +48,8 @@ proc processCalendars(calendarPath: string, past: int, future: int) = for file in os.walkDirRec(calendarPath): if file.toLower().endsWith(".soon"): calendar = calendar.concat(getCalendarFileLines(file)) - printAgenda(calendar, past, future) + agenda.printAgenda(calendar, past, future) + todos.printTodos(calendar) proc parseArgs(config: StringTableRef): seq[string] = for kind, key, val in getopt(): @@ -145,9 +73,7 @@ proc main() = var conf = config.loadConfig() let commands = parseArgs(conf) validateConfig(conf) - let past = 0 - parseInt(conf["past"]) - let future = parseInt(conf["future"]) - processCalendars(conf["calendarPath"], past, future) + processCalendars(conf["calendarPath"], 0 - parseInt(conf["past"]), parseInt(conf["future"])) when isMainModule: main() diff --git a/src/soon/agenda.nim b/src/soon/agenda.nim new file mode 100644 index 0000000..014be1e --- /dev/null +++ b/src/soon/agenda.nim @@ -0,0 +1,72 @@ +import std/[times, strutils, strbasics, strscans, tables] +import conditionchecker, typechecker + +# Splits a string on whitespace, but maintains whitespace in date groups +proc splitWhitespaceExceptParens(str:string): seq[string] = + var output: seq[string] = newSeq[string]() + var inParens = false + for token in str.splitWhitespace: + if inParens: + output.add(output.pop & ' ' & token) + else: + output.add(token) + for c in token: + if c == '(': inParens = true + if c == ')': inParens = false + return output + +# Returns Table[conditionType, seq of conditions] +proc parseDateToTable(date:string): Table[string, seq[string]] = + var table = initTable[string,seq[string]]() + let conditions = splitWhitespaceExceptParens(date) + for c in conditions: + let condType = typechecker.checkType(c) + if not table.hasKey(condType): + table[condType] = @[] + table[condType].add(c) + return table + +proc allConditionsMet(dateConditions: string, date: DateTime): bool = + let conditions = parseDateToTable(dateConditions.strip) + for k,cs in conditions: + var keyMatched = false + for c in cs: + if conditionchecker.checkCond(c, date) == true: + keyMatched = true + break + if not keyMatched: + return false + return true + +# Returns Table[event, attachments] +proc getEventsForDate(cal: seq[string], date: DateTime): Table[string, seq[string]] = + var lastInsertion = "" + for line in cal: + var (success, dateConditions, event) = scanTuple(line, "$*,$*") + if success: + if allConditionsMet(dateConditions, date) == true: + event.strip + result[event] = @[] + lastInsertion = event + elif line.strip[0] == '+': + if result.hasKey(lastInsertion): + result[lastInsertion].add(line) + elif line.strip[0] == '-': + lastInsertion = "" + else: + echo "WARNING: Cannot parse line: ", line + +proc printEvents(cal: seq[string], date: DateTime, format: string) = + let events = getEventsForDate(cal, date) + if events.len > 0: + for e in events.keys: + echo date.format(format), " ", e + for attachment in events[e]: + let space = date.format(format).len + 2 + echo attachment.indent(space) + +proc printAgenda*(calendar: seq[string], past: int, future: int) = + let now = now() + for i in past..future: + let curDay = now + i.days + printEvents(calendar, curDay, "ddd yyyy MMM dd") diff --git a/src/soon/conditionchecker.nim b/src/soon/conditionchecker.nim new file mode 100644 index 0000000..1880d42 --- /dev/null +++ b/src/soon/conditionchecker.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/conditions.nim b/src/soon/conditions.nim deleted file mode 100644 index 1880d42..0000000 --- a/src/soon/conditions.nim +++ /dev/null @@ -1,186 +0,0 @@ -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/todos.nim b/src/soon/todos.nim new file mode 100644 index 0000000..2129ec1 --- /dev/null +++ b/src/soon/todos.nim @@ -0,0 +1,49 @@ +import std/[times, strutils, tables, re] + +# Returns Table[todo, attachments] +proc getTodosFromCalendar(calendar: seq[string]): Table[string, seq[string]] = + var lastInsertion = "" + for l in calendar: + let line: string = l.strip + if line[0] == '-': + result[line] = @[] + lastInsertion = line + elif line[0] == '+': + if result.hasKey(lastInsertion): + result[lastInsertion].add(l) + else: + lastInsertion = "" + +proc splitRegularAndDeadlineTodos(todos: Table[string, seq[string]]): (Table[string, seq[string]], Table[string, seq[string]]) = + for todo in todos.keys: + var attachments = newSeq[string]() + var hasDeadline = false + for attach in todos[todo]: + if match(attach, re("^\\s*\\+\\s*Due:", flags = {reIgnoreCase})): + hasDeadline = true + attachments.add(attach) + if hasDeadline: + result[1][todo] = attachments + else: + result[0][todo] = attachments + +proc printRegularTodos(todos: Table[string, seq[string]]) = + if todos.len > 0: + echo "\nTodo List:" + for todo in todos.keys: + echo todo + for attachment in todos[todo]: + echo attachment + +proc printDeadlineTodos(todos: Table[string, seq[string]]) = + if todos.len > 0: + echo "\nUpcoming Deadlines:" + for todo in todos.keys: + echo todo + echo "Deadline logic not yet implemented" + +proc printTodos*(calendar: seq[string]) = + let todos = getTodosFromCalendar(calendar) + var (regular, deadline) = splitRegularAndDeadlineTodos(todos) + printRegularTodos(regular) + printDeadlineTodos(deadline) -- cgit v1.2.3