3

在我的一个项目中,我需要确定相当复杂单位的转换因子。我能够使用出色的 boost 库编写静态转换函数,以防静态定义的单位Boost.Units

在我的情况下,用户在运行时输入转换的类型,所以我需要一个动态转换函数。一个好的解决方案应该使用Boost.Units. 这可能吗?

我自己的最终解决方案

经过一番思考,我能够为我的问题得出以下部分解决方案,这足以满足我的需求。我依靠boost-spirit解析单位字符串,使这项任务确实非常容易。很棒的图书馆!

解析单元字符串可能是其他人可能感兴趣的常见任务。因此,我在此处发布我的最终解决方案,包括一些用于说明的测试。最重要的功能是convertUnit计算从一个单位到另一个单位的转换因子,如果这种转换是可能的。

单元解析器.cpp

#include "UnitParser.h"
#pragma warning(push)
#pragma warning(disable: 4512 4100 4503 4127 4348 4459)

#include <map>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/qi_symbols.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/math/constants/constants.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <vector>
#include <algorithm>
using namespace boost;

namespace {
    struct modifier_ : spirit::qi::symbols<char, int> {
        modifier_() { add("m", -4)("c", -3)("k", 4); }
    } modifier;

    struct baseUnit_ : spirit::qi::symbols<char, UnitParser::UnitType>  {
        baseUnit_() {
            add
            ("g", UnitParser::UnitType::GRAM)
            ("m", UnitParser::UnitType::METER)
            ("s", UnitParser::UnitType::SECONDS)
            ("rad", UnitParser::UnitType::RADIANS)
            ("deg", UnitParser::UnitType::DEGREE)
            ("N", UnitParser::UnitType::NEWTON)
            ;
        }
    } baseUnit;

    class UnitParserImpl : public spirit::qi::grammar<std::string::iterator, UnitParser::Units()>
    {
    public:

        UnitParserImpl() : UnitParserImpl::base_type(unitsTop_)
        {
            using namespace  boost::spirit::qi;
            unitsTop_ = units_.alias();
            units_ = (unit_ % '*');
            unit_ = (-(modifier >> &baseUnit) >> baseUnit >> -(lexeme["^"] >> int_ ))[_val = boost::phoenix::construct<UnitParser::Unit>(_2, _3, _1)];
        }
        spirit::qi::rule<std::string::iterator, UnitParser::Units()> unitsTop_;
        spirit::qi::rule<std::string::iterator, UnitParser::Units()> units_;
        spirit::qi::rule<std::string::iterator, UnitParser::Unit()> unit_;
    };
}

boost::optional<UnitParser::Units> UnitParser::parse(const std::string& expression, std::string&& errorMessage)
{
    boost::optional<UnitParser::Units> result;
    try {
        Units units;
        std::string formula = expression;
        auto b = formula.begin();
        auto e = formula.end();
        UnitParserImpl parser;
        bool ok = spirit::qi::phrase_parse(b, e, parser, spirit::qi::space, units);
        if (!ok || b != e) {
            return result;
        }
        result = units;
        return result;
    }
    catch (const spirit::qi::expectation_failure<std::string::iterator>& except) {
        errorMessage = except.what();
        return result;
    }
}

std::map<UnitParser::UnitType, UnitParser::Dimension> dimMap() {
    std::map<UnitParser::UnitType, UnitParser::Dimension> ret;
    ret[UnitParser::UnitType::SECONDS] = UnitParser::Dimension({ 0,1,0,0 });
    ret[UnitParser::UnitType::METER] = UnitParser::Dimension({ 1,0,0,0 });
    ret[UnitParser::UnitType::DEGREE] = UnitParser::Dimension({ 0,0,1,0 });
    ret[UnitParser::UnitType::RADIANS] = UnitParser::Dimension({ 0,0,1,0 });
    ret[UnitParser::UnitType::GRAM] = UnitParser::Dimension({ 0,0,0,1 });
    ret[UnitParser::UnitType::NEWTON] = UnitParser::Dimension({ 1,-2,0,1 });
    return ret;
}

UnitParser::Dimension UnitParser::getDimension(const UnitParser::Units& units)
{
    auto map = dimMap();

    UnitParser::Dimension ret;
    for (auto unit : units) {
        if (map.find(unit.unitType) != map.end()) {
            auto dim=map[unit.unitType];
            auto exp = unit.exponent;
            ret.length += exp*dim.length;
            ret.time += exp*dim.time;
            ret.weigth += exp*dim.weigth;
            ret.planarAngle += exp*dim.planarAngle;
        }
    }
    return ret;
}

bool UnitParser::equalDimension(const Units& u1, const Units& u2)
{
    return getDimension(u1) == getDimension(u2);
}

bool UnitParser::checkDimension(const UnitParser::Units& u1, const UnitParser::Units& u2)
{
    return true;
}
// Bezogen auf die Einheiten: m,s,kg,rad
std::pair<double,int> UnitParser::getScale(const Units& units)
{
    double ret = 1.;
    int exp = 0;
    for (auto unit : units) {
        double scale = 1;       
        int e = 0;
        if (unit.unitType==UnitType::DEGREE) {
            scale = 180./boost::math::constants::pi<double>();
        }  

        if (unit.unitType == UnitType::GRAM) {
            e = unit.exponent*(unit.modifier-4);
        }
        else {
            e = unit.exponent*unit.modifier;
        }
        exp += e;
        ret *= scale;
    }
    return{ ret, exp };
}

