nholthaus / units

a compile-time, header-only, dimensional analysis and unit conversion library built on c++14 with no dependencies.
http://nholthaus.github.io/units/
MIT License
955 stars 135 forks source link

Function input parameters with unit type #267

Closed behzaadh closed 3 years ago

behzaadh commented 3 years ago

I am using units version 3.0.0 with MinGW 8.1.0 64bit for compiling. I wonder how would it be possible to have my own function with unit type as the input parameters. For example, I have my function like

double myconvert(unittype1 from, unittype2 to)
{
    return unittype1<double>(3.0).convert<unittype2<double>::conversion_factor>().to<double>();
}

then regardless of knowing the typename, I am going to the conversion. Of course, unittype1 and unittype2 are in the same family unit type for example length.

nholthaus commented 3 years ago

so the beauty of units is that all units of the same dimension are safely implicitly convertible to one another. For example:

meters<double> a = 3.0_m;
feet<double> b = a;  // b now contains the value 9.84252 ft

no function required!

behzaadh commented 3 years ago

@nholthaus thanks for your response. Actually, I know how to convert value like that. But I am going to write a unit converter with a ComboBox option, I wonder how can I show the converted value without defining hundreds if/switch-case condition. Imagine in the software the user wants to convert meters to feet by selecting ComboBox toolboxes, my question is what would it be the easiest way to do this conversion?

nholthaus commented 3 years ago

Not sure if there's a really great way to do it, you'd really need reflection for an elegant solution.

I made a combobox for time a few years ago. Not necessarily recommending this approach, but I'll share the code:


TimeSpinBox.h

#pragma once
#ifndef TimeSpinbox_h__
#define TimeSpinbox_h__

//------------------------------
//  INCLUDES
//------------------------------

#include <QAbstractSpinBox>
#include <variant>
#include <units.h>

//--------------------------------------------------------------------------------------------------
//  CLASS TimeSpinbox
//--------------------------------------------------------------------------------------------------
class TimeSpinbox : public QAbstractSpinBox
{
    Q_OBJECT
public:

#ifndef TIMESPINBOX_USE_SUBSECONDS
    using TimeUnit = std::variant
    <
        units::time::second_t,
        units::time::minute_t,
        units::time::hour_t
    >;
#else
    using TimeUnit = std::variant
    <
        units::time::nanosecond_t,
        units::time::microsecond_t,
        units::time::millisecond_t,
        units::time::second_t,
        units::time::minute_t,
        units::time::hour_t
    >;
#endif

    friend class TimeEdit;

public:

    TimeSpinbox(QWidget* parent = nullptr);

    //------------------------------
    //  GETTERS
    //------------------------------

    template<class Unit = units::time::second_t>
    Unit value() const;

    template<class Unit = units::time::second_t>
    Unit minimum() const;

    template<class Unit = units::time::second_t>
    Unit maximum() const;

    template<class Unit = units::time::second_t>
    Unit singleStep() const;

    virtual QSize sizeHint() const override;
    virtual QString textFromValue(const TimeUnit& value) const;
    virtual TimeUnit valueFromText(const QString& text) const;
    QString prefix() const;
    QString suffix() const;
    QString unit() const;
    QString unit(const TimeUnit& value) const;
    QStringList units() const;

    //------------------------------
    //  SETTERS
    //------------------------------

    void setMaximum(TimeUnit max);
    void setMinimum(TimeUnit min);
    void setRange(TimeUnit min, TimeUnit max);
    void setSingleStep(TimeUnit step);
    void setValue(TimeUnit value);
    void setPrefix(const QString& prefix);
    void setSuffix(const QString& suffix);
    void setDecimals(int num = -1); // -1 shows whatever double wants to show

signals:

    void valueChanged(units::time::second_t value);
    void unitsChanged(QString name);
    void unitsChanged(int index);

protected:

    virtual StepEnabled stepEnabled() const override;
    virtual void fixup(QString &input) const override;
    virtual void stepBy(int steps) override;
    virtual QValidator::State validate(QString &input, int &pos) const override;

    void setTextFromValue(const TimeUnit& value);
    void setValueFromText(const QString& text);

private:

    void normalizeValue();
    void limitToMinMax();
    void roundToStep();

    void changeUnits(QString unitName);

private:

