I'm working on my first Clojure project, which is a parser. Since it's my first project I'm unsure of many aspects, from my general approach to solving this problem to the small implementation details. I also don't know anyone proficient in Clojure so any guidance is welcome.
In higher terms, my implementation will take a string (which is one message to be parsed) and break it into fields. Each field it will apply some function to (often a dictionary lookup), and finally return a dictionary of results.
(use 'clojure.string)
;; This defines the breakdown of each line in the file. The line contains an entry and start with something
;; like "1AXYZ ". In this case the first two characters ("1A") are the "Record Type" and the next four
;; ("XYZ ") are the "Entering Firm". This data structure holds that information.
(def parser
{:shared '( ; These are the entries shared amongst all types of message
;; Field-Name num-chars
Record-Type 2
Entering-Firm 4
Clearing-Number 4
Symbol 11
Lot-Indicator 1
;;...
)
:1A '( ; this is one type of message, indicated with record-type=1A
;; Field-Name num-chars
Filler 9
Poss-Dupe 1
Booth-Booked-Indicator
Order-Time 6
Order-Date 8
;; ...
)
}
)
;; if no implementation is found for a field, just return a map to its contents.
(defn default [header x]
{(str header) (trim (join x))})
;; most of the fields that dont use the default function will just be a dictionary lookup
(defn Poss-Dupe [[x]] (default 'Poss-Dupe ({\1 "Odd Lot" \D "Rule 902" \E "Poss Dupe Rule 902"
\F "Auto Execution" \G "Poss Dupe of Auto-Execution"
\H "Non-auto Execution" \I "Poss Dupe of Non-auto Execution"
\0 "" \space ""}
x (str "ERROR: " x))))
(defn Booth-Booked-Indicator [[x]] (default 'Booth-Booked-Indicator ({\0 "Post" \1 "Booth" \2 "Booked"} x (str "ERROR: " x))))
(defn Order-Time [x] (default 'Order-Time (->> x (partition 2) (map join) (join ":") ))) ; "HHMMSS" -> "HH:MM:SS"
(defn Order-Date [x] (default 'Order-Date
(join "-" (map join (list (take 4 x) (take 2 (drop 4 x)) (drop 6 x)))))) ; "YYYYMMDD"->"YYYY-MM-DD"
(defn Filler [x] nil)
(defn try-call [f subseq]
(try
((eval f) subseq)
(catch Exception e ; f not defined -> call default
(default f subseq))))
;; Where the recursion happens
(defn grab-call [[f n & rest-instructions] line]
(let [[field rest-line] (split-at n line)]
(if (or (empty? line) (not f))
nil
(merge (try-call f field) (grab-call rest-instructions rest-line)))))
;;; Entry point
(defn parse-line [line]
(grab-call (concat (:shared parser)
((keyword (subs line 0 2)) parser))
line ))
If you care to know the particulars of what is being parsed, that can be found at NYSE MRO specs.