Skip to content

feat(combinator): displayExpression with prefix notation #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/combinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
// You should have received a copy of the GNU General Public License
// along with metricq-combinator. If not, see <http://www.gnu.org/licenses/>.
#include "combinator.hpp"
#include "display_expression.hpp"

#include <metricq/logger/nitro.hpp>
#include <metricq/source.hpp>
#include <metricq/utils.hpp>

#include <fmt/format.h>

#include <numeric>
#include <stdexcept>

using Log = metricq::logger::nitro::Log;

Expand Down Expand Up @@ -93,6 +94,17 @@ void Combinator::on_transformer_config(const metricq::json& config)
// Register the combined metric as a new source metric
auto& metric = (*this)[combined_name];

try
{
metric.metadata["displayExpression"] = displayExpression(combined_expression);
}
catch (const std::runtime_error& e)
{
Log::error() << fmt::format(
"Failed to create the Display Expression, metric: {}, error: {}", metric.id(),
e.what());
}

if (combined_config.count("chunk_size"))
{
auto chunk_size = combined_config["chunk_size"].get<int>();
Expand Down
174 changes: 174 additions & 0 deletions src/display_expression.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#include <cmath>
#include <iomanip>
#include <metricq/json.hpp>
#include <numeric>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <vector>

inline std::string toFormattedString(double value)
{
// Special case for integers
if (std::round(value) == value)
{
return std::to_string(static_cast<long long>(value));
}

std::ostringstream oss;
oss << std::defaultfloat << std::setprecision(15);
oss << value;
std::string str = oss.str();

if (str.find('e') != std::string::npos)
{
oss.str("");
oss << std::fixed << std::setprecision(15);
oss << value;
str = oss.str();
}

size_t decimal_pos = str.find('.');
if (decimal_pos != std::string::npos)
{
// Remove trailing zeros
str.erase(str.find_last_not_of('0') + 1, std::string::npos);

if (str.back() == '.')
{
str.pop_back();
}

// Handle floating point representation issues
if (str.length() > decimal_pos + 15)
{
str = str.substr(0, decimal_pos + 15);
// Remove trailing zeros again
str.erase(str.find_last_not_of('0') + 1, std::string::npos);
if (str.back() == '.')
{
str.pop_back();
}
}
}

return str;
}

inline std::string handleBasicExpression(const nlohmann::json& expression)
{
if (expression.is_number())
{
const auto value = expression.get<double>();

return toFormattedString(value);
}

if (expression.is_string())
{
return expression.get<std::string>();
}

throw std::invalid_argument("Expression is not a basic type (number or string)!");
}

inline std::string handleOperatorExpression(const std::string& operation,
const std::string& leftStr, const std::string& rightStr)
{
if (operation.size() > 1)
{
throw std::invalid_argument("Invalid operator length!");
}

switch (operation[0])
{
case '+':
case '-':
case '*':
case '/':
return "(" + leftStr + " " + operation + " " + rightStr + ")";
default:
throw std::invalid_argument("Invalid operator: " + operation);
}
}

inline std::string handleCombinationExpression(const std::string& operation,
const std::vector<std::string>& inputs)
{
static const std::unordered_set<std::string> validAggregates = { "sum", "min", "max" };

if (validAggregates.find(operation) == validAggregates.end())
{
throw std::invalid_argument("Invalid aggregate operation: " + operation);
}

if (inputs.empty())
{
throw std::invalid_argument("Aggregate operation missing inputs!");
}

auto input = std::accumulate(std::next(inputs.begin()), inputs.end(), inputs[0],
[](std::string a, const std::string& b) { return a + ", " + b; });

return operation + "[" + input + "]";
}

inline std::string buildExpression(const nlohmann::json& expression)
{
if (expression.is_number() || expression.is_string())
{
return handleBasicExpression(expression);
}

if (!expression.is_object() || !expression.contains("operation"))
{
throw std::invalid_argument("Unknown expression format!");
}

std::string operation = expression.value("operation", "");

if (operation == "throttle")
{
if (!expression.contains("input"))
{
throw std::invalid_argument("Throttle does not contain a input");
}
return handleBasicExpression(expression["input"]);
}

if (expression.contains("left") && expression.contains("right"))
{
std::string leftStr = buildExpression(expression["left"]);
std::string rightStr = buildExpression(expression["right"]);
return handleOperatorExpression(operation, leftStr, rightStr);
}

if (expression.contains("inputs"))
{
if (!expression["inputs"].is_array())
{
throw std::invalid_argument("Inputs must be an array!");
}

std::vector<std::string> inputStrings;
for (const auto& input : expression["inputs"])
{
inputStrings.push_back(buildExpression(input));
}
return handleCombinationExpression(operation, inputStrings);
}

throw std::invalid_argument("Unsupported operation type: " + operation);
}

inline std::string displayExpression(const nlohmann::json& expression)
{
std::string result = buildExpression(expression);
if (!result.empty() && result.front() == '(' && result.back() == ')')
{
result = result.substr(1, result.size() - 2);
}

return result;
}
14 changes: 7 additions & 7 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
add_executable(metricq-combinator.test_single_derivation test_single_derivation.cpp)
add_test(metricq-combinator.test_single_derivation metricq-combinator.test_single_derivation)
macro(add_test_case test_name)
add_executable(${test_name} ${test_name}.cpp)
add_test(${test_name} ${test_name})
target_link_libraries(${test_name} PRIVATE metricq-combinator-lib)
endmacro()

target_link_libraries(
metricq-combinator.test_single_derivation
PRIVATE
metricq-combinator-lib
)
add_test_case(test_single_derivation)
add_test_case(test_display_expression)
110 changes: 110 additions & 0 deletions tests/test_display_expression.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "../src/display_expression.hpp"
#include <iostream>
#include <metricq/json.hpp>

struct TestMakeString
{
double input;
std::string expected;
};

struct TestDisplayExpression
{
nlohmann::json input;
std::string expected;
};

void runMakeStringTests()
{
std::vector<TestMakeString> stringTestCases = { { 100, "100" },
{ 10.0, "10" },
{ 10.3, "10.3" },
{ 10.25, "10.25" },
{ 0.125, "0.125" },
{ 0.123456789, "0.123456789" },
{ 123456789, "123456789" },
{ 123456789.01234, "123456789.01234" } };

for (const auto& testCase : stringTestCases)
{
std::string result = toFormattedString(testCase.input);
if (result != testCase.expected)
{
std::cerr << "Test case " << testCase.input << " failed:\n";
std::cerr << "Expected: " << testCase.expected << "\n";
std::cerr << "Got: " << result << "\n";
exit(1);
}
}
std::cout << "All toFormattedString test cases passed successfully!" << std::endl;
}

void testExpression()
{
std::vector<TestDisplayExpression> testCases;

testCases.emplace_back(TestDisplayExpression{
{ { "expression",
{ { "operation", "*" },
{ "left", 5 },
{ "right", { { "operation", "-" }, { "left", 45 }, { "right", 3 } } } } } },
"5 * (45 - 3)" });

testCases.emplace_back(TestDisplayExpression{
{ { "expression",
{ { "operation", "*" },
{ "left", { { "operation", "+" }, { "left", 1 }, { "right", 2 } } },
{ "right",
{ { "operation", "-" }, { "left", 10 }, { "right", "dummy.source" } } } } } },
"(1 + 2) * (10 - dummy.source)" });

testCases.emplace_back(TestDisplayExpression{
{ { "expression",
{ { "operation", "-" },
{ "left",
{ { "operation", "+" },
{ "left", 15.3 },
{ "right", { { "operation", "min" }, { "inputs", { 42, 24, 8, 12 } } } } } },
{ "right",
{ { "operation", "throttle" },
{ "cooldown_period", "42" },
{ "input", 8 } } } } } },
"(15.3 + min[42, 24, 8, 12]) - 8" });

// Test the cases
for (const auto& testCase : testCases)
{

auto expression = testCase.input["expression"];
std::string result = displayExpression(expression);

// Compare with expected result
if (result != testCase.expected) // comparing with the expected output
{
std::cerr << "Test case " << testCase.input << " failed:\n";
std::cerr << "Expected: " << testCase.expected << "\n";
std::cerr << "Got: " << result << "\n";
return;
}
}

std::cout << "All displayExpression test cases passed successfully!" << std::endl;
}

int main()
{
try
{
testExpression();
runMakeStringTests();

std::cout << "All tests passed successfully!" << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}

return 0;
}