diff --git a/lib/src/model/product.dart b/lib/src/model/product.dart index f29af095b3..f656827683 100644 --- a/lib/src/model/product.dart +++ b/lib/src/model/product.dart @@ -115,6 +115,18 @@ class Product extends JsonObject { includeIfNull: false) Map? genericNameInLanguages; + /// Abbreviated product name. + @JsonKey(name: 'abbreviated_product_name', includeIfNull: false) + String? abbreviatedName; + + /// Localized abbreviated product name. + @JsonKey( + name: 'abbreviated_product_name_in_languages', + fromJson: LanguageHelper.fromJsonStringMap, + toJson: LanguageHelper.toJsonStringMap, + includeIfNull: false) + Map? abbreviatedNameInLanguages; + @JsonKey(name: 'brands', includeIfNull: false) String? brands; @JsonKey(name: 'brands_tags', includeIfNull: false) @@ -610,6 +622,11 @@ class Product extends JsonObject { result.genericNameInLanguages ??= {}; result.genericNameInLanguages![language] = label; break; + case ProductField.ABBREVIATED_NAME_IN_LANGUAGES: + case ProductField.ABBREVIATED_NAME_ALL_LANGUAGES: + result.abbreviatedNameInLanguages ??= {}; + result.abbreviatedNameInLanguages![language] = label; + break; case ProductField.INGREDIENTS_TEXT_IN_LANGUAGES: case ProductField.INGREDIENTS_TEXT_ALL_LANGUAGES: result.ingredientsTextInLanguages ??= {}; @@ -692,6 +709,7 @@ class Product extends JsonObject { switch (productField) { case ProductField.NAME_IN_LANGUAGES: case ProductField.GENERIC_NAME_IN_LANGUAGES: + case ProductField.ABBREVIATED_NAME_IN_LANGUAGES: case ProductField.INGREDIENTS_TEXT_IN_LANGUAGES: case ProductField.PACKAGING_TEXT_IN_LANGUAGES: setLanguageString(productField, language, json[key]); @@ -951,4 +969,75 @@ class Product extends JsonObject { } _nutriments = nutriments; } + + /// Returns the best version of a product name. + /// + /// cf. openfoodfacts-server/lib/ProductOpener/Products.pm + String getBestProductName(final OpenFoodFactsLanguage language) { + String? tmp; + if ((tmp = productNameInLanguages?[language])?.isNotEmpty == true) { + return tmp!; + } + if ((tmp = productName)?.isNotEmpty == true) { + return tmp!; + } + if ((tmp = genericNameInLanguages?[language])?.isNotEmpty == true) { + return tmp!; + } + if ((tmp = genericName)?.isNotEmpty == true) { + return tmp!; + } + if ((tmp = abbreviatedNameInLanguages?[language])?.isNotEmpty == true) { + return tmp!; + } + if ((tmp = abbreviatedName)?.isNotEmpty == true) { + return tmp!; + } + return ''; + } + + /// Returns the first of all brands. + String? getFirstBrand() { + if (brands == null) { + return null; + } + final List items = brands!.split(','); + if (items.isEmpty) { + return null; + } + return items.first; + } + + /// Returns a combo of the best product name and the first brand. + /// + /// cf. openfoodfacts-server/lib/ProductOpener/Products.pm + String getProductNameBrand( + final OpenFoodFactsLanguage language, + final String separator, + ) { + final String bestProductName = getBestProductName(language); + final String? firstBrand = getFirstBrand(); + if (firstBrand == null) { + return bestProductName; + } + return '$bestProductName$separator$firstBrand'; + } + + /// Returns a combo of best product name, first brand and quantity. + /// + /// cf. openfoodfacts-server/lib/ProductOpener/Products.pm + String getProductNameBrandQuantity( + final OpenFoodFactsLanguage language, + final String separator, + ) { + final String productNameBrand = getProductNameBrand(language, separator); + if (quantity?.isNotEmpty != true) { + return productNameBrand; + } + if (productNameBrand.contains(quantity!)) { + return productNameBrand; + } + // quantity: put non breaking spaces between numbers and units + return '$productNameBrand$separator${quantity!.replaceAll(' ', '\u{00A0}')}'; + } } diff --git a/lib/src/model/product.g.dart b/lib/src/model/product.g.dart index a5955630b5..6a6f201c97 100644 --- a/lib/src/model/product.g.dart +++ b/lib/src/model/product.g.dart @@ -103,6 +103,9 @@ Product _$ProductFromJson(Map json) => Product( ) ..genericNameInLanguages = LanguageHelper.fromJsonStringMap(json['generic_name_in_languages']) + ..abbreviatedName = json['abbreviated_product_name'] as String? + ..abbreviatedNameInLanguages = LanguageHelper.fromJsonStringMap( + json['abbreviated_product_name_in_languages']) ..brandsTagsInLanguages = LanguageHelper.fromJsonStringsListMap( json['brands_tags_in_languages']) ..imagesFreshnessInLanguages = @@ -180,6 +183,9 @@ Map _$ProductToJson(Product instance) { writeNotNull('generic_name', instance.genericName); writeNotNull('generic_name_in_languages', LanguageHelper.toJsonStringMap(instance.genericNameInLanguages)); + writeNotNull('abbreviated_product_name', instance.abbreviatedName); + writeNotNull('abbreviated_product_name_in_languages', + LanguageHelper.toJsonStringMap(instance.abbreviatedNameInLanguages)); writeNotNull('brands', instance.brands); writeNotNull('brands_tags', instance.brandsTags); writeNotNull('brands_tags_in_languages', diff --git a/lib/src/utils/product_fields.dart b/lib/src/utils/product_fields.dart index af897f3fa5..92f61f8d5d 100644 --- a/lib/src/utils/product_fields.dart +++ b/lib/src/utils/product_fields.dart @@ -10,6 +10,9 @@ enum ProductField implements OffTagged { GENERIC_NAME(offTag: 'generic_name'), GENERIC_NAME_IN_LANGUAGES(offTag: 'generic_name_'), GENERIC_NAME_ALL_LANGUAGES(offTag: 'generic_name_languages'), + ABBREVIATED_NAME(offTag: 'abbreviated_product_name'), + ABBREVIATED_NAME_IN_LANGUAGES(offTag: 'abbreviated_product_name_'), + ABBREVIATED_NAME_ALL_LANGUAGES(offTag: 'abbreviated_product_name_languages'), BRANDS(offTag: 'brands'), BRANDS_TAGS(offTag: 'brands_tags'), BRANDS_TAGS_IN_LANGUAGES(offTag: 'brands_tags_'), @@ -111,6 +114,7 @@ enum ProductField implements OffTagged { const Set fieldsInLanguages = { ProductField.NAME_IN_LANGUAGES, ProductField.GENERIC_NAME_IN_LANGUAGES, + ProductField.ABBREVIATED_NAME_IN_LANGUAGES, ProductField.INGREDIENTS_TEXT_IN_LANGUAGES, ProductField.PACKAGING_TEXT_IN_LANGUAGES, ProductField.CATEGORIES_TAGS_IN_LANGUAGES, @@ -129,6 +133,7 @@ const Set fieldsInLanguages = { const Set fieldsAllLanguages = { ProductField.NAME_ALL_LANGUAGES, ProductField.GENERIC_NAME_ALL_LANGUAGES, + ProductField.ABBREVIATED_NAME_ALL_LANGUAGES, ProductField.INGREDIENTS_TEXT_ALL_LANGUAGES, ProductField.PACKAGING_TEXT_ALL_LANGUAGES, }; diff --git a/test/api_get_product_test.dart b/test/api_get_product_test.dart index 3df19ba186..d4eb50a98a 100644 --- a/test/api_get_product_test.dart +++ b/test/api_get_product_test.dart @@ -515,11 +515,18 @@ void main() { }); test('product fields', () async { - String barcode = '20004361'; + const String barcode = '7300400481588'; ProductQueryConfiguration configurations = ProductQueryConfiguration( barcode, language: OpenFoodFactsLanguage.GERMAN, - fields: [ProductField.NAME, ProductField.BRANDS_TAGS], + fields: [ + ProductField.NAME, + ProductField.BRANDS_TAGS, + ProductField.ABBREVIATED_NAME, + ProductField.ABBREVIATED_NAME_ALL_LANGUAGES, + ProductField.BRANDS, + ProductField.QUANTITY, + ], version: ProductQueryVersion.v3, ); ProductResultV3 result = await OpenFoodAPIClient.getProductV3( @@ -536,6 +543,15 @@ void main() { expect(result.product!.additives!.names, isEmpty); expect(result.product!.nutrientLevels!.levels, isEmpty); expect(result.product!.lang, OpenFoodFactsLanguage.UNDEFINED); + expect(result.product!.abbreviatedName, isNotNull); + expect(result.product!.abbreviatedNameInLanguages, isNotNull); + expect( + result + .product!.abbreviatedNameInLanguages![OpenFoodFactsLanguage.FRENCH], + isNotNull, + ); + expect(result.product!.brands, isNotNull); + expect(result.product!.quantity, isNotNull); configurations = ProductQueryConfiguration( barcode,