Skip to content

Commit

Permalink
fix(lensPath) works with negative indexes (ramda#3479)
Browse files Browse the repository at this point in the history
 * assoc/assocPath set negative indexes starting from the end, matching the behavior of nth()
 * fixes dependent functions eg lensPath
  • Loading branch information
Harris-Miller authored Jul 16, 2024
1 parent 9f2bf8f commit c1bb88a
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 11 deletions.
6 changes: 6 additions & 0 deletions source/adjust.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
*/
Expand Down
3 changes: 3 additions & 0 deletions source/assoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
9 changes: 7 additions & 2 deletions source/assocPath.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,14 +27,19 @@ 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) {
return val;
}
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);
Expand Down
5 changes: 4 additions & 1 deletion source/internal/_assoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down
11 changes: 11 additions & 0 deletions source/internal/_prop.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions source/lensIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
10 changes: 2 additions & 8 deletions source/prop.js
Original file line number Diff line number Diff line change
@@ -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';


/**
Expand All @@ -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;
6 changes: 6 additions & 0 deletions source/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
22 changes: 22 additions & 0 deletions test/assoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
14 changes: 14 additions & 0 deletions test/assocPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
});
});
3 changes: 3 additions & 0 deletions test/lensPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit c1bb88a

Please sign in to comment.