From 4433fbe133954fd5e5268255d3c39a17e461e470 Mon Sep 17 00:00:00 2001 From: John Williams Date: Sun, 8 Oct 2023 15:24:03 -0700 Subject: [PATCH 1/3] added lexigographical sort --- .vscode/launch.json | 21 ++++++++ package.json | 1 + src/index.ts | 3 ++ src/sortables/lexicographically.ts | 37 ++++++++++++++ test/lexicographically.spec.ts | 77 ++++++++++++++++++++++++++++++ yarn.lock | 12 +++++ 6 files changed, 151 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 src/sortables/lexicographically.ts create mode 100644 test/lexicographically.spec.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4f39eb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug tests", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/node_modules/.bin/mocha", + "runtimeArgs": ["--require", "ts-node/register"], + "args": ["${relativeFile}"], + "outFiles": [ + "${workspaceFolder}/src/**/*.js", + "${workspaceFolder}/test/**/*.js" + ] + } + ] +} diff --git a/package.json b/package.json index 0fef426..a4d1765 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.33.2", + "fast-check": "^3.13.1", "husky": "^8.0.3", "mocha": "^10.2.0", "nyc": "^15.1.0", diff --git a/src/index.ts b/src/index.ts index d9ef4ca..f0fca43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import byString from "./sortables/byString"; import byNumber from "./sortables/byNumber"; import byBoolean from "./sortables/byBoolean"; import sortAsync, { AsyncArray } from "./sortables/byAsyncValue"; +import lexicographically from "./sortables/lexicographically"; export default { byAny, @@ -17,6 +18,7 @@ export default { sortAsync, byBoolean, AsyncArray, + lexicographically, }; export { @@ -29,4 +31,5 @@ export { sortAsync, byBoolean, AsyncArray, + lexicographically, }; diff --git a/src/sortables/lexicographically.ts b/src/sortables/lexicographically.ts new file mode 100644 index 0000000..3c29be0 --- /dev/null +++ b/src/sortables/lexicographically.ts @@ -0,0 +1,37 @@ +import { sortable } from "../types/types"; + +/** + * the sortable to sort **sequences** lexicographically + * + * The values `null` and `undefined` are treated as empty sequences. + * + * @param sortFn the sortable to be applied corresponding items in each sequence + */ +function lexicographically(sortFn: sortable): sortable> { + return ( + left: Iterable | null | undefined, + right: Iterable | null | undefined + ): number => { + const leftIter = (left || [])[Symbol.iterator](); + const rightIter = (right || [])[Symbol.iterator](); + for (;;) { + const { done: leftDone, value: leftValue } = leftIter.next(); + const { done: rightDone, value: rightValue } = rightIter.next(); + if (leftDone && rightDone) { + return 0; + } + if (leftDone) { + return -1; + } + if (rightDone) { + return 1; + } + const v = sortFn(leftValue, rightValue); + if (v !== 0) { + return v; + } + } + }; +} + +export default lexicographically; diff --git a/test/lexicographically.spec.ts b/test/lexicographically.spec.ts new file mode 100644 index 0000000..bcdf0a0 --- /dev/null +++ b/test/lexicographically.spec.ts @@ -0,0 +1,77 @@ +import "mocha"; +import fc from "fast-check"; +import { byNumber, lexicographically } from "../src"; + +describe("lexiocgraphically", () => { + const innerCmp = byNumber(); + const cmp = lexicographically(innerCmp); + + it("treats identical sequences as equal", () => { + fc.assert( + fc.property(fc.array(fc.integer()), (seq) => cmp(seq, [...seq]) === 0) + ); + }); + + it("treats treats null or undefined as an empty sequence", () => { + fc.assert( + fc.property( + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.array(fc.integer(), { minLength: 1 }), + (empty1, empty2, nonempty) => + cmp(empty1, nonempty) < 0 && + cmp(nonempty, empty1) > 0 && + cmp(empty1, empty2) === 0 + ) + ); + }); + + it("sorts a shorter sequence before a longer sequence", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + (prefix, item) => cmp(prefix, [...prefix, item]) < 0 + ) + ); + }); + + it("sorts a longer sequence after a shorter sequence", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + (prefix, item) => cmp([...prefix, item], prefix) > 0 + ) + ); + }); + + it("sorts sequences like their first differing element", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.tuple(fc.integer(), fc.integer()).filter(([x, y]) => x !== y), + fc.array(fc.integer()), + fc.array(fc.integer()), + (prefix, [item1, item2], suffix1, suffix2) => + cmp( + [...prefix, item1, ...suffix1], + [...prefix, item2, ...suffix2] + ) === innerCmp(item1, item2) + ) + ); + }); + + it("sorts sequences with a common prefix like their tails", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.array(fc.integer()), + fc.array(fc.integer()), + (prefix, tail1, tail2) => + cmp([...prefix, ...tail1], [...prefix, ...tail2]) === + cmp(tail1, tail2) + ) + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index b209cba..60e2b35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1859,6 +1859,13 @@ execa@^7.1.1: signal-exit "^3.0.7" strip-final-newline "^3.0.0" +fast-check@^3.13.1: + version "3.13.1" + resolved "https://domoreexp.pkgs.visualstudio.com/_packaging/npm-mirror/npm/registry/fast-check/-/fast-check-3.13.1.tgz#32c7a78621098bd30d71e40c0e269a728a3188e2" + integrity sha1-MsenhiEJi9MNceQMDiaacooxiOI= + dependencies: + pure-rand "^6.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3436,6 +3443,11 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +pure-rand@^6.0.0: + version "6.0.4" + resolved "https://domoreexp.pkgs.visualstudio.com/_packaging/npm-mirror/npm/registry/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" + integrity sha1-ULc39qklRoZ5v/AK0g6t5T831cc= + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" From 08bd56c496956345c85390ebc57cbef3ca6fd942 Mon Sep 17 00:00:00 2001 From: John Williams Date: Sun, 8 Oct 2023 15:39:54 -0700 Subject: [PATCH 2/3] added descending lexicographical sort --- src/sortables/lexicographically.ts | 15 +++- test/lexicographically.spec.ts | 134 +++++++++++++++-------------- 2 files changed, 82 insertions(+), 67 deletions(-) diff --git a/src/sortables/lexicographically.ts b/src/sortables/lexicographically.ts index 3c29be0..ab65dad 100644 --- a/src/sortables/lexicographically.ts +++ b/src/sortables/lexicographically.ts @@ -1,3 +1,4 @@ +import { SortOption } from "src/interfaces/interfaces"; import { sortable } from "../types/types"; /** @@ -6,8 +7,14 @@ import { sortable } from "../types/types"; * The values `null` and `undefined` are treated as empty sequences. * * @param sortFn the sortable to be applied corresponding items in each sequence + * @param options the option to control whether the sort is descending */ -function lexicographically(sortFn: sortable): sortable> { +function lexicographically( + sortFn: sortable, + options: Omit = {} +): sortable> { + const { desc = false } = options; + const sign = desc ? -1 : 1; return ( left: Iterable | null | undefined, right: Iterable | null | undefined @@ -21,14 +28,14 @@ function lexicographically(sortFn: sortable): sortable> { return 0; } if (leftDone) { - return -1; + return -sign; } if (rightDone) { - return 1; + return sign; } const v = sortFn(leftValue, rightValue); if (v !== 0) { - return v; + return sign * v; } } }; diff --git a/test/lexicographically.spec.ts b/test/lexicographically.spec.ts index bcdf0a0..41c86c6 100644 --- a/test/lexicographically.spec.ts +++ b/test/lexicographically.spec.ts @@ -4,74 +4,82 @@ import { byNumber, lexicographically } from "../src"; describe("lexiocgraphically", () => { const innerCmp = byNumber(); - const cmp = lexicographically(innerCmp); - it("treats identical sequences as equal", () => { - fc.assert( - fc.property(fc.array(fc.integer()), (seq) => cmp(seq, [...seq]) === 0) - ); - }); + [ + { desc: false, sign: 1, suffix: " (ascending)" }, + { desc: true, sign: -1, suffix: " (descending)" }, + ].forEach(({ desc, sign, suffix }) => { + const cmp = lexicographically(innerCmp, { desc }); - it("treats treats null or undefined as an empty sequence", () => { - fc.assert( - fc.property( - fc.oneof(fc.constantFrom(null, undefined, [])), - fc.oneof(fc.constantFrom(null, undefined, [])), - fc.array(fc.integer(), { minLength: 1 }), - (empty1, empty2, nonempty) => - cmp(empty1, nonempty) < 0 && - cmp(nonempty, empty1) > 0 && - cmp(empty1, empty2) === 0 - ) - ); - }); + it("treats identical sequences as equal" + suffix, () => { + fc.assert( + fc.property(fc.array(fc.integer()), (seq) => cmp(seq, [...seq]) === 0) + ); + }); - it("sorts a shorter sequence before a longer sequence", () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.integer(), - (prefix, item) => cmp(prefix, [...prefix, item]) < 0 - ) - ); - }); + it("treats treats null or undefined as an empty sequence" + suffix, () => { + fc.assert( + fc.property( + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.array(fc.integer(), { minLength: 1 }), + (empty1, empty2, nonempty) => + cmp(empty1, nonempty) * sign < 0 && + cmp(nonempty, empty1) * sign > 0 && + cmp(empty1, empty2) === 0 + ) + ); + }); - it("sorts a longer sequence after a shorter sequence", () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.integer(), - (prefix, item) => cmp([...prefix, item], prefix) > 0 - ) - ); - }); + it("sorts a shorter sequence before a longer sequence" + suffix, () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + (prefix, item) => cmp(prefix, [...prefix, item]) * sign < 0 + ) + ); + }); - it("sorts sequences like their first differing element", () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.tuple(fc.integer(), fc.integer()).filter(([x, y]) => x !== y), - fc.array(fc.integer()), - fc.array(fc.integer()), - (prefix, [item1, item2], suffix1, suffix2) => - cmp( - [...prefix, item1, ...suffix1], - [...prefix, item2, ...suffix2] - ) === innerCmp(item1, item2) - ) - ); - }); + it("sorts a longer sequence after a shorter sequence" + suffix, () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + (prefix, item) => cmp([...prefix, item], prefix) * sign > 0 + ) + ); + }); + + it("sorts sequences like their first differing element" + suffix, () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.tuple(fc.integer(), fc.integer()).filter(([x, y]) => x !== y), + fc.array(fc.integer()), + fc.array(fc.integer()), + (prefix, [item1, item2], suffix1, suffix2) => + cmp( + [...prefix, item1, ...suffix1], + [...prefix, item2, ...suffix2] + ) * + sign === + innerCmp(item1, item2) + ) + ); + }); - it("sorts sequences with a common prefix like their tails", () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.array(fc.integer()), - fc.array(fc.integer()), - (prefix, tail1, tail2) => - cmp([...prefix, ...tail1], [...prefix, ...tail2]) === - cmp(tail1, tail2) - ) - ); + it("sorts sequences with a common prefix like their tails" + suffix, () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.array(fc.integer()), + fc.array(fc.integer()), + (prefix, tail1, tail2) => + cmp([...prefix, ...tail1], [...prefix, ...tail2]) === + cmp(tail1, tail2) + ) + ); + }); }); }); From ca92fd00fb41fad330450ba13a0fd40300a6ca2e Mon Sep 17 00:00:00 2001 From: John Williams Date: Fri, 13 Oct 2023 22:51:18 -0700 Subject: [PATCH 3/3] improved tests --- test/lexicographically.spec.ts | 157 ++++++++++++++++++--------------- test/utils/sort.ts | 4 +- 2 files changed, 88 insertions(+), 73 deletions(-) diff --git a/test/lexicographically.spec.ts b/test/lexicographically.spec.ts index 41c86c6..1c2a2e0 100644 --- a/test/lexicographically.spec.ts +++ b/test/lexicographically.spec.ts @@ -1,85 +1,98 @@ import "mocha"; import fc from "fast-check"; -import { byNumber, lexicographically } from "../src"; +import { lexicographically } from "../src"; +import { sortable } from "../src/types/types"; +import { expect } from "chai"; +import { descToSign } from "./utils/sort"; -describe("lexiocgraphically", () => { - const innerCmp = byNumber(); - - [ - { desc: false, sign: 1, suffix: " (ascending)" }, - { desc: true, sign: -1, suffix: " (descending)" }, - ].forEach(({ desc, sign, suffix }) => { - const cmp = lexicographically(innerCmp, { desc }); - - it("treats identical sequences as equal" + suffix, () => { - fc.assert( - fc.property(fc.array(fc.integer()), (seq) => cmp(seq, [...seq]) === 0) - ); +function comparators(): fc.Arbitrary<{ + sign: number; + itemCmp: sortable; + seqCmp: sortable; +}> { + return fc.tuple(fc.compareFunc(), fc.boolean()).chain(([itemCmp, desc]) => { + return fc.constant({ + sign: descToSign(desc), + itemCmp, + seqCmp: lexicographically(itemCmp, { desc }), }); + }); +} - it("treats treats null or undefined as an empty sequence" + suffix, () => { - fc.assert( - fc.property( - fc.oneof(fc.constantFrom(null, undefined, [])), - fc.oneof(fc.constantFrom(null, undefined, [])), - fc.array(fc.integer(), { minLength: 1 }), - (empty1, empty2, nonempty) => - cmp(empty1, nonempty) * sign < 0 && - cmp(nonempty, empty1) * sign > 0 && - cmp(empty1, empty2) === 0 - ) - ); - }); +describe("sorting lexiocgraphically", () => { + it("treats identical sequences as equal", () => { + fc.assert( + fc.property(comparators(), fc.array(fc.integer()), ({ seqCmp }, seq) => { + expect(seqCmp(seq, [...seq])).to.equal(0); + }) + ); + }); - it("sorts a shorter sequence before a longer sequence" + suffix, () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.integer(), - (prefix, item) => cmp(prefix, [...prefix, item]) * sign < 0 - ) - ); - }); + it("treats treats null or undefined as an empty sequence", () => { + fc.assert( + fc.property( + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.oneof(fc.constantFrom(null, undefined, [])), + fc.array(fc.integer(), { minLength: 1 }), + comparators(), + (empty1, empty2, nonempty, { seqCmp, sign }) => { + expect(seqCmp(empty1, nonempty) * sign).to.be.below(0); + expect(seqCmp(nonempty, empty1) * sign).to.be.above(0); + expect(seqCmp(empty1, empty2)).to.equal(0); + } + ) + ); + }); - it("sorts a longer sequence after a shorter sequence" + suffix, () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.integer(), - (prefix, item) => cmp([...prefix, item], prefix) * sign > 0 - ) - ); - }); + it("sorts a shorter sequence before a longer sequence", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + comparators(), + (prefix, item, { seqCmp, sign }) => { + expect(seqCmp(prefix, [...prefix, item]) * sign).to.be.below(0); + expect(seqCmp([...prefix, item], prefix) * sign).to.be.above(0); + } + ) + ); + }); - it("sorts sequences like their first differing element" + suffix, () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.tuple(fc.integer(), fc.integer()).filter(([x, y]) => x !== y), - fc.array(fc.integer()), - fc.array(fc.integer()), - (prefix, [item1, item2], suffix1, suffix2) => - cmp( + it("sorts sequences like their first differing element", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.integer(), + fc.integer(), + fc.array(fc.integer()), + fc.array(fc.integer()), + comparators(), + (prefix, item1, item2, suffix1, suffix2, { seqCmp, itemCmp, sign }) => { + fc.pre(itemCmp(item1, item2) !== 0); + expect( + seqCmp( [...prefix, item1, ...suffix1], [...prefix, item2, ...suffix2] - ) * - sign === - innerCmp(item1, item2) - ) - ); - }); + ) * sign + ).to.equal(itemCmp(item1, item2)); + } + ) + ); + }); - it("sorts sequences with a common prefix like their tails" + suffix, () => { - fc.assert( - fc.property( - fc.array(fc.integer()), - fc.array(fc.integer()), - fc.array(fc.integer()), - (prefix, tail1, tail2) => - cmp([...prefix, ...tail1], [...prefix, ...tail2]) === - cmp(tail1, tail2) - ) - ); - }); + it("sorts sequences with a common prefix like their tails", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.array(fc.integer()), + fc.array(fc.integer()), + comparators(), + (prefix, tail1, tail2, { seqCmp }) => { + expect( + seqCmp([...prefix, ...tail1], [...prefix, ...tail2]) + ).to.be.equal(seqCmp(tail1, tail2)); + } + ) + ); }); }); diff --git a/test/utils/sort.ts b/test/utils/sort.ts index 665099a..3aea0cc 100644 --- a/test/utils/sort.ts +++ b/test/utils/sort.ts @@ -5,4 +5,6 @@ const getFirstAndLast = (array: T[]): [T, T] => { const reverse = (array: T[]): T[] => array.concat().reverse(); -export {getFirstAndLast, reverse} +const descToSign = (desc: boolean): number => desc ? -1 : 1; + +export {getFirstAndLast, reverse, descToSign}