import algorithm, json, sequtils, strutils, times, timeutils, uuids import ./util type Mark* = tuple[id: UUID, time: DateTime, summary: string, notes: string, tags: seq[string]] ## Representation of a single mark on the timeline. Timeline* = tuple[name: string, marks: seq[Mark]] ## Representation of a timeline: a name and sequence of Marks. OffsetFrom = enum Year, Month, Day, None const STOP_MSG* = "STOP" let NO_MARK*: Mark = ( id: parseUUID("00000000-0000-0000-0000-000000000000"), time: fromUnix(0).local, summary: "", notes: "", tags: @[]) const ISO_TIME_FORMAT* = "yyyy-MM-dd'T'HH:mm:ss" ## The canonical time format used by PTK. const TIME_FORMATS* = @[ (fmtStr: "yyyy-MM-dd'T'HH:mm:sszzz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd HH:mm:sszzz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd'T'HH:mm:sszz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd HH:mm:sszz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd'T'HH:mm:ssz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd HH:mm:ssz", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd'T'HH:mm:ss", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd HH:mm:ss", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd'T'HH:mm", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd HH:mm", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM-dd", offsetFrom: OffsetFrom.None), (fmtStr: "yyyy-MM", offsetFrom: OffsetFrom.None), (fmtStr: "MM-dd'T'HH:mm:ss", offsetFrom: OffsetFrom.Year), (fmtStr: "MM-dd HH:mm:ss", offsetFrom: OffsetFrom.Year), (fmtStr: "MM-dd'T'HH:mm", offsetFrom: OffsetFrom.Year), (fmtStr: "MM-dd HH:mm", offsetFrom: OffsetFrom.Year), (fmtStr: "HH:mm:ss", offsetFrom: OffsetFrom.Day), (fmtStr: "H:mm:ss", offsetFrom: OffsetFrom.Day), (fmtStr: "H:mm", offsetFrom: OffsetFrom.Day), (fmtStr: "HH:mm", offsetFrom: OffsetFrom.Day) ] ## Other time formats that PTK will accept as input. proc getOrFail*(n: JsonNode, key: string, objName: string = ""): JsonNode = ## convenience method to get a key from a JObject or raise an exception if not n.hasKey(key): raiseEx objName & " missing key '" & key & "'" return n[key] proc getIfExists*(n: JsonNode, key: string): JsonNode = ## convenience method to get a key from a JObject or return null result = if n.hasKey(key): n[key] else: newJNull() proc parseTime*(timeStr: string): DateTime = ## Helper to parse time strings trying multiple known formats. let now = now() for fmt in TIME_FORMATS: try: let parsed = parse(timeStr, fmt.fmtStr) case fmt.offsetFrom: of OffsetFrom.None: return parsed of OffsetFrom.Year: return dateTime(now.year, parsed.month, parsed.monthday, parsed.hour, parsed.minute, parsed.second, parsed.nanosecond, now.timezone) of OffsetFrom.Month: return initDateTime(parsed.monthday, now.month, now.year, parsed.hour, parsed.minute, parsed.second, parsed.nanosecond, now.timezone) of OffsetFrom.Day: return initDateTime(now.monthday, now.month, now.year, parsed.hour, parsed.minute, parsed.second, parsed.nanosecond, now.timezone) except: discard nil raise newException(Exception, "unable to interpret as a date: " & timeStr) proc parseMark*(json: JsonNode): Mark = return ( id: parseUUID(json["id"].getStr()), time: parse(json["time"].getStr(), ISO_TIME_FORMAT), summary: json["summary"].getStr(), notes: json["notes"].getStr(), tags: json["tags"].getElems(@[]).map(proc (t: JsonNode): string = t.getStr())) template `%`*(mark: Mark): JsonNode = %* { "id": $(mark.id), "time": mark.time.format(ISO_TIME_FORMAT), "summary": mark.summary, "notes": mark.notes, "tags": mark.tags } template `%`*(timeline: Timeline): JsonNode = %* { "name": timeline.name, "marks": timeline.marks } proc loadTimeline*(filename: string): Timeline = ## Load a timeline from a file. Expects a path to a file (can be relative or ## absolute) and returns a Timeline. The marks in the timeline are guaranteed ## to be ordered by time. var timelineJson: JsonNode try: timelineJson = parseFile(filename) except: raise newException(ValueError, "unable to parse the timeline file as JSON: " & filename) var timeline: Timeline = (name: timelineJson["name"].getStr(), marks: @[]) for markJson in timelineJson["marks"]: timeline.marks.add(parseMark(markJson)) timeline.marks = timeline.marks.sorted( proc(a, b: Mark): int = cmp(a.time, b.time)) return timeline proc saveTimeline*(timeline: Timeline, location: string): void = ## Write the timeline to disk at the file location given. var timelineFile: File try: timelineFile = open(location, fmWrite) timelineFile.writeLine(pretty(%timeline)) except: raise newException(IOError, "unable to save changes to " & location) finally: close(timelineFile) proc findById*(marks: seq[Mark], id: string): int = var idx = 0 for mark in marks: if startsWith($mark.id, id): return idx inc(idx) return -1 proc getLastIndex*(marks: seq[Mark]): int = ## Find and return the index of the last Mark that was not a STOP mark. ## Returns -1 if there is no such last mark. var idx = marks.len - 1 while idx >= 0 and marks[idx].summary == STOP_MSG: idx -= 1 if idx < 0: result = -1 else: result = idx