<commit_before>#include "catch_ext.hpp"

#include <mapnik/expression.hpp>
#include <mapnik/expression_evaluator.hpp>
#include <mapnik/expression_string.hpp>
#include <mapnik/wkt/wkt_factory.hpp>
#include <mapnik/feature.hpp>
#include <mapnik/feature_factory.hpp>
#include <mapnik/unicode.hpp>

#include <functional>
#include <vector>

namespace {

template <typename Properties>
mapnik::feature_ptr make_test_feature(mapnik::value_integer id, std::string const& wkt, Properties const& prop)
{
    auto ctx = std::make_shared<mapnik::context_type>();
    mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx, id));
    mapnik::geometry::geometry<double> geom;
    if (mapnik::from_wkt(wkt, geom))
    {
        feature->set_geometry(std::move(geom));
    }

    for (auto const& kv : prop)
    {
        feature->put_new(kv.first, kv.second);
    }
    return feature;
}

template <typename Feature, typename Expression>
mapnik::value_type evaluate(Feature const& feature, Expression const& expr)
{
    auto value = mapnik::util::apply_visitor(
        mapnik::evaluate<Feature, mapnik::value_type, mapnik::attributes>(
            feature, mapnik::attributes()), expr);
    return value;
}

mapnik::value evaluate_string(mapnik::feature_ptr const& feature, std::string const& str)
{
    auto expr = mapnik::parse_expression(str);
    return evaluate(*feature, *expr);
}

std::string parse_and_dump(std::string const& str)
{
    auto expr = mapnik::parse_expression(str);
    return mapnik::to_expression_string(*expr);
}

} // namespace

TEST_CASE("expressions")
{
    using namespace std::placeholders;
    using properties_type = std::vector<std::pair<std::string, mapnik::value> > ;
    mapnik::transcoder tr("utf8");

    properties_type prop = {{ "foo"   , tr.transcode("bar") },
                            { "name"  , tr.transcode("Québec")},
                            { "double", mapnik::value_double(1.23456)},
                            { "int"   , mapnik::value_integer(123)},
                            { "bool"  , mapnik::value_bool(true)},
                            { "null"  , mapnik::value_null()}};

    auto feature = make_test_feature(1, "POINT(100 200)", prop);
    auto eval = std::bind(evaluate_string, feature, _1);
    auto approx = Approx::custom().epsilon(1e-6);

    TRY_CHECK(eval(" [foo]='bar' ") == true);

    // primary expressions
    // null
    TRY_CHECK(parse_and_dump("null") == "null");
    // boolean
    TRY_CHECK(parse_and_dump("true") == "true");
    TRY_CHECK(parse_and_dump("false") == "false");
    // floating point
    TRY_CHECK(parse_and_dump("3.14159") == "3.14159");
    // integer
    TRY_CHECK(parse_and_dump("123") == "123");
    // unicode
    TRY_CHECK(parse_and_dump("'single-quoted string'") == "'single-quoted string'");
    TRY_CHECK(parse_and_dump("\"double-quoted string\"") == "'double-quoted string'");

    // floating point constants
    TRY_CHECK(parse_and_dump("pi") == "3.14159");
    TRY_CHECK(parse_and_dump("deg_to_rad") == "0.0174533");
    TRY_CHECK(parse_and_dump("rad_to_deg") == "57.2958");

    // unary functions
    // sin / cos
    TRY_CHECK(eval(" sin(0.25 * pi) / cos(0.25 * pi) ").to_double() == approx(1.0));
    // tan
    TRY_CHECK(eval(" tan(0.25 * pi) ").to_double() == approx(1.0));
    // atan
    TRY_CHECK(eval(" rad_to_deg * atan(1.0) ").to_double() == approx(45.0));
    // exp
    TRY_CHECK(eval(" exp(0.0) ") == 1.0);
    // abs
    TRY_CHECK(eval(" abs(cos(-pi)) ") == 1.0);
    // length (string)
    TRY_CHECK(eval(" length('1234567890') ").to_int() == 10);

    // binary functions
    // min
    TRY_CHECK(eval(" min(-0.01, 0.001) ") == -0.01);
    // max
    TRY_CHECK(eval(" max(0.01, -0.1) ") == 0.01);
    // pow
    TRY_CHECK(eval(" pow(2, 32) ") == 4294967296.0);

    // geometry types
    TRY_CHECK(eval(" [mapnik::geometry_type] = point ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] <> linestring ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] != polygon ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] neq collection ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] eq collection ") == false);

    //unary expression
    TRY_CHECK(eval(" -123.456 ") == -123.456);
    TRY_CHECK(eval(" +123.456 ") == 123.456);

    // multiplicative/additive
    auto expr = mapnik::parse_expression("(2.0 * 2.0 + 3.0 * 3.0)/(2.0 * 2.0 - 3.0 * 3.0)");
    TRY_CHECK(evaluate(*feature, *expr) == -2.6);
    auto expr2 = mapnik::parse_expression("(2.0 * 2.0 + 3.0 * 3.0)/((2.0 - 3.0) * (2.0 + 3.0))");
    TRY_CHECK(evaluate(*feature, *expr) == evaluate(*feature, *expr2));

    // logical
    TRY_CHECK(eval(" [int] = 123 and [double] = 1.23456 && [bool] = true and [null] = null && [foo] = 'bar' ") == true);
    TRY_CHECK(eval(" [int] = 456 or [foo].match('foo') || length([foo]) = 3 ") == true);

    // relational
    TRY_CHECK(eval(" [int] > 100 and [int] gt 100.0 and [double] < 2 and [double] lt 2.0 ") == true);
    TRY_CHECK(eval(" [int] >= 123 and [int] ge 123.0 and [double] <= 1.23456 and [double] le 1.23456 ") == true);

    // regex
    // replace
    TRY_CHECK(eval(" [foo].replace('(\\B)|( )','$1 ') ") == tr.transcode("b a r"));

    // match
    TRY_CHECK(eval(" [name].match('Québec') ") == true);

}<commit_msg>test/expressions: add checks for logical operator precedence<commit_after>#include "catch_ext.hpp"

