6
\$\begingroup\$

Inspired by an earlier question I've made a temperature converter that can convert from one temperature to another and optionally output a series of conversions.

This was made in Visual Studio 2015 but it also compiles with g++ and clang++ on Debian with no warnings.

Usage

tc.exe -40 c f

Celcius           | Fahrenheit
------------------+------------------
           -40.00 |            -40.00

or

tc.exe -40 c f 10 10
Celcius           | Fahrenheit
------------------+------------------
           -40.00 |            -40.00
           -30.00 |            -22.00
           -20.00 |             -4.00
           -10.00 |             14.00
             0.00 |             32.00
            10.00 |             50.00

Where the arguments are:

Usage: tc.exe Degrees InitialUnit ConvertedUnit [InitialUnitStepSize InitialUnitUpperBoundInclusive]

stdafx.h

#pragma once

#include <stdio.h>
#include <string>
#include <cstring>
#include <sstream>
#include <memory>
#include <iostream>
#include <limits>
#include <cmath>

Temperature.h

#pragma once

#include "stdafx.h"

namespace hest {

enum class TemperatureUnit
{
    Invalid,
    Celcius,
    Fahrenheit,
    Kelvin,
    Rankine,
};

class TemperatureData;
class Kelvin;

class Temperature {
public:
    typedef std::unique_ptr<Temperature> UniqueTemperature;

    static std::string ToShortString(TemperatureUnit const unit);
    static std::string ToString(TemperatureUnit const unit);
    static TemperatureUnit StringToUnit(std::string const & unit);
    static TemperatureUnit CharToUnit(char unit);
    static UniqueTemperature CreateTemperature(double degrees, TemperatureUnit unit);
    static UniqueTemperature Convert(Temperature const & from, TemperatureUnit const to);

    double degrees() const;
    TemperatureUnit unit() const;


    std::string ToString() const;

    virtual Kelvin ToKelvin() const = 0;

protected:
    Temperature(double degrees, TemperatureUnit unit);

private:
    std::shared_ptr<TemperatureData> data_;
};

class Kelvin final : public Temperature {
public:
    Kelvin(double degrees);

    Kelvin ToKelvin() const;
    UniqueTemperature ConvertTo(TemperatureUnit const unit) const;
};

class Celcius final : public Temperature {
public:
    Celcius(double degrees);

    Kelvin ToKelvin() const;
};

class Rankine final : public Temperature {
public:
    Rankine(double degrees);

    Kelvin ToKelvin() const;
};

class Fahrenheit final : public Temperature {
public:
    Fahrenheit(double degrees);

    Kelvin ToKelvin() const;
};

inline bool operator==(Kelvin const & lhs, Kelvin const & rhs) { return lhs.degrees() == rhs.degrees(); }
inline bool operator!=(Kelvin const & lhs, Kelvin const & rhs) { return !operator==(lhs, rhs); }
inline bool operator< (Kelvin const & lhs, Kelvin const & rhs) { return lhs.degrees() < rhs.degrees(); }
inline bool operator> (Kelvin const & lhs, Kelvin const & rhs) { return  operator< (rhs, lhs); }
inline bool operator<=(Kelvin const & lhs, Kelvin const & rhs) { return !operator> (lhs, rhs); }
inline bool operator>=(Kelvin const & lhs, Kelvin const & rhs) { return !operator< (lhs, rhs); }

inline bool operator==(Temperature const & lhs, Temperature const & rhs) { return lhs.ToKelvin() == rhs.ToKelvin(); }
inline bool operator!=(Temperature const & lhs, Temperature const & rhs) { return !operator==(lhs, rhs); }
inline bool operator< (Temperature const & lhs, Temperature const & rhs) { return lhs.ToKelvin() < rhs.ToKelvin(); }
inline bool operator> (Temperature const & lhs, Temperature const & rhs) { return  operator< (rhs, lhs); }
inline bool operator<=(Temperature const & lhs, Temperature const & rhs) { return !operator> (lhs, rhs); }
inline bool operator>=(Temperature const & lhs, Temperature const & rhs) { return !operator< (lhs, rhs); }

inline Kelvin operator+(Kelvin const & lhs, double rhs) { return Kelvin(lhs.degrees() + rhs); }
inline Kelvin operator+(Kelvin const & lhs, Kelvin const & rhs) { return Kelvin(lhs.degrees() + rhs.degrees()); }
inline Kelvin operator-(Kelvin const & lhs, double rhs) { return Kelvin(lhs.degrees() - rhs); }
inline Kelvin operator-(Kelvin const & lhs, Kelvin const & rhs) { return Kelvin(lhs.degrees() - rhs.degrees()); }

inline Temperature::UniqueTemperature operator+(Temperature const & lhs, double rhs) {
    return Temperature::CreateTemperature(lhs.degrees() + rhs, lhs.unit());
}
inline Temperature::UniqueTemperature operator+(Temperature const & lhs, Temperature const & rhs) {
    if (lhs.unit() == rhs.unit()) {
        return Temperature::CreateTemperature(lhs.degrees() + rhs.degrees(), lhs.unit());
    }

    auto converted = Temperature::Convert(lhs, rhs.unit());
    return Temperature::CreateTemperature(rhs.degrees() + converted->degrees(), rhs.unit());
}
inline Temperature::UniqueTemperature operator-(Temperature const & lhs, double rhs) {
    return Temperature::CreateTemperature(lhs.degrees() - rhs, lhs.unit());
}
inline Temperature::UniqueTemperature operator-(Temperature const & lhs, Temperature const & rhs) {
    if (lhs.unit() == rhs.unit()) {
        return Temperature::CreateTemperature(lhs.degrees() - rhs.degrees(), lhs.unit());
    }

    auto converted = Temperature::Convert(lhs, rhs.unit());
    return Temperature::CreateTemperature(rhs.degrees() - converted->degrees(), rhs.unit());
}

}

