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:
dodde 2025-10-07 17:39:35 +02:00
parent 3df7bde01e
commit e02d48c05d
4 changed files with 256 additions and 4 deletions

View file

@ -31,11 +31,11 @@ add_compile_options(
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring hyprutils in Debug")
add_compile_definitions(HYPRLAND_DEBUG)
message(STATUS "Configuring hyprutils in Debug")
add_compile_definitions(HYPRLAND_DEBUG)
else()
add_compile_options(-O3)
message(STATUS "Configuring hyprutils in Release")
add_compile_options(-O3)
message(STATUS "Configuring hyprutils in Release")
endif()
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/*.hpp")
@ -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})

View 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
View 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;
}

View file

@ -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; \
} \
}