#include "catch_ext.hpp" #include #include #include #include #include #include #include #include #include #include namespace { template mapnik::feature_ptr make_test_feature(mapnik::value_integer id, std::string const& wkt, Properties const& prop) { auto ctx = std::make_shared(); mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx, id)); mapnik::geometry::geometry 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 mapnik::value_type evaluate(Feature const& feature, Expression const& expr) { auto value = mapnik::util::apply_visitor( mapnik::evaluate( 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 namespace mapnik::util; using properties_type = std::map; 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")}, { "τ" , mapnik::value_double(6.2831853)}, { "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); // 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("''") == "''"); TRY_CHECK(parse_and_dump("'single-quoted string'") == "'single-quoted string'"); TRY_CHECK(parse_and_dump("\"double-quoted string\"") == "'double-quoted string'"); TRY_CHECK(parse_and_dump("'escaped \\' apostrophe'") == "'escaped \\' apostrophe'"); TRY_CHECK(parse_and_dump("'escaped \\\\ backslash'") == "'escaped \\\\ backslash'"); // 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"); // ascii attribute name TRY_CHECK(eval(" [foo]='bar' ") == true); // unicode attribute name TRY_CHECK(eval("[τ]") == prop.at("τ")); TRY_CHECK(eval("[τ]") == eval(from_u8string(u8"[\u03C4]"))); // change to TRY_CHECK once \u1234 escape sequence in attribute name // is implemented in expression grammar CHECK_NOFAIL(eval("[τ]") == eval(from_u8string(u8"[\\u03C3]"))); // 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); // log TRY_CHECK(eval(" log(1.0) ") == 0.0); TRY_CHECK(eval(" log(exp(1.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 ") == false); // (not true) and (not true) TRY_CHECK(eval(" not false and not true ") == false); // (not false) and (not true) TRY_CHECK(eval(" not true or not false ") == true); // (not true) or (not false) TRY_CHECK(eval(" not false or not false ") == true); // (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); // empty string/null equality TRY_CHECK(eval("[null] = null") == true); TRY_CHECK(eval("[null] != null") == false); TRY_CHECK(eval("[null] = ''") == false); ///////////////////// ref: https://github.com/mapnik/mapnik/issues/1859 TRY_CHECK(eval("[null] != ''") == false); // back compatible - will be changed in 3.1.x ////////////////////// TRY_CHECK(eval("'' = [null]") == false); TRY_CHECK(eval("'' != [null]") == true); // regex // replace TRY_CHECK(eval(" [foo].replace('(\\B)|( )','$1 ') ") == tr.transcode("b a r")); // single quotes TRY_CHECK(eval(" [foo].replace(\"(\\B)|( )\",\"$1 \") ") == tr.transcode("b a r")); // double quotes // https://en.wikipedia.org/wiki/Chess_symbols_in_Unicode //'\u265C\u265E\u265D\u265B\u265A\u265D\u265E\u265C' - black chess figures // replace black knights with white knights auto val0 = eval(from_u8string(u8"'\u265C\u265E\u265D\u265B\u265A\u265D\u265E\u265C'.replace('\u265E','\u2658')")); auto val1 = eval("'♜♞♝♛♚♝♞♜'.replace('♞','♘')"); // ==> expected ♜♘♝♛♚♝♘♜ TRY_CHECK(val0 == val1); TRY_CHECK(val0.to_string() == val1.to_string()); // UTF-8 TRY_CHECK(val0.to_unicode() == val1.to_unicode()); // Unicode // \u+NNNN \U+NNNNNNNN \xNN\xNN // single quotes auto val3 = eval("'\\u262f\\xF0\\x9F\\x8D\\xB7'"); auto val4 = eval("'\\U0000262f\\U0001F377'"); // double quotes auto val5 = eval("\"\\u262f\\xF0\\x9F\\x8D\\xB7\""); auto val6 = eval("\"\\U0000262f\\U0001F377\""); // UTF16 surrogate pairs work also ;) auto val7 = eval("'\\ud83d\\udd7a\\ud83c\\udffc'"); auto val8 = eval("'\\U0001F57A\\U0001F3FC'"); TRY_CHECK(val3 == val4); TRY_CHECK(val5 == val6); TRY_CHECK(val3.to_string() == val4.to_string()); // UTF-8 TRY_CHECK(val3.to_unicode() == val4.to_unicode()); // Unicode TRY_CHECK(val5.to_string() == val6.to_string()); // UTF-8 TRY_CHECK(val5.to_unicode() == val6.to_unicode()); // Unicode TRY_CHECK(val7 == val8); TRY_CHECK(val7.to_string() == val8.to_string()); // UTF-8 TRY_CHECK(val7.to_unicode() == val8.to_unicode()); // Unicode // following test will fail if boost_regex is built without ICU support (unpaired surrogates in output) TRY_CHECK(eval("[name].replace('(\\B)|( )',' ') ") == tr.transcode("Q u é b e c")); TRY_CHECK(eval("'Москва'.replace('(?