Sfoglia il codice sorgente

shopping_list: Rewrite, with better api & features

I explicitely spelled out the notion that you always have a grouping
method (for all articles) and a sorting method (within those groups).
You can now combine them as you wish with a nice combinator function.
Another features is that you can reverse both methods independently via
flags which is a nice thing.

Sorting/displaying prices and weights correctly is also handled, but I
am not yet sure whether I like the implementation. Another alternative
would be to keep all conversion out of the core api, but then the
rendering code would have to know about the current grouping method, so
there are advantages to it. This would be a great topic to talk about,
but then so many topics are when you're new to a language.

One thing that is missing as I am not sure whether I want that feature
is to sort groups individually. Technically it would be simple, but
would probably be confusing in a ui if the sorting method is not obvious.

All in all, it took me far longer than I expected, in part because I
realized so late that you would need two algorithms (group & sort). The
other part is that I hadn't really thought about handling units and how
to compare them. I solved it with a `defrecord` for now, but that is
probably going to change.
Lucas Stadler 12 anni fa
parent
commit
40f7914054

+ 0 - 88
clj/shopping_list/shopping_list.clj

@ -1,88 +0,0 @@
1
(ns shopping-list
2
  (:use [clojure.core :as core])
3
  (:use [clojure.repl :as repl]))
