2
\$\begingroup\$

I've been trying Advent of Code challenges to learn Clojure. I've successfully completed Day 2, but I feel like my solution is very clumsy, especially the functions for parsing text input.

Instructions:

Imagine there is a sequence of games played, encoded in a text file, one game per line. Each game consists of an id and a sequence of (semicolon-delimited) draws of colored cubes from a small bag. Each draw is encoded as a (comma-delimited) sequence of numbers of cubes drawn per color.

Example line (game):
Game 2: 1 green, 1 blue, 1 red; 11 red, 3 blue; 1 blue, 18 red; 9 red, 1 green; 2 blue, 11 red, 1 green; 1 green, 2 blue, 10 red

The goal is to parse such file for further processing.

My solution:

(defn parse-coloring [coloring]
  (let [tokens (clojure.string/split coloring #" ")
        color (second tokens)
        value (Integer/parseInt (first tokens))]
    { :color color :value value }))

(defn parse-draw [draw]
  (let [colorings (clojure.string/split draw #", ")]
    (map parse-coloring colorings)))

(defn parse-line [line]
  (let [matches (re-matches #"^Game (\d+): (.*)$" line)
        game-id (Integer/parseInt (nth matches 1))
        game (nth matches 2)
        draws (clojure.string/split game #"; ")]
    { :id game-id :draws (map parse-draw draws)}))

(def games
  (let [input (slurp "C:\\Users\\...\\input.txt")
        lines (clojure.string/split-lines input)]
    (map parse-line lines)))

I'm looking for a more simple, succinct, idiomatic-clojure solution.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I think you've nailed it pretty well. The only thing I would do is replace map with mapv everywhere (simpler to avoid lazy lists). Also, as a big believer in TDD, I've added how I would throw in some unit tests to include "live" documentation of what/how each fn works.

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [tupelo.string :as str]))

I start off using my favorite template project and library.

(defn parse-coloring [coloring]
  (let [tokens (clojure.string/split coloring #" ")
        color  (second tokens)
        value  (Integer/parseInt (first tokens))]
    {:color color :value value}))

(verify
  (is= (parse-coloring "1 green")
    {:color "green" :value 1}))

So we see the pattern of a unit test for each small function, so a new reader sees sample input/output.

(defn parse-draw [draw]
  (let [colorings (clojure.string/split draw #", ")]
    (map parse-coloring colorings)))

(verify
  (is= (parse-draw "1 green, 1 blue, 1 red")
    [{:color "green" :value 1}
     {:color "blue" :value 1}
     {:color "red" :value 1}]))

Same for this function...

(defn parse-game [line]
  (let [matches (re-matches #"^Game (\d+): (.*)$" line)
        game-id (Integer/parseInt (nth matches 1))
        game    (nth matches 2)
        draws   (clojure.string/split game #"; ")]
    {:id game-id :draws (map parse-draw draws)}))

(verify
  (is= (parse-game "Game 1: 1 green, 1 blue, 1 red; 11 red, 3 blue")
    {:id    1
     :draws [[{:color "green" :value 1}
              {:color "blue" :value 1}
              {:color "red" :value 1}]
             [{:color "red" :value 11}
              {:color "blue" :value 3}]]}))

and this one. We're almost done.

(defn separate-games
  [text]
  (mapv str/trim (str/split-lines (str/trim text))))

(defn parse-input
  [text]
  (let [game-strs    (separate-games text)
        games-parsed (mapv parse-game game-strs)]
    games-parsed))

I broke this up into 2 functions to make unit testing easier.


(verify
  (let [; throw in some gratuitous leading/trailing newlines to check robustness
        input         "

            Game 1: 1 green, 1 blue, 1 red; 11 red, 3 blue
            Game 2: 9 green, 9 blue, 1 red; 11 red, 3 blue; 1 blue, 18 red

    "
        game-strs     (separate-games input)
        strs-expected ["Game 1: 1 green, 1 blue, 1 red; 11 red, 3 blue"
                       "Game 2: 9 green, 9 blue, 1 red; 11 red, 3 blue; 1 blue, 18 red"]]
    (is= game-strs strs-expected)

So we can verify the line-by-line parsing independently of the other processing.

Now, we verify the whole kit-n-kaboodle for a 2 line file.

    (let [games-parsed (parse-input input)]
      (is= games-parsed
        [{:draws
          [[{:color "green", :value 1}
            {:color "blue", :value 1}
            {:color "red", :value 1}]
           [{:color "red", :value 11} {:color "blue", :value 3}]],
          :id 1}
         {:draws
          [[{:color "green", :value 9}
            {:color "blue", :value 9}
            {:color "red", :value 1}]
           [{:color "red", :value 11} {:color "blue", :value 3}]
           [{:color "blue", :value 1} {:color "red", :value 18}]],
          :id 2}]))))
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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