views:

181

answers:

2

I'm trying to create a table (a work schedule) I have coded previously using python, I think it would be a nice introduction to the Clojure language for me.

I have very little experience in Clojure (or lisp in that matter) and I've done my rounds in google and a good bit of trial and error but can't seem to get my head around this style of coding.

Here is my sample data (will be coming from an sqlite database in the future):

(def smpl2 (ref {"Salaried" 
             [{"John Doe" ["12:00-20:00" nil nil nil "11:00-19:00"]}
              {"Mary Jane" [nil "12:00-20:00" nil nil nil "11:00-19:00"]}]
             "Shift Manager"
             [{"Peter Simpson" ["12:00-20:00" nil nil nil "11:00-19:00"]}
              {"Joe Jones" [nil "12:00-20:00" nil nil nil "11:00-19:00"]}]
             "Other"
             [{"Super Man" ["07:00-16:00" "07:00-16:00" "07:00-16:00" 
                       "07:00-16:00" "07:00-16:00"]}]}))

I was trying to step through this originally using for then moving onto doseq and finally domap (which seems more successful) and dumping the contents into a html table (my original python program outputed this from a sqlite database into an excel spreadsheet using COM).

Here is my attempt (the create-table fn):

(defn html-doc [title & body] 
  (html (doctype "xhtml/transitional") 
    [:html [:head [:title title]] [:body body]])) 

(defn create-table []
  [:h1 "Schedule"]
  [:hr]
  [:table (:style "border: 0; width: 90%")
   [:th "Name"][:th "Mon"][:th "Tue"][:th "Wed"]
   [:th "Thur"][:th "Fri"][:th "Sat"][:th "Sun"]
   [:tr
    (domap [ct @smpl2] 
       [:tr [:td (key ct)]
        (domap [cl (val ct)]
           (domap [c cl]
              [:tr [:td (key c)]]))])
    ]])

(defroutes tstr
  (GET "/" ((html-doc "Sample" create-table)))
  (ANY "*" 404))

That outputs the table with the sections (salaried, manager, etc) and the names in the sections, I just feel like I'm abusing the domap by nesting it too many times as I'll probably need to add more domaps just to get the shift times in their proper columns and the code is getting a 'dirty' feel to it.

I apologize in advance if I'm not including enough information, I don't normally ask for help on coding, also this is my 1st SO question :).

If you know any better approaches to do this or even tips or tricks I should know as a newbie, they are definitely welcome.

Thanks.

+1  A: 

I think you've got a few minor problems with your code. I've made an attempt at correcting them in the code below. Testing this with Compojure 0.3.2, I dare say that it works. (Feel free to point out anything that requires improvement or seems not to work for you, of course.)

