Mars Rovers III: The Solutions — Clojure

This is part of the Mars Rovers series.

Having now introduced and analyzed the problem, let’s see the Clojure code.

I clustered my functions according to which solutions they’re used in. So, for example, parse-input is used by all three solutions, but ensure-no-collisions is used only by the third. This makes clearer how much extra code is needed for each additional solution.

The code

;;; rover/core.clj --- Mars Rovers Problem
;;
;; SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful>
;; SPDX-License-Identifier: EPL-2.0
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Code
;;;; Common to all solutions
;;;;; Namespace

(ns ^{:doc    "My solutions to the Mars Rovers Problem."
      :author "flandrew" :version "0.1.0" :updated "2026-04-17"}
    rover.core
  (:require (clojure [string :as s]
                     [zip    :as z])))


;;;;; Parse input

(defn- rover-seq->map
  "Given a sequence of two strings of rover data, convert it to map."
  [[s1 s2]]
  (let [[x y h cs] (s/split (str s1 " " s2) #" +")]
    {:id nil :x (Integer. x) :y (Integer. y) :h h :cs cs}))

(defn parse-input
  "Parse input string into a vector.
  Vector's first two items are the given pairs of min and max coordinates.
  The remaining items are maps, each of which for a rover."
  [s]
  (if (s/blank? s) ""           ; this⬎ for present but empty commands
      (let [[x-y-max & rs] (->> (str s " ")  s/split-lines  (map s/trim))
            rovers         (->> rs  (partition 2)  (map rover-seq->map)
                                (map-indexed #(assoc %2 :id (inc %1))))
            [xM yM]        (map #(Integer. %) (s/split x-y-max #" +"))]
        (into [[0 xM] [0 yM]] rovers))))


;;;;; Make maps

(defn- deltas
  "Given a heading and a change, return vector of deltas.
  The vector corresponds to the changes in x, y, and heading."
  [h c]
  (case c
    "R" [0 0  1]
    "L" [0 0 -1]
    "M" (case h
          "N" [ 0  1  0]
          "E" [ 1  0  0]
          "S" [ 0 -1  0]
          "W" [-1  0  0])))

(defn- new-head
  "Given a heading and a delta integer, return new heading."
  [h i]
  (->> "NESW" cycle
       (drop-while #(not= (str %) h))
       (drop (+ 4 i))   first   str))

(defn- rover-map-next
  "Given a rover map, return map of after-the-next-change.
  If there's no next change, return the very map."
  [{:keys [id x y h cs] :as m}]
  (if-not (empty? cs)
    (let [next-change (-> cs  first  str)
          [Δx Δy Δh]  (deltas h next-change)]
      {:id id
       :x  (+ x Δx)
       :y  (+ y Δy)
       :h  (new-head h Δh)
       :cs (subs cs 1)})
    m))

(defn rover-map-all
  "Given a rover initial map, list all its maps, up to the last change."
  [m]
  (into [] (take (-> m  :cs  count  inc)
                 (iterate rover-map-next m))))


;;;;; Make output string

(defn- rover-map->str
  "Given a rover map, convert it to a string line: x y h."
  [{:keys [x y h]}]
  (format "%s %s %s" x y h))

(defn- rover-maps-all->str
  "Convert to output string a collection of map collections."
  [coll]
  (->> coll
       (mapv (comp rover-map->str peek))
       (s/join "\n")))


;;;; Used only by Solution 1
;;;;; Output

(defn output-1
  "Given input string, produce output string."
  [s]
  (let [[_ _ & rovers] (parse-input s)]
    (->> rovers
         (mapv rover-map-all)
         (rover-maps-all->str))))


;;;; Common to Solutions 2 and 3
;;;;; Validate

(defn- validations
  "Given xrange and yrange, create validations map."
  [[xmin xmax] [ymin ymax]]
  {:id pos-int?
   :x  (every-pred number? #(<= xmin % xmax))
   :y  (every-pred number? #(<= ymin % ymax))
   :h  (every-pred string? #(s/includes? "NESW" %))
   :cs (every-pred string? #(re-seq #"^[RLM]*$" %))})

(defn- valid-pair?
  "Given validations map and a pair, validate the pair."
  [vmap [k v]]
  ((k vmap) v))

(defn- valid-map?
  "Given validations map and a map, validate the map."
  [vmap m]
  (every? (partial valid-pair? vmap) m))

(defn- valid-map
  "Return map if valid; throw exception otherwise."
  [vmap m]
  (if (valid-map? vmap m)
    m (throw (ex-info "Invalid rover map" m))))

(defn- ensure-valid
  "Ensure that all rover maps are valid.

  Each item of collection represents a rover. Each of these items is itself a
  collection, whose items are all the maps of the rover, from first to last.

  A rover is valid if all of its maps are valid.
  If all rovers are valid, return coll. Otherwise, throw exception."
  [vmap coll]
  (mapv (partial mapv (partial valid-map vmap)) coll))


;;;; Used only by Solution 2
;;;;; Output with validation of each map

(defn output-2
  "Given input string, produce output string.
  Throw an exception if any map is invalid."
  [s]
  (let [[xr yr & rovers] (parse-input s)
        vmap             (validations xr yr)]
    (->> rovers
         (mapv rover-map-all)
         (ensure-valid vmap)
         (rover-maps-all->str))))


;;;; Used only by Solution 3
;;;;; Look for collisions

(def xy
  "Extract position from map."
  (juxt :x :y))

(defn- assert-no-collisions
  "Assert that a moving rover won't collide.

  Args are:
  - n (node), a map collection: all maps of a rover, from first to last
  - two collections of map collections:
    - l (left):  previous rovers
    - r (right): next rovers

  A moving rover is collision-free if the last position of each rover before
  it and the first position of each rover after it are not in its path.

  If a collision is detected, throw exception. Otherwise return nil."
  [l n r]
  (let [path (map xy n)
        left (map (comp xy last)  l)
        rite (map (comp xy first) r)
        test `[~@left nil ~@rite]]
    (dotimes [i (count test)]
      (let [p (get test i)]
        (when (some #{p} path)
          (let [id1 (inc (count left))
                id2 (inc i)]
            (throw (ex-info "Collision detected!"
                            {:moving {:id id1 :path path}
                             :into   {:id id2 :at p}}))))))))

(defn- ensure-no-collisions
  "Ensure that there wouldn't be rover collisions.

  Each item of collection represents a rover. Each of these items is itself a
  collection, whose items are all the maps of the rover, from first to last.

  For each of these rovers, assert that there're no collisions.

  If all rovers are collision-free, return coll. Otherwise, throw exception."
  [coll]
  (loop [loc (-> coll  z/vector-zip  z/down)]
    (if loc
      (recur (let [[l n r] ((juxt z/lefts z/node z/rights) loc)]
               (assert-no-collisions l n r)
               (z/right loc)))
      coll)))


;;;;; Output with validation of each map, plus no collisions

(defn output-3
  "Given input string, produce output string.
  Throw an exception if any map is invalid or if there're any collisions."
  [s]
  (let [[xr yr & rovers] (parse-input s)
        vmap             (validations xr yr)]
    (->> rovers
         (mapv rover-map-all)
         (ensure-valid vmap)
         (ensure-no-collisions)
         (rover-maps-all->str))))


;;; Local Variables:
;;  coding:                     utf-8
;;  indent-tabs-mode:           nil
;;  sentence-end-double-space:  nil
;;  outline-regexp:             ";;;;* "
;;  End:

;;; rover/core.clj ends here

The tests

;;; rover/core_test.clj --- Mars Rovers Problem: Tests


;;; Code
;;;; Namespace

(ns rover.core-test
  (:require [clojure.test :refer :all]
            [rover.core   :refer :all]))


;;;; Tests
;;;;; All three solutions must parse the sample input correctly

(def good-input   "Given valid input"
  "5 5
  1 2 N
  LMLMLMLMM
  3 3 E
  MMRMMRMRRM")

(def good-output  "Given expected output"
  "1 3 N\n5 1 E")

(deftest test-good-input
  (testing "Good input: given."
    (are [f] (= (f good-input) good-output)
      output-1
      output-2
      output-3)))


;;;;; All three solutions must throw an error at this bad input


(def bad-input-1  "Has invalid change Z"
  "5 5
  1 2 N
  LMZZLM
  3 3 E
  MMMMMR")

(deftest test-bad-input-1
  (testing "Bad input: bogus command Z"
    (are [f] (thrown-with-msg?
              IllegalArgumentException #"No matching clause: Z"
              (f bad-input-1))
      output-1
      output-2
      output-3)))


;;;;; The 2nd and 3rd solutions must be able to catch rovers going off-grid


(def bad-input-2a "Goes to :y -1"
  "5 5
  1 2 N
  LLMMMMRR
  3 3 E
  MMRMMRMRRM")

(def bad-input-2b "Goes to :x 6"
  "5 5
  1 2 N
  LMLMLMLMM
  3 3 E
  MMMMMR")

(deftest test-bad-input-2
  (testing "Bad input: rover ends up out of bounds"
    (are [in out] (= (output-1 in) out)
      bad-input-2a "1 -2 N\n5 1 E"
      bad-input-2b "1 3 N\n8 3 S")

    (are [f in] (thrown-with-msg?
                 clojure.lang.ExceptionInfo #"Invalid rover map"
                 (f in))
      output-2 bad-input-2a
      output-2 bad-input-2b
      output-3 bad-input-2a
      output-3 bad-input-2b)))


;;;;; The 3rd solution must be able to catch rover collisions

(def bad-input-3a "Rover #2 collides with #1 at 1,3"
  "5 5
  1 2 N
  LMLMLMLMM
  3 3 W
  MMRMMRMRRM")

(def bad-input-3b "Rover #2 collides with #4 at 5,2"
  "5 5
  1 2 N
  LMLMLMLMM
  3 3 E
  MMRMMRMRRM
  0 0 N
  MMMMRM
  5 2 W
  MMMLM")

(deftest test-bad-input-3
  (testing "Bad input: a collision would happen"
    (are [f] (= (f bad-input-3a) "1 3 N\n1 5 W")
      output-1
      output-2)

    (are [f] (= (f bad-input-3b) "1 3 N\n5 1 E\n1 4 E\n2 1 S")
      output-1
      output-2)

    (are [in] (thrown-with-msg?
               clojure.lang.ExceptionInfo #"Collision detected!"
               (output-3 in))
      bad-input-3a
      bad-input-3b)))


;;;; Running them

(comment
  (run-tests) => {:test 4, :pass 18, :fail 0, :error 0, :type :summary})


;;; Local Variables:
;;  coding:                     utf-8
;;  indent-tabs-mode:           nil
;;  sentence-end-double-space:  nil
;;  outline-regexp:             ";;;;* "
;;  End:

;;; rover/core_test.clj ends here

See next

Mars Rovers IV: The Solutions — Emacs Lisp

📆 2026-W16-6📆 2026-04-18