[core] Add number-format expression

This commit is contained in:
Juha Alanen
2019-06-03 15:34:13 +03:00
committed by Juha Alanen
parent 81ea5d020e
commit 0ca8ea6f16
25 changed files with 533 additions and 7 deletions

View File

@@ -19,13 +19,18 @@ target_link_libraries(mbgl-core PRIVATE
wagyu
)
# linux uses ICU from mason, other platforms use vendored ICU
if(NOT MBGL_PLATFORM STREQUAL "linux")
set(ICU_LIBRARY "icu")
endif()
# FIXME: We should not leak these many
# libraries in our public interface.
target_link_libraries(mbgl-core PUBLIC
boost
geojson.hpp
geometry.hpp
icu
${ICU_LIBRARY}
optional
polylabel
protozero

View File

@@ -8,6 +8,7 @@ elseif(MBGL_PLATFORM STREQUAL "linux")
mason_use(libuv VERSION 1.9.1)
mason_use(libpng VERSION 1.6.25)
mason_use(libjpeg-turbo VERSION 1.5.0)
mason_use(icu VERSION 63.1-min-static-data)
if(WITH_EGL)
mason_use(swiftshader VERSION 2018-05-31)

View File

@@ -142,7 +142,8 @@ enum class Kind : int32_t {
All,
Comparison,
FormatExpression,
FormatSectionOverride
FormatSectionOverride,
NumberFormat
};
class Expression {

View File

@@ -0,0 +1,40 @@
#pragma once
#include <mbgl/style/expression/expression.hpp>
#include <mbgl/style/expression/parsing_context.hpp>
namespace mbgl {
namespace style {
namespace expression {
class NumberFormat final : public Expression {
public:
NumberFormat(std::unique_ptr<Expression> number_,
std::unique_ptr<Expression> locale_,
std::unique_ptr<Expression> currency_,
std::unique_ptr<Expression> minFractionDigits_,
std::unique_ptr<Expression> maxFractionDigits_);
~NumberFormat();
static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx);
EvaluationResult evaluate(const EvaluationContext& params) const override;
void eachChild(const std::function<void(const Expression&)>& visit) const override;
bool operator==(const Expression& e) const override;
std::vector<optional<Value>> possibleOutputs() const override;
mbgl::Value serialize() const override;
std::string getOperator() const override { return "number-format"; }
private:
std::unique_ptr<Expression> number;
std::unique_ptr<Expression> locale;
std::unique_ptr<Expression> currency;
std::unique_ptr<Expression> minFractionDigits;
std::unique_ptr<Expression> maxFractionDigits;
};
} // namespace expression
} // namespace style
} // namespace mbgl

View File

@@ -16,6 +16,9 @@ std::string lowercase(const std::string &string);
// Gets the name of the current thread.
std::string getCurrentThreadName();
std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
uint8_t minFractionDigits, uint8_t maxFractionDigits);
// Set the name of the current thread, truncated at 15.
void setCurrentThreadName(const std::string& name);

View File

@@ -81,6 +81,7 @@
"platform/android/src/style/value.cpp",
"platform/android/src/text/collator.cpp",
"platform/android/src/text/local_glyph_rasterizer.cpp",
"platform/android/src/text/format_number.cpp",
"platform/android/src/gl_functions.cpp",
"platform/android/src/thread.cpp",
"platform/android/src/timer.cpp",

View File