Temperature.cpp

#include "stdafx.h"
#include "temperature.h"

namespace hest {

constexpr double kCelciusToKelvinOffset = 273.15;
constexpr double kFahrenheitToRankineOffset = 459.67;

static inline double RankineToKelvin(double rankineDegrees) {
    return rankineDegrees * 5.0 / 9.0;
}

static inline double KelvinToRankine(double kelvinDegrees) {
    return kelvinDegrees * 9.0 / 5.0;
}

class TemperatureData {
private:
    double const degrees_;
    TemperatureUnit const unit_;
public:
    TemperatureData(double degrees, TemperatureUnit unit) : degrees_(degrees), unit_(unit) {
        if (!std::isfinite(degrees)) {
            throw std::invalid_argument("Degrees must be finite");
        }
        if (unit == TemperatureUnit::Invalid) {
            throw std::invalid_argument("Invalid unit");
        }
    }

    double degrees() const { return degrees_; }
    TemperatureUnit unit() const { return unit_; }
};

Temperature::Temperature(double degrees, TemperatureUnit unit) : data_(std::make_shared<TemperatureData>(degrees, unit)) {}

Kelvin::Kelvin(double degrees) : Temperature(degrees, TemperatureUnit::Kelvin) { }
Celcius::Celcius(double degrees) : Temperature(degrees, TemperatureUnit::Celcius) { }
Rankine::Rankine(double degrees) : Temperature(degrees, TemperatureUnit::Rankine) { }
Fahrenheit::Fahrenheit(double degrees) : Temperature(degrees, TemperatureUnit::Fahrenheit) { }

Kelvin Kelvin::ToKelvin() const {
    return Kelvin(degrees());
}
Kelvin Celcius::ToKelvin() const {
    return Kelvin(degrees() + kCelciusToKelvinOffset);
}
Kelvin Rankine::ToKelvin() const {
    return Kelvin(RankineToKelvin(degrees()));
}
Kelvin Fahrenheit::ToKelvin() const {
    return Kelvin(RankineToKelvin(degrees() + kFahrenheitToRankineOffset));
}

std::string Temperature::ToShortString(TemperatureUnit const unit)
{
    switch (unit)
    {
    case hest::TemperatureUnit::Kelvin:
        return "K";
    case hest::TemperatureUnit::Celcius:
        return "C";
    case hest::TemperatureUnit::Rankine:
        return "R";
    case hest::TemperatureUnit::Fahrenheit:
        return "F";
    default:
        return "";
    }
}

std::string Temperature::ToString(TemperatureUnit const unit)
{
    switch (unit)
    {
    case hest::TemperatureUnit::Kelvin:
        return "Kelvin";
    case hest::TemperatureUnit::Celcius:
        return "Celcius";
    case hest::TemperatureUnit::Rankine:
        return "Rankine";
    case hest::TemperatureUnit::Fahrenheit:
        return "Fahrenheit";
    default:
        return "";
    }
}

TemperatureUnit Temperature::StringToUnit(std::string const & unit) {
    auto length = unit.length();

    switch (length)
    {
    case 1:
        return CharToUnit(unit[0]);
    case 6:
        if (unit.compare("Kelvin") == 0 || unit.compare("kelvin") == 0) {
            return TemperatureUnit::Kelvin;
        }
    case 7:
        if (unit.compare("Celcius") == 0 || unit.compare("celcius") == 0) {
            return TemperatureUnit::Celcius;
        }
        if (unit.compare("Rankine") == 0 || unit.compare("rankine") == 0) {
            return TemperatureUnit::Rankine;
        }
    case 10:
        if (unit.compare("Fahrenheit") == 0 || unit.compare("fahrenheit") == 0) {
            return TemperatureUnit::Fahrenheit;
        }
    }

    return TemperatureUnit::Invalid;
}

TemperatureUnit Temperature::CharToUnit(char unit) {
    switch (unit)
    {
    case 'c':
    case 'C':
        return TemperatureUnit::Celcius;
    case 'f':
    case 'F':
        return TemperatureUnit::Fahrenheit;
    case 'k':
    case 'K':
        return TemperatureUnit::Kelvin;
    case 'r':
    case 'R':
        return TemperatureUnit::Rankine;
    default:
        return TemperatureUnit::Invalid;
    }
}

auto Temperature::CreateTemperature(double degrees, TemperatureUnit unit) -> UniqueTemperature {
    switch (unit)
    {
    case hest::TemperatureUnit::Kelvin:
        return std::make_unique<Kelvin>(degrees);
    case hest::TemperatureUnit::Celcius:
        return std::make_unique<Celcius>(degrees);
    case hest::TemperatureUnit::Rankine:
        return std::make_unique<Rankine>(degrees);
    case hest::TemperatureUnit::Fahrenheit:
        return std::make_unique<Fahrenheit>(degrees);
    default:
        throw std::invalid_argument("Invalid unit");
    }
}

auto Temperature::Convert(Temperature const & from, TemperatureUnit const to) -> UniqueTemperature {
    auto kelvin = from.ToKelvin();
    return kelvin.ConvertTo(to);
}

double Temperature::degrees() const {
    return data_->degrees();
}

TemperatureUnit Temperature::unit() const {
    return data_->unit();
}

std::string Temperature::ToString() const {
    std::ostringstream format_stream;
    format_stream << degrees() << ToShortString(unit());
    return format_stream.str();
}

auto Kelvin::ConvertTo(TemperatureUnit const unit) const -> UniqueTemperature {
    switch (unit)
    {
    case hest::TemperatureUnit::Kelvin:
        return std::make_unique<Kelvin>(degrees());
    case hest::TemperatureUnit::Celcius:
        return std::make_unique<Celcius>(degrees() - kCelciusToKelvinOffset);
    case hest::TemperatureUnit::Rankine:
        return std::make_unique<Rankine>(KelvinToRankine(degrees()));
    case hest::TemperatureUnit::Fahrenheit:
        return std::make_unique<Fahrenheit>(KelvinToRankine(degrees()) - kFahrenheitToRankineOffset);
    default:
        throw std::invalid_argument("Invalid unit");
    }
}

}

