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 C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TEMPERATURE_CONVERTERS \
    TEMPERATURE_CONVERTER(kelvin,     kelvin,                           kelvin,                            "Kelvin", "K", "k") \
    TEMPERATURE_CONVERTER(celsius,    celsius + 273.15,                 kelvin - 273.15,                   "Celsius", "°C", "C", "c") \
    TEMPERATURE_CONVERTER(delisle,    373.15 - delisle * 2 / 3,         (373.15 - kelvin) * 3 / 2,         "Delisle", "°De", "De", "DE", "de") \
    TEMPERATURE_CONVERTER(fahrenheit, (fahrenheit + 459.67) * 5 / 9,    kelvin * 9 / 5 - 459.67,           "Fahrenheit", "°F", "F", "f") \
    TEMPERATURE_CONVERTER(newton,     newton * 100 / 33 + 273.15,       (kelvin - 273.15) * 33 / 100,      "Newton", "°N", "N", "n") \
    TEMPERATURE_CONVERTER(rankine,    rankine * 5 / 9,                  kelvin * 9 / 5,                    "Rankine", "°R", "R", "r") \
    TEMPERATURE_CONVERTER(réaumur,    réaumur * 5 / 4 + 273.15,         (kelvin - 273.15) * 4 / 5,         "Réaumur", "°Ré", "°Re", "Ré", "RÉ", "ré", "Re", "RE", "re") \
    TEMPERATURE_CONVERTER(rømer,      (rømer - 7.5) * 40 / 21 + 273.15, (kelvin - 273.15) * 21 / 40 + 7.5, "Rømer", "°Rø", "°Ro", "Rø", "RØ", "rø", "Ro", "RO", "ro") \

struct TemperatureConverter {
    const char **names;
    double (*toKelvin)(double other);
    double (*fromKelvin)(double kelvin);
};

#define TEMPERATURE_CONVERTER(name, toKelvinExpression, fromKelvinExpression, ...) \
    double name ## ToKelvin(double name) { \
        return toKelvinExpression; \
    } \
    double name ## FromKelvin(double kelvin) { \
        return fromKelvinExpression; \
    } \
    const struct TemperatureConverter name = { \
        (const char *[]) { __VA_ARGS__, NULL }, \
        name ## ToKelvin, \
        name ## FromKelvin, \
    };

TEMPERATURE_CONVERTERS

#undef TEMPERATURE_CONVERTER
#define TEMPERATURE_CONVERTER(name, ...) &name,

const struct TemperatureConverter* const converters[] = { TEMPERATURE_CONVERTERS NULL };

const struct TemperatureConverter *TemperatureConverter_get(const char *requestedName) {
    for (const struct TemperatureConverter *const *converter = converters; *converter; converter++)
        for (const char **name = (*converter)->names; *name; name++)
            if (strcmp(*name, requestedName) == 0)
                return *converter;
    fprintf(stderr, "error: Unknown temperature scale %s\n", requestedName);
    exit(1);
}

int main([[maybe_unused]] const int argc, const char *const argv[]) {
    double (*const toKelvin)(double) = TemperatureConverter_get(argv[1])->toKelvin;
    double (*const fromKelvin)(double) = TemperatureConverter_get(argv[2])->fromKelvin;
    for (argv += 3; *argv; argv++)
        printf("%.6g\n", fromKelvin(toKelvin(atof(*argv))));
}

Note:

  • I know that the error handling is incomplete, and I'm fine with that.
  • I know that some code styles say that if, for etc should always have braces. I'm fine with single statement if, for to not have braces.
\$\endgroup\$
2
  • \$\begingroup\$ maybe_unused is part of C23 and hasn't been accepted yet as part of the standard. \$\endgroup\$
    – pacmaninbw
    Commented Sep 7, 2022 at 14:52
  • \$\begingroup\$ Not accepted or not published? AFAIK it was accepted, as it's already in the main draft. \$\endgroup\$ Commented Sep 8, 2022 at 10:08

1 Answer 1

4
\$\begingroup\$

Instead of having to provide a matching pair of functions for each unit, prefer to just pass the linear conversion parameters (slope and intercept).

We can reduce the amount of linear searching, if we fold to lower-case and accept any prefix of the unit name. That's a functional change, in that we accept more versions of each unit, but it's consistent with the original code's policy of assuming inputs are valid.

I don't like calling exit() from within functions like that - it makes the code harder to reuse in a larger program (for example if we made a GUI version).