boost::optional<double> UnitParser::convertUnit(const std::string& unitString1, const std::string& unitString2, std::string&& errorMessage)
{
    boost::optional<double> ret;
    auto unit1 = parse(unitString1);
    auto unit2 = parse(unitString2);
    if (!unit1) { errorMessage = unitString1 + " is not valid!"; return ret; }
    if (!unit2) { errorMessage = unitString2 + " is not valid!"; return ret; }
    if (!equalDimension(*unit1, *unit2)) {
        errorMessage = "Dimensions of " + unitString1 + " and " + unitString2 + " mismatch!"; return ret;
    }
    auto s1 = getScale(*unit1);
    auto s2 = getScale(*unit2);
    int exp = s1.second - s2.second;
    double scale = s1.first / s2.first;
    ret = scale*std::pow(10, exp);
    return ret;
}

单元解析器.h

#pragma once

#include <boost/optional.hpp>
#include <vector>

namespace UnitParser {
    enum class UnitType {
        SECONDS, METER, DEGREE, RADIANS, GRAM, NEWTON
    };
    struct Unit {
        Unit() {}
        Unit(const UnitType& unitType, const boost::optional<int> exponent, const boost::optional<int>& modifier) : unitType(unitType), exponent(exponent.value_or(1)), modifier(modifier.value_or(0)) {}
        UnitType unitType;
        int exponent;
        int modifier;
    };

    typedef std::vector<Unit> Units;

    struct Dimension {
        Dimension() {};
        Dimension(int length, int time, int planarAngle, int weigth) : length(length), time(time), planarAngle(planarAngle), weigth(weigth) {}
        int length = 0;
        int time = 0;
        int planarAngle = 0;
        int weigth = 0;
        bool operator==(const UnitParser::Dimension& dim) {
            return length == dim.length && planarAngle == dim.planarAngle && time == dim.time && weigth == dim.weigth;
        }

    };

    boost::optional<Units> parse(const std::string& string, std::string&& errorMessage=std::string());
    Dimension getDimension(const Units& units);
    bool equalDimension(const Units& u1, const Units& u2);
    bool checkDimension(const Units& u1, const Units& u2);
    std::pair<double,int> getScale(const Units& u1);

    boost::optional<double> convertUnit(const std::string& unitString1, const std::string& unitString2, std::string&& errorMessage=std::string());
}

UnitParserCatch.cpp

#define CATCH_CONFIG_MAIN
#include "catch.h"
#include "UnitParser.h"
#include <boost/math/constants/constants.hpp>
using namespace UnitParser;

TEST_CASE("ConvertUnit", "[UnitParser]") {
    SECTION("Simple") {
        auto s = convertUnit("mm^2", "cm^2"); // 1*mm^2 = 0.01*cm^2
        REQUIRE(s);
        CHECK(*s == 0.01);
    }
    SECTION("Newton") {
        auto s = convertUnit("N", "kg*m*s^-2");
        REQUIRE(s);
        CHECK(*s == 1.);
    }
    SECTION("Wrong") {
        std::string err;
        auto s = convertUnit("m", "m*kg", std::move(err));
        REQUIRE(!s);
        CHECK(!err.empty());
    }
}

TEST_CASE("Dimension", "[UnitParser]") {
    SECTION("Simple") {
        auto a=*parse("mm^2");
        auto dim=getDimension(a);
        CHECK(dim == Dimension(2, 0, 0, 0));
    }
    SECTION("Newton") {
        auto a = *parse("mN^2");
        auto dim = getDimension(a);
        CHECK(dim == Dimension(2, -4, 0, 2));
    }
    SECTION("Fits") {
        auto a = *parse("mm^2");
        auto b = *parse("cm^2");
        auto fits = equalDimension(a, b);
        CHECK(fits);
    }
    SECTION("Newton") {
        auto a = *parse("N");
        auto b = *parse("kg*m*s^-2");
        auto fits = equalDimension(a, b);
        CHECK(fits);
    }
    SECTION("NoFit") {
        auto a = *parse("mm^2*g");
        auto b = *parse("cm^2");
        auto fits = equalDimension(a, b);
        CHECK(!fits);
    }
}

TEST_CASE("Scale", "[UnitParser]") {
    SECTION("Length") {
        auto s = getScale(*parse("mm^2")); // 1*mm^2=1e-8*m^2
        CHECK(s == std::make_pair(1., -8));
    }
    SECTION("Degree") {
        auto s = getScale(*parse("deg"));
        CHECK(s == std::make_pair(180. / boost::math::constants::pi<double>(),0));
    }
    SECTION("Complex") {
        auto s = getScale(*parse("km^2*kg")); 
        CHECK(s == std::make_pair(1., 8));
    }
}

TEST_CASE("Simple", "[UnitParser]") {
    SECTION("Complex") {
        SECTION("Full") {
            auto u = parse("mm^2");
            CHECK(u);
        }
        SECTION("Many") {
            auto u = parse("mm^2*ms^-1");
            CHECK(u);
        }
    }
    SECTION("Units") {
        SECTION("Newton") {
            auto u = parse("N");
            CHECK(u);
        }
        SECTION("Meter") {
            auto u = parse("m"); 
            CHECK(u);
        }
        SECTION("Seconds") {
            auto u = parse("s");
            CHECK(u);
            SECTION("Exponent") {
                CHECK(parse("s^2"));
                CHECK(parse("ms^-2"));
                CHECK(parse("ks^-3"));
            }
        }
        SECTION("PlanarAngle") {
            auto u = parse("deg");
            CHECK(u);
        }
    }
}
4

0 回答 0