    TimeUnit m_minimum;
    TimeUnit m_maximum;
    TimeUnit m_step;
    TimeUnit m_value;

    QString m_prefix;
    QString m_suffix;

    int m_decimals = -1;
};

template<class Unit /*= units::time::second_t*/>
Unit TimeSpinbox::value() const
{
    return std::visit([this](auto &&value) -> Unit { return value; }, m_value);
}

template<class Unit /*= units::time::second_t*/>
Unit TimeSpinbox::singleStep() const
{
    return std::visit([this](auto &&value) -> Unit { return value; }, m_step);
}

template<class Unit /*= units::time::second_t*/>
Unit TimeSpinbox::maximum() const
{
    return std::visit([this](auto &&value) -> Unit { return value; }, m_maximum);
}

template<class Unit /*= units::time::second_t*/>
Unit TimeSpinbox::minimum() const
{
    return std::visit([this](auto &&value) -> Unit { return value; }, m_minimum);
}

//------------------------------
//  HELPER FUNCTIONS
//------------------------------

template<class Stream, class Variant, std::size_t... I>
void variantTypes_impl(const TimeSpinbox* ts, Stream& list, Variant var, std::index_sequence<I...> intSequence)
{
    // this is a fold expression: http://en.cppreference.com/w/cpp/language/fold
    //(..., (/*list <<*/ unit(std::declval<std::variant_alternative_t<I, Variant>>())));
    ((list << ts->unit(std::variant_alternative_t<I, Variant>{})), ...);
}

/// Prints out all the types stored within a variant
template<class Stream, class ...Args>
void variantTypes(const TimeSpinbox* ts, Stream& os, std::variant<Args...> var)
{
    variantTypes_impl(ts, os, var, std::index_sequence_for<Args...>{});
}

#endif // TimeSpinbox_h__

TimeSpinBox.cpp

#include "TimeSpinbox.h"
#include <QString>

//------------------------------
//  CONSTANTS
//------------------------------

const QString number = QStringLiteral(R"(([0-9]*\.?[0-9]*))");

#ifdef TIMESPINBOX_USE_SUBSECONDS
const QString str_ns = QStringLiteral(R"(\bns\b|\bnanoseconds?\b)");
const QString str_us = QStringLiteral(R"(\bus\b|\bmicroseconds?\b)");
const QString str_ms = QStringLiteral(R"(\bms\b|\bmilliseconds?\b)");
#endif
const QString str_s = QStringLiteral(R"(\bs\b|\bsecs?\b|\bseconds?\b)");
const QString str_m = QStringLiteral(R"(\bm\b|\bmins?\b|\bminutes?\b)");
const QString str_h = QStringLiteral(R"(\bh\b|\bhrs?\b|\bhours?\b)");

#ifdef TIMESPINBOX_USE_SUBSECONDS
const QString unit = '(' + str_ns + '|' + str_us + '|' + str_ms + '|' + str_s + '|' + str_m + '|' + str_h + ')';
#else
const QString unit = '(' + str_s + '|' + str_m + '|' + str_h + ')';
#endif

#ifdef TIMESPINBOX_USE_SUBSECONDS
const QRegularExpression rx_ns(str_ns);
const QRegularExpression rx_us(str_us);
const QRegularExpression rx_ms(str_ms);
#endif
const QRegularExpression rx_s(str_s);
const QRegularExpression rx_m(str_m);
const QRegularExpression rx_h(str_h);

const QRegularExpression rxNumber('^' + number + '$');
const QRegularExpression rxIntermediate('^' + number + " $");
const QRegularExpression rxUnit('^' + number + " ?" + unit + '$');

//------------------------------
//  USING
//------------------------------

using namespace units::literals;
using namespace units::time;

//--------------------------------------------------------------------------------------------------
//  METHODS
//--------------------------------------------------------------------------------------------------
TimeSpinbox::TimeSpinbox(QWidget* parent /*= nullptr*/)
    : QAbstractSpinBox(parent)
    , m_minimum(0_s)
    , m_maximum(std::numeric_limits<units::time::second_t>::max())
    , m_value(0_s)
    , m_step(1_s)
    , m_prefix("")
    , m_suffix("")
{
    QFontMetrics fm(this->lineEdit()->font());
    this->lineEdit()->setMaximumWidth(fm.width("9.999999 min"));

    normalizeValue();

    connect(this, &QAbstractSpinBox::editingFinished, [this]()
    {
        int pos;
        QString input = this->lineEdit()->text();
        if (this->validate(input, pos) == QValidator::Acceptable)
            setValueFromText(input);
        else
            setTextFromValue(m_value);
        this->clearFocus();
    });
}