@@ -54,6 +54,7 @@
#endif
#include "text/collator_jni.hpp"
#include "text/local_glyph_rasterizer_jni.hpp"
#include "text/format_number_jni.hpp"
#include "logger.hpp"
namespace mbgl {
@@ -200,6 +201,7 @@ void registerNatives(JavaVM *vm) {
Locale::registerNative(env);
Collator::registerNative(env);
StringUtils::registerNative(env);
NumberFormat::registerNative(env);
// Logger
Logger::registerNative(env);

View File

@@ -0,0 +1,81 @@
#include <mbgl/style/expression/collator.hpp>
#include <mbgl/text/language_tag.hpp>
#include <mbgl/util/platform.hpp>
#include <jni/jni.hpp>
#include "../attach_env.hpp"
#include "format_number_jni.hpp"
namespace mbgl {
namespace android {
void NumberFormat::registerNative(jni::JNIEnv& env) {
jni::Class<NumberFormat>::Singleton(env);
}
jni::Local<jni::Object<NumberFormat>> NumberFormat::getInstance(jni::JNIEnv& env, const jni::Object<Locale>& locale) {
static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
static auto method = javaClass.GetStaticMethod<jni::Object<NumberFormat> (jni::Object<Locale>)>(env, "getInstance");
return javaClass.Call(env, method, locale);
}
jni::Local<jni::Object<NumberFormat>> NumberFormat::getCurrencyInstance(jni::JNIEnv& env, const jni::Object<Locale>& locale) {
static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
static auto method = javaClass.GetStaticMethod<jni::Object<NumberFormat> (jni::Object<Locale>)>(env, "getCurrencyInstance");
return javaClass.Call(env, method, locale);
}
jni::Local<jni::String> NumberFormat::format(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jdouble number) {
static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
static auto method = javaClass.GetMethod<jni::String (jni::jdouble)>(env, "format");
return nf.Call(env, method, number);
}
void NumberFormat::setMinimumFractionDigits(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jint value) {
static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
static auto method = javaClass.GetMethod<void (jni::jint)>(env, "setMinimumFractionDigits");
return nf.Call(env, method, value);
}
void NumberFormat::setMaximumFractionDigits(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jint value) {
static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
static auto method = javaClass.GetMethod<void (jni::jint)>(env, "setMaximumFractionDigits");
return nf.Call(env, method, value);
}
} // namespace android
namespace platform {
std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
uint8_t minFractionDigits, uint8_t maxFractionDigits) {
auto env{ android::AttachEnv() };
jni::Global<jni::Object<android::Locale>> locale;
LanguageTag languageTag = !localeId.empty() ? LanguageTag::fromBCP47(localeId) : LanguageTag();
if (!languageTag.language) {
locale = jni::NewGlobal(*env, android::Locale::getDefault(*env));
} else if (!languageTag.region) {
locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make<jni::String>(*env, *languageTag.language)));
} else {
locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make<jni::String>(*env, *languageTag.language),
jni::Make<jni::String>(*env, *languageTag.region)));
}
jni::Global<jni::Object<android::NumberFormat>> formatter;
if (currency.empty()) {
formatter = jni::NewGlobal(*env, android::NumberFormat::getInstance(*env, locale));
android::NumberFormat::setMinimumFractionDigits(*env, formatter, static_cast<jni::jint>(minFractionDigits));
android::NumberFormat::setMaximumFractionDigits(*env, formatter, static_cast<jni::jint>(maxFractionDigits));
} else {
formatter = jni::NewGlobal(*env, android::NumberFormat::getCurrencyInstance(*env, locale));
}
auto result = android::NumberFormat::format(*env, formatter, static_cast<jni::jdouble>(number));
return jni::Make<std::string>(*env, result);
}
} // namespace platform
} // namespace mbgl

View File

@@ -0,0 +1,29 @@
#pragma once
#include <jni/jni.hpp>
#include "collator_jni.hpp"
/*
android::NumberFormat is the JNI wrapper
of java/text/NumberFormat.
*/
namespace mbgl {
namespace android {
class NumberFormat {
public:
static constexpr auto Name() { return "java/text/NumberFormat"; };
static jni::Local<jni::Object<NumberFormat>> getInstance(jni::JNIEnv&, const jni::Object<Locale>&);
static jni::Local<jni::Object<NumberFormat>> getCurrencyInstance(jni::JNIEnv&, const jni::Object<Locale>&);
static jni::Local<jni::String> format(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jdouble);
static void setMinimumFractionDigits(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jint);
static void setMaximumFractionDigits(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jint);
static void registerNative(jni::JNIEnv&);
};
} // namespace android
} // namespace mbgl

