diff --git a/lib/string_extensions.dart b/lib/string_extensions.dart new file mode 100644 index 00000000..131da666 --- /dev/null +++ b/lib/string_extensions.dart @@ -0,0 +1,106 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'strings.dart' as s; + +extension StringExtensions on String { + /// Returns [true] if the [String] is either null, empty or is solely made of + /// whitespace characters (as defined by [String.trim]). + /// + /// See also: + /// + /// * [isNotBlank] + bool get isBlank => s.isBlank(this); + + /// Returns [true] if the [String] is neither null, empty nor is solely made + /// of whitespace characters. + /// + /// See also: + /// + /// * [isBlank] + bool get isNotBlank => s.isNotBlank(this); + + /// Returns [true] if the [String] is either null or empty. + /// + /// See also: + /// + /// * [isNotEmpty] + bool get isEmpty => s.isEmpty(this); + + /// Returns [true] if the [String] is neither null nor empty. + /// + /// See also: + /// + /// * [isEmpty] + bool get isNotEmpty => s.isNotEmpty(this); + + /// Loops over the [String] and returns traversed characters. Takes arbitrary + /// [from] and [to] indices. Works as a substitute for [String.substring], + /// except it never throws [RangeError]. Supports negative indices. Think of + /// an index as a coordinate in an infinite in both directions vector, filled + /// with this repeating string, whose 0-th coordinate coincides with the 0-th + /// character in [s]. Then [loop] returns the sub-vector defined by the + /// interval ([from], [to]). [from] is inclusive. [to] is exclusive. + /// + /// This method throws exceptions on an empty string. + /// + /// If [to] is omitted or is [null] the traversing ends at the end of the loop. + /// + /// If [to] < [from], traverses [s] in the opposite direction. + /// + /// For example: + /// + /// 'Hello, World!'.loop(7) == 'World!' + /// 'ab'.loop(0, 6) == 'ababab' + /// 'test.txt'.loop(-3) == 'txt' + /// 'ldwor'.loop(-3, 2) == 'world' + String loop(int from, [int? to]) => s.loop(this, from, to); + + /// Returns a [String] of length [width] padded with the same number of + /// characters on the left and right from [fill]. On the right, characters are + /// selected from [fill] starting at the end so that the last character in + /// [fill] is the last character in the result. [fill] is repeated if + /// necessary to pad. + /// + /// Returns this [String] if its length is equal to or greater than [width]. + /// + /// If there are an odd number of characters to pad, then the right will be + /// padded with one more than the left. + String center(int width, String fill) => s.center(this, width, fill); + + /// Returns `true` if [other] is equal after being converted to lower + /// case. + bool equalsIgnoreCase(String? other) => s.equalsIgnoreCase(this, other); + + /// Compares to [other] after converting to lower case. + /// + /// [other] must not be null. + int compareIgnoreCase(String other) => s.compareIgnoreCase(this, other); +} + +extension CodeUnitExtensions on int { + /// Returns `true` if this Rune represents a digit. + /// + /// The definition of digit matches the Unicode `0x3?` range of Western + /// European digits. + bool get isDigit => s.isDigit(this); + + /// Returns `true` if this Rune represents a whitespace character. + /// + /// The definition of whitespace matches that used in [String.trim] which is + /// based on Unicode 6.2. This maybe be a different set of characters than the + /// environment's [RegExp] definition for whitespace, which is given by the + /// ECMAScript standard: http://ecma-international.org/ecma-262/5.1/#sec-15.10 + bool get isWhitespace => s.isWhitespace(this); +} diff --git a/lib/strings.dart b/lib/strings.dart index 6bad8a9d..56539e17 100644 --- a/lib/strings.dart +++ b/lib/strings.dart @@ -16,6 +16,10 @@ library quiver.strings; /// Returns [true] if [s] is either null, empty or is solely made of whitespace /// characters (as defined by [String.trim]). +/// +/// See also: +/// +/// * [isNotBlank] bool isBlank(String? s) => s == null || s.trim().isEmpty; /// Returns [true] if [s] is neither null, empty nor is solely made of whitespace @@ -27,9 +31,17 @@ bool isBlank(String? s) => s == null || s.trim().isEmpty; bool isNotBlank(String? s) => s != null && s.trim().isNotEmpty; /// Returns [true] if [s] is either null or empty. +/// +/// See also: +/// +/// * [isNotEmpty] bool isEmpty(String? s) => s == null || s.isEmpty; -/// Returns [true] if [s] is a not empty string. +/// Returns [true] if [s] is neither null nor empty. +/// +/// See also: +/// +/// * [isEmpty] bool isNotEmpty(String? s) => s != null && s.isNotEmpty; /// Returns a string with characters from the given [s] in reverse order. @@ -50,7 +62,7 @@ String _reverse(String s) { /// Loops over [s] and returns traversed characters. Takes arbitrary [from] and /// [to] indices. Works as a substitute for [String.substring], except it never /// throws [RangeError]. Supports negative indices. Think of an index as a -/// coordinate in an infinite in both directions vector filled with repeating +/// coordinate in an infinite in both directions vector, filled with repeating /// string [s], whose 0-th coordinate coincides with the 0-th character in [s]. /// Then [loop] returns the sub-vector defined by the interval ([from], [to]). /// [from] is inclusive. [to] is exclusive. @@ -123,12 +135,12 @@ bool isWhitespace(int rune) => rune == 0xFEFF; /// Returns a [String] of length [width] padded with the same number of -/// characters on the left and right from [fill]. On the right, characters are +/// characters on the left and right from [fill]. On the right, characters are /// selected from [fill] starting at the end so that the last character in /// [fill] is the last character in the result. [fill] is repeated if /// necessary to pad. /// -/// Returns [input] if `input.length` is equal to or greater than width. +/// Returns [input] if `input.length` is equal to or greater than [width]. /// [input] can be `null` and is treated as an empty string. /// /// If there are an odd number of characters to pad, then the right will be diff --git a/test/strings_test.dart b/test/strings_test.dart index 50257385..3aaa205e 100644 --- a/test/strings_test.dart +++ b/test/strings_test.dart @@ -14,6 +14,7 @@ library quiver.strings; +import 'package:quiver/string_extensions.dart'; import 'package:quiver/strings.dart'; import 'package:test/test.dart' hide isEmpty, isNotEmpty; @@ -24,12 +25,15 @@ void main() { }); test('should consider empty string a blank', () { expect(isBlank(''), isTrue); + expect(''.isBlank, isTrue); }); test('should consider white-space-only string a blank', () { expect(isBlank(' \n\t\r\f'), isTrue); + expect(' \n\t\r\f'.isBlank, isTrue); }); test('should consider non-whitespace string not a blank', () { expect(isBlank('hello'), isFalse); + expect('hello'.isBlank, isFalse); }); }); @@ -39,12 +43,15 @@ void main() { }); test('should consider empty string a blank', () { expect(isNotBlank(''), isFalse); + expect(''.isNotBlank, isFalse); }); test('should consider white-space-only string a blank', () { expect(isNotBlank(' \n\t\r\f'), isFalse); + expect(' \n\t\r\f'.isNotBlank, isFalse); }); test('should consider non-whitespace string not a blank', () { expect(isNotBlank('hello'), isTrue); + expect('hello'.isNotBlank, isTrue); }); }); @@ -54,12 +61,15 @@ void main() { }); test('should consider the empty string to be empty', () { expect(isEmpty(''), isTrue); + expect(''.isEmpty, isTrue); }); test('should consider whitespace string to be not empty', () { expect(isEmpty(' '), isFalse); + expect(' '.isEmpty, isFalse); }); test('should consider non-whitespace string to be not empty', () { expect(isEmpty('hello'), isFalse); + expect('hello'.isEmpty, isFalse); }); }); @@ -69,25 +79,36 @@ void main() { }); test('should consider the empty string to be empty', () { expect(isNotEmpty(''), isFalse); + expect(''.isNotEmpty, isFalse); }); test('should consider whitespace string to be not empty', () { expect(isNotEmpty(' '), isTrue); + expect(' '.isNotEmpty, isTrue); }); test('should consider non-whitespace string to be not empty', () { expect(isNotEmpty('hello'), isTrue); + expect('hello'.isNotEmpty, isTrue); }); }); group('isDigit', () { test('should return true for standard digits', () { for (var i = 0; i <= 9; i++) { - expect(isDigit('$i'.codeUnitAt(0)), isTrue); + int codeUnit = '$i'.codeUnitAt(0); + + expect(isDigit(codeUnit), isTrue); + expect(codeUnit.isDigit, isTrue); } }); test('should return false for non-digits', () { expect(isDigit('a'.codeUnitAt(0)), isFalse); + expect('a'.codeUnitAt(0).isDigit, isFalse); + expect(isDigit(' '.codeUnitAt(0)), isFalse); + expect(' '.codeUnitAt(0).isDigit, isFalse); + expect(isDigit('%'.codeUnitAt(0)), isFalse); + expect('%'.codeUnitAt(0).isDigit, isFalse); }); }); @@ -95,95 +116,130 @@ void main() { // Forward direction test cases test('should work like normal substring', () { expect(loop('hello', 1, 3), 'el'); + expect('hello'.loop(1, 3), 'el'); }); test('should work like normal substring full-string', () { expect(loop('hello', 0, 5), 'hello'); + expect('hello'.loop(0, 5), 'hello'); }); test('should be circular', () { expect(loop('ldwor', -3, 2), 'world'); + expect('ldwor'.loop(-3, 2), 'world'); }); test('should be circular over many loops', () { expect(loop('ab', 0, 8), 'abababab'); + expect('ab'.loop(0, 8), 'abababab'); }); test('should be circular over many loops starting loops away', () { expect(loop('ab', 4, 12), 'abababab'); + expect('ab'.loop(4, 12), 'abababab'); }); test('should be circular over many loops starting mid-way', () { expect(loop('ab', 1, 9), 'babababa'); + expect('ab'.loop(1, 9), 'babababa'); }); test('should be circular over many loops starting mid-way loops away', () { expect(loop('ab', 5, 13), 'babababa'); + expect('ab'.loop(5, 13), 'babababa'); }); test('should default to end of string', () { expect(loop('hello', 3), 'lo'); + expect('hello'.loop(3), 'lo'); }); test('should default to end of string from negative index', () { expect(loop('/home/user/test.txt', -3), 'txt'); + expect('/home/user/test.txt'.loop(-3), 'txt'); }); test('should default to end of string from far negative index', () { expect(loop('ab', -5), 'b'); + expect('ab'.loop(-5), 'b'); }); test('should handle in-fragment substring loops away negative', () { expect(loop('hello', -4, -2), 'el'); + expect('hello'.loop(-4, -2), 'el'); }); test('should handle in-fragment substring loops away positive', () { expect(loop('hello', 6, 8), 'el'); + expect('hello'.loop(6, 8), 'el'); }); // Backward direction test cases test('should traverse backwards', () { expect(loop('hello', 3, 0), 'leh'); + expect('hello'.loop(3, 0), 'leh'); }); test('should traverse backwards across boundary', () { expect(loop('eholl', 2, -3), 'hello'); + expect('eholl'.loop(2, -3), 'hello'); }); test('should traverse backwards many loops', () { expect(loop('ab', 0, -6), 'bababa'); + expect('ab'.loop(0, -6), 'bababa'); }); // Corner cases test('should throw on empty', () { expect(() => loop('', 6, 8), throwsArgumentError); + expect(() => ''.loop(6, 8), throwsArgumentError); }); }); group('center', () { test('should return the input if length greater than width', () { expect(center('abc', 2, '0'), 'abc'); + expect('abc'.center(2, '0'), 'abc'); + expect(center('abc', 3, '0'), 'abc'); + expect('abc'.center(3, '0'), 'abc'); }); test('should pad equal chars on left and right for even padding count', () { expect(center('abc', 5, '0'), '0abc0'); + expect('abc'.center(5, '0'), '0abc0'); + expect(center('abc', 9, '0'), '000abc000'); + expect('abc'.center(9, '0'), '000abc000'); }); test('should pad extra char on right for odd padding amount', () { expect(center('abc', 4, '0'), 'abc0'); + expect('abc'.center(4, '0'), 'abc0'); + expect(center('abc', 8, '0'), '00abc000'); + expect('abc'.center(8, '0'), '00abc000'); }); test('should use multi-character fills', () { expect(center('abc', 7, '012345'), '01abc45'); + expect('abc'.center(7, '012345'), '01abc45'); + expect(center('abc', 6, '012345'), '0abc45'); + expect('abc'.center(6, '012345'), '0abc45'); + expect(center('abc', 9, '01'), '010abc101'); + expect('abc'.center(9, '01'), '010abc101'); }); test('should handle null and empty inputs', () { expect(center(null, 4, '012345'), '0145'); expect(center('', 4, '012345'), '0145'); + expect(''.center(4, '012345'), '0145'); + expect(center(null, 5, '012345'), '01345'); expect(center('', 5, '012345'), '01345'); + expect(''.center(5, '012345'), '01345'); }); }); group('equalsIgnoreCase', () { test('should return true for equal Strings', () { expect(equalsIgnoreCase('abc', 'abc'), isTrue); + expect('abc'.equalsIgnoreCase('abc'), isTrue); }); test('should return true for case-insensitivly equal Strings', () { expect(equalsIgnoreCase('abc', 'AbC'), isTrue); + expect('abc'.equalsIgnoreCase('AbC'), isTrue); }); test('should return true for nulls', () { @@ -192,25 +248,37 @@ void main() { test('should return false for unequal Strings', () { expect(equalsIgnoreCase('abc', 'bcd'), isFalse); + expect('abc'.equalsIgnoreCase('bcd'), isFalse); }); test('should return false if one is null', () { - expect(equalsIgnoreCase('abc', null), isFalse); expect(equalsIgnoreCase(null, 'abc'), isFalse); + expect(equalsIgnoreCase('abc', null), isFalse); + expect('abc'.equalsIgnoreCase(null), isFalse); }); }); group('compareIgnoreCase', () { test('should return 0 for case-insensitivly equal Strings', () { expect(compareIgnoreCase('abc', 'abc'), 0); + expect('abc'.compareIgnoreCase('abc'), 0); + expect(compareIgnoreCase('abc', 'AbC'), 0); + expect('abc'.compareIgnoreCase('AbC'), 0); }); test('should return compare unequal Strings correctly', () { expect(compareIgnoreCase('abc', 'abd'), lessThan(0)); + expect('abc'.compareIgnoreCase('abd'), lessThan(0)); + expect(compareIgnoreCase('abc', 'abD'), lessThan(0)); + expect('abc'.compareIgnoreCase('abD'), lessThan(0)); + expect(compareIgnoreCase('abd', 'abc'), greaterThan(0)); + expect('abd'.compareIgnoreCase('abc'), greaterThan(0)); + expect(compareIgnoreCase('abD', 'abc'), greaterThan(0)); + expect('abD'.compareIgnoreCase('abc'), greaterThan(0)); }); }); }