从嵌套的地图(和向量)创建HTML表格

8

我正在尝试创建一个表格(工作时间表),之前我已经使用Python编码了它,我认为这将是我学习Clojure语言的很好入门。

我对Clojure(或lisp)几乎没有任何经验,我在Google上搜索并做了很多试错,但似乎无法理解这种编码风格。

以下是我的示例数据(将来将来自sqlite数据库):

(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"]}]}))

我原本尝试使用for,然后转向使用doseq,最终采用了更成功的domap方法,并将内容转储到HTML表格中(我的原始Python程序通过COM从SQLite数据库输出到Excel电子表格中)。
以下是我的尝试(create-table函数):
(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))

那个输出表格的部分(薪资、经理等)和各部分中的姓名,我觉得我过多地嵌套了domap,可能需要添加更多的domap才能将班次时间放在正确的列中,代码感觉变得“dirty”。如果您知道更好的方法来完成这个任务,或者作为新手应该知道的技巧和提示,那么请一定告诉我。谢谢。

将来,您不应将可回答的问题标记为社区维基。这会破坏我们的声誉积累游戏。;-) - Michał Marczyk
抱歉,我没有意识到它会丢弃声望系统。我只是认为它意味着我的问题可以编辑(其实也不需要)。但无论如何,感谢您的回答,我从您的帖子中学到了很多。 :) - Kenny164
感谢您的帮助。我已经成功地显示了数据,只是还没有决定如何从SQL数据库创建那个混乱的哈希映射(就像我的Python版本),或者使用另一种方法,比如nosql(我没有任何经验)... 我应该在这里问吗,还是开一个新问题? - Kenny164
2个回答

5

无法避免某种形式的嵌套循环。 但是你根本不需要使用 domap,Compojure 足够聪明(有时候)可以自动为你展开序列。只需使用 listmapfor 即可。例如 Michał Marczyk 的答案,或者:

(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)])))]]))

“对序列进行映射并将所有内容包装在相同的标记中”的模式很常见,有时我喜欢使用辅助函数来处理它。

Compojure会为您扩展一个seq级别。因此,您可以将一些内容放入list中,以使标记按顺序出现在HTML输出中,就像我使用h1和hr一样。在您的代码中,您只是将h1和hr丢弃了。

但请注意,它只会扩展一个级别。当您有一个列表的列表或序列的列表时,外部序列会展开,但内部序列不会。

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

观察内部序列如何使Compojure出现问题。查看compojure.html.gen/expand-seqs以了解其工作原理,或者如果您愿意的话,请更改/修复它。

当您嵌套使用for或在list中使用for时,这可能是一个问题,因为for返回一个惰性序列。但是只需避免在序列中有另一个序列即可。我在上面使用了list*listhtml的组合也可以起作用。还有许多其他方法。

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>

哇,谢谢,我真的很喜欢map-tag辅助函数的想法,你让我去研究了list*(我以前也不知道有这个函数)。 - Kenny164

3

我认为你的代码存在一些小问题。我尝试在下面的代码中进行了更正。使用Compojure 0.3.2测试,我敢说它可以工作。(当然,如果有需要改进或似乎对你不起作用的地方,请随时指出。)

(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))

嗯,实际上我喜欢使用div,这是一种很好的方式来包含嵌套循环(这也是我从一开始就放弃for的主要原因,因为它抱怨有太多参数)。谢谢。 - Kenny164
哎呀,Brian的帖子让我意识到我没有在create-table中用html表单包装:h1:hr:table,所以我一直在抛弃它们...马上修复。至于:div,我认为它也没问题,实际上可以使代码更清晰,尽管你原则上可以通过一些concat/list*等方法来避免使用它。 - Michał Marczyk
我认为在HTML标准中不允许将表格行或单元格包装在div中。虽然我可能是错的。 - Brian Carper
显然这是真的 - http://validator.w3.org/ 拒绝 <table><div><tr><td>asdf</td></tr></div></table> 并指出标记嵌套不正确的错误消息。好吧,这就更有理由采用更简洁 (list*) 的方法了。感谢您的提示! - Michał Marczyk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接