View File

@@ -27,5 +27,28 @@ std::string lowercase(const std::string &string) {
return result;
}
std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
uint8_t minFractionDigits, uint8_t maxFractionDigits) {
static NSNumberFormatter *numberFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
numberFormatter = [[NSNumberFormatter alloc] init];
});
numberFormatter.locale = !localeId.empty() ? [NSLocale localeWithLocaleIdentifier:@(localeId.c_str())] : nil;
numberFormatter.currencyCode = !currency.empty() ? @(currency.c_str()) : nil;
if (currency.empty()) {
numberFormatter.minimumFractionDigits = minFractionDigits;
numberFormatter.maximumFractionDigits = maxFractionDigits;
numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
} else {
numberFormatter.numberStyle = NSNumberFormatterCurrencyStyle;
}
NSString *formatted = [numberFormatter stringFromNumber:@(number)];
std::string result = std::string([formatted UTF8String]);
return result;
}
}
}

View File

@@ -0,0 +1,35 @@
#include <mbgl/util/platform.hpp>
#include <unicode/numberformatter.h>
namespace mbgl {
namespace platform {
std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
uint8_t minFractionDigits, uint8_t maxFractionDigits) {
UErrorCode status = U_ZERO_ERROR;
icu::UnicodeString ustr;
std::string formatted;
icu::Locale locale = icu::Locale(localeId.c_str());
// Print the value as currency
if (!currency.empty()) {
icu::UnicodeString ucurrency = icu::UnicodeString::fromUTF8(currency);
ustr = icu::number::NumberFormatter::with()
.unit(icu::CurrencyUnit(ucurrency.getBuffer(), status))
.locale(locale)
.formatDouble(number, status)
.toString();
} else {
ustr = icu::number::NumberFormatter::with()
.precision(icu::number::Precision::minMaxFraction(minFractionDigits, maxFractionDigits))
.locale(locale)
.formatDouble(number, status)
.toString();
}
return ustr.toUTF8String(formatted);
}
} // namespace platform
} // namespace mbgl

View File

