View difference between Paste ID: bkAeTduq and
SHOW: | | - or go back to the newest paste.
1-
1+
;;; -*- mode: Clojure; coding: utf-8 -*-
2
;;; Solution by Luis Sergio Oliveira for the Alien Numbers exercise from
3
;;; Programming Praxis
4
;;; See http://programmingpraxis.com/2010/09/24/alien-numbers/
5
6
;;; Run from the shell as so:
7
;;; ~/programacao/programmingpraxis$ ../clojure/clj-dev.sh alien-numbers.clj <<-END
8
;;; > 2
9
;;; > abc abcd 0123456789
10
;;; > ([}} {([}? 0123456
11
;;; > END
12
;;;
13
;;; Or execute its tests like so:
14
;;; (clojure.test/run-tests 'euluis.aliennum)
15
16
(ns euluis.aliennum
17
  (:use [clojure.test :only [is are testing deftest]]))
18
19
^L
20
;;; Conversion given strings alien-num source-lang target-lang.
21
;;; See convert-num fn which is the entry point.
22
23
(defn decimal-to-lang
24
  "Converts decimal-num to the equivalent number in lang as a string.
25
  The algorithm was a translation of the algorithm by Rodrigo Menezes
26
  in C# that he posted in the programming praxis site."
27
  [decimal-num lang]
28
  (let [lang-radix (count lang)]
29
    (loop [decimal-value decimal-num lang-num []]
30
      (if (>= 0 decimal-value)
31
	(apply str lang-num)
32
	(recur (int (/ decimal-value lang-radix))
33
	       (concat [(nth lang (mod decimal-value lang-radix))] lang-num))))))
34
35
(deftest decimal-to-lang-test
36
  (testing "that given a decimal-num and a target lang, decimal-to-lang fn
37
converts it correctly to expected-num"
38
    (are [expected-num decimal-num lang]
39
	 (= expected-num (decimal-to-lang decimal-num lang))
40
	 "1100120" 987 "012"
41
	 "33123" 987 "0123"
42
	 "3" 3 "0123456789")))
43
44
(defn digit-index
45
  [digit lang]
46
  (loop [i 0]
47
    (if (= digit (nth lang i))
48
      i
49
      (recur (inc i)))))
50
51
(defn lang-to-decimal
52
  [alien-num lang]
53
  (let [radix (count lang)
54
	ralien-num (reverse alien-num)]
55
    (loop [i 0 decimal-num 0 product 1]
56
      (if (= i (count ralien-num))
57
	decimal-num
58
	(recur (inc i) (+ decimal-num (* (digit-index (nth ralien-num i) lang) product))
59
	       (* radix product))))))
60
61
(deftest lang-to-decimal-test
62
  (testing "that given an alien-num and a lang we can transform it to a decimal number"
63
    (are [expected-num alien-num lang]
64
	 (= expected-num (lang-to-decimal alien-num lang))
65
	 987 "1100120" "012"
66
	 987 "33123" "0123"
67
	 3 "11" "01")))
68
69
(defn convert-num
70
  "Convert alien-num which is in source-lang into the same number in target-lang"
71
  [alien-num source-lang target-lang]
72
  (decimal-to-lang (lang-to-decimal alien-num source-lang) target-lang))
73
74
(deftest convert-num-tests
75
  (testing "convert-num tests based on 2 to 36 radix numerals"
76
    (are [expected-num alien-num source-lang target-lang]
77
	 (= expected-num
78
	    (convert-num alien-num source-lang target-lang))
79
	 ;; test: 987 0123456789 012 -> should be 1100120
80
	 "1100120"
81
	 "987" "0123456789" "012"
82
	 "987"
83
	 "1100120" "012" "0123456789"
84
	 "33123"
85
	 "987" "0123456789" "0123"
86
	 "//-*/"
87
	 "987" "0123456789" "+-*/"
88
	 "RF"
89
	 "987" "0123456789" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
90
	 "35"
91
	 "Z" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789"
92
	 "cj"
93
	 "\"!" "!\"#$%&'()*+,-./:;?@[\\]^_`{|}~" "abcdefghij"
94
	 "3"
95
	 "11" "01" "0123456789")))
96
97
^L
98
;;; Process the I/O
99
;;; N
100
;;; "<alien number>" "<sl0>..<slN>" "<tl0>..<tlM>"
101
102
;; production code to retrieve the lines containing the numbers to convert
103
(defn my-read-line
104
  ([] (read-line))
105
  ([stream]
106
     (binding [*in* stream]
107
       (read-line))))
108
109
(defn get-lines
110
  "Processes stream, returning a seq of lines in the order they are given
111
   and each line is guaranteed to be a string."
112
  [stream]
113
  (let [lines-number (Integer. (my-read-line stream))
114
	lines (loop [n lines-number lines '()]
115
		(if (>= 0 n)
116
		  lines
117
		  (recur (dec n) (conj lines (my-read-line stream)))))]
118
    ;; what should be the behavior when we inconsistent number and lines?
119
    ;; I'll go for beneficent behavior...
120
    (reverse (filter #(not (nil? %)) lines))))
121
122
;; test code to retrieve the lines containing the numbers to convert
123
(defn stream-str
124
  "Returns an open BufferedReader from str."
125
  [str]
126
  (java.io.BufferedReader. (java.io.StringReader. str)))
127
128
(defn get-lines-str
129
  "Given str, creates and opens a stream from this and hands over to get-lines."
130
  [str]
131
  (with-open [s (stream-str str)]
132
    (get-lines s)))
133
134
(deftest get-lines-tests
135
  (testing "get-lines for happy path test cases"
136
    (are [expected-lines in-str]
137
	 (= expected-lines (get-lines-str in-str))
138
	 ["123 abc" "4 5 7"] "2\n123 abc\n4 5 7\n"
139
	 ;; No \\n in the end, but, get-lines has benevolent behavior.
140
	 ["1" "2" "3"] "3\n1\n2\n3"
141
	 ;; Although the specified number of lines is larger than the actual
142
	 ;; lines, get-lines has benevolent behavior.
143
	 ["1" "2" "3"] "4\n1\n2\n3"
144
	 [] "0"
145
	 [] "-1\n33\n44"))
146
  (testing "get-lines for exceptional cases"
147
    (is (thrown? NullPointerException (get-lines nil)))
148
    (are [str]
149
	 (thrown? NumberFormatException (get-lines-str str))
150
	 "ab1\n line 1"
151
	 "1 not valid\n1")))
152
153
^L
154
;; transform each line to convert, validating it and making it amiable for conversion
155
156
;; 0-9a-zA-Z!"#$%&'()*+,-./:;?@[\]^_`{|}~
157
(def valid-chars (set (concat "!\"#$%&'()*+,-./:;?@[\\]^_`{|}~"
158
			      (map char (range (int \0) (inc (int \9))))
159
			      (map char (range (int \a) (inc (int \z))))
160
			      (map char (range (int \A) (inc (int \Z)))))))
161
162
(defn throw-illegal-arg
163
  "Throws IllegalArgumentException constructed with s."
164
  [s]
165
  (throw (IllegalArgumentException. s)))
166
167
(defn if-line-invalid-throw
168
  "Checks if line-data is valid, throwing an IllegalArgumentException if it
169
   isn't."
170
  [line-data original-line]
171
  (if (not (= 3 (count line-data)))
172
    (throw-illegal-arg
173
     (str "Expected \"<alien-num> <source-lang> <target-lang>\", but, was: "
174
	  original-line))
175
    (let [alien-num (first line-data)
176
	  source-lang (second line-data)
177
	  target-lang (nth line-data 2)]
178
      (cond
179
       (= (first alien-num) (first source-lang))
180
       (throw-illegal-arg
181
	"First digit of alien-num is the lowest valued digit of the source-lang.")
182
       (not (every? (set source-lang) alien-num))
183
       (throw-illegal-arg
184
	(str "Not all digits of alien-num (" alien-num
185
	     ") are contained in the source-lang (" source-lang ")."))
186
       (not (= (count (set source-lang)) (count source-lang)))
187
       (throw-illegal-arg (str "Not all digits of the source-lang ("
188
			       source-lang ") are unique."))
189
       (not (= (count (set target-lang)) (count target-lang)))
190
       (throw-illegal-arg (str "Not all digits of the target-lang ("
191
			       target-lang ") are unique."))
192
       ;; 0-9a-zA-Z!"#$%&'()*+,-./:;?@[\]^_`{|}~
193
       (not (every? valid-chars (set (concat alien-num source-lang target-lang))))
194
       (throw-illegal-arg (str "There are invalid chars in "
195
			       original-line "."))))))
196
197
(defn process-line [s]
198
  "Being s an input line, transform and validate it."
199
  (let [split-chars #{\space \tab}
200
	line-data (remove #(some split-chars %)
201
			  (partition-by #(contains? split-chars %) s))]
202
    (if-line-invalid-throw line-data s)
203
    (map #(apply str %) line-data)))
204
205
;; tests for the transformation and validation functions
206
(deftest process-line-tests
207
  (testing "that given well formed lines the result is correct"
208
    (are [expected line]
209
	 (= expected (process-line line))
210
	 ["123" "0123456789" "abcdefghij"]
211
	 "123 0123456789 abcdefghij"
212
	 ;; notice that the benevolent behavior is guaranteed by accepting
213
	 ;; a tab character and one space
214
	 ["1" "01" "abc"]
215
	 (str "1 01" \tab " abc")))
216
  (testing "that invalid input is rejected"
217
    (are [line]
218
	 (thrown? IllegalArgumentException (process-line line))
219
	 "1 0123 ABC xy" ; too many arguments
220
	 "123 012345678" ; too few arguments
221
	 ;; the number should not start with the lowest digit of the source language
222
	 "012 01234 ABC"
223
	 ;; the source language doesn't contain all digits in the number
224
	 "12x 0123 xyz0"
225
	 "12x x122 YZXO" ; the source language contains duplicate digits
226
	 "12x x123 YZXY" ; the target language contains duplicate digits
227
	 ;; the target language contains the invalid digit €
228
	 "12x x123 €ZXY")))
229
230
^L
231
;; output
232
(defn case-out
233
  [case-num converted-num]
234
  (str "Case #" case-num ": " converted-num))
235
236
(deftest case-out-tests
237
  (is (= "Case #3: xyz!!"
238
	 (case-out 3 "xyz!!"))
239
      "given a case-num and a converted-num a correct str is returned")
240
  (is (= "Case #55: java.lang.IllegalArgumentException: There are invalid chars in 12x x123 €ZXY."
241
	 (case-out 55 (IllegalArgumentException. "There are invalid chars in 12x x123 €ZXY.")))))
242
243
^L
244
;; the entry point
245
(defn main [args]
246
  (let [lines (get-lines *in*)
247
	numbered-lines (map-indexed (fn [case-num line] [(inc case-num) line]) lines)
248
	cases (map (fn [[case-num line]]
249
		     [case-num
250
		      (try (->> (process-line line)
251
				(apply convert-num))
252
			   (catch IllegalArgumentException e e))])
253
		   numbered-lines)]
254
    (doseq [[case-num result] cases]
255
      (println (case-out case-num result)))))
256
257
(main *command-line-args*)