This function was hard to write as a Clojure newbie, and I don't like the result. Can you help me find a better (more readable) way to do it?

(defn split-seq
  "Splits a seq into blocks defined by start-fn and stop-fn.
  Returns a lazy seq of seqs"
  [start-fn stop-fn lines]
  (let [step (fn [c state]
                 (when-let [s (seq c)]
                   (if (stop-fn (first s)) 
                     (cons state (split-seq start-fn stop-fn (rest s) ))
                     (recur (rest s) 
                        (if (start-fn (first s))
                            (cons (first s) state))))))]
    (lazy-seq (step lines '()))))

(defn post-start? [l] (.startsWith l "#ENTRY_START"))
(defn post-end? [l] (.startsWith l "#ENTRY_END"))

(defn split-lines
    "Split a line-seq into entries based on #ENTRY_START and #ENTRY_END."
    (split-seq post-start? post-end? data))

; Test
(def test-data [
    "Header line"
    "entry line 1 "
    "entry line 2"
    "This line should be filtered out"
    "#ENTRY_START Having data here shouldn't make a difference."
    "entry line 1 "
    "entry line 2"
    "This should be gone too"])

(split-lines test-data)

; yields (("entry line 2" "entry line 1 ") ("entry line 2" "entry line 1 "))
; The order of elements doesn't matter in my case because I'm making a map with this data 

With Arthurs help, the final code looks like this:

(defn entry-seq [data] 
    (let [[f r] (split-with #(not (post-end? %)) 
                  (rest (drop-while #(not (post-start? %)) data)))] 
      (when (not-empty f) (lazy-seq (cons f (entry-seq2 r))))))

Still lazy and a lot more readable!


So the idea in this new approach is to write two expressions:

  • one that extracts the next expresstion:
    (take-while #(not= "#ENTRY_END" %) (rest (drop-while #(not= "#ENTRY_START" %) data)))
  • one that extracts everything after the next expression
    (rest (drop-while #(not= "#ENTRY_END" %) (rest (drop-while #(not= "#ENTRY_START" %) data))))

and then wrap them up into a lazy sequence:

first lets expand the test data to include some additional edge cases:

user> (def test-data [
       "Header line"
       "entry line 1 "
       "entry line 2"
       "not part of an entry"
       "also not part of an entry"
       "entry line 1 "
       "entry line 2"

then wrap our two expressions into a function:

user> (defn entry-seq [data] 
        (let [f (take-while #(not= "#ENTRY_END" %) 
                            (rest (drop-while #(not= "#ENTRY_START" %) data))) 
              r (rest (drop-while #(not= "#ENTRY_END" %) 
                            (rest (drop-while #(not= "#ENTRY_START" %) data))))] 
      (when (not-empty f) (lazy-seq (cons f (entry-seq r))))))

and we test it:

user> (take 4  (entry-seq test-data))
(("entry line 1 " "entry line 2") ("entry line 1 " "entry line 2"))
|improve this answer|||||
  • \$\begingroup\$ Thanks. I started out that way, but to filter out anything between or outside delimiters, I have to know too much about the contents. All this function should know about is start-fn and end-fn. \$\endgroup\$ – Robert Jeppesen Oct 30 '12 at 7:44
  • \$\begingroup\$ There can be lines before, after or in-between. I want to get rid of those without knowing what they are. \$\endgroup\$ – Robert Jeppesen Oct 30 '12 at 7:46
  • \$\begingroup\$ I'll fix this answer then ... please stand by! \$\endgroup\$ – Arthur Ulfeldt Oct 30 '12 at 8:01
  • \$\begingroup\$ I updated the test data to show more what I had in mind. \$\endgroup\$ – Robert Jeppesen Oct 30 '12 at 8:05
  • \$\begingroup\$ if they weren't already broken into a sequence of string then this would just be one call to re-seq \$\endgroup\$ – Arthur Ulfeldt Oct 30 '12 at 8:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.