I organize my daily schedules by month. There is a folder called 2020
and it has subfolders for each month - 1
, 2
, ..., 12
. Each month should have one file for each day a schedule was created and reported. For example March may have 1.ses
, 2.ses
, ..., 31.ses
. It may be missing a few files. The missing files are for days where I didn't create a schedule or I didn't record how I used my time.
I wanted to create a diagram that would give me an overview of my religiousness with the schedules. A square for each day of each month with the reported days in green and the unreported days colored red. Example.
I wrote a single Haskell file called ScheduleMetric.hs
, which I ran as
$ runghc -package --ghc-arg=diagrams-lib \
-package --ghc-arg=diagrams-svg \
-- ScheduleMetric.hs -w 200 -o /home/pi/schedule/report/report.svg
The file was
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
module ScheduleMetric where
import Control.Monad
import Data.Function
import Data.Functor
import Data.List
import Data.Maybe
import qualified Data.Set as Set
import Diagrams.Prelude
import Diagrams.Backend.SVG.CmdLine
import System.Directory
import System.FilePath
main :: IO ()
main =
-- for each month, prepare the set of days for which a schedule is present
scheduled <&>
-- construct a Haskell representation of the diagram
scorecard >>=
-- render the diagram
mainWith
-- given a list of (month number, set of days in month) pairs construct a
-- Haskell representation of the diagram to be rendered
scorecard :: [(Int, Set.Set Int)] -> Diagram B
scorecard monthSets =
monthSets &
-- prepare a separate "scorecard" for each month
map monthlyScorecard &
-- arrange the monthly scorecards vertically, starting from January at the
-- top to December at the bottom
vcat
-- given a tuple whose first element is the number of a month (a number between
-- 1 and 12, inclusive) and second element is a set of numbers representing
-- dates within that month for which a schedule is present, create the Haskell
-- representation of horizontal line of squares where the leftmost square is
-- just the number of the month and the subsequent squares represent the days of
-- that month. Each day where a schedule is present is colored green otherwise
-- it is colored red.
monthlyScorecard :: (Int, Set.Set Int) -> Diagram B
monthlyScorecard (month, days) = monthName ||| monthScorecard
where
-- the leftmost square, a white box containing the number of the month.
monthName = month # show # text # fontSizeL 2 # fc black <>
square 2 # fc white
-- the subsequent squares, there is one for each day of the month.
monthScorecard = [1..month & size] &
map (\i -> square 2 # fc (color i)) &
hcat
-- the count of days in the month given its number. 1 represents January
-- which has 31 days, 2 represents February with 28 days, and so on.
size month
| month == 2 = 28
| month `elem` [4,6,9,11] = 30
| otherwise = 31
-- if a day is present in the input set then its corresponding box is
-- colored green and otherwise colored red.
color day
| day `Set.member` days = green
| otherwise = red
-- this function evaluates to a list of tuples where the first element of each
-- tuple is a number representing a month (1 stands for January and 12 stands
-- for December) and the second element is a set of numbers representing the
-- days in the month for which schedules are present
--
-- suppose the contents of the target directory are:
-- 1/1.ses
-- 1/4.ses
-- 1/9.ses
-- 2/3.ses
-- 2/4.ses
--
-- then the function below will evaluate to
-- [ (1, {1,4,9})
-- , (2, {3,4})
-- ]
scheduled :: IO [(Int, Set.Set Int)]
scheduled =
listDirectory target <&>
sort <&>
-- make the paths of the month folders absolute
map (target </>) >>=
filterM doesDirectoryExist <&>
-- turn each folder path into a Just (folder path, month number) or Nothing
map dirMonthFromDirectory <&>
-- remove all the Nothing values
catMaybes >>= \directories ->
forM directories $ \(directory, month) ->
listDirectory directory <&>
-- make the paths of the day files absolute
map (directory </>) >>=
filterM doesFileExist <&>
-- throw away any file that doesn't end in a .md or a .ses
filter mdOrSes <&>
-- turn each file path into a (Just day) or Nothing
map dayFromFile <&>
catMaybes <&>
Set.fromList <&> \set ->
(month, set)
-- given the path to a file, relative or absolute, evaluate to True if the
-- file's extension indicates that it is a markdown file or a simple Emacs
-- spreadsheet file, this is relevant because my daily schedule files are
-- confined to these two formats.
mdOrSes :: FilePath -> Bool
mdOrSes filename = ".md" `isSuffixOf` filename || ".ses" `isSuffixOf` filename
-- given the relative or absolute path to a directory, kill the part containing
-- its parent directory and if the remaining part - the "dir2" in "/dir1/dir2/"
-- - is a String that can be read as a number, do so and return a tuple, the
-- first element of which is the directory path as-is and the second part is the
-- numbe and if that remaining part cannot be read as a number then return
-- Nothing.
dirMonthFromDirectory :: FilePath -> Maybe (FilePath, Int)
dirMonthFromDirectory dir
| baseName & isNumber = (dir, baseName & read) & Just
| otherwise = Nothing
where
baseName = dir & takeBaseName
-- given the path to a file, relative or absolute, kill its extension and
-- directory portion, and if the remaining part - the "base" in
-- "/dir1/dir2/base.ses" - is a string that can be read as a number, do so and
-- return the number. Otherwise return None.
dayFromFile :: FilePath -> Maybe Int
dayFromFile file
| baseName & isNumber = baseName & read & Just
| otherwise = Nothing
where
baseName = file & takeBaseName
-- evaluates to True if the argument is a String containing only digits, False
-- otherwise
isNumber :: String -> Bool
isNumber s = s & map (`elem` "0123456789") & and
-- the directory containing numbered folders representing months
target = "/home/pi/schedule/2020"
I used cron
to refresh the diagram once every day at 10 pm US CDT.
I started by making sure that the cron
daemon was running. $ systemctl status cron
was good enough to check that it was active.
Then I followed the instructions [here] to use crontab -e
to schedule a new task. I added the line
0 23 * * * /usr/bin/runghc -package --ghc-arg=diagrams-lib -package --ghc-arg=diagrams-svg -- /home/pi/code/haskell/schedule-metric/ScheduleMetric.hs -w 200 -o /home/pi/schedule/report/report.svg