void TimeSpinbox::setMaximum(TimeUnit max)
{
    m_maximum = max;
    normalizeValue();
}

void TimeSpinbox::setMinimum(TimeUnit min)
{
    m_minimum = min;
    normalizeValue();
}

void TimeSpinbox::setRange(TimeUnit min, TimeUnit max)
{
    m_maximum = max;
    m_minimum = min;
    normalizeValue();
}

void TimeSpinbox::setSingleStep(TimeUnit step)
{
    m_step = step;
    normalizeValue();
}

void TimeSpinbox::setValue(TimeUnit value)
{
    m_value = value;
    roundToStep();
    limitToMinMax();
    setTextFromValue(m_value);
}

void TimeSpinbox::setPrefix(const QString& prefix)
{
    QSignalBlocker bl(this);
    m_prefix = prefix;
    normalizeValue();
}

void TimeSpinbox::setSuffix(const QString& suffix)
{
    QSignalBlocker bl(this);
    m_suffix = suffix;
    normalizeValue();
}

void TimeSpinbox::setDecimals(int num /*= -1*/)
{
    QSignalBlocker bl(this);
    m_decimals = num;
    normalizeValue();
}

void TimeSpinbox::setTextFromValue(const TimeUnit& value)
{
    std::visit([this](auto&& value, auto&& prevValue)
    {
        this->lineEdit()->setText(m_prefix + textFromValue(value) + m_suffix);
        if(value != prevValue)
            emit this->valueChanged(value);
        if (!std::is_same_v<std::decay_t<decltype(value)>, std::decay<decltype(prevValue)>>)
            emit this->unitsChanged(m_value.index());
    }, value, m_value);
}

void TimeSpinbox::setValueFromText(const QString& text)
{
    setValue(valueFromText(text));
}

void TimeSpinbox::fixup(QString &input) const
{

}

void TimeSpinbox::stepBy(int steps)
{
    setValue(std::visit([steps](auto&& step, auto&& value) -> TimeUnit
    {
        value += steps * step;
        return value;
    }, m_step, m_value));
}

void TimeSpinbox::limitToMinMax()
{
    // cap to min
    m_value = std::visit([](auto&& value, auto&& min) -> TimeUnit
    {
        std::decay_t<decltype(value)> realMin = min;
        value = value < realMin ? realMin : value;
        return value;
    }, m_value, m_minimum);

    // cap to max
    m_value = std::visit([](auto&& value, auto&& max) -> TimeUnit
    {
        std::decay_t<decltype(value)> realMax = max;
        value = value > realMax ? realMax : value;
        return value;
    }, m_value, m_maximum);
}

void TimeSpinbox::normalizeValue()
{
    // triggers all necessary rounding/quantization
    setValue(m_value);
}

QValidator::State TimeSpinbox::validate(QString &input, int &pos) const
{
    if (input.isEmpty())
        return QValidator::Intermediate;
    else if(rxNumber.match(input).hasMatch())
        return QValidator::Acceptable;
    else if (rxIntermediate.match(input).hasMatch())
        return QValidator::Intermediate;
    else if (rxUnit.match(input).hasMatch())
        return QValidator::Acceptable;
    else if (rxUnit.match(input, 0, QRegularExpression::PartialPreferCompleteMatch).hasPartialMatch())
        return QValidator::Intermediate;

    return QValidator::Invalid;
}

QAbstractSpinBox::StepEnabled TimeSpinbox::stepEnabled() const
{
    return QAbstractSpinBox::StepDownEnabled | QAbstractSpinBox::StepUpEnabled;
}

void TimeSpinbox::roundToStep()
{
    m_value = std::visit([this](auto&& value, auto&& step) -> TimeUnit
    {
        double norm = value / step;
        norm = round(norm);
        value = norm * step;
        return value;
    }, m_value, m_step);
}