TemperatureConverter.cpp

#include "stdafx.h"
#include "temperature.h"
#include <iomanip>

constexpr int kMaxDigits = std::numeric_limits<double>::digits10;
constexpr int kColumnWidth = kMaxDigits + 2;
constexpr auto kRowSeparator = " | ";
static const auto kRowSeparatorLength = std::strlen(kRowSeparator);

std::string PrintResultHeader(hest::TemperatureUnit const initial, hest::TemperatureUnit const converted) {
    std::ostringstream format_stream;
    format_stream << std::left;
    format_stream << std::setw(kColumnWidth) << hest::Temperature::ToString(initial);
    format_stream << std::setw(0) << kRowSeparator;
    format_stream << std::setw(kColumnWidth) << hest::Temperature::ToString(converted);

    auto column_header_separator = std::string(kColumnWidth + kRowSeparatorLength / 2, '-');
    format_stream << std::setw(0) << "\n" << column_header_separator;
    if (kRowSeparatorLength % 2 == 1) {
        format_stream << "+";
    }
    format_stream << column_header_separator;

    return format_stream.str();
}

std::string PrintResultRow(hest::Temperature::UniqueTemperature const & initial, hest::Temperature::UniqueTemperature const & converted) {
    std::ostringstream format_stream;
    format_stream << std::right << std::setprecision(2) << std::fixed;
    format_stream << std::setw(kColumnWidth) << initial->degrees();
    format_stream << std::setw(0) << kRowSeparator;
    format_stream << std::setw(kColumnWidth) << converted->degrees();

    return format_stream.str();
}