[[maybe_unused]] is not yet standard C.

The precision of %g conversion defaults to 6, so it's redundant to write %.6g.

The standard signature of main() requires that *argv and **argv are mutable types (whether or not *argv can be written to - see Is argv[n] writable?).


Modified code

The error handling is still incomplete - we'll want to use strtod() instead of atof() to detect non-numeric values.

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a)[0])

struct TemperatureConverter {
    const char *names;         /* space-delimited string of names */
    double per_kelvin;         /* number of units in 1 kelvin interval */
    double zero_val;           /* value of zero, in kelvins */
};

const struct TemperatureConverter *converter_for(const char *const requestedName)
{
    static const struct TemperatureConverter converters[] =
        {
            {"kelvins", 1, 0},
            {"celsius centigrade °c", 1, 273.15},
            {"delisle °de", -1.5, 373.15},
            {"fahrenheit °f", 1.8, 273.15 - 32 / 1.8 },
            {"newton °n", 1, 273.15},
            {"rankine °r", 1.8, 0}, /* must precede other R units */
            {"réaumur reaumur °ré °re", 0.8, 273.15},
            {"rømer romer °rø °ro", 0.525, 273.15 - 7.5 / 0.525}
    };

    const size_t len = strlen(requestedName);

    char name_lower[sizeof "fahrenheit"];        /* longest valid name */
    if (len < sizeof name_lower) {
        /* convert to lower-case */
        char *p = name_lower;
        const unsigned char *q = (const unsigned char*)requestedName;
        while ((*p++ = (char)tolower(*q++)))
            ;

        /* linear search for any prefix */
        for (size_t i = 0;  i < ARRAY_SIZE(converters);  ++i) {
            const struct TemperatureConverter *candidate = converters + i;
            for (const char *name = candidate->names;  name;  name = strchr(name + 1, ' ')) {
                while (*name == ' ') { ++name; }
                if (strncmp(name, name_lower, len) == 0) {
                    return candidate;
                }
            }
        }
    }
    /* name too long, or search failed */
    return NULL;
}

int main(const int argc, char *argv[])
{
    (void) argc;

    const struct TemperatureConverter *const in_unit = converter_for(*++argv);
    if (!in_unit) {
        fprintf(stderr, "error: Unknown input temperature scale \"%s\"\n", *argv);
        return EXIT_FAILURE;
    }

    const struct TemperatureConverter *const out_unit = converter_for(*++argv);
    if (!out_unit) {
        fprintf(stderr, "error: Unknown output temperature scale \"%s\"\n", *argv);
        return EXIT_FAILURE;
    }

    while (*++argv) {
        const double kelvins = atof(*argv) / in_unit->per_kelvin + in_unit->zero_val;
        printf("%g\n", (kelvins - out_unit->zero_val) * out_unit->per_kelvin);
    }
}

If we really can't accept matching all prefixes of our units, then we'll need to include all the names in each converter, and change the test to ensure we match a whole word, like this:

if (strncmp(name, name_lower, len) == 0
    && (name[len] == ' ' || name[len] == '\0'))

We're not able to do the cheaper test first, because we don't know that name still has len or more characters before the strncmp() succeeds.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Very good. Some ideas: 1) 15 is a naked magic number in name_lower[15];, better as a defined constant. 2) fahrenheit + 459.67) * 5 / 9 has a computed 459.67. For good-to-the last-bit conversion, use (f - 32)*5/9 -−273.15 (IOWs a slightly more expanded conversion table. 3) quibble on using strncmp() v. strcmp() the whole name should match. \$\endgroup\$
    – chux
    Commented Sep 9, 2022 at 0:09
  • \$\begingroup\$ Thanks @chux. (1) I would have liked a more automatic way, and I think I have one now. (2) yes, I just copied that value from the question (and wondered why my results were slightly inexact!). (3) yes, that's a functional difference: see paragraph 2. It means we can reduce the number of strings to match. If we don't make that choice, then we would still use strncmp(), but confirm that it's immediately followed by space or nul. \$\endgroup\$ Commented Sep 9, 2022 at 6:57
  • \$\begingroup\$ main() is special. "... requires that *argv ... be modifiable" is debatable Is argv[n] writable?. \$\endgroup\$
    – chux
    Commented Sep 9, 2022 at 12:34
  • \$\begingroup\$ @chux, thanks for the link. I've re-worded that section to more clearly state what I meant. \$\endgroup\$ Commented Sep 9, 2022 at 14:15

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.