Skip to content
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

Merged
merged 11 commits into from
Nov 13, 2017
6 changes: 5 additions & 1 deletion src/array/convertArrayMethod.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { convert } from 'core/convert'

import {
isNil,
} from 'util/lang'

const copyArray = array => {
if (array === undefined || array === null) return []
if (isNil(array)) return []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (Array.isArray(array)) return [...array]
return [array]
}
Expand Down
40 changes: 27 additions & 13 deletions src/core/apply.js
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'

/**
Expand All @@ -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
Expand All @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

const [start, end] = getSliceBounds(prop, length(curObj))

const newArr = copy(curObj, true)
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as this is documented...

Copy link
Member Author

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need it ?

Copy link
Member Author

@nlepage nlepage Nov 13, 2017

Choose a reason for hiding this comment

The 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)
Expand Down
125 changes: 125 additions & 0 deletions src/core/apply.spec.js
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',
)
})
42 changes: 26 additions & 16 deletions src/core/path.utils.js
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) => ([
Copy link
Contributor

Choose a reason for hiding this comment

The 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>.
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 9 additions & 22 deletions src/core/path.utils.spec.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
/* eslint-env jest */
import {
isIndex,
getSliceBounds,
isSlice,
isSliceIndex,
} from './path.utils'

describe('Path Utils', () => {
describe('IsIndex', () => {
it('should return true for any non negative integer', () => {
expect(isIndex(0)).toBe(true)
expect(isIndex(1)).toBe(true)
expect(isIndex(6)).toBe(true)
expect(isIndex(100000000000)).toBe(true)
})

it('should return false for any negative integer', () => {
expect(isIndex(-1)).toBe(false)
expect(isIndex(-6)).toBe(false)
expect(isIndex(-100000000000)).toBe(false)
})
describe('GetSliceBounds', () => {
it('should return actual slice bounds', () => {
expect(getSliceBounds([undefined, undefined], 0)).toEqual([0, 0])
expect(getSliceBounds([-2, -1], 0)).toEqual([0, 0])
expect(getSliceBounds([1, 2], 0)).toEqual([1, 2])

it('should return false for any non integer', () => {
expect(isIndex(undefined)).toBe(false)
expect(isIndex(null)).toBe(false)
expect(isIndex(true)).toBe(false)
expect(isIndex({})).toBe(false)
expect(isIndex([])).toBe(false)
expect(isIndex('')).toBe(false)
expect(isIndex(.6)).toBe(false)
expect(getSliceBounds([undefined, undefined], 6)).toEqual([0, 6])
expect(getSliceBounds([1, -1], 6)).toEqual([1, 5])
expect(getSliceBounds([7, 8], 6)).toEqual([7, 8])
})
})

Expand Down
37 changes: 37 additions & 0 deletions src/util/lang.js
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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -20,6 +54,9 @@ const isSymbol = arg => typeof arg === 'symbol'
const toString = arg => typeof arg === 'string' ? arg : `${arg}`

export {
isNaturalInteger,
isNil,
isSymbol,
length,
toString,
}
Loading