···11+open Base
22+33+type ('a, 'b) t = { tree : ('a, 'b) Avltree.t; cmp : 'a -> 'a -> int }
44+55+let create ~cmp = { tree = Avltree.empty; cmp }
66+77+let add t ~key ~data =
88+ {
99+ t with
1010+ tree =
1111+ Avltree.add t.tree ~replace:true ~compare:t.cmp ~added:(ref true) ~key
1212+ ~data;
1313+ }
1414+1515+let remove t ~key =
1616+ { t with tree = Avltree.remove t.tree key ~removed:(ref true) ~compare:t.cmp }
1717+1818+let find t ~key = Avltree.find t.tree key ~compare:t.cmp
1919+let iter t ~f = Avltree.iter t.tree ~f
+7
lib/ordered_map.mli
···11+type ('a, 'b) t
22+33+val create : cmp:('a -> 'a -> int) -> ('a, 'b) t
44+val add : ('a, 'b) t -> key:'a -> data:'b -> ('a, 'b) t
55+val remove : ('a, 'b) t -> key:'a -> ('a, 'b) t
66+val find : ('a, 'b) t -> key:'a -> 'b option
77+val iter : ('a, 'b) t -> f:(key:'a -> data:'b -> unit) -> unit
+112
lib/tracker.ml
···11+open Core
22+open Stdio
33+open Base
44+55+type mood = Val.t
66+type t = (Date.t, mood) Ordered_map.t
77+88+let now () = Date.today ~zone:Time_float.Zone.utc
99+let mood_of_day t day = Ordered_map.find t ~key:day
1010+let create () = Ordered_map.create ~cmp:Core.Date.compare
1111+let record_day t ~day ~mood = Ordered_map.add t ~key:day ~data:mood
1212+1313+let record ?day t ~mood =
1414+ record_day t ~day:(Option.value day ~default:(now ())) ~mood
1515+1616+let today t =
1717+ let now = now () in
1818+ (now, mood_of_day t now)
1919+2020+let this_week t =
2121+ let now = now () in
2222+ let day_of_week = Day_of_week.to_int @@ Date.day_of_week now in
2323+ let monday = Date.add_days now (-day_of_week) in
2424+ let week = List.init 7 ~f:(fun i -> Date.add_days monday i) in
2525+ List.map week ~f:(fun d -> (d, mood_of_day t d))
2626+2727+let this_month t =
2828+ let now = now () in
2929+ let y = Date.year now in
3030+ let m = Date.month now in
3131+ let first_day = Date.create_exn ~y ~m ~d:1 in
3232+ let days_in_month = Date.days_in_month ~year:y ~month:m in
3333+ let month = List.init days_in_month ~f:(fun i -> Date.add_days first_day i) in
3434+ List.map month ~f:(fun d -> (d, mood_of_day t d))
3535+3636+let this_year t =
3737+ let now = now () in
3838+ let y = Date.year now in
3939+ let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in
4040+ let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in
4141+ let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in
4242+ List.map year ~f:(fun d -> (d, mood_of_day t d))
4343+4444+let all t =
4545+ let acc = ref [] in
4646+ Ordered_map.iter t ~f:(fun ~key ~data -> acc := (key, data) :: !acc);
4747+ List.rev !acc
4848+4949+let entry_of_string l =
5050+ match String.split_on_chars ~on:[ ',' ] l with
5151+ | [ date; v ] -> Some (Date.of_string date, Val.of_string v)
5252+ | _ -> None
5353+5454+let entry_to_string (day, mood) =
5555+ String.concat ~sep:"," [ Date.to_string day; Val.to_string mood ]
5656+5757+let of_list ls =
5858+ List.fold ~init:(create ())
5959+ ~f:(fun acc (day, mood) -> record_day acc ~day ~mood)
6060+ ls
6161+6262+let to_list = all
6363+6464+let load location =
6565+ try
6666+ In_channel.with_file location ~f:(fun c ->
6767+ List.filter_map (In_channel.input_lines c) ~f:entry_of_string)
6868+ |> of_list
6969+ with _ -> create ()
7070+7171+let store t ~location =
7272+ Out_channel.write_lines location (List.map ~f:entry_to_string (all t))
7373+7474+let pp t = String.concat ~sep:"\n" @@ List.map ~f:entry_to_string (all t)
7575+7676+let random () =
7777+ Random.init 12334254;
7878+ let now = now () in
7979+ let y = Date.year now in
8080+ let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in
8181+ let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in
8282+ let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in
8383+ List.map year ~f:(fun d -> (d, Val.of_int (Random.int 5))) |> of_list
8484+8585+let render t =
8686+ let vals = this_year t in
8787+ let now = now () in
8888+ let y = Date.year now in
8989+ let start_date = Date.create_exn ~y ~m:Month.Jan ~d:1 in
9090+9191+ let start_dow = Date.day_of_week start_date |> Day_of_week.to_int in
9292+ let pad_tracker n =
9393+ List.init n ~f:(fun _ -> (Date.of_string "2000-01-01", None))
9494+ in
9595+ let start_padding = pad_tracker start_dow in
9696+ let total_len = List.length start_padding + List.length vals in
9797+ let remainder = total_len % 7 in
9898+ let end_padding = pad_tracker (7 - remainder) in
9999+ let padded = start_padding @ vals @ end_padding in
100100+ let weeks = List.chunks_of padded ~length:7 in
101101+ let rows = List.transpose_exn weeks in
102102+ let day_labels = [ "Sun"; ""; "Tue"; ""; "Thu"; ""; "Sat" ] in
103103+ List.map2_exn day_labels rows ~f:(fun label row ->
104104+ let blocks =
105105+ List.map row ~f:(fun (_, mood) ->
106106+ Option.value_map mood
107107+ ~default:("\027[2m" ^ "·" ^ "\027[0m")
108108+ ~f:Val.render)
109109+ |> String.concat
110110+ in
111111+ sprintf "%-3s %s" label blocks)
112112+ |> String.concat ~sep:"\n"
+19
lib/tracker.mli
···11+open Base
22+open Core
33+44+type t
55+66+val create : unit -> t
77+val record : ?day:Date.t -> t -> mood:Val.t -> t
88+val today : t -> Date.t * Val.t option
99+val this_week : t -> (Date.t * Val.t option) list
1010+val this_month : t -> (Date.t * Val.t option) list
1111+val this_year : t -> (Date.t * Val.t option) list
1212+val of_list : (Date.t * Val.t) list -> t
1313+val to_list : t -> (Date.t * Val.t) list
1414+val all : t -> (Date.t * Val.t) list
1515+val load : string -> t
1616+val store : t -> location:string -> unit
1717+val pp : t -> string
1818+val render : t -> string
1919+val random : unit -> t
+25
lib/val.ml
···11+open Base
22+33+type t = int
44+55+let of_int = Fn.id
66+let to_int = Fn.id
77+let of_string = Int.of_string
88+let to_string = Int.to_string
99+1010+let render t =
1111+ let value = to_int t in
1212+ let scaled = Int.min 5 (Int.max 0 value) in
1313+ let yellow = "\027[33m" in
1414+ let reset = "\027[0m" in
1515+ let block =
1616+ match scaled with
1717+ | 0 -> " "
1818+ | 1 -> "░"
1919+ | 2 -> "▒"
2020+ | 3 -> "▓"
2121+ | 4 -> "█"
2222+ | 5 -> "█"
2323+ | _ -> assert false
2424+ in
2525+ yellow ^ block ^ reset
+7
lib/val.mli
···11+type t
22+33+val of_int : int -> t
44+val to_int : t -> int
55+val of_string : string -> t
66+val to_string : t -> string
77+val render : t -> string
···11+mood
22+----
33+44+i am attempting mood tracking this year. not sure if it
55+really helps achieve anything, but i want to try it
66+nonetheless!