(use 'compojure) ; you'd use a ns form normally

;;; I'm not using a ref here; this doesn't change much,
;;; though with a ref / atom / whatever you'd have to take care
;;; to dereference it once per request so as to generate a consistent
;;; (though possibly outdated, of course) view of data;
;;; this doesn't come into play here anyway
(def smpl2 {"Salaried"      [{"John Doe" ["12:00-20:00" nil nil nil "11:00-19:00"]}
                             {"Mary Jane" [nil "12:00-20:00" nil nil nil "11:00-19:00"]}]
            "Shift Manager" [{"Peter Simpson" ["12:00-20:00" nil nil nil "11:00-19:00"]}
                             {"Joe Jones" [nil "12:00-20:00" nil nil nil "11:00-19:00"]}]
            "Other"         [{"Super Man" ["07:00-16:00" "07:00-16:00" "07:00-16:00" 
                                           "07:00-16:00" "07:00-16:00"]}]})

(defn html-doc [title & body] 
  (html (doctype :xhtml-transitional) ; the idiomatic way to insert
                                      ; the xtml/transitional doctype
        [:html
         [:head [:title title]]
         [:body body]]))

(defn create-table []
  (html
   [:h1 "Schedule"]
   [:hr]
   [:table {:style "border: 0; width: 90%;"}
    [:tr
     [:th "Name"][:th "Mon"][:th "Tue"][:th "Wed"]
     [:th "Thur"][:th "Fri"][:th "Sat"][:th "Sun"]]
    (for [category smpl2]
      [:div [:tr [:td (key category)]] ; for returns just one thing per
                                       ; 'iteration', so I'm using a div
                                       ; to package two things together;
                                       ; it could be avoided, so tell me
                                       ; if it's a problem
       (for [people (val category)]
         (for [person people]
           [:tr
            [:td (key person)]
            (for [hours (val person)]
              [:td hours])]))])]))

(defn index-html [request]
  (html-doc "Sample" (create-table)))

(defroutes test-routes
  (GET "/" index-html)
  (ANY "*" 404))

(defserver test-server
  {:port 8080}
  "/*"
  (servlet test-routes))
Michał Marczyk
hmm, liked the use of the div actually, it's a nice way to contain the nested loops (thats the main reason I moved from for in the 1st place, for complained about having too many arguments). Thanks
Kenny164
Ouch, Brian's posting made me realise that I failed to wrap the `:h1`, `:hr` and `:table` in an `html` form in create-table, so I've been throwing them away too... Will fix in a sec. As for the `:div`, I thought it was ok too and actually made for the clearest code, though with some `concat`s / `list*`s etc. you could in principle make do without it.
Michał Marczyk
I think wrapping table rows or cells in divs isn't allowed by the HTML standard. I could be wrong though.
Brian Carper
Apparently that's true -- http://validator.w3.org/ rejects `<table><div><tr><td>asdf</td></tr></div></table>` with error messages pointing to incorrect nesting of tags. Oh well, all the more reason to adopt your cleaner (`list*`) approach. Thanks for the heads-up!
Michał Marczyk
A: 

There's no way to avoid some kind of nested loop. But you don't need domap at all, Compojure is smart enough (sometimes) to expand a seq for you. list and map and for are enough. For example Michał Marczyk's answer, or:

(defn map-tag [tag xs]
  (map (fn [x] [tag x]) xs))

(defn create-table []
  (list
   [:h1 "Schedule"]
   [:hr]
   [:table {:style "border: 0; width: 90%"}
    [:tr (map-tag :th ["Name" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"])]
    [:tr (for [[category people] smpl2]
           (list* [:tr [:td category]]
                  (for [person people
                        [name hours] person]
                    [:tr [:td name] (map-tag :td hours)])))]]))

The "map over a seq and wrap everything in the same tag" pattern is common enough that I like to use a helper function for it sometimes.

Compojure expands one level of seq for you. So you can throw some stuff into a list to get tags to appear sequentially in your HTML output, which I did to get the h1 and hr to show up. In your code, you're just throwing the h1 and hr away.

But note that it only expands one level. When you have a list of lists, or a list of seqs, the outer seq will expand but the inner ones won't.

user> (println (html (list [:div "foo"] (for [x [1 2 3]] [:div x]))))
<div>foo</div>clojure.lang.LazySeq@ea73bbfd

See how the inner seq makes Compojure barf. Look into compojure.html.gen/expand-seqs to see how this works, or change/fix it if you care to.

This can be an issue when you have nested for or a for in a list, since for returns a lazy seq. But you just have to avoid having a seq-in-a-seq. I use list* above. A combination of list and html would work too. There are lots of other ways.

user> (println (html (list* [:div "foo"] (for [x [1 2 3]] [:div x]))))
<div>foo</div><div>1</div><div>2</div><div>3</div>

user> (println (html (list [:div "foo"] (html (for [x [1 2 3]] [:div x])))))
<div>foo</div><div>1</div><div>2</div><div>3</div>
Brian Carper
Wow thanks, I really like the map-tag helper function idea, and you've made me research list* (which I didn't know existed either)
Kenny164