Clojure Golf – USGA Handicap

I recently discovered the convoluted USGA rules for calculating a golf handicap. I thought it might be fun to implement them in Clojure.

Here’s my interpretation of how raw data and intermediate results flow toward the final value:

If you want to calculate your own USGA handicap, do the following:

1. Supply course data – as below – for your course. You’ll have to get the pars, plus the rating and slope for the tees you play.

2. Create a player map that holds your current handicap and set of rounds for a given course and tee. You can supply as many rounds as you like; only a specific subset of them wind up being used in the final par calculation.

3. Call the main function, usga-handicap, passing in the 2 structures above.

Sample data:

(def course-data {
   :course-pars [5 4 3 4 3 4 5 3 5 4 3 5 4 3 4 5 4 4]
   :tees {:black   {:rating 72.5 :slope 133}
          :blue    {:rating 70.4 :slope 128}
          :white-m {:rating 68.6 :slope 120}
          :white-f {:rating 72.1 :slope 127}
          :red     {:rating 69.4 :slope 120}}})

(def player-course-tee
  {:course-handicap 12
   :gender :male
   :tee :white-m
   :rounds [[5 5 3 4 4 5 6 4 5 5 7 5 4 4 6 6 5 5]
            [5 5 3 4 4 5 6 4 5 5 8 5 4 4 6 7 5 5]
            [4 4 4 5 3 5 5 3 6 4 5 5 5 3 5 6 4 4]
            [5 4 5 3 3 5 4 4 5 5 5 4 6 5 4 5 6 5]
            [6 5 2 4 4 4 5 5 4 5 8 5 4 3 5 4 4 5]
            [4 3 5 4 4 5 4 3 6 6 5 6 6 3 5 6 4 6]
            [5 3 3 5 3 6 5 3 4 5 5 6 5 5 5 5 6 4]
            [6 6 3 3 4 5 5 4 6 4 8 5 4 4 4 6 4 6]            
            [7 5 4 4 5 4 6 5 6 5 7 6 7 4 6 5 5 5]]})

Unfortunately these are not my scores – I’m much worse.

Here are the functions that perform the calculation. They were derived from the excellent USGA handicap description I found at golfsoftware.com. Any mistakes in the calculation are probably mine not theirs.

Step 1: Convert Original Gross Scores to Adjusted Gross Scores (AGS).

The first step is to apply maximums to your score for each hole. Your maximum score depends only on your handicap unless your handicap is 9 or less, in which case you’re limited to a double bogey. We apply the maximums to all rounds, then total up the score for each round to produce the Adjusted Gross Scores.

(defn ESC-max
  "Return the maximum score given the PAR for the hole
   and the players current course handicap"
  [chcp holepar]
  (cond
    (<= chcp 9) (+ holepar 2)
    (<= 10 chcp 19) 7
    (<= 20 chcp 29) 8
    (<= 30 chcp 39) 9
    :else 10))

(defn AGS4Player
  "Return all Adjusted Gross Scores for a given player, course, and tee"
  [player-hcp course-pars rounds]
  (let [maximums (map (partial ESC-max player-hcp) course-pars)]
    (map #(apply + (map min % maximums)) rounds)))

Step 2: Calculate Handicap Differentials for Each Score.

I.e. perform some voodoo involving the slope and rating for the course and tees that were used for all rounds. The result is your “handicap differentials”.

;;Handicap Differential = (AGS - Course Rating) X 113 ÷ Slope Rating
(defn HDiff4Player [player-hcp course-pars rounds tee]
  "Return all Handicap Differentials for a given player, course, and tee"
  (letfn [(round [n] (/ (int (* 10 (+ 0.05 n))) 10.0))
          (HDiff [ags] (round (* (- ags (:rating tee)) 
                                 (/ 113 (:slope tee)))))]
    (map HDiff (AGS4Player player-hcp course-pars rounds))))

Step 3: Select the Best, i.e. Lowest, Handicap Differentials.

The formula for the number of differentials to be used in the final calculation is a table look-up. If you have 5 or 6 rounds recorded, use the lowest round. If you have 7 or 8 rounds, use the lowest 2, etc. Max is 10 differentials used.

;USGA does not calculate a handicap until five scores have been recorded.
(defn LowestHandicapDifferentials [player course] 
  "Return the lowest handicap differentials for a given player, course, tee"
  (let [hdiffs (HDiff4Player player course)
        diffs-used [1 1 1 1 1 1 2 2 3 3 4 4 5 5 6 6 6 7 8 9]
        n (nth diffs-used (count hdiffs) 10)]
    (take n (sort hdiffs)))) ; lowest N

Step 4: Calculate the Average of the Lowest Handicap Differentials
Step 5: Multiply Average of Handicap Differentials by 0.96 or 96%
Step 6: Truncate, or Delete, Numbers to the Right of Tenths (do not round!)
Step 6a: Max Handicap Index is 36.4 for men, 40.4 for women
Step 7: Course Handicap = Index x (Slope Rating of Tee on Course / 113)

This final calculation is a big mess with a bunch of special truncation rules and magic numbers. I suppose there’s some method to the madness, but it’s not exactly obvious. In particular, note that truncation is used here rather than rounding. Of course! right?

(defn usga-handicap [player course]
  (let [avg #(/ (apply + %) (count %))
        trunc #(/ (int (* 10 %)) 10.0)
        hcpmax (if (= :male (:gender player)) 36.4 40.4)
        tee  ((comp (:tee player) :tees) course)
        hdiffs (HDiff4Player
                (:course-handicap player)
                (:course-pars course)
                (:rounds player)
                tee)
        player-avg (avg (LowestHandicapDifferentials hdiffs))
        hcp-index (min hcpmax (trunc (* 0.96 player-avg)))]
    (int (* hcp-index (/ (:slope tee) 113)))))

And there you have it. To use this code, just load your data and the functions above, then call usga-handicap with player and course data. Out pops your handicap:

(usga-handicap player-course-tee course-data)
12

Exercise for the reader: Handle 9 hole rounds. It should be pretty easy; mostly just adjusting some of the magic numbers.

I’d be interested to get suggestions for refactoring this toward more idiomatic Clojure. Corrections and ideas for improvements are welcome!

Thanks!