简体   繁体   中英

Clojure: transform collection into multiple trees

So I've got a database table for comments, and I learned how to use WITH RECURSIVE to return all the comments for a topic, as a tree. However, because it's SQL, it's just returned as a list.

When I execute my query, these are the results I get back ( level is not a column on the table, it's calculated by the query as it gathers results ):

[
 {
  :id "1"
  :parent_id nil,
  :content "This is another top-level comment",
  :level "1",
  :rating 0,
 }
 {
  :id "2"
  :parent_id "1",
  :content "What a comment!",
  :level "1 -> 2",
  :rating 0,
 }
 {
  :id "4"
  :parent_id "2",
  :content "Trying to see how trees work",
  :level "1 -> 2 -> 4",
  :rating 0,
 }
 {
  :id "3"
  :parent_id "2",
  :content "No idea how this will turn out",
  :level "1 -> 2 -> 3",
  :rating 0,
 }
 {
  :id "5"
  :parent_id nil,
  :content "This is a top-level comment",
  :level "5",
  :rating 0,
 }
 {
  :id "9"
  :parent_id "5",
  :content "This is yet another testing comment",
  :level "5 -> 9",
  :rating 0,
 }
 {
  :id "8"
  :parent_id "7",
  :content "It sure is!",
  :level "5 -> 7 -> 8",
  :rating 0,
 }
 {
  :id "7"
  :parent_id "5",
  :content "This!",
  :level "5 -> 7",
  :rating 0,
 }
 {
  :id "6"
  :parent_id "5",
  :content "Hey look at me",
  :level "5 -> 6",
  :rating 0,
 }
]

What I'd like to figure out is how to turn multiple trees, so that I end up with something like so:

1 'This is another top-level comment'
↳ 2 'What a comment!'
  ↳ 4 'Trying to see how trees work'
  ↳ 3 'No idea how this will turn out'
5 'This is a top-level comment'
↳ 9 'This is yet another testing comment'
↳ 7 'This!'
  ↳ 8 'It sure is!'
↳ 6 'Hey look at me'  

Using this function only gets me the first tree ( the one with root node of id 1 ):

(defn make-tree
   ([coll] (let [root (first (remove :parent coll))]
               {:node root :children (make-tree root coll)}))
   ([root coll]
       (for [x coll :when (= (:parent_id x) (:id root))]
           {:node x :children (make-tree x coll)})))

Any ideas or hints on how I could either modify that function, or change what I'm passing in so that I end up with multiple trees?

you can define a make-trees function:

(defn make-trees [id coll] 
  (map 
    (fn [node] {:node node :children (make-trees (node :id) coll)})
    (filter #(= (% :parent_id) id) coll)))

called like this:

(make-trees nil YOUR_COLL)

If you can rely on the :level entry, that can work OK as a source of key sequences to use with assoc-in . You can then do the approach @coredump mentioned with a dedicated root node pretty simply using reduce and a small lambda built on assoc-in :

(defn- key-seq [comment]
  (->> comment
       :level 
       (re-seq (re-pattern "\\d+"))
       (interpose :children))) 

(defn list->forest [comments]
  (vals (reduce (fn [root comment] 
            (assoc-in root (key-seq comment) {:node comment :children {}}))
          {} 
          comments)))

Here I use vals on the result of the reduce to discard the outer root map again, but that's kinda optional.

Edit for regex issues:

If the real data you want to use this on actually has UUIDs in the :level then we'll need to use a more appropriate regex. The above will treat any section of decimal digits as an ID. Using these answers we can collect all the UUIDs in the :level string instead.

I reworked your example data with some random UUIDs in place of the numbers you gave. Using Gajus Kuizinas' regex from the above link I then did these redefinitions:

(ns comment-forest
  (:require [clojure.walk :refer [postwalk]]
            [clojure.pprint :refer [pprint]])
  (:import java.util.UUID))

(defn- key-seq [comment]
  (->> comment
       :level 
       (re-seq (re-pattern "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}"))
       (map #(UUID/fromString %))
       (interpose :children))) 


;;This is just to print the trees with less unnecessary detail
(defn- prune [value]
  (if
    (or 
     (not (map? value))
     (every? (partial contains? value) [:node :children]) 
     (every? #(= UUID (type %)) (keys value))) 
    value
    (select-keys value [:id :content])))

(pprint (map (partial postwalk prune) (list->forest querylist)))

to get output

({:node
  {:content "This is a top-level comment",
   :id "ee9a2671-b47e-40ef-994f-a7b0fa81d717"},
  :children
  {#uuid "f28a159c-de66-4712-9cb8-e1841afeebf6"
   {:node
    {:content "Hey look at me",
     :id "f28a159c-de66-4712-9cb8-e1841afeebf6"},
    :children {}},
   #uuid "d3fccc58-5e59-486d-b784-c54f0e4698b1"
   {:node
    {:content "This!", :id "d3fccc58-5e59-486d-b784-c54f0e4698b1"},
    :children
    {#uuid "e6387f7d-4f29-42c9-a386-7f799341f48f"
     {:node
      {:content "It sure is!",
       :id "e6387f7d-4f29-42c9-a386-7f799341f48f"},
      :children {}}}},
   #uuid "3de27950-7340-49d1-a28e-54ad2e4ea0f1"
   {:node
    {:content "This is yet another testing comment",
     :id "3de27950-7340-49d1-a28e-54ad2e4ea0f1"},
    :children {}}}}
 {:node
  {:content "This is another top-level comment",
   :id "fdc8a8b9-19c7-4fad-963d-2c2ca0bcbe8a"},
  :children
  {#uuid "b17bc5b8-9968-48ce-8ff3-83c8123cd327"
   {:node
    {:content "What a comment!",
     :id "b17bc5b8-9968-48ce-8ff3-83c8123cd327"},
    :children
    {#uuid "1cee5390-e810-49b7-ad10-098bfbe03ab2"
     {:node
      {:content "No idea how this will turn out",
       :id "1cee5390-e810-49b7-ad10-098bfbe03ab2"},
      :children {}}}}}})

Turns out @coredump had the right idea. By having the top-level comments have their parent-id be the topic, then I can just use clojure.zip/zipper to build a tree pretty easily.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM