mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 05:50:11 +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")
|
||||
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
|
||||
install(TARGETS hyprutils)
|
||||
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"; \
|
||||
} \
|
||||
} 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