#include <mapnik/expression.hpp>
#include <mapnik/expression_evaluator.hpp>
#include <mapnik/expression_string.hpp>
#include <mapnik/wkt/wkt_factory.hpp>
#include <mapnik/feature.hpp>
#include <mapnik/feature_factory.hpp>
#include <mapnik/unicode.hpp>

#include <functional>
#include <vector>

namespace {

template <typename Properties>
mapnik::feature_ptr make_test_feature(mapnik::value_integer id, std::string const& wkt, Properties const& prop)
{
    auto ctx = std::make_shared<mapnik::context_type>();
    mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx, id));
    mapnik::geometry::geometry<double> geom;
    if (mapnik::from_wkt(wkt, geom))
    {
        feature->set_geometry(std::move(geom));
    }

    for (auto const& kv : prop)
    {
        feature->put_new(kv.first, kv.second);
    }
    return feature;
}

template <typename Feature, typename Expression>
mapnik::value_type evaluate(Feature const& feature, Expression const& expr)
{
    auto value = mapnik::util::apply_visitor(
        mapnik::evaluate<Feature, mapnik::value_type, mapnik::attributes>(
            feature, mapnik::attributes()), expr);
    return value;
}

mapnik::value evaluate_string(mapnik::feature_ptr const& feature, std::string const& str)
{
    auto expr = mapnik::parse_expression(str);
    return evaluate(*feature, *expr);
}

std::string parse_and_dump(std::string const& str)
{
    auto expr = mapnik::parse_expression(str);
    return mapnik::to_expression_string(*expr);
}

} // namespace

