Sveriges mest populära poddar

Functional Design in Clojure

Ep 019: Dazed by Weak Weeks

30 min • 8 mars 2019

Nate 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 then map, then reduce
  • 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"
  • "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!
  • "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 with partition-by
  • take-while, drop-while, and split-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))
  )
Förekommer på
00:00 -00:00