template<typename TResult> bool TryParse(std::string const & input, TResult & result) {
    std::istringstream input_stream(input);
    input_stream >> result;
    return !input_stream.fail() && input_stream.eof();
}

int main(int argc, char* argv[]) {
    if (!(argc == 4 || argc == 6)) {
        std::cout << "Usage: " << argv[0] << " Degrees InitialUnit ConvertedUnit [InitialUnitStepSize InitialUnitUpperBoundInclusive]" << std::endl;
        return EXIT_FAILURE;
    }

    bool has_errors = false;

    auto degrees_argument = std::string(argv[1]);
    double degrees = 0;
    if (!TryParse(degrees_argument, degrees)) {
        std::cerr << "Degrees: " << degrees_argument << " is not a valid number" << std::endl;
        has_errors = true;
    }

    auto initial_unit_argument = std::string(argv[2]);
    auto initial_unit = hest::Temperature::StringToUnit(initial_unit_argument);
    if (initial_unit == hest::TemperatureUnit::Invalid) {
        std::cerr << "InitialUnit: " << initial_unit_argument << " is not a valid unit" << std::endl;
        has_errors = true;
    }

    auto converted_unit_argument = std::string(argv[3]);
    auto converted_unit = hest::Temperature::StringToUnit(converted_unit_argument);
    if (converted_unit == hest::TemperatureUnit::Invalid) {
        std::cerr << "ConvertedUnit: " << converted_unit_argument << " is not a valid unit" << std::endl;
        has_errors = true;
    }

    /* step_size and upper_bound are initialized such that if they're not specified
       the conversion loop will run exactly once. */
    double step_size = std::numeric_limits<double>::max();
    double upper_bound = degrees;
    if (argc == 6) {
        auto step_size_argument = std::string(argv[4]);
        if (!TryParse(step_size_argument, step_size)) {
            std::cerr << "InitialUnitStepSize: " << step_size_argument << " is not a valid number" << std::endl;
            has_errors = true;
        }
        if (!has_errors && step_size <= 0) {
            std::cerr << "InitialUnitStepSize must be positive" << std::endl;
            has_errors = true;
        }

        auto upper_bound_argument = std::string(argv[5]);
        if (!TryParse(upper_bound_argument, upper_bound)) {
            std::cerr << "InitialUnitUpperBoundInclusive: " << upper_bound_argument << " is not a valid number" << std::endl;
            has_errors = true;
        }
        if (!has_errors && upper_bound <= 0) {
            std::cerr << "InitialUnitUpperBoundInclusive must be greater than Degrees" << std::endl;
            has_errors = true;
        }
    }

    if (has_errors) {
        return EXIT_FAILURE;
    }

    try
    {
        std::cout << PrintResultHeader(initial_unit, converted_unit) << std::endl;

        for (auto initial_temperature = hest::Temperature::CreateTemperature(degrees, initial_unit); 
             initial_temperature->degrees() <= upper_bound; 
             initial_temperature = *initial_temperature + step_size) 
        {
            auto converted_temperature = hest::Temperature::Convert(*initial_temperature, converted_unit);
            std::cout << PrintResultRow(initial_temperature, converted_temperature) << std::endl;
        }
    }
    catch (const std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
\$\endgroup\$
  • \$\begingroup\$ I see a lot more -> than is nice. Which means you have pointers. This seems overcomplicated. Use normal objects rather than pointers (even if you wrap them in smart pointers). \$\endgroup\$ – Martin York Sep 21 '15 at 17:10
  • 1
    \$\begingroup\$ Celcius should be Celsius. \$\endgroup\$ – DarioP Sep 22 '15 at 10:24
3
\$\begingroup\$

I don't think I would have used a class hierarchy to represent the different types of temperature. In the end they represent the same thing (the amount of energy in a system). You should pick a unit to store that data in then covert all types to this global unit.

class Temperature
{
    double kelvin;
    public:
       // Explicit: We don't want auto-conversion.
       explicit Temperature(double kelvin)
           : kelvin(kelvin)
       {}
 };

Then write make_temp_from_X() functions that convert a particular scale to your chosen form.

 Temperature make_temp_from_kelvin(double k)  {return Temperature(k);}
 Temperature make_temp_from_celcius{double c) {return Temperature(c-273.15);}
 ... etc

The cost of passing an object of type Temperature around is the same cost as passing a double around (so you don't really need to worry about that).

\$\endgroup\$
  • \$\begingroup\$ But then I don't get to play with smart pointers and some of the other new features ;) \$\endgroup\$ – Johnbot Sep 22 '15 at 7:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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