void TimeSpinbox::changeUnits(QString unitName)
{
    setValue(std::visit([this](auto&& newValImpl) -> TimeUnit
    {
        std::decay_t<decltype(newValImpl)> newVal = this->value();
        return newVal;      
    }, valueFromText(lineEdit()->text() + ' ' + unitName)));
}

QSize TimeSpinbox::sizeHint() const
{
    // copied/altered from QAbstractSpinbox sizehint
    ensurePolished();

    const QFontMetrics fm(fontMetrics());
    int h = lineEdit()->sizeHint().height();
    int w = 0;
    QString s;
    QString fixedContent = m_prefix + m_suffix + QLatin1Char(' ');
    s = textFromValue(m_value);
    s.truncate(18);
    s += fixedContent;
    w = qMax(w, fm.width(s));
    s = textFromValue(9999.999999_min);
    s.truncate(18);
    s += fixedContent;
    w = qMax(w, fm.width(s));

    if (this->specialValueText().size()) {
        s = this->specialValueText();
        w = qMax(w, fm.width(s));
    }
    w += 2; // cursor blinking space

    QStyleOptionSpinBox opt;
    initStyleOption(&opt);
    QSize hint(w, h);
    return style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this)/*.expandedTo(QApplication::globalStrut())*/;
}

QString TimeSpinbox::textFromValue(const TimeUnit& value) const
{
    return std::visit([this](auto&& value) -> QString
    {
        QString text = QString::fromStdString(units::time::to_string(value)).remove(QRegularExpression(R"(\s+.*)"));
        if (m_decimals < 0)
            return text;
        else
        {
            double val = text.toDouble();
            return QString::number(val, 'f', m_decimals);
        }
    }, value);
}

TimeSpinbox::TimeUnit TimeSpinbox::valueFromText(const QString& text) const
{
    auto numMatch = rxNumber.match(text);
    auto unitMatch = rxUnit.match(text);

    if (numMatch.hasMatch() && !numMatch.capturedTexts().isEmpty())
    {
        return std::visit([this, num = numMatch.captured(1).toDouble()](auto &&value)->TimeUnit
        {
            return (decltype(value))num;
        }, m_value);
    }
    else if (unitMatch.hasMatch() && unitMatch.capturedTexts().size() == 3)
    {
        double value = unitMatch.captured(1).toDouble();
        QString unit = unitMatch.captured(2);
        if (rx_m.match(unit).hasMatch())
            return minute_t(value);
        else if (rx_s.match(unit).hasMatch())
            return second_t(value);
        else if (rx_h.match(unit).hasMatch())
            return hour_t(value);
#ifdef TIMESPINBOX_USE_SUBSECONDS
        else if (rx_ms.match(unit).hasMatch())
            return millisecond_t(value);
        else if (rx_us.match(unit).hasMatch())
            return microsecond_t(value);
        else if (rx_ns.match(unit).hasMatch())
            return nanosecond_t(value);
#endif
    }

    return TimeUnit();
}

QString TimeSpinbox::prefix() const
{
    return m_prefix;
}

QString TimeSpinbox::suffix() const
{
    return m_suffix;
}

QString TimeSpinbox::unit() const
{
    return unit(m_value);
}

QString TimeSpinbox::unit(const TimeUnit& value) const
{
    // This may be the most amazing thing I've ever written in c++ - Nic H.
    return std::visit([this](auto&& value) -> QString
    {
        using T = std::decay_t<decltype(value)>;
        if constexpr(std::is_same_v<T, hour_t>)
            return QStringLiteral("hours");
        else if constexpr(std::is_same_v<T, minute_t>)
            return QStringLiteral("minutes");
        else if constexpr(std::is_same_v<T, second_t>)
            return QStringLiteral("seconds");
#ifdef TIMESPINBOX_USE_SUBSECONDS
        else if constexpr(std::is_same_v<T, millisecond_t>)
            return QStringLiteral("milliseconds");
        else if constexpr(std::is_same_v<T, microsecond_t>)
            return QStringLiteral("microseconds");
        else if constexpr(std::is_same_v<T, nanosecond_t>)
            return QStringLiteral("nanoseconds");
#endif
    }, value);
}

QStringList TimeSpinbox::units() const
{
    QStringList list;
    variantTypes(this, list, TimeUnit());
    return list;
}
behzaadh commented 3 years ago

I have not been familiarized with std::variant and std::visit. Thanks for sharing your code.