Search code examples
parsingclojure

Clojure idiomatic read file to map


I need to read file to map {:v '[] :f '[]}. I split each line and if first element "v" then I add remaining part to v-array, same for f-array.

Example:

v 1.234 3.234 4.2345234
v 2.234 4.235235 6.2345
f 1 1 1

Expected result:

{:v [("1.234" "3.234" "4.2345234"), ("2.234" "4.235235" "6.2345")]
 :f [("1" "1" "1")]}

My result:

{:v [("2.234" "4.235235" "6.2345")]
 :f [("1" "1" "1")]}

Questions:

  1. How can I fix error? (only last line was added to map)
  2. Can I avoid global variable (model) and side effects?

Code:

(def model
  {:v '[]
   :f '[]})

(defn- file-lines
  [filename]
  (line-seq (io/reader filename)))

(defn- lines-with-data
  [filename]
  (->>
    (file-lines filename)
    (filter not-empty)
    (filter #(not (str/starts-with? % "#")))))

(defn- to-item [data]
  (let [[type & remaining] data]
    (case type
      "v" [:v (conj (:v model) remaining)]
      "f" [:f (conj (:f model) remaining)])))

(defn- fill-model
  [lines]
  (into model
        (for [data lines] (to-item data))))

(defn parse
  [filename]
  (->>
    (lines-with-data filename)
    (map #(str/split % #"\s+"))
    (fill-model)))

Solution

  • You seem to be dropping the changing state of your model, instead appending the data of all lines to the original model with two empty vectors. You can keep the state of your model as you read the file, for example using reduce:

    (defn- add-item [m data]
      (let [[type & remaining] data]
        ;; Updates the model `m` by appending data under the given key.
        ;; In the line below, `(keyword type)` will be :v or :f depending on `type`.
        ;; Returns the updated model.
        (update m (keyword type) conj remaining)))
    
    (defn fill-model [lines]
      ;; Applies `add-item` to the original model and each line of the file
      (reduce add-item model lines)))