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 KelvinC
for CelsiusF
for FahrenheitR
for RankineD
for DelisleN
for NewtonRø
for RømerRé
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.
-
1\$\begingroup\$ Fun effort using some obscure scales. Too bad review sets aside error handling as error handling is such a critical aspect of robust code design. I'd especially want detection of negative Kelvin errors, \$\endgroup\$chux– chux2023年06月06日 02:45:34 +00:00Commented Jun 6, 2023 at 2:45
2 Answers 2
Math detail:
Conversion via the SI unit of Kelvin is a good approach.
Sometimes numerical computation artifacts though will result in a unexpected result like -1e-16 Kelvin.
To cope with such, consider instead of Kelvin as the central unit, use centiKelvin, then conversions (+ celsius 273.15))
--> (+ celsius 27315.0))
and we avoid problems that stem from 273.15
is not exactly representable. For some of the more obscure scales we may need another factor than x100
, but I hope OP gets the idea.
- 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 isidentity
):
(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 removedo
). - 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, uselet
instead- You shouldn't use
read-string
for reading data from an untrusted source. While there is safe equivalentclojure.edn/read-string
, I think you're actually looking forparse-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)))))))