Repository: plaid/sanctuary
Pull request #87 (2015-09-18 02:26:36)

string: add S.words, S.unwords, S.lines, and S.unlines
Supersedes ramda/ramda#1382

See [Data.String][1] from the Haskell Prelude.


[1]: https://hackage.haskell.org/package/base-4.8.1.0/docs/Data-String.html#g:1


benperez: I see the utility of these methods but what's the rationale for having them live in Sanctuary?
davidchambers: > I see the utility of these methods but what's the rationale for having them live in Sanctuary?

@CrossEye made a good point in https://github.com/ramda/ramda/pull/1382#issuecomment-140272971:

> I simply don't want anyone to think -- ourselves included -- that we're trying to port over all the Haskell functions.

Making Haskell concepts and functions available to those programming in JavaScript is very much the goal of Sanctuary, as suggested by the first line of the README:

> Sanctuary is a small functional programming library inspired by Haskell and PureScript.

In the aforementioned Ramda issue, @paldepind noted that there exist two valid behaviours for a "join lines" function: it could map `['foo', 'bar', 'baz']` to `'foo\nbar\nbaz'` or to `'foo\nbar\nbaz\n'`. In the context of Ramda this is a legitimate conundrum. In the context of Sanctuary the answer is clear: name the function `unlines` and have it behave like its Haskell namesake.

These functions could one day be defined in Ramda as well, of course.

benperez: :church: 

Patch:
diff --git a/index.js b/index.js
index b0e68d8..00f0458 100644
--- a/index.js
+++ b/index.js
@@ -1643,6 +1643,65 @@
     return R.map(R.map(toMaybe), toMaybe(s.match(pattern)));
   });
 
+  //. ### String
+
+  //# words :: String -> [String]
+  //.
+  //. Takes a string and returns the list of words the string contains
+  //. (words are delimited by whitespace characters).
+  //.
+  //. See also [`unwords`](#unwords).
+  //.
+  //. ```javascript
+  //. > S.words(' foo bar baz ')
+  //. ["foo", "bar", "baz"]
+  //. ```
+  S.words =
+  def('words', [String], R.compose(R.reject(R.isEmpty), R.split(/\s+/)));
+
+  //# unwords :: [String] -> String
+  //.
+  //. Takes a list of words and returns the result of joining the words
+  //. with separating spaces.
+  //.
+  //. See also [`words`](#words).
+  //.
+  //. ```javascript
+  //. > S.unwords(['foo', 'bar', 'baz'])
+  //. "foo bar baz"
+  //. ```
+  S.unwords = def('unwords', [List], R.join(' '));
+
+  //# lines :: String -> [String]
+  //.
+  //. Takes a string and returns the list of lines the string contains
+  //. (lines are delimited by newlines: `'\n'` or `'\r\n'` or `'\r'`).
+  //. The resulting strings do not contain newlines.
+  //.
+  //. See also [`unlines`](#unlines).
+  //.
+  //. ```javascript
+  //. > S.lines('foo\nbar\nbaz\n')
+  //. ["foo", "bar", "baz"]
+  //. ```
+  S.lines =
+  def('lines', [String],
+      R.compose(R.match(/^(?=[\s\S]).*/gm), R.replace(/\r\n?/g, '\n')));
+
+  //# unlines :: [String] -> String
+  //.
+  //. Takes a list of lines and returns the result of joining the lines
+  //. after appending a terminating line feed (`'\n'`) to each.
+  //.
+  //. See also [`lines`](#lines).
+  //.
+  //. ```javascript
+  //. > S.unlines(['foo', 'bar', 'baz'])
+  //. "foo\nbar\nbaz\n"
+  //. ```
+  S.unlines =
+  def('unlines', [List], R.compose(R.join(''), R.map(R.concat(_, '\n'))));
+
 }.call(this));
 
 //. [Extend]:       https://github.com/fantasyland/fantasy-land#extend


Test patch:
diff --git a/test/index.js b/test/index.js
index 3273759..4975d9f 100644
--- a/test/index.js
+++ b/test/index.js
@@ -2494,3 +2494,108 @@ describe('regexp', function() {
   });
 
 });
+
+describe('string', function() {
+
+  describe('words', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.words, 'function');
+      eq(S.words.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.words(['foo']); },
+                    errorEq(TypeError,
+                            '‘words’ requires a value of type String ' +
+                            'as its first argument; received ["foo"]'));
+    });
+
+    it('splits a string into a list of words', function() {
+      eq(S.words(''), []);
+      eq(S.words(' '), []);
+      eq(S.words(' \t\r\n'), []);
+      eq(S.words('foo bar baz'), ['foo', 'bar', 'baz']);
+      eq(S.words(' foo bar baz '), ['foo', 'bar', 'baz']);
+      eq(S.words('\tfoo\r\n\tbar\r\n\tbaz\r\n'), ['foo', 'bar', 'baz']);
+    });
+
+  });
+
+  describe('unwords', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.unwords, 'function');
+      eq(S.unwords.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.unwords(null); },
+                    errorEq(TypeError,
+                            '‘unwords’ requires a value of type List ' +
+                            'as its first argument; received null'));
+    });
+
+    it('joins -- with separating spaces -- a list of words', function() {
+      eq(S.unwords([]), '');
+      eq(S.unwords(['']), '');
+      eq(S.unwords(['', '']), ' ');
+      eq(S.unwords([' ']), ' ');
+      eq(S.unwords([' ', ' ']), '   ');
+      eq(S.unwords(['foo', 'bar', 'baz']), 'foo bar baz');
+      eq(S.unwords([' foo ', ' bar ', ' baz ']), ' foo   bar   baz ');
+    });
+
+  });
+
+  describe('lines', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.lines, 'function');
+      eq(S.lines.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.lines(['foo']); },
+                    errorEq(TypeError,
+                            '‘lines’ requires a value of type String ' +
+                            'as its first argument; received ["foo"]'));
+    });
+
+    it('splits a string into a list of lines', function() {
+      eq(S.lines(''), []);
+      eq(S.lines('\n'), ['']);
+      eq(S.lines('\n\n'), ['', '']);
+      eq(S.lines('foo\nbar\nbaz'), ['foo', 'bar', 'baz']);
+      eq(S.lines('foo\nbar\nbaz\n'), ['foo', 'bar', 'baz']);
+      eq(S.lines('\tfoo\r\n\tbar\r\n\tbaz\r\n'), ['\tfoo', '\tbar', '\tbaz']);
+    });
+
+  });
+
+  describe('unlines', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.unlines, 'function');
+      eq(S.unlines.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.unlines(null); },
+                    errorEq(TypeError,
+                            '‘unlines’ requires a value of type List ' +
+                            'as its first argument; received null'));
+    });
+
+    it('joins a list of lines after appending "\n" to each', function() {
+      eq(S.unlines([]), '');
+      eq(S.unlines(['']), '\n');
+      eq(S.unlines(['', '']), '\n\n');
+      eq(S.unlines(['\n']), '\n\n');
+      eq(S.unlines(['\n', '\n']), '\n\n\n\n');
+      eq(S.unlines(['foo', 'bar', 'baz']), 'foo\nbar\nbaz\n');
+    });
+
+  });
+
+});