├── .gitignore ├── .semaphore └── semaphore.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── docker-compose.yml ├── lockfile.jdn ├── project.janet ├── src ├── alas.janet ├── commands.janet ├── commands │ ├── backup.janet │ ├── insert_days.janet │ ├── insert_task.janet │ ├── list_contacts.janet │ ├── remove_empty_days.janet │ ├── report.janet │ ├── schedule_contacts.janet │ ├── schedule_tasks.janet │ └── stats.janet ├── contact.janet ├── contact │ ├── parser.janet │ └── repository.janet ├── date.janet ├── day.janet ├── errors.janet ├── event.janet ├── file_repository.janet ├── plan.janet ├── plan │ ├── parser.janet │ └── serializer.janet ├── schedule_parser.janet ├── task.janet └── utils.janet ├── test.sh └── test ├── alas_test.janet ├── commands ├── backup_test.janet ├── insert_days_test.janet ├── insert_task_test.janet ├── list_contacts_test.janet ├── remove_empty_days_test.janet ├── report_test.janet ├── schedule_contacts_test.janet ├── schedule_tasks_test.janet └── stats_test.janet ├── commands_test.janet ├── contact ├── parser_test.janet └── repository_test.janet ├── contact_test.janet ├── date_test.janet ├── day_test.janet ├── examples ├── contacts │ ├── jane-doe.md │ └── john-doe.md ├── empty-schedule.md ├── plan-2020-08-02.md ├── plan-2020-08-03-1.md ├── plan-2020-08-03.md ├── schedule-without-date.md ├── schedule.md ├── todo.md ├── unparsable-schedule.md └── unparsable-todo.md ├── file_repository_test.janet ├── plan ├── parser_test.janet └── serializer_test.janet ├── plan_test.janet ├── schedule_parser_test.janet ├── task_test.janet └── utils.janet /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /jpm_tree 3 | /tags* 4 | /test/examples/*.bkp 5 | *.tested 6 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: Alas Tests 3 | agent: 4 | machine: 5 | type: e2-standard-2 6 | os_image: ubuntu2204 7 | blocks: 8 | - name: Test 9 | task: 10 | jobs: 11 | - name: 'Tests' 12 | commands: 13 | - wget https://github.com/janet-lang/janet/archive/refs/tags/v1.32.1.tar.gz 14 | - tar xvf v1.32.1.tar.gz 15 | - cd janet-1.32.1 16 | - make 17 | - sudo make install 18 | - cd 19 | - git clone https://github.com/janet-lang/jpm.git 20 | - git config --global --add safe.directory /home/semaphore/jpm 21 | - cd jpm 22 | - sudo janet bootstrap.janet 23 | - cd 24 | - checkout 25 | - jpm load-lockfile --local 26 | - ./test.sh 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | ARG JANET_VERSION 4 | ARG JPM_VERSION 5 | 6 | RUN apt update && apt install -y git make build-essential libssl-dev wget 7 | 8 | RUN mkdir -p /home/janet 9 | 10 | RUN cd /home/janet && wget https://github.com/janet-lang/janet/archive/refs/tags/v${JANET_VERSION}.tar.gz 11 | 12 | RUN cd /home/janet && tar xvf v${JANET_VERSION}.tar.gz 13 | 14 | RUN cd /home/janet/janet-$JANET_VERSION && make && make test && make install 15 | 16 | RUN cd /home && git clone --depth 1 --branch $JPM_VERSION https://github.com/janet-lang/jpm.git 17 | 18 | RUN cd /home/jpm && janet bootstrap.janet 19 | 20 | WORKDIR /home/alas 21 | 22 | CMD ["/usr/bin/bash"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hackberrydev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alas 2 | 3 | Alas is a command line utility for managing a plan in a single Markdown file. 4 | 5 | An example plan: 6 | 7 | ```markdown 8 | # My Plan 9 | 10 | ## Inbox 11 | 12 | - [ ] #home - Fix the lamp 13 | - [ ] Update Rust 14 | 15 | ## 2020-08-01, Saturday 16 | 17 | - [ ] Develop photos 18 | - [X] Pay bills 19 | 20 | ## 2020-07-31, Friday 21 | 22 | - Met with Mike and Molly 23 | - [X] #work - Review open pull requests 24 | - [X] #work - Fix the flaky test 25 | ``` 26 | 27 | The plan file has days in present and future that serve as your plan, but also 28 | past days that serve as a log. 29 | 30 | Alas can insert new empty days into your plan, remove empty days from past, 31 | schedule tasks and help you stay in touch with your contacts. 32 | 33 | For more information, visit the the main [Alas 34 | website](https://www.hackberry.dev/alas/). 35 | 36 | ## Development 37 | 38 | Install dependencies with: 39 | 40 | ```sh 41 | jpm load-lockfile --local 42 | ``` 43 | 44 | Run tests with: 45 | 46 | ```sh 47 | ./test.sh 48 | ``` 49 | 50 | ### Development With Docker 51 | 52 | Run the following command to start the Docker container: 53 | 54 | ```sh 55 | docker compose up --build -d 56 | ``` 57 | 58 | Run the following command to run Bash inside the container: 59 | 60 | ```sh 61 | docker compose exec alas bash 62 | ``` 63 | 64 | Then run tests with: 65 | 66 | ```sh 67 | ./test.sh 68 | ``` 69 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | jpm clean && jpm build --local && strip build/alas 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | alas: 3 | build: 4 | context: . 5 | args: 6 | JANET_VERSION: 1.38.0 7 | JPM_VERSION: v1.1.0 8 | container_name: alas 9 | image: alas:1.0 10 | tty: true 11 | volumes: 12 | - .:/home/alas 13 | -------------------------------------------------------------------------------- /lockfile.jdn: -------------------------------------------------------------------------------- 1 | @[{:url "https://github.com/ianthehenry/cmd" :tag "7551225818a8a5f6665a918f1862db0c7704a1f4" :type :git} 2 | {:url "https://github.com/ianthehenry/judge.git" :tag "a380c85753cb720adfd4fdac6a1393cac955359b" :type :git} 3 | {:url "https://github.com/janet-lang/argparse" :tag "a18ae94a3c2cfcf2071b1130fd533d0b6b1ee00d" :type :git}] 4 | -------------------------------------------------------------------------------- /project.janet: -------------------------------------------------------------------------------- 1 | (declare-project 2 | :name "janet" 3 | :description "A command line utility for planing your days" 4 | :dependencies [{:url "https://github.com/ianthehenry/judge.git" :tag "v2.6.1"} 5 | {:repo "https://github.com/janet-lang/argparse"}]) 6 | 7 | (declare-executable 8 | :name "alas" 9 | :entry "src/alas.janet") 10 | -------------------------------------------------------------------------------- /src/alas.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### The main file. 3 | 4 | (import argparse :prefix "") 5 | 6 | (import ./commands :prefix "") 7 | 8 | (import ./errors) 9 | (import ./file_repository) 10 | (import ./plan/parser :as plan_parser) 11 | (import ./plan/serializer :as plan_serializer) 12 | 13 | # Keep commands sorted alphabetically. 14 | (def argparse-params 15 | ["A command line utility for planning your days" 16 | "insert-days" {:kind :option 17 | :help "Insert the following number of days into the plan."} 18 | "insert-task" {:kind :option 19 | :help "Insert new task for today, if today is in the plan."} 20 | "list-contacts" {:kind :option 21 | :help "List all contacts."} 22 | "remove-empty-days" {:kind :flag 23 | :help "Remove past days without events or tasks."} 24 | "report" {:kind :option 25 | :help "Print tasks for the selected number of days."} 26 | "schedule-contacts" {:kind :option 27 | :help "Schedule contacts to be contacted today."} 28 | "schedule-tasks" {:kind :option 29 | :help "Schedule tasks from a list of tasks in a file."} 30 | "skip-backup" {:kind :flag 31 | :help "Don't create a backup."} 32 | "stats" {:kind :flag 33 | :help "Show stats for the plan file."} 34 | "version" {:kind :flag 35 | :short "v" 36 | :help "Output version information."} 37 | :default {:kind :option}]) 38 | 39 | (defn run-with-file-path [arguments file-path] 40 | (def load-file-result (file_repository/load file-path)) 41 | (def file-errors (load-file-result :errors)) 42 | (if (any? file-errors) 43 | [file-errors (errors/exit-status-codes :file-error)] 44 | (let [plan-string (load-file-result :text) 45 | parse-result (plan_parser/parse plan-string) 46 | parse-errors (parse-result :errors) 47 | plan (parse-result :plan)] 48 | (if (any? parse-errors) 49 | [parse-errors (errors/exit-status-codes :parse-error)] 50 | (let [{:plan new-plan :errors run-errors} (run-commands plan file-path arguments)] 51 | (if (any? run-errors) 52 | [run-errors (errors/exit-status-codes :command-error)] 53 | (let [serialize-empty-inbox (plan_parser/serialize-empty-inbox? plan-string) 54 | new-plan-string (plan_serializer/serialize 55 | new-plan 56 | {:serialize-empty-inbox serialize-empty-inbox})] 57 | (file_repository/save new-plan-string file-path) 58 | errors/no-error))))))) 59 | 60 | (defn run-with-arguments [arguments] 61 | (def file-path (arguments :default)) 62 | (if file-path 63 | (run-with-file-path arguments file-path) 64 | (if (arguments "version") 65 | (do 66 | (print-version) 67 | errors/no-error) 68 | [["Plan file path is missing"] 69 | (errors/exit-status-codes :plan-path-missing)]))) 70 | 71 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 72 | ## Public Interface 73 | 74 | (defn main [& args] 75 | (def arguments (argparse ;argparse-params)) 76 | (if arguments 77 | (let [[errors exit-code] (run-with-arguments arguments)] 78 | (errors/print-errors errors exit-code)))) 79 | -------------------------------------------------------------------------------- /src/commands.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements commands runner. 3 | 4 | (import ./commands/backup) 5 | (import ./commands/insert_days) 6 | (import ./commands/insert_task) 7 | (import ./commands/list_contacts) 8 | (import ./commands/remove_empty_days) 9 | (import ./commands/report) 10 | (import ./commands/schedule_contacts) 11 | (import ./commands/schedule_tasks) 12 | (import ./commands/stats) 13 | 14 | (import ./date :as d) 15 | (import ./errors) 16 | (import ./schedule_parser) 17 | 18 | # backup command needs to be first 19 | # insert-task command needs to be after insert-days 20 | # schedule-contacts command needs to be after insert-days 21 | (def commands [backup/build-command 22 | insert_days/build-command 23 | insert_task/build-command 24 | list_contacts/build-command 25 | remove_empty_days/build-command 26 | report/build-command 27 | schedule_contacts/build-command 28 | schedule_tasks/build-command 29 | stats/build-command]) 30 | 31 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 32 | ## Public Interface 33 | 34 | (defn print-version 35 | ``` 36 | Output version information. 37 | ``` 38 | [] 39 | (print "Alas version 1.5.1")) 40 | 41 | (defn build-commands [arguments file-path] 42 | (filter any? 43 | (map (fn [build-command] (build-command arguments file-path)) 44 | commands))) 45 | 46 | (defn run-commands [plan file-path arguments] 47 | (def commands (build-commands arguments file-path)) 48 | (def errors (filter identity (flatten (map (fn [c] (c :errors)) commands)))) 49 | (var new-plan plan) 50 | (if (empty? errors) 51 | (set new-plan (reduce (fn [new-plan command-and-arguments] 52 | (def command (first (command-and-arguments :command))) 53 | (def arguments (drop 1 (command-and-arguments :command))) 54 | (apply command new-plan arguments)) 55 | plan 56 | commands))) 57 | {:plan new-plan :errors errors}) 58 | -------------------------------------------------------------------------------- /src/commands/backup.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————— 2 | ### This module implements backup command. 3 | 4 | (import ../date) 5 | 6 | (defn- split-path-to-segments [path] 7 | (map (fn [segment] (if (= "" segment) "." segment)) 8 | (string/split "." path))) 9 | 10 | (defn- append-path-index 11 | [path index] 12 | (def path-segments (split-path-to-segments path)) 13 | (string (apply string (array/slice path-segments 0 -2)) 14 | "-" 15 | index 16 | "." 17 | (array/peek path-segments))) 18 | 19 | (defn- append-backup-path-index 20 | ``` 21 | If a file with the path exists, append an index to make the file path unique. 22 | ``` 23 | [path] 24 | (var index 1) 25 | (var new-path path) 26 | (while (os/stat new-path) 27 | (set new-path (append-path-index path index)) 28 | (set index (+ index 1))) 29 | new-path) 30 | 31 | ## ————————————————————————————————————————————————————————————————————————————— 32 | ## Public Interface 33 | 34 | (defn backup-path 35 | ``` 36 | Returns the backup path that includes the date. 37 | 38 | file-path - Path to the backup file. 39 | date - Date to include in the backup file name (today). 40 | 41 | Example: 42 | 43 | (backup-path 'plan.md' (date 2020 8 1)) 44 | > 'plan-2020-08-01.md' 45 | ``` 46 | [file-path date] 47 | (def path-segments (split-path-to-segments file-path)) 48 | (def path (string (apply string (array/slice path-segments 0 -2)) 49 | "-" 50 | (date/format date true) 51 | "." 52 | (array/peek path-segments))) 53 | (append-backup-path-index path)) 54 | 55 | (defn backup [plan plan-path date] 56 | ``` 57 | Creates a backup on a backup path created from plan-path. 58 | 59 | plan - The plan. 60 | plan-path - The path where the plan is located. Used as a base for the backup 61 | path. 62 | date - Today. 63 | ``` 64 | (def plan-file (file/open plan-path :r)) 65 | (def backup-file (file/open (backup-path plan-path date) :w)) 66 | (file/write backup-file (file/read plan-file :all)) 67 | (file/close plan-file) 68 | (file/close backup-file) 69 | (print "Created backup.") 70 | plan) 71 | 72 | (defn build-command [arguments file-path] 73 | (if (arguments "skip-backup") 74 | {} 75 | {:command [backup file-path (date/today)]})) 76 | -------------------------------------------------------------------------------- /src/commands/insert_days.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a command for inserting new days in a plan. 3 | 4 | (import ../date) 5 | (import ../plan) 6 | (import ../day) 7 | 8 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 9 | ## Public Interface 10 | 11 | (defn insert-days 12 | ``` 13 | Inserts new days into the plan. 14 | 15 | (insert-days plan date today) 16 | 17 | plan - The plan entity. 18 | date - The date up to which new days will be generated. 19 | today - Date. 20 | ``` 21 | [plan date today] 22 | (plan/insert-days plan (day/generate-days today date))) 23 | 24 | (defn build-command [arguments &] 25 | (def argument (arguments "insert-days")) 26 | (if argument 27 | (let [insert-days-count (parse argument)] 28 | (if (number? insert-days-count) 29 | {:command [insert-days 30 | (date/days-from-now (- insert-days-count 1)) 31 | (date/today)]} 32 | {:errors ["--insert-days argument is not a number."]})) 33 | {})) 34 | -------------------------------------------------------------------------------- /src/commands/insert_task.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a command for inserting a new task in a plan. 3 | 4 | (import ../date) 5 | (import ../day) 6 | (import ../plan) 7 | (import ../task) 8 | 9 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 10 | ## Public Interface 11 | 12 | (defn insert-task 13 | ``` 14 | Inserts a new pending task into the plan. If a task with the same title already exists, the 15 | command won't insert a new task. If a date with the date doesn't exist, the command won't insert 16 | a new task. 17 | 18 | plan - The plan entity. 19 | date - Date of the day into which to insert the task. 20 | task-title - Task title. 21 | ``` 22 | [plan date task-title] 23 | (def task (task/build-task task-title false)) 24 | (def day (plan/day-with-date plan date)) 25 | (if day 26 | (day/add-task day task)) 27 | plan) 28 | 29 | (defn build-command [arguments &] 30 | (def task-title (arguments "insert-task")) 31 | (if task-title 32 | {:command [insert-task (date/today) task-title]} 33 | {})) 34 | -------------------------------------------------------------------------------- /src/commands/list_contacts.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements the list contacts command. 3 | 4 | (import ../utils :prefix "") 5 | (import ../date :as d) 6 | (import ../errors) 7 | (import ../contact/repository :as contacts_repository) 8 | 9 | (defn- to-csv-line [contact] 10 | (def last-contact (if (contact :last-contact) 11 | (d/format (contact :last-contact) true))) 12 | (string (contact :name) "," 13 | (contact :category) "," 14 | (contact :birthday) "," 15 | last-contact)) 16 | 17 | (defn- print-contacts [plan contacts] 18 | (print "Name,Category,Birthday,Last Contact") 19 | (loop [contact :in contacts] 20 | (print (to-csv-line contact))) 21 | plan) 22 | 23 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 24 | ## Public Interface 25 | 26 | (defn list-contacts 27 | ``` 28 | Returns array of strings that represent the CSV lines, separated by commas. 29 | ``` 30 | [contacts] 31 | (map to-csv-line contacts)) 32 | 33 | (defn build-command [arguments &] 34 | (def argument (arguments "list-contacts")) 35 | (if argument 36 | (let [load-result (contacts_repository/load-contacts argument) 37 | errors (load-result :errors)] 38 | (if errors 39 | {:errors (errors/format-command-errors "--list-contacts" errors)} 40 | {:command [print-contacts (load-result :contacts)]})) 41 | {})) 42 | -------------------------------------------------------------------------------- /src/commands/remove_empty_days.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————— 2 | ### This module implements command for inserting new days in a plan. 3 | 4 | (import ../date) 5 | (import ../plan) 6 | 7 | ## ————————————————————————————————————————————————————————————————————————————— 8 | ## Public Interface 9 | 10 | (defn remove-empty-days 11 | ``` 12 | Removes empty days from plan that are before today. 13 | ``` 14 | [plan today] 15 | (def empty-days (filter (fn [day] (date/before? (day :date) today)) 16 | (plan/empty-days plan))) 17 | (plan/remove-days plan empty-days)) 18 | 19 | (defn build-command [arguments &] 20 | (def argument (arguments "remove-empty-days")) 21 | (if argument 22 | {:command [remove-empty-days (date/today)]} 23 | {})) 24 | -------------------------------------------------------------------------------- /src/commands/report.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements the report command. 3 | 4 | (import ../date) 5 | (import ../plan) 6 | 7 | (defn- unique-tasks [tasks] 8 | (def unique-tasks @[]) 9 | (def sorted-tasks (sorted-by (fn [task] (task :title)) tasks)) 10 | (loop [task :in sorted-tasks 11 | :when (or (empty? unique-tasks) 12 | (not (= (task :title) ((array/peek unique-tasks) :title))))] 13 | (array/push unique-tasks task)) 14 | unique-tasks) 15 | 16 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 17 | ## Public Interface 18 | 19 | (defn report 20 | ``` 21 | Returns tasks from the selected number of days, excluding tasks from today. 22 | ``` 23 | [plan date days-count] 24 | (def start-date (date/-days date days-count)) 25 | (def end-date (date/-days date 1)) 26 | (unique-tasks (plan/tasks-between plan start-date end-date))) 27 | 28 | (defn print-report 29 | [plan date days-count] 30 | (loop [task :in (report plan date days-count)] 31 | (print "- " (task :title))) 32 | plan) 33 | 34 | (defn build-command [arguments &] 35 | (def argument (arguments "report")) 36 | (if argument 37 | (let [days-count (parse argument)] 38 | (if (number? days-count) 39 | {:command [print-report (date/today) days-count]} 40 | {:errors ["--report argument is not a number."]})) 41 | {})) 42 | -------------------------------------------------------------------------------- /src/commands/schedule_contacts.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a command for scheduling contacts for today in a plan. 3 | 4 | (import ../errors) 5 | (import ../utils :prefix "") 6 | 7 | (import ../contact) 8 | (import ../date) 9 | (import ../day) 10 | (import ../plan) 11 | (import ../task) 12 | (import ../contact/repository :as contacts_repository) 13 | 14 | (def contact-prefix "Contact ") 15 | (def birthday-prefix "Congratulate birthday to ") 16 | 17 | (defn- build-task-title [prefix contact] 18 | (string prefix (contact :name))) 19 | 20 | (defn- build-task [prefix contact] 21 | (task/build-contact-task (build-task-title prefix contact) contact)) 22 | 23 | (defn- birthdays [plan contact] 24 | (filter (fn [day] (contact/birthday? contact (day :date))) 25 | (reverse (plan :days)))) 26 | 27 | (defn- missed-birthday [plan contact date] 28 | (def task (build-task birthday-prefix contact)) 29 | (find (fn [day] (and (day/missed-task? day task) 30 | (not (plan/has-task-after? plan task (day :date))))) 31 | (birthdays plan contact))) 32 | 33 | (defn- schedule-missed-birthday-tasks [plan contacts today] 34 | (def day (plan/day-with-date plan today)) 35 | (loop [contact :in contacts] 36 | (let [birthday (missed-birthday plan contact today)] 37 | (if birthday 38 | (day/add-task day 39 | (task/mark-as-missed (build-task birthday-prefix contact) 40 | (birthday :date))))))) 41 | 42 | (defn- schedule-tasks [plan contacts today prefix predicate] 43 | (def future-days (reverse (plan/days-on-or-after plan today))) 44 | (loop [day :in future-days 45 | contact :in contacts] 46 | (let [task (build-task prefix contact)] 47 | (if (and (predicate contact (day :date)) 48 | (not (plan/has-task-on-or-after? plan task today))) 49 | (day/add-task day task))))) 50 | 51 | (defn- schedule-contact-tasks [plan contacts today] 52 | (schedule-tasks plan contacts today contact-prefix contact/contact-on-date?)) 53 | 54 | (defn- schedule-birthday-tasks [plan contacts today] 55 | (schedule-tasks plan contacts today birthday-prefix contact/birthday?)) 56 | 57 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 58 | ## Public Interface 59 | 60 | (defn schedule-contacts [plan contacts today] 61 | (schedule-contact-tasks plan contacts today) 62 | (schedule-birthday-tasks plan contacts today) 63 | (schedule-missed-birthday-tasks plan contacts today) 64 | plan) 65 | 66 | (defn build-command [arguments &] 67 | (def argument (arguments "schedule-contacts")) 68 | (if argument 69 | (let [load-result (contacts_repository/load-contacts argument) 70 | errors (load-result :errors) 71 | contacts (load-result :contacts)] 72 | (if errors 73 | {:errors (errors/format-command-errors "--schedule-contacts" errors)} 74 | {:command [schedule-contacts contacts (date/today)]})) 75 | {})) 76 | -------------------------------------------------------------------------------- /src/commands/schedule_tasks.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a command for scheduling days for today in a plan. 3 | 4 | (import ../utils :prefix "") 5 | 6 | (import ../date) 7 | (import ../day) 8 | (import ../plan) 9 | (import ../task) 10 | 11 | (import ../errors) 12 | (import ../file_repository) 13 | (import ../schedule_parser) 14 | 15 | (def command "--schedule-tasks") 16 | (def weekdays ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday"]) 17 | 18 | (defn- remove-year [formatted-date] 19 | (string/join (drop 1 (string/split "-" formatted-date)) "-")) 20 | 21 | # Public 22 | (defn scheduled-for? [task date] 23 | (def formatted-date (date/format date true)) 24 | (case (task :schedule) 25 | (string "every " (date :week-day)) true 26 | "every weekday" (number? (index-of (date :week-day) weekdays)) 27 | "every month" (= (date :day) 1) 28 | "every 3 months" (and (= (date :day) 1) 29 | (number? (index-of (date :month) [1 4 7 10]))) 30 | (string "every month on " (date :day)) true 31 | (string "every year on " (remove-year formatted-date)) true 32 | (string "on " formatted-date) true 33 | "every last day" (date/last-day-of-month? date) 34 | "every last weekday" (date/last-weekday-of-month? date) 35 | "every last Friday" (date/last-friday-of-month? date) 36 | false)) 37 | 38 | (defn- missed-on-day [plan task date] 39 | (find (fn [day] (and (scheduled-for? task (day :date)) 40 | (not (day/has-task? day task)))) 41 | (plan/all-days-before plan date))) 42 | 43 | (defn- older-than-30-days? [date today] 44 | (date/before? date (date/-days today 30))) 45 | 46 | # Public 47 | (defn missed? 48 | ``` 49 | Checks if the task was missed in the plan up to the passed date. 50 | 51 | Returns true or false. 52 | 53 | plan - The plan. 54 | task - The scheduled task. 55 | date - A date. Typically today. 56 | ``` 57 | [plan task date] 58 | (def day (missed-on-day plan task date)) 59 | (and day 60 | (not (older-than-30-days? (day :date) date)) 61 | (not (plan/has-task-after? plan task (day :date))))) 62 | 63 | (defn- mark-tasks-as-missed [plan tasks date] 64 | (map (fn [task] 65 | (let [day (missed-on-day plan task date)] 66 | (task/mark-as-missed task (day :date)))) 67 | tasks)) 68 | 69 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 70 | ## Public Interface 71 | 72 | (defn schedule-tasks 73 | [plan scheduled-tasks date] 74 | (def future-days (reverse (plan/days-on-or-after plan date))) 75 | (loop [day :in future-days] 76 | (let [tasks (filter (fn [task] (scheduled-for? task (day :date))) scheduled-tasks)] 77 | (day/add-tasks day tasks))) 78 | (loop [day :in future-days] 79 | (let [tasks (filter (fn [task] (missed? plan task date)) scheduled-tasks) 80 | missed-tasks (mark-tasks-as-missed plan tasks date)] 81 | (day/add-tasks day missed-tasks))) 82 | 83 | plan) 84 | 85 | (defn build-command [arguments &] 86 | (def argument (arguments "schedule-tasks")) 87 | (if argument 88 | (let [load-file-result (file_repository/load argument) 89 | file-errors (load-file-result :errors)] 90 | (if (any? file-errors) 91 | {:errors (errors/format-command-errors command file-errors)} 92 | (let [parse-result (schedule_parser/parse (load-file-result :text)) 93 | parse-errors (parse-result :errors)] 94 | (if (any? parse-errors) 95 | {:errors (errors/format-command-errors command parse-errors)} 96 | {:command [schedule-tasks (parse-result :tasks) (date/today)] 97 | :errors []})))) 98 | {})) 99 | -------------------------------------------------------------------------------- /src/commands/stats.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————— 2 | ### This module implements stats command. 3 | 4 | (import ../utils :prefix "") 5 | (import ../plan) 6 | 7 | ## ————————————————————————————————————————————————————————————————————————————— 8 | ## Public Interface 9 | 10 | (defn stats 11 | ``` 12 | Prints stats for the plan. Returns the plan. 13 | ``` 14 | [plan] 15 | (print (pluralize (length (plan :days)) "day")) 16 | (print (pluralize (length (plan/completed-tasks plan)) "completed task")) 17 | (print (pluralize (length (plan/pending-tasks plan)) "pending task")) 18 | plan) 19 | 20 | (defn build-command [arguments &] 21 | (def argument (arguments "stats")) 22 | (if argument 23 | {:command [stats]} 24 | {})) 25 | -------------------------------------------------------------------------------- /src/contact.janet: -------------------------------------------------------------------------------- 1 | ### ————————————————————————————————————————————————————————————————————————————–––––––––––––––––––– 2 | ### This module implements contact entity and related functions. 3 | 4 | (import ./date :as d) 5 | 6 | (def next-contact-periods # in days 7 | {:a 20 8 | :b 60 9 | :c 180 10 | :d 360}) 11 | 12 | ## ————————————————————————————————————————————————————————————————————————————––––––––––––––––––––– 13 | ## Public interface 14 | 15 | (defn build-contact [name &keys {:category category :birthday birthday :last-contact last-contact}] 16 | (def category-val (cond 17 | (keyword? category) category 18 | (string? category) (keyword (string/ascii-lower category)) 19 | category)) 20 | {:name name 21 | :category category-val 22 | :birthday birthday 23 | :last-contact last-contact}) 24 | 25 | (defn next-contact-date [contact] 26 | (if (and (contact :category) (contact :last-contact)) 27 | (d/+days (contact :last-contact) (next-contact-periods (contact :category))))) 28 | 29 | (defn contact-on-date? [contact date] 30 | (d/after-or-eq? date (next-contact-date contact))) 31 | 32 | (defn birthday? [contact date] 33 | (if (contact :birthday) 34 | (let [year (date :year) 35 | birthday (d/parse (string year "-" (contact :birthday)))] 36 | (d/equal? date birthday)) 37 | false)) 38 | -------------------------------------------------------------------------------- /src/contact/parser.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a PEG parser that parses a contact as a string into entities. 3 | 4 | (import ../date :as d) 5 | (import ../contact) 6 | 7 | (def contact-grammar 8 | ~{:main (replace (* :name 9 | (? "\n") 10 | (any :detail) 11 | (? "\n") 12 | (? (* (constant :last-contact) :last-contact))) 13 | ,contact/build-contact) 14 | :name (* "# " (? (some :d)) (replace (capture (some (if-not "\n" 1))) ,string/trim) "\n") 15 | :detail 16 | {:main (+ :category :birthday :other-detail) 17 | :category (* "- Category: " (constant :category) (capture (+ "A" "a" "B" "b" "C" "c" "D" "d")) "\n") 18 | :birthday (* "- Birthday: " (constant :birthday) (capture (* :d :d "-" :d :d)) "\n") 19 | :other-detail (* "- " (some (if-not ":" 1)) ": " (some (if-not "\n" 1)) "\n")} 20 | :last-contact (* "## " (replace :date ,d/parse)) 21 | :date (capture (* :d :d :d :d "-" :d :d "-" :d :d))}) 22 | 23 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 24 | ## Public Interface 25 | 26 | (defn parse 27 | ``` 28 | Parses a string and returns a contact entity. 29 | ``` 30 | [contact-string] 31 | (def parse-result (peg/match contact-grammar contact-string)) 32 | {:contact (first parse-result)}) 33 | -------------------------------------------------------------------------------- /src/contact/repository.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a repository that loads contacts from Markdown files. 3 | 4 | (import ../utils) 5 | 6 | (import ../file_repository) 7 | (import ./parser :as contact_parser) 8 | 9 | (defn- load-contact [contact-path] 10 | (def text ((file_repository/load contact-path) :text)) 11 | (if text 12 | ((contact_parser/parse text) :contact))) 13 | 14 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 15 | ## Public Interface 16 | 17 | (defn load-contacts [path] 18 | (if (os/stat path) 19 | (let [directory (utils/dirname path) 20 | contacts (filter 21 | identity 22 | (map 23 | (fn [contact-file] (load-contact (string directory "/" contact-file))) 24 | (os/dir directory)))] 25 | {:contacts contacts}) 26 | {:errors ["Directory does not exist"]})) 27 | -------------------------------------------------------------------------------- /src/date.janet: -------------------------------------------------------------------------------- 1 | ### ————————————————————————————————————————————————————————————————————————————–––––––––––––––––––– 2 | ### This module implements date utilities. 3 | 4 | (def seconds-in-day (* 60 60 24)) 5 | 6 | (defn- prepend-with-0 7 | [s] 8 | (if (= 1 (length s)) 9 | (string "0" s) 10 | s)) 11 | 12 | (defn- week-day-string 13 | [week-day] 14 | (case week-day 15 | 0 "Sunday" 16 | 1 "Monday" 17 | 2 "Tuesday" 18 | 3 "Wednesday" 19 | 4 "Thursday" 20 | 5 "Friday" 21 | 6 "Saturday")) 22 | 23 | (defn- week-day [date-struct] 24 | (week-day-string ((os/date (os/mktime date-struct)) :week-day))) 25 | 26 | (defn- to-os-date-struct 27 | ``` 28 | Turns an Alas date struct into the Janet date struct. 29 | ``` 30 | [date] 31 | {:year (date :year) 32 | :month (- (date :month) 1) 33 | :month-day (- (date :day) 1)}) 34 | 35 | (defn- from-os-date-struct 36 | [date] 37 | {:year (date :year) 38 | :month (+ 1 (date :month)) 39 | :day (+ 1 (date :month-day)) 40 | :week-day (week-day date)}) 41 | 42 | ## ————————————————————————————————————————————————————————————————————————————––––––––––––––––––––– 43 | ## Public interface 44 | 45 | (defn to-time [date] 46 | (os/mktime (to-os-date-struct date))) 47 | 48 | (defn date 49 | ``` 50 | Builds the date struct. 51 | ``` 52 | [year month day] 53 | (def date-struct {:year year :month (- month 1) :month-day (- day 1)}) 54 | {:year year :month month :day day :week-day (week-day date-struct)}) 55 | 56 | (defn parse 57 | ``` 58 | Builds the date struct from a string. 59 | ``` 60 | [date-string] 61 | (date (splice (map scan-number (string/split "-" date-string))))) 62 | 63 | (defn format 64 | ``` 65 | Formats date in ISO 8601 format. E.g '2021-12-30, Thursday'. 66 | ``` 67 | [date &opt skip-week-day] 68 | (default skip-week-day false) 69 | (def year (string (date :year))) 70 | (def month (prepend-with-0 (string (date :month)))) 71 | (def day (prepend-with-0 (string (date :day)))) 72 | (if skip-week-day 73 | (string/format "%s-%s-%s" year month day) 74 | (string/format "%s-%s-%s, %s" year month day (date :week-day)))) 75 | 76 | (defn today 77 | ``` 78 | Returns today's day in the following format: 79 | 80 | {:year 2021 :month 12 :day 31 :week-day "Friday"} 81 | ``` 82 | [] 83 | (let [today (os/date (os/time) false)] 84 | @{:year (today :year) 85 | :month (+ (today :month) 1) 86 | :day (+ (today :month-day) 1) 87 | :week-day (week-day-string (today :week-day))})) 88 | 89 | (defn +days [date n] 90 | (def new-date-time (+ (to-time date) (* n seconds-in-day))) 91 | (from-os-date-struct (os/date new-date-time))) 92 | 93 | (defn -days [date n] 94 | (def new-date-time (- (to-time date) (* n seconds-in-day))) 95 | (from-os-date-struct (os/date new-date-time))) 96 | 97 | (defn days-from-now [n] 98 | (+days (today) n)) 99 | 100 | (defn equal? 101 | [d1 d2] 102 | (= (to-time d1) (to-time d2))) 103 | 104 | (defn before? 105 | ``` 106 | Returns true if d1 is before d2. 107 | ``` 108 | [d1 d2] 109 | (< (to-time d1) (to-time d2))) 110 | 111 | (defn before-or-eq? 112 | [d1 d2] 113 | (<= (to-time d1) (to-time d2))) 114 | 115 | (defn after? 116 | ``` 117 | Returns true if d1 is after d2. 118 | ``` 119 | [d1 d2] 120 | (> (to-time d1) (to-time d2))) 121 | 122 | (defn after-or-eq? 123 | [d1 d2] 124 | (>= (to-time d1) (to-time d2))) 125 | 126 | (defn weekday? 127 | ``` 128 | Returns true if the date is a week day (Monday-Friday). 129 | ``` 130 | [date] 131 | (has-value? ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday"] 132 | (date :week-day))) 133 | 134 | (defn last-day-of-month? 135 | ``` 136 | Returns true if the date is the last day of a month. 137 | ``` 138 | [date] 139 | (def tomorrow (+days date 1)) 140 | (not= (date :month) (tomorrow :month))) 141 | 142 | (defn last-friday-of-month? 143 | ``` 144 | Returns true if the date is the last friday of a month. 145 | ``` 146 | [date] 147 | (def next-week (+days date 7)) 148 | (and (= (date :week-day) "Friday") 149 | (not= (date :month) (next-week :month)))) 150 | 151 | (defn last-weekday-of-month? 152 | ``` 153 | Return true if the date is the last week day of a month. 154 | ``` 155 | [date] 156 | (or (and (weekday? date) (last-day-of-month? date)) 157 | (and (last-friday-of-month? date) 158 | (not= (date :month) ((+days date 3) :month))))) 159 | -------------------------------------------------------------------------------- /src/day.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements day entity and related functions. 3 | 4 | (import ./date) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Public Interface 8 | 9 | (defn build-day [date &opt events tasks] 10 | (default events @[]) 11 | (default tasks @[]) 12 | {:date date :events events :tasks tasks}) 13 | 14 | (defn generate-days [from-date to-date] 15 | (def days @[]) 16 | (var date from-date) 17 | (while (date/before-or-eq? date to-date) 18 | (array/push days (build-day date)) 19 | (set date (date/+days date 1))) 20 | (reverse days)) 21 | 22 | (defn empty-day? [day] 23 | (and (empty? (day :events)) 24 | (empty? (day :tasks)))) 25 | 26 | (defn has-task? [day task] 27 | (some (fn [t] (= (t :title) (task :title))) 28 | (day :tasks))) 29 | 30 | (defn missed-task? [day task] 31 | (not (has-task? day task))) 32 | 33 | (defn get-time [day] 34 | (date/to-time (day :date))) 35 | 36 | (defn add-task [day task] 37 | (unless (has-task? day task) 38 | (array/push (day :tasks) task))) 39 | 40 | (defn add-tasks [day tasks] 41 | (loop [task :in tasks] 42 | (add-task day task))) 43 | -------------------------------------------------------------------------------- /src/errors.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### Errors. 3 | 4 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 5 | ## Public Interface 6 | 7 | (def exit-status-codes 8 | {:ok 0 9 | :error 1 10 | :plan-path-missing 2 11 | :file-error 3 12 | :parse-error 4 13 | :command-error 5}) 14 | 15 | (def no-error [[] (exit-status-codes :ok)]) 16 | 17 | (defn format-command-errors [command errors] 18 | (map (fn [error] (string command " " (string/ascii-lower error))) errors)) 19 | 20 | (defn print-errors [errors exit-status-code] 21 | (each error errors (print (string error "."))) 22 | (os/exit exit-status-code)) 23 | -------------------------------------------------------------------------------- /src/event.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements event entity and related functions. 3 | 4 | (defn build-event [title &opt body] 5 | {:title title :body body}) 6 | -------------------------------------------------------------------------------- /src/file_repository.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements the file repository. 3 | 4 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 5 | ## Public interface 6 | 7 | (defn save 8 | ``` 9 | Save string to a file to the supplied path. 10 | ``` 11 | [text path] 12 | (def copy-path (string path ".copy")) 13 | (let [file (file/open copy-path :w)] 14 | (file/write file text) 15 | (file/close file)) 16 | (if (os/stat path) 17 | (os/rm path)) 18 | (os/rename copy-path path)) 19 | 20 | (defn load 21 | ``` 22 | Read a string from the file on the file path. 23 | Returns a struct: 24 | 25 | {:text string} - When the file was successfully read. 26 | {:error message} - When the file was not successfully read. 27 | 28 | ``` 29 | [path] 30 | (if (= (os/stat path) nil) 31 | {:errors ["File does not exist"]} 32 | {:text (string (file/read (file/open path) :all)) 33 | :errors []})) 34 | -------------------------------------------------------------------------------- /src/plan.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements plan entity and related functions. 3 | 4 | (import ./date) 5 | (import ./day) 6 | 7 | (defn- tasks-from-days [days] 8 | (array/concat @[] (splice (map (fn [day] (day :tasks)) days)))) 9 | 10 | (defn- days-between [plan start-date end-date] 11 | (defn- day-in-period? [day] 12 | (def date (day :date)) 13 | (and (date/after-or-eq? date start-date) 14 | (date/before-or-eq? date end-date))) 15 | (filter day-in-period? (plan :days))) 16 | 17 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 18 | ## Public Interface 19 | 20 | (defn build-plan [&keys {:title title :inbox inbox :days days}] 21 | (default title "Plan") 22 | (default inbox @[]) 23 | (default days @[]) 24 | {:title title :inbox inbox :days days}) 25 | 26 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 27 | ## Days functions 28 | 29 | (defn day-with-date [plan date] 30 | (find (fn [day] (date/equal? date (day :date))) 31 | (plan :days))) 32 | 33 | (defn has-day-with-date? [plan date] 34 | (day-with-date plan date)) 35 | 36 | (defn empty-days [plan] 37 | (filter day/empty-day? (plan :days))) 38 | 39 | (defn sort-days [plan] 40 | (build-plan :title (plan :title) 41 | :inbox (plan :inbox) 42 | :days (reverse (sort-by day/get-time (plan :days))))) 43 | 44 | (defn insert-days [plan days] 45 | (loop [day :in days 46 | :when (not (has-day-with-date? plan (day :date)))] 47 | (array/push (plan :days) day)) 48 | (sort-days plan)) 49 | 50 | (defn remove-days [plan days] 51 | (defn- keep-day? [day] (not (index-of day days))) 52 | (build-plan :title (plan :title) 53 | :inbox (plan :inbox) 54 | :days (filter keep-day? (plan :days)))) 55 | 56 | (defn days-before 57 | ``` 58 | Returns days that are before the date. 59 | ``` 60 | [plan date] 61 | (drop-while (fn [day] (date/after-or-eq? (day :date) date)) 62 | (plan :days))) 63 | 64 | (defn days-after 65 | ``` 66 | Returns days that are after the date. 67 | ``` 68 | [plan date] 69 | (take-while (fn [day] (date/after? (day :date) date)) 70 | (plan :days))) 71 | 72 | 73 | (defn days-on-or-after 74 | ``` 75 | Returns days that are on or after the date. 76 | ``` 77 | [plan date] 78 | (take-while (fn [day] (date/after-or-eq? (day :date) date)) 79 | (plan :days))) 80 | 81 | (defn all-days 82 | ``` 83 | Return days from the plan without any 'holes'. For days that are missing, a new day will be 84 | generated, without any tasks. 85 | ``` 86 | [plan] 87 | (var date ((last (plan :days)) :date)) 88 | (reverse 89 | (flatten 90 | (map (fn [day] 91 | (def days (reverse (day/generate-days (date/+days date 1) (date/-days (day :date) 1)))) 92 | (array/push days day) 93 | (set date (day :date)) 94 | days) 95 | (reverse (plan :days)))))) 96 | 97 | (defn all-days-before 98 | ``` 99 | Return days from the plan without any 'holes' up to the date. For days that are missing, a new day 100 | will be generated, without any tasks. 101 | ``` 102 | [plan date] 103 | (drop-while (fn [day] (date/after-or-eq? (day :date) date)) 104 | (all-days plan))) 105 | 106 | 107 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 108 | ## Tasks functions 109 | 110 | (defn all-tasks [plan] 111 | (tasks-from-days (plan :days))) 112 | 113 | (defn tasks-between [plan start-date end-date] 114 | (tasks-from-days (days-between plan start-date end-date))) 115 | 116 | (defn completed-tasks [plan] 117 | (filter (fn [t] (t :done)) (all-tasks plan))) 118 | 119 | (defn pending-tasks [plan] 120 | (filter (fn [t] (not (t :done))) (all-tasks plan))) 121 | 122 | (defn has-task-after? 123 | ``` 124 | Returns true if the plan has the task scheduled on a day after the date. 125 | ``` 126 | [plan task date] 127 | (find (fn [day] (day/has-task? day task)) 128 | (days-after plan date))) 129 | 130 | (defn has-task-on-or-after? 131 | ``` 132 | Returns true if the plan has the task scheduled on the day with the date or after. 133 | ``` 134 | [plan task date] 135 | (find (fn [day] (day/has-task? day task)) 136 | (days-on-or-after plan date))) 137 | -------------------------------------------------------------------------------- /src/plan/parser.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a PEG parser that parses a plan as a string into entities. 3 | 4 | (import ../date :as d) 5 | (import ../day) 6 | (import ../event) 7 | (import ../plan) 8 | (import ./serializer :as plan_serializer) 9 | 10 | (def plan-grammar 11 | ~{:main (replace (* (constant :title) :title 12 | (? "\n") 13 | (? (* (constant :inbox) :inbox)) 14 | (constant :days) :days) 15 | ,plan/build-plan) 16 | :title (* "# " :text-line "\n") 17 | :text-line (capture (to (+ "\n" -1))) 18 | :inbox 19 | {:main (* :inbox-title (? "\n") :tasks (? "\n")) 20 | :inbox-title (* "## Inbox" (? "\n"))} 21 | :days 22 | {:main (group (any :day)) 23 | :day 24 | {:main (replace (* :day-title (? "\n") :events :tasks (? "\n")) ,day/build-day) 25 | :day-title (* "## " :date ", " :week-day (? "\n")) 26 | :week-day (+ "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday") 27 | :events 28 | {:main (group (any :event)) 29 | :event 30 | {:main (replace (* :event-begin 31 | :text-line 32 | (? "\n") 33 | :event-body 34 | (? "\n")) 35 | ,event/build-event) 36 | :event-begin (* "- " (not "[")) 37 | :event-body {:main (group (any :event-body-line)) 38 | :event-body-line (* " " :text-line (? "\n"))}}}}} 39 | :tasks 40 | {:main (group (any :task)) 41 | :task 42 | {:main (replace (* (constant :done) :task-begin 43 | " " 44 | (constant :title) (capture (some (if-not (+ "\n" " (missed on") 1))) 45 | (? :task-missed-on-date) 46 | (? "\n") 47 | (constant :body) :task-body 48 | (? "\n")) 49 | ,struct) 50 | :task-begin 51 | {:main (* "- " :checkbox) 52 | :checkbox 53 | {:main (+ :checkbox-done :checkbox-pending) 54 | :checkbox-done (* (+ "[x]" "[X]") (constant true)) 55 | :checkbox-pending (* "[ ]" (constant false))}} 56 | :task-missed-on-date (* " (missed on " (constant :missed-on) :date ")") 57 | :task-body 58 | {:main (group (any :task-body-line)) 59 | :task-body-line (* " " :text-line (? "\n"))}}} 60 | :date (replace (capture (* :d :d :d :d "-" :d :d "-" :d :d)) ,d/parse)}) 61 | 62 | (defn- lines-count [plan-string &opt options] 63 | (default options {:ignore-whitespace true}) 64 | (def filter-function (if (options :ignore-whitespace) 65 | (fn [line] (not= (string/trim line) "")) 66 | (fn [_line] true))) 67 | (length (filter filter-function (string/split "\n" plan-string)))) 68 | 69 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 70 | ## Public Interface 71 | 72 | (defn serialize-empty-inbox? [plan-string] 73 | (truthy? (string/find "## Inbox" plan-string))) 74 | 75 | (defn parse 76 | ``` 77 | Parses plan string and returns plan as entities. 78 | ``` 79 | [plan-string] 80 | (def serialize-empty-inbox (serialize-empty-inbox? plan-string)) 81 | (def parse-result (peg/match plan-grammar plan-string)) 82 | (if parse-result 83 | (let [plan (first parse-result) 84 | parsed-plan-string (plan_serializer/serialize 85 | plan 86 | {:serialize-empty-inbox serialize-empty-inbox})] 87 | (if (= (lines-count parsed-plan-string) (lines-count plan-string)) 88 | {:plan plan :errors []} 89 | {:errors [(string "Plan can not be parsed: last parsed line is line " 90 | (lines-count parsed-plan-string {:ignore-whitespace false}))]})) 91 | {:errors ["Plan can not be parsed"]})) 92 | -------------------------------------------------------------------------------- /src/plan/serializer.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements serializing a plan into a string. 3 | 4 | (import ../date) 5 | 6 | (def body-indentation " ") 7 | 8 | (defn- plan-title [plan] 9 | (string "# " (plan :title))) 10 | 11 | (defn- checkbox [done] 12 | (if done "[X]" "[ ]")) 13 | 14 | (defn- serialize-event-title [event] 15 | (string "- " (event :title))) 16 | 17 | (defn- serialize-event-body [event] 18 | (def body (event :body)) 19 | (if (or (nil? body) (empty? body)) 20 | "" 21 | (string "\n" 22 | (string/join 23 | (map (fn [line] (string body-indentation line)) body) 24 | "\n")))) 25 | 26 | (defn- serialize-event [event] 27 | (string 28 | (serialize-event-title event) 29 | (serialize-event-body event))) 30 | 31 | (defn- task-mark [task] 32 | (if (task :missed-on) 33 | (string " (missed on " (date/format (task :missed-on) true) ")") 34 | "")) 35 | 36 | (defn- serialize-task-title [task] 37 | (string "- " (checkbox (task :done)) " " (task :title) (task-mark task))) 38 | 39 | (defn- serialize-task-body [task] 40 | (def body (task :body)) 41 | (if (or (nil? body) (empty? body)) 42 | "" 43 | (string "\n" 44 | (string/join 45 | (map (fn [line] (string body-indentation line)) body) 46 | "\n")))) 47 | 48 | (defn- serialize-task [task] 49 | (string 50 | (serialize-task-title task) 51 | (serialize-task-body task))) 52 | 53 | (defn- plan-inbox [plan serialize-empty-inbox] 54 | (if (any? (plan :inbox)) 55 | (array/concat @["\n## Inbox\n"] (map serialize-task (plan :inbox))) 56 | (if serialize-empty-inbox 57 | ["\n## Inbox"] 58 | []))) 59 | 60 | (defn- day-title [day new-line] 61 | (def title (string "\n## " (date/format (day :date)))) 62 | (if new-line 63 | (string title "\n") 64 | title)) 65 | 66 | (defn- serialize-day [day] 67 | (def events (map serialize-event (day :events))) 68 | (def tasks (map serialize-task (day :tasks))) 69 | (def new-line (or (any? events) (any? tasks))) 70 | (array/concat @[(day-title day new-line)] events tasks)) 71 | 72 | (defn- serialize-days [days] 73 | (flatten (map serialize-day days))) 74 | 75 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 76 | ## Public Interface 77 | 78 | (defn serialize 79 | ``` 80 | Serializes plan into a string. 81 | ``` 82 | [plan &opt options] 83 | (default options {}) 84 | (def serialize-empty-inbox (get options :serialize-empty-inbox false)) 85 | (def plan-lines @[]) 86 | (array/push plan-lines (plan-title plan)) 87 | (array/concat plan-lines (plan-inbox plan serialize-empty-inbox)) 88 | (array/concat plan-lines (serialize-days (plan :days))) 89 | (string (string/join plan-lines "\n") "\n")) 90 | -------------------------------------------------------------------------------- /src/schedule_parser.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements a PEG parser that parses schedule string into 3 | ### task entities. 4 | 5 | (import ./task) 6 | 7 | (def schedule-grammar 8 | ~{:main (* (drop :title) :tasks) 9 | :title (* "# " (some (+ :w+ :s+))) 10 | :tasks 11 | {:main (group (any :task)) 12 | :task 13 | {:main (replace (* "- " (line) :task-title :task-schedule (? "\n")) 14 | ,task/build-scheduled-task) 15 | :task-title (replace (capture (some (to (+ "(" "\n")))) ,string/trim) 16 | :task-schedule (* "(" (replace (capture (some (+ :w+ :s+ "-"))) ,string/trim) ")")}}}) 17 | 18 | (defn- task-lines-count 19 | ``` 20 | Returns the number of lines that start with '-' in the schedule string. 21 | ``` 22 | [schedule-string] 23 | (length (filter (fn [line] (string/has-prefix? "-" line)) 24 | (string/split "\n" schedule-string)))) 25 | 26 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 27 | ## Public Interface 28 | 29 | (defn parse 30 | ``` 31 | Parses schedule-string and returns a tuple: 32 | 33 | - {:tasks tasks} Where tasks is an array of task entities, when parsing was successfull. 34 | - {:errors errors} Where errors is an array of strings. 35 | ``` 36 | [schedule-string] 37 | (let [parse-result (peg/match schedule-grammar schedule-string)] 38 | (if parse-result 39 | (let [tasks (first parse-result)] 40 | (if (empty? tasks) 41 | {:errors ["Schedule is empty"]} 42 | (do 43 | (if (= (length tasks) (task-lines-count schedule-string)) 44 | {:tasks tasks :errors []} 45 | {:errors [(string "Schedule can not be parsed - last parsed task is \"" 46 | ((last tasks) :title) 47 | "\"" 48 | " on line " 49 | ((last tasks) :line))]})))) 50 | {:errors ["Schedule can not be parsed"]}))) 51 | -------------------------------------------------------------------------------- /src/task.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements task entity and related functions. 3 | 4 | (import ./date) 5 | 6 | (defn build-task [title done &opt body] 7 | (default body @[]) 8 | {:title title :body body :done done}) 9 | 10 | (defn build-scheduled-task [line title schedule] 11 | {:line line :title title :done false :schedule schedule}) 12 | 13 | (defn build-missed-task [title date &opt body] 14 | (default body @[]) 15 | {:title title :body body :done false :missed-on date}) 16 | 17 | (defn build-contact-task [title contact &opt body] 18 | (default body @[]) 19 | {:title title :body body :done false :contact contact}) 20 | 21 | (defn mark-as-missed 22 | ``` 23 | Adds :missed-on key to the task. 24 | ``` 25 | [task date] 26 | (merge task {:missed-on date})) 27 | -------------------------------------------------------------------------------- /src/utils.janet: -------------------------------------------------------------------------------- 1 | ### ———————————————————————————————————————————————————————————————————————————————————————————————— 2 | ### This module implements various utilites. 3 | 4 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 5 | ## Public Interface 6 | 7 | (defn pluralize [n word] 8 | (if (= n 1) 9 | (string n " " word) 10 | (string n " " word "s"))) 11 | 12 | (defn dirname [path] 13 | (if (string/has-suffix? "/" path) 14 | (string/trimr path "/") 15 | path)) 16 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./jpm_tree/bin/judge 4 | -------------------------------------------------------------------------------- /test/alas_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/alas) 4 | 5 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 6 | ## Test run-with-file-path 7 | 8 | (deftest "returns exit status 0 when there were no errors" 9 | (let [arguments {"skip-backup" true "stats" true} 10 | [errors exit-status] (alas/run-with-file-path arguments "./test/examples/todo.md")] 11 | (test (empty? errors) true) 12 | (test exit-status 0))) 13 | 14 | (deftest "returns exit status 3 when there are file errors" 15 | (let [arguments {"skip-backup" true "stats" true} 16 | [errors exit-status] (alas/run-with-file-path arguments "./test/examples/missing-todo.md")] 17 | (test (empty? errors) false) 18 | (test exit-status 3))) 19 | 20 | (deftest "returns exit status 4 when there are parsing errors" 21 | (let [arguments {"skip-backup" true "stats" true} 22 | [errors exit-status] (alas/run-with-file-path arguments "./test/examples/unparsable-todo.md")] 23 | (test (empty? errors) false) 24 | (test exit-status 4))) 25 | 26 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 27 | ## Test run-with-arguments 28 | 29 | (deftest "returns exit status 0 when there were no errors" 30 | (let [arguments {"skip-backup" true "stats" true :default "./test/examples/todo.md"} 31 | [errors exit-status] (alas/run-with-arguments arguments)] 32 | (test (empty? errors) true) 33 | (test exit-status 0))) 34 | 35 | (deftest "returns exit status 2 when when the file path is missing" 36 | (let [arguments {"skip-backup" true "stats" true} 37 | [errors exit-status] (alas/run-with-arguments arguments)] 38 | (test (empty? errors) false) 39 | (test exit-status 2))) 40 | -------------------------------------------------------------------------------- /test/commands/backup_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date) 4 | (import ../../src/file_repository) 5 | (import ../../src/commands/backup :prefix "") 6 | 7 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 8 | ## Test backup-path 9 | 10 | (deftest "when file doesn't exist" 11 | (test (backup-path "test/examples/plan.md" (date/date 2020 8 1)) 12 | "test/examples/plan-2020-08-01.md")) 13 | 14 | (deftest "when plan path begins with dot" 15 | (test (backup-path "./test/examples/plan.md" (date/date 2020 8 1)) 16 | "./test/examples/plan-2020-08-01.md") 17 | (test (backup-path "./test/examples/plan.md" (date/date 2020 8 2)) 18 | "./test/examples/plan-2020-08-02-1.md")) 19 | 20 | (deftest "when plan path begins with two dots" 21 | (test (backup-path "../alas/test/examples/plan.md" (date/date 2020 8 1)) 22 | "../alas/test/examples/plan-2020-08-01.md") 23 | (test (backup-path "../alas/test/examples/plan.md" (date/date 2020 8 2)) 24 | "../alas/test/examples/plan-2020-08-02-1.md")) 25 | 26 | (deftest "when file exists" 27 | (test (backup-path "test/examples/plan.md" (date/date 2020 8 2)) 28 | "test/examples/plan-2020-08-02-1.md") 29 | (test (backup-path "test/examples/plan.md" (date/date 2020 8 3)) 30 | "test/examples/plan-2020-08-03-2.md")) 31 | 32 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 33 | ## Test backup 34 | 35 | (deftest "backup plan" 36 | (def plan-path "test/examples/todo.md") 37 | (def backup-path "test/examples/todo-2020-08-01.md") 38 | (def plan (file_repository/load plan-path)) 39 | (backup plan plan-path (date/date 2020 8 1)) 40 | (test (table? (os/stat backup-path)) true) 41 | (os/rm backup-path)) 42 | 43 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 44 | ## Test build-command 45 | 46 | (deftest "when not skipping backup" 47 | (def arguments {"stats" true}) 48 | (def result (build-command arguments "test/examples/plan.md")) 49 | (test (tuple? (result :command)) true)) 50 | 51 | (deftest "when skipping backup" 52 | (def arguments {"skip-backup" true "stats" true}) 53 | (def result (build-command arguments "test/examples/plan.md")) 54 | (test (empty? result) true)) 55 | -------------------------------------------------------------------------------- /test/commands/insert_days_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as "d") 4 | (import ../../src/day) 5 | (import ../../src/plan) 6 | (import ../../src/commands/insert_days :prefix "") 7 | 8 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 9 | ## Test insert-days 10 | 11 | (deftest "insert day at the top" 12 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 3)) 13 | (day/build-day (d/date 2020 8 2))])) 14 | (def new-plan (insert-days plan (d/date 2020 8 4) (d/date 2020 8 4))) 15 | (def day-1 (first (new-plan :days))) 16 | (test (length (new-plan :days)) 3) 17 | (test (day-1 :date) 18 | {:day 4 :month 8 :week-day "Tuesday" :year 2020}) 19 | (test (empty? (day-1 :tasks)) true)) 20 | 21 | (deftest "insert three days at the top" 22 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 3)) 23 | (day/build-day (d/date 2020 8 2))])) 24 | (def new-plan (insert-days plan (d/date 2020 8 7) (d/date 2020 8 5))) 25 | (def day-1 (first (new-plan :days))) 26 | (test (length (new-plan :days)) 5) 27 | (test (day-1 :date) 28 | {:day 7 :month 8 :week-day "Friday" :year 2020})) 29 | 30 | (deftest "insert days in the middle" 31 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 6)) 32 | (day/build-day (d/date 2020 8 2))])) 33 | (def new-plan (insert-days plan (d/date 2020 8 4) (d/date 2020 8 4))) 34 | (def day-2 ((new-plan :days) 1)) 35 | (test (length (new-plan :days)) 3) 36 | (test (day-2 :date) 37 | {:day 4 :month 8 :week-day "Tuesday" :year 2020})) 38 | 39 | (deftest "insert days when today already exists" 40 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 6)) 41 | (day/build-day (d/date 2020 8 4))])) 42 | (def new-plan (insert-days plan (d/date 2020 8 6) (d/date 2020 8 4))) 43 | (def day-2 ((new-plan :days) 1)) 44 | (test (length (new-plan :days)) 3) 45 | (test (day-2 :date) 46 | {:day 5 :month 8 :week-day "Wednesday" :year 2020})) 47 | 48 | (deftest "insert days with empty todo" 49 | (def plan (plan/build-plan)) 50 | (def new-plan (insert-days plan (d/date 2020 8 4) (d/date 2020 8 4))) 51 | (test (length (new-plan :days)) 1) 52 | (test ((first (new-plan :days)) :date) 53 | {:day 4 :month 8 :week-day "Tuesday" :year 2020})) 54 | 55 | (deftest "insert days with one day in the future" 56 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 6))])) 57 | (def new-plan (insert-days plan(d/date 2020 8 5) (d/date 2020 8 4))) 58 | (def day-2 ((new-plan :days) 1)) 59 | (test (length (new-plan :days)) 3) 60 | (test (day-2 :date) 61 | {:day 5 :month 8 :week-day "Wednesday" :year 2020})) 62 | 63 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 64 | ## Test build-command 65 | 66 | (deftest "without matching arguments" 67 | (def arguments {"stats" true}) 68 | (test (empty? (build-command arguments)) true)) 69 | 70 | (deftest "with correct arguments" 71 | (def arguments {"insert-days" "3"}) 72 | (def result (build-command arguments)) 73 | (test (tuple? (result :command)) true)) 74 | 75 | (deftest "with incorrect arguments" 76 | (def arguments {"insert-days" "three"}) 77 | (def result (build-command arguments)) 78 | (test (nil? (result :command)) true) 79 | (test (first (result :errors)) "--insert-days argument is not a number.")) 80 | -------------------------------------------------------------------------------- /test/commands/insert_task_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as "d") 4 | (import ../../src/day) 5 | (import ../../src/plan) 6 | (import ../../src/task) 7 | (import ../../src/commands/insert_task :prefix "") 8 | 9 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 10 | ## Test insert-task 11 | 12 | (deftest "when the day exists and the task doesn't exist" 13 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 10))])) 14 | (def new-plan (insert-task plan (d/date 2020 8 10) "Upgrade OS")) 15 | (def day (first (new-plan :days))) 16 | (test (length (day :tasks)) 1) 17 | (test ((first (day :tasks)) :title) "Upgrade OS")) 18 | 19 | (deftest "when the day doesn't exist" 20 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 8 10))])) 21 | (def new-plan (insert-task plan (d/date 2020 8 11) "Upgrade OS")) 22 | (test (length (plan :days)) 1) 23 | (test (empty? ((first (plan :days)) :tasks)) true)) 24 | 25 | (deftest "when the task already exists" 26 | (def day (day/build-day (d/date 2020 8 10) 27 | @[] 28 | @[(task/build-task "Upgrade OS" false)])) 29 | (def plan (plan/build-plan :days @[day])) 30 | (def new-plan (insert-task plan (d/date 2020 8 10) "Upgrade OS")) 31 | (def new-day (first (new-plan :days))) 32 | (test (length (new-day :tasks)) 1) 33 | (test ((first (new-day :tasks)) :title) "Upgrade OS")) 34 | 35 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 36 | ## Test build-command 37 | 38 | (deftest "without matching arguments" 39 | (def arguments {"stats" true}) 40 | (test (empty? (build-command arguments)) true)) 41 | 42 | (deftest "with correct arguments" 43 | (def arguments {"insert-task" "Upgrade OS"}) 44 | (def result (build-command arguments)) 45 | (test (tuple? (result :command)) true)) 46 | -------------------------------------------------------------------------------- /test/commands/list_contacts_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/commands/list_contacts :prefix "") 4 | (import ../../src/date :as d) 5 | (import ../../src/contact) 6 | 7 | ## ————————————————————————————————————————————————————————————————————————————————————————————————– 8 | ## Test list-contacts 9 | 10 | (deftest "#list-contacts" 11 | (def contacts @[(contact/build-contact "John Doe" :category :a) 12 | (contact/build-contact "Jane Doe" :birthday "03-21") 13 | (contact/build-contact "Marry Doe" :last-contact (d/date 2022 03 15))]) 14 | (def lines (list-contacts contacts)) 15 | (test (length lines) 3) 16 | (test (lines 0) "John Doe,a,,") 17 | (test (lines 1) "Jane Doe,,03-21,") 18 | (test (lines 2) "Marry Doe,,,2022-03-15")) 19 | 20 | ## ————————————————————————————————————————————————————————————————————————————————————————————————– 21 | ## Test build-command 22 | 23 | (deftest "without matching arguments" 24 | (def arguments {"stats" true}) 25 | (test (empty? (build-command arguments)) true)) 26 | 27 | (deftest "with correct arguments" 28 | (def arguments {"list-contacts" "test/examples/contacts"}) 29 | (def result (build-command arguments)) 30 | (test (tuple? (result :command)) true)) 31 | 32 | (deftest "when the directory doesn't exist" 33 | (def arguments {"list-contacts" "test/missing-directory"}) 34 | (def result (build-command arguments)) 35 | (test (first (result :errors)) "--list-contacts directory does not exist")) 36 | -------------------------------------------------------------------------------- /test/commands/remove_empty_days_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as "d") 4 | (import ../../src/plan) 5 | (import ../../src/day) 6 | (import ../../src/task) 7 | (import ../../src/commands/remove_empty_days :prefix "") 8 | 9 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 10 | ## Test remove-empty-days 11 | 12 | (deftest "removes empty days in the past" 13 | (def plan 14 | (plan/build-plan 15 | :days @[(day/build-day (d/date 2020 8 7)) 16 | (day/build-day (d/date 2020 8 5)) 17 | (day/build-day (d/date 2020 8 4)) 18 | (day/build-day (d/date 2020 8 3) @[] 19 | @[(task/build-task "Buy milk" true)])])) 20 | (def new-plan (remove-empty-days plan (d/date 2020 8 6))) 21 | (test (length (new-plan :days)) 2) 22 | (test (((new-plan :days) 0) :date) 23 | {:day 7 :month 8 :week-day "Friday" :year 2020}) 24 | (test (((new-plan :days) 1) :date) 25 | {:day 3 :month 8 :week-day "Monday" :year 2020})) 26 | 27 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 28 | ## Test build-command 29 | 30 | (deftest "doesn't build the command when arguments are not matching" 31 | (def arguments {"stats" true}) 32 | (test (empty? (build-command arguments)) true)) 33 | 34 | (deftest "builds the command when arguments are matching" 35 | (def arguments {"remove-empty-days" true}) 36 | (def result (build-command arguments)) 37 | (test (tuple? (result :command)) true)) 38 | -------------------------------------------------------------------------------- /test/commands/report_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as "d") 4 | (import ../../src/plan) 5 | (import ../../src/day) 6 | (import ../../src/task) 7 | (import ../../src/commands/report :prefix "") 8 | 9 | ## ————————————————————————————————————————————————————————————————————————————————————————————————– 10 | ## Test report 11 | 12 | (deftest "returns the specified number of tasks" 13 | (def plan 14 | (plan/build-plan 15 | :days @[(day/build-day (d/date 2020 8 7) @[] 16 | @[(task/build-task "Task 1" true)]) 17 | (day/build-day (d/date 2020 8 6) @[] 18 | @[(task/build-task "Task 2" true) 19 | (task/build-task "Task 3" true)]) 20 | (day/build-day (d/date 2020 8 5) @[] 21 | @[(task/build-task "Task 3" true) 22 | (task/build-task "Task 4" true)]) 23 | (day/build-day (d/date 2020 8 4) @[] 24 | @([(task/build-task "Task 5" true)]))])) 25 | (def tasks (report plan (d/date 2020 8 7) 2)) 26 | (test (length tasks) 3) 27 | (test ((tasks 0) :title) "Task 2") 28 | (test ((tasks 1) :title) "Task 3") 29 | (test ((tasks 2) :title) "Task 4")) 30 | 31 | ## ————————————————————————————————————————————————————————————————————————————————————————————————– 32 | ## Test build-command 33 | 34 | (deftest "doesn't build the command when arguments are not matching" 35 | (def arguments {"stats" true}) 36 | (test (empty? (build-command arguments)) true)) 37 | 38 | (deftest "builds the command when arguments are matching" 39 | (def arguments {"report" "7"}) 40 | (def result (build-command arguments)) 41 | (test (tuple? (result :command)) true)) 42 | 43 | (deftest "returns error when arguments are not correct" 44 | (def arguments {"report" "seven"}) 45 | (def result (build-command arguments)) 46 | (test (nil? (result :command)) true) 47 | (test (first (result :errors)) "--report argument is not a number.")) 48 | -------------------------------------------------------------------------------- /test/commands/schedule_contacts_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as d) 4 | (import ../../src/contact) 5 | (import ../../src/plan) 6 | (import ../../src/day) 7 | (import ../../src/task) 8 | (import ../../src/commands/schedule_contacts :prefix "") 9 | 10 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 11 | ## Test schedule-contacts 12 | 13 | (deftest "schedules contacts for today" 14 | (def contact (contact/build-contact "John Doe" :last-contact (d/date 2022 4 1) :category :a)) 15 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2022 4 25))])) 16 | (schedule-contacts plan @[contact] (d/date 2022 4 25)) 17 | (let [day ((plan :days) 0) 18 | task ((day :tasks) 0)] 19 | (test (task :title) "Contact John Doe") 20 | (test (task :done) false) 21 | (test (empty? (task :body)) true))) 22 | 23 | (deftest "schedules contacts for future" 24 | (def contact (contact/build-contact "John Doe" :last-contact (d/date 2022 4 1) :category :a)) 25 | (def day-1 (day/build-day (d/date 2022 4 22))) 26 | (def day-2 (day/build-day (d/date 2022 4 21))) 27 | (def day-3 (day/build-day (d/date 2022 4 20))) 28 | (def plan (plan/build-plan :days @[day-1 day-2 day-3])) 29 | (schedule-contacts plan @[contact] (d/date 2022 4 20)) 30 | (test (empty? (day-1 :tasks)) true) 31 | (test (not (empty? (day-2 :tasks))) true) 32 | (test (empty? (day-3 :tasks)) true) 33 | (if (= 1 (length (day-2 :tasks))) 34 | (let [task ((day-2 :tasks) 0)] 35 | (test (task :title) "Contact John Doe") 36 | (test (task :done) false) 37 | (test (empty? (task :body)) true)))) 38 | 39 | (deftest "schedules contacts with birthday" 40 | (def contact (contact/build-contact "John Doe" 41 | :birthday "04-25" 42 | :last-contact (d/date 2022 4 21) 43 | :category :a)) 44 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2022 4 25))])) 45 | (schedule-contacts plan @[contact] (d/date 2022 4 25)) 46 | (let [day ((plan :days) 0) 47 | task ((day :tasks) 0)] 48 | (test (task :title) "Congratulate birthday to John Doe") 49 | (test (task :done) false) 50 | (test (empty? (task :body)) true))) 51 | 52 | (deftest "schedules contacts with missed birthday" 53 | (def contact (contact/build-contact "John Doe" 54 | :birthday "04-25" 55 | :last-contact (d/date 2022 4 21) 56 | :category :a)) 57 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2022 4 26)) 58 | (day/build-day (d/date 2022 4 25))])) 59 | (schedule-contacts plan @[contact] (d/date 2022 4 26)) 60 | (let [day ((plan :days) 0) 61 | task ((day :tasks) 0)] 62 | (test (task :title) "Congratulate birthday to John Doe") 63 | (test (d/equal? (d/date 2022 4 25) (task :missed-on)) true))) 64 | 65 | (deftest "schedules a contact with the birthday on a missed day" 66 | (def contact (contact/build-contact "John Doe" 67 | :birthday "04-25" 68 | :last-contact (d/date 2022 4 21) 69 | :category :a)) 70 | (def day-1 (day/build-day (d/date 2022 4 26))) 71 | (def day-2 (day/build-day (d/date 2022 4 25) 72 | @[] 73 | @[(task/build-task "Weekly meeting" false)])) 74 | (def plan (plan/build-plan :days @[day-1 day-2])) 75 | (schedule-contacts plan @[contact] (d/date 2022 4 26)) 76 | (test (not (empty? (day-1 :tasks))) true) 77 | (test (length (day-2 :tasks)) 1) 78 | (if (not (empty? (day-1 :tasks))) 79 | (let [task ((day-1 :tasks) 0)] 80 | (test (task :title) "Congratulate birthday to John Doe")))) 81 | 82 | (deftest "schedules contact with the birthday on a day in future" 83 | (def contact (contact/build-contact "John Doe" 84 | :birthday "04-26" 85 | :last-contact (d/date 2022 4 21) 86 | :category :a)) 87 | (def day-1 (day/build-day (d/date 2022 4 26))) 88 | (def day-2 (day/build-day (d/date 2022 4 25))) 89 | (def plan (plan/build-plan :days @[day-1 day-2])) 90 | (schedule-contacts plan @[contact] (d/date 2022 4 25)) 91 | (test (not (empty? (day-1 :tasks))) true) 92 | (test (empty? (day-2 :tasks)) true) 93 | (if (not (empty? (day-1 :tasks))) 94 | (let [task ((day-1 :tasks) 0)] 95 | (test (task :title) "Congratulate birthday to John Doe") 96 | (test (not (task :missed-on)) true)))) 97 | 98 | (deftest "doesn't schedule contact after missed date twice" 99 | (def contact (contact/build-contact "John Doe" 100 | :birthday "04-25" 101 | :last-contact (d/date 2022 4 21) 102 | :category :a)) 103 | (def day-1 (day/build-day (d/date 2022 4 27))) 104 | (def day-2 (day/build-day (d/date 2022 4 26) 105 | @[] 106 | @[(task/build-task "Congratulate birthday to John Doe" true)])) 107 | (def day-3 (day/build-day (d/date 2022 4 25) 108 | @[] 109 | @[(task/build-task "Weekly meeting" false)])) 110 | (def plan (plan/build-plan :days @[day-1 day-2 day-3])) 111 | (schedule-contacts plan @[contact] (d/date 2022 4 27)) 112 | (test (empty? (day-1 :tasks)) true)) 113 | 114 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 115 | ## Test build-command 116 | 117 | (deftest "doesn't build the command when arguments are not matching" 118 | (def arguments {"stats" true}) 119 | (test (empty? (build-command arguments)) true)) 120 | 121 | (deftest "builds the command when arguments are matching" 122 | (def arguments {"schedule-contacts" "test/examples/contacts"}) 123 | (def result (build-command arguments)) 124 | (test (tuple? (result :command)) true)) 125 | 126 | (deftest "returns an error when the contacts directory doesn't exist" 127 | (def arguments {"schedule-contacts" "test/examples/people"}) 128 | (def result (build-command arguments)) 129 | (test (nil? (result :command)) true) 130 | (test (first (result :errors)) "--schedule-contacts directory does not exist")) 131 | -------------------------------------------------------------------------------- /test/commands/schedule_tasks_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/date :as "d") 4 | (import ../../src/plan) 5 | (import ../../src/day) 6 | (import ../../src/task) 7 | (import ../../src/commands/schedule_tasks :prefix "") 8 | 9 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 10 | ## Test scheduled-for? 11 | 12 | (deftest "every Monday" 13 | (def task (task/build-scheduled-task 42 "Weekly meeting" "every Monday")) 14 | (test (scheduled-for? task (d/date 2022 1 24)) true) 15 | (test (scheduled-for? task (d/date 2022 1 25)) false)) 16 | 17 | (deftest "every Tuesday" 18 | (def task (task/build-scheduled-task 42 "Weekly meeting" "every Tuesday")) 19 | (test (scheduled-for? task (d/date 2022 1 24)) false) 20 | (test (scheduled-for? task (d/date 2022 1 25)) true)) 21 | 22 | (deftest "every month" 23 | (def task (task/build-scheduled-task 42 "Review logs" "every month")) 24 | (test (scheduled-for? task (d/date 2022 1 1)) true) 25 | (test (scheduled-for? task (d/date 2022 1 2)) false) 26 | (test (scheduled-for? task (d/date 2022 6 1)) true) 27 | (test (scheduled-for? task (d/date 2022 6 15)) false)) 28 | 29 | (deftest "every 3 months" 30 | (def task (task/build-scheduled-task 42 "Review logs" "every 3 months")) 31 | (test (scheduled-for? task (d/date 2022 1 1)) true) 32 | (test (scheduled-for? task (d/date 2022 2 1)) false) 33 | (test (scheduled-for? task (d/date 2022 3 1)) false) 34 | (test (scheduled-for? task (d/date 2022 4 1)) true) 35 | (test (scheduled-for? task (d/date 2022 5 1)) false) 36 | (test (scheduled-for? task (d/date 2022 6 1)) false) 37 | (test (scheduled-for? task (d/date 2022 7 1)) true) 38 | (test (scheduled-for? task (d/date 2022 8 1)) false) 39 | (test (scheduled-for? task (d/date 2022 9 1)) false) 40 | (test (scheduled-for? task (d/date 2022 10 1)) true)) 41 | 42 | (deftest "every weekday" 43 | (def task (task/build-scheduled-task 42 "Review logs" "every weekday")) 44 | (test (scheduled-for? task (d/date 2022 1 24)) true) # Monday 45 | (test (scheduled-for? task (d/date 2022 1 25)) true) # Tuesday 46 | (test (scheduled-for? task (d/date 2022 1 26)) true) # Wednesday 47 | (test (scheduled-for? task (d/date 2022 1 27)) true) # Thursday 48 | (test (scheduled-for? task (d/date 2022 1 28)) true) # Friday 49 | (test (scheduled-for? task (d/date 2022 1 29)) false) # Saturday 50 | (test (scheduled-for? task (d/date 2022 1 30)) false)) # Sunday 51 | 52 | (deftest "every month on some date" 53 | (def task (task/build-scheduled-task 42 "Review logs" "every month on 15")) 54 | (test (scheduled-for? task (d/date 2022 1 15)) true) 55 | (test (scheduled-for? task (d/date 2022 2 15)) true) 56 | (test (scheduled-for? task (d/date 2022 10 15)) true) 57 | (test (scheduled-for? task (d/date 2022 1 14)) false) 58 | (test (scheduled-for? task (d/date 2022 1 16)) false)) 59 | 60 | (deftest "every year on some date" 61 | (def task (task/build-scheduled-task 42 "Review logs" "every year on 01-27")) 62 | (test (scheduled-for? task (d/date 2022 1 27)) true) 63 | (test (scheduled-for? task (d/date 2023 1 27)) true) 64 | (test (scheduled-for? task (d/date 2024 1 27)) true) 65 | (test (scheduled-for? task (d/date 2022 1 26)) false) 66 | (test (scheduled-for? task (d/date 2022 1 28)) false) 67 | (test (scheduled-for? task (d/date 2022 2 1)) false)) 68 | 69 | (deftest "on some date" 70 | (def task (task/build-scheduled-task 42 "Review logs" "on 2022-01-27")) 71 | (test (scheduled-for? task (d/date 2022 1 27)) true) 72 | (test (scheduled-for? task (d/date 2022 1 26)) false) 73 | (test (scheduled-for? task (d/date 2022 1 28)) false) 74 | (test (scheduled-for? task (d/date 2022 2 1)) false) 75 | (test (scheduled-for? task (d/date 2023 1 27)) false)) 76 | 77 | (deftest "every last day" 78 | (def task (task/build-scheduled-task 42 "Review logs" "every last day")) 79 | (test (scheduled-for? task (d/date 2022 1 31)) true) 80 | (test (scheduled-for? task (d/date 2022 2 28)) true) 81 | (test (scheduled-for? task (d/date 2022 3 31)) true) 82 | (test (scheduled-for? task (d/date 2022 4 30)) true) 83 | (test (scheduled-for? task (d/date 2022 5 31)) true) 84 | (test (scheduled-for? task (d/date 2022 6 30)) true) 85 | (test (scheduled-for? task (d/date 2022 7 31)) true) 86 | (test (scheduled-for? task (d/date 2022 8 31)) true) 87 | (test (scheduled-for? task (d/date 2022 9 30)) true) 88 | (test (scheduled-for? task (d/date 2022 10 31)) true) 89 | (test (scheduled-for? task (d/date 2022 11 30)) true) 90 | (test (scheduled-for? task (d/date 2022 12 31)) true) 91 | (test (scheduled-for? task (d/date 2023 1 31)) true) 92 | (test (scheduled-for? task (d/date 2022 1 30)) false)) 93 | 94 | (deftest "every last weekday" 95 | (def task (task/build-scheduled-task 42 "Review logs" "every last weekday")) 96 | (test (scheduled-for? task (d/date 2022 1 31)) true) 97 | (test (scheduled-for? task (d/date 2022 2 28)) true) 98 | (test (scheduled-for? task (d/date 2022 3 31)) true) 99 | (test (scheduled-for? task (d/date 2022 4 29)) true) 100 | (test (scheduled-for? task (d/date 2022 5 31)) true) 101 | (test (scheduled-for? task (d/date 2022 6 30)) true) 102 | (test (scheduled-for? task (d/date 2022 7 29)) true) 103 | (test (scheduled-for? task (d/date 2022 8 31)) true) 104 | (test (scheduled-for? task (d/date 2022 9 30)) true) 105 | (test (scheduled-for? task (d/date 2022 10 31)) true) 106 | (test (scheduled-for? task (d/date 2022 11 30)) true) 107 | (test (scheduled-for? task (d/date 2022 12 30)) true) 108 | (test (scheduled-for? task (d/date 2022 1 30)) false) 109 | (test (scheduled-for? task (d/date 2025 4 25)) false)) 110 | 111 | (deftest "every last Friday" 112 | (def task (task/build-scheduled-task 42 "Review logs" "every last Friday")) 113 | (test (scheduled-for? task (d/date 2022 1 28)) true) 114 | (test (scheduled-for? task (d/date 2022 2 25)) true) 115 | (test (scheduled-for? task (d/date 2022 3 25)) true) 116 | (test (scheduled-for? task (d/date 2022 4 29)) true) 117 | (test (scheduled-for? task (d/date 2022 5 27)) true) 118 | (test (scheduled-for? task (d/date 2022 1 31)) false)) 119 | 120 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 121 | ## Test missed? 122 | 123 | (def scheduled-task (task/build-scheduled-task 42 "Weekly meeting" "on 2022-08-01")) 124 | 125 | (deftest "returns true when the task is missed" 126 | (def plan (plan/build-plan 127 | :days @[(day/build-day (d/date 2022 8 2)) 128 | (day/build-day (d/date 2022 8 1))])) 129 | (test (missed? plan scheduled-task (d/date 2022 8 3)) true)) 130 | 131 | (deftest "returns true when the task is missed and the day doesn't exist" 132 | (def plan (plan/build-plan 133 | :days @[(day/build-day (d/date 2022 8 2)) 134 | (day/build-day (d/date 2022 7 15))])) 135 | (test (missed? plan scheduled-task (d/date 2022 8 3)) true)) 136 | 137 | (deftest "returns false when the task is missed, but it's older than 30 days" 138 | (def plan (plan/build-plan 139 | :days @[(day/build-day (d/date 2022 9 11)) 140 | (day/build-day (d/date 2022 8 1))])) 141 | (test (not (missed? plan scheduled-task (d/date 2022 9 11))) true)) 142 | 143 | (deftest "returns false when the task is scheduled for another day" 144 | (def plan (plan/build-plan 145 | :days @[(day/build-day (d/date 2022 8 2) 146 | @[] 147 | @[(task/build-task "Weekly meeting" true)]) 148 | (day/build-day (d/date 2022 8 1))])) 149 | (test (not (missed? plan scheduled-task (d/date 2022 8 3))) true)) 150 | 151 | (deftest "returns false when the task will be scheduled in future" 152 | (def plan (plan/build-plan 153 | :days @[(day/build-day (d/date 2022 8 2)) 154 | (day/build-day (d/date 2022 7 31))])) 155 | (test (not (missed? plan scheduled-task (d/date 2022 7 31))) true)) 156 | 157 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 158 | ## Test schedule-tasks 159 | 160 | (def scheduled-tasks 161 | @[(task/build-scheduled-task 42 "Weekly meeting" "every Monday") 162 | (task/build-scheduled-task 42 "Check logs" "every Wednesday")]) 163 | 164 | (deftest "schedules tasks scheduled on specific weekdays" 165 | (def plan (plan/build-plan 166 | :days @[(day/build-day (d/date 2022 1 18)) 167 | (day/build-day (d/date 2022 1 17))])) 168 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 17)) 169 | (test (empty? (((plan :days) 0) :tasks)) true) 170 | (let [day ((plan :days) 1) 171 | task ((day :tasks) 0)] 172 | (test (task :title) "Weekly meeting") 173 | (test (task :done) false) 174 | (test (task :schedule) "every Monday"))) 175 | 176 | (deftest "doesn't insert duplicate tasks" 177 | (def plan (plan/build-plan 178 | :days @[(day/build-day (d/date 2022 1 18)) 179 | (day/build-day (d/date 2022 1 17))])) 180 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 17)) 181 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 17)) 182 | (test (empty? (((plan :days) 0) :tasks)) true) 183 | (test (length (((plan :days) 1) :tasks)) 1)) 184 | 185 | (deftest "schedules missed tasks" 186 | (def plan (plan/build-plan 187 | :days @[(day/build-day (d/date 2022 1 18)) 188 | (day/build-day (d/date 2022 1 17))])) 189 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 18)) 190 | (test (empty? (((plan :days) 1) :tasks)) true) 191 | (let [day ((plan :days) 0)] 192 | (test (not (empty? (day :tasks))) true) 193 | (if (not (empty? (day :tasks))) 194 | (let [task ((day :tasks) 0)] 195 | (test (task :title) "Weekly meeting") 196 | (test (d/equal? (d/date 2022 1 17) (task :missed-on)) true) 197 | (test (task :done) false) 198 | (test (task :schedule) "every Monday"))))) 199 | 200 | (deftest "schedules missed monthly tasks" 201 | (def scheduled-tasks 202 | @[(task/build-scheduled-task 42 "Review logs" "every month")]) 203 | (def day-1 (day/build-day (d/date 2022 7 5))) 204 | (def day-2 (day/build-day (d/date 2022 6 15))) 205 | (def plan (plan/build-plan :days @[day-1 day-2])) 206 | (schedule-tasks plan scheduled-tasks (d/date 2022 7 5)) 207 | (test (not (empty? (day-1 :tasks))) true) 208 | (if (not (empty? (day-1 :tasks))) 209 | (test (((day-1 :tasks) 0) :title) "Review logs"))) 210 | 211 | (deftest "doesn't schedule tasks that are not yet scheduled for future" 212 | (def plan (plan/build-plan 213 | :days @[(day/build-day (d/date 2022 1 19)) 214 | (day/build-day (d/date 2022 1 18)) 215 | (day/build-day (d/date 2022 1 17))])) 216 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 18)) 217 | (test (length (((plan :days) 0) :tasks)) 1) 218 | (test (length (((plan :days) 1) :tasks)) 1) 219 | (test (empty? (((plan :days) 2) :tasks)) true) 220 | (let [day ((plan :days) 1)] 221 | (if (not (empty? (day :tasks))) 222 | (let [task ((day :tasks) 0)] 223 | (test (task :title) "Weekly meeting") 224 | (test (task :done) false) 225 | (test (task :schedule) "every Monday"))))) 226 | 227 | (deftest "doesn't schedule tasks that are not scheduled for future day that is not in the plan" 228 | (def plan (plan/build-plan 229 | :days @[(day/build-day (d/date 2022 1 19)) 230 | (day/build-day (d/date 2022 1 18)) 231 | (day/build-day (d/date 2022 1 16))])) 232 | (schedule-tasks plan scheduled-tasks (d/date 2022 1 16)) 233 | (test (length (((plan :days) 0) :tasks)) 1) 234 | (test (empty? (((plan :days) 1) :tasks)) true) 235 | (test (empty? (((plan :days) 2) :tasks)) true)) 236 | 237 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 238 | ## Test build-command 239 | 240 | (deftest "doesn't build the command when arguments are not matching" 241 | (def arguments {"stats" true}) 242 | (test (empty? (build-command arguments)) true)) 243 | 244 | (deftest "builds the command when arguments are matching" 245 | (def arguments {"schedule-tasks" "test/examples/schedule.md"}) 246 | (def result (build-command arguments)) 247 | (test (tuple? (result :command)) true)) 248 | 249 | (deftest "returns an error when the schedule doesn't exist" 250 | (def arguments {"schedule-tasks" "test/examples/missing-schedule.md"}) 251 | (def result (build-command arguments)) 252 | (test (nil? (result :command)) true) 253 | (test (first (result :errors)) "--schedule-tasks file does not exist")) 254 | 255 | (deftest "returns an error when the schedule cannot be parsed" 256 | (def arguments {"schedule-tasks" "test/examples/unparsable-schedule.md"}) 257 | (def result (build-command arguments)) 258 | (test (nil? (result :command)) true) 259 | (test (first (result :errors)) "--schedule-tasks schedule can not be parsed")) 260 | 261 | (deftest "returns an error when the schedule is empty" 262 | (def arguments {"schedule-tasks" "test/examples/empty-schedule.md"}) 263 | (def result (build-command arguments)) 264 | (test (nil? (result :command)) true) 265 | (test (first (result :errors)) "--schedule-tasks schedule is empty")) 266 | -------------------------------------------------------------------------------- /test/commands/stats_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/commands/stats :prefix "") 4 | 5 | ## ———————————————————————————————————————————————————————————————————————————————————————————————— 6 | ## Test build-command 7 | 8 | (deftest "doesn't build the command when arguments are not matching" 9 | (def arguments {"report" true}) 10 | (test (empty? (build-command arguments)) true)) 11 | 12 | (deftest "builds the command when arguments are matching" 13 | (def arguments {"stats" true}) 14 | (def result (build-command arguments)) 15 | (test (tuple? (result :command)) true)) 16 | -------------------------------------------------------------------------------- /test/commands_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/date :as d) 4 | (import ../src/day) 5 | (import ../src/plan) 6 | (import ../src/task) 7 | 8 | (import ../src/commands/insert_days :prefix "") 9 | (import ../src/commands/remove_empty_days :prefix "") 10 | 11 | (import ../src/commands :prefix "") 12 | 13 | 14 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 15 | ## Setup 16 | 17 | (def file-path "test/examples/todo.md") 18 | 19 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 20 | ## Test build-commands 21 | 22 | (deftest "when there are no errors" 23 | (def arguments {"skip-backup" true 24 | "report" "7" 25 | "stats" true 26 | "list-contacts" "test/examples/contacts"}) 27 | (def commands (build-commands arguments file-path)) 28 | (test (length commands) 3) 29 | (loop [command :in commands] 30 | (test (not (nil? (command :command))) true) 31 | (test (nil? (command :errors)) true))) 32 | 33 | (deftest "when there are errors" 34 | (def arguments {"skip-backup" true "report" "seven"}) 35 | (def commands (build-commands arguments file-path)) 36 | (test (length commands) 1) 37 | (test (not (nil? ((first commands) :errors))) true)) 38 | 39 | (deftest"when not skipping backup" 40 | (def arguments {"report" "7"}) 41 | (def commands (build-commands arguments file-path)) 42 | (test (length commands) 2) 43 | (test (not (nil? ((commands 0) :command))) true) 44 | (test (not (nil? ((commands 1) :command))) true)) 45 | 46 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 47 | ## Test run-commands 48 | 49 | (deftest "with valid arguments" 50 | (def today (d/today)) 51 | (def plan (plan/build-plan 52 | :days @[(day/build-day today) 53 | (day/build-day (d/date 2020 8 4)) 54 | (day/build-day (d/date 2020 8 2) @[] 55 | @[(task/build-task "Buy milk" true)])])) 56 | (def arguments {"skip-backup" true "remove-empty-days" true "insert-days" "3"}) 57 | (def {:plan new-plan :errors errors} (run-commands plan file-path arguments)) 58 | (def days (new-plan :days)) 59 | (test (empty? errors) true) 60 | (test (length days) 4) 61 | (test (= (d/+days today 2) ((days 0) :date)) true) 62 | (test (= (d/+days today 1) ((days 1) :date)) true) 63 | (test (= today ((days 2) :date)) true) 64 | (test (= (d/date 2020 8 2) ((days 3) :date)) true)) 65 | 66 | (deftest "with invalid arguments" 67 | (def today (d/today)) 68 | (def plan (plan/build-plan 69 | :days @[(day/build-day today) 70 | (day/build-day (d/date 2020 8 4)) 71 | (day/build-day (d/date 2020 8 2) @[] 72 | @[(task/build-task "Buy milk" true)])])) 73 | (def arguments {"skip-backup" true "remove-empty-days" true "insert-days" "three"}) 74 | (def {:plan new-plan :errors errors} (run-commands plan file-path arguments)) 75 | (def days (new-plan :days)) 76 | (test (length days) 3) 77 | (test (= today ((days 0) :date)) true) 78 | (test (= (d/date 2020 8 4) ((days 1) :date)) true) 79 | (test (= (d/date 2020 8 2) ((days 2) :date)) true) 80 | (test (= (first errors) "--insert-days argument is not a number.") true)) 81 | -------------------------------------------------------------------------------- /test/contact/parser_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/contact/parser :prefix "") 4 | (import ../../src/date :as d) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Test parse 8 | 9 | (deftest "parses contacts" 10 | (def contact-string 11 | ``` 12 | # John Doe 13 | 14 | - Category: A 15 | - Birthday: 04-23 16 | 17 | ## 2022-02-15 18 | 19 | Talked over the phone about stuff. 20 | 21 | ## 2022-01-28 22 | 23 | Grabbed a beer and talked about stuff. 24 | ```) 25 | (def contact ((parse contact-string) :contact)) 26 | (test (contact :name) "John Doe") 27 | (test (contact :category) :a) 28 | (test (contact :birthday) "04-23") 29 | (test (= (d/date 2022 2 15) (contact :last-contact)) true)) 30 | 31 | (deftest "parses contacts without details" 32 | (def contact-string 33 | ``` 34 | # John Doe 35 | 36 | ## 2022-02-19 37 | 38 | Talked over the phone about stuff. 39 | ```) 40 | (def contact ((parse contact-string) :contact)) 41 | (test (contact :name) "John Doe") 42 | (test (= (d/date 2022 2 19) (contact :last-contact)) true)) 43 | 44 | (deftest "parses contacts with extra details" 45 | (def contact-string 46 | ``` 47 | # John Doe 48 | 49 | - Spouse: Jane Doe 50 | 51 | ## 2022-02-19 52 | 53 | Talked over the phone about stuff. 54 | ```) 55 | (def contact ((parse contact-string) :contact)) 56 | (test (contact :name) "John Doe") 57 | (test (= (d/date 2022 2 19) (contact :last-contact)) true)) 58 | 59 | (deftest "parses contacts with non ASCII characters in the name" 60 | (def contact-string 61 | ``` 62 | # Petar Petrović 63 | 64 | ## 2022-02-19 65 | 66 | Talked over the phone about stuff. 67 | ```) 68 | (def contact ((parse contact-string) :contact)) 69 | (test (contact :name) "Petar Petrović")) 70 | 71 | (deftest "parses contacts with non ASCII characters in extra details" 72 | (def contact-string 73 | ``` 74 | # John Doe 75 | 76 | - Spouse: Jane Doić 77 | 78 | ## 2022-02-19 79 | 80 | Talked over the phone about stuff. 81 | ```) 82 | (def contact ((parse contact-string) :contact)) 83 | (test (contact :name) "John Doe") 84 | (test (= (d/date 2022 2 19) (contact :last-contact)) true)) 85 | 86 | (deftest "parses contacts with ID in the name line" 87 | (def contact-string 88 | ``` 89 | # 202201081224 John Doe 90 | 91 | ## 2022-02-19 92 | 93 | Talked over the phone about stuff. 94 | ```) 95 | (def contact ((parse contact-string) :contact)) 96 | (test (contact :name) "John Doe") 97 | (test (= (d/date 2022 2 19) (contact :last-contact)) true)) 98 | -------------------------------------------------------------------------------- /test/contact/repository_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/contact/repository :prefix "") 4 | 5 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 6 | ## Test load-contacts 7 | 8 | (deftest "loads contacts from a directory" 9 | (def path (os/realpath "test/examples/contacts")) 10 | (def contacts ((load-contacts path) :contacts)) 11 | (def names (sort (map (fn [contact] (contact :name)) contacts))) 12 | (test (length contacts) 2) 13 | (test (names 0) "Jane Doe") 14 | (test (names 1) "John Doe")) 15 | 16 | (deftest "returns an error when the directory doesn't exist" 17 | (def result (load-contacts "test/missing-directory")) 18 | (test (first (result :errors)) "Directory does not exist")) 19 | -------------------------------------------------------------------------------- /test/contact_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/contact :prefix "") 4 | (import ../src/date :as d) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Test build-contact 8 | 9 | (deftest "builds contact with a name" 10 | (def contact (build-contact "John Doe")) 11 | (test (contact :name) "John Doe") 12 | (test (nil? (contact :category)) true) 13 | (test (nil? (contact :birthday)) true) 14 | (test (nil? (contact :last-contact)) true)) 15 | 16 | (deftest "builds contact with a category" 17 | (def contact (build-contact "John Doe" :category :b)) 18 | (test (contact :name) "John Doe") 19 | (test (contact :category) :b) 20 | (test (nil? (contact :birthday)) true) 21 | (test (nil? (contact :last-contact)) true)) 22 | 23 | (deftest "builds contact with a string as category" 24 | (def contact (build-contact "John Doe" :category "B")) 25 | (test (contact :category) :b)) 26 | 27 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 28 | ## Test next-contact-date 29 | 30 | (deftest "calculates the next contact date when a category is :a" 31 | (def contact (build-contact "John Doe" :last-contact (d/date 2022 4 1) :category :a)) 32 | (test (d/equal? (d/date 2022 4 21) (next-contact-date contact)) true)) 33 | 34 | (deftest "calculates the next contact date when a category is :b" 35 | (def contact (build-contact "John Doe" :last-contact (d/date 2022 4 1) :category :b)) 36 | (test (d/equal? (d/date 2022 5 31) (next-contact-date contact)) true)) 37 | 38 | (deftest "returns nil when there's no category" 39 | (def contact (build-contact "John Doe" :last-contact (d/date 2022 4 1))) 40 | (test (nil? (next-contact-date contact)) true)) 41 | 42 | (deftest "returns nil when the last contact is blank" 43 | (def contact (build-contact "John Doe" :category :b)) 44 | (test (nil? (next-contact-date contact)) true)) 45 | 46 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 47 | ## Test contact-on-date? 48 | 49 | (deftest "returns boolean that indicates if the contact should be contacted on the date" 50 | (def contact (build-contact "John Doe" :last-contact (d/date 2022 4 1) :category :a)) 51 | (test (contact-on-date? contact (d/date 2022 4 21)) true) 52 | (test (contact-on-date? contact (d/date 2022 5 1)) true) 53 | (test (not (contact-on-date? contact (d/date 2022 4 20))) true) 54 | (test (not (contact-on-date? contact (d/date 2022 4 2))) true)) 55 | 56 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 57 | ## Test birthday? 58 | 59 | (deftest "returns boolean that indicates if the date is the contacts birthday" 60 | (def contact (build-contact "John Doe" :birthday "04-01")) 61 | (test (birthday? contact (d/date 2022 4 1)) true) 62 | (test (not (birthday? contact (d/date 2022 4 20))) true)) 63 | -------------------------------------------------------------------------------- /test/date_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/date :as d) 4 | 5 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 6 | ## Test today 7 | 8 | (deftest "returns the correct struct" 9 | (let [today (d/today)] 10 | (test (not (nil? (today :year))) true) 11 | (test (not (nil? (today :month))) true) 12 | (test (not (nil? (today :day))) true) 13 | (test (not (nil? (today :week-day))) true))) 14 | 15 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 16 | ## Test date 17 | 18 | (deftest "returns the correct struct" 19 | (test (d/date 2020 8 31) 20 | {:year 2020 :month 8 :day 31 :week-day "Monday"})) 21 | 22 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 23 | ## Test parse 24 | 25 | (deftest "parses a string to a date" 26 | (test (d/parse "2020-08-31") 27 | {:year 2020 :month 8 :day 31 :week-day "Monday"})) 28 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 29 | ## Test format 30 | 31 | (deftest "returns a correctly formatted string for the date" 32 | (test (d/format {:year 2021 :month 8 :day 9 :week-day "Monday"}) 33 | "2021-08-09, Monday")) 34 | 35 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 36 | ## Test before? 37 | 38 | (deftest "returns true when the first date is before the second date" 39 | (test (d/before? (d/date 2021 7 1) (d/date 2021 7 2)) true) 40 | (test (d/before? (d/date 2021 7 1) (d/date 2021 7 10)) true) 41 | (test (not (d/before? (d/date 2021 7 1) (d/date 2021 6 15))) true) 42 | (test (not (d/before? (d/date 2021 7 1) (d/date 2021 7 1))) true)) 43 | 44 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 45 | ## Test before-or-eq? 46 | 47 | (deftest "returns true when the first date is before or equal to the second date" 48 | (test (d/before-or-eq? (d/date 2021 7 1) (d/date 2021 7 2)) true) 49 | (test (d/before-or-eq? (d/date 2021 7 1) (d/date 2021 7 10)) true) 50 | (test (d/before-or-eq? (d/date 2021 7 1) (d/date 2021 7 1)) true) 51 | (test (not (d/before-or-eq? (d/date 2021 7 1) (d/date 2021 6 15))) true)) 52 | 53 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 54 | ## Test after? 55 | 56 | (deftest "returns true when the first date is after the second date" 57 | (test (d/after? (d/date 2021 7 3) (d/date 2021 7 2)) true) 58 | (test (d/after? (d/date 2021 7 3) (d/date 2021 6 10)) true) 59 | (test (not (d/after? (d/date 2021 7 1) (d/date 2021 7 15))) true) 60 | (test (not (d/after? (d/date 2021 7 1) (d/date 2021 7 1))) true)) 61 | 62 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 63 | ## Test after-or-eq? 64 | 65 | (deftest "returns true when the first date is after or equal to the second date" 66 | (test (d/after-or-eq? (d/date 2021 7 3) (d/date 2021 7 2)) true) 67 | (test (d/after-or-eq? (d/date 2021 7 3) (d/date 2021 6 10)) true) 68 | (test (d/after-or-eq? (d/date 2021 7 1) (d/date 2021 7 1)) true) 69 | (test (not (d/after-or-eq? (d/date 2021 7 1) (d/date 2021 7 15))) true)) 70 | 71 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 72 | ## Test weekday? 73 | 74 | (deftest "returns true if the date is a weekday" 75 | (test (d/weekday? (d/date 2022 1 3)) true) 76 | (test (d/weekday? (d/date 2022 1 4)) true) 77 | (test (d/weekday? (d/date 2022 1 5)) true) 78 | (test (d/weekday? (d/date 2022 1 6)) true) 79 | (test (d/weekday? (d/date 2022 1 7)) true) 80 | (test (d/weekday? (d/date 2022 1 8)) false) 81 | (test (d/weekday? (d/date 2022 1 9)) false)) 82 | 83 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 84 | ## Test last-day-of-month? 85 | 86 | (deftest "returns true when the date is the last date of the month" 87 | (test (d/last-day-of-month? (d/date 2022 1 31)) true) 88 | (test (d/last-day-of-month? (d/date 2022 2 28)) true) 89 | (test (d/last-day-of-month? (d/date 2022 3 31)) true) 90 | (test (d/last-day-of-month? (d/date 2022 4 30)) true) 91 | (test (d/last-day-of-month? (d/date 2022 5 31)) true) 92 | (test (d/last-day-of-month? (d/date 2022 6 30)) true) 93 | (test (d/last-day-of-month? (d/date 2022 7 31)) true) 94 | (test (d/last-day-of-month? (d/date 2022 8 31)) true) 95 | (test (d/last-day-of-month? (d/date 2022 9 30)) true) 96 | (test (d/last-day-of-month? (d/date 2022 10 31)) true) 97 | (test (d/last-day-of-month? (d/date 2022 11 30)) true) 98 | (test (d/last-day-of-month? (d/date 2022 12 31)) true) 99 | (test (d/last-day-of-month? (d/date 2023 1 31)) true) 100 | (test (not (d/last-day-of-month? (d/date 2022 1 30))) true)) 101 | 102 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 103 | ## Test last-weekday-of-month? 104 | 105 | (deftest "returns true when the date is the last week day of the month" 106 | (test (d/last-weekday-of-month? (d/date 2022 1 31)) true) 107 | (test (d/last-weekday-of-month? (d/date 2022 2 28)) true) 108 | (test (d/last-weekday-of-month? (d/date 2022 3 31)) true) 109 | (test (d/last-weekday-of-month? (d/date 2022 4 29)) true) 110 | (test (d/last-weekday-of-month? (d/date 2022 5 31)) true) 111 | (test (d/last-weekday-of-month? (d/date 2022 6 30)) true) 112 | (test (d/last-weekday-of-month? (d/date 2022 7 29)) true) 113 | (test (d/last-weekday-of-month? (d/date 2022 8 31)) true) 114 | (test (d/last-weekday-of-month? (d/date 2022 9 30)) true) 115 | (test (d/last-weekday-of-month? (d/date 2022 10 31)) true) 116 | (test (d/last-weekday-of-month? (d/date 2022 11 30)) true) 117 | (test (d/last-weekday-of-month? (d/date 2022 12 30)) true) 118 | (test (not (d/last-weekday-of-month? (d/date 2025 4 25))) true) 119 | (test (not (d/last-weekday-of-month? (d/date 2022 1 30))) true)) 120 | 121 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 122 | ## Test last-friday-of-month? 123 | 124 | (deftest "returns true when the date is the last friday of the month" 125 | (test (d/last-friday-of-month? (d/date 2022 1 28)) true) 126 | (test (d/last-friday-of-month? (d/date 2022 2 25)) true) 127 | (test (d/last-friday-of-month? (d/date 2022 3 25)) true) 128 | (test (d/last-friday-of-month? (d/date 2022 4 29)) true) 129 | (test (d/last-friday-of-month? (d/date 2022 5 27)) true) 130 | (test (not (d/last-friday-of-month? (d/date 2022 1 31))) true) 131 | (test (not (d/last-friday-of-month? (d/date 2022 1 21))) true) 132 | (test (not (d/last-friday-of-month? (d/date 2022 2 11))) true)) 133 | 134 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 135 | ## Test +days 136 | 137 | (deftest "adds the number of days to the date" 138 | (test (= (d/date 2020 8 6) (d/+days (d/date 2020 8 5) 1)) true) 139 | (test (= (d/date 2020 8 7) (d/+days (d/date 2020 8 5) 2)) true) 140 | (test (= (d/date 2020 8 8) (d/+days (d/date 2020 8 5) 3)) true)) 141 | 142 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 143 | ## Test -days 144 | 145 | (deftest "substract the number of days from the date" 146 | (test (= (d/date 2020 8 4) (d/-days (d/date 2020 8 5) 1)) true) 147 | (test (= (d/date 2020 8 3) (d/-days (d/date 2020 8 5) 2)) true) 148 | (test (= (d/date 2020 8 2) (d/-days (d/date 2020 8 5) 3)) true)) 149 | 150 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 151 | ## Test days-from-now 152 | 153 | (deftest "adds the number of days to today" 154 | (test (= (d/days-from-now 1) (d/+days (d/today) 1)) true) 155 | (test (= (d/days-from-now 2) (d/+days (d/today) 2)) true)) 156 | -------------------------------------------------------------------------------- /test/day_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/date :as d) 4 | (import ../src/event) 5 | (import ../src/task) 6 | (import ../src/day :prefix "") 7 | 8 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 9 | ## Test generate-days 10 | 11 | (deftest "generates days from the first date to the second date" 12 | (def days (generate-days (d/date 2020 8 2) (d/date 2020 8 5))) 13 | (test (length days) 4) 14 | (test (= (d/date 2020 8 5) ((days 0) :date)) true) 15 | (test (= (d/date 2020 8 4) ((days 1) :date)) true) 16 | (test (= (d/date 2020 8 3) ((days 2) :date)) true) 17 | (test (= (d/date 2020 8 2) ((days 3) :date)) true)) 18 | 19 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 20 | ## Test empty-day? 21 | 22 | (deftest "checks if the day doesn't have any tasks" 23 | (test (empty-day? (build-day (d/date 2020 8 1))) true) 24 | (test (not (empty-day? (build-day (d/date 2020 8 1) 25 | @[(event/build-event "Visited museum")] 26 | @[]))) 27 | true) 28 | (test (not (empty-day? (build-day (d/date 2020 8 1) 29 | @[] 30 | @[(task/build-task "Buy milk" true)]))) 31 | true)) 32 | 33 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 34 | ## Test add-tasks 35 | 36 | (deftest "adds tasks to the day" 37 | (def day (build-day (d/date 2020 8 1))) 38 | (add-tasks day @[(task/build-task "Buy milk" true) 39 | (task/build-task "Fix the lamp" false)]) 40 | (test (length (day :tasks)) 2) 41 | (test (((day :tasks) 0) :title) "Buy milk") 42 | (test (((day :tasks) 1) :title) "Fix the lamp")) 43 | 44 | (deftest "doesn't add duplicate tasks" 45 | (def day (build-day (d/date 2020 8 1) 46 | @[] 47 | @[(task/build-task "Fix the lamp" false)])) 48 | (add-tasks day @[(task/build-task "Buy milk" true) 49 | (task/build-task "Fix the lamp" false)]) 50 | (test (length (day :tasks)) 2) 51 | (test (((day :tasks) 0) :title) "Fix the lamp") 52 | (test (((day :tasks) 1) :title) "Buy milk")) 53 | -------------------------------------------------------------------------------- /test/examples/contacts/jane-doe.md: -------------------------------------------------------------------------------- 1 | # Jane Doe 2 | 3 | - Type: Contact 4 | - Category: A 5 | - Birthday: 07-01 6 | 7 | ## 2022-03-01 8 | 9 | Met in park. 10 | -------------------------------------------------------------------------------- /test/examples/contacts/john-doe.md: -------------------------------------------------------------------------------- 1 | # John Doe 2 | 3 | - Category: A 4 | - Birthday: 04-27 5 | 6 | ## 2022-02-15 7 | 8 | Talked over the phone about stuff. 9 | 10 | ## 2022-01-28 11 | 12 | Grabbed a beer and talked about stuff. 13 | -------------------------------------------------------------------------------- /test/examples/empty-schedule.md: -------------------------------------------------------------------------------- 1 | # Scheduled Tasks 2 | 3 | -------------------------------------------------------------------------------- /test/examples/plan-2020-08-02.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackberrydev/alas/42673df763dc7c2e2ecbfaf9ca52a330f25aa571/test/examples/plan-2020-08-02.md -------------------------------------------------------------------------------- /test/examples/plan-2020-08-03-1.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackberrydev/alas/42673df763dc7c2e2ecbfaf9ca52a330f25aa571/test/examples/plan-2020-08-03-1.md -------------------------------------------------------------------------------- /test/examples/plan-2020-08-03.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackberrydev/alas/42673df763dc7c2e2ecbfaf9ca52a330f25aa571/test/examples/plan-2020-08-03.md -------------------------------------------------------------------------------- /test/examples/schedule-without-date.md: -------------------------------------------------------------------------------- 1 | # Scheduled Tasks 2 | 3 | - Weekly Meeting (every Tuesday) 4 | - Deploy the web app (every weekday) 5 | - Pay football practice 6 | - Martha's birthsday (every 05-24) 7 | - Meeting with Jack (on 2022-05-03) 8 | -------------------------------------------------------------------------------- /test/examples/schedule.md: -------------------------------------------------------------------------------- 1 | # Scheduled Tasks 2 | 3 | - Weekly Meeting (every Tuesday) 4 | - Puzzle Storm on Lichess (every day) 5 | - Deploy the web app (every weekday) 6 | - Pay football practice (every month) 7 | - Martha's birthsday (every 05-24) 8 | - Meeting with Jack (on 2022-05-03) 9 | -------------------------------------------------------------------------------- /test/examples/todo.md: -------------------------------------------------------------------------------- 1 | # Main TODO 2 | 3 | ## Inbox 4 | 5 | ## 2020-08-01, Saturday 6 | 7 | - [ ] Develop photos for the grandmother 8 | - [X] Pay bills 9 | 10 | ## 2020-07-31, Friday 11 | 12 | - [X] Review open pull requests 13 | - [X] Fix flaky test 14 | -------------------------------------------------------------------------------- /test/examples/unparsable-schedule.md: -------------------------------------------------------------------------------- 1 | ## Scheduled Tasks 2 | 3 | This is my schedule. 4 | 5 | * Weekly Meeting (every Tuesday) 6 | * Puzzle Storm on Lichess (every day) 7 | * Deploy the web app (every weekday) 8 | * Pay football practice (every month) 9 | * Martha's birthsday (every 05-24) 10 | * Meeting with Jack (on 2022-05-03) 11 | -------------------------------------------------------------------------------- /test/examples/unparsable-todo.md: -------------------------------------------------------------------------------- 1 | # Main TODO 2 | 3 | # 2020-08-01, Saturday 4 | 5 | - [ ] Develop photos for the grandmother 6 | - [X] Pay bills 7 | 8 | # 2020-07-31, Friday 9 | 10 | - [X] Review open pull requests 11 | - [X] Fix flaky test 12 | -------------------------------------------------------------------------------- /test/file_repository_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/file_repository :prefix "") 4 | 5 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 6 | ## Test save 7 | 8 | (deftest "saves plan to a file" 9 | (def new-plan-path "test/examples/new_plan.md") 10 | (def plan 11 | 12 | ``` 13 | # Main TODO 14 | 15 | ## Inbox 16 | 17 | - [ ] Fix the lamp 18 | 19 | ## 2020-08-03, Monday 20 | 21 | ## 2020-08-02, Sunday 22 | 23 | ## 2020-08-01, Saturday 24 | 25 | - [ ] Develop photos 26 | - [x] Pay bills 27 | 28 | ## 2020-07-31, Friday 29 | 30 | - [x] Review open pull requests 31 | - [x] Fix the flaky test 32 | ```) 33 | (save plan new-plan-path) 34 | (test (= ((load new-plan-path) :text) plan) true) 35 | (os/rm new-plan-path)) 36 | 37 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 38 | ## Test load 39 | 40 | (deftest "loads a plan from a file to a string" 41 | (let [result (load "test/examples/todo.md") 42 | text (result :text)] 43 | (test (length (string/split "\n" text)) 14))) 44 | 45 | (deftest "returns an error when the file doesn't exist" 46 | (def result (load "missing_file.md")) 47 | (test (first (result :errors)) "File does not exist")) 48 | -------------------------------------------------------------------------------- /test/plan/parser_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/plan/parser :prefix "") 4 | (import ../../src/date :as d) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Test parse 8 | 9 | (deftest "parses a plan string" 10 | (def plan-string 11 | ``` 12 | # Main TODO 13 | 14 | ## Inbox 15 | 16 | - [ ] #home - Fix the lamp 17 | - [ ] Update Rust 18 | 19 | ## 2020-08-01, Saturday 20 | 21 | - [ ] Develop photos 22 | - [x] Pay bills 23 | - [ ] Fix the lamp (missed on 2020-07-30) 24 | 25 | ## 2020-07-31, Friday 26 | 27 | - Talked to Mike & Molly 28 | - [x] #work - Review open pull requests 29 | - [x] #work - Fix the flaky test 30 | ```) 31 | (def parse-result (parse plan-string)) 32 | (def plan (parse-result :plan)) 33 | (def inbox (plan :inbox)) 34 | (def day-1 ((plan :days) 0)) 35 | (def day-2 ((plan :days) 1)) 36 | (test (plan :title) "Main TODO") 37 | (let [task (inbox 0)] 38 | (test (task :title) "#home - Fix the lamp") 39 | (test (not (task :done)) true)) 40 | (let [task (inbox 1)] 41 | (test (task :title) "Update Rust") 42 | (test (not (task :done)) true)) 43 | (test (= (d/date 2020 8 1) (day-1 :date)) true) 44 | (let [task ((day-1 :tasks) 0)] 45 | (test (task :title) "Develop photos") 46 | (test (not (task :done)) true)) 47 | (let [task ((day-1 :tasks) 1)] 48 | (test (task :title) "Pay bills") 49 | (test (task :done) true)) 50 | (let [task ((day-1 :tasks) 2)] 51 | (test (task :title) "Fix the lamp") 52 | (test (not (task :done)) true) 53 | (test (d/equal? (d/date 2020 7 30) (task :missed-on)) true)) 54 | (test (= (d/date 2020 7 31) (day-2 :date)) true) 55 | (let [event ((day-2 :events) 0)] 56 | (test (event :title) "Talked to Mike & Molly") 57 | (test (empty? (event :body)) true)) 58 | (let [task ((day-2 :tasks) 0)] 59 | (test (task :title) "#work - Review open pull requests") 60 | (test (task :done) true)) 61 | (let [task ((day-2 :tasks) 1)] 62 | (test (task :title) "#work - Fix the flaky test") 63 | (test (task :done) true))) 64 | 65 | (deftest "parses a template plan" 66 | (def plan-string 67 | ``` 68 | # Main TODO 69 | 70 | ## Inbox 71 | 72 | ## 2022-05-12, Thursday 73 | ```) 74 | (def plan ((parse plan-string) :plan)) 75 | (test (length (plan :days)) 1) 76 | (test (= (d/date 2022 5 12) (((plan :days) 0) :date)) true)) 77 | 78 | (deftest "parses a plan with one task" 79 | (def plan-string 80 | ``` 81 | # Main TODO 82 | 83 | ## 2020-07-30, Thursday 84 | 85 | - [ ] Pay bills 86 | ```) 87 | (def plan ((parse plan-string) :plan)) 88 | (test (length (plan :days)) 1) 89 | (let [day ((plan :days) 0) 90 | task ((day :tasks) 0)] 91 | (test (= (d/date 2020 7 30) (day :date)) true) 92 | (test (task :title) "Pay bills") 93 | (test (not (task :done)) true))) 94 | 95 | (deftest "parses a plan with one task that has a body" 96 | (def plan-string 97 | ``` 98 | # Main TODO 99 | 100 | ## 2020-07-30, Thursday 101 | 102 | - [ ] Pay bills 103 | - Electricity 104 | - Water 105 | ```) 106 | (def plan ((parse plan-string) :plan)) 107 | (test (length (plan :days)) 1) 108 | (let [day ((plan :days) 0) 109 | task ((day :tasks) 0) 110 | task-body (task :body)] 111 | (test (= (d/date 2020 7 30) (day :date)) true) 112 | (test (task :title) "Pay bills") 113 | (test (not (task :done)) true) 114 | (test (task-body 0) "- Electricity") 115 | (test (task-body 1) "- Water"))) 116 | 117 | (deftest "parses a plan with 2 tasks" 118 | (def plan-string 119 | ``` 120 | # Main TODO 121 | 122 | ## 2020-07-30, Thursday 123 | 124 | - [ ] Pay bills 125 | - [x] Fix the lamp 126 | ```) 127 | (def plan ((parse plan-string) :plan)) 128 | (test (length (plan :days)) 1) 129 | (let [day ((plan :days) 0) 130 | task-1 ((day :tasks) 0) 131 | task-2 ((day :tasks) 1)] 132 | (test (= (d/date 2020 7 30) (day :date)) true) 133 | (test (task-1 :title) "Pay bills") 134 | (test (not (task-1 :done)) true) 135 | (test (task-2 :title) "Fix the lamp") 136 | (test (task-2 :done) true))) 137 | 138 | (deftest "parses a plan without inbox" 139 | (def plan-string 140 | ``` 141 | # Main TODO 142 | 143 | ## 2020-08-01, Saturday 144 | 145 | - [ ] Develop photos 146 | - [x] Pay bills 147 | 148 | ## 2020-07-31, Friday 149 | 150 | - Talked to Mike & Molly 151 | - [x] #work - Review open pull requests 152 | - [x] #work - Fix the flaky test 153 | ```) 154 | (def plan ((parse plan-string) :plan)) 155 | (def day-1 ((plan :days) 0)) 156 | (def day-2 ((plan :days) 1)) 157 | (test (plan :title) "Main TODO") 158 | (test (empty? (plan :inbox)) true) 159 | (test (= (d/date 2020 8 1) (day-1 :date)) true) 160 | (test (= (d/date 2020 7 31) (day-2 :date)) true)) 161 | 162 | (deftest "parses a plan with empty inbox" 163 | (def plan-string 164 | ``` 165 | # Main TODO 166 | 167 | ## Inbox 168 | 169 | ## 2020-08-01, Saturday 170 | 171 | - [ ] Develop photos 172 | - [x] Pay bills 173 | 174 | ## 2020-07-31, Friday 175 | 176 | - Talked to Mike & Molly 177 | - [x] #work - Review open pull requests 178 | - [x] #work - Fix the flaky test 179 | ```) 180 | (def plan ((parse plan-string) :plan)) 181 | (def day-1 ((plan :days) 0)) 182 | (def day-2 ((plan :days) 1)) 183 | (test (plan :title) "Main TODO") 184 | (test (empty? (plan :inbox)) true) 185 | (test (= (d/date 2020 8 1) (day-1 :date)) true) 186 | (test (= (d/date 2020 7 31) (day-2 :date)) true)) 187 | 188 | (deftest "parses a plan with an event that has a body" 189 | (def plan-string 190 | ``` 191 | # Main TODO 192 | 193 | ## 2020-07-30, Thursday 194 | 195 | - Talked to Mike & Molly 196 | - They moved to a new apartment 197 | - [x] Fix the lamp 198 | ```) 199 | (def parse-result (parse plan-string)) 200 | (def plan (parse-result :plan)) 201 | (test (length (plan :days)) 1) 202 | (let [day ((plan :days) 0) 203 | event ((day :events) 0) 204 | task ((day :tasks) 0)] 205 | (test (= (d/date 2020 7 30) (day :date)) true) 206 | (test (event :title) "Talked to Mike & Molly") 207 | (test ((event :body) 0) "- They moved to a new apartment") 208 | (test (task :title) "Fix the lamp") 209 | (test (task :done) true))) 210 | 211 | (deftest "parses a plan with an event without any tasks" 212 | (def plan-string 213 | ``` 214 | # Main TODO 215 | 216 | ## 2020-07-30, Thursday 217 | 218 | - Talked to Mike & Molly 219 | ```) 220 | (def plan ((parse plan-string) :plan)) 221 | (test (length (plan :days)) 1) 222 | (let [day ((plan :days) 0) 223 | event ((day :events) 0)] 224 | (test (event :title) "Talked to Mike & Molly"))) 225 | 226 | (deftest "returns an error when the plan can't be parsed" 227 | (def plan-string 228 | ``` 229 | ## Main TODO 230 | 231 | - [ ] Pay bills 232 | - [O] Talk to Mike 233 | ```) 234 | (def parse-result (parse plan-string)) 235 | (test (parse-result :errors) 236 | ["Plan can not be parsed"])) 237 | 238 | (deftest "returns an error when the plan can be partially parsed" 239 | (def plan-string 240 | ``` 241 | # Main TODO 242 | 243 | ## 2020-01-31, Friday 244 | 245 | ## 2020-01-30, Thursday 246 | 247 | ## Tomorrow 248 | ```) 249 | (def parse-result (parse plan-string)) 250 | (test (parse-result :errors) 251 | ["Plan can not be parsed: last parsed line is line 6"])) 252 | -------------------------------------------------------------------------------- /test/plan/serializer_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../../src/plan/serializer :prefix "") 4 | (import ../../src/date :as d) 5 | (import ../../src/day) 6 | (import ../../src/event) 7 | (import ../../src/plan) 8 | (import ../../src/task) 9 | 10 | (deftest "serializes a plan" 11 | (def plan 12 | (plan/build-plan 13 | :title "Main TODO" 14 | :inbox @[(task/build-task "Fix the lamp" false)] 15 | :days @[(day/build-day (d/date 2020 8 3)) 16 | (day/build-day (d/date 2020 8 2)) 17 | (day/build-day (d/date 2020 8 1) 18 | @[(event/build-event "Talked to Mike" @["- He has a new car"])] 19 | @[(task/build-task "Develop photos" false) 20 | (task/build-task "Pay bills" true @["- Electricity" "- Water"]) 21 | (task/build-missed-task "Organize photos" (d/date 2020 7 20))]) 22 | (day/build-day (d/date 2020 7 31) 23 | @[] 24 | @[(task/build-task "Review open pull requests" true) 25 | (task/build-task "Fix the flaky test" true)])])) 26 | (def plan-string 27 | ``` 28 | # Main TODO 29 | 30 | ## Inbox 31 | 32 | - [ ] Fix the lamp 33 | 34 | ## 2020-08-03, Monday 35 | 36 | ## 2020-08-02, Sunday 37 | 38 | ## 2020-08-01, Saturday 39 | 40 | - Talked to Mike 41 | - He has a new car 42 | - [ ] Develop photos 43 | - [X] Pay bills 44 | - Electricity 45 | - Water 46 | - [ ] Organize photos (missed on 2020-07-20) 47 | 48 | ## 2020-07-31, Friday 49 | 50 | - [X] Review open pull requests 51 | - [X] Fix the flaky test 52 | 53 | ```) 54 | (test (= (serialize plan) plan-string) true)) 55 | 56 | (deftest "serializes a plan with an empty inbox" 57 | (def plan 58 | (plan/build-plan :title "My Plan" 59 | :days @[(day/build-day (d/date 2020 8 1))])) 60 | (def plan-string 61 | ``` 62 | # My Plan 63 | 64 | ## Inbox 65 | 66 | ## 2020-08-01, Saturday 67 | 68 | ```) 69 | (test (= (serialize plan {:serialize-empty-inbox true}) plan-string) true)) 70 | 71 | (deftest "serializes a plan without an inbox" 72 | (def plan 73 | (plan/build-plan :title "My Plan" 74 | :days @[(day/build-day (d/date 2020 8 1))])) 75 | (def plan-string 76 | ``` 77 | # My Plan 78 | 79 | ## 2020-08-01, Saturday 80 | 81 | ```) 82 | (test (= (serialize plan) plan-string) true)) 83 | -------------------------------------------------------------------------------- /test/plan_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/plan) 4 | (import ../src/day) 5 | (import ../src/task) 6 | (import ../src/event) 7 | (import ../src/date :as d) 8 | 9 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 10 | ## Test day-with-date 11 | 12 | (deftest "finds the day with the date in the plan" 13 | (def day-1 (day/build-day (d/date 2020 7 15))) 14 | (def day-2 (day/build-day (d/date 2020 7 16))) 15 | (def plan (plan/build-plan :days @[day-1 day-2])) 16 | (test (= (plan/day-with-date plan (d/date 2020 7 15)) day-1) true)) 17 | 18 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 19 | ## Test has-day-with-date? 20 | 21 | (deftest "checks if the plan has a day with the date" 22 | (def day (day/build-day (d/date 2020 7 31))) 23 | (def plan (plan/build-plan :days @[day])) 24 | (test (= (plan/has-day-with-date? plan (d/date 2020 7 31)) day) true) 25 | (test (not (plan/has-day-with-date? plan (d/date 2020 8 1))) true)) 26 | 27 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 28 | ## Test empty-days 29 | 30 | (deftest "finds days without any tasks in the plan" 31 | (def day-1 (day/build-day (d/date 2020 8 5))) 32 | (def day-2 (day/build-day (d/date 2020 8 4) 33 | @[] 34 | @[(task/build-task "Buy milk" true)])) 35 | (def day-3 (day/build-day (d/date 2020 8 3) 36 | @[(event/build-event "Visited museum")] 37 | @[])) 38 | (def day-4 (day/build-day (d/date 2020 8 2))) 39 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 40 | (def empty-days (plan/empty-days plan)) 41 | (test (= (empty-days 0) day-1) true) 42 | (test (= (empty-days 1) day-4) true)) 43 | 44 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 45 | ## Test sort-days 46 | 47 | (deftest "sorts days by date ascending" 48 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 7 31)) 49 | (day/build-day (d/date 2020 8 1))])) 50 | (def new-plan (plan/sort-days plan)) 51 | (test (= (((new-plan :days) 0) :date) (d/date 2020 8 1)) true) 52 | (test (= (((new-plan :days) 1) :date) (d/date 2020 7 31)) true)) 53 | 54 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 55 | ## Test insert-days 56 | 57 | (deftest "inserts new days in the plan" 58 | (def plan (plan/build-plan)) 59 | (def days @[(day/build-day (d/date 2020 8 1)) 60 | (day/build-day (d/date 2020 7 31))]) 61 | (def new-plan (plan/insert-days plan days)) 62 | (def new-days (new-plan :days)) 63 | (test (length new-days) 2) 64 | (test (= ((new-days 0) :date) (d/date 2020 8 1)) true) 65 | (test (= ((new-days 1) :date) (d/date 2020 7 31)) true)) 66 | 67 | (deftest "doens't insert duplicate days" 68 | (def plan (plan/build-plan :days @[(day/build-day (d/date 2020 7 31))])) 69 | (def days @[(day/build-day (d/date 2020 8 1)) 70 | (day/build-day (d/date 2020 7 31))]) 71 | (def new-plan (plan/insert-days plan days)) 72 | (def new-days (new-plan :days)) 73 | (test (length new-days) 2) 74 | (test (= (d/date 2020 8 1) ((new-days 0) :date)) true) 75 | (test (= (d/date 2020 7 31) ((new-days 1) :date)) true)) 76 | 77 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 78 | ## Test remove-days 79 | 80 | (deftest "removes days from the plan" 81 | (def day-1 (day/build-day (d/date 2020 8 5))) 82 | (def day-2 (day/build-day (d/date 2020 8 4) 83 | @[] 84 | @[(task/build-task "Buy milk" true)])) 85 | (def day-3 (day/build-day (d/date 2020 8 3) 86 | @[(event/build-event "Visited museum")] 87 | @[])) 88 | (def day-4 (day/build-day (d/date 2020 8 2))) 89 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 90 | (def new-plan (plan/remove-days plan @[day-2 day-4])) 91 | (def new-days (new-plan :days)) 92 | (test (length new-days) 2) 93 | (test (= day-1 (new-days 0)) true) 94 | (test (= day-3 (new-days 1)) true)) 95 | 96 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 97 | ## Test days-before 98 | 99 | (deftest "returns all days befor the date" 100 | (def day-1 (day/build-day (d/date 2020 8 5))) 101 | (def day-2 (day/build-day (d/date 2020 8 4))) 102 | (def day-3 (day/build-day (d/date 2020 8 3))) 103 | (def day-4 (day/build-day (d/date 2020 8 2))) 104 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 105 | (def days (plan/days-before plan (d/date 2020 8 4))) 106 | (test (length days) 2) 107 | (if (>= (length days) 2) 108 | (do 109 | (test (= day-3 (days 0)) true) 110 | (test (= day-4 (days 1)) true)))) 111 | 112 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 113 | ## Test days-after 114 | 115 | (deftest "returns all days after the date" 116 | (def day-1 (day/build-day (d/date 2020 8 5))) 117 | (def day-2 (day/build-day (d/date 2020 8 4))) 118 | (def day-3 (day/build-day (d/date 2020 8 3))) 119 | (def day-4 (day/build-day (d/date 2020 8 2))) 120 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 121 | (def days (plan/days-after plan (d/date 2020 8 3))) 122 | (test (length days) 2) 123 | (if (>= (length days) 2) 124 | (do 125 | (test (= day-1 (days 0)) true) 126 | (test (= day-2 (days 1)) true)))) 127 | 128 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 129 | ## Test days-on-or-after 130 | 131 | (deftest "returns days with the date or after the date" 132 | (def day-1 (day/build-day (d/date 2020 8 5))) 133 | (def day-2 (day/build-day (d/date 2020 8 4))) 134 | (def day-3 (day/build-day (d/date 2020 8 3))) 135 | (def day-4 (day/build-day (d/date 2020 8 2))) 136 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 137 | (def days (plan/days-on-or-after plan (d/date 2020 8 4))) 138 | (test (length days) 2) 139 | (test (= day-1 (days 0)) true) 140 | (test (= day-2 (days 1)) true)) 141 | 142 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 143 | ## Test all-days 144 | 145 | (deftest "returns all days from the plan" 146 | (def day-1 (day/build-day (d/date 2020 8 6))) 147 | (def day-2 (day/build-day (d/date 2020 8 3))) 148 | (def plan (plan/build-plan :days @[day-1 day-2])) 149 | (def days (plan/all-days plan)) 150 | (test (length days) 4) 151 | (if (= 4 (length days)) 152 | (do 153 | (test (d/equal? (d/date 2020 8 6) ((days 0) :date)) true) 154 | (test (d/equal? (d/date 2020 8 5) ((days 1) :date)) true) 155 | (test (d/equal? (d/date 2020 8 4) ((days 2) :date)) true) 156 | (test (d/equal? (d/date 2020 8 3) ((days 3) :date)) true)))) 157 | 158 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 159 | ## Test all-days-before 160 | 161 | (deftest "returns all days before the date and it generates new days for 'holes'" 162 | (def day-1 (day/build-day (d/date 2020 8 6))) 163 | (def day-2 (day/build-day (d/date 2020 8 3))) 164 | (def plan (plan/build-plan :days @[day-1 day-2])) 165 | (def days (plan/all-days-before plan (d/date 2020 8 5))) 166 | (test (length days) 2) 167 | (if (= 2 (length days)) 168 | (do 169 | (test (d/equal? (d/date 2020 8 4) ((days 0) :date)) true) 170 | (test (d/equal? (d/date 2020 8 3) ((days 1) :date)) true)))) 171 | 172 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 173 | ## Test all-tasks 174 | 175 | (deftest "returns all tasks from the plan" 176 | (def day-1 (day/build-day (d/date 2020 8 4) 177 | @[] 178 | @[(task/build-task "Buy milk" true)])) 179 | (def day-2 (day/build-day (d/date 2020 8 3) 180 | @[(event/build-event "Visited museum")] 181 | @[(task/build-task "Review PRs" false)])) 182 | (def plan (plan/build-plan :days @[day-1 day-2])) 183 | (def tasks (plan/all-tasks plan)) 184 | (test (length tasks) 2) 185 | (test ((tasks 0) :title) "Buy milk") 186 | (test ((tasks 1) :title) "Review PRs")) 187 | 188 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 189 | ## Test tasks-between 190 | 191 | (deftest "returns all tasks from the days between 2 dates" 192 | (def day-1 (day/build-day (d/date 2020 8 4) 193 | @[] 194 | @[(task/build-task "Task 1" true)])) 195 | (def day-2 (day/build-day (d/date 2020 8 3) 196 | @[] 197 | @[(task/build-task "Task 2" false)])) 198 | (def day-3 (day/build-day (d/date 2020 8 2) 199 | @[] 200 | @[(task/build-task "Task 3" false)])) 201 | (def day-4 (day/build-day (d/date 2020 8 1) 202 | @[] 203 | @[(task/build-task "Task 4" false)])) 204 | (def plan (plan/build-plan :days @[day-1 day-2 day-3 day-4])) 205 | (def tasks (plan/tasks-between plan (d/date 2020 8 2) (d/date 2020 8 3))) 206 | (test (length tasks) 2) 207 | (test ((tasks 0) :title) "Task 2") 208 | (test ((tasks 1) :title) "Task 3")) 209 | -------------------------------------------------------------------------------- /test/schedule_parser_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/date :as d) 4 | (import ../src/schedule_parser) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Test parse-schedule 8 | 9 | (deftest "parses the schedule" 10 | (def schedule-string 11 | ``` 12 | # Scheduled Tasks 13 | 14 | - Weekly Meeting (every Tuesday) 15 | - Puzzle Storm on Lichess (every day) 16 | - Deploy the web app (every weekday) 17 | - Pay football practice (every month) 18 | - Martha's birthday (every 05-24) 19 | - Meeting with Jack (on 2022-05-03) 20 | - Review logs (every last day) 21 | - Send invoice (every last weekday) 22 | - Pay bills (every month on 15) 23 | ```) 24 | (def result (schedule_parser/parse schedule-string)) 25 | (def scheduled-tasks (result :tasks)) 26 | (test (length scheduled-tasks) 9) 27 | (let [task (scheduled-tasks 0)] 28 | (test (task :title) "Weekly Meeting") 29 | (test (task :done) false) 30 | (test (task :schedule) "every Tuesday")) 31 | (let [task (scheduled-tasks 1)] 32 | (test (task :title) "Puzzle Storm on Lichess") 33 | (test (task :schedule) "every day")) 34 | (let [task (scheduled-tasks 5)] 35 | (test (task :title) "Meeting with Jack") 36 | (test (task :schedule) "on 2022-05-03")) 37 | (let [task (scheduled-tasks 6)] 38 | (test (task :title) "Review logs") 39 | (test (task :schedule) "every last day")) 40 | (let [task (scheduled-tasks 7)] 41 | (test (task :title) "Send invoice") 42 | (test (task :schedule) "every last weekday")) 43 | (let [task (scheduled-tasks 8)] 44 | (test (task :title) "Pay bills") 45 | (test (task :schedule) "every month on 15"))) 46 | 47 | (deftest "returns an error when the schedule can't be parsed" 48 | (def schedule-string 49 | ``` 50 | ## Schedule 51 | 52 | * One (always) 53 | ```) 54 | (def result (schedule_parser/parse schedule-string)) 55 | (test (first (result :errors)) "Schedule can not be parsed")) 56 | 57 | (deftest "returns an error when the schedule can be partially parsed" 58 | (def schedule-string 59 | ``` 60 | # Scheduled Tasks 61 | 62 | - Weekly Meeting (every Tuesday) 63 | - Puzzle Storm on Lichess 64 | - Deploy the web app (every weekday) 65 | ```) 66 | (def result (schedule_parser/parse schedule-string)) 67 | (test (first (result :errors)) 68 | "Schedule can not be parsed - last parsed task is \"Weekly Meeting\" on line 3")) 69 | 70 | 71 | (deftest "returns an error when the schedule is empty" 72 | (def schedule-string 73 | ``` 74 | # Scheduled Tasks 75 | ```) 76 | (def result (schedule_parser/parse schedule-string)) 77 | (test (first (result :errors)) "Schedule is empty")) 78 | -------------------------------------------------------------------------------- /test/task_test.janet: -------------------------------------------------------------------------------- 1 | (use judge) 2 | 3 | (import ../src/date :as d) 4 | (import ../src/task) 5 | 6 | ## ————————————————————————————————————————————————————————————————————————————————————————————————— 7 | ## Test mark-as-missed 8 | 9 | (deftest "marks task as missed" 10 | (def date (d/date 2022 7 15)) 11 | (def task (task/build-task "Weekly meeting" false)) 12 | (def new-task (task/mark-as-missed task date)) 13 | (test (task :title) "Weekly meeting") 14 | (test (new-task :title) "Weekly meeting") 15 | (test (= (new-task :missed-on) date) true)) 16 | -------------------------------------------------------------------------------- /test/utils.janet: -------------------------------------------------------------------------------- 1 | (defn backup-file 2 | ``` 3 | Copies file to FILE_NAME.bkp. 4 | ``` 5 | [file-path] 6 | (os/execute @["cp" file-path (string file-path ".bkp")] :p)) 7 | 8 | (defn restore-file 9 | ``` 10 | Copies file from FILE_NAME.bkp to FILE_NAME and removes FILE_NAME.bkp. 11 | ``` 12 | [file-path] 13 | (os/execute @["cp" (string file-path ".bkp") file-path] :p) 14 | (os/rm (string file-path ".bkp"))) 15 | --------------------------------------------------------------------------------