4
5
(defn sort-map
6
  ([m] (sort-map compare m))
7
  ([c m] (apply sorted-map-by c (reduce #(into %1 %2) (seq m)))))
8
9
(def example-article
10
  (sort-map
11
    {:all-articles
12
       [{:name "spinach", :price 3.1, :weight 0.5, :category "vegetables"}
13
        {:name "apple", :price 2.3, :weight 1.34, :category "vegetables"}
14
        {:name "onion bread", :price 1.79, :weight 1.1, :category "baked goods"}
15
        {:name "soy pudding", :price 1.99, :weight 0.5, :category "sweeties"}
16
        {:name "gum hearts", :price 2.49, :weight 0.4, :category "sweeties"}]}))
17
18
(defn create []
19
  {:known_articles []
20
   :articles []
21
   :sort-method :default})
22
23
(defn mmap [f m]
24
  (into (sorted-map) (map (fn [[key val]]
25
                  [key (f val)])
26
                m)))
27
28
(defn sort-by [key-or-keyfn grouped-articles]
29
  (mmap #(core/sort-by key-or-keyfn %) grouped-articles))
30
31
(def sort-by-category
32
  (partial sort-by :category))
33
34
(sort-by-category {:expensive-things
35
                     [{:category "a" :n 1}, {:category "b" :n 2} {:category "c" :n 3}]
36
                   :cheap-things
37
                     [{:category "d" :n 4}, {:category "a" :n 5}]})
38
(defn regroup-by [key-or-keyfn grouped-articles]
39
  (sort-map
40
    (group-by key-or-keyfn (flatten (vals grouped-articles)))))
41
42
(defn first-char-of-name [article]
43
  (first (:name article)))
44
45
(def group-by-name
46
  (partial mmap #(group-by first-char-of-name)))
47
48
(def regroup-by-name
49
  (partial regroup-by first-char-of-name))
50
51
(def group-by-category
52
  (partial mmap #(group-by :category %)))
53
54
(def regroup-by-category
55
  (partial regroup-by :category))
56
57
(defn categorize-by-price [article]
58
  (let [price (:price article)]
59
    (condp >= price ; (> x price)
60
      0.5 "<0.5$"
61
      1 "<1$"
62
      2.5 "<2.5$"
63
      5 "<5$"
64
      10 "<10$"
65
      (Integer/MAX_VALUE) ">10$")))
66
67
(def group-by-price
68
  (partial mmap #(group-by categorize-by-price %)))
69
70
(def regroup-by-price
71
  (partial regroup-by categorize-by-price))
72
73
(defn categorize-by-weight [article]
74
  (let [weight (:weight article)]
75
    (condp >= weight
76
      0.1 "<100g"
77
      0.25 "<250g"
78
      0.5 "<500g"
79
      1 "<1kg"
80
      2.5 "<2.5kg"
81
      5 "<5kg"
82
      (Integer/MAX_VALUE) ">5kg")))
83
84
(def group-by-weight
85
  (partial mmap #(group-by categorize-by-weight %)))
86
87
(def regroup-by-weight
88
  (partial regroup-by categorize-by-weight))

+ 80 - 0
clj/shopping_list/shopping_list/core.clj

@ -0,0 +1,80 @@
1
(ns shopping-list.core)
2
3
(defn map-map [f m]
4
  (into (empty m) (map (fn [[key val]]
5
                  [key (f val)])
6
                m)))
7
8
(defn sort-map
9
  ([m] (sort-map compare m))
10
  ([c m] (apply sorted-map-by c (reduce #(into %1 %2) (seq m)))))
11
12
(def default
13
  {:group (constantly :all-articles)
14
   :sort  :name})
15
16
(def category
17
  {:group :category
18
   :sort  :category})
19
20
(defn categorize-number [categories n]
21
  (last (filter #(< % n) (concat [0] categories [Double/POSITIVE_INFINITY]))))
22
23
(defrecord PriceInEuros [price-in-cents]
24
  Comparable
25
  (compareTo [this other-price]
26
             (compare price-in-cents (:price-in-cents other-price)))
27
  Object
28
  (toString [this]
29
            (if (< price-in-cents 100)
30
                   (str price-in-cents "ct")
31
                   (str (float (/ price-in-cents 100)) "€"))))
32
(defn pretty-price [price-in-cents] (str (PriceInEuros. price-in-cents)))
33
34
(def price
35
  {:group (comp #(PriceInEuros. %) #(categorize-number [50 100 250 500 1000] %) :price)
36
   :sort  :price})
37
38
(defrecord MetricWeight [weight-in-grams]
39
  Comparable
40
  (compareTo [this other-weight]
41
             (compare weight-in-grams (:weight-in-grams other-weight)))
42
  Object
43
  (toString [this]
44
            (if (< weight-in-grams 1000)
45
              (str weight-in-grams "g")
46
              (str (float (/ weight-in-grams 1000)) "kg"))))
47
(defn pretty-weight [price-in-grams] (str (MetricWeight. price-in-grams)))
48
49
(def weight
50
  {:group (comp #(MetricWeight. %) #(categorize-number [100 250 500 1000 2500 5000] %) :weight)
51
   :sort  :weight})
52
53
(def alphabet
54
  {:group (comp first :name)
55
   :sort  :name})
56
57
(defn ungroup [grouped-articles]
58
  (flatten (vals grouped-articles)))
59
60
(defn regroup [{:keys [group sort reverse-group reverse-sort]} grouped-articles]
61
  (let [articles   (ungroup grouped-articles)
62
        compare-fn (fn [reverse?] (if reverse? (comp - compare) compare))]
63
    (map-map #(sort-by sort (compare-fn reverse-sort) %)
64
             (sort-map (compare-fn reverse-group) (group-by group articles)))))
65
66
(defn combine [{group :group} {sort :sort}]
67
  {:group group
68
   :sort  sort})
69
70
(def example-articles
71
  (regroup default
72
    {:all-articles
73
       [{:name "spinach", :price 310, :weight 500, :category "vegetables"}
74
        {:name "apple", :price 230, :weight 1340, :category "vegetables"}
75
        {:name "onion bread", :price 179, :weight 1100, :category "baked goods"}
76
        {:name "soy pudding", :price 199, :weight 500, :category "sweeties"}
77
        {:name "gum hearts", :price 249, :weight 400, :category "sweeties"}]}))
78
79
(regroup (assoc alphabet :reverse-group false :reverse-sort true) example-articles)
80
(map str (keys (regroup price example-articles)))

+ 3 - 3
clj/shopping_list/shopping_list/server.clj

@ -1,5 +1,5 @@
1 1
(ns shopping-list.server
2
  (:require [shopping-list :as sl])
2
  (:require [shopping-list.core :as sl])
3 3
  (:use ring.util.response)
4 4
  (:use compojure.core)
5 5
  (:require [compojure.route :as route])
@ -13,7 +13,7 @@
13 13
     [:ul
14 14
      (for [item group-items]
15 15
        (let [{:keys [name price weight category]} item]
16
          [:li (str name)]))]]))
16
          [:li (str name " (" (sl/pretty-price price) ", " (sl/pretty-weight weight) ", " category ")")]))]]))
17 17
18 18
(defn index []
19 19
  (html
@ -23,7 +23,7 @@
23 23
     [:div {:id "main"}
24 24
      [:h1 "Hi there"]
25 25
      [:p "Mhh yeah, what's up?"]
26
      (render-groups (sl/sort-map (comp identity compare) (sl/regroup-by-name sl/example-articles)))]]]))
26
      (render-groups (sl/regroup (sl/combine sl/category sl/weight) sl/example-articles))]]]))
27 27
28 28
(defroutes shopping-list
29 29
  (GET "/" [] (index))