Repository: sanctuary-js/sanctuary
Pull request #256 (2016-09-18 16:32:07)

string, array: add S.splitOn and S.joinWith
``` haskell
splitOn  :: String -> String -> Array String
joinWith :: String -> Array String -> String
```

Related:
- [`split`](http://ramdajs.com/0.22.1/docs/#split), [`join`](http://ramdajs.com/0.22.1/docs/#join) (Ramda)
- [`split`](https://pursuit.purescript.org/packages/purescript-strings/1.1.0/docs/Data.String#v:split), [`joinWith`](https://pursuit.purescript.org/packages/purescript-strings/1.1.0/docs/Data.String#v:joinWith) (PureScript)

At some point I'd like to add a Sanctuary equivalent of [`R.unnest`](http://ramdajs.com/0.22.1/docs/#unnest), which in Haskell is named [`join`](http://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Monad.html#v:join). I'd like to keep `S.join` open for this. Also, `joinWith` is more suggestive of a binary function than `join` is, so I consider it a better name even though it's a little longer.


safareli: +1 on preserving `join`

Patch:
diff --git a/index.js b/index.js
index 112358a..e89f110 100644
--- a/index.js
+++ b/index.js
@@ -2473,6 +2473,26 @@
   }
   S.prepend = def('prepend', {}, [a, $.Array(a), $.Array(a)], prepend);
 
+  //# joinWith :: String -> Array String -> String
+  //.
+  //. Joins the strings of the second argument separated by the first argument.
+  //.
+  //. Properties:
+  //.
+  //.   - `forall s :: String, t :: String. S.joinWith(s, S.splitOn(s, t)) = t`
+  //.
+  //. See also [`splitOn`](#splitOn).
+  //.
+  //. ```javascript
+  //. > S.joinWith(':', ['foo', 'bar', 'baz'])
+  //. 'foo:bar:baz'
+  //. ```
+  function joinWith(separator, ss) {
+    return ss.join(separator);
+  }
+  S.joinWith =
+  def('joinWith', {}, [$.String, $.Array($.String), $.String], joinWith);
+
   //# find :: (a -> Boolean) -> Array a -> Maybe a
   //.
   //. Takes a predicate and an array and returns Just the leftmost element of
@@ -3305,6 +3325,23 @@
   }
   S.unlines = def('unlines', {}, [$.Array($.String), $.String], unlines);
 
+  //# splitOn :: String -> String -> Array String
+  //.
+  //. Returns the substrings of its second argument separated by occurrences
+  //. of its first argument.
+  //.
+  //. See also [`joinWith`](#joinWith).
+  //.
+  //. ```javascript
+  //. > S.splitOn('::', 'foo::bar::baz')
+  //. ['foo', 'bar', 'baz']
+  //. ```
+  function splitOn(separator, s) {
+    return s.split(separator);
+  }
+  S.splitOn =
+  def('splitOn', {}, [$.String, $.String, $.Array($.String)], splitOn);
+
   return S;
 
   /* eslint-enable indent */


Test patch:
diff --git a/test/joinWith.js b/test/joinWith.js
new file mode 100644
index 0000000..4002519
--- /dev/null
+++ b/test/joinWith.js
@@ -0,0 +1,54 @@
+'use strict';
+
+var throws = require('assert').throws;
+
+var S = require('..');
+
+var eq = require('./internal/eq');
+var errorEq = require('./internal/errorEq');
+
+
+describe('joinWith', function() {
+
+  it('is a binary function', function() {
+    eq(typeof S.joinWith, 'function');
+    eq(S.joinWith.length, 2);
+  });
+
+  it('type checks its arguments', function() {
+    throws(function() { S.joinWith(null); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'joinWith :: String -> Array String -> String\n' +
+                   '            ^^^^^^\n' +
+                   '              1\n' +
+                   '\n' +
+                   '1)  null :: Null\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘String’.\n'));
+
+    throws(function() { S.joinWith('', [1, 2, 3]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'joinWith :: String -> Array String -> String\n' +
+                   '                            ^^^^^^\n' +
+                   '                              1\n' +
+                   '\n' +
+                   '1)  1 :: Number, FiniteNumber, NonZeroFiniteNumber, Integer, ValidNumber\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘String’.\n'));
+  });
+
+  it('joins an array of strings with a separator', function() {
+    eq(S.joinWith('', ['a', 'b', 'c']), 'abc');
+    eq(S.joinWith(':', []), '');
+    eq(S.joinWith(':', ['']), '');
+    eq(S.joinWith(':', ['', '']), ':');
+    eq(S.joinWith(':', ['', 'foo', '']), ':foo:');
+    eq(S.joinWith(':', ['foo', 'bar', 'baz']), 'foo:bar:baz');
+    eq(S.joinWith('::', ['foo', 'bar', 'baz']), 'foo::bar::baz');
+  });
+
+});
diff --git a/test/splitOn.js b/test/splitOn.js
new file mode 100644
index 0000000..aaef11c
--- /dev/null
+++ b/test/splitOn.js
@@ -0,0 +1,66 @@
+'use strict';
+
+var throws = require('assert').throws;
+
+var jsc = require('jsverify');
+
+var S = require('..');
+
+var eq = require('./internal/eq');
+var errorEq = require('./internal/errorEq');
+
+
+describe('splitOn', function() {
+
+  it('is a binary function', function() {
+    eq(typeof S.splitOn, 'function');
+    eq(S.splitOn.length, 2);
+  });
+
+  it('type checks its arguments', function() {
+    throws(function() { S.splitOn(/x/); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'splitOn :: String -> String -> Array String\n' +
+                   '           ^^^^^^\n' +
+                   '             1\n' +
+                   '\n' +
+                   '1)  /x/ :: RegExp\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘String’.\n'));
+
+    throws(function() { S.splitOn('', null); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'splitOn :: String -> String -> Array String\n' +
+                   '                     ^^^^^^\n' +
+                   '                       1\n' +
+                   '\n' +
+                   '1)  null :: Null\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘String’.\n'));
+  });
+
+  it('splits a string at occurrences of a separator', function() {
+    eq(S.splitOn('', 'abc'), ['a', 'b', 'c']);
+    eq(S.splitOn(':', ''), ['']);
+    eq(S.splitOn(':', ':'), ['', '']);
+    eq(S.splitOn(':', ':foo:'), ['', 'foo', '']);
+    eq(S.splitOn(':', 'foo:bar:baz'), ['foo', 'bar', 'baz']);
+    eq(S.splitOn('::', 'foo::bar::baz'), ['foo', 'bar', 'baz']);
+  });
+
+  it('property: joinWith(s, splitOn(s, t)) = t', function() {
+    jsc.assert(jsc.forall(jsc.asciistring, function(t) {
+      var min = 0;
+      var max = t.length;
+      var i = jsc.random(min, max);
+      var j = jsc.random(min, max);
+      var s = t.slice(Math.min(i, j), Math.max(i, j));
+      return S.joinWith(s, S.splitOn(s, t)) === t;
+    }), {tests: 1000});
+  });
+
+});