@@ -56,6 +56,7 @@ macro(mbgl_platform_core)
PRIVATE platform/default/src/mbgl/text/unaccent.cpp
PRIVATE platform/default/include/mbgl/text/unaccent.hpp
PRIVATE platform/default/src/mbgl/util/utf.cpp
PRIVATE platform/default/src/mbgl/util/format_number.cpp
# Image handling
PRIVATE platform/default/src/mbgl/util/image.cpp
@@ -83,10 +84,21 @@ macro(mbgl_platform_core)
target_add_mason_package(mbgl-core PUBLIC libpng)
target_add_mason_package(mbgl-core PUBLIC libjpeg-turbo)
target_add_mason_package(mbgl-core PRIVATE icu)
# Ignore warning caused by ICU header unistr.h in some CI environments
set_source_files_properties(platform/default/src/mbgl/util/format_number.cpp PROPERTIES COMPILE_FLAGS -Wno-error=shadow)
# Link all ICU libraries (by default only libicuuc is linked)
find_library(LIBICUI18N NAMES icui18n HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
find_library(LIBICUUC NAMES icuuc HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
find_library(LIBICUDATA NAMES icudata HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
target_link_libraries(mbgl-core
PRIVATE ${LIBICUI18N}
PRIVATE ${LIBICUUC}
PRIVATE ${LIBICUDATA}
PRIVATE nunicode
PRIVATE icu
PUBLIC -lz
)

View File

@@ -42,7 +42,8 @@ type::Type parseType(v8::Local<v8::Object> type) {
{"object", type::Object},
{"color", type::Color},
{"value", type::Value},
{"formatted", type::Formatted}
{"formatted", type::Formatted},
{"number-format", type::String}
};
v8::Local<v8::Value> v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked();

View File

@@ -35,6 +35,7 @@ set(MBGL_QT_CORE_FILES
PRIVATE platform/qt/src/timer_impl.hpp
PRIVATE platform/qt/src/utf.cpp
PRIVATE platform/qt/src/gl_functions.cpp
PRIVATE platform/qt/src/format_number.cpp
PRIVATE platform/default/src/mbgl/text/collator.cpp
PRIVATE platform/default/src/mbgl/text/unaccent.cpp

View File

@@ -0,0 +1,26 @@
#include <mbgl/util/platform.hpp>
#include <QLocale>
#include <QString>
namespace mbgl {
namespace platform {
std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
uint8_t minFractionDigits, uint8_t maxFractionDigits) {
QString formatted;
// Qt Locale::toString() API takes only one precision argument
(void)minFractionDigits;
QLocale locale = QLocale(QString::fromStdString(localeId));
if (!currency.empty()) {
formatted = locale.toCurrencyString(number);
} else {
formatted = locale.toString(number, 'f', maxFractionDigits);
}
return formatted.toStdString();
}
} // namespace platform
} // namespace mbgl

View File

@@ -192,6 +192,7 @@
"src/mbgl/style/expression/let.cpp",
"src/mbgl/style/expression/literal.cpp",
"src/mbgl/style/expression/match.cpp",
"src/mbgl/style/expression/number_format.cpp",
"src/mbgl/style/expression/parsing_context.cpp",
"src/mbgl/style/expression/step.cpp",
"src/mbgl/style/expression/util.cpp",
@@ -416,6 +417,7 @@
"mbgl/style/expression/let.hpp": "include/mbgl/style/expression/let.hpp",
"mbgl/style/expression/literal.hpp": "include/mbgl/style/expression/literal.hpp",
"mbgl/style/expression/match.hpp": "include/mbgl/style/expression/match.hpp",
"mbgl/style/expression/number_format.hpp": "include/mbgl/style/expression/number_format.hpp",
"mbgl/style/expression/parsing_context.hpp": "include/mbgl/style/expression/parsing_context.hpp",
"mbgl/style/expression/step.hpp": "include/mbgl/style/expression/step.hpp",
"mbgl/style/expression/type.hpp": "include/mbgl/style/expression/type.hpp",

View File

@@ -0,0 +1,215 @@
#include <mbgl/style/expression/number_format.hpp>
#include <mbgl/style/conversion_impl.hpp>
#include <mbgl/util/platform.hpp>
namespace mbgl {
namespace style {
namespace expression {
const char localeKey[] = "locale";
const char currencyKey[] = "currency";
const char minFractionDigitsKey[] = "min-fraction-digits";
const char maxFractionDigitsKey[] = "max-fraction-digits";
NumberFormat::NumberFormat(std::unique_ptr<Expression> number_,
std::unique_ptr<Expression> locale_,
std::unique_ptr<Expression> currency_,
std::unique_ptr<Expression> minFractionDigits_,
std::unique_ptr<Expression> maxFractionDigits_)
: Expression(Kind::NumberFormat, type::String),
number(std::move(number_)),
locale(std::move(locale_)),
currency(std::move(currency_)),
minFractionDigits(std::move(minFractionDigits_)),
maxFractionDigits(std::move(maxFractionDigits_))
{}
NumberFormat::~NumberFormat() = default;
EvaluationResult NumberFormat::evaluate(const EvaluationContext& params) const {
auto numberResult = number->evaluate(params);
if (!numberResult) {
return numberResult.error();
}
double evaluatedNumber = numberResult->get<double>();
std::string evaluatedLocale;
if (locale) {
auto localeResult = locale->evaluate(params);
if (!localeResult) {
return localeResult.error();
}
evaluatedLocale = toString(*localeResult);
}
std::string evaluatedCurrency;
if (currency) {
auto currencyResult = currency->evaluate(params);
if (!currencyResult) {
return currencyResult.error();
}
evaluatedCurrency = toString(*currencyResult);
}
uint8_t evaluatedMinFractionDigits = 0;
if (minFractionDigits) {
auto minDigitsResult = minFractionDigits->evaluate(params);
if (!minDigitsResult) {
return minDigitsResult.error();
}
evaluatedMinFractionDigits = minDigitsResult->get<double>();
}
uint8_t evaluatedMaxFractionDigits = 3;
if (maxFractionDigits) {
auto maxDigitsResult = maxFractionDigits->evaluate(params);
if (!maxDigitsResult) {
return maxDigitsResult.error();
}
evaluatedMaxFractionDigits = maxDigitsResult->get<double>();
}
std::string output;
output = platform::formatNumber(evaluatedNumber,
evaluatedLocale,
evaluatedCurrency,
evaluatedMinFractionDigits,
evaluatedMaxFractionDigits);
return output;
}
void NumberFormat::eachChild(const std::function<void(const Expression&)>& visit) const {
visit(*number);
if (locale) visit(*locale);
if (currency) visit(*currency);
if (minFractionDigits) visit(*minFractionDigits);
if (maxFractionDigits) visit(*maxFractionDigits);
}
bool NumberFormat::operator==(const Expression& e) const {
if (e.getKind() == Kind::NumberFormat) {
auto rhs = static_cast<const NumberFormat*>(&e);
if ((locale && (!rhs->locale || *locale != *rhs->locale)) ||
(!locale && rhs->locale)) {
return false;
}
if ((currency && (!rhs->currency || *currency != *rhs->currency)) ||
(!currency && rhs->currency)) {
return false;
}
if ((minFractionDigits && (!rhs->minFractionDigits || *minFractionDigits != *rhs->minFractionDigits)) ||
(!minFractionDigits && rhs->minFractionDigits)) {
return false;
}
if ((maxFractionDigits && (!rhs->maxFractionDigits || *maxFractionDigits != *rhs->maxFractionDigits)) ||
(!maxFractionDigits && rhs->maxFractionDigits)) {
return false;
}
return *number == *rhs->number;
}
return false;
}
std::vector<optional<Value>> NumberFormat::possibleOutputs() const {
return { nullopt };
}
using namespace mbgl::style::conversion;
ParseResult NumberFormat::parse(const Convertible& value, ParsingContext& ctx) {
std::size_t length = arrayLength(value);
if (length != 3) {
ctx.error("Expected two arguments, but found " + util::toString(length) + " instead.");
return ParseResult();
}
ParseResult numberResult = ctx.parse(arrayMember(value, 1), 1, {type::Number});
if (!numberResult) {
ctx.error("Failed to parse the number.");
return ParseResult();
}
type::Type type = (*numberResult)->getType();
if (!type.is<type::NumberType>()) {
ctx.error("Expected argument of type number, but found " + toString(type) + " instead.");
return ParseResult();
}
auto options = arrayMember(value, 2);
if (!isObject(options)) {
ctx.error("Number-format options argument must be an object.");
return ParseResult();
}
const optional<Convertible> localeOption = objectMember(options, localeKey);
ParseResult localeResult;
if (localeOption) {
localeResult = ctx.parse(*localeOption, 1, {type::String});
if (!localeResult) {
ctx.error("Number-format locale parsing failed.");
return ParseResult();
}
}
const optional<Convertible> currencyOption = objectMember(options, currencyKey);
ParseResult currencyResult;
if (currencyOption) {
currencyResult = ctx.parse(*currencyOption, 1, {type::String});
if (!currencyResult) {
ctx.error("Number-format currency parsing failed.");
return ParseResult();
}
}
const optional<Convertible> minFractionDigitsOption = objectMember(options, minFractionDigitsKey);
ParseResult minFractionDigitsResult;
if (minFractionDigitsOption) {
minFractionDigitsResult = ctx.parse(*minFractionDigitsOption, 1, {type::Number});
if (!minFractionDigitsResult) {
ctx.error("Number-format min-fraction-digits parsing failed.");
return ParseResult();
}
}
const optional<Convertible> maxFractionDigitsOption = objectMember(options, maxFractionDigitsKey);
ParseResult maxFractionDigitsResult;
if (maxFractionDigitsOption) {
maxFractionDigitsResult = ctx.parse(*maxFractionDigitsOption, 1, {type::Number});
if (!maxFractionDigitsResult) {
ctx.error("Number-format max-fraction-digits parsing failed.");
return ParseResult();
}
}
return ParseResult(std::make_unique<NumberFormat>(std::move(*numberResult),
localeResult ? std::move(*localeResult) : nullptr,
currencyResult? std::move(*currencyResult) : nullptr,
minFractionDigitsResult ? std::move(*minFractionDigitsResult) : nullptr,
maxFractionDigitsResult ? std::move(*maxFractionDigitsResult) : nullptr));
}
mbgl::Value NumberFormat::serialize() const {
std::vector<mbgl::Value> serialized{{ getOperator() }};
serialized.emplace_back(number->serialize());
std::unordered_map<std::string, mbgl::Value> options;
if (locale) {
options[localeKey] = locale->serialize();
}
if (currency) {
options[currencyKey] = currency->serialize();
}
if (minFractionDigits) {
options[minFractionDigitsKey] = minFractionDigits->serialize();
}
if (maxFractionDigits) {
options[maxFractionDigitsKey] = maxFractionDigits->serialize();
}
serialized.emplace_back(options);
return serialized;
}
} // namespace expression
} // namespace style
} // namespace mbgl

View File

@@ -13,6 +13,7 @@
#include <mbgl/style/expression/coercion.hpp>
#include <mbgl/style/expression/compound_expression.hpp>
#include <mbgl/style/expression/comparison.hpp>
#include <mbgl/style/expression/number_format.hpp>
#include <mbgl/style/expression/format_expression.hpp>
#include <mbgl/style/expression/interpolate.hpp>
#include <mbgl/style/expression/length.hpp>
@@ -120,6 +121,7 @@ MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry = mapbox::eternal::hash_m
{"literal", Literal::parse},
{"match", parseMatch},
{"number", Assertion::parse},
{"number-format", NumberFormat::parse},
{"object", Assertion::parse},
{"step", Step::parse},
{"string", Assertion::parse},

View File

@@ -0,0 +1,9 @@
[
"number-format",
123456.789,
{
"locale": "de-DE",
"currency": "EUR"
}
]

View File

@@ -0,0 +1,9 @@
[
"number-format",
123456.789,
{
"locale": "ja-JP",
"currency": "JPY"
}
]

View File

@@ -0,0 +1,5 @@
[
"number-format",
123456.789, {}
]

View File

@@ -0,0 +1,5 @@
[
"number-format",
-123456.789, {}
]

View File

@@ -0,0 +1,10 @@
[
"number-format",
987654321.23456789,
{
"locale": "en-US",
"min-fraction-digits": 15,
"max-fraction-digits": 20
}
]

View File

@@ -0,0 +1,10 @@
[
"number-format",
987654321.23456789,
{
"locale": "en-US",
"min-fraction-digits": 2,
"max-fraction-digits": 4
}
]

View File

@@ -36,10 +36,8 @@ TEST(Expression, IsExpression) {
// TODO: "feature-state": https://github.com/mapbox/mapbox-gl-native/issues/12613
// TODO: "interpolate-hcl": https://github.com/mapbox/mapbox-gl-native/issues/8720
// TODO: "interpolate-lab": https://github.com/mapbox/mapbox-gl-native/issues/8720
// TODO: "number-format": https://github.com/mapbox/mapbox-gl-native/issues/13632
// TODO: "accumulated": https://github.com/mapbox/mapbox-gl-native/issues/14043
if (name == "feature-state" || name == "interpolate-hcl" || name == "interpolate-lab" || name == "number-format" ||
name == "accumulated") {
if (name == "feature-state" || name == "interpolate-hcl" || name == "interpolate-lab" || name == "accumulated") {
if (expression::isExpression(conversion::Convertible(expression))) {
ASSERT_TRUE(false) << "Expression name" << name << "is implemented - please update Expression.IsExpression test.";
}