Reputation: 6084
In one of my project I need to determine the conversion factors of fairly complex units. I was able to write a static conversion function in case of statically defined units using the excellent boost library Boost.Units
.
In my case the user enters the type of a conversion at run-time, so that I need a dynamic conversion function. A nice solution should use the already implemented functions in Boost.Units
. Is this possible?
My own final solution
After some thoughts I was able to derive the following partial solution to my problem, which is sufficient for my needs. I'm relying on boost-spirit
to parse the unit string, making this task indeed very easy. Great library!
Parsing unit strings might be a common task, that others might be interested in. Hence, I'm posting my final solution here including some tests for illustration.
The most important function is here convertUnit
computing the conversion factor from one unit to another, if this conversion is possible.
UnitParser.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;
}
UnitParser.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);
}
}
}
Upvotes: 3
Views: 263