diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c048b6b39..592542200ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * `StringConversionSink` #### `dart:core` +- Added `bool.parse` and `bool.tryParse` static methods. - **Breaking change** [#49529][]: - Removed the deprecated `List` constructor, as it wasn't null safe. diff --git a/pkg/analyzer_cli/test/data/sky_engine/lib/core.dart b/pkg/analyzer_cli/test/data/sky_engine/lib/core.dart index b2d972f2f81..b9907b40675 100644 --- a/pkg/analyzer_cli/test/data/sky_engine/lib/core.dart +++ b/pkg/analyzer_cli/test/data/sky_engine/lib/core.dart @@ -34,7 +34,9 @@ abstract class String implements Comparable { List get codeUnits; } -class bool extends Object {} +class bool extends Object { + static bool parse(String source, {bool caseSensitive = false}) => false; +} abstract class num implements Comparable { bool operator <(num other); diff --git a/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart index eee50b280dd..fdcf03e6aa8 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart @@ -703,6 +703,16 @@ class bool { 'bool.hasEnvironment can only be used as a const constructor'); } + @patch + static bool parse(String source, {bool caseSensitive = true}) => + Primitives.parseBool(source, caseSensitive) ?? + (throw FormatException("Invalid boolean", source)); + + @patch + static bool? tryParse(String source, {bool caseSensitive = true}) { + return Primitives.parseBool(source, caseSensitive); + } + @patch int get hashCode => super.hashCode; diff --git a/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart b/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart index 0a7a5175915..9e788b13150 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/js_helper.dart @@ -159,6 +159,31 @@ class Primitives { return result; } + static bool? parseBool( + @nullCheck String source, @nullCheck bool caseSensitive) { + if (caseSensitive) { + return JS('bool', r'# == "true" || # != "false" && null', source, source); + } + return _compareIgnoreCase(source, "true") + ? true + : _compareIgnoreCase(source, "false") + ? false + : null; + } + + /// Compares a string against an ASCII lower-case letter-only string. + /// + /// Returns `true` if the [input] has the same length and same letters + /// as [lowerCaseTarget], `false` if not. + static bool _compareIgnoreCase(String input, String lowerCaseTarget) { + if (input.length != lowerCaseTarget.length) return false; + var delta = 0x20; + for (var i = 0; i < input.length; i++) { + delta |= input.codeUnitAt(i) ^ lowerCaseTarget.codeUnitAt(i); + } + return delta == 0x20; + } + /** `r"$".codeUnitAt(0)` */ static const int DOLLAR_CHAR_VALUE = 36; diff --git a/sdk/lib/_internal/js_runtime/lib/core_patch.dart b/sdk/lib/_internal/js_runtime/lib/core_patch.dart index 3b8d19bd874..392f879e326 100644 --- a/sdk/lib/_internal/js_runtime/lib/core_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/core_patch.dart @@ -580,6 +580,16 @@ class String { class bool { @patch int get hashCode => super.hashCode; + + @patch + static bool parse(String source, {bool caseSensitive = true}) => + tryParse(source, caseSensitive: caseSensitive) ?? + (throw FormatException("Invalid boolean", source)); + + @patch + static bool? tryParse(String source, {bool caseSensitive = true}) { + return Primitives.parseBool(source, caseSensitive); + } } @patch diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart index f3cd851bd33..a74961fe987 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart @@ -388,7 +388,6 @@ class Primitives { static bool? parseBool(String source, bool caseSensitive) { checkNotNullable(source, "source"); checkNotNullable(caseSensitive, "caseSensitive"); - // The caseSensitive defaults to true. if (caseSensitive) { return source == "true" ? true @@ -396,7 +395,6 @@ class Primitives { ? false : null; } - // Compare case-sensitive when caseSensitive is false. return _compareIgnoreCase(source, "true") ? true : _compareIgnoreCase(source, "false") diff --git a/sdk/lib/_internal/vm_shared/lib/bool_patch.dart b/sdk/lib/_internal/vm_shared/lib/bool_patch.dart index 998483b24c4..eddef63d3f5 100644 --- a/sdk/lib/_internal/vm_shared/lib/bool_patch.dart +++ b/sdk/lib/_internal/vm_shared/lib/bool_patch.dart @@ -2,7 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import "dart:_internal" show patch; +import "dart:_internal" show patch, checkNotNullable; @patch @pragma("vm:entry-point") @@ -20,4 +20,50 @@ class bool { int get hashCode => this ? 1231 : 1237; int get _identityHashCode => this ? 1231 : 1237; + + @patch + static bool parse(String source, {bool caseSensitive = true}) { + checkNotNullable(source, "source"); + checkNotNullable(caseSensitive, "caseSensitive"); + if (caseSensitive) { + return source == "true" || + source != "false" && + (throw FormatException("Invalid boolean", source)); + } + // Ignore case-sensitive when `caseSensitive` is false. + return _compareIgnoreCase(source, "true") || + !_compareIgnoreCase(source, "false") && + (throw FormatException("Invalid boolean", source)); + } + + @patch + static bool? tryParse(String source, {bool caseSensitive = true}) { + checkNotNullable(source, "source"); + checkNotNullable(caseSensitive, "caseSensitive"); + if (caseSensitive) { + return source == "true" + ? true + : source == "false" + ? false + : null; + } + return _compareIgnoreCase(source, "true") + ? true + : _compareIgnoreCase(source, "false") + ? false + : null; + } + + /// Compares a string against an ASCII lower-case letter-only string. + /// + /// Returns `true` if the [input] has the same length and same letters + /// as [lowerCaseTarget], `false` if not. + static bool _compareIgnoreCase(String input, String lowerCaseTarget) { + if (input.length != lowerCaseTarget.length) return false; + var delta = 0x20; + for (var i = 0; i < input.length; i++) { + delta |= input.codeUnitAt(i) ^ lowerCaseTarget.codeUnitAt(i); + } + return delta == 0x20; + } } diff --git a/sdk/lib/core/bool.dart b/sdk/lib/core/bool.dart index 1f62168931a..a3fc7009d36 100644 --- a/sdk/lib/core/bool.dart +++ b/sdk/lib/core/bool.dart @@ -94,6 +94,68 @@ final class bool { //ignore: const_factory external const factory bool.hasEnvironment(String name); + /// Parses [source] as an, optionally case-insensitive, boolean literal. + /// + /// If [caseSensitive] is `true`, which is the default, + /// the only accepted inputs are the strings `"true"` and `"false"`, + /// which returns the results `true` and `false` respectively. + /// + /// If [caseSensitive] is `false`, any combination of upper and lower case + /// ASCII letters in the words `"true"` and `"false"` are accepted, + /// as if the input was first lower-cased. + /// + /// Throws a [FormatException] if the [source] string does not contain + /// a valid boolean literal. + /// + /// Rather than throwing and immediately catching the [FormatException], + /// instead use [tryParse] to handle a potential parsing error. + /// + /// Example: + /// ```dart + /// print(bool.tryParse('true')); // true + /// print(bool.tryParse('false')); // false + /// print(bool.tryParse('TRUE')); // throws FormatException + /// print(bool.tryParse('TRUE', caseSensitive: false)); // true + /// print(bool.tryParse('FALSE', caseSensitive: false)); // false + /// print(bool.tryParse('NO')); // throws FormatException + /// print(bool.tryParse('YES')); // throws FormatException + /// print(bool.tryParse('0')); // throws FormatException + /// print(bool.tryParse('1')); // throws FormatException + /// ``` + @Since("3.0") + external static bool parse(String source, {bool caseSensitive = true}); + + /// Parses [source] as an, optionally case-insensitive, boolean literal. + /// + /// If [caseSensitive] is `true`, which is the default, + /// the only accepted inputs are the strings `"true"` and `"false"`, + /// which returns the results `true` and `false` respectively. + /// + /// If [caseSensitive] is `false`, any combination of upper and lower case + /// ASCII letters in the words `"true"` and `"false"` are accepted, + /// as if the input was first lower-cased. + /// + /// Returns `null` if the [source] string does not contain a valid + /// boolean literal. + /// + /// If the input can be assumed to be valid, use [bool.parse] to avoid + /// having to deal with a possible `null` result. + /// + /// Example: + /// ```dart + /// print(bool.tryParse('true')); // true + /// print(bool.tryParse('false')); // false + /// print(bool.tryParse('TRUE')); // null + /// print(bool.tryParse('TRUE', caseSensitive: false)); // true + /// print(bool.tryParse('FALSE', caseSensitive: false)); // false + /// print(bool.tryParse('NO')); // null + /// print(bool.tryParse('YES')); // null + /// print(bool.tryParse('0')); // null + /// print(bool.tryParse('1')); // null + /// ``` + @Since("3.0") + external static bool? tryParse(String source, {bool caseSensitive = true}); + external int get hashCode; /// The logical conjunction ("and") of this and [other]. diff --git a/tests/corelib/bool_parse_test.dart b/tests/corelib/bool_parse_test.dart new file mode 100644 index 00000000000..5cb273a0d12 --- /dev/null +++ b/tests/corelib/bool_parse_test.dart @@ -0,0 +1,23 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// SharedOptions=-Da=true -Db=false -Dc=NOTBOOL -Dd=True + +import "package:expect/expect.dart"; + +main() { + Expect.isTrue(bool.parse('true')); + Expect.isFalse(bool.parse('false')); + Expect.isTrue(bool.parse('TRUE', caseSensitive: false)); + Expect.isFalse(bool.parse('FALSE', caseSensitive: false)); + Expect.isTrue(bool.parse('true', caseSensitive: true)); + Expect.isFalse(bool.parse('false', caseSensitive: true)); + Expect.throws(() => bool.parse('True')); + Expect.throws(() => bool.parse('False')); + Expect.throws(() => bool.parse('y')); + Expect.throws(() => bool.parse('n')); + Expect.throws(() => bool.parse('0')); + Expect.throws(() => bool.parse('1')); + Expect.throws(() => bool.parse('TRUE', caseSensitive: true)); + Expect.throws(() => bool.parse('FALSE', caseSensitive: true)); +} diff --git a/tests/corelib/bool_try_parse_test.dart b/tests/corelib/bool_try_parse_test.dart new file mode 100644 index 00000000000..ba7034cfb3f --- /dev/null +++ b/tests/corelib/bool_try_parse_test.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// SharedOptions=-Da=true -Db=false -Dc=NOTBOOL -Dd=True + +import "package:expect/expect.dart"; + +main() { + Expect.isTrue(bool.tryParse('true')); + Expect.isFalse(bool.tryParse('false')); + Expect.isTrue(bool.tryParse('TRUE', caseSensitive: false)); + Expect.isFalse(bool.tryParse('FALSE', caseSensitive: false)); + Expect.isNull(bool.tryParse('TRUE')); + Expect.isNull(bool.tryParse('FALSE')); + Expect.isNull(bool.tryParse('y')); + Expect.isNull(bool.tryParse('n')); + Expect.isNull(bool.tryParse(' true ', caseSensitive: false)); + Expect.isNull(bool.tryParse(' false ', caseSensitive: false)); + Expect.isNull(bool.tryParse('0', caseSensitive: true)); + Expect.isNull(bool.tryParse('1', caseSensitive: true)); +}