(prop :Array dailyEntries []) (prop :Array monthlyEntries []) (prop :Array intervalEntries []) (prop :Array bonusEntries []) (prop :Array todoEntries []) (prop :kiss.List rewardFiles []) (prop &mut :Int pomodoroPoints 0) (defNew [&prop :String textFile] (let [s (Stream.fromFile textFile) &mut lastHeader ""] // TODO could be whileLet (loop (case (s.takeLine) ((Some "DAILY") (set lastHeader "DAILY")) ((Some "MONTHLY") (set lastHeader "MONTHLY")) ((Some "INTERVAL") (set lastHeader "INTERVAL")) ((Some "BONUS") (set lastHeader "BONUS")) ((Some "TODO") (set lastHeader "TODO")) ((Some "FILES") (set lastHeader "FILES")) ((Some "POMODORO") (set lastHeader "POMODORO")) ((when (apply = (concat ["-"] (line.split ""))) (Some line)) (continue)) ((Some "") (continue)) // Types won't unify with the next case, so this is its own: ((when (= lastHeader "FILES") (Some line)) (rewardFiles.push (let [parts (line.split " ") skipped (case (parts.pop) ("true" true) ("false" false) (otherwise (throw "bad bool"))) piecesPerPoint (Std.parseInt (parts.pop)) puzzleHeight (Std.parseInt (parts.pop)) puzzleWidth (Std.parseInt (parts.pop)) startingPoints (Std.parseInt (parts.pop)) path (parts.join " ")] (objectWith path startingPoints puzzleWidth puzzleHeight piecesPerPoint skipped)))) ((when (= lastHeader "POMODORO") (Some line)) (set pomodoroPoints (Std.parseInt line))) ((Some line) (let [topPriority (line.startsWith "! ")] (when topPriority (set line (line.substr 2))) (.push (case lastHeader ("DAILY" dailyEntries) ("MONTHLY" monthlyEntries) ("INTERVAL" intervalEntries) ("BONUS" bonusEntries) ("TODO" todoEntries) (otherwise (throw "bad header"))) (new Entry (case lastHeader ("BONUS" Bonus) ("TODO" Todo) ("DAILY" (case (line.split ":") ([noColon] (Daily // all days of week (collect (range 1 8)) // never done before "")) ([::&mut preColon ...afterColon] (set line (afterColon.join ":")) (Daily // Days of week specified by abbreviation: (sort (filter [ // disambiguate Th from T and Su from S: (when (contains preColon "Th") {(set preColon (StringTools.replace preColon "Th" "")) 4}) (when (contains preColon "Su") {(set preColon (StringTools.replace preColon "Su" "")) 7}) (when (contains preColon "M") 1) (when (contains preColon "T") 2) (when (contains preColon "W") 3) (when (contains preColon "F") 5) (when (contains preColon "S") 6) ])) // Last date completed after that: (ifLet [[days date] (preColon.split " ")] date ""))) (otherwise (throw "bad line")))) ("MONTHLY" (case (line.split ": ") ([::&mut preColon ...afterColon] (set line (afterColon.join ": ")) (Monthly // Days of month can be positive (1-31) or negative (-1 to -31) (map (.split (first (preColon.split " ")) ",") Std.parseInt) // Last date completed after that: (ifLet [[::days ...date] (preColon.split " ")] (date.join " ") ""))) (otherwise (throw "bad line")))) ("INTERVAL" (case (line.split ": ") ([::&mut preColon ...afterColon] (set line (afterColon.join ": ")) (case (preColon.split " ") ([days] (Interval (Std.parseInt days) "")) ([::days ...lastDayDone] (Interval (Std.parseInt days) (lastDayDone.join " "))) (otherwise (throw "bad interval habit: $line")))) (otherwise (throw "bad interval habit: $line")))) (otherwise (throw "bad header: $lastHeader"))) (for l (line.split "/") (object label (StringTools.trim (withoutPointsStr l)) points (countPoints l))) topPriority)))) (otherwise (break)))))) (method :Array allEntries [] (cast (concat dailyEntries monthlyEntries intervalEntries bonusEntries todoEntries))) (method :Array allUndeletedEntries [] (filter (allEntries) isNotDeleted)) (method :Int totalPoints [] (+ pomodoroPoints (apply + (for l (flatten (for e (allEntries) e.labels)) l.points)))) (function stringify [:Entry e] (e.toString)) (function :String stringifyRewardFile [:RewardFile rewardFile] "${rewardFile.path} ${rewardFile.startingPoints} ${rewardFile.puzzleWidth} ${rewardFile.puzzleHeight} ${rewardFile.piecesPerPoint} ${rewardFile.skipped}") (method :Void save [] (localVar &mut content "DAILY\n-----\n") (+= content (.join (map dailyEntries stringify) "\n") "\n") (+= content "\nMONTHLY\n--------\n") (+= content (.join (map monthlyEntries stringify) "\n") "\n") (+= content "\nINTERVAL\n--------\n") (+= content (.join (map intervalEntries stringify) "\n") "\n") (+= content "\nBONUS\n-----\n") (+= content (.join (map bonusEntries stringify) "\n") "\n") (+= content "\nTODO\n----\n") (+= content (.join (map todoEntries stringify) "\n") "\n") (+= content "\nPOMODORO\n--------\n${pomodoroPoints}\n") (+= content "\nFILES\n-----\n") (+= content (.join (map rewardFiles stringifyRewardFile) "\n") "\n") (File.saveContent textFile content)) // With rotating entries, the active one is the first one with the lowest score: (function :EntryLabel activeLabel [:Entry e] (let [lowScore (apply min (for label e.labels label.points))] (doFor label e.labels (when (= lowScore label.points) (return label))) (throw "no active?!"))) (function todayString [] (let [d (Date.now)] "$(d.getDate)-$(+ 1 (d.getMonth))-$(d.getFullYear)")) (function isDeleted [:Entry e] (or (.startsWith .label (first e.labels) "~") (case e.type (Todo !(= 0 .points (activeLabel e))) (otherwise false)))) (function isNotDeleted [:Entry e] !(isDeleted e)) (function _isActive [:Entry e] (when (isDeleted e) (return false)) (case e.type ((Daily days lastDayDone) (let [&mut today (.getDay (Date.now))] (when (= today 0) (set today 7)) (and !(= lastDayDone (todayString)) (contains days today)))) ((Monthly days lastDayDone) // TODO logic (let [&mut nextDay (DateTime.fromDate (Date.now)) oneDayInterval (DateTimeInterval.create (DateTime.make 1970 1 1) (DateTime.make 1970 1 2)) dayToEndSearch (if lastDayDone (DateTime.fromString lastDayDone) (let [&mut now (DateTime.fromDate (Date.now))] (until (= 1 (now.getDay)) #{now -= oneDayInterval;}#) now))] (until (and (= (nextDay.getDay) (dayToEndSearch.getDay)) (= (nextDay.getMonth) (dayToEndSearch.getMonth)) (= (nextDay.getYear) (dayToEndSearch.getYear))) (let [daysInMonth (DateTime.daysInMonth (nextDay.getMonth) (nextDay.isLeapYear)) adjustedDays (for day days (% (+ daysInMonth day) daysInMonth))] (when (contains adjustedDays (nextDay.getDay)) (return true))) #{nextDay -= oneDayInterval;}#) (return false))) ((Interval days lastDayDone) (or !lastDayDone (<= days #{(DateTime.fromDate(Date.now()) - DateTime.fromString(lastDayDone)).getTotalDays();}#))) (Todo (= 0 .points (activeLabel e))) (otherwise true))) // Check if an entry is active PRE-top priority filtering (method :Array _activeDailyEntries [] (filter dailyEntries _isActive)) (method :Array _activeMonthlyEntries [] (filter monthlyEntries _isActive)) (method :Array _activeIntervalEntries [] (filter intervalEntries _isActive)) (method :Array _activeBonusEntries [] (filter bonusEntries _isActive)) (method :Array _activeTodoEntries [] (filter todoEntries _isActive)) (method :Array _allActiveEntries [] (cast (concat (_activeDailyEntries) (_activeMonthlyEntries) (_activeIntervalEntries) (_activeBonusEntries) (_activeTodoEntries)))) (prop &mut :Bool showLowerPriority false) (method :Bool topPriorityIsActive [] ?(unless showLowerPriority (apply or (for e (_allActiveEntries) e.topPriority)))) (defMacro topPriority [name] `(method :Array ,name [] (let [_active (,(ReaderExp.Symbol (+ "_" (symbolNameValue name))))] (if (topPriorityIsActive) (filter _active ->e e.topPriority) _active)))) (topPriority activeDailyEntries) (topPriority activeMonthlyEntries) (topPriority activeIntervalEntries) (topPriority activeBonusEntries) (topPriority activeTodoEntries) (method addEntry [:EntryType type :Array labels :Bool topPriority] (.push (case type (Todo todoEntries) (Bonus bonusEntries) ((Interval _ _) intervalEntries) ((Daily _ _) dailyEntries) ((Monthly _ _) monthlyEntries) (otherwise (throw ""))) (new Entry type (for label labels (objectWith [points 0] label)) topPriority)) (save)) (method deleteEntry [:Entry e] (set e.labels (for label e.labels (object points label.points label "~"))) (save)) (method toggleEntryPriority [:Entry e] (set e.topPriority !e.topPriority) (save)) (method addRewardFile [path startingPoints puzzleWidth puzzleHeight piecesPerPoint] (rewardFiles.push (objectWith [skipped false] path startingPoints puzzleWidth puzzleHeight piecesPerPoint)) (save)) (method skipRewardFile [] (set .skipped (last rewardFiles) true) (save)) (method addPoint [:Entry e] (let [label (activeLabel e)] (+= label.points 1) // For task-list types, set lastDayDone when the final label is done (when (apply = (for label e.labels label.points)) (whenLet [(Daily days lastDayDone) e.type] (set e.type (Daily days (HabitModel.todayString)))) (whenLet [(Monthly days lastDayDone) e.type] (set e.type (Monthly days (.toString (DateTime.now))))) (whenLet [(Interval days lastDayDone) e.type] (set e.type (Interval days (.toString (DateTime.now))))))) (save)) (method addPomPoint [] (+= pomodoroPoints 1) (save)) (var tallyUnit 5) (function pointsStr [points] (let [&mut str "" symbols ["+" "*" "\$"]] (doFor i (reverse (collect (range symbols.length))) (let [scaledTallyUnit (^ tallyUnit i) tallies (Math.floor (/ points scaledTallyUnit))] (+= str (* (nth symbols i) tallies)) (-= points (* tallies scaledTallyUnit)))) str)) (function withoutPointsStr [:String label] (doFor c (.split "|+*\$" "") (set label (label.replace c ""))) label) (function countPoints [:String pointStr] (apply + (for char (pointStr.split "") (case char ("|" 1) ("+" 1) ("*" tallyUnit) ("\$" (^ tallyUnit 2)) (otherwise 0)))))