TEST_CASE("expressions")
{
    using namespace std::placeholders;
    using properties_type = std::vector<std::pair<std::string, mapnik::value> > ;
    mapnik::transcoder tr("utf8");

    properties_type prop = {{ "foo"   , tr.transcode("bar") },
                            { "name"  , tr.transcode("Québec")},
                            { "grass" , tr.transcode("grow")},
                            { "wind"  , tr.transcode("blow")},
                            { "sky"   , tr.transcode("is blue")},
                            { "double", mapnik::value_double(1.23456)},
                            { "int"   , mapnik::value_integer(123)},
                            { "bool"  , mapnik::value_bool(true)},
                            { "null"  , mapnik::value_null()}};

    auto feature = make_test_feature(1, "POINT(100 200)", prop);
    auto eval = std::bind(evaluate_string, feature, _1);
    auto approx = Approx::custom().epsilon(1e-6);

    TRY_CHECK(eval(" [foo]='bar' ") == true);

    // primary expressions
    // null
    TRY_CHECK(parse_and_dump("null") == "null");
    // boolean
    TRY_CHECK(parse_and_dump("true") == "true");
    TRY_CHECK(parse_and_dump("false") == "false");
    // floating point
    TRY_CHECK(parse_and_dump("3.14159") == "3.14159");
    // integer
    TRY_CHECK(parse_and_dump("123") == "123");
    // unicode
    TRY_CHECK(parse_and_dump("'single-quoted string'") == "'single-quoted string'");
    TRY_CHECK(parse_and_dump("\"double-quoted string\"") == "'double-quoted string'");

    // floating point constants
    TRY_CHECK(parse_and_dump("pi") == "3.14159");
    TRY_CHECK(parse_and_dump("deg_to_rad") == "0.0174533");
    TRY_CHECK(parse_and_dump("rad_to_deg") == "57.2958");

    // unary functions
    // sin / cos
    TRY_CHECK(eval(" sin(0.25 * pi) / cos(0.25 * pi) ").to_double() == approx(1.0));
    // tan
    TRY_CHECK(eval(" tan(0.25 * pi) ").to_double() == approx(1.0));
    // atan
    TRY_CHECK(eval(" rad_to_deg * atan(1.0) ").to_double() == approx(45.0));
    // exp
    TRY_CHECK(eval(" exp(0.0) ") == 1.0);
    // abs
    TRY_CHECK(eval(" abs(cos(-pi)) ") == 1.0);
    // length (string)
    TRY_CHECK(eval(" length('1234567890') ").to_int() == 10);

    // binary functions
    // min
    TRY_CHECK(eval(" min(-0.01, 0.001) ") == -0.01);
    // max
    TRY_CHECK(eval(" max(0.01, -0.1) ") == 0.01);
    // pow
    TRY_CHECK(eval(" pow(2, 32) ") == 4294967296.0);

    // geometry types
    TRY_CHECK(eval(" [mapnik::geometry_type] = point ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] <> linestring ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] != polygon ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] neq collection ") == true);
    TRY_CHECK(eval(" [mapnik::geometry_type] eq collection ") == false);

    //unary expression
    TRY_CHECK(eval(" -123.456 ") == -123.456);
    TRY_CHECK(eval(" +123.456 ") == 123.456);

    // multiplicative/additive
    auto expr = mapnik::parse_expression("(2.0 * 2.0 + 3.0 * 3.0)/(2.0 * 2.0 - 3.0 * 3.0)");
    TRY_CHECK(evaluate(*feature, *expr) == -2.6);
    auto expr2 = mapnik::parse_expression("(2.0 * 2.0 + 3.0 * 3.0)/((2.0 - 3.0) * (2.0 + 3.0))");
    TRY_CHECK(evaluate(*feature, *expr) == evaluate(*feature, *expr2));

    // logical
    TRY_CHECK(eval(" [int] = 123 and [double] = 1.23456 && [bool] = true and [null] = null && [foo] = 'bar' ") == true);
    TRY_CHECK(eval(" [int] = 456 or [foo].match('foo') || length([foo]) = 3 ") == true);
    TRY_CHECK(eval(" not true  and not true  ") == eval(" (not true ) and (not true ) "));
    TRY_CHECK(eval(" not true  or  not true  ") == eval(" (not true ) or  (not true ) "));
    TRY_CHECK(eval(" not false and not false ") == eval(" (not false) and (not false) "));
    TRY_CHECK(eval(" not false or  not false ") == eval(" (not false) or  (not false) "));

    // test not/and/or precedence using combinations of "not EQ1 OP1 not EQ2 OP2 not EQ3"
    TRY_CHECK(eval(" not [grass] = 'grow' and not [wind] = 'blow' and not [sky] = 'is blue' ") == false);
    TRY_CHECK(eval(" not [grass] = 'grow' and not [wind] = 'blow' or  not [sky] = 'is blue' ") == false);
    TRY_CHECK(eval(" not [grass] = 'grow' or  not [wind] = 'blow' and not [sky] = 'is blue' ") == false);
    TRY_CHECK(eval(" not [grass] = 'grow' or  not [wind] = 'blow' or  not [sky] = 'is blue' ") == false);
    TRY_CHECK(eval(" not [grass] = 'grew' and not [wind] = 'blew' and not [sky] = 'was blue' ") == true);
    TRY_CHECK(eval(" not [grass] = 'grew' and not [wind] = 'blew' or  not [sky] = 'was blue' ") == true);
    TRY_CHECK(eval(" not [grass] = 'grew' or  not [wind] = 'blew' and not [sky] = 'was blue' ") == true);
    TRY_CHECK(eval(" not [grass] = 'grew' or  not [wind] = 'blew' or  not [sky] = 'was blue' ") == true);

    // relational
    TRY_CHECK(eval(" [int] > 100 and [int] gt 100.0 and [double] < 2 and [double] lt 2.0 ") == true);
    TRY_CHECK(eval(" [int] >= 123 and [int] ge 123.0 and [double] <= 1.23456 and [double] le 1.23456 ") == true);

    // regex
    // replace
    TRY_CHECK(eval(" [foo].replace('(\\B)|( )','$1 ') ") == tr.transcode("b a r"));

    // match
    TRY_CHECK(eval(" [name].match('Québec') ") == true);

}
{'fraction_non_alphanumeric': 0.19360964332893, 'fraction_numerical': 0.03822655217965654, 'mean_word_length': 3.239761988099405, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5268701', 'n_tokens_mistral': 4614, 'n_tokens_neox': 4210, 'n_words': 895}