Want more features on Pastebin? Sign Up, it's FREE!
Guest

Alien Numbers in Clojure

By: a guest on Oct 26th, 2010  |  syntax: Lisp  |  size: 8.83 KB  |  views: 325  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
This paste has a previous version, view the difference. Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  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*)
clone this paste RAW Paste Data