2
\$\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 Kotlin:

enum class TemperatureConverter(
    val toKelvin: (Double) -> Double,
    val fromKelvin: (Double) -> Double,
    vararg val names: String,
) {
    KELVIN    ({kelvin     -> kelvin                          }, {kelvin -> kelvin                           }, "K", "k"),
    CELSIUS   ({celsius    -> celsius + 273.15                }, {kelvin -> kelvin - 273.15                  }, "°C", "C", "c"),
    DELISLE   ({delisle    -> 373.15 - delisle * 2 / 3        }, {kelvin -> (373.15 - kelvin) * 3 / 2        }, "°De", "De", "DE", "de"),
    FAHRENHEIT({fahrenheit -> (fahrenheit + 459.67) * 5 / 9   }, {kelvin -> kelvin * 9 / 5 - 459.67          }, "°F", "F", "f"),
    NEWTON    ({newton     -> newton * 100 / 33 + 273.15      }, {kelvin -> (kelvin - 273.15) * 33 / 100     }, "°N", "N", "n"),
    RANKINE   ({rankine    -> rankine * 5 / 9                 }, {kelvin -> kelvin * 9 / 5                   }, "°R", "R", "r"),
    RÉAUMUR   ({réaumur    -> réaumur * 5 / 4 + 273.15        }, {kelvin -> (kelvin - 273.15) * 4 / 5        }, "°Ré", "°Re", "Ré", "RÉ", "ré", "Re", "RE", "re"),
    RØMER     ({rømer      -> (rømer - 7.5) * 40 / 21 + 273.15}, {kelvin -> (kelvin - 273.15) * 21 / 40 + 7.5}, "°Rø", "°Ro", "Rø", "RØ", "rø", "Ro", "RO", "ro"),
    ;

    companion object {
        @JvmStatic
        private val converters = values()
            .asSequence()
            .flatMap { it.names.asSequence().map { name -> name to it }}
            .toMap()

        operator fun get(name: String) = converters[name]!!
    }
}

fun main(vararg args: String) {
    val toKelvin = TemperatureConverter[args[0]].toKelvin
    val fromKelvin = TemperatureConverter[args[1]].fromKelvin
    args
        .asSequence()
        .drop(2)
        .map(String::toDouble)
        .map(toKelvin)
        .map(fromKelvin)
        .forEach { println(it) }
}

Note: I'm aware that the lines defining the enum are a bit long. I'm okay with sacrificing a bit of line length for retaining a tabular format in this case.

I'm aware that error handling will dump exceptions to the user. I'm ok with that, this is not central to the design.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I have been thinking about this for few days, I dont know it can improved except reformating the long lines :D maybe adding Kotlin/kotlinx-cli library and then compiling to native will be better? \$\endgroup\$ Sep 9, 2022 at 8:56

2 Answers 2

2
\$\begingroup\$

Now this is some really nice piece of code and design in my opinion: very clear, clean, minimum boilerplate. I can think only of:

operator fun get(name: String) = converters[name]!!

You mentioned you are okay with exceptions not being handled, which I understand. You are probably only interested in the conversion code and not UI. What you can improve here is exception message for consumers of your library.

If your statement fails here, it will be simple NullPointerException with no extra information.

I would change the code to explicitly call getValue; that expects the key in the map and throws a nicer exception:

operator fun get(name: String) = converters.getValue(name)

Or go even further and provide your own message if necessary, e.g.

operator fun get(name: String) = converters[name] ?: throw NoSuchElementException("'$name' scale not found, possible scales are : ${values().asSequence().flatMap{ it.names }}")

Also I see the inner asSequence call in flatMap as redundant, unless I am missing something.

\$\endgroup\$
-1
\$\begingroup\$

Your code looks fine. But personally I think that as long as you do not need the information held by the enum elsewhere in your application, I would drop it and replace it with a more simple solution. Note that this solution includes error checking for invalid symbols and also for values not parseable as doubles.

fun convertTemperatures(from: String, to: String, values: List<String>): List<Double?> {

  data class Temperature(val symbols: String, val toKelvin: (Double) -> Double, val fromKelvin: (Double) -> Double)

  val temperatures = listOf(
    Temperature("K|k", { v -> v }, { v -> v }),
    Temperature("°C|C|c", { v -> v + 273.15 }, { v -> v - 273.15 }),
    Temperature("°De|De|DE|de", { v -> 373.15 - v * 2 / 3 }, { v -> (373.15 - v) * 3 / 2 }),
    Temperature("°F|F|f", { v -> (v + 459.67) * 5 / 9 }, { v -> v * 9 / 5 - 459.67 }),
    Temperature("°N|N|n", { v -> v * 100 / 33 + 273.15 }, { v -> (v - 273.15) * 33 / 100 }),
    Temperature("°R|R|r", { v -> v * 5 / 9 }, { v -> v * 9 / 5 }),
    Temperature("°Ré|°Re|Ré|RÉ|ré|Re|RE|re", { v -> v * 5 / 4 + 273.15 }, { v -> (v - 273.15) * 4 / 5 }),
    Temperature("°Rø|°Ro|Rø|RØ|rø|Ro|RO|ro", { v -> (v - 7.5) * 40 / 21 + 273.15 }, { v -> (v - 273.15) * 21 / 40 + 7.5 }),
  )

  val fromKelvin = temperatures.find { from in it.symbols.split('|') }?.fromKelvin ?: return emptyList()
  val toKelvin = temperatures.find { to in it.symbols.split('|') }?.toKelvin ?: return emptyList()
  return values
    .map { it.toDoubleOrNull() }
    .map { if (it != null) fromKelvin(toKelvin(it)) else null }

}

// Test
val args = arrayOf("K", "C", "0", "273.15", "373.15")
val result = convertTemperatures(args[0], args[1], args.drop(2))
result.forEach(::println)
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Can you be specific? How is this simpler or better solution? I honestly don't see even a single point. \$\endgroup\$
    – K.H.
    Oct 22, 2022 at 19:44

Your Answer

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

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