8  Custom Extensions

Binding to Custom Variables

Along with the built-in functions and constants, you can create custom variables for use in expressions:

#include "tinyexpr.h"
#include <iostream>
#include <iomanip>

int main(int argc, char* argv[])
    {
    if (argc < 2)
        {
        std::cout << "Usage: example \"expression\"\n";
        return EXIT_SUCCESS;
        }
    const char* expression = argv[1];

    te_type x{ 0 }, y{ 0 }; // x and y are bound at eval-time.
    // Store variable names and pointers.
    te_parser tep;
    tep.set_variables_and_functions({ {"x", &x}, {"y", &y} });

    if (tep.compile(expression)) // Compile the expression and check for errors.
        {
        /* The variables can be changed here, and evaluate can be called multiple
           times. This is efficient because the parsing has already been done.*/
        x = 3; y = 4;
        const auto r = tep.evaluate();
        std::cout << "Result:\n\t" << r << "\n";
        }
    else // Show the user where the error is at.
        {
        std::cout << "\t " << std::setfill(' ') <<
            std::setw(tep.get_last_error_position()) << '^' << "\tError here\n";
        }
    return EXIT_SUCCESS;
    }

Binding to Custom Functions

TinyExpr++ can also call custom functions. Here is a short example:

te_type my_sum(te_type a, te_type b)
    {
    /* Example function that adds two numbers together. */
    return a + b;
    }

te_parser tep;
tep.set_variables_and_functions(
{
    { "mysum", my_sum } // function pointer
});

const auto r = tep.evaluate("mysum(5, 6)");
// will be 11

Here is an example of using a lambda:

te_parser tep;
tep.set_variables_and_functions({
    { "mysum",
        [](te_type a, te_type b) noexcept
            { return a + b; } }
    });

const auto r = tep.evaluate("mysum(5, 6)");
// will be 11

Binding to Functions Accepting Strings

Functions can also accept quoted string literals from a formula. For example:

DBQUERY("/Equipment/Temp", 3)

Such a function takes a std::span of te_arg, where each te_arg is either a number or a string:

using te_arg = std::variant<te_type, std::string_view>;

using te_arg_fun = te_type (*)(std::span<const te_arg>);
using te_arg_confun = te_type (*)(const te_expr*, std::span<const te_arg>);

Any argument can be a string or a number, in any position, and there is no limit on how many are passed. Because of this, the parser does not verify the argument count the way it does for te_fun0te_fun24. Review args.size() and each argument’s type yourself, returning te_parser::te_nan if the call is not valid:

te_type db_query(std::span<const te_arg> args)
    {
    if (args.size() != 2 ||
        !std::holds_alternative<std::string_view>(args[0]) ||
        !std::holds_alternative<te_type>(args[1]))
        { return te_parser::te_nan; }

    // 'lookup' could be a database query or such
    return lookup(std::get<std::string_view>(args[0]),
                  std::get<te_type>(args[1]));
    }

te_parser tep;
tep.set_variables_and_functions(
{
    { "dbquery", static_cast<te_arg_fun>(db_query) }
});

const auto r = tep.evaluate(R"(DBQUERY("/Equipment/Temp", 3))");

Note the cast to te_arg_fun; this tells the compiler which of the function types to bind to.

A te_arg_confun receives a client object as its first argument, exactly like the te_confun0te_confun24 functions described in Binding to Custom Classes:

class te_database : public te_expr
    {
public:
    explicit te_database(const te_variable_flags type) noexcept :
        te_expr(type) {}
    std::map<std::string, te_type, std::less<>> m_rows =
        { { "voltage", 240 }, { "current", 13 } };
    };

te_type query_db(const te_expr* context, std::span<const te_arg> args)
    {
    auto* db = dynamic_cast<const te_database*>(context);
    if (db == nullptr || args.size() != 1 ||
        !std::holds_alternative<std::string_view>(args[0]))
        { return te_parser::te_nan; }

    const auto found = db->m_rows.find(std::get<std::string_view>(args[0]));
    return (found == db->m_rows.cend()) ? te_parser::te_nan : found->second;
    }

te_database db{ TE_DEFAULT };

te_parser tep;
tep.set_variables_and_functions(
    {
        { "query", static_cast<te_arg_confun>(query_db), TE_DEFAULT, &db }
    });

// will be 3120
const auto r = tep.evaluate(R"(QUERY("voltage") * QUERY("current"))");

A string literal is opened and closed by a double quote. There are no escape sequences, so a literal cannot itself contain a double quote.

String literals are only valid as arguments to these functions. Using one anywhere else (sin("abc"), 1 + "abc", or "abc" on its own) is a syntax error.

A std::string_view argument points into the parser’s copy of the expression, so it remains valid for the lifetime of the compiled expression. The std::span, however, is only valid for the duration of the call; copy anything you need to keep.

Binding to Custom Classes

A class derived from te_expr can be bound to custom functions. This enables you to have full access to an object (via these functions) when parsing an expression.

The following demonstrates creating a te_expr-derived class which contains an array of values:

class te_expr_array : public te_expr
    {
public:
    explicit te_expr_array(const te_variable_flags type) noexcept :
        te_expr(type) {}
    std::array<te_type, 5> m_data = { 5, 6, 7, 8, 9 };
    };

Next, create two functions that can accept this object and perform actions on it. (Note that proper error handling is not included for brevity.):

// Returns the value of a cell from the object's data.
te_type cell(const te_expr* context, te_type a)
    {
    auto* c = dynamic_cast<const te_expr_array*>(context);
    return static_cast<te_type>(c->m_data[static_cast<size_t>(a)]);
    }

// Returns the max value of the object's data.
te_type cell_max(const te_expr* context)
    {
    auto* c = dynamic_cast<const te_expr_array*>(context);
    return static_cast<te_type>(
        *std::max_element(c->m_data.cbegin(), c->m_data.cend()));
    }

Finally, create an instance of the class and connect the custom functions to it, while also adding them to the parser:

te_expr_array teArray{ TE_DEFAULT };

te_parser tep;
tep.set_variables_and_functions(
    {
        {"cell", cell, TE_DEFAULT, &teArray},
        {"cellmax", cell_max, TE_DEFAULT, &teArray}
    });

// change the object's data and evaluate their summation
// (will be 30)
teArray.m_data = { 6, 7, 8, 5, 4 };
auto result = tep.evaluate("SUM(CELL 0, CELL 1, CELL 2, CELL 3, CELL 4)");

// call the other function, getting the object's max value
// (will be 8)
result = tep.evaluate("CellMax()");

Valid variable and function names consist of a letter or underscore followed by any combination of: letters a–z or A–Z, digits 0–9, periods, and underscores.