diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef581b..53130ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/include/hyprutils/string/Expression.hpp b/include/hyprutils/string/Expression.hpp new file mode 100644 index 0000000..d3693f5 --- /dev/null +++ b/include/hyprutils/string/Expression.hpp @@ -0,0 +1,174 @@ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Hyprutils::Expression { + + template + using calc_t = std::conditional_t, T, long double>; + + static inline void skip(std::string_view s, size_t& i) { + while (i < s.size() && std::isspace(static_cast(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 + static std::expected, std::string> parseExpr(std::string_view, size_t&, const std::unordered_map&); + + template + static std::expected, std::string> parsePrimary(std::string_view s, size_t& i, const std::unordered_map& vars) { + skip(s, i); + + if (match(s, i, '(')) { + auto v = parseExpr(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>(it->second); + return std::unexpected("Unknown variable: " + name); + } + + if (i >= s.size()) + return std::unexpected("Expected number, got ''"); + + char c = peek(s, i); + if (!std::isdigit(static_cast(c)) && c != '.') + return std::unexpected(std::string("Expected number, got: '") + c + "'"); + + calc_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>(tmp); + i = ptr - s.data(); + + return val; + } + + template + static std::expected, std::string> parseFactor(std::string_view s, size_t& i, const std::unordered_map& 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(s, i, vars); + if (!v) + return v; + + skip(s, i); + if (match(s, i, '%')) + *v /= 100.0; + + if (neg) + *v = -*v; + return v; + } + + template + static std::expected, std::string> parseTerm(std::string_view s, size_t& i, const std::unordered_map& vars) { + auto lhs = parseFactor(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(s, i, vars); + if (!rhs) + return rhs; + + if (op == '*') + *lhs *= *rhs; + else { + if constexpr (std::is_floating_point_v) + if (std::abs(*rhs) < 1e-12) + return std::unexpected("Division by zero"); + if constexpr (std::is_integral_v) + if (*rhs == 0) + return std::unexpected("Division by zero"); + *lhs /= *rhs; + } + } + return lhs; + } + + template + static std::expected, std::string> parseExpr(std::string_view s, size_t& i, const std::unordered_map& vars) { + auto lhs = parseTerm(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(s, i, vars); + if (!rhs) + return rhs; + + if (op == '+') + *lhs += *rhs; + else + *lhs -= *rhs; + } + return lhs; + } + + template + std::expected eval(std::string_view expr, const std::unordered_map& vars) { + size_t i = 0; + auto res = parseExpr(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(*res); + } + +} // namespace Hyprutils::Expression diff --git a/tests/expression.cpp b/tests/expression.cpp new file mode 100644 index 0000000..70b1830 --- /dev/null +++ b/tests/expression.cpp @@ -0,0 +1,48 @@ +#include +#include "shared.hpp" + +using namespace Hyprutils::Expression; + +int main() { + int ret = 0; + + std::unordered_map VARS_INT = {{"x", 5}, {"y", 10}}; + std::unordered_map VARS_FLOAT = {{"x", 5.0f}, {"y", 10.0f}}; + std::unordered_map VARS_DOUBLE = {{"x", 5.0f}, {"y", 10.0f}}; + try { + // int + EXPECT_RESULT_PASS(eval("(2+3)*4", VARS_INT), 20); + EXPECT_RESULT_PASS(eval("x + y", VARS_INT), 15); + EXPECT_RESULT_PASS(eval("x - y", VARS_INT), -5); + EXPECT_RESULT_PASS(eval("x * y", VARS_INT), 50); + EXPECT_RESULT_PASS(eval("y / x", VARS_INT), 2); + + EXPECT_RESULT_FAIL(eval("y / 0", VARS_INT), "Division by zero"); + EXPECT_RESULT_FAIL(eval("unknownVar + 1", VARS_INT), "Unknown variable: unknownVar"); + + // float + EXPECT_RESULT_PASS(eval("(2.0+3.0)*4.0", VARS_FLOAT), 20.0f); + EXPECT_RESULT_PASS(eval("x + y", VARS_FLOAT), 15.0f); + EXPECT_RESULT_PASS(eval("x - y", VARS_FLOAT), -5.0f); + EXPECT_RESULT_PASS(eval("x * y", VARS_FLOAT), 50.0f); + EXPECT_RESULT_PASS(eval("y / x", VARS_FLOAT), 2.0f); + + EXPECT_RESULT_FAIL(eval("y / 0.0", VARS_FLOAT), "Division by zero"); + EXPECT_RESULT_FAIL(eval("unknownVar + 1", VARS_FLOAT), "Unknown variable: unknownVar"); + + // double + EXPECT_RESULT_PASS(eval("(2.0 + 3.0) * 4.0", VARS_DOUBLE), 20.0); + EXPECT_RESULT_PASS(eval("x + y", VARS_DOUBLE), 15.0); + EXPECT_RESULT_PASS(eval("x - y", VARS_DOUBLE), -5.0); + EXPECT_RESULT_PASS(eval("x * y", VARS_DOUBLE), 50.0); + EXPECT_RESULT_PASS(eval("y / x", VARS_DOUBLE), 2.0); + + EXPECT_RESULT_FAIL(eval("y / 0.0", VARS_DOUBLE), "Division by zero"); + EXPECT_RESULT_FAIL(eval("unknownVar + 1", VARS_DOUBLE), "Unknown variable: unknownVar"); + } catch (const std::exception& e) { + std::cout << e.what() << "\n"; + ret = 1; + } + + return ret; +} diff --git a/tests/shared.hpp b/tests/shared.hpp index 33109f8..a627c6f 100644 --- a/tests/shared.hpp +++ b/tests/shared.hpp @@ -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; \ + } \ + }