mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 08:10:10 +01:00
Added expressions.
Added expressions with var substitution.
Using std::expected to handle errors.
Usage example:
auto val = Hyprutils::Expression::eval("5 + 5 * (2 / w)", {{"w", 10}});
if(val)
doThingWithValue(*val);
if(!val)
std::cout << val.error();
This commit is contained in:
parent
3df7bde01e
commit
e02d48c05d
4 changed files with 256 additions and 4 deletions
|
|
@ -112,6 +112,14 @@ add_test(
|
||||||
COMMAND hyprutils_animation "utils")
|
COMMAND hyprutils_animation "utils")
|
||||||
add_dependencies(tests hyprutils_animation)
|
add_dependencies(tests hyprutils_animation)
|
||||||
|
|
||||||
|
add_executable(hyprutils_expression "tests/expression.cpp")
|
||||||
|
target_link_libraries(hyprutils_expression PRIVATE hyprutils PkgConfig::deps)
|
||||||
|
add_test(
|
||||||
|
NAME "Expression"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
|
||||||
|
COMMAND hyprutils_expression "expression")
|
||||||
|
add_dependencies(tests hyprutils_expression)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
install(TARGETS hyprutils)
|
install(TARGETS hyprutils)
|
||||||
install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
|
|
|
||||||
174
include/hyprutils/string/Expression.hpp
Normal file
174
include/hyprutils/string/Expression.hpp
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <expected>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace Hyprutils::Expression {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using calc_t = std::conditional_t<std::is_floating_point_v<T>, T, long double>;
|
||||||
|
|
||||||
|
static inline void skip(std::string_view s, size_t& i) {
|
||||||
|
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i])))
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char peek(std::string_view s, size_t i) {
|
||||||
|
return i < s.size() ? s[i] : '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char get(std::string_view s, size_t& i) {
|
||||||
|
return i < s.size() ? s[i++] : '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool match(std::string_view s, size_t& i, char c) {
|
||||||
|
if (peek(s, i) == c) {
|
||||||
|
++i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::expected<calc_t<T>, std::string> parseExpr(std::string_view, size_t&, const std::unordered_map<std::string, T>&);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::expected<calc_t<T>, std::string> parsePrimary(std::string_view s, size_t& i, const std::unordered_map<std::string, T>& vars) {
|
||||||
|
skip(s, i);
|
||||||
|
|
||||||
|
if (match(s, i, '(')) {
|
||||||
|
auto v = parseExpr<T>(s, i, vars);
|
||||||
|
if (!v)
|
||||||
|
return v;
|
||||||
|
skip(s, i);
|
||||||
|
if (!match(s, i, ')'))
|
||||||
|
return std::unexpected("Expected ')'");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::isalpha(peek(s, i))) {
|
||||||
|
std::string name;
|
||||||
|
while (std::isalnum(peek(s, i)))
|
||||||
|
name += get(s, i);
|
||||||
|
if (auto it = vars.find(name); it != vars.end())
|
||||||
|
return static_cast<calc_t<T>>(it->second);
|
||||||
|
return std::unexpected("Unknown variable: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= s.size())
|
||||||
|
return std::unexpected("Expected number, got '<end of input>'");
|
||||||
|
|
||||||
|
char c = peek(s, i);
|
||||||
|
if (!std::isdigit(static_cast<unsigned char>(c)) && c != '.')
|
||||||
|
return std::unexpected(std::string("Expected number, got: '") + c + "'");
|
||||||
|
|
||||||
|
calc_t<T> val{};
|
||||||
|
double tmp{};
|
||||||
|
auto [ptr, ec] = std::from_chars(s.data() + i, s.data() + s.size(), tmp);
|
||||||
|
if (ec != std::errc())
|
||||||
|
return std::unexpected("Invalid number");
|
||||||
|
val = static_cast<calc_t<T>>(tmp);
|
||||||
|
i = ptr - s.data();
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::expected<calc_t<T>, std::string> parseFactor(std::string_view s, size_t& i, const std::unordered_map<std::string, T>& vars) {
|
||||||
|
skip(s, i);
|
||||||
|
bool neg = false;
|
||||||
|
while (match(s, i, '+') || match(s, i, '-')) {
|
||||||
|
if (s[i - 1] == '-')
|
||||||
|
neg = !neg;
|
||||||
|
skip(s, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto v = parsePrimary<T>(s, i, vars);
|
||||||
|
if (!v)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
skip(s, i);
|
||||||
|
if (match(s, i, '%'))
|
||||||
|
*v /= 100.0;
|
||||||
|
|
||||||
|
if (neg)
|
||||||
|
*v = -*v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::expected<calc_t<T>, std::string> parseTerm(std::string_view s, size_t& i, const std::unordered_map<std::string, T>& vars) {
|
||||||
|
auto lhs = parseFactor<T>(s, i, vars);
|
||||||
|
if (!lhs)
|
||||||
|
return lhs;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
skip(s, i);
|
||||||
|
char op = peek(s, i);
|
||||||
|
if (op != '*' && op != '/')
|
||||||
|
break;
|
||||||
|
get(s, i);
|
||||||
|
|
||||||
|
auto rhs = parseFactor<T>(s, i, vars);
|
||||||
|
if (!rhs)
|
||||||
|
return rhs;
|
||||||
|
|
||||||
|
if (op == '*')
|
||||||
|
*lhs *= *rhs;
|
||||||
|
else {
|
||||||
|
if constexpr (std::is_floating_point_v<T>)
|
||||||
|
if (std::abs(*rhs) < 1e-12)
|
||||||
|
return std::unexpected("Division by zero");
|
||||||
|
if constexpr (std::is_integral_v<T>)
|
||||||
|
if (*rhs == 0)
|
||||||
|
return std::unexpected("Division by zero");
|
||||||
|
*lhs /= *rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static std::expected<calc_t<T>, std::string> parseExpr(std::string_view s, size_t& i, const std::unordered_map<std::string, T>& vars) {
|
||||||
|
auto lhs = parseTerm<T>(s, i, vars);
|
||||||
|
if (!lhs)
|
||||||
|
return lhs;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
skip(s, i);
|
||||||
|
char op = peek(s, i);
|
||||||
|
if (op != '+' && op != '-')
|
||||||
|
break;
|
||||||
|
get(s, i);
|
||||||
|
|
||||||
|
auto rhs = parseTerm<T>(s, i, vars);
|
||||||
|
if (!rhs)
|
||||||
|
return rhs;
|
||||||
|
|
||||||
|
if (op == '+')
|
||||||
|
*lhs += *rhs;
|
||||||
|
else
|
||||||
|
*lhs -= *rhs;
|
||||||
|
}
|
||||||
|
return lhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::expected<T, std::string> eval(std::string_view expr, const std::unordered_map<std::string, T>& vars) {
|
||||||
|
size_t i = 0;
|
||||||
|
auto res = parseExpr<T>(expr, i, vars);
|
||||||
|
if (!res)
|
||||||
|
return std::unexpected(res.error());
|
||||||
|
skip(expr, i);
|
||||||
|
if (i != expr.size())
|
||||||
|
return std::unexpected("Unexpected trailing characters");
|
||||||
|
return static_cast<T>(*res);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Hyprutils::Expression
|
||||||
48
tests/expression.cpp
Normal file
48
tests/expression.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#include <hyprutils/string/Expression.hpp>
|
||||||
|
#include "shared.hpp"
|
||||||
|
|
||||||
|
using namespace Hyprutils::Expression;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, int> VARS_INT = {{"x", 5}, {"y", 10}};
|
||||||
|
std::unordered_map<std::string, float> VARS_FLOAT = {{"x", 5.0f}, {"y", 10.0f}};
|
||||||
|
std::unordered_map<std::string, double> VARS_DOUBLE = {{"x", 5.0f}, {"y", 10.0f}};
|
||||||
|
try {
|
||||||
|
// int
|
||||||
|
EXPECT_RESULT_PASS(eval<int>("(2+3)*4", VARS_INT), 20);
|
||||||
|
EXPECT_RESULT_PASS(eval<int>("x + y", VARS_INT), 15);
|
||||||
|
EXPECT_RESULT_PASS(eval<int>("x - y", VARS_INT), -5);
|
||||||
|
EXPECT_RESULT_PASS(eval<int>("x * y", VARS_INT), 50);
|
||||||
|
EXPECT_RESULT_PASS(eval<int>("y / x", VARS_INT), 2);
|
||||||
|
|
||||||
|
EXPECT_RESULT_FAIL(eval<int>("y / 0", VARS_INT), "Division by zero");
|
||||||
|
EXPECT_RESULT_FAIL(eval<int>("unknownVar + 1", VARS_INT), "Unknown variable: unknownVar");
|
||||||
|
|
||||||
|
// float
|
||||||
|
EXPECT_RESULT_PASS(eval<float>("(2.0+3.0)*4.0", VARS_FLOAT), 20.0f);
|
||||||
|
EXPECT_RESULT_PASS(eval<float>("x + y", VARS_FLOAT), 15.0f);
|
||||||
|
EXPECT_RESULT_PASS(eval<float>("x - y", VARS_FLOAT), -5.0f);
|
||||||
|
EXPECT_RESULT_PASS(eval<float>("x * y", VARS_FLOAT), 50.0f);
|
||||||
|
EXPECT_RESULT_PASS(eval<float>("y / x", VARS_FLOAT), 2.0f);
|
||||||
|
|
||||||
|
EXPECT_RESULT_FAIL(eval<float>("y / 0.0", VARS_FLOAT), "Division by zero");
|
||||||
|
EXPECT_RESULT_FAIL(eval<float>("unknownVar + 1", VARS_FLOAT), "Unknown variable: unknownVar");
|
||||||
|
|
||||||
|
// double
|
||||||
|
EXPECT_RESULT_PASS(eval<double>("(2.0 + 3.0) * 4.0", VARS_DOUBLE), 20.0);
|
||||||
|
EXPECT_RESULT_PASS(eval<double>("x + y", VARS_DOUBLE), 15.0);
|
||||||
|
EXPECT_RESULT_PASS(eval<double>("x - y", VARS_DOUBLE), -5.0);
|
||||||
|
EXPECT_RESULT_PASS(eval<double>("x * y", VARS_DOUBLE), 50.0);
|
||||||
|
EXPECT_RESULT_PASS(eval<double>("y / x", VARS_DOUBLE), 2.0);
|
||||||
|
|
||||||
|
EXPECT_RESULT_FAIL(eval<double>("y / 0.0", VARS_DOUBLE), "Division by zero");
|
||||||
|
EXPECT_RESULT_FAIL(eval<double>("unknownVar + 1", VARS_DOUBLE), "Unknown variable: unknownVar");
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cout << e.what() << "\n";
|
||||||
|
ret = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
@ -30,3 +30,25 @@ namespace Colors {
|
||||||
std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got (" << RESULT.x << ", " << RESULT.y << ")\n"; \
|
std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got (" << RESULT.x << ", " << RESULT.y << ")\n"; \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
|
#define EXPECT_RESULT_PASS(EXPR, VAL) \
|
||||||
|
{ \
|
||||||
|
const auto RES = EXPR; \
|
||||||
|
if (RES) { \
|
||||||
|
EXPECT(RES.value(), VAL); \
|
||||||
|
} else { \
|
||||||
|
std::cout << Colors::RED << "Unexpected failure: " << Colors::RESET << RES.error() << "\n"; \
|
||||||
|
ret = 1; \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define EXPECT_RESULT_FAIL(EXPR, ERR) \
|
||||||
|
{ \
|
||||||
|
auto RES = EXPR; \
|
||||||
|
if (!RES) { \
|
||||||
|
EXPECT(RES.error(), ERR); \
|
||||||
|
} else { \
|
||||||
|
std::cout << Colors::RED << "Unexpected success: " << Colors::RESET << RES.value() << "\n"; \
|
||||||
|
ret = 1; \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue