Skip to content

Commit 0bc0ca0

Browse files
committed
feat: group(), multiline() and takeUntil()
Tests for group() and takeUntil() will be added with the ferrum.doctest introduction.
1 parent 01ee6d4 commit 0bc0ca0

File tree

4 files changed

+191
-3
lines changed

4 files changed

+191
-3
lines changed

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ module.exports = {
1919
...require('./trait'),
2020
...require('./stdtraits'),
2121
...require('./sequence'),
22+
...require('./string'),
2223
};

src/sequence.js

+75-3
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ const assert = require('assert');
1919
const { inspect } = require('util');
2020
const { curry, pipe } = require('./functional');
2121
const {
22-
plus, or, mul, and,
22+
plus, or, mul, and, is,
2323
} = require('./op');
2424
const { type } = require('./typesafe');
2525
const { Trait } = require('./trait');
2626
const {
27-
size, Size, pairs, eq, empty, _typedArrays,
27+
size, Size, pairs, eq, empty, _typedArrays, setdefault,
2828
} = require('./stdtraits');
2929

3030
// ITERATOR GENERATORS ///////////////////////////////////////
@@ -1249,6 +1249,18 @@ const takeWhile = curry('takeWhile', function* takeWhile(seq, fn) {
12491249
}
12501250
});
12511251

1252+
/**
1253+
* Cut off the sequence at the first point where the given condition is met.
1254+
*
1255+
* `list(takeUntil([1,2,3,4,5,6...], x => x > 4))` yields `[1,2,3,4]`
1256+
*
1257+
* @function
1258+
* @param {Sequence} seq Any sequence for which iter() is defined
1259+
* @param {Function} fn The predicate function
1260+
* @returns {Iterator}
1261+
*/
1262+
const takeUntil = curry('takeUntil', (seq, fn) => takeWhile(seq, (v) => !fn(v)));
1263+
12521264
/**
12531265
* Cut of the sequence at the point where the given value is
12541266
* first encountered.
@@ -1257,7 +1269,7 @@ const takeWhile = curry('takeWhile', function* takeWhile(seq, fn) {
12571269
* @param {Sequence} seq Any sequence for which iter() is defined
12581270
* @returns {Iterator}
12591271
*/
1260-
const takeUntilVal = curry('takeUntilVal', (seq, val) => takeWhile(seq, (x) => x !== val));
1272+
const takeUntilVal = curry('takeUntilVal', (seq, val) => takeUntil(seq, is(val)));
12611273

12621274
/**
12631275
* Cut of the given sequence at the first undefined or null value.
@@ -1659,6 +1671,64 @@ const chunkifyWithFallback = curry('chunkifyWithFallback', (seq, len, fallback)
16591671
}),
16601672
));
16611673

1674+
/**
1675+
* Group the elements of the user defined sequence using a custom container.
1676+
*
1677+
* This will:
1678+
*
1679+
* - Calculate the key for every element in the given sequence by
1680+
* applying the key function
1681+
* - Create a bucket (array) for every key calculated
1682+
* - Insert each element into the bucket associated with
1683+
* it's calculated key in order
1684+
*
1685+
* ```js,test
1686+
* const { group, assertEquals } = require('ferrum');
1687+
*
1688+
* const seq = [
1689+
* { foo: 42, bar: 22 },
1690+
* { foo: 13, bar: 22 },
1691+
* { foo: 42, bar: 99 },
1692+
* ];
1693+
*
1694+
* // Group by `foo`
1695+
* assertEquals(
1696+
* group(seq, ({foo}) => foo),
1697+
* new Map([
1698+
* [42, [
1699+
* { foo: 42, bar: 22 }, // Note that the order in here is well defined
1700+
* { foo: 42, bar: 99 }]],
1701+
* [13, [
1702+
* { foo: 13, bar: 22 }]]
1703+
* ])
1704+
* );
1705+
*
1706+
* // Group by `bar`
1707+
* assertEquals(
1708+
* group(seq, ({bar}) => bar),
1709+
* new Map([
1710+
* [22, [
1711+
* { foo: 42, bar: 22 },
1712+
* { foo: 13, bar: 22 }]],
1713+
* [42, [
1714+
* { foo: 42, bar: 99 }]]
1715+
* ])
1716+
* );
1717+
* ```
1718+
*
1719+
* @function
1720+
* @sourcecode
1721+
* @param {Sequence} seq
1722+
* @param {Function} keyfn
1723+
* @returns {Map} The es6 map containing the keys.
1724+
*/
1725+
const group = curry('group', (seq, keyfn) => {
1726+
const cont = new Map();
1727+
each(seq, (elm) => setdefault(cont, keyfn(elm), []).push(elm));
1728+
return cont;
1729+
});
1730+
1731+
16621732
/**
16631733
* Calculate the cartesian product of the given sequences.
16641734
*
@@ -1831,6 +1901,7 @@ module.exports = {
18311901
take,
18321902
takeWithFallback,
18331903
takeWhile,
1904+
takeUntil,
18341905
takeUntilVal,
18351906
takeDef,
18361907
flat,
@@ -1851,6 +1922,7 @@ module.exports = {
18511922
chunkifyShort,
18521923
chunkify,
18531924
chunkifyWithFallback,
1925+
group,
18541926
cartesian,
18551927
cartesian2,
18561928
mod,

src/string.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2019 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
const { pipe } = require('./functional');
14+
const { size } = require('./stdtraits');
15+
const {
16+
filter, map, count, foldl, join,
17+
} = require('./sequence');
18+
19+
/**
20+
* Helpers for working with strings.
21+
*/
22+
23+
/**
24+
* This is a helper for declaring multiline strings.
25+
*
26+
* ```
27+
* const s = multiline(`
28+
* Foo
29+
* Bar
30+
* Baz
31+
*
32+
* Hello
33+
*
34+
* Bang
35+
* `);
36+
* ```
37+
*
38+
* The function basically just takes a string and then
39+
* strips the first & last lines if they are empty.
40+
*
41+
* In order to remove indentation, we determine the common
42+
* whitespace prefix length (number of space 0x20 characters
43+
* at the start of the line). This prefix is simply removed
44+
* from each line...
45+
*/
46+
const multiline = (str) => {
47+
// Discard the leading & trailing line
48+
const lines = str.split('\n');
49+
50+
// Strip the first and the last line
51+
if (lines[0].match(/^\s*$/)) {
52+
lines.shift();
53+
}
54+
if (size(lines) > 0 && lines[size(lines) - 1].match(/^\s*$/)) {
55+
lines.pop();
56+
}
57+
58+
// Find the prefix length
59+
const prefixLen = pipe(
60+
lines,
61+
filter((l) => !l.match(/^\s*$/)), // Disregarding empty lines
62+
map((l) => l.match(/^ */)[0]), // Extract prefixes
63+
map(count), // calculate length
64+
foldl(Infinity, (a, b) => Math.min(a, b)),
65+
); // minimum
66+
67+
return pipe(
68+
lines,
69+
map((l) => l.slice(prefixLen)), // discard prefixes
70+
join('\n'),
71+
);
72+
};
73+
74+
module.exports = { multiline };

test/string.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2019 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/* eslint-env mocha */
14+
15+
const assert = require('assert');
16+
const { multiline } = require('../src/index');
17+
18+
describe('String tests', () => {
19+
it('multiline()', () => {
20+
const ck = (ref, str) => assert.strictEqual(multiline(str), ref);
21+
22+
ck('', '');
23+
ck('Hello', 'Hello');
24+
ck('Hello', `
25+
Hello`);
26+
ck('Hello', `
27+
Hello`);
28+
ck('Hello\nWorld', `
29+
Hello
30+
World`);
31+
ck('Hello\nWorld', `
32+
Hello
33+
World
34+
`);
35+
ck('Hello\n Foo\nWorld', `
36+
Hello
37+
Foo
38+
World
39+
`);
40+
});
41+
});

0 commit comments

Comments
 (0)