Skip to content

Commit

Permalink
Precise parsing of floats (#4)
Browse files Browse the repository at this point in the history
* Precise parsing of floats

* Updated version
  • Loading branch information
Nitrillo authored Oct 18, 2022
1 parent 40d9b7e commit 2be2a1d
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 6 deletions.
4 changes: 3 additions & 1 deletion examples/function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ auto main() -> int {
std::cout << result.getError() << std::endl;
}

pm.addFunction("rand", [](picomath::number_t input) -> picomath::number_t { return input * rand() / RAND_MAX; });
pm.addFunction("rand", [](picomath::number_t input) -> picomath::number_t {
return input * static_cast<picomath::number_t>(rand()) / static_cast<picomath::number_t>(RAND_MAX);
});
result = pm.evalExpression("rand(100)");
if (result.isOk()) {
std::cout << "Result: " << result.getResult() << std::endl;
Expand Down
57 changes: 52 additions & 5 deletions include/picomath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @file picomath.hpp
* @author Cesar Guirao Robles (a.k.a. Nitro) <cesar@no2.es>
* @brief Math expression evaluation. BSD 3-Clause License
* @version 1.0.0
* @version 1.1.0
* @date 2022-06-01
*
* @copyright Copyright (c) 2022, Cesar Guirao Robles (a.k.a. Nitro) <cesar@no2.es>
Expand All @@ -21,6 +21,12 @@

namespace picomath {

// Enable to use correct and precise float parsing
// #define PM_USE_PRECISE_FLOAT_PARSING

// Enable to use floats instead of doubles
// #define PM_USE_FLOAT

#ifndef PM_MAX_ARGUMENTS
#define PM_MAX_ARGUMENTS 8
#endif
Expand All @@ -35,8 +41,14 @@ class PicoMath;

#ifdef PM_USE_FLOAT
using number_t = float;
#ifdef PM_USE_PRECISE_FLOAT_PARSING
#define PM_STR_TO_FLOAT strtof
#endif
#else
using number_t = double;
#ifdef PM_USE_PRECISE_FLOAT_PARSING
#define PM_STR_TO_FLOAT strtod
#endif
#endif
using argument_list_t = std::array<number_t, PM_MAX_ARGUMENTS>;
using error_t = std::string;
Expand Down Expand Up @@ -305,6 +317,18 @@ class Expression {
return *str == 0;
}

[[nodiscard]] PM_INLINE auto isSign() const -> bool {
return *str == '+' || *str == '-';
}

[[nodiscard]] PM_INLINE auto isExponent() const -> bool {
// This doesn't read out of bounds because:
// if first character is 'e' or 'E', next character can be a valid character
// or zero if it's the end of the string
return (*str == 'e' || *str == 'E') &&
(*(str + 1) == '+' || *(str + 1) == '-' || (*(str + 1) >= '0' && *(str + 1) <= '9'));
}

PM_INLINE auto parseFunction(std::string_view identifier) noexcept -> Result {
auto f = context.functions.find(identifier);
if (PM_UNLIKELY(f == context.functions.end())) {
Expand Down Expand Up @@ -409,9 +433,17 @@ class Expression {
PM_INLINE auto parseNumber() noexcept -> Result {
number_t ret = 0;

while (isDigit()) {
ret *= 10;
ret += *str - '0';
#if defined(PM_USE_PRECISE_FLOAT_PARSING)
char *end;
ret = PM_STR_TO_FLOAT(str, &end);
if (PM_UNLIKELY(errno == ERANGE)) {
std::string_view identifier{str, static_cast<size_t>(end - str)};
return generateError("Float out of range", identifier);
}
str = end;
#else
while (PM_LIKELY(isDigit())) {
ret = ret * 10 + (*str - '0');
consume();
}
// Decimal point
Expand All @@ -424,7 +456,22 @@ class Expression {
consume();
}
}

if (PM_UNLIKELY(isExponent())) {
consume();
bool sign = false;
if (isSign()) {
if (consume() == '-') {
sign = true;
}
}
int exp = 0;
while (PM_LIKELY(isDigit())) {
exp = exp * 10 + (*str - '0');
consume();
}
ret *= std::pow(static_cast<number_t>(10), sign ? -exp : exp);
}
#endif
// Space before units
consumeSpace();

Expand Down
6 changes: 6 additions & 0 deletions test/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ TEST_CASE("Simple expressions") {
REQUIRE(AreSame(ctx.evalExpression("2.003 * 1000").getResult(), 2003.0));
REQUIRE(AreSame(ctx.evalExpression(".1234").getResult(), 0.1234));
REQUIRE(AreSame(ctx.evalExpression("123.1234").getResult(), 123.1234));
REQUIRE(AreSame(ctx.evalExpression("5e+7").getResult(), 50000000.0));
REQUIRE(AreSame(ctx.evalExpression("5e7").getResult(), 50000000.0));
REQUIRE(AreSame(ctx.evalExpression("5.0E-7").getResult(), 0.00000049999999873762));
REQUIRE(AreSame(ctx.evalExpression("-(2+2)").getResult(), -4));
REQUIRE(ctx.evalExpression("(2+2)").isOk());
REQUIRE_FALSE(ctx.evalExpression("(2+2)").isError());
Expand Down Expand Up @@ -62,6 +65,9 @@ TEST_CASE("Custom units") {
REQUIRE(AreSame(ctx.evalExpression("1km").getResult(), 1000.0));
REQUIRE(AreSame(ctx.evalExpression("100km").getResult(), 100000.0));
REQUIRE(AreSame(ctx.evalExpression("100km - 10").getResult(), 100000.0 - 10.0));

ctx.addUnit("em") = 10.0;
REQUIRE(AreSame(ctx.evalExpression("1em").getResult(), 10.0));
}

TEST_CASE("Invalid expressions") {
Expand Down

0 comments on commit 2be2a1d

Please sign in to comment.