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*) |