···1+open Core
2+open Stdio
3+open Base
4+5+type mood = Val.t
6+type t = (Date.t, mood) Ordered_map.t
7+8+let now () = Date.today ~zone:Time_float.Zone.utc
9+let mood_of_day t day = Ordered_map.find t ~key:day
10+let create () = Ordered_map.create ~cmp:Core.Date.compare
11+let record_day t ~day ~mood = Ordered_map.add t ~key:day ~data:mood
12+13+let record ?day t ~mood =
14+ record_day t ~day:(Option.value day ~default:(now ())) ~mood
15+16+let today t =
17+ let now = now () in
18+ (now, mood_of_day t now)
19+20+let this_week t =
21+ let now = now () in
22+ let day_of_week = Day_of_week.to_int @@ Date.day_of_week now in
23+ let monday = Date.add_days now (-day_of_week) in
24+ let week = List.init 7 ~f:(fun i -> Date.add_days monday i) in
25+ List.map week ~f:(fun d -> (d, mood_of_day t d))
26+27+let this_month t =
28+ let now = now () in
29+ let y = Date.year now in
30+ let m = Date.month now in
31+ let first_day = Date.create_exn ~y ~m ~d:1 in
32+ let days_in_month = Date.days_in_month ~year:y ~month:m in
33+ let month = List.init days_in_month ~f:(fun i -> Date.add_days first_day i) in
34+ List.map month ~f:(fun d -> (d, mood_of_day t d))
35+36+let this_year t =
37+ let now = now () in
38+ let y = Date.year now in
39+ let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in
40+ let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in
41+ let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in
42+ List.map year ~f:(fun d -> (d, mood_of_day t d))
43+44+let all t =
45+ let acc = ref [] in
46+ Ordered_map.iter t ~f:(fun ~key ~data -> acc := (key, data) :: !acc);
47+ List.rev !acc
48+49+let entry_of_string l =
50+ match String.split_on_chars ~on:[ ',' ] l with
51+ | [ date; v ] -> Some (Date.of_string date, Val.of_string v)
52+ | _ -> None
53+54+let entry_to_string (day, mood) =
55+ String.concat ~sep:"," [ Date.to_string day; Val.to_string mood ]
56+57+let of_list ls =
58+ List.fold ~init:(create ())
59+ ~f:(fun acc (day, mood) -> record_day acc ~day ~mood)
60+ ls
61+62+let to_list = all
63+64+let load location =
65+ try
66+ In_channel.with_file location ~f:(fun c ->
67+ List.filter_map (In_channel.input_lines c) ~f:entry_of_string)
68+ |> of_list
69+ with _ -> create ()
70+71+let store t ~location =
72+ Out_channel.write_lines location (List.map ~f:entry_to_string (all t))
73+74+let pp t = String.concat ~sep:"\n" @@ List.map ~f:entry_to_string (all t)
75+76+let random () =
77+ Random.init 12334254;
78+ let now = now () in
79+ let y = Date.year now in
80+ let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in
81+ let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in
82+ let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in
83+ List.map year ~f:(fun d -> (d, Val.of_int (Random.int 5))) |> of_list
84+85+let render t =
86+ let vals = this_year t in
87+ let now = now () in
88+ let y = Date.year now in
89+ let start_date = Date.create_exn ~y ~m:Month.Jan ~d:1 in
90+91+ let start_dow = Date.day_of_week start_date |> Day_of_week.to_int in
92+ let pad_tracker n =
93+ List.init n ~f:(fun _ -> (Date.of_string "2000-01-01", None))
94+ in
95+ let start_padding = pad_tracker start_dow in
96+ let total_len = List.length start_padding + List.length vals in
97+ let remainder = total_len % 7 in
98+ let end_padding = pad_tracker (7 - remainder) in
99+ let padded = start_padding @ vals @ end_padding in
100+ let weeks = List.chunks_of padded ~length:7 in
101+ let rows = List.transpose_exn weeks in
102+ let day_labels = [ "Sun"; ""; "Tue"; ""; "Thu"; ""; "Sat" ] in
103+ List.map2_exn day_labels rows ~f:(fun label row ->
104+ let blocks =
105+ List.map row ~f:(fun (_, mood) ->
106+ Option.value_map mood
107+ ~default:("\027[2m" ^ "·" ^ "\027[0m")
108+ ~f:Val.render)
109+ |> String.concat
110+ in
111+ sprintf "%-3s %s" label blocks)
112+ |> String.concat ~sep:"\n"
+19
lib/tracker.mli
···0000000000000000000
···1+open Base
2+open Core
3+4+type t
5+6+val create : unit -> t
7+val record : ?day:Date.t -> t -> mood:Val.t -> t
8+val today : t -> Date.t * Val.t option
9+val this_week : t -> (Date.t * Val.t option) list
10+val this_month : t -> (Date.t * Val.t option) list
11+val this_year : t -> (Date.t * Val.t option) list
12+val of_list : (Date.t * Val.t) list -> t
13+val to_list : t -> (Date.t * Val.t) list
14+val all : t -> (Date.t * Val.t) list
15+val load : string -> t
16+val store : t -> location:string -> unit
17+val pp : t -> string
18+val render : t -> string
19+val random : unit -> t
+25
lib/val.ml
···0000000000000000000000000
···1+open Base
2+3+type t = int
4+5+let of_int = Fn.id
6+let to_int = Fn.id
7+let of_string = Int.of_string
8+let to_string = Int.to_string
9+10+let render t =
11+ let value = to_int t in
12+ let scaled = Int.min 5 (Int.max 0 value) in
13+ let yellow = "\027[33m" in
14+ let reset = "\027[0m" in
15+ let block =
16+ match scaled with
17+ | 0 -> " "
18+ | 1 -> "░"
19+ | 2 -> "▒"
20+ | 3 -> "▓"
21+ | 4 -> "█"
22+ | 5 -> "█"
23+ | _ -> assert false
24+ in
25+ yellow ^ block ^ reset
+7
lib/val.mli
···0000000
···1+type t
2+3+val of_int : int -> t
4+val to_int : t -> int
5+val of_string : string -> t
6+val to_string : t -> string
7+val render : t -> string