[racket] Lunch choosing program ported from Haskell to Racket
The first draft was in excellent shape. But I decided to start
re-writing it according to the Style guide to illustrate the
ideas from there:
#lang racket
(provide
;; -> Void
;; process input data to obtain restaurant ranking
main)
;; ---------------------------------------------------------------------------------------------------
;; input data: (note data is now separated from processing so that it's easy to perform I/O, too)
(define users '(brian duff fred jimmy john luke matthew michael nathaniel tom))
(define restaurants '(chik chipotle dickeys mexican nwc pizza thai))
; Edit presentUsers to match lunch goers for the day
(define presentUsers '(brian duff fred jimmy john luke matthew michael nathaniel tom))
; Edit lunchHistory to reflect visit history
(define lunchHistory
'( ("2014-06-30" pizza (brian duff jimmy matthew nathaniel tom))
("2014-06-15" thai (brian))
("2014-06-09" thai (brian nathaniel tom))
("2014-06-02" pizza (brian jimmy john michael nathaniel tom))
("2014-05-26" mexican (brian nathaniel))
("2014-05-19" nwc (brian fred jimmy nathaniel tom))
("2014-05-12" chipotle (brian fred jimmy michael nathaniel tom))
("2014-05-05" thai (brian fred jimmy michael nathaniel tom))
("2014-04-28" mexican (brian jimmy michael nathaniel tom))
("2014-04-21" chik (fred jimmy nathaniel tom))
("2014-04-14" pizza (brian fred jimmy michael nathaniel))
("2014-04-07" mexican (brian jimmy michael tom))
("2014-03-31" chipotle (fred nathaniel))
("2014-03-24" nwc (brian fred jimmy michael nathaniel tom))
("2014-03-17" mexican (brian duff fred jimmy michael nathaniel tom))
("2014-03-10" pizza (brian jimmy nathaniel tom))
("2014-03-03" mexican (brian john nathaniel tom))
("2014-02-24" thai (brian jimmy michael nathaniel tom))
("2014-02-17" pizza (brian nathaniel tom))
("2014-02-10" mexican (brian fred jimmy luke tom))
("2014-02-03" mexican (brian fred jimmy luke nathaniel tom))
("2014-01-27" chipotle (brian fred jimmy nathaniel tom))
("2014-01-20" pizza (brian luke nathaniel tom))
("2014-01-13" thai (brian fred jimmy john michael nathaniel tom))
("2014-01-06" mexican (brian fred jimmy nathaniel tom))
("2013-12-30" pizza (brian jimmy nathaniel tom))
("2013-12-23" dickeys (brian duff nathaniel tom))
("2013-12-16" mexican (brian jimmy michael nathaniel tom))
("2013-12-09" nwc (brian fred jimmy michael nathaniel tom))
("2013-12-02" dickeys (brian fred jimmy john michael nathaniel tom))))
(define rawRatings
'(( brian duff fred jimmy matthew nathaniel tom)
(chik 0.0 11.0 0.5 5.0 0.5 1.0 0.0)
(chipotle 0.3 8.0 0.7 7.0 1.0 2.0 0.8)
(dickeys 0.3 5.0 0.7 1.0 0.0 1.0 0.4)
(mexican 1.5 1.0 1.0 1.0 0.5 2.0 0.6)
(nwc 0.4 5.0 0.7 7.0 0.0 1.0 0.5)
(pizza 0.9 12.0 0.7 1.0 0.0 2.0 0.7)
(thai 0.9 11.0 0.8 5.0 2.0 2.0 0.8)))
;; ---------------------------------------------------------------------------------------------------
;; main function:
(define (main)
;; completeRatings adds a default (User, 1.0) rating to
;; restaurants so each restaurant has a rating from every user
(define completeRatings
(for/list ([entry (cross rawRatings)])
(match entry
[(list restaurant ratings) (list restaurant (filledOut ratings))])))
; userSums is a #hash(user . sum) where sum is the sum of all the user's ratings
(define userSums
(for/fold ((hsh (make-immutable-hash))) ([pair (append-map second completeRatings)])
(match pair
[(list user rating) (hash-set hsh user (+ rating (hash-ref hsh user .0)))])))
;; Take the rawRatings and normalize them so that:
;; 1) the sum of a user's ratings across all restaurants adds up to 1.0
;; 2) each restaurant has a rating for all users
(define normalizedRatings
(for/list ([pair completeRatings])
(match pair
[(list rest ratings) (list rest (map (normalize userSums) ratings))])))
(define rawResults (rankRestaurants lunchHistory normalizedRatings presentUsers restaurants))
(pretty-print (sort rawResults #:key second >)))
;; -----------------------------------------------------------------------------
;; Participant = Symbol
;; Restaurant = Symbol
;; Rating = NonnegativeNumber
;; -----------------------------------------------------------------------------
;; [Cons [List-of Participant] [List-of [Cons Restaurant [List-of Rating]]]]
;; ->
;; [List-of [Cons Restaurant [List-of [List Participant Rating]]]]
;; format the given rating matrix into one that pairs each rating with a name
(module+ test
(require rackunit)
(check-equal? (cross '(( a b c)
(x 1 2 3)
(y 4 5 6)))
'((x ((a 1) (b 2) (c 3)))
(y ((a 4) (b 5) (c 6))))))
(define (cross m)
(define names (first m))
(for/list ((rating (rest m)))
(list (first rating) (map list names (rest rating)))))
;; -----------------------------------------------------------------------------
;; for the rest of the functions, consider writing down a signature for each
;; a purpose statement is basically superfluous because the names are so
;; well-chosen
(define (filledOut ratings)
(for/list ([user users]) (or (assoc user ratings) (list user 1.0))))
(define ((normalize userSums) pair)
(match pair
[(list user rating) (list user (/ rating (hash-ref userSums user 0.0)))]))
;; Return the list of (User, Rating) for a particular restaurant
(define (restaurantRatings ratings rest)
(second (assoc rest ratings)))
;; Return a user's rating for a particular restaurant
(define (userRestaurantRating user rest ratings)
(define restRatings (restaurantRatings ratings rest))
(second (assoc user restRatings)))
;; Return (User Restaurant) => VisitCount hash
(define (summedHistory history)
(for/fold ((hsh (hash))) ((3tuple history))
(define rest (second 3tuple))
(for/fold ((hsh hsh)) ((user (third 3tuple)))
(hash-set hsh (list user rest) (+ 1 (hash-ref hsh (list user rest) 0))))))
(define (rankRestaurants hist ratings users restaurants)
(define userCounts (summedHistory hist))
(define (rankUser user rest userCounts)
(define ratingValue (userRestaurantRating user rest ratings))
; Use a minimum of 2 for the count to weaken frequency weighting a little
(define count (+ 2 (hash-ref userCounts (list user rest) 0)))
(* (/ 1.0 count) ratingValue 100.0))
(define (rank rest userCounts users)
(if (empty? users)
0.0
(+ (rankUser (car users) rest userCounts) (rank rest userCounts (cdr users)))))
(for/list ([rest restaurants])
(list rest (rank rest userCounts users))))