-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Slice walking implementation #124
Changes from all commits
971ce3a
a7aaf27
a821b1e
124e1ef
8ebda31
a481a95
0313da6
f1116e9
23edb07
5aae427
ddf4c50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,14 @@ | ||
import { | ||
isArrayProp, | ||
getSliceBounds, | ||
isIndex, | ||
isSlice, | ||
} from './path.utils' | ||
|
||
import { | ||
isNil, | ||
length, | ||
} from 'util/lang' | ||
|
||
import { unsafeToPath } from './toPath' | ||
|
||
/** | ||
|
@@ -14,26 +22,20 @@ import { unsafeToPath } from './toPath' | |
* @since 0.4.0 | ||
*/ | ||
const copy = (value, asArray) => { | ||
if (value === undefined || value === null) { | ||
if (asArray) | ||
return [] | ||
if (isNil(value)) { | ||
if (asArray) return [] | ||
return {} | ||
} | ||
if (Array.isArray(value)) return [...value] | ||
return { ...value } | ||
} | ||
|
||
const callback = (obj, prop) => { | ||
if (obj === undefined || obj === null) return undefined | ||
return obj[prop] | ||
} | ||
|
||
/** | ||
* Operation to apply on a nested property of an object, to be called by {@link core.apply|apply}. | ||
* @memberof core | ||
* @callback operation | ||
* @param {*} obj The last nested object | ||
* @param {string|number|Array<number>} prop The prop of the last nested object | ||
* @param {string|number} prop The prop of the last nested object | ||
* @param {*} value The value of the prop | ||
* @returns {*} Result of the operation | ||
* @private | ||
|
@@ -52,12 +54,24 @@ const callback = (obj, prop) => { | |
* @since 0.4.0 | ||
*/ | ||
const apply = (obj, path, operation) => { | ||
const walkPath = (curObj, curPath) => { | ||
const walkPath = (curObj, curPath, doCopy = true) => { | ||
const [prop, ...pathRest] = curPath | ||
|
||
const value = callback(curObj, prop) | ||
if (isSlice(prop)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👏 |
||
const [start, end] = getSliceBounds(prop, length(curObj)) | ||
|
||
const newArr = copy(curObj, true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the second param is not clear. Usually the second param in a copy function is for deep not asArray There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as this is documented... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this is private code |
||
|
||
for (let i = start; i < end; i++) | ||
walkPath(newArr, [i, ...pathRest], false) | ||
|
||
return newArr | ||
} | ||
|
||
const value = isNil(curObj) ? undefined : curObj[prop] | ||
|
||
const newObj = copy(curObj, isArrayProp(prop)) | ||
let newObj = curObj | ||
if (doCopy) newObj = copy(curObj, isIndex(prop)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need it ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we are iterating on a slice, only one copy of the array is necessary, so this is to avoid several copies |
||
|
||
if (curPath.length === 1) { | ||
operation(newObj, prop, value) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* eslint-env jest */ | ||
import { apply } from './apply' | ||
import { immutaTest } from 'test.utils' | ||
|
||
describe('Apply', () => { | ||
|
||
const _inc = (v, i = 1) => { | ||
let r = Number(v) | ||
if (Number.isNaN(r)) r = 0 | ||
return r + i | ||
} | ||
|
||
const inc = (obj, path, ...args) => apply(obj, path, (obj, prop) => { obj[prop] = _inc(obj[prop], ...args) }) | ||
|
||
it('should inc in an array slice', () => { | ||
immutaTest( | ||
input => { | ||
const output = inc(input, 'nested.prop[:].val', 2) | ||
expect(output).toEqual({ | ||
nested: { | ||
prop: [ | ||
{ | ||
val: 6, | ||
other: {}, | ||
}, | ||
{ val: -6 }, | ||
{ val: 2 }, | ||
{ val: 2 }, | ||
], | ||
}, | ||
}) | ||
return output | ||
}, | ||
{ | ||
nested: { | ||
prop: [ | ||
{ | ||
val: 4, | ||
other: {}, | ||
}, | ||
{ val: -8 }, | ||
{ val: 'a' }, | ||
{}, | ||
], | ||
}, | ||
}, | ||
'nested.prop.0.val', | ||
'nested.prop.1.val', | ||
'nested.prop.2.val', | ||
'nested.prop.3.val', | ||
) | ||
|
||
immutaTest( | ||
input => { | ||
const output = inc(input, 'nested.prop[1:3].val') | ||
expect(output).toEqual({ | ||
nested: { | ||
prop: [{ val: 0 }, { | ||
val: 2, | ||
other: {}, | ||
}, { val: 3 }, { val: 3 }], | ||
}, | ||
other: {}, | ||
}) | ||
return output | ||
}, | ||
{ | ||
nested: { | ||
prop: [{ val: 0 }, { | ||
val: 1, | ||
other: {}, | ||
}, { val: 2 }, { val: 3 }], | ||
}, | ||
other: {}, | ||
}, | ||
'nested.prop.1.val', | ||
'nested.prop.2.val', | ||
) | ||
|
||
immutaTest( | ||
input => { | ||
const output = inc(input, 'nested.prop[-3:-1].val') | ||
expect(output).toEqual({ | ||
nested: { | ||
prop: [{ val: 0 }, { | ||
val: 2, | ||
other: {}, | ||
}, { val: 3 }, { val: 3 }], | ||
}, | ||
other: {}, | ||
}) | ||
return output | ||
}, | ||
{ | ||
nested: { | ||
prop: [{ val: 0 }, { | ||
val: 1, | ||
other: {}, | ||
}, { val: 2 }, { val: 3 }], | ||
}, | ||
other: {}, | ||
}, | ||
'nested.prop.1.val', | ||
'nested.prop.2.val', | ||
) | ||
}) | ||
|
||
immutaTest( | ||
input => { | ||
const output = inc(input, 'nested.prop[3:5].val', 6) | ||
expect(output).toEqual({ | ||
nested: { prop: [{ val: 0 }, { val: 1 }, undefined, { val: 6 }, { val: 6 }] }, | ||
other: {}, | ||
}) | ||
return output | ||
}, | ||
{ | ||
nested: { prop: [{ val: 0 }, { val: 1 }] }, | ||
other: {}, | ||
}, | ||
'nested.prop.2', | ||
'nested.prop.3.val', | ||
'nested.prop.4.val', | ||
) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,34 @@ | ||
import { | ||
isNaturalInteger, | ||
} from 'util/lang' | ||
|
||
const getSliceBound = (value, defaultValue, length) => { | ||
if (value === undefined) return defaultValue | ||
if (value < 0) return Math.max(length + value, 0) | ||
return value | ||
} | ||
|
||
/** | ||
* Tests whether <code>arg</code> is a valid index, that is a positive integer. | ||
* Get the actual bounds of a slice. | ||
* @param {Array<number>} bounds The bounds of the slice | ||
* @param {number} length The length of the actual array | ||
* @returns {Array<number>} The actual bounds of the slice | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const getSliceBounds = ([start, end], length) => ([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
getSliceBound(start, 0, length), | ||
getSliceBound(end, length, length), | ||
]) | ||
|
||
/** | ||
* This is an alias for {@link util/isNaturalInteger}. | ||
* @function | ||
* @param {*} arg The value to test | ||
* @return {boolean} True if <code>arg</code> is a valid index, false otherwise | ||
* @memberof core | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const isIndex = arg => Number.isSafeInteger(arg) && arg >= 0 | ||
const isIndex = isNaturalInteger | ||
|
||
/** | ||
* Tests whether <code>arg</code> is a valid slice index, that is an integer or <code>undefined</code>. | ||
|
@@ -35,19 +56,8 @@ const isSlice = arg => { | |
return isSliceIndex(arg[0]) && isSliceIndex(arg[1]) | ||
} | ||
|
||
/** | ||
* Tests whether <code>arg</code> is either an index or a slice. | ||
* @function | ||
* @param {*} arg The value to test | ||
* @return {boolean} True if <code>arg</code> is either an index or a slice, false otherwise | ||
* @memberof core | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const isArrayProp = arg => isIndex(arg) || isSlice(arg) | ||
|
||
export { | ||
isArrayProp, | ||
getSliceBounds, | ||
isIndex, | ||
isSlice, | ||
isSliceIndex, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,25 @@ | ||
/** | ||
* Tests whether <code>arg</code> is a natural integer. | ||
* @function | ||
* @param {*} arg The value to test | ||
* @return {boolean} True if <code>arg</code> is a natural integer, false otherwise | ||
* @memberof util | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const isNaturalInteger = arg => Number.isSafeInteger(arg) && arg >= 0 | ||
|
||
/** | ||
* Tests whether <code>arg</code> is a <code>undefined</code> or <code>null</code>. | ||
* @function | ||
* @param {*} arg The value to test | ||
* @return {boolean} True if <code>arg</code> is <code>undefined</code> or <code>null</code>, false otherwise | ||
* @memberof util | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const isNil = arg => arg === undefined || arg === null | ||
|
||
/** | ||
* Tests whether <code>arg</code> is a Symbol. | ||
* @param {*} arg The value to test | ||
|
@@ -9,6 +31,18 @@ | |
*/ | ||
const isSymbol = arg => typeof arg === 'symbol' | ||
|
||
/** | ||
* Returns the length of <code>arg</code>. | ||
* @param {*} arg The value of which length must be returned | ||
* @returns {number} The length of <code>arg</code> | ||
* @private | ||
* @since 0.4.0 | ||
*/ | ||
const length = arg => { | ||
if (isNil(arg) || !isNaturalInteger(arg.length)) return 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
return arg.length | ||
} | ||
|
||
/** | ||
* Converts <code>arg</code> to a string using string interpolation. | ||
* @param {*} arg The value to convert | ||
|
@@ -20,6 +54,9 @@ const isSymbol = arg => typeof arg === 'symbol' | |
const toString = arg => typeof arg === 'string' ? arg : `${arg}` | ||
|
||
export { | ||
isNaturalInteger, | ||
isNil, | ||
isSymbol, | ||
length, | ||
toString, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