;;; -*- mode: Clojure; coding: utf-8 -*-
;;; Solution by Luis Sergio Oliveira for the Alien Numbers exercise from
;;; Programming Praxis
;;; See http://programmingpraxis.com/2010/09/24/alien-numbers/
;;; Run from the shell as so:
;;; ~/programacao/programmingpraxis$ ../clojure/clj-dev.sh alien-numbers.clj <<-END
;;; > 2
;;; > abc abcd 0123456789
;;; > ([}} {([}? 0123456
;;; > END
;;;
;;; Or execute its tests like so:
;;; (clojure.test/run-tests 'euluis.aliennum)
(ns euluis.aliennum
(:use [clojure.test :only [is are testing deftest]]))
^L
;;; Conversion given strings alien-num source-lang target-lang.
;;; See convert-num fn which is the entry point.
(defn decimal-to-lang
"Converts decimal-num to the equivalent number in lang as a string.
The algorithm was a translation of the algorithm by Rodrigo Menezes
in C# that he posted in the programming praxis site."
[decimal-num lang]
(let [lang-radix (count lang)]
(loop [decimal-value decimal-num lang-num []]
(if (>= 0 decimal-value)
(apply str lang-num)
(recur (int (/ decimal-value lang-radix))
(concat [(nth lang (mod decimal-value lang-radix))] lang-num))))))
(deftest decimal-to-lang-test
(testing "that given a decimal-num and a target lang, decimal-to-lang fn
converts it correctly to expected-num"
(are [expected-num decimal-num lang]
(= expected-num (decimal-to-lang decimal-num lang))
"1100120" 987 "012"
"33123" 987 "0123"
"3" 3 "0123456789")))
(defn digit-index
[digit lang]
(loop [i 0]
(if (= digit (nth lang i))
i
(recur (inc i)))))
(defn lang-to-decimal
[alien-num lang]
(let [radix (count lang)
ralien-num (reverse alien-num)]
(loop [i 0 decimal-num 0 product 1]
(if (= i (count ralien-num))
decimal-num
(recur (inc i) (+ decimal-num (* (digit-index (nth ralien-num i) lang) product))
(* radix product))))))
(deftest lang-to-decimal-test
(testing "that given an alien-num and a lang we can transform it to a decimal number"
(are [expected-num alien-num lang]
(= expected-num (lang-to-decimal alien-num lang))
987 "1100120" "012"
987 "33123" "0123"
3 "11" "01")))
(defn convert-num
"Convert alien-num which is in source-lang into the same number in target-lang"
[alien-num source-lang target-lang]
(decimal-to-lang (lang-to-decimal alien-num source-lang) target-lang))
(deftest convert-num-tests
(testing "convert-num tests based on 2 to 36 radix numerals"
(are [expected-num alien-num source-lang target-lang]
(= expected-num
(convert-num alien-num source-lang target-lang))
;; test: 987 0123456789 012 -> should be 1100120
"1100120"
"987" "0123456789" "012"
"987"
"1100120" "012" "0123456789"
"33123"
"987" "0123456789" "0123"
"//-*/"
"987" "0123456789" "+-*/"
"RF"
"987" "0123456789" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"35"
"Z" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789"
"cj"
"\"!" "!\"#$%&'()*+,-./:;?@[\\]^_`{|}~" "abcdefghij"
"3"
"11" "01" "0123456789")))
^L
;;; Process the I/O
;;; N
;;; "<alien number>" "<sl0>..<slN>" "<tl0>..<tlM>"
;; production code to retrieve the lines containing the numbers to convert
(defn my-read-line
([] (read-line))
([stream]
(binding [*in* stream]
(read-line))))
(defn get-lines
"Processes stream, returning a seq of lines in the order they are given
and each line is guaranteed to be a string."
[stream]
(let [lines-number (Integer. (my-read-line stream))
lines (loop [n lines-number lines '()]
(if (>= 0 n)
lines
(recur (dec n) (conj lines (my-read-line stream)))))]
;; what should be the behavior when we inconsistent number and lines?
;; I'll go for beneficent behavior...
(reverse (filter #(not (nil? %)) lines))))
;; test code to retrieve the lines containing the numbers to convert
(defn stream-str
"Returns an open BufferedReader from str."
[str]
(java.io.BufferedReader. (java.io.StringReader. str)))
(defn get-lines-str
"Given str, creates and opens a stream from this and hands over to get-lines."
[str]
(with-open [s (stream-str str)]
(get-lines s)))
(deftest get-lines-tests
(testing "get-lines for happy path test cases"
(are [expected-lines in-str]
(= expected-lines (get-lines-str in-str))
["123 abc" "4 5 7"] "2\n123 abc\n4 5 7\n"
;; No \\n in the end, but, get-lines has benevolent behavior.
["1" "2" "3"] "3\n1\n2\n3"
;; Although the specified number of lines is larger than the actual
;; lines, get-lines has benevolent behavior.
["1" "2" "3"] "4\n1\n2\n3"
[] "0"
[] "-1\n33\n44"))
(testing "get-lines for exceptional cases"
(is (thrown? NullPointerException (get-lines nil)))
(are [str]
(thrown? NumberFormatException (get-lines-str str))
"ab1\n line 1"
"1 not valid\n1")))
^L
;; transform each line to convert, validating it and making it amiable for conversion
;; 0-9a-zA-Z!"#$%&'()*+,-./:;?@[\]^_`{|}~
(def valid-chars (set (concat "!\"#$%&'()*+,-./:;?@[\\]^_`{|}~"
(map char (range (int \0) (inc (int \9))))
(map char (range (int \a) (inc (int \z))))
(map char (range (int \A) (inc (int \Z)))))))
(defn throw-illegal-arg
"Throws IllegalArgumentException constructed with s."
[s]
(throw (IllegalArgumentException. s)))
(defn if-line-invalid-throw
"Checks if line-data is valid, throwing an IllegalArgumentException if it
isn't."
[line-data original-line]
(if (not (= 3 (count line-data)))
(throw-illegal-arg
(str "Expected \"<alien-num> <source-lang> <target-lang>\", but, was: "
original-line))
(let [alien-num (first line-data)
source-lang (second line-data)
target-lang (nth line-data 2)]
(cond
(= (first alien-num) (first source-lang))
(throw-illegal-arg
"First digit of alien-num is the lowest valued digit of the source-lang.")
(not (every? (set source-lang) alien-num))
(throw-illegal-arg
(str "Not all digits of alien-num (" alien-num
") are contained in the source-lang (" source-lang ")."))
(not (= (count (set source-lang)) (count source-lang)))
(throw-illegal-arg (str "Not all digits of the source-lang ("
source-lang ") are unique."))
(not (= (count (set target-lang)) (count target-lang)))
(throw-illegal-arg (str "Not all digits of the target-lang ("
target-lang ") are unique."))
;; 0-9a-zA-Z!"#$%&'()*+,-./:;?@[\]^_`{|}~
(not (every? valid-chars (set (concat alien-num source-lang target-lang))))
(throw-illegal-arg (str "There are invalid chars in "
original-line "."))))))
(defn process-line [s]
"Being s an input line, transform and validate it."
(let [split-chars #{\space \tab}
line-data (remove #(some split-chars %)
(partition-by #(contains? split-chars %) s))]
(if-line-invalid-throw line-data s)
(map #(apply str %) line-data)))
;; tests for the transformation and validation functions
(deftest process-line-tests
(testing "that given well formed lines the result is correct"
(are [expected line]
(= expected (process-line line))
["123" "0123456789" "abcdefghij"]
"123 0123456789 abcdefghij"
;; notice that the benevolent behavior is guaranteed by accepting
;; a tab character and one space
["1" "01" "abc"]
(str "1 01" \tab " abc")))
(testing "that invalid input is rejected"
(are [line]
(thrown? IllegalArgumentException (process-line line))
"1 0123 ABC xy" ; too many arguments
"123 012345678" ; too few arguments
;; the number should not start with the lowest digit of the source language
"012 01234 ABC"
;; the source language doesn't contain all digits in the number
"12x 0123 xyz0"
"12x x122 YZXO" ; the source language contains duplicate digits
"12x x123 YZXY" ; the target language contains duplicate digits
;; the target language contains the invalid digit €
"12x x123 €ZXY")))
^L
;; output
(defn case-out
[case-num converted-num]
(str "Case #" case-num ": " converted-num))
(deftest case-out-tests
(is (= "Case #3: xyz!!"
(case-out 3 "xyz!!"))
"given a case-num and a converted-num a correct str is returned")
(is (= "Case #55: java.lang.IllegalArgumentException: There are invalid chars in 12x x123 €ZXY."
(case-out 55 (IllegalArgumentException. "There are invalid chars in 12x x123 €ZXY.")))))
^L
;; the entry point
(defn main [args]
(let [lines (get-lines *in*)
numbered-lines (map-indexed (fn [case-num line] [(inc case-num) line]) lines)
cases (map (fn [[case-num line]]
[case-num
(try (->> (process-line line)
(apply convert-num))
(catch IllegalArgumentException e e))])
numbered-lines)]
(doseq [[case-num result] cases]
(println (case-out case-num result)))))
(main *command-line-args*)