From c1bb88aea3b132cd69819ad29d1f54b9ec62ad8b Mon Sep 17 00:00:00 2001 From: Harris Miller Date: Tue, 16 Jul 2024 10:28:02 -0600 Subject: [PATCH] fix(lensPath) works with negative indexes (#3479) * assoc/assocPath set negative indexes starting from the end, matching the behavior of nth() * fixes dependent functions eg lensPath --- source/adjust.js | 6 ++++++ source/assoc.js | 3 +++ source/assocPath.js | 9 +++++++-- source/internal/_assoc.js | 5 ++++- source/internal/_prop.js | 11 +++++++++++ source/lensIndex.js | 6 ++++++ source/prop.js | 10 ++-------- source/update.js | 6 ++++++ test/assoc.js | 22 ++++++++++++++++++++++ test/assocPath.js | 14 ++++++++++++++ test/lensPath.js | 3 +++ 11 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 source/internal/_prop.js diff --git a/source/adjust.js b/source/adjust.js index f59385e8c2..fa091eed06 100644 --- a/source/adjust.js +++ b/source/adjust.js @@ -7,6 +7,8 @@ import _curry3 from './internal/_curry3.js'; * new copy of the array with the element at the given index replaced with the * result of the function application. * + * When `idx < -list.length || idx >= list.length`, the original list is returned. + * * @func * @memberOf R * @since v0.14.0 @@ -24,6 +26,10 @@ import _curry3 from './internal/_curry3.js'; * * R.adjust(1, R.toUpper, ['a', 'b', 'c', 'd']); //=> ['a', 'B', 'c', 'd'] * R.adjust(-1, R.toUpper, ['a', 'b', 'c', 'd']); //=> ['a', 'b', 'c', 'D'] + * + * // out-of-range returns original list + * R.adjust(4, R.toUpper, ['a', 'b', 'c', 'd']); //=> ['a', 'b', 'c', 'd'] + * R.adjust(-5, R.toUpper, ['a', 'b', 'c', 'd']); //=> ['a', 'b', 'c', 'd'] * @symb R.adjust(-1, f, [a, b]) = [a, f(b)] * @symb R.adjust(0, f, [a, b]) = [f(a), b] */ diff --git a/source/assoc.js b/source/assoc.js index f7267eb27f..d445c84925 100644 --- a/source/assoc.js +++ b/source/assoc.js @@ -21,6 +21,9 @@ import assocPath from './assocPath.js'; * @example * * R.assoc('c', 3, {a: 1, b: 2}); //=> {a: 1, b: 2, c: 3} + * + * R.assoc(4, 3, [1, 2]); //=> [1, 2, undefined, undefined, 3] + * R.assoc(-1, 3, [1, 2]); //=> [1, 3] */ var assoc = _curry3(function assoc(prop, val, obj) { return assocPath([prop], val, obj); }); export default assoc; diff --git a/source/assocPath.js b/source/assocPath.js index b32eb9035f..7196606869 100644 --- a/source/assocPath.js +++ b/source/assocPath.js @@ -1,8 +1,8 @@ import _curry3 from './internal/_curry3.js'; -import _has from './internal/_has.js'; import _isInteger from './internal/_isInteger.js'; import _assoc from './internal/_assoc.js'; import isNil from './isNil.js'; +import _prop from './internal/_prop.js'; /** * Makes a shallow clone of an object, setting or overriding the nodes required @@ -27,6 +27,8 @@ import isNil from './isNil.js'; * * // Any missing or non-object keys in path will be overridden * R.assocPath(['a', 'b', 'c'], 42, {a: 5}); //=> {a: {b: {c: 42}}} + * R.assocPath(['a', 1, 'c'], 42, {a: []}); // => {a: [undefined, {c: 42}]} + * R.assocPath(['a', -1], 42, {a: [1, 2]}); // => {a: [1, 42]} */ var assocPath = _curry3(function assocPath(path, val, obj) { if (path.length === 0) { @@ -34,7 +36,10 @@ var assocPath = _curry3(function assocPath(path, val, obj) { } var idx = path[0]; if (path.length > 1) { - var nextObj = (!isNil(obj) && _has(idx, obj) && typeof obj[idx] === 'object') ? obj[idx] : _isInteger(path[1]) ? [] : {}; + var nextObj = _prop(idx, obj); + if (isNil(nextObj) || typeof nextObj !== 'object') { + nextObj = _isInteger(path[1]) ? [] : {}; + } val = assocPath(Array.prototype.slice.call(path, 1), val, nextObj); } return _assoc(idx, val, obj); diff --git a/source/internal/_assoc.js b/source/internal/_assoc.js index a20d39e058..d4730d103a 100644 --- a/source/internal/_assoc.js +++ b/source/internal/_assoc.js @@ -15,9 +15,12 @@ import _isInteger from './_isInteger.js'; */ export default function _assoc(prop, val, obj) { if (_isInteger(prop) && _isArray(obj)) { + var _idx = prop < 0 ? obj.length + prop : prop; + var arr = [].concat(obj); - arr[prop] = val; + arr[_idx] = val; return arr; + } var result = {}; diff --git a/source/internal/_prop.js b/source/internal/_prop.js new file mode 100644 index 0000000000..ceb83b2636 --- /dev/null +++ b/source/internal/_prop.js @@ -0,0 +1,11 @@ +import _isInteger from './_isInteger.js'; +import _nth from './_nth.js'; + +function _prop(p, obj) { + if (obj == null) { + return; + } + return _isInteger(p) ? _nth(p, obj) : obj[p]; +} + +export default _prop; diff --git a/source/lensIndex.js b/source/lensIndex.js index 4cdc573563..159996204a 100644 --- a/source/lensIndex.js +++ b/source/lensIndex.js @@ -7,6 +7,8 @@ import update from './update.js'; /** * Returns a lens whose focus is the specified index. * + * When `idx < -list.length || idx >= list.length`, `R.set` or `R.over`, the original list is returned. + * * @func * @memberOf R * @since v0.14.0 @@ -23,6 +25,10 @@ import update from './update.js'; * R.view(headLens, ['a', 'b', 'c']); //=> 'a' * R.set(headLens, 'x', ['a', 'b', 'c']); //=> ['x', 'b', 'c'] * R.over(headLens, R.toUpper, ['a', 'b', 'c']); //=> ['A', 'b', 'c'] + * + * // out-of-range returns original list + * R.set(R.lensIndex(3), 'x', ['a', 'b', 'c']); //=> ['a', 'b', 'c'] + * R.over(R.lensIndex(-4), R.toUpper, ['a', 'b', 'c']); //=> ['a', 'b', 'c'] */ var lensIndex = _curry1(function lensIndex(n) { return lens( diff --git a/source/prop.js b/source/prop.js index e1be288c95..f562c5ed8d 100644 --- a/source/prop.js +++ b/source/prop.js @@ -1,6 +1,5 @@ import _curry2 from './internal/_curry2.js'; -import _isInteger from './internal/_isInteger.js'; -import _nth from './internal/_nth.js'; +import _prop from './internal/_prop.js'; /** @@ -25,10 +24,5 @@ import _nth from './internal/_nth.js'; * R.compose(R.inc, R.prop('x'))({ x: 3 }) //=> 4 */ -var prop = _curry2(function prop(p, obj) { - if (obj == null) { - return; - } - return _isInteger(p) ? _nth(p, obj) : obj[p]; -}); +var prop = _curry2(_prop); export default prop; diff --git a/source/update.js b/source/update.js index 68d1b2f275..d85ab39b2b 100644 --- a/source/update.js +++ b/source/update.js @@ -7,6 +7,8 @@ import always from './always.js'; * Returns a new copy of the array with the element at the provided index * replaced with the given value. * + * When `idx < -list.length || idx >= list.length`, the original list is returned. + * * @func * @memberOf R * @since v0.14.0 @@ -21,6 +23,10 @@ import always from './always.js'; * * R.update(1, '_', ['a', 'b', 'c']); //=> ['a', '_', 'c'] * R.update(-1, '_', ['a', 'b', 'c']); //=> ['a', 'b', '_'] + * + * // out-of-range returns original list + * R.update(3, '_', ['a', 'b', 'c']); //=> ['a', 'b', 'c'] + * R.update(-4, '_', ['a', 'b', 'c']); //=> ['a', 'b', 'c'] * @symb R.update(-1, a, [b, c]) = [b, a] * @symb R.update(0, a, [b, c]) = [a, c] * @symb R.update(1, a, [b, c]) = [b, a] diff --git a/test/assoc.js b/test/assoc.js index aee925d746..2f7ba477b7 100644 --- a/test/assoc.js +++ b/test/assoc.js @@ -49,4 +49,26 @@ describe('assoc', function() { assert.strictEqual(ary2[5], newValue); }); + it('handles negative indexes from end of array', function() { + var newValue = 8; + var ary1 = [1, 2]; + var ary2 = R.assoc(-2, 8, ary1); + eq(ary2, [8, 2]); + // Note: reference equality below! + assert.strictEqual(ary2[0], newValue); + assert.strictEqual(ary2[1], ary1[1]); + }); + + it('sets garbage key when negative indexes wraps to < 0', function() { + var newValue = 8; + var ary1 = [1, 2]; + var ary2 = R.assoc(-3, 8, ary1); + var expected = [1, 2]; + expected[-1] = 8; + eq(ary2, expected); + // Note: reference equality below! + assert.strictEqual(ary2[-1], newValue); + assert.strictEqual(ary2[0], ary1[0]); + assert.strictEqual(ary2[1], ary1[1]); + }); }); diff --git a/test/assocPath.js b/test/assocPath.js index 425d5056cf..b2f30e93d7 100644 --- a/test/assocPath.js +++ b/test/assocPath.js @@ -45,4 +45,18 @@ describe('assocPath', function() { eq(R.assocPath(['foo', 'bar', 'baz'], 42, {foo: null}), {foo: {bar: {baz: 42}}}); }); + it('sets in indexes regardless of length', function() { + eq(R.assocPath(['foo', 1, 0], 42, {foo : []}), {foo: [undefined, [42]]}); + }); + + it('handles negative indexes from end of array', function() { + eq(R.assocPath(['foo', -1], 42, {foo : [1, 2, 3]}), {foo: [1, 2, 42]}); + eq(R.assocPath(['foo', -1, 'X'], 42, {foo : [{a: 0}, {b: 0}]}), {foo: [{a: 0}, {b: 0, X: 42}]}); + }); + + it('sets garbage key when negative indexes wraps to < 0', function() { + var expected = [1, 2, 3]; + expected[-1] = 42; + eq(R.assocPath(['foo', -4], 42, {foo : [1, 2, 3]}), {foo: expected}); + }); }); diff --git a/test/lensPath.js b/test/lensPath.js index 52cb3f3a65..ad31aa94cb 100644 --- a/test/lensPath.js +++ b/test/lensPath.js @@ -29,6 +29,9 @@ describe('lensPath', function() { eq(R.set(R.lensPath(['X']), 0, testObj), {a: [{b: 1}, {b: 2}], d: 3, X: 0}); eq(R.set(R.lensPath(['a', 0, 'X']), 0, testObj), {a: [{b: 1, X: 0}, {b: 2}], d: 3}); }); + it('treats negative index from the end of the array', function() { + eq(R.set(R.lensPath(['a', -1, 'X']), 0, testObj), {a: [{b: 1}, {b: 2, X: 0}], d: 3}); + }); }); describe('over', function() { it('applies function to the value of the specified object property', function() {