diff --git a/executors/dart/bin/executor.dart b/executors/dart/bin/executor.dart index 478de2f6..bef1677a 100644 --- a/executors/dart/bin/executor.dart +++ b/executors/dart/bin/executor.dart @@ -82,7 +82,7 @@ void printVersion() { final dartVersion = version.substring(0, version.indexOf(' ')); final versionInfo = { 'platform': 'Dart Native', - 'icuVersion': '73', //TODO: get from ICU4X somehow + 'icuVersion': '77', //TODO: get from ICU4X somehow 'platformVersion': dartVersion, 'intlVersion': intl4xVersion, }; diff --git a/executors/dart/lib/collator.dart b/executors/dart/lib/collator.dart index 0d9a9f7c..84e87a57 100644 --- a/executors/dart/lib/collator.dart +++ b/executors/dart/lib/collator.dart @@ -6,16 +6,22 @@ import 'package:intl4x/collation.dart'; import 'package:intl4x/intl4x.dart'; String testCollation(String jsonEncoded) { - final json = - jsonDecode(jsonEncoded) - as Map< - String, - dynamic - >; // For the moment, use strings for easier interop + final json = jsonDecode(jsonEncoded) as Map; // Global default locale - final testLocale = json['locale'] as String? ?? 'en'; final outputLine = {'label': json['label']}; - + if (json.containsKey('rules')) { + outputLine['error_type'] = 'unsupported'; + outputLine['unsupported'] = 'unsupported_options'; + outputLine['error_message'] = 'Rules are not supported'; + return jsonEncode(outputLine); + } + final localeString = json['locale'] as String? ?? 'en'; + if (localeString == 'root') { + outputLine['error_type'] = 'unsupported'; + outputLine['unsupported'] = 'unsupported_options'; + outputLine['error_message'] = 'Locale `root` is unsupported'; + return jsonEncode(outputLine); + } // Set up collator object with optional locale and testOptions. final s1 = json['s1']; final s2 = json['s2']; @@ -36,6 +42,7 @@ String testCollation(String jsonEncoded) { .where((value) => value.jsName == json['case_first']) .firstOrNull ?? CaseFirst.localeDependent; + final compareType = json['compare_type'] as String?; if (s1 == null || s2 == null) { outputLine.addAll({ @@ -45,7 +52,7 @@ String testCollation(String jsonEncoded) { }); } else { try { - final coll = Intl(locale: Locale.parse(testLocale)); + final coll = Intl(locale: Locale.parse(localeString)); final collationOptions = CollationOptions( ignorePunctuation: ignorePunctuation, @@ -55,7 +62,17 @@ String testCollation(String jsonEncoded) { ); final compared = coll.collation(collationOptions).compare(s1, s2); - final result = compared <= 0; + + bool result; + if (compareType == '=') { + // Check for strict equality comparison + result = compared == 0; + } else if (compareType != null && compareType.startsWith('<')) { + // Check results with different compare types + result = compared < 0; + } else { + result = compared <= 0; + } outputLine['result'] = result; if (result != true) { diff --git a/executors/dart/lib/datetime_format.dart b/executors/dart/lib/datetime_format.dart index a32861ea..21c270f8 100644 --- a/executors/dart/lib/datetime_format.dart +++ b/executors/dart/lib/datetime_format.dart @@ -1,4 +1,5 @@ import 'dart:convert'; + import 'package:collection/collection.dart'; import 'package:intl4x/datetime_format.dart'; import 'package:intl4x/intl4x.dart'; @@ -10,22 +11,21 @@ import 'package:intl4x/intl4x.dart'; String testDateTimeFmt(String jsonEncoded) { final json = jsonDecode(jsonEncoded) as Map; - final label = json['label']; var localeString = json['locale'] as String?; // locale can be null from JSON if (localeString == null || localeString.isEmpty) { localeString = 'und'; // Default to 'und' if locale is null or empty } + final returnJson = {'label': json['label']}; + // Parse Locale string Locale locale; try { locale = Locale.parse(localeString.replaceAll('_', '-')); } catch (e) { - return jsonEncode({ - 'label': label, - 'error': 'Invalid locale format: ${e.toString()}', - 'locale': localeString, - }); + returnJson['error'] = 'Invalid locale format: ${e.toString()}'; + returnJson['locale'] = localeString; + return jsonEncode(returnJson); } var testOptionsJson = {}; @@ -33,6 +33,12 @@ String testDateTimeFmt(String jsonEncoded) { testOptionsJson = json['options'] as Map; } + if (testOptionsJson['dateTimeFormatType'] == 'atTime') { + returnJson['error_type'] = 'unsupported'; + returnJson['unsupported'] = '`at` not supported'; + return jsonEncode(returnJson); + } + // Initialize DateTimeFormatOptions var dateTimeFormatOptions = DateTimeFormatOptions(); @@ -53,11 +59,22 @@ String testDateTimeFmt(String jsonEncoded) { } // ignore: unused_local_variable - to be used with the timezoneformatter - String? timezone; - if (testOptionsJson.containsKey('time_zone')) { - timezone = testOptionsJson['time_zone'] as String; + String? timeZoneName; + int? offsetSeconds; + if (testOptionsJson.containsKey('timeZone') && + json.containsKey('tz_offset_secs')) { + timeZoneName = testOptionsJson['timeZone'] as String; + offsetSeconds = (json['tz_offset_secs'] as num).toInt(); } + final semanticSkeleton = testOptionsJson['semanticSkeleton'] as String?; + final semanticSkeletonLength = + testOptionsJson['semanticSkeletonLength'] as String?; + + final dateStyle = testOptionsJson['dateStyle'] as String?; + final timeStyle = testOptionsJson['timeStyle'] as String?; + final yearStyle = testOptionsJson['yearStyle'] as String?; + DateTime? testDate; if (json['input_string'] != null) { final isoDateString = json['input_string'] as String; @@ -72,34 +89,133 @@ String testDateTimeFmt(String jsonEncoded) { try { testDate = DateTime.parse(testDateString); } catch (e) { - return jsonEncode({ - 'label': label, - 'error': 'Invalid input_string date format: ${e.toString()}', - 'input_string': isoDateString, - }); + returnJson['error'] = 'Invalid input_string date format: ${e.toString()}'; + returnJson['input_string'] = isoDateString; + return jsonEncode(returnJson); } } else { testDate = DateTime.now(); } - final returnJson = {'label': label}; final dtFormatter = Intl( locale: locale, ).dateTimeFormat(dateTimeFormatOptions); - try {} catch (error) { - returnJson['error'] = 'DateTimeFormat Constructor: ${error.toString()}'; - returnJson['options'] = testOptionsJson; - return jsonEncode(returnJson); - } - try { - final formattedDt = dtFormatter.ymd(testDate); + final formatter = semanticSkeleton != null + ? getFormatterForSkeleton( + semanticSkeleton, + semanticSkeletonLength, + dtFormatter, + ) + : getFormatterForStyle(dateStyle, timeStyle, yearStyle, dtFormatter); + String formattedDt; + if (timeStyle == 'full' && timeZoneName != null) { + final offset = Duration(seconds: offsetSeconds!); + final timeZone = TimeZone(name: timeZoneName, offset: offset); + final timeZoneStyle = 'long'; + final zonedFormatter = getZonedFormatter(timeZoneStyle, formatter); + formattedDt = zonedFormatter.format(testDate.add(offset), timeZone); + } else { + formattedDt = formatter.format(testDate); + } returnJson['result'] = formattedDt; - returnJson['actual_options'] = dateTimeFormatOptions.toString(); - } catch (error) { - returnJson['unsupported'] = ': ${error.toString()}'; + } on Exception catch (e) { + returnJson['error_type'] = 'unsupported'; + returnJson['unsupported'] = ': ${e.toString()}'; + return jsonEncode(returnJson); } + returnJson['actual_options'] = dateTimeFormatOptions.humanReadable; + returnJson['options'] = testOptionsJson; return jsonEncode(returnJson); } + +DateTimeFormatter getFormatterForSkeleton( + String semanticSkeleton, + String? semanticSkeletonLength, + DateTimeFormatBuilder dtFormatter, +) { + // The provided Rust code implies a more complex logic, but here we'll map the known skeletons. + // The Rust code's `None => None` and `None => Ok(...)` branches aren't directly translatable + // to a Dart function that must return a Formatter. We'll handle the valid cases and throw for others. + + final semanticDateStyle = switch (semanticSkeletonLength) { + 'short' => DateFormatStyle.short, + 'medium' => DateFormatStyle.medium, + 'long' => DateFormatStyle.long, + _ => throw Exception(), + }; + return switch (semanticSkeleton) { + 'D' || 'DT' || 'DTZ' => dtFormatter.d(), + 'MD' => dtFormatter.md(), + 'MDT' || 'MDTZ' => dtFormatter.mdt(dateStyle: semanticDateStyle), + 'YMD' || 'YMDT' || 'YMDTZ' => dtFormatter.ymd(dateStyle: semanticDateStyle), + 'YMDE' => dtFormatter.ymde(dateStyle: semanticDateStyle), + 'YMDET' || 'YMDETZ' => dtFormatter.ymdet(dateStyle: semanticDateStyle), + 'M' => dtFormatter.m(), + 'Y' => dtFormatter.y(), + 'T' || 'Z' || 'TZ' => dtFormatter.t(), + _ => throw Exception('Unknown skeleton: $semanticSkeleton'), + }; +} + +ZonedDateTimeFormatter getZonedFormatter( + String timeZoneStyle, + DateTimeFormatter formatter, +) => switch (timeZoneStyle) { + 'short' => formatter.withTimeZoneShort(), + 'long' => formatter.withTimeZoneLong(), + 'full' => formatter.withTimeZoneLongGeneric(), + String() => throw Exception('Unknown time zone style `$timeZoneStyle`'), +}; + +DateTimeFormatter getFormatterForStyle( + String? dateStyle, + String? timeStyle, + String? yearStyle, + DateTimeFormatBuilder dtFormatter, +) => switch ((dateStyle, timeStyle, yearStyle)) { + ('medium', null, _) => dtFormatter.ymd(dateStyle: DateFormatStyle.medium), + (null, 'short', _) => dtFormatter.t(style: TimeFormatStyle.short), + ('full', 'short', null) => dtFormatter.ymdet( + dateStyle: DateFormatStyle.full, + timeStyle: TimeFormatStyle.short, + ), + ('full', 'full', null) => dtFormatter.ymdet( + dateStyle: DateFormatStyle.full, + timeStyle: TimeFormatStyle.full, + ), + ('short', 'full', null) => dtFormatter.ymdt( + dateStyle: DateFormatStyle.short, + timeStyle: TimeFormatStyle.full, + ), + ('short', 'full', 'with_era') => dtFormatter.ymdet( + dateStyle: DateFormatStyle.short, + timeStyle: TimeFormatStyle.full, + ), + (_, _, 'with_era') => dtFormatter.ymde(), + (_, _, _) => throw Exception( + 'Unknown combination of date style `$dateStyle`, time style `$timeStyle`, and year style `$yearStyle`', + ), +}; + +extension on DateTimeFormatOptions { + String get humanReadable { + final fields = { + if (calendar != null) 'calendar': calendar, + if (dayPeriod != null) 'dayPeriod': dayPeriod, + if (numberingSystem != null) 'numberingSystem': numberingSystem, + if (clockstyle != null) 'clockstyle': clockstyle, + if (era != null) 'era': era, + if (timestyle != null) 'timestyle': timestyle, + if (fractionalSecondDigits != null) + 'fractionalSecondDigits': fractionalSecondDigits, + 'formatMatcher': formatMatcher, + }; + final entries = fields.entries + .map((e) => '${e.key}: ${e.value}') + .join(', '); + return 'DateTimeFormatOptions($entries)'; + } +} diff --git a/executors/dart/lib/lang_names.dart b/executors/dart/lib/lang_names.dart index 107fbfe6..f04c2e3b 100644 --- a/executors/dart/lib/lang_names.dart +++ b/executors/dart/lib/lang_names.dart @@ -26,6 +26,14 @@ String testLangNames(String jsonEncoded) { } final languageLabel = json['language_label'] as String; + if (languageLabel.contains('u-kr')) { + outputLine.addAll({ + 'unsupported': 'u-kr extension not supported', + 'error_retry': false, // Do not repeat + }); + return jsonEncode(outputLine); + } + Locale languageLabelLocale; try { languageLabelLocale = Locale.parse(languageLabel); @@ -49,7 +57,6 @@ String testLangNames(String jsonEncoded) { outputLine['result'] = resultLangName; } catch (error) { outputLine.addAll({ - 'error_type': 'unsupported', 'error_detail': error.toString(), 'actual_options': options.toJson(), 'error_retry': false, // Do not repeat diff --git a/executors/dart/lib/numberformat.dart b/executors/dart/lib/numberformat.dart index 7419b5cc..e2a8caf3 100644 --- a/executors/dart/lib/numberformat.dart +++ b/executors/dart/lib/numberformat.dart @@ -280,7 +280,8 @@ NumberFormatOptions _decimalPatternToOptions( ); return numberFormatOptions.copyWith(roundingMode: roundingMode); } else { - return numberFormatOptions; + //TODO: remove this halfEven default override, as soon as it is always passed in the numberformat args. + return numberFormatOptions.copyWith(roundingMode: RoundingMode.halfEven); } } @@ -324,9 +325,6 @@ NumberFormatOptions _fromJson(Map options) { final signDisplay = SignDisplay.values .where((element) => element.name == options['signDisplay']) .firstOrNull; - final localeMatcher = LocaleMatcher.values - .where((element) => element.jsName == options['localeMatcher']) - .firstOrNull; final useGrouping = Grouping.values .where((element) => element.jsName == options['useGrouping']) .firstOrNull; @@ -374,7 +372,6 @@ NumberFormatOptions _fromJson(Map options) { return NumberFormatOptions.custom().copyWith( style: style, currency: currency, - localeMatcher: localeMatcher, signDisplay: signDisplay, notation: notation, useGrouping: useGrouping, @@ -391,11 +388,10 @@ extension on NumberFormatOptions { return { 'style': style.name, 'currency': currency, - 'localeMatcher': localeMatcher.jsName, 'signDisplay': signDisplay.name, 'notation': notation.name, 'useGrouping': useGrouping.jsName, - 'numberingSystem': numberingSystem?.toString(), + 'numberingSystem': numberingSystem, 'roundingMode': roundingMode.name, 'trailingZeroDisplay': trailingZeroDisplay.name, 'minimumIntegerDigits': minimumIntegerDigits, diff --git a/executors/dart/lib/version.dart b/executors/dart/lib/version.dart index cdd58ea8..d54983bd 100644 --- a/executors/dart/lib/version.dart +++ b/executors/dart/lib/version.dart @@ -1,2 +1,2 @@ /// This file is autogenerated by bin/set_version.dart, do not modify manually. -const intl4xVersion = '0.12.2'; +const intl4xVersion = '0.13.0'; diff --git a/executors/dart/out/version.js b/executors/dart/out/version.js index 1cfd1d1a..92fc0e76 100644 --- a/executors/dart/out/version.js +++ b/executors/dart/out/version.js @@ -1,2 +1,2 @@ -const dartVersion = "0.12.2"; +const dartVersion = "0.13.0"; module.exports = { dartVersion }; diff --git a/executors/dart/pubspec.lock b/executors/dart/pubspec.lock index 1c22bf3d..c2800c5c 100644 --- a/executors/dart/pubspec.lock +++ b/executors/dart/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f7bac1065b51df46b2291296e1c1b3616a47aeb735aea46a8ca3dcb7bb700ee7 + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "86.0.0" + version: "88.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "4001e2de7c9d125af9504b4c4f64ebba507c9cb9c712caf02ac1d4c37824f58c" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.1" args: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: code_assets - sha256: dd7ed641b7f642092092969f2dcd5845ab31c9f3efead0c06ca437bf9ce8a8b2 + sha256: "7f42899e7b22f7810ea8c2b281c979add25555fbe391dde539621069e20b90b4" url: "https://pub.dev" source: hosted - version: "0.19.4" + version: "0.19.5" collection: dependency: "direct main" description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: hooks - sha256: "75363eae6c0c2db051c4f6b3b1fcdea8a09c4a596cc83bfff847661da6e80dfc" + sha256: "2bd640e4625fdfe5788ef33d825a8639797d44bce05695ab13543cde0ff9a078" url: "https://pub.dev" source: hosted - version: "0.19.5" + version: "0.20.0" http_multi_server: dependency: transitive description: @@ -153,14 +153,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + icu4x: + dependency: transitive + description: + name: icu4x + sha256: "9e89a363c410608de336e1a15c599ecc49245cc6f9d7d719b4b1ced57151c92c" + url: "https://pub.dev" + source: hosted + version: "2.0.0-dev.0" intl4x: dependency: "direct main" description: name: intl4x - sha256: "2684e98ae48b288e345bfc32356d9497d120fafc5a5bf8d840d1a6fd19f3a526" + sha256: "17ca4511e95d036954897ca192d771598b3744c07bd43de72a35af795f244228" url: "https://pub.dev" source: hosted - version: "0.12.2" + version: "0.13.0" io: dependency: transitive description: @@ -229,10 +237,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "74a0c80d877c519bc6bde2c4e27b6b01c1f93c9b480f65ceae8bedd3aba3c086" + sha256: "7e8358a4f6ec69a4f2d366bb971af298aca50d6c2e8a07be7c12d7f6d40460aa" url: "https://pub.dev" source: hosted - version: "0.16.8" + version: "0.17.1" node_preamble: dependency: "direct main" description: @@ -421,10 +429,10 @@ packages: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" web: dependency: transitive description: diff --git a/executors/dart/pubspec.yaml b/executors/dart/pubspec.yaml index 25a564ff..6f18a0b3 100644 --- a/executors/dart/pubspec.yaml +++ b/executors/dart/pubspec.yaml @@ -6,7 +6,7 @@ environment: dependencies: collection: ^1.19.1 - intl4x: 0.12.2 + intl4x: 0.13.0 node_preamble: ^2.0.2 pubspec_lock_parse: ^2.2.0 diff --git a/executors/dart/test/browser_test.dart b/executors/dart/test/misc_test.dart similarity index 55% rename from executors/dart/test/browser_test.dart rename to executors/dart/test/misc_test.dart index 72a85d58..976f4e17 100644 --- a/executors/dart/test/browser_test.dart +++ b/executors/dart/test/misc_test.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:dart_executor/collator.dart'; +import 'package:dart_executor/datetime_format.dart'; import 'package:dart_executor/lang_names.dart'; import 'package:dart_executor/numberformat.dart'; +import 'package:intl4x/datetime_format.dart'; +import 'package:intl4x/intl4x.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart'; @@ -57,6 +60,58 @@ void main() { final decoded = jsonDecode(outputLine) as Map; expect(decoded['result'], 'Spanish'); }); + + testWithFormatting('decimal format compact', () { + final input = { + 'label': '0730', + 'locale': 'es-MX', + 'skeleton': 'compact-short unit-width-narrow @@', + 'input': '91827.3645', + 'options': { + 'notation': 'compact', + 'compactDisplay': 'short', + 'unitDisplay': 'narrow', + 'currencyDisplay': 'narrowSymbol', + 'maximumSignificantDigits': 2, + 'minimumSignificantDigits': 2, + }, + 'hexhash': '95514d4fd3ab3f2a24c86bb0e0f21c8f7ec57142', + }; + final outputLine = testDecimalFormatWrapped(jsonEncode(input)); + final output = jsonDecode(outputLine) as Map; + expect(output['result'], '92 k'); + }, skip: 'Failing for now'); + + testWithFormatting('datetime format short', () { + final input = { + 'label': '048', + 'locale': 'en', + 'input_string': '2000-01-01T00:00:00Z', + 'options': { + 'timeZone': 'Etc/GMT', + 'calendar': 'gregory', + 'zoneStyle': 'location', + 'skeleton': 'MdjmsVVVV', + 'semanticSkeleton': 'MDTZ', + 'semanticSkeletonLength': 'short', + }, + 'tz_offset_secs': 0, + 'original_input': '2000-01-01T00:00Z[Etc/GMT]', + 'hexhash': '3b045161836e839e3d19eefc37c1d6923ba63615', + }; + final outputLine = testDateTimeFmt(jsonEncode(input)); + final output = jsonDecode(outputLine) as Map; + print( + Intl(locale: Locale.parse(input['locale'] as String)) + .dateTimeFormat(DateTimeFormatOptions()) + .ymdt( + dateStyle: DateFormatStyle.short, + timeStyle: TimeFormatStyle.short, + ) + .format(DateTime.parse(input['input_string'] as String)), + ); + expect(output['result'], '1/1, 12:00:00 AM GMT'); + }, skip: 'Failing for now'); } @isTest diff --git a/generateDataAndRun.sh b/generateDataAndRun.sh index 672b3195..d812f093 100755 --- a/generateDataAndRun.sh +++ b/generateDataAndRun.sh @@ -48,8 +48,9 @@ mkdir -p $TEMP_DIR/testData # Generates all new test data source_file=${1:-'run_config.json'} pushd testgen +all_test_types=$(jq '.[].run.test_type' ../$source_file | jq -s '.' | jq 'add' | jq 'unique' | jq -r 'join(" ")') all_icu_versions=$(jq '.[].run.icu_version' ../$source_file | jq -s '.' | jq 'unique' | jq -r 'join(" ")') -python3 testdata_gen.py --icu_versions $all_icu_versions +python3 testdata_gen.py --icu_versions $all_icu_versions --test_types $all_test_types # And copy results to subdirectories. cp -r icu* ../$TEMP_DIR/testData popd @@ -145,7 +146,6 @@ popd mkdir -p $TEMP_DIR/testReports pushd verifier -all_test_types=$(jq '.[].run.test_type' ../$source_file | jq -s '.' | jq 'add' | jq 'unique' | jq -r 'join(" ")') all_execs=$(jq -r 'join(" ")' <<< $all_execs_json) # Specifies the arrangement of the columns in the summary dashboard diff --git a/run_config.json b/run_config.json index 369e5f7d..7ea7a446 100644 --- a/run_config.json +++ b/run_config.json @@ -224,7 +224,7 @@ }, { "run": { - "icu_version": "icu74", + "icu_version": "icu77", "exec": "dart_native", "test_type": [ "collation", @@ -236,6 +236,26 @@ "per_execution": 10000 } }, + { + "prereq": { + "name": "nvm 24.0.0, icu77.1", + "version": "24.0.0", + "command": "nvm install 24.0.0;nvm use 24.0.0 --silent" + }, + "run": { + "icu_version": "icu76", + "exec": "dart_web", + "test_type": [ + "collation", + "datetime_fmt", + "number_fmt", + "lang_names", + "plural_rules", + "list_fmt" + ], + "per_execution": 10000 + } + }, { "prereq": { "name": "nvm",