Skip to content

MadSimple/heart

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Heart

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), >, >=, <, <=, *

Directions:

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 == operator

Equivalents for Dart collection methods

Dart 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'

Methods for numbers

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.5

Increment, 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]

Other methods

mode

mode returns all of the most common elements:

[2, 2, 3, 3, 9].mode() // [2, 3]
'aabbc'.mode // 'ab'

ascending, descending

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'

backwards

Reverse a String or iterable:

List<int> l = [1, 2, 3].backwards; // [3, 2, 1]
String s = 'hello'.backwards; // 'olleh'

indicesOf, indicesWhere

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

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) // 2

frequencies

Map of frequencies of each element:

[1, 2, 3].frequencies() // {1: 1, 2: 1, 3: 1}
'aabc'.frequencies() // {'a': 2, 'b': 1, 'c': 1}

before, after, beforeWhere, afterWhere

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]

startsWith

Dart already has this for Strings.

bool b = [1, 2, 3].startsWith([1, 2]); // true

nub

Remove 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'

intersperse

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]

any, every

(These already exist for iterables)

bool b = 'hello'.any((char) => char == 'h'); // true
bool b2 = 'hello'.every((char) => char == 'h'); // false

replace

Remove 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, addMissing

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.)

keepIndices, dropIndices

[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, subtractAll

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, tail(s), last, inits,

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']

splitAt

Split an iterable or string into two:

[5, 6, 7, 8].splitAt(2); // [[5, 6], [7, 8]]
'hello'.splitAt(2) // ['he', 'llo'] 

interleave

Combine by taking turns:

[1, 2, 3].interleave([4, 5, 6]); 
// [1, 4, 2, 5, 3, 6]

'abc'.interleave('123')
// 'a1b2c3'

riffleIn, riffleOut

Riffle shuffle: splits in half and interleaves them:

riffle
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, groupBy

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, chrs

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 codes

toStrings

Convert all elements to Strings:

List<String> l = [1, 2, 3].toStrings(); // ['1', '2', '3']

Methods for nested iterables

concat

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'

intercalate (in-TER-kuh-late)

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, zipWith

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]

Other methods for Strings

removeWhitespace

String s = '  hello \n world  '.removeWhitespace(); // 'helloworld'

shuffled

String s = 'hello world'.shuffled() // edwl hlloro

words, wordCount

words 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

is(Strictly)Upper, is(Strictly)Lower

'hello world'.isLower // true
'hello world'.isStrictlyLower // false (because of space)
'HELLO WORLD'.isUpper // true
'HELLO WORLD'.isStrictlyUpper // false (because of space)

Generating a list of numbers

range(List), inclusive(List)

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)

rangeString, inclusiveString

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'

Operators for strings and iterables

>, >=, <, <=, *

Compare elements in two iterables, starting at the beginning:

[1, 2, 3] > [1, 1, 3] // true

Compare 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'

Methods for sorting Maps

{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}

About

Dart extension methods for strings and lists, inspired by Haskell

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages