From 50bdbea570ad079490f160a189010271bd33bcb1 Mon Sep 17 00:00:00 2001 From: Juha Alanen Date: Mon, 3 Jun 2019 15:34:13 +0300 Subject: [PATCH] [core] Add number-format expression --- cmake/core.cmake | 7 +- cmake/mason-dependencies.cmake | 1 + include/mbgl/style/expression/expression.hpp | 3 +- .../mbgl/style/expression/number_format.hpp | 40 ++++ include/mbgl/util/platform.hpp | 3 + platform/android/core-files.json | 1 + platform/android/src/jni.cpp | 2 + platform/android/src/text/format_number.cpp | 81 +++++++ .../android/src/text/format_number_jni.hpp | 29 +++ platform/darwin/src/string_nsstring.mm | 23 ++ .../default/src/mbgl/util/format_number.cpp | 35 +++ platform/linux/config.cmake | 14 +- platform/node/src/node_expression.cpp | 3 +- platform/qt/qt.cmake | 1 + platform/qt/src/format_number.cpp | 26 +++ src/core-files.json | 2 + src/mbgl/style/expression/number_format.cpp | 215 ++++++++++++++++++ src/mbgl/style/expression/parsing_context.cpp | 2 + .../number-format-currency.a.json | 9 + .../number-format-currency.b.json | 9 + .../number-format-default.a.json | 5 + .../number-format-default.b.json | 5 + .../number-format-precision.a.json | 10 + .../number-format-precision.b.json | 10 + test/style/expression/expression.test.cpp | 4 +- 25 files changed, 533 insertions(+), 7 deletions(-) create mode 100644 include/mbgl/style/expression/number_format.hpp create mode 100644 platform/android/src/text/format_number.cpp create mode 100644 platform/android/src/text/format_number_jni.hpp create mode 100644 platform/default/src/mbgl/util/format_number.cpp create mode 100644 platform/qt/src/format_number.cpp create mode 100644 src/mbgl/style/expression/number_format.cpp create mode 100644 test/fixtures/expression_equality/number-format-currency.a.json create mode 100644 test/fixtures/expression_equality/number-format-currency.b.json create mode 100644 test/fixtures/expression_equality/number-format-default.a.json create mode 100644 test/fixtures/expression_equality/number-format-default.b.json create mode 100644 test/fixtures/expression_equality/number-format-precision.a.json create mode 100644 test/fixtures/expression_equality/number-format-precision.b.json diff --git a/cmake/core.cmake b/cmake/core.cmake index 1971352c371..3278c4f7d30 100644 --- a/cmake/core.cmake +++ b/cmake/core.cmake @@ -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 diff --git a/cmake/mason-dependencies.cmake b/cmake/mason-dependencies.cmake index a6ef7a3a534..5ec6a44a799 100644 --- a/cmake/mason-dependencies.cmake +++ b/cmake/mason-dependencies.cmake @@ -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) diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index 22ae57c2bec..5f66fc6dc70 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -142,7 +142,8 @@ enum class Kind : int32_t { All, Comparison, FormatExpression, - FormatSectionOverride + FormatSectionOverride, + NumberFormat }; class Expression { diff --git a/include/mbgl/style/expression/number_format.hpp b/include/mbgl/style/expression/number_format.hpp new file mode 100644 index 00000000000..9571c7d98a9 --- /dev/null +++ b/include/mbgl/style/expression/number_format.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class NumberFormat final : public Expression { +public: + NumberFormat(std::unique_ptr number_, + std::unique_ptr locale_, + std::unique_ptr currency_, + std::unique_ptr minFractionDigits_, + std::unique_ptr 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& visit) const override; + bool operator==(const Expression& e) const override; + std::vector> possibleOutputs() const override; + + mbgl::Value serialize() const override; + std::string getOperator() const override { return "number-format"; } + +private: + std::unique_ptr number; + std::unique_ptr locale; + std::unique_ptr currency; + std::unique_ptr minFractionDigits; + std::unique_ptr maxFractionDigits; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/util/platform.hpp b/include/mbgl/util/platform.hpp index 35446597401..2e11e5f1861 100644 --- a/include/mbgl/util/platform.hpp +++ b/include/mbgl/util/platform.hpp @@ -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); diff --git a/platform/android/core-files.json b/platform/android/core-files.json index 7e2f7cc07b3..62ecc0c671c 100644 --- a/platform/android/core-files.json +++ b/platform/android/core-files.json @@ -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", diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp index 410c9623849..088b3b796c0 100755 --- a/platform/android/src/jni.cpp +++ b/platform/android/src/jni.cpp @@ -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); diff --git a/platform/android/src/text/format_number.cpp b/platform/android/src/text/format_number.cpp new file mode 100644 index 00000000000..3a41175ecc1 --- /dev/null +++ b/platform/android/src/text/format_number.cpp @@ -0,0 +1,81 @@ +#include +#include +#include + +#include + +#include "../attach_env.hpp" +#include "format_number_jni.hpp" + +namespace mbgl { +namespace android { + +void NumberFormat::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local> NumberFormat::getInstance(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "getInstance"); + return javaClass.Call(env, method, locale); +} + +jni::Local> NumberFormat::getCurrencyInstance(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "getCurrencyInstance"); + return javaClass.Call(env, method, locale); +} + +jni::Local NumberFormat::format(jni::JNIEnv& env, const jni::Object& nf, jni::jdouble number) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "format"); + return nf.Call(env, method, number); +} + +void NumberFormat::setMinimumFractionDigits(jni::JNIEnv& env, const jni::Object& nf, jni::jint value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "setMinimumFractionDigits"); + return nf.Call(env, method, value); +} + +void NumberFormat::setMaximumFractionDigits(jni::JNIEnv& env, const jni::Object& nf, jni::jint value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(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> 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(*env, *languageTag.language))); + } else { + locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make(*env, *languageTag.language), + jni::Make(*env, *languageTag.region))); + } + + jni::Global> formatter; + if (currency.empty()) { + formatter = jni::NewGlobal(*env, android::NumberFormat::getInstance(*env, locale)); + android::NumberFormat::setMinimumFractionDigits(*env, formatter, static_cast(minFractionDigits)); + android::NumberFormat::setMaximumFractionDigits(*env, formatter, static_cast(maxFractionDigits)); + } else { + formatter = jni::NewGlobal(*env, android::NumberFormat::getCurrencyInstance(*env, locale)); + } + + auto result = android::NumberFormat::format(*env, formatter, static_cast(number)); + return jni::Make(*env, result); +} + +} // namespace platform +} // namespace mbgl diff --git a/platform/android/src/text/format_number_jni.hpp b/platform/android/src/text/format_number_jni.hpp new file mode 100644 index 00000000000..17200389254 --- /dev/null +++ b/platform/android/src/text/format_number_jni.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#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> getInstance(jni::JNIEnv&, const jni::Object&); + static jni::Local> getCurrencyInstance(jni::JNIEnv&, const jni::Object&); + static jni::Local format(jni::JNIEnv&, const jni::Object&, jni::jdouble); + static void setMinimumFractionDigits(jni::JNIEnv&, const jni::Object&, jni::jint); + static void setMaximumFractionDigits(jni::JNIEnv&, const jni::Object&, jni::jint); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/platform/darwin/src/string_nsstring.mm b/platform/darwin/src/string_nsstring.mm index 08f9aeccef5..096ed2b212b 100644 --- a/platform/darwin/src/string_nsstring.mm +++ b/platform/darwin/src/string_nsstring.mm @@ -27,5 +27,28 @@ 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; +} + } } diff --git a/platform/default/src/mbgl/util/format_number.cpp b/platform/default/src/mbgl/util/format_number.cpp new file mode 100644 index 00000000000..7cc863818aa --- /dev/null +++ b/platform/default/src/mbgl/util/format_number.cpp @@ -0,0 +1,35 @@ +#include + +#include + +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 diff --git a/platform/linux/config.cmake b/platform/linux/config.cmake index 7cc1f1fe4d0..aa65ddb6060 100644 --- a/platform/linux/config.cmake +++ b/platform/linux/config.cmake @@ -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 ) diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp index 4ea66124f82..041f60d0297 100644 --- a/platform/node/src/node_expression.cpp +++ b/platform/node/src/node_expression.cpp @@ -42,7 +42,8 @@ type::Type parseType(v8::Local type) { {"object", type::Object}, {"color", type::Color}, {"value", type::Value}, - {"formatted", type::Formatted} + {"formatted", type::Formatted}, + {"number-format", type::String} }; v8::Local v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked(); diff --git a/platform/qt/qt.cmake b/platform/qt/qt.cmake index 48523057c99..33acb7a0302 100644 --- a/platform/qt/qt.cmake +++ b/platform/qt/qt.cmake @@ -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 diff --git a/platform/qt/src/format_number.cpp b/platform/qt/src/format_number.cpp new file mode 100644 index 00000000000..b6fe3558e6f --- /dev/null +++ b/platform/qt/src/format_number.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include + +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 diff --git a/src/core-files.json b/src/core-files.json index 665c203a78e..fcc0dba9f5b 100644 --- a/src/core-files.json +++ b/src/core-files.json @@ -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", diff --git a/src/mbgl/style/expression/number_format.cpp b/src/mbgl/style/expression/number_format.cpp new file mode 100644 index 00000000000..e31a9ce3987 --- /dev/null +++ b/src/mbgl/style/expression/number_format.cpp @@ -0,0 +1,215 @@ +#include +#include +#include + +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 number_, + std::unique_ptr locale_, + std::unique_ptr currency_, + std::unique_ptr minFractionDigits_, + std::unique_ptr 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(); + + 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(); + } + + uint8_t evaluatedMaxFractionDigits = 3; + if (maxFractionDigits) { + auto maxDigitsResult = maxFractionDigits->evaluate(params); + if (!maxDigitsResult) { + return maxDigitsResult.error(); + } + evaluatedMaxFractionDigits = maxDigitsResult->get(); + } + + std::string output; + output = platform::formatNumber(evaluatedNumber, + evaluatedLocale, + evaluatedCurrency, + evaluatedMinFractionDigits, + evaluatedMaxFractionDigits); + return output; +} + +void NumberFormat::eachChild(const std::function& 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(&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> 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()) { + 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 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 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 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 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(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 serialized{{ getOperator() }}; + serialized.emplace_back(number->serialize()); + + std::unordered_map 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 diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp index 411aa660e82..a7c04f563d0 100644 --- a/src/mbgl/style/expression/parsing_context.cpp +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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}, diff --git a/test/fixtures/expression_equality/number-format-currency.a.json b/test/fixtures/expression_equality/number-format-currency.a.json new file mode 100644 index 00000000000..d22fb5bc4a1 --- /dev/null +++ b/test/fixtures/expression_equality/number-format-currency.a.json @@ -0,0 +1,9 @@ +[ + "number-format", + 123456.789, + { + "locale": "de-DE", + "currency": "EUR" + } +] + diff --git a/test/fixtures/expression_equality/number-format-currency.b.json b/test/fixtures/expression_equality/number-format-currency.b.json new file mode 100644 index 00000000000..c85b4571427 --- /dev/null +++ b/test/fixtures/expression_equality/number-format-currency.b.json @@ -0,0 +1,9 @@ +[ + "number-format", + 123456.789, + { + "locale": "ja-JP", + "currency": "JPY" + } +] + diff --git a/test/fixtures/expression_equality/number-format-default.a.json b/test/fixtures/expression_equality/number-format-default.a.json new file mode 100644 index 00000000000..f41348d8f8c --- /dev/null +++ b/test/fixtures/expression_equality/number-format-default.a.json @@ -0,0 +1,5 @@ +[ + "number-format", + 123456.789, {} +] + diff --git a/test/fixtures/expression_equality/number-format-default.b.json b/test/fixtures/expression_equality/number-format-default.b.json new file mode 100644 index 00000000000..8bbc1bcfb5c --- /dev/null +++ b/test/fixtures/expression_equality/number-format-default.b.json @@ -0,0 +1,5 @@ +[ + "number-format", + -123456.789, {} +] + diff --git a/test/fixtures/expression_equality/number-format-precision.a.json b/test/fixtures/expression_equality/number-format-precision.a.json new file mode 100644 index 00000000000..24d56d8d100 --- /dev/null +++ b/test/fixtures/expression_equality/number-format-precision.a.json @@ -0,0 +1,10 @@ +[ + "number-format", + 987654321.23456789, + { + "locale": "en-US", + "min-fraction-digits": 15, + "max-fraction-digits": 20 + } +] + diff --git a/test/fixtures/expression_equality/number-format-precision.b.json b/test/fixtures/expression_equality/number-format-precision.b.json new file mode 100644 index 00000000000..d8935f9f5fe --- /dev/null +++ b/test/fixtures/expression_equality/number-format-precision.b.json @@ -0,0 +1,10 @@ +[ + "number-format", + 987654321.23456789, + { + "locale": "en-US", + "min-fraction-digits": 2, + "max-fraction-digits": 4 + } +] + diff --git a/test/style/expression/expression.test.cpp b/test/style/expression/expression.test.cpp index 42a8fdd7262..8e2acfcd325 100644 --- a/test/style/expression/expression.test.cpp +++ b/test/style/expression/expression.test.cpp @@ -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."; }