Repository: calmm-js/partial.lenses (js)

Issue:
`L.slice` -lens
`L.slice(beginIndex, endIndex)` for lensing array-like objects with the signature `L.slice: Maybe Index -> Maybe Index -> PLens [a] [a]` should be straightforward to implement, but currently hasn't been.

Pull request description:
WIP: slice, closes #45

Patch:
diff --git a/README.md b/README.md
index d6beefe..333e311 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,7 @@ parts.  [Try Lenses!](https://calmm-js.github.io/partial.lenses/)
       * [`L.find((value, index) => testable)`](#L-find "L.find: ((a, Index) -> Boolean) -> PLens [a] a")
       * [`L.findWith(...lenses)`](#L-findWith "L.findWith: (PLens s s1, ...PLens sN a) -> PLens [s] a")
       * [`L.index(integer)`](#L-index "L.index: Integer -> PLens [a] a")
+      * [`L.slice(maybeBegin, maybeEnd)`](#L-slice "L.slice: Maybe Integer -> Maybe Integer -> PLens [a] [a]")
     * [Lensing objects](#lensing-objects)
       * [`L.prop(propName)`](#L-prop "L.prop: (p: a) -> PLens {p: a, ...ps} a")
       * [`L.props(...propNames)`](#L-props "L.props: (p1: a1, ...ps) -> PLens {p1: a1, ...ps, ...o} {p1: a1, ...ps}")
@@ -1453,6 +1454,28 @@ the handling of `undefined` within partial lenses, this is often not a problem,
 but sometimes you need the "default" value both ways.  In that case you can
 use [`L.define`](#L-define).
 
+##### <a name="L-slice"></a> [≡](#contents) [`L.slice(maybeBegin, maybeEnd)`](#L-slice "L.slice: Maybe Integer -> Maybe Integer -> PLens [a] [a]")
+
+`L.slice` focuses on a specified range of elements of
+an [array-like](#array-like) object.  The range is determined like with the
+standard
+[`slice`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) method
+of arrays, basically
+- non-negative values are relative to the beginning of the array-like object,
+- negative values are relative to the end of the array-like object, and
+- `undefined` gives the defaults: 0 for the begin and length for the end.
+
+For example:
+
+```js
+L.get(L.slice(1, -1), [1,2,3,4])
+// [ 2, 3 ]
+```
+```js
+L.set(L.slice(-2, undefined), [0], [1,2,3,4])
+// [ 1, 2, 0 ]
+```
+
 #### Lensing objects
 
 ##### <a name="L-prop"></a> [≡](#contents) [`L.prop(propName)`](#L-prop "L.prop: (p: a) -> PLens {p: a, ...ps} a")
diff --git a/bench/bench.js b/bench/bench.js
index b6fdf4e..33c3c97 100644
--- a/bench/bench.js
+++ b/bench/bench.js
@@ -109,6 +109,15 @@ R.forEach(bs => {
   s.run()
 }, [
   [
+    `L.get(L.slice(100, -100), xs10000)`,
+    `R.slice(100, -100, xs10000)`,
+  ], [
+    `L.get(L.slice(1, -1), xs)`,
+    `R.slice(1, -1, xs)`,
+  ], [
+    `L.get(L.slice(10, -10), xs100)`,
+    `R.slice(10, -10, xs100)`,
+  ], [
     `L.foldr(add, 0, L.elems, xs100)`,
     `O.Fold.foldrOf(O.Traversal.traversed, addC, 0, xs100)`,
     `R.reduceRight(add, 0, xs100)`,
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index c017d1d..a8cf01c 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -17,6 +17,9 @@ import {
 
 //
 
+const sliceIndex = (m, l, d, i) =>
+  void 0 === i ? d : Math.min(Math.max(m, i < 0 ? l + i : i), l)
+
 function pair(x0, x1) {return [x0, x1]}
 
 const flip = bop => (x, y) => bop(y, x)
@@ -655,6 +658,27 @@ export const index = process.env.NODE_ENV === "production" ? id : x => {
   return x
 }
 
+export const slice = curry((begin, end) => (F, xsi2yF, xs, i) => {
+  const seems = seemsArrayLike(xs),
+        xsN = seems && xs.length,
+        b = sliceIndex(0, xsN, 0, begin),
+        e = sliceIndex(b, xsN, xsN, end)
+  return (0,F.map)(
+    zs => {
+      const zsN = zs ? zs.length : 0, bPzsN = b + zsN, n = xsN - e + bPzsN
+      return n
+        ? copyToFrom(copyToFrom(copyToFrom(Array(n), 0, xs, 0, b),
+                                b,
+                                zs, 0, zsN),
+                     bPzsN,
+                     xs, e, xsN)
+        : undefined
+    },
+    xsi2yF(seems ? copyToFrom(Array(Math.max(0, e - b)), 0, xs, b, e) :
+           undefined,
+           i))
+})
+
 // Lensing objects
 
 export const prop = process.env.NODE_ENV === "production" ? id : x => {


Test patch:
diff --git a/test/core.js b/test/core.js
index 893c739..aacc91b 100644
--- a/test/core.js
+++ b/test/core.js
@@ -3,6 +3,8 @@ import * as R from "ramda"
 
 //
 
+const clamp = (min, max, x) => Math.max(min, Math.min(x, max))
+
 const isNat = x => x === (x >> 0) && 0 <= x
 
 const seemsArrayLike = x =>
@@ -112,6 +114,23 @@ export const findWith = (...ls) => {
   return [find(x => isDefined(get(lls, x))), lls]
 }
 export const index = L.index
+export const slice = R.curry((b, e) => lens(
+  xs => seemsArrayLike(xs) ? fromArrayLike(xs).slice(b, e) : undefined,
+  (ys, xs) => {
+    xs = seemsArrayLike(xs) ? fromArrayLike(xs) : []
+    const i = clamp(0, xs.length,
+                    undefined === b ? 0 :
+                    b < 0 ? xs.length + b :
+                    b)
+    const zs = [].concat(xs.slice(0, i),
+                         fromArrayLike(ys || ""),
+                         xs.slice(clamp(i, xs.length,
+                                        undefined === e ? xs.length :
+                                        e < 0 ? xs.length + e :
+                                        e)))
+    return zs.length ? zs : undefined
+  }
+))
 
 export const prop = L.prop
 export const props = (...ps) => pick(R.zipObj(ps, ps))
diff --git a/test/tests.js b/test/tests.js
index 7b17394..420bfda 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -321,6 +321,23 @@ describe("L.filter", () => {
   testEq('L.remove(L.filter(c => "a" <= c), "JavaScript")', ["J", "S"])
 })
 
+describe("L.slice", () => {
+  testEq(`L.get(L.slice(undefined, undefined), undefined)`, undefined)
+  testEq(`L.get(L.slice(undefined, undefined), 45)`, undefined)
+  testEq(`L.get(L.slice(undefined, undefined), [])`, [])
+  testEq(`L.get(L.slice(undefined, undefined), "")`, [])
+  testEq(`L.set(L.slice(undefined, undefined), [], [101])`, undefined)
+  testEq(`L.set(L.slice(undefined, undefined), undefined, [101])`, undefined)
+  testEq(`L.get(L.slice(4, 1), "abcde")`, [])
+  testEq(`L.set([L.rewrite(R.join("")), L.slice(4, 1)], "xyz", "abcde")`,
+         "abcdxyze")
+  testEq(`L.set(L.slice(undefined, undefined), "abba", 45)`, ["a","b","b","a"])
+  testEq(`L.set([L.rewrite(R.join("")), L.slice(-1, -1)], "world", "Hello, !")`,
+         "Hello, world!")
+  testEq(`L.modify([L.slice(1,-1), L.elems], R.negate, [1,-2,-3,4])`, [1,2,3,4])
+  testEq(`L.modify([L.slice(-3,3), L.elems], R.negate, [1,-2,-3,4])`, [1,2,3,4])
+})
+
 describe("L.append", () => {
   testEq('L.remove(L.append, 45)', undefined)
   testEq('L.remove([L.rewrite(R.join("")), L.append], "anything")', "anything")
diff --git a/test/types.js b/test/types.js
index ee163f0..0dd281a 100644
--- a/test/types.js
+++ b/test/types.js
@@ -3,6 +3,7 @@ import * as T from "./type"
 const T_maybeData = T.any
 const T_data = T.def
 const T_index = T.or(T.nonNegative, T.string, T.undef)
+const T_sliceIndex = T.or(T.integer, T.undef)
 
 const T_functor = T.object({
   map: T.fn([T.fn([T.any], T.any), T.any], T.any)
@@ -116,6 +117,7 @@ export const filter = T.fn([T.fn([T_data, T_index], T.any)], T_lens)
 export const find = T.fn([T.fn([T_data, T_index], T.any)], T_lens)
 export const findWith = T.fnVar(T_lens, T_lens)
 export const index = T.fn([T.nonNegative], T_lens)
+export const slice = T.fn([T_sliceIndex, T_sliceIndex], T_lens)
 
 export const prop = T.fn([T.string], T_lens)
 export const props = T.fnVar(T.string, T_lens)