Extension methods for strings and iterables (lists, sets, etc.), inspired by Haskell.
Strings are treated as lists, and have many of the same functions.
Alphabetical list of features: addMissing after, afterWhere, any, ascending, ascendingKeys, ascendingValues, average(orNull), backwards, before, beforeWhere, chr, chrs, concat, count, dec, deepContains, descending, descendingKeys, descendingValues, div, dropIndices, every, filter, flatMap, frequencies group, groupBy, head, inc, inclusive, inclusiveString, indicesOf, indicesWhere, inits, insertInOrder, intercalate, interleave, intersperse, is(Strictly)Lower, is(Strictly)Upper, keep, keepIndices, last, max, median(orNull), min, mode, mult, nub, product(OrNull), range, rangeString, removeWhitespace, replace, riffleIn, riffleOut, shuffled splitAt, startsWith, subtract, subtractAll, sum(OrNull), tail, tails, toInts, toDoubles, toRounded, toStrings, transform, wordCount, words, zip(With), >, >=, <, <=, *
For methods that return lazy iterables, import heart.dart.
For methods that return the same type (either List, Queue, QueueList, or Set), import heart_types.dart. Examples below show List type. Sets may have unexpected behavior since they remove duplicates.
Note: For methods that check occurrences of elements in an iterable, the default in this package is to apply Dart's DeepCollectionEquality().equals in both directions since it is normally asymmetric (different answers are possible depending on the order of the arguments), but a custom equalityFunction can be used for these methods, such as:
[1, {2}, 3].deepContains([1, {2}]) // true
[1, {2}, 3].deepContains([1, {2}], equalityFunction: (a, b) => a == b) // false
/// Iterables are not equal with == operatorDart has methods that always return lazy iterables. The methods here can maintain their types if importing heart_types.dart, and can apply to Strings.
The equivalents for map, where, and expand are transform, filter, and flatMap, respectively.
[10, 11, 12].transform((e) => e % 3) // [1, 2, 0]
'abc'.transform((char) => char * 2) // 'aabbcc'
[10, 11, 12].filter((e) => e.isEven) // [10, 12]
'abc'.filter((char) => char < 'c') // 'ab'
[[1, 2], [3, 4]].flatMap((e) => e) // [1, 2, 3, 4](There is also filterIndexed, filterType, filterNot, filterNotIndexed, flatMapIndexed)
flatMapString and flatMapStringIndexed apply to an iterable of Strings and return a String:
['hello', 'world'].flatMapString((element) => [element]) // 'helloworld'
['hello', 'world'].flatMapStringIndexed((index, element) => [index.toString(), element]) // '0hello1world'To complement Dart's maxOrNull and minOrNull for iterables.
These return null if empty:
[4, 5, 6].sumOrNull // 15
[4, 5, 6].productOrNull // 120
[2, 2, 3, 9].averageOrNull // 4.0
[2, 2, 3, 9].medianOrNull // 2.5Increment, decrement, multiply and divide each element:
[1, 2, 3].inc() // [2, 3, 4]
[1, 2, 3].inc(5) // [6, 7, 8]
[1, 2, 3].dec() // [0, 1, 2]
[1, 2, 3].dec(2) // [-1, 0, 1]
[1, 2, 3].mult(2) // [2, 4, 6]
[1, 2, 3].div(2) // [0, 1, 1]
<double>[1, 2, 3].div(2) // [0.5, 1.0, 1.5]insertInOrder inserts each new element before the first element that is greater than or equal:
[5, 3].insertInOrder([2, 1]) // [1, 2, 5, 3];For Strings, sum, product, average, median, max, min, inc, dec, mult, div, and insertInOrder work by using character codes:
'abc'.inc() // 'bcd'
'abc'.average // 'b'
'MA'.insertInOrder('aJbc') // 'JMAabc'Truncate, round, or convert to doubles:
[1.9, 2.9].toInts() // [1, 2]
[1.9, 2.9].toRounded() // [2, 3]
[1, 2].toDoubles() // [1.0, 2.0]mode returns all of the most common elements:
[2, 2, 3, 3, 9].mode() // [2, 3]
'aabbc'.mode // 'ab'Sorting:
List<int> l = [4, 5, 1, 2, 3].ascending; // [1, 2, 3, 4, 5]
List<int> l2 = [4, 5, 1, 2, 3].descending; // [5, 4, 3, 2, 1]
String s = 'hello'.ascending; // 'ehllo'
String s2 = 'hello'.descending; // 'ollhe'Reverse a String or iterable:
List<int> l = [1, 2, 3].backwards; // [3, 2, 1]
String s = 'hello'.backwards; // 'olleh'indicesOf finds every index where a pattern occurs:
[1, 2, 1, 2, 1].indicesOf([1, 2, 1]) // [0]
// To include overlap:
[1, 2, 1, 2, 1].indicesOf([1, 2, 1], overlap: true) // [0, 2]
// To start from the right:
[1, 2, 1, 2, 1].indicesOf([1, 2, 1], reverse: true) // [2]
'hello'.indicesOf('ll') // [2]indicesWhere finds indices where a condition is true:
[1, 2, 3].indicesWhere((e) => e.isEven) // [1]
'oneTWO'.indicesWhere((c) => c.isUpper) // [3, 4, 5]Count occurrences of a pattern, with option to include overlap:
[1, 2, 1, 2, 1].count([1, 2, 1]) // 1
[1, 2, 1, 2, 1].count([1, 2, 1], overlap: true) // 2
'12121'.count('121', overlap: true) // 2Map of frequencies of each element:
[1, 2, 3].frequencies() // {1: 1, 2: 1, 3: 1}
'aabc'.frequencies() // {'a': 2, 'b': 1, 'c': 1}Get everything before or after input:
[1, 2, 3].before([2, 3]) // [1]
[1, 2, 3].beforeWhere((e) => e.isEven) // [1]
[1, 2, 3, 4].after([3]) // [4]
[1, 2, 3, 4].afterWhere((e) => e.isEven) // [3, 4]
'hello'.before('l') // 'he'Optional parameters:
// Skip the first occurrence:
[1, 2, 3, 4, 3].before([3], skip: 1) // [1, 2, 3, 4]
// Include 3 in result:
[1, 2, 3, 4, 3].before([3], includeInResult: true) // [1, 2, 3]
// Start from the right:
[1, 2, 3, 4, 3].before([3], reverse: true) // [1, 2, 3, 4]Dart already has this for Strings.
bool b = [1, 2, 3].startsWith([1, 2]); // trueRemove duplicates:
List<int> l = [1, 2, 1, 2].nub(); // [1, 2]
String s = 'hello'.nub(); // 'helo'Optional paramater only nubs those elements:
[1, 1, 2, 2, 3, 3].nub([1, 2]) // [1, 2, 3, 3]
'aaabbbcc'.nub('ab') // 'abcc'Inserts an item in between all other elements:
[1, 2, 3].intersperse([0, 0]) // [1, 0, 0, 2, 0, 0, 3]
'hello'.intersperse('-') // 'h-e-l-l-o'
// Note: 'replace' method with empty first argument has similar behavior:
'hello'.replace('', '-') // '-h-e-l-l-o-'Optional parameters:
[1, 2, 3, 4].intersperse([0, 0], count: 1) // [1, 0, 0, 2, 3, 4]
[1, 2, 3, 4].intersperse([0, 0], count: 1, reverse: true) // [1, 2, 3, 0, 0, 4]
[1, 2, 3, 4].intersperse([0, 0], skip: 1) // [1, 2, 0, 0, 3, 0, 0, 4](These already exist for iterables)
bool b = 'hello'.any((char) => char == 'h'); // true
bool b2 = 'hello'.every((char) => char == 'h'); // falseRemove all occurrences or replace with something else:
[1, 1, 2, 3].replace([1], []); // [2, 3]
[1, 1, 2, 3].replace([1, 1], [99]) // [99, 2, 3]
'aaaa'.replace('a', 'b') // 'bbbb'Optional paramaters:
[1, 1, 1].replace([1], [3], count: 2) // [3, 3, 1]
[1, 1, 1].replace([1], [3], count: 1, reverse: true) // [1, 1, 3]
[1, 1, 1].replace([1], [3], skip: 1) // [1, 3, 3]recursive parameter will keep cycling through to remove the pattern again (but may stop to avoid infinite loops):
[3, 2, 2, 2, 1, 1, 1].replace([2, 1], [], recursive: true) // [3]keep keeps all elements from the original that are also in input.
[1, 1, 2, 3].keep([1, 2]) // [1, 1, 2]
'hello'.keep('world'); // 'llo'(Remove duplicates with nub)
addMissing adds elements that aren't already present.
It doesn't remove duplicates from original, but doesn't add duplicates from input.
[1, 1, 2, 3].addMissing([2, 3, 4, 4]) // [1, 1, 2, 3, 4]
'hello'.addMissing(' world') // 'hello wrd'(Use nub to remove duplicates, and concatenate normally to keep duplicates.)
[10, 11, 12].keepIndices([0, 2]) // [10, 12]
[10, 11, 12].dropIndices([0, 2]) // [11]
'123'.keepIndices([0, 2]) // '13'
'123'.dropIndices([0, 2]) // '2'subtract removes elements one at a time (like Haskell's \\):
[1, 1, 2, 2, 3].subtract([1, 3]) // [1, 2, 2]
[1, 1, 2, 2].subtract([1, 2, 3]) // [1, 2]
// ignores 3 since it is not in original list
'hello'.subtract('eo'); // 'hll'subtractAll removes all occurrences:
[1, 1, 2, 2].subtractAll([1]) // [2, 2]
'hello'.subtractAll('lo'); // 'he'head returns first element.
tail returns everything but the first element.
last returns the last element (Dart has this for iterables but not strings).
[1, 2, 3].head // 1
[].head // null
[1, 2, 3].tail // [2, 3]
[1].tail // []
[].tail // null
'hello'.head // 'h'
'hello'.tail // 'ello'
'hello'.last // 'o'tails
returns a nested iterable by removing one element at a time from the beginning:
[1, 2, 3].tails // [[1, 2, 3], [2, 3], [3], []]
// inclusive function defined in this package
List<List<int>> twelveDaysOfChristmas = inclusiveList(12, 1).tails.backwards;
// [[], [1], [2, 1], [3, 2, 1], [4, 3, 2, 1], [5, 4, 3, 2, 1], [6, 5, 4, 3, 2, 1], [7, 6, 5, 4, 3, 2, 1], [8, 7, 6, 5, 4, 3, 2, 1], [9, 8, 7, 6, 5, 4, 3, 2, 1], [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]]
'hello'.tails
// ['hello', 'ello', 'llo', 'lo', 'o', '']inits
returns a nested iterable by adding elements from the beginning:
[1, 2, 3].inits
// [[], [1], [1, 2], [1, 2, 3]]
'hi'.inits
// ['', 'h', 'hi']Split an iterable or string into two:
[5, 6, 7, 8].splitAt(2); // [[5, 6], [7, 8]]
'hello'.splitAt(2) // ['he', 'llo'] Combine by taking turns:
[1, 2, 3].interleave([4, 5, 6]);
// [1, 4, 2, 5, 3, 6]
'abc'.interleave('123')
// 'a1b2c3'Riffle shuffle: splits in half and interleaves them:
List<int> l = [1, 2, 3, 4, 5, 6].riffleOut(); // [1, 4, 2, 5, 3, 6]
List<int> l2 = [1, 2, 3, 4, 5, 6].riffleIn(); // [4, 1, 5, 2, 6, 3]
'12345'.riffleOut(); // '14253'
'12345'.riffleIn(); // '31425'
// inverse goes back to original:
'14253'.riffleOut(inverse: true) // '12345'group combines consecutive elements together if they are equal:
[1, 2, 3, 3, 1].group() // [[1], [2], [3, 3], [1]]
'hello'.group() // ['h', 'e', 'll', 'o']groupBy combines consecutive elements if they meet criteria.
In this example, items are in the same sublist if they are less than the one after:
[1, 2, 3, 2, 1].groupBy((a, b) => a < b)
// [[1, 2, 3], [2], [1]]
List<String> ls = 'HelLo'.groupBy((a, b) => a.isUpper && b.isLower);
// ['He', 'l', 'Lo']chr returns a String from a character code.
chrs returns a String from a list of codes.
97.chr // 'a'
[97, 98].chrs // 'ab'
// .codeUnits converts back to codesConvert all elements to Strings:
List<String> l = [1, 2, 3].toStrings(); // ['1', '2', '3']Concatenate nested iterable or Strings:
List<int> l = [[1, 2], [3, 4], [5, 6]].concat(); // [1, 2, 3, 4, 5, 6]
String str = ['hello', 'world'].concat(); // 'helloworld'Inserts elements between iterables (or String between Strings) and concatenates the result:
List<int> l = [[1, 2], [3, 4], [5, 6]].intercalate([0, 0]);
// [1, 2, 0, 0, 3, 4, 0, 0, 5, 6]
String s = ['hello', 'world'].intercalate('-');
// 'hello-world'Optional parameters:
[[1], [2], [3], [4]].intercalate([0, 0], count: 1) // [1, 0, 0, 2, 3, 4]
[[1], [2], [3], [4]].intercalate([0, 0], count: 1, reverse: true) // [1, 2, 3, 0, 0, 4]
[[1], [2], [3], [4]].intercalate([0, 0], skip: 1) // [1, 2, 0, 0, 3, 0, 0, 4]zip takes in a nested iterable, returns a nested iterable where corresponding elements are paired together.
[[1, 2, 3], [4, 5, 6]].zip() // [[1, 4], [2, 5], [3, 6]]
['hello', 'world'].zip() // ['hw', 'eo', 'lr', 'll', 'od']zipWith performs a function between corresponding elements:
[[1, 2, 3], [4, 5, 6]].zipWith((args) => args[0] + args[1]) // [5, 7, 9]
['hello', 'world'].zipWith((args) => args[0] == args[1]) // [false, false, false, true, false]String s = ' hello \n world '.removeWhitespace(); // 'helloworld'String s = 'hello world'.shuffled() // edwl hllorowords returns a List of words without whitespace.
wordCount takes the length of this List. Equivalent to .words.length.
List<String> listOfWords = 'hello world'.words; // ['hello', 'world']
int w = 'hello world'.wordCount; // 2'hello world'.isLower // true
'hello world'.isStrictlyLower // false (because of space)
'HELLO WORLD'.isUpper // true
'HELLO WORLD'.isStrictlyUpper // false (because of space)range and inclusive generate a lazy iterable of numbers.
rangeList and inclusiveList generate a List.
Iterable<int> l = range(5); // (0, 1, 2, 3, 4)
inclusive(5) // (0, 1, 2, 3, 4, 5)
range(-5) // (-4, -3, -2, -1, 0)
inclusive(-5) // (-5, -4, -3, -2, -1, 0)
range(0) // ()
inclusive(0) // (0)With two arguments, inclusive includes the second one, range does not .
range(1, 5) // (1, 2, 3, 4)
inclusive(1, 5) // (1, 2, 3, 4, 5)
range(1, -2) // (1, 0, -1)
inclusive(1, -2) // (1, 0, -1, -2)Third argument adds a step count:
range(1, 5, 2) // (1, 3)
inclusive(1, 5, 2) // (1, 3, 5)
range(1, -5, -2) // (1, -1, -3)
inclusive(1, -5, -2) // (1, -1, -3, -5)Arguments must have exactly one character:
rangeString('a', 'f') // 'abcde'
rangeString('c', 'a') // 'cb'
rangeString('a', 'g', 2) // 'ace'
inclusiveString('a', 'c') // 'abc'
inclusiveString('c', 'a') // 'cba'
inclusiveString('a', 'g', 2) // 'aceg'Compare elements in two iterables, starting at the beginning:
[1, 2, 3] > [1, 1, 3] // trueCompare strings according to their character codes:
'b' > 'a' // true
'hello' < 'hi' // true
['a', 1] >= ['b', 1] // false(If elements cannot be compared, both >= and <= will return false.)
Repeat elements with *:
List<int> l = [1, 2] * 3; // [1, 2, 1, 2, 1, 2]
// Dart has this for Strings:
String s = 'hello' * 3; // 'hellohellohello'{9: 5, 1: 2, 4: 8}.ascendingKeys // {1: 2, 4: 8, 9: 5}
{9: 5, 1: 2, 4: 8}.descendingKeys // {9: 5, 4: 8, 1: 2}
{9: 5, 1: 2, 4: 8}.ascendingValues // {1: 2, 9: 5, 4: 8}
{9: 5, 1: 2, 4: 8}.descendingValues // {4: 8, 9: 5, 1: 2}