2
votes

I have started started my journey into using clojure, and got stumped by the first problem I set for myself. I have a text file, that is basically a table nXm rows/columns. The first row is column names and first column is row names. I want to be able to parse this table using clojure and later query table[row][column] and get that value.

  a  b  c
1 7  8  9
2 s  q  r
3 2  7  1

So, how would I consume the above table in clojure? I am not really sure where to start. Can someone get me going in the right direction?

2

2 Answers

6
votes

@Hendekagon's answer is a good way to get the job done, but we can look at a from-scratch implementation. Though probably not the best solution, hopefully the sample design helps get you under way.

If you want to query your structure, in Clojure, you're going to be thinking about maps. Let's take as our goal something that looks like this:

{"1" {"a" "7", "b" "8", "c" "9"},
 "2" {"a" "s", "b" "q", "c" "r"},
 "3" {"a" "2", "b" "7", "c" "1"}}

Here, row names are keys into to maps of column names to table elements. With this structure, we can easily query an element of the table using get-in.

(get-in table ["2" "b"]) ; => "q"

Okay. How do we do it?

Let's pretend for a second we've already read in our file and have it as a string. Then, we need to transform it into our map-of-maps. Our function's going to look something like this:

(defn parse-table
  [raw-table-data]
  ...)

The first step is to pull out all of the important bits of data - the row names, the column names, and the table elements. However, before we can grab them, we need to parse the raw-table-data string into a structure more easily traversed. We'll split the string on newlines, then tokenize the lines on whitespace using a helper function tokens.

(use '[clojure.string :only [split split-lines trim]])

(defn tokens
  [s]
  (-> s trim (split #"\s+")))

(defn parse-table
  [raw-table-data]
  (let [table-data (map tokens (split-lines raw-table-data))]
    ...
)

table-data looks something like this:

 [["a", "b", "c"],
  ["1", "7", "8", "9"],
  ["2", "s", "q", "r"],
  ["3", "2", "7", "1"]]

This makes it easy to get to the good stuff:

(defn parse-table
  [raw-table-data]
  (let [table-data (map tokens (split-lines raw-table-data))
        column-names (first table-data)
        row-names (map first (next table-data))
        contents (map next (next table-data))]
    ...
)

With the data teased out, we just need to stitch it together. An easy way to do this is to build all of our individual mappings of row-to-column-to-elements and then combine them. I'll mention that this isn't the most efficient way, but it's pretty clean.

Creating a helper function pairs that simply sticks elements of two collections side-by-side, we can get a sequence of mappings using a for comprehension.

(defn pairs
  [coll1 coll2]
  (map vector coll1 coll2))

(for [[row-name row-contents] (pairs row-names contents)
      [column-name element] (pairs column-names row-contents)]
  {row-name {column-name element}})

This gives a sequence of maps-to-maps. We just need to merge it into one big map and the function is complete.

(defn parse-table
  [raw-table-data]
  (let [table-data (map tokens (split-lines raw-table-data))
        column-names (first table-data)
        row-names (map first (next table-data))
        contents (map next (next table-data))]
    (apply merge-with merge
      (for [[row-name row-contents] (pairs row-names contents)
            [column-name element] (pairs column-names row-contents)]
        {row-name {column-name element}}))))

Now, we can slurp up a table file and parse it.

(def table
  (->
    "file"
    slurp
    parse-table))

This gets us to our goal.

(println (get-in table ["2" "b"])) ; => "q"
2
votes

Use https://github.com/clojure/data.csv, your file will become a sequence of vectors, each being a row, you can then parse the rows with a function like this:

(defn parse-row [[a b c]]
 [(Integer/parseInt a) (Double/parseDouble b) (str c)])

(note the destructuring in the argument list, this makes it easier to read the column names)

then (map parse-row rows) to get the parsed table

But, another way is to use Incanter, which will turn your csv file into a matrix which will be easier to query.