2

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?

Sean Hagen
  • 742
  • 8
  • 27
  • 2
    You have a forest, not a single tree. You could have a special node where all comments having a `nil` parent could be attached to. – coredump Oct 28 '15 at 09:33
  • Yeah, that occurred to me as well. I'm trying to modify the query and the code so that the top level comments have the post id as the parent id, but the make-tree function doesn't seem to like that. I think this is something I can fix by having the query select the post as well, and having that as the very first node in the tree. – Sean Hagen Oct 29 '15 at 00:24

3 Answers3

0

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)
syllabus
  • 581
  • 3
  • 8
0

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 {}}}}}})
Community
  • 1
  • 1
Magos
  • 2,894
  • 1
  • 16
  • 18
0

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.

Sean Hagen
  • 742
  • 8
  • 27