Tuesday, 13 November 2012

Just the Squares

Today's exercise is to take a string that contains numbers separated by commas, like this:

"3,4,5,6,7,8,9"

and return the same except containing only the numbers that are perfect squares, which in this case would be

"4,9"

So the first step is to break up that string into the individual numbers.  In the Clojure String library we get the split function, which takes your string and a regular expression that determines what part of the string is to be used to split.  We're using just about the simplest possible option, just chopping through the commas.  A function to do this would look so:

(defn split-string [s]
(clojure.string/split s #","))

This gives us a sequence of the separated strings:-

user=> (split-string "4,5,45,6,7,67")
["4" "5" "45" "6" "7" "67"]

When we want to join these back again to restore the single string we have split's partner join, so:-

(defn join-string [ss]
(clojure.string/join "," ss))

We just specify the string "," to be added between the strings in our sequence.

user=> (join-string ["4" "5" "45" "6" "7" "67"])
"4,5,45,6,7,67"

Now we will want to get the integer values of these strings.  So we dip into the Java class Integer and bring back the method parseInt.  I love it when you can step between languages and they play nicely together.  In the system I use professionally I can step from C to assembler and back again. It's similar in that Clojure has Java hiding inside and C has assembler hiding inside. Anyway to change a string to an integer I can summon the Integer class and call the parseInt method on it, like this:-

user=> (. Integer parseInt "123")
123

That's a macro but it can become a function quite easily:-

user=> (#(. Integer parseInt %) "123")
123

And we will want to convert these integers back into strings: the Clojure function str will do this:-

user=> (str 3)
"3"

We will want a filter function that will decide whether a number is a square.  For this let's dip into Java again and get the square root method from the Math class:-

user=> (. Math sqrt 2.0)
1.4142135623730951

So if I take the integer part of the square root (the Clojure function int will give this) and square this and compare with the original number that indicates whether it is was a square number. Along these lines:-

(defn is-square [n]
(let [root (int (. Math sqrt n))]
(= (* root root) n)))

user=> (is-square 100)
true
user=> (is-square 101)
false
user=> (is-square 99)
false

OK so putting the parts together.  The first version of my function just-squares will open up the string into the individual numbers and then put them together again:-

(def just-squares
(fn [s]
(join-string (split-string s))))

user=> (def s "4,5,6,7,8,9")
#'user/s
user=> (just-squares s)
"4,5,6,7,8,9"

So far so good.  Now convert them to integers and back again.

(def just-squares
(fn [s]
(join-string
(map str
(map #(. Integer parseInt %) (split-string s))))))

user=> (just-squares s)
"4,5,6,7,8,9"

Still works.  Now add that filter to allow only the square ones:-

(def just-squares
(fn [s]
(join-string
(map str
(filter (fn [n] (let [root (int (. Math sqrt n))](= (* root root) n)))
(map #(. Integer parseInt %) (split-string s)))))))

user=> (just-squares s)
"4,9"

Ok so this works.  Now to make this code complete in itself I should fill in the definitions of those functions split-string and join-string, so we get this:-

(def just-squares
(fn [s]
(#(clojure.string/join "," %)
(map str
(filter (fn [n] (let [root (int (. Math sqrt n))](= (* root root) n)))
(map #(. Integer parseInt %) (clojure.string/split s #",")))))))

Hmm.  Well, that works on the 4Clojure page but I don't like it.  Maybe there's scope to use the -> macro to link the sections together instead of nesting them. Or use more lets to give the sections some names.

Also a more idiomatic name for is-square would have been square?.