Repository: plaid/sanctuary
Pull request #42 (2015-05-29 20:51:08)

Add Slice Function
As discussed in #39. This actually turned out to be more difficult than anticipated, I think I've managed to test all the edge cases.

davidchambers: Thanks! I'm away from home this weekend, and don't like to review code on my phone. I'll have a proper look on Monday. :)

svozza: Actually, probably for the best you're not looking at it now, just realised I completely forgot to add documentation!
svozza: Just thinking, do we want to do a check with `R.nil` as well, give that this sort of thing gives a result:

```javascript
R.slice(null, 2, [1, 2, 3, 4, 5])
// => [1, 2]

R.slice(void 0, 2, [1, 2, 3, 4, 5])
// => [1, 2]
```
davidchambers: I'd prefer to leave the behaviour of non-number index values unspecified.


Patch:
diff --git a/index.js b/index.js
index 38235ac..45250a5 100644
--- a/index.js
+++ b/index.js
@@ -864,6 +864,44 @@
 
   //. ### List
 
+  //# slice :: Number -> Number -> [a] -> Maybe [a]
+  //.
+  //. Returns Just a list containing the elements from the supplied list
+  //. from a beginning index (inclusive) to an end index (exclusive).
+  //. Returns Nothing unless the start interval is less than or equal to
+  //. the end interval, and the list contains both (half-open) intervals.
+  //. Accepts negative indices, which indicate an offset from the end of
+  //. the list.
+  //.
+  //. ```javascript
+  //. > S.slice(1, 1, ['a', 'b', 'c', 'd', 'e'])
+  //. Just([])
+  //.
+  //. > S.slice(1, 3, ['a', 'b', 'c', 'd', 'e'])
+  //. Just(["b", "c"])
+  //.
+  //. > S.slice(-2, -0, ['a', 'b', 'c', 'd', 'e'])
+  //. Just(["d", "e"])
+  //.
+  //. > S.slice(2, -0, ['a', 'b', 'c', 'd', 'e'])
+  //. Just(["c", "d", "e"])
+  //.
+  //. > S.slice(0, 2, [])
+  //. Nothing()
+  //.
+  //. > S.slice(1, 6, ['a', 'b', 'c', 'd', 'e'])
+  //. Nothing()
+  //. ```
+  var slice = S.slice = R.curry(function(start, end, xs) {
+    var len = xs.length;
+    var startIdx = R.eq(-0, start) ? len : start < 0 ? start + len : start;
+    var endIdx = R.eq(-0, end) ? len : end < 0 ? end + len : end;
+
+    return (Math.abs(start) <= len && Math.abs(end) <= len && startIdx <= endIdx) ?
+      Just(R.slice(startIdx, endIdx, xs)) :
+      Nothing();
+  });
+
   //# at :: Number -> [a] -> Maybe a
   //.
   //. Takes an index and a list and returns Just the element of the list at
@@ -881,9 +919,7 @@
   //. Just("d")
   //. ```
   var at = S.at = R.curry(function(n, xs) {
-    var len = xs.length;
-    var idx = n < 0 ? len + n : n;
-    return idx >= 0 && idx < len ? Just(xs[idx]) : Nothing();
+    return R.map(R.head, slice(n, n === -1 ? -0 : n + 1, xs));
   });
 
   //# head :: [a] -> Maybe a
@@ -927,7 +963,7 @@
   //. > S.tail([])
   //. Nothing()
   //. ```
-  S.tail = R.ifElse(R.isEmpty, Nothing, R.compose(Just, R.tail));
+  S.tail = S.slice(1, -0);
 
   //# init :: [a] -> Maybe [a]
   //.
@@ -942,7 +978,7 @@
   //. > S.init([])
   //. Nothing()
   //. ```
-  S.init = R.ifElse(R.isEmpty, Nothing, R.compose(Just, R.init));
+  S.init = S.slice(0, -1);
 
   //# find :: (a -> Boolean) -> [a] -> Maybe a
   //.


Test patch:
diff --git a/test/index.js b/test/index.js
index 90df157..3776178 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1107,6 +1107,7 @@ describe('list', function() {
     });
 
     it('returns a Nothing if index out of bounds', function() {
+      assert(S.at(-0, ['foo', 'bar', 'baz']).equals(S.Nothing()));
       assert(S.at(3, ['foo', 'bar', 'baz']).equals(S.Nothing()));
       assert(S.at(-4, ['foo', 'bar', 'baz']).equals(S.Nothing()));
     });
@@ -1117,6 +1118,58 @@ describe('list', function() {
 
   });
 
+  describe('slice', function() {
+
+    it('returns a Nothing with a positive end index greater than start index', function() {
+      assert(S.slice(6, 1, [1, 2, 3, 4, 5]).equals(S.Nothing()));
+    });
+
+    it('returns a Nothing with a positive end index greater than list length', function() {
+      assert(S.slice(1, 6, [1, 2, 3, 4, 5]).equals(S.Nothing()));
+    });
+
+    it('returns a Nothing with a negative end index greater than list length', function() {
+      assert(S.slice(1, -6, [1, 2, 3, 4, 5]).equals(S.Nothing()));
+    });
+
+    it('returns a Nothing with a negative start index greater than list length', function() {
+      assert(S.slice(-6, 1, [1, 2, 3, 4, 5]).equals(S.Nothing()));
+    });
+
+    it('returns a Just with an empty array when start index equals end index', function() {
+      eq(S.slice(1, 1, [1, 2, 3, 4, 5]).toString(), 'Just([])');
+      eq(S.slice(1, -4, [1, 2, 3, 4, 5]).toString(), 'Just([])');
+      eq(S.slice(-4, 1, [1, 2, 3, 4, 5]).toString(), 'Just([])');
+      eq(S.slice(-4, -4, [1, 2, 3, 4, 5]).toString(), 'Just([])');
+      eq(S.slice(0, 0, []).toString(), 'Just([])');
+      eq(S.slice(0, -0, []).toString(), 'Just([])');
+      eq(S.slice(-0, 0, []).toString(), 'Just([])');
+      eq(S.slice(-0, -0, []).toString(), 'Just([])');
+    });
+
+    it('returns a Just with a positive start and end index', function() {
+      eq(S.slice(1, 3, [1, 2, 3, 4, 5]).toString(), 'Just([2, 3])');
+    });
+
+    it('returns a Just with a negative start index', function() {
+      eq(S.slice(-3, 5, [1, 2, 3, 4, 5]).toString(), 'Just([3, 4, 5])');
+    });
+
+    it('returns a Just with a negative end index', function() {
+      eq(S.slice(1, -2, [1, 2, 3, 4, 5]).toString(), 'Just([2, 3])');
+    });
+
+    it('accepts -0 as the position half a step beyond the last index', function() {
+      eq(S.slice(-0, 5, [1, 2, 3, 4, 5]).toString(), 'Just([])');
+      eq(S.slice(2, -0, [1, 2, 3, 4, 5]).toString(), 'Just([3, 4, 5])');
+    });
+
+    it('returns a Just with the whole list', function() {
+      eq(S.slice(0, 5, [1, 2, 3, 4, 5]).toString(), 'Just([1, 2, 3, 4, 5])');
+    });
+
+  });
+
   describe('head', function() {
 
     it('is a unary function', function() {