Ep 019: Dazed by Weak Weeks
► Play EpisodeNate wants to see more than data structures in a REPL.
- Goal: see a text-based calendar of hours worked by day and week.
- "With the power of the predicate, I summon thee from the bag of data!"
- If data items share structure, you only need one predicate function, not separate functions per "type".
- Common pattern:
filter
thenmap
, thenreduce
- Output:
- Looks like a calendar with one line per week
- Show daily total for each day
- Show weekly total
- Problem: how do we split the days into weeks?
- "I don't remember
split-weeks
in the Clojure cheat sheet." - "What day does your week start on? My week starts on Tuesday."
- "I always assume that when a programmer says 'most', they mean 'most of the people around me'." "I actually think it just means 'me'."
- "Anecdata: when you have two anecdotes, it makes data."
- Create higher-level summaries: entry → day → week
- Sift through data at the level of your question.
- Problem: A date identifies a "day", but what identifies a week? A "week ID", so to speak.
- It would be nice if that week ID was ordered, so we could sort on it.
- Idea: Use the date of the first day of the week
- uniq, sortable, spans years
- weeks that start on Tuesday will never have the same ID as weeks that start on Sunday
- Might as well have the starting day of the week in the map too:
:sunday
,:tuesday
, etc. - Convenient to have two different views for the same data in the same data structure
- immutable data won't change and get inconsistent.
- it's pre-calculated if you need to use it a lot vs just using a predicate "on the fly".
- Want to
group-by
the week:- need a predicate from "date ID" → "week ID":
(week-id starting-day-of-week day)
- need to take a "date" and produce "date of first day in week"
- Eg.
(week-id :sunday {:date "2019-03-08" ...}) => "2019-03-03"
- need a predicate from "date ID" → "week ID":
- "You're teaching Clojure core how to describe your data by giving it vocabulary."
- Write something that converts data to data, not data to
println
. - Principle: move the I/O to the edges
- make the
println
a trivial step at the end - even making the strings is a data transform
- everything prior to the
println
could be tested, even the formatting!
- make the
- "Test it to the level of detail you can tolerate."
- Much easier to reason about pure functions.
Related episodes:
Clojure in this episode:
filter
,map
,reduce
group-by
sort-by
withpartition-by
take-while
,drop-while
, andsplit-with
Code sample from this episode:
(ns time.week-05
(:require
[time.week-04 :as week-04]
[java-time :as jt]
))
; Date helpers
(defn start-of-week
"Get the the starting date of the week containing local-date. The week will
start on the named day. Eg. :sunday"
[starting-day-of-week local-date]
(jt/adjust local-date :previous-or-same-day-of-week (jt/day-of-week starting-day-of-week)))
(defn week-dates
"Create a week's worth of dates starting from the given date."
[starting-date]
(->> (range 0 7)
(map #(jt/plus starting-date (jt/days %)))))
; Aggregates
(defn sum-minutes
"Sums the minutes for all kinds: entry, day, week"
[entries]
(->> entries
(map :minutes)
(reduce +)))
; Conversions
(defn entries->days
"Convert a seq of entries to days."
[entries]
(->> (group-by :date entries)
(map (fn [[date xs]] {:date date :minutes (sum-minutes xs)}))))
(defn days->week
"Convert a seq of days into a week. Week is picked by first date."
[starting-day-of-week days]
(let [lookup (into {} (map (juxt :date identity) days))
starting-date (start-of-week starting-day-of-week (:date (first days)))
all-days (->> (week-dates starting-date)
(map #(or (get lookup %)
{:date %
:minutes 0})))]
{:starting-day-of-week starting-day-of-week
:date starting-date
:days (vec all-days)
:minutes (sum-minutes all-days)}))
(defn partition-weeks
[starting-day-of-week days]
(->> days
(sort-by :date)
(partition-by #(start-of-week starting-day-of-week (:date %)))))
(defn days->weeks
"Convert a seq of days into an ordered seq of weeks."
[starting-day-of-week days]
(->> (partition-weeks starting-day-of-week days)
(map (partial days->week starting-day-of-week))))
(comment
(->> (week-04/log-times "time-log.txt")
(entries->days))
(->> (week-04/log-times "time-log.txt")
(entries->days)
(days->weeks :sunday))
)