0
\$\begingroup\$

Problem statement:

Write a program that converts all given temperatures from a given input temperature scale to a given output temperature scale. The temperature scales to be supported are Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Rømer, Réaumur.

Synopsis: tempconv INPUT_SCALE OUTPUT_SCALE [TEMPERATURE]...

The INPUT_SCALE and OUTPUT_SCALE shall be given as follows:

  • K for Kelvin
  • C for Celsius
  • F for Fahrenheit
  • R for Rankine
  • D for Delisle
  • N for Newton
  • for Rømer
  • for Réaumur.

Example:

tempconv K C 0 273.15 373.15
-273.15
0.0
100.0

My solution in Clojure:

#!/usr/bin/env -S clojure

(defrecord TemperatureConverter [toKelvin fromKelvin names])
(def converters [
  (TemperatureConverter. (fn [kelvin]     kelvin)                                  (fn [kelvin] kelvin)                                  #{"K" "k"})
  (TemperatureConverter. (fn [celsius]    (+ celsius 273.15))                      (fn [kelvin] (- kelvin 273.15))                       #{"°C" "C" "c"})
  (TemperatureConverter. (fn [delisle]    (- 373.15 (* delisle (/ 2 3))))          (fn [kelvin] (/ (* (- 373.15 kelvin) 3) / 2))         #{"°De" "De" "DE" "de"})
  (TemperatureConverter. (fn [fahrenheit] (+ fahrenheit (* 459.67 (/ 5 9))))       (fn [kelvin] (- (* kelvin (/ 9 5)) 459.67))           #{"°F" "F" "f"})
  (TemperatureConverter. (fn [newton]     (+ (* newton * (/ 100  33)) 273.15))     (fn [kelvin] (/ (* (- kelvin 273.15) 33) 100))        #{"°N" "N" "n"})
  (TemperatureConverter. (fn [rankine]    (* rankine (/ 5 9)))                     (fn [kelvin] (/ (* kelvin 9) 5))                      #{"°R" "R" "r"})
  (TemperatureConverter. (fn [réaumur]    (+ (* réaumur (/ 5 4)) 273.15))          (fn [kelvin] (/ (* (- kelvin 273.15) 4) 5))           #{"°Ré" "°Re" "Ré" "RÉ" "ré" "Re" "RE" "re"})
  (TemperatureConverter. (fn [rømer]      (+ (/ (* (- rømer 7.5) 40 ) 21) 273.15)) (fn [kelvin] (+ (/ (* (- kelvin 273.15) 21) 40) 7.5)) #{"°Rø" "°Ro" "Rø" "RØ" "rø" "Ro" "RO" "ro"})
])

(defn getconv [name] (first (filter #(contains? (.names %) name) converters)))

(if-let [[inputScale outputScale & args] *command-line-args*]
  (do
    (def toKelvin (. (getconv inputScale) toKelvin))
    (def fromKelvin (. (getconv outputScale) fromKelvin))
    (doseq [arg args]
      (println (fromKelvin (toKelvin (read-string arg))))
    )
  )
)

I seek to understand if there is anything that can be improved, especially if there is some smart way to use a macro to simplify the code that I missed.

Note:

  • For now, error handling (user inputs text instead of a number, or an unknown scale) is not of concern.
  • I'm aware that my "brace style" with closing parentheses on separate lines is not the usual Lisp/Clojure style that would collect them at the end of the last line. I prefer this way because it causes less noise in diffs in version control.
  • I'm aware that the lines defining the temperature converters are long. I usually don't like long lines, but in this case, a tabular layout makes the source code easier to read.
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$
  • I have to repeat that: your "brace style" is bad.
  • Use kebab-case for names of record fields: toKelvin fromKelvin -> to-kelvin from-kelvin
  • Use different constructor for records: TemperatureConverter. -> ->TemperatureConverter
  • All from-kelvin functions have the same name of the argument, so I would also consider rewriting them with anonymous function literal #(...) (and that first one is identity):
(fn [kelvin] kelvin) -> identity
(fn [kelvin] (- kelvin 273.15)) -> #(- % 273.15)
...
  • You should access record fields with keywords: :to-kelvin, :from-kelvin, :names...
  • Function getconv: you can rewrite this with ->> ("thread-last") macro:
(defn getconv [name]
  (->> converters
       (filter #(contains? (:names %) name))
       first))
  • Because your if-let has only one branch, when-let will be better (and with that, you can also remove do).
  • Also use kebab-case for names of variables: inputScale outputScale -> input-scale output-scale, toKelvin fromKelvin -> to-kelvin from-kelvin
  • def creates global variables and you should use it only on the top-level, use let instead
  • You shouldn't use read-string for reading data from an untrusted source. While there is safe equivalent clojure.edn/read-string, I think you're actually looking for parse-double

Main function with all suggested changes:

(when-let [[input-scale output-scale & args] *command-line-args*]
  (let [to-kelvin (:to-kelvin (getconv input-scale))
        from-kelvin (:from-kelvin (getconv output-scale))]
    (doseq [arg args]
      (println (from-kelvin (to-kelvin (parse-double arg)))))))

(You can add some more -> or ->> for readability.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.