formula-one
Advanced tools
Comparing version 0.9.0-alpha.4 to 0.9.0-alpha.5
@@ -11,3 +11,3 @@ # Changelog | ||
- Add `addFields` and `filterFields` array manipulators. These are currently necessary due to the non-atomic nature of the current `addField` and `removeField` manipulators. They will be made atomic in a future version. | ||
- Add `addFields`, `filterFields`, and `modifyFields` array manipulators. These are currently necessary due to the non-atomic nature of the current `addField` and `removeField` manipulators. They will be made atomic in a future version. | ||
@@ -25,2 +25,8 @@ The API is: | ||
filterFields: (predicate: (item: E, index: number) => boolean) => void | ||
// A way to simultaneously add and remove fields from an ArrayField<E> | ||
modifyFields: ({ | ||
insertSpans: $ReadOnlyArray<Span<E>, | ||
filterPredicate: (item: E, index: number) => boolean | ||
}) => void | ||
``` | ||
@@ -27,0 +33,0 @@ |
@@ -152,7 +152,12 @@ "use strict"; | ||
var _unzip = (0, _array.unzip)(zipped.filter(function (_ref4, i) { | ||
var _unzip = (0, _array.unzip)(zipped.filter(function (_ref4, i, arr) { | ||
var _ref5 = _slicedToArray(_ref4, 1), | ||
value = _ref5[0]; | ||
return predicate(value, i); | ||
return predicate(value, i, arr.map(function (_ref6) { | ||
var _ref7 = _slicedToArray(_ref6, 1), | ||
v = _ref7[0]; | ||
return v; | ||
})); | ||
})), | ||
@@ -166,3 +171,6 @@ _unzip2 = _slicedToArray(_unzip, 2), | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._removeChildField = function (index) { | ||
}, _this._modifyChildFields = function (_ref8) { | ||
var insertSpans = _ref8.insertSpans, | ||
filterPredicate = _ref8.filterPredicate; | ||
var _this$props$link$form5 = _slicedToArray(_this.props.link.formState, 2), | ||
@@ -172,7 +180,47 @@ oldValue = _this$props$link$form5[0], | ||
var newValue = (0, _array.removeAt)(index, oldValue); | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.removeAt)(index, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
var cleanNode = { | ||
errors: _types.cleanErrors, | ||
meta: _types.cleanMeta | ||
}; | ||
// TODO(zach): there's a less complicated, more functorial way to do this | ||
// augment, then unaugment | ||
var zipped = (0, _array.zip)(oldValue, (0, _shapedTree.shapedArrayChildren)(oldTree)); | ||
// augment the spans with fresh nodes | ||
var augmentedSpans = insertSpans !== undefined ? insertSpans.map(function (_ref9) { | ||
var _ref10 = _slicedToArray(_ref9, 2), | ||
index = _ref10[0], | ||
contents = _ref10[1]; | ||
return [index, contents.map(function (v) { | ||
return [v, (0, _shapedTree.treeFromValue)(v, cleanNode)]; | ||
})]; | ||
}) : undefined; | ||
// augment the predicate to work on formstates | ||
var augmentedPredicate = filterPredicate !== undefined ? function (_ref11, i, arr) { | ||
var _ref12 = _slicedToArray(_ref11, 2), | ||
v = _ref12[0], | ||
_ = _ref12[1]; | ||
return filterPredicate(v, i, arr.map(function (_ref13) { | ||
var _ref14 = _slicedToArray(_ref13, 2), | ||
v = _ref14[0], | ||
_ = _ref14[1]; | ||
return v; | ||
})); | ||
} : undefined; | ||
var _unzip3 = (0, _array.unzip)((0, _array.modify)({ insertSpans: augmentedSpans, filterPredicate: augmentedPredicate }, zipped)), | ||
_unzip4 = _slicedToArray(_unzip3, 2), | ||
newValue = _unzip4[0], | ||
newChildren = _unzip4[1]; | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)(newChildren, oldTree); | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._moveChildField = function (from, to) { | ||
}, _this._removeChildField = function (index) { | ||
var _this$props$link$form6 = _slicedToArray(_this.props.link.formState, 2), | ||
@@ -182,2 +230,11 @@ oldValue = _this$props$link$form6[0], | ||
var newValue = (0, _array.removeAt)(index, oldValue); | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.removeAt)(index, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._moveChildField = function (from, to) { | ||
var _this$props$link$form7 = _slicedToArray(_this.props.link.formState, 2), | ||
oldValue = _this$props$link$form7[0], | ||
oldTree = _this$props$link$form7[1]; | ||
var newValue = (0, _array.moveFromTo)(from, to, oldValue); | ||
@@ -216,4 +273,4 @@ var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.moveFromTo)(from, to, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
value: function forceChildRemount() { | ||
this.setState(function (_ref6) { | ||
var nonce = _ref6.nonce; | ||
this.setState(function (_ref15) { | ||
var nonce = _ref15.nonce; | ||
return { nonce: nonce + 1 }; | ||
@@ -238,3 +295,4 @@ }); | ||
addFields: this._addChildFields, | ||
filterFields: this._filterChildFields | ||
filterFields: this._filterChildFields, | ||
modifyFields: this._modifyChildFields | ||
}, { | ||
@@ -241,0 +299,0 @@ touched: (0, _formState3.getExtras)(formState).meta.touched, |
@@ -488,3 +488,3 @@ "use strict"; | ||
}); | ||
it("validates after entry is added", function () { | ||
it("validates after fields are added", function () { | ||
var formStateValue = ["one", "two", "three"]; | ||
@@ -520,3 +520,3 @@ var formState = (0, _tools.mockFormState)(formStateValue); | ||
describe("filterFields", function () { | ||
it("exposes addFields to add an entry", function () { | ||
it("exposes filterFields to filter entries", function () { | ||
var formStateValue = ["one", "two", "three"]; | ||
@@ -539,3 +539,3 @@ var formState = (0, _tools.mockFormState)(formStateValue); | ||
}); | ||
it("validates after entry is added", function () { | ||
it("validates after fields are filtered", function () { | ||
var formStateValue = ["one", "two", "three", "four", "five"]; | ||
@@ -573,2 +573,56 @@ var formState = (0, _tools.mockFormState)(formStateValue); | ||
}); | ||
describe("modifyFields", function () { | ||
it("exposes modifyFields to add and remove entries atomically", function () { | ||
var formStateValue = ["one", "two", "three"]; | ||
var formState = (0, _tools.mockFormState)(formStateValue); | ||
var link = (0, _tools.mockLink)(formState); | ||
var renderFn = jest.fn(function () { | ||
return null; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ link: link }, | ||
renderFn | ||
)); | ||
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ | ||
modifyFields: expect.any(Function) | ||
}), expect.anything()); | ||
}); | ||
it("validates after fields are modified", function () { | ||
var formStateValue = ["one", "two", "three"]; | ||
var formState = (0, _tools.mockFormState)(formStateValue); | ||
var link = (0, _tools.mockLink)(formState); | ||
var renderFn = jest.fn(function () { | ||
return null; | ||
}); | ||
var validation = jest.fn(function () { | ||
return ["an error"]; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ validation: validation, link: link }, | ||
renderFn | ||
)); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
var _renderFn$mock$calls$6 = _slicedToArray(renderFn.mock.calls[0], 2), | ||
_ = _renderFn$mock$calls$6[0], | ||
modifyFields = _renderFn$mock$calls$6[1].modifyFields; | ||
modifyFields({ | ||
insertSpans: [[0, ["start"]], [2, ["middle", "content"]]], | ||
filterPredicate: function filterPredicate(v) { | ||
return v !== "one"; | ||
} | ||
}); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith(["start", "two", "middle", "content", "three"]); | ||
}); | ||
}); | ||
}); | ||
@@ -575,0 +629,0 @@ |
@@ -21,2 +21,122 @@ "use strict"; | ||
describe("modify", function () { | ||
it("is identity when given nothing to insert or remove", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({}, [])).toEqual([]); | ||
expect((0, _array.modify)({}, a)).toEqual(a); | ||
}); | ||
describe("inserting spans", function () { | ||
it("is identity when given no spans", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ insertSpans: [] }, [])).toEqual([]); | ||
expect((0, _array.modify)({ insertSpans: [] }, a)).toEqual(a); | ||
}); | ||
it("inserts a single span", function () { | ||
var a = ["one", "two", "three"]; | ||
var s = [1, ["hello", "world"]]; | ||
expect((0, _array.modify)({ insertSpans: [s] }, a)).toEqual(["one", "hello", "world", "two", "three"]); | ||
}); | ||
it("inserts a span at the end of the array", function () { | ||
var a = ["one", "two", "three"]; | ||
var s = [3, ["the", "end"]]; | ||
expect((0, _array.modify)({ insertSpans: [s] }, a)).toEqual(["one", "two", "three", "the", "end"]); | ||
}); | ||
it("errors if two spans with the same index are provided", function () { | ||
var redundantSpans = [[0, ["one", "thing"]], [0, ["and", "another"]]]; | ||
expect(function () { | ||
(0, _array.modify)({ insertSpans: redundantSpans }, []); | ||
}).toThrowError("at the same index"); | ||
}); | ||
it("inserts multiple spans in the correct place", function () { | ||
var s0 = [1, ["uno"]]; | ||
var s1 = [2, ["dos"]]; | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ insertSpans: [s0, s1] }, a)).toEqual(["one", "uno", "two", "dos", "three"]); | ||
}); | ||
}); | ||
describe("filtering", function () { | ||
it("is identity when given (const true)", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate() { | ||
return true; | ||
} }, [])).toEqual([]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate() { | ||
return true; | ||
} }, a)).toEqual(a); | ||
}); | ||
it("removes everything when given (const false)", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate() { | ||
return false; | ||
} }, [])).toEqual([]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate() { | ||
return false; | ||
} }, a)).toEqual([]); | ||
}); | ||
it("removes things based on values", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(s) { | ||
return s === "one"; | ||
} }, [])).toEqual([]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(s) { | ||
return s === "one"; | ||
} }, a)).toEqual(["one"]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(s) { | ||
return s !== "one"; | ||
} }, a)).toEqual(["two", "three"]); | ||
}); | ||
it("removes things based on index", function () { | ||
var a = ["one", "two", "three"]; | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(_, i) { | ||
return i === 1; | ||
} }, [])).toEqual([]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(_, i) { | ||
return i === 1; | ||
} }, a)).toEqual(["two"]); | ||
expect((0, _array.modify)({ filterPredicate: function filterPredicate(_, i) { | ||
return i !== 1; | ||
} }, a)).toEqual(["one", "three"]); | ||
}); | ||
it("removes things based on the whole array", function () { | ||
var a = ["one", "two", "three"]; | ||
var pred = jest.fn(); | ||
(0, _array.modify)({ filterPredicate: pred }, a); | ||
expect(pred).toHaveBeenCalledWith(expect.anything(), expect.anything(), a); | ||
expect((0, _array.modify)({ | ||
filterPredicate: function filterPredicate(_0, i, arr) { | ||
return i + 1 < arr.length && arr[i + 1] === "three"; | ||
} | ||
}, a)).toEqual(["two"]); | ||
}); | ||
}); | ||
describe("inserting and filtering simultaneously", function () { | ||
it("can insert and remove simultaneously", function () { | ||
expect((0, _array.modify)({ | ||
insertSpans: [[0, ["front"]], [3, ["middle", "content"]]], | ||
filterPredicate: function filterPredicate(_, i) { | ||
return !(i === 0 || i === 2 || i === 3); | ||
} | ||
}, ["one", "two", "three", "four", "five"])).toEqual(["front", "two", "middle", "content", "five"]); | ||
}); | ||
}); | ||
}); | ||
describe("insertSpans", function () { | ||
@@ -23,0 +143,0 @@ it("is identity when given no spans", function () { |
@@ -13,2 +13,3 @@ "use strict"; | ||
exports.moveFromTo = moveFromTo; | ||
exports.modify = modify; | ||
exports.insertSpans = insertSpans; | ||
@@ -40,44 +41,69 @@ exports.zipWith = zipWith; | ||
function insertSpans(spans, arr) { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
var indexSet = new Set(spans.map(function (_ref) { | ||
var _ref2 = _slicedToArray(_ref, 1), | ||
i = _ref2[0]; | ||
function modify(_ref, arr) { | ||
var insertSpans = _ref.insertSpans, | ||
filterPredicate = _ref.filterPredicate; | ||
return i; | ||
})); | ||
if (indexSet.size !== spans.length) { | ||
throw new Error("You cannot insert two spans at the same index. Combine the values of the spans."); | ||
} | ||
var sortedSpans = []; | ||
if (insertSpans !== undefined) { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
var indexSet = new Set(insertSpans.map(function (_ref2) { | ||
var _ref3 = _slicedToArray(_ref2, 1), | ||
i = _ref3[0]; | ||
// sort spans by insertion position | ||
var spansCopy = [].concat(_toConsumableArray(spans)); | ||
spansCopy.sort(function (_ref3, _ref4) { | ||
var _ref6 = _slicedToArray(_ref3, 1), | ||
i = _ref6[0]; | ||
return i; | ||
})); | ||
if (indexSet.size !== insertSpans.length) { | ||
throw new Error("You cannot insert two spans at the same index. Combine the values of the spans."); | ||
} | ||
var _ref5 = _slicedToArray(_ref4, 1), | ||
j = _ref5[0]; | ||
// sort spans by insertion position | ||
sortedSpans = [].concat(_toConsumableArray(insertSpans)); | ||
sortedSpans.sort(function (_ref4, _ref5) { | ||
var _ref7 = _slicedToArray(_ref4, 1), | ||
i = _ref7[0]; | ||
return i - j; | ||
}); | ||
var _ref6 = _slicedToArray(_ref5, 1), | ||
j = _ref6[0]; | ||
return i - j; | ||
}); | ||
} | ||
// The next span to insert | ||
var nextSpanIndex = 0; | ||
// build the new array in one pass | ||
var ret = []; | ||
var lastIndexInsertedAt = 0; | ||
spansCopy.forEach(function (_ref7) { | ||
var _ref8 = _slicedToArray(_ref7, 2), | ||
index = _ref8[0], | ||
contents = _ref8[1]; | ||
for (var i = 0; i < arr.length; i += 1) { | ||
if (nextSpanIndex < sortedSpans.length) { | ||
var _sortedSpans$nextSpan = _slicedToArray(sortedSpans[nextSpanIndex], 2), | ||
index = _sortedSpans$nextSpan[0], | ||
contents = _sortedSpans$nextSpan[1]; | ||
// All the content before this | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt, index)); | ||
ret = ret.concat(contents); | ||
lastIndexInsertedAt = index; | ||
}); | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt)); | ||
if (index === i) { | ||
ret = ret.concat(contents); | ||
nextSpanIndex += 1; | ||
} | ||
} | ||
if (filterPredicate === undefined || filterPredicate(arr[i], i, arr)) { | ||
ret.push(arr[i]); | ||
} | ||
} | ||
// insert spans after the end of the array | ||
for (var _i = nextSpanIndex; _i < sortedSpans.length; _i += 1) { | ||
var _sortedSpans$_i = _slicedToArray(sortedSpans[_i], 2), | ||
_ = _sortedSpans$_i[0], | ||
_contents = _sortedSpans$_i[1]; | ||
ret = ret.concat(_contents); | ||
} | ||
return ret; | ||
} | ||
function insertSpans(spans, arr) { | ||
return modify({ insertSpans: spans }, arr); | ||
} | ||
// Strict on length | ||
@@ -84,0 +110,0 @@ function zipWith(f, left, right) { |
{ | ||
"name": "formula-one", | ||
"version": "0.9.0-alpha.4", | ||
"version": "0.9.0-alpha.5", | ||
"description": "Strongly-typed React form state management", | ||
@@ -5,0 +5,0 @@ "author": "Zach Gotsch", |
@@ -289,3 +289,3 @@ # formula-one | ||
| touched | `boolean` | Whether the field has been touched (blurred or changed) | | ||
| ch}anged | `boolean` | Whether the field has been changed | | ||
| changed | `boolean` | Whether the field has been changed | | ||
| shouldShowErrors | `boolean` | Whether errors should be shown according to the current feedback strategy | | ||
@@ -292,0 +292,0 @@ | unfilteredErrors | `$ReadOnlyArray<string>` | All validation errors for the current field. (This differs from the `errors` argument in `<Field>`, since the `errors` argument in `<Field>` will be empty if `shouldShowErrors` is false) | |
@@ -28,2 +28,3 @@ // @flow strict | ||
insertSpans, | ||
modify, | ||
zip, | ||
@@ -62,3 +63,9 @@ unzip, | ||
addFields: (spans: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>) => void, | ||
filterFields: (predicate: (E, number) => boolean) => void, | ||
filterFields: ( | ||
predicate: (E, number, $ReadOnlyArray<E>) => boolean | ||
) => void, | ||
modifyFields: ({ | ||
insertSpans?: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>, | ||
filterPredicate?: (E, number, $ReadOnlyArray<E>) => boolean, | ||
}) => void, | ||
}, | ||
@@ -244,3 +251,3 @@ additionalInfo: AdditionalRenderInfo<Array<E>> | ||
_filterChildFields: ( | ||
predicate: (E, number) => boolean | ||
predicate: (E, number, $ReadOnlyArray<E>) => boolean | ||
) => void = predicate => { | ||
@@ -251,3 +258,5 @@ const [oldValue, oldTree] = this.props.link.formState; | ||
const [newValue, newChildren] = unzip( | ||
zipped.filter(([value], i) => predicate(value, i)) | ||
zipped.filter(([value], i, arr) => | ||
predicate(value, i, arr.map(([v]) => v)) | ||
) | ||
); | ||
@@ -264,2 +273,48 @@ const newTree = dangerouslySetChildren(newChildren, oldTree); | ||
_modifyChildFields: ({ | ||
insertSpans?: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>, | ||
filterPredicate?: (E, number, $ReadOnlyArray<E>) => boolean, | ||
}) => void = ({insertSpans, filterPredicate}) => { | ||
const [oldValue, oldTree] = this.props.link.formState; | ||
const cleanNode = { | ||
errors: cleanErrors, | ||
meta: cleanMeta, | ||
}; | ||
// TODO(zach): there's a less complicated, more functorial way to do this | ||
// augment, then unaugment | ||
const zipped = zip(oldValue, shapedArrayChildren(oldTree)); | ||
// augment the spans with fresh nodes | ||
const augmentedSpans = | ||
insertSpans !== undefined | ||
? insertSpans.map(([index, contents]) => [ | ||
index, | ||
contents.map(v => [v, treeFromValue(v, cleanNode)]), | ||
]) | ||
: undefined; | ||
// augment the predicate to work on formstates | ||
const augmentedPredicate = | ||
filterPredicate !== undefined | ||
? ([v, _], i, arr) => filterPredicate(v, i, arr.map(([v, _]) => v)) | ||
: undefined; | ||
const [newValue, newChildren] = unzip( | ||
modify( | ||
{insertSpans: augmentedSpans, filterPredicate: augmentedPredicate}, | ||
zipped | ||
) | ||
); | ||
const newTree = dangerouslySetChildren(newChildren, oldTree); | ||
this.props.link.onChange( | ||
validate( | ||
this.props.validation, | ||
setChanged(setTouched([newValue, newTree])) | ||
) | ||
); | ||
}; | ||
_removeChildField = (index: number) => { | ||
@@ -318,2 +373,3 @@ const [oldValue, oldTree] = this.props.link.formState; | ||
filterFields: this._filterChildFields, | ||
modifyFields: this._modifyChildFields, | ||
}, | ||
@@ -320,0 +376,0 @@ { |
@@ -379,3 +379,3 @@ // @flow | ||
}); | ||
it("validates after entry is added", () => { | ||
it("validates after fields are added", () => { | ||
const formStateValue = ["one", "two", "three"]; | ||
@@ -412,3 +412,3 @@ const formState = mockFormState(formStateValue); | ||
describe("filterFields", () => { | ||
it("exposes addFields to add an entry", () => { | ||
it("exposes filterFields to filter entries", () => { | ||
const formStateValue = ["one", "two", "three"]; | ||
@@ -429,3 +429,3 @@ const formState = mockFormState(formStateValue); | ||
}); | ||
it("validates after entry is added", () => { | ||
it("validates after fields are filtered", () => { | ||
const formStateValue = ["one", "two", "three", "four", "five"]; | ||
@@ -453,2 +453,51 @@ const formState = mockFormState(formStateValue); | ||
}); | ||
describe("modifyFields", () => { | ||
it("exposes modifyFields to add and remove entries atomically", () => { | ||
const formStateValue = ["one", "two", "three"]; | ||
const formState = mockFormState(formStateValue); | ||
const link = mockLink(formState); | ||
const renderFn = jest.fn(() => null); | ||
TestRenderer.create(<ArrayField link={link}>{renderFn}</ArrayField>); | ||
expect(renderFn).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.objectContaining({ | ||
modifyFields: expect.any(Function), | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
it("validates after fields are modified", () => { | ||
const formStateValue = ["one", "two", "three"]; | ||
const formState = mockFormState(formStateValue); | ||
const link = mockLink(formState); | ||
const renderFn = jest.fn(() => null); | ||
const validation = jest.fn(() => ["an error"]); | ||
TestRenderer.create( | ||
<ArrayField validation={validation} link={link}> | ||
{renderFn} | ||
</ArrayField> | ||
); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
const [_, {modifyFields}] = renderFn.mock.calls[0]; | ||
modifyFields({ | ||
insertSpans: [[0, ["start"]], [2, ["middle", "content"]]], | ||
filterPredicate: v => v !== "one", | ||
}); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith([ | ||
"start", | ||
"two", | ||
"middle", | ||
"content", | ||
"three", | ||
]); | ||
}); | ||
}); | ||
}); | ||
@@ -455,0 +504,0 @@ |
// @flow strict | ||
import {moveFromTo, insertSpans} from "../../utils/array"; | ||
import {moveFromTo, modify, insertSpans} from "../../utils/array"; | ||
@@ -21,2 +21,137 @@ describe("moveFromTo", () => { | ||
describe("modify", () => { | ||
it("is identity when given nothing to insert or remove", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({}, [])).toEqual([]); | ||
expect(modify({}, a)).toEqual(a); | ||
}); | ||
describe("inserting spans", () => { | ||
it("is identity when given no spans", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({insertSpans: []}, [])).toEqual([]); | ||
expect(modify({insertSpans: []}, a)).toEqual(a); | ||
}); | ||
it("inserts a single span", () => { | ||
const a = ["one", "two", "three"]; | ||
const s = [1, ["hello", "world"]]; | ||
expect(modify({insertSpans: [s]}, a)).toEqual([ | ||
"one", | ||
"hello", | ||
"world", | ||
"two", | ||
"three", | ||
]); | ||
}); | ||
it("inserts a span at the end of the array", () => { | ||
const a = ["one", "two", "three"]; | ||
const s = [3, ["the", "end"]]; | ||
expect(modify({insertSpans: [s]}, a)).toEqual([ | ||
"one", | ||
"two", | ||
"three", | ||
"the", | ||
"end", | ||
]); | ||
}); | ||
it("errors if two spans with the same index are provided", () => { | ||
const redundantSpans = [[0, ["one", "thing"]], [0, ["and", "another"]]]; | ||
expect(() => { | ||
modify({insertSpans: redundantSpans}, []); | ||
}).toThrowError("at the same index"); | ||
}); | ||
it("inserts multiple spans in the correct place", () => { | ||
const s0 = [1, ["uno"]]; | ||
const s1 = [2, ["dos"]]; | ||
const a = ["one", "two", "three"]; | ||
expect(modify({insertSpans: [s0, s1]}, a)).toEqual([ | ||
"one", | ||
"uno", | ||
"two", | ||
"dos", | ||
"three", | ||
]); | ||
}); | ||
}); | ||
describe("filtering", () => { | ||
it("is identity when given (const true)", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({filterPredicate: () => true}, [])).toEqual([]); | ||
expect(modify({filterPredicate: () => true}, a)).toEqual(a); | ||
}); | ||
it("removes everything when given (const false)", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({filterPredicate: () => false}, [])).toEqual([]); | ||
expect(modify({filterPredicate: () => false}, a)).toEqual([]); | ||
}); | ||
it("removes things based on values", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({filterPredicate: s => s === "one"}, [])).toEqual([]); | ||
expect(modify({filterPredicate: s => s === "one"}, a)).toEqual(["one"]); | ||
expect(modify({filterPredicate: s => s !== "one"}, a)).toEqual([ | ||
"two", | ||
"three", | ||
]); | ||
}); | ||
it("removes things based on index", () => { | ||
const a = ["one", "two", "three"]; | ||
expect(modify({filterPredicate: (_, i) => i === 1}, [])).toEqual([]); | ||
expect(modify({filterPredicate: (_, i) => i === 1}, a)).toEqual(["two"]); | ||
expect(modify({filterPredicate: (_, i) => i !== 1}, a)).toEqual([ | ||
"one", | ||
"three", | ||
]); | ||
}); | ||
it("removes things based on the whole array", () => { | ||
const a = ["one", "two", "three"]; | ||
const pred = jest.fn(); | ||
modify({filterPredicate: pred}, a); | ||
expect(pred).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.anything(), | ||
a | ||
); | ||
expect( | ||
modify( | ||
{ | ||
filterPredicate: (_0, i, arr) => | ||
i + 1 < arr.length && arr[i + 1] === "three", | ||
}, | ||
a | ||
) | ||
).toEqual(["two"]); | ||
}); | ||
}); | ||
describe("inserting and filtering simultaneously", () => { | ||
it("can insert and remove simultaneously", () => { | ||
expect( | ||
modify( | ||
{ | ||
insertSpans: [[0, ["front"]], [3, ["middle", "content"]]], | ||
filterPredicate: (_, i) => !(i === 0 || i === 2 || i === 3), | ||
}, | ||
["one", "two", "three", "four", "five"] | ||
) | ||
).toEqual(["front", "two", "middle", "content", "five"]); | ||
}); | ||
}); | ||
}); | ||
describe("insertSpans", () => { | ||
@@ -23,0 +158,0 @@ it("is identity when given no spans", () => { |
@@ -32,28 +32,52 @@ // @flow strict | ||
export function insertSpans<E>( | ||
spans: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>, | ||
type AddSpan<E> = [number, $ReadOnlyArray<E>]; | ||
export function modify<E>( | ||
{ | ||
insertSpans, | ||
filterPredicate, | ||
}: { | ||
insertSpans?: $ReadOnlyArray<AddSpan<E>>, | ||
filterPredicate?: (E, number, $ReadOnlyArray<E>) => boolean, | ||
}, | ||
arr: $ReadOnlyArray<E> | ||
): Array<E> { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
const indexSet = new Set(spans.map(([i]) => i)); | ||
if (indexSet.size !== spans.length) { | ||
throw new Error( | ||
"You cannot insert two spans at the same index. Combine the values of the spans." | ||
); | ||
let sortedSpans = []; | ||
if (insertSpans !== undefined) { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
const indexSet = new Set(insertSpans.map(([i]) => i)); | ||
if (indexSet.size !== insertSpans.length) { | ||
throw new Error( | ||
"You cannot insert two spans at the same index. Combine the values of the spans." | ||
); | ||
} | ||
// sort spans by insertion position | ||
sortedSpans = [...insertSpans]; | ||
sortedSpans.sort(([i], [j]) => i - j); | ||
} | ||
// sort spans by insertion position | ||
const spansCopy = [...spans]; | ||
spansCopy.sort(([i], [j]) => i - j); | ||
// The next span to insert | ||
let nextSpanIndex = 0; | ||
// build the new array in one pass | ||
let ret = []; | ||
let lastIndexInsertedAt = 0; | ||
spansCopy.forEach(([index, contents]) => { | ||
// All the content before this | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt, index)); | ||
for (let i = 0; i < arr.length; i += 1) { | ||
if (nextSpanIndex < sortedSpans.length) { | ||
const [index, contents] = sortedSpans[nextSpanIndex]; | ||
if (index === i) { | ||
ret = ret.concat(contents); | ||
nextSpanIndex += 1; | ||
} | ||
} | ||
if (filterPredicate === undefined || filterPredicate(arr[i], i, arr)) { | ||
ret.push(arr[i]); | ||
} | ||
} | ||
// insert spans after the end of the array | ||
for (let i = nextSpanIndex; i < sortedSpans.length; i += 1) { | ||
const [_, contents] = sortedSpans[i]; | ||
ret = ret.concat(contents); | ||
lastIndexInsertedAt = index; | ||
}); | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt)); | ||
} | ||
@@ -63,2 +87,9 @@ return ret; | ||
export function insertSpans<E>( | ||
spans: $ReadOnlyArray<AddSpan<E>>, | ||
arr: $ReadOnlyArray<E> | ||
): Array<E> { | ||
return modify({insertSpans: spans}, arr); | ||
} | ||
// Strict on length | ||
@@ -65,0 +96,0 @@ export function zipWith<A, B, C>( |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
326490
8229