diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 8d5476be..7bb323d7 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -120,7 +120,11 @@ const Categories = [ name: "Arithmetic / Logic", ops: [ "Set Union", - "Set Intersection" + "Set Intersection", + "Set Difference", + "Symmetric Difference", + "Cartesian Product", + "Power Set", // "XOR", // "XOR Brute Force", // "OR", diff --git a/src/core/config/OperationConfig.json b/src/core/config/OperationConfig.json index fee0ec09..531f7ce7 100644 --- a/src/core/config/OperationConfig.json +++ b/src/core/config/OperationConfig.json @@ -1,4 +1,23 @@ { + "Cartesian Product": { + "module": "Default", + "description": "Get the cartesian product of two sets", + "inputType": "string", + "outputType": "string", + "flowControl": false, + "args": [ + { + "name": "Sample delimiter", + "type": "binaryString", + "value": "\\n\\n" + }, + { + "name": "Item delimiter", + "type": "binaryString", + "value": "," + } + ] + }, "From Base32": { "module": "Default", "description": "Base32 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers. It uses a smaller set of characters than Base64, usually the uppercase alphabet and the numbers 2 to 7.", @@ -155,6 +174,20 @@ } ] }, + "Power Set": { + "module": "Default", + "description": "Generate the power set of a set", + "inputType": "string", + "outputType": "string", + "flowControl": false, + "args": [ + { + "name": "Item delimiter", + "type": "binaryString", + "value": "," + } + ] + }, "Raw Deflate": { "module": "Default", "description": "Compresses data using the deflate algorithm with no headers.", @@ -305,6 +338,25 @@ } ] }, + "Symmetric Difference": { + "module": "Default", + "description": "Get the symmetric difference of two sets", + "inputType": "string", + "outputType": "string", + "flowControl": false, + "args": [ + { + "name": "Sample delimiter", + "type": "binaryString", + "value": "\\n\\n" + }, + { + "name": "Item delimiter", + "type": "binaryString", + "value": "," + } + ] + }, "To Base32": { "module": "Default", "description": "Base32 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers. It uses a smaller set of characters than Base64, usually the uppercase alphabet and the numbers 2 to 7.", diff --git a/src/core/config/modules/Default.mjs b/src/core/config/modules/Default.mjs index 8a1496fa..e892720e 100644 --- a/src/core/config/modules/Default.mjs +++ b/src/core/config/modules/Default.mjs @@ -5,15 +5,18 @@ * @copyright Crown Copyright 2018 * @license Apache-2.0 */ +import CartesianProduct from "../../operations/CartesianProduct"; import FromBase32 from "../../operations/FromBase32"; import FromBase64 from "../../operations/FromBase64"; import FromHex from "../../operations/FromHex"; +import PowerSet from "../../operations/PowerSet"; import RawDeflate from "../../operations/RawDeflate"; import SetDifference from "../../operations/SetDifference"; import SetIntersection from "../../operations/SetIntersection"; import SetOps from "../../operations/SetOps"; import SetUnion from "../../operations/SetUnion"; import ShowBase64Offsets from "../../operations/ShowBase64Offsets"; +import SymmetricDifference from "../../operations/SymmetricDifference"; import ToBase32 from "../../operations/ToBase32"; import ToBase64 from "../../operations/ToBase64"; import ToHex from "../../operations/ToHex"; @@ -21,15 +24,18 @@ import ToHex from "../../operations/ToHex"; const OpModules = typeof self === "undefined" ? {} : self.OpModules || {}; OpModules.Default = { + "Cartesian Product": CartesianProduct, "From Base32": FromBase32, "From Base64": FromBase64, "From Hex": FromHex, + "Power Set": PowerSet, "Raw Deflate": RawDeflate, "Set Difference": SetDifference, "Set Intersection": SetIntersection, "": SetOps, "Set Union": SetUnion, "Show Base64 offsets": ShowBase64Offsets, + "Symmetric Difference": SymmetricDifference, "To Base32": ToBase32, "To Base64": ToBase64, "To Hex": ToHex, diff --git a/src/core/operations/CartesianProduct.mjs b/src/core/operations/CartesianProduct.mjs new file mode 100644 index 00000000..19365474 --- /dev/null +++ b/src/core/operations/CartesianProduct.mjs @@ -0,0 +1,84 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Utils from "../Utils"; +import Operation from "../Operation"; + +/** + * Set cartesian product operation + */ +class CartesianProduct extends Operation { + + /** + * Cartesian Product constructor + */ + constructor() { + super(); + + this.name = "Cartesian Product"; + this.module = "Default"; + this.description = "Get the cartesian product of two sets"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Sample delimiter", + type: "binaryString", + value: Utils.escapeHtml("\\n\\n") + }, + { + name: "Item delimiter", + type: "binaryString", + value: "," + }, + ]; + } + + /** + * Validate input length + * @param {Object[]} sets + * @throws {Error} if not two sets + */ + validateSampleNumbers(sets) { + if (!sets || (sets.length !== 2)) { + throw "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?"; + } + } + + /** + * Run the product operation + * @param input + * @param args + */ + run(input, args) { + [this.sampleDelim, this.itemDelimiter] = args; + const sets = input.split(this.sampleDelim); + + try { + this.validateSampleNumbers(sets); + } catch (e) { + return e; + } + + return Utils.escapeHtml(this.runCartesianProduct(...sets.map(s => s.split(this.itemDelimiter)))); + } + + /** + * Return the cartesian product of the two inputted sets. + * + * @param {Object[]} a + * @param {Object[]} b + * @returns {String[]} + */ + runCartesianProduct(a, b) { + return Array(Math.max(a.length, b.length)) + .fill(null) + .map((item, index) => `(${a[index] || undefined},${b[index] || undefined})`) + .join(this.itemDelimiter); + } +} + +export default CartesianProduct; diff --git a/src/core/operations/PowerSet.mjs b/src/core/operations/PowerSet.mjs new file mode 100644 index 00000000..e40e3027 --- /dev/null +++ b/src/core/operations/PowerSet.mjs @@ -0,0 +1,91 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Utils from "../Utils"; +import Operation from "../Operation"; + +/** + * Power Set operation + */ +class PowerSet extends Operation { + + /** + * Power set constructor + */ + constructor() { + super(); + + this.name = "Power Set"; + this.module = "Default"; + this.description = "Generate the power set of a set"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Item delimiter", + type: "binaryString", + value: "," + }, + ]; + } + + /** + * Generate the power set + * @param input + * @param args + */ + run(input, args) { + [this.itemDelimiter] = args; + // Split and filter empty strings + const inputArray = input.split(this.itemDelimiter).filter(a => a); + + if (inputArray.length) { + return Utils.escapeHtml(this.runPowerSet(inputArray)); + } + + return ""; + } + + /** + * Return the power set of the inputted set. + * + * @param {Object[]} a + * @returns {Object[]} + */ + runPowerSet(a) { + // empty array items getting picked up + a = a.filter(i => i.length); + if (!a.length) { + return []; + } + + /** + * Decimal to binary function + * @param {*} dec + */ + const toBinary = (dec) => (dec >>> 0).toString(2); + const result = new Set(); + // Get the decimal number to make a binary as long as the input + const maxBinaryValue = parseInt(Number(a.map(i => "1").reduce((p, c) => p + c)), 2); + // Make an array of each binary number from 0 to maximum + const binaries = [...Array(maxBinaryValue + 1).keys()] + .map(toBinary) + .map(i => i.padStart(toBinary(maxBinaryValue).length, "0")); + + // XOR the input with each binary to get each unique permutation + binaries.forEach((binary) => { + const split = binary.split(""); + result.add(a.filter((item, index) => split[index] === "1")); + }); + + // map for formatting & put in length order. + return [...result] + .map(r => r.join(this.itemDelimiter)).sort((a, b) => a.length - b.length) + .map(i => `${i}\n`).join(""); + } +} + +export default PowerSet; diff --git a/src/core/operations/SetDifference.mjs b/src/core/operations/SetDifference.mjs index 6b2d076f..653ef2d8 100644 --- a/src/core/operations/SetDifference.mjs +++ b/src/core/operations/SetDifference.mjs @@ -63,7 +63,7 @@ class SetDifference extends Operation { return e; } - return Utils.escapeHtml(this.runSetDifferencez(...sets.map(s => s.split(this.itemDelimiter)))); + return Utils.escapeHtml(this.runSetDifference(...sets.map(s => s.split(this.itemDelimiter)))); } /** diff --git a/src/core/operations/SymmetricDifference.mjs b/src/core/operations/SymmetricDifference.mjs new file mode 100644 index 00000000..09132802 --- /dev/null +++ b/src/core/operations/SymmetricDifference.mjs @@ -0,0 +1,97 @@ +/** + * @author d98762625 [d98762625@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Utils from "../Utils"; +import Operation from "../Operation"; + +/** + * Set Symmetric Difference operation + */ +class SymmetricDifference extends Operation { + + /** + * Symmetric Difference constructor + */ + constructor() { + super(); + + this.name = "Symmetric Difference"; + this.module = "Default"; + this.description = "Get the symmetric difference of two sets"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Sample delimiter", + type: "binaryString", + value: Utils.escapeHtml("\\n\\n") + }, + { + name: "Item delimiter", + type: "binaryString", + value: "," + }, + ]; + } + + /** + * Validate input length + * @param {Object[]} sets + * @throws {Error} if not two sets + */ + validateSampleNumbers(sets) { + if (!sets || (sets.length !== 2)) { + throw "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?"; + } + } + + /** + * Run the difference operation + * @param input + * @param args + */ + run(input, args) { + [this.sampleDelim, this.itemDelimiter] = args; + const sets = input.split(this.sampleDelim); + + try { + this.validateSampleNumbers(sets); + } catch (e) { + return e; + } + + return Utils.escapeHtml(this.runSymmetricDifference(...sets.map(s => s.split(this.itemDelimiter)))); + } + + /** + * Get elements in set a that are not in set b + * + * @param {Object[]} a + * @param {Object[]} b + * @returns {Object[]} + */ + runSetDifference(a, b) { + return a.filter((item) => { + return b.indexOf(item) === -1; + }); + } + + /** + * Get elements of each set that aren't in the other set. + * + * @param {Object[]} a + * @param {Object[]} b + * @return {Object[]} + */ + runSymmetricDifference(a, b) { + return this.runSetDifference(a, b) + .concat(this.runSetDifference(b, a)) + .join(this.itemDelimiter); + } + +} + +export default SymmetricDifference; diff --git a/src/core/operations/index.mjs b/src/core/operations/index.mjs index fce3dd9b..e84459e4 100644 --- a/src/core/operations/index.mjs +++ b/src/core/operations/index.mjs @@ -5,11 +5,13 @@ * @copyright Crown Copyright 2018 * @license Apache-2.0 */ +import CartesianProduct from "./CartesianProduct"; import FromBase32 from "./FromBase32"; import FromBase64 from "./FromBase64"; import FromHex from "./FromHex"; import Gunzip from "./Gunzip"; import Gzip from "./Gzip"; +import PowerSet from "./PowerSet"; import RawDeflate from "./RawDeflate"; import RawInflate from "./RawInflate"; import SetDifference from "./SetDifference"; @@ -17,6 +19,7 @@ import SetIntersection from "./SetIntersection"; import SetOps from "./SetOps"; import SetUnion from "./SetUnion"; import ShowBase64Offsets from "./ShowBase64Offsets"; +import SymmetricDifference from "./SymmetricDifference"; import ToBase32 from "./ToBase32"; import ToBase64 from "./ToBase64"; import ToHex from "./ToHex"; @@ -26,11 +29,13 @@ import ZlibDeflate from "./ZlibDeflate"; import ZlibInflate from "./ZlibInflate"; export { + CartesianProduct, FromBase32, FromBase64, FromHex, Gunzip, Gzip, + PowerSet, RawDeflate, RawInflate, SetDifference, @@ -38,6 +43,7 @@ export { SetOps, SetUnion, ShowBase64Offsets, + SymmetricDifference, ToBase32, ToBase64, ToHex, diff --git a/test/index.mjs b/test/index.mjs index fdcd3102..d1390270 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -50,6 +50,9 @@ import "./tests/operations/Base64"; import "./tests/operations/SetUnion"; import "./tests/operations/SetIntersection"; import "./tests/operations/SetDifference"; +import "./tests/operations/SymmetricDifference"; +import "./tests/operations/CartesianProduct"; +import "./tests/operations/PowerSet"; let allTestsPassing = true; const testStatusCounts = { diff --git a/test/tests/operations/CartesianProduct.mjs b/test/tests/operations/CartesianProduct.mjs new file mode 100644 index 00000000..d5505b2f --- /dev/null +++ b/test/tests/operations/CartesianProduct.mjs @@ -0,0 +1,78 @@ +/** + * Cartesian Product tests. + * + * @author d98762625 + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister"; + +TestRegister.addTests([ + { + name: "Cartesian Product", + input: "1 2 3 4 5\n\na b c d e", + expectedOutput: "(1,a) (2,b) (3,c) (4,d) (5,e)", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Cartesian Product: wrong sample count", + input: "1 2\n\n3 4 5\n\na b c d e", + expectedOutput: "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Cartesian Product: too many on left", + input: "1 2 3 4 5 6\n\na b c d e", + expectedOutput: "(1,a) (2,b) (3,c) (4,d) (5,e) (6,undefined)", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Cartesian Product: too many on right", + input: "1 2 3 4 5\n\na b c d e f", + expectedOutput: "(1,a) (2,b) (3,c) (4,d) (5,e) (undefined,f)", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Cartesian Product: item delimiter", + input: "1-2-3-4-5\n\na-b-c-d-e", + expectedOutput: "(1,a)-(2,b)-(3,c)-(4,d)-(5,e)", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["\n\n", "-"], + }, + ], + }, + { + name: "Cartesian Product: sample delimiter", + input: "1 2 3 4 5_a b c d e", + expectedOutput: "(1,a) (2,b) (3,c) (4,d) (5,e)", + recipeConfig: [ + { + op: "Cartesian Product", + args: ["_", " "], + }, + ], + }, +]); diff --git a/test/tests/operations/PowerSet.mjs b/test/tests/operations/PowerSet.mjs new file mode 100644 index 00000000..f3fffed4 --- /dev/null +++ b/test/tests/operations/PowerSet.mjs @@ -0,0 +1,34 @@ +/** + * Power Set tests. + * + * @author d98762625 + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister"; + +TestRegister.addTests([ + { + name: "Power set: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Power Set", + args: [","], + }, + ], + }, + { + name: "Power set", + input: "1 2 4", + expectedOutput: "\n4\n2\n1\n2 4\n1 4\n1 2\n1 2 4\n", + recipeConfig: [ + { + op: "Power Set", + args: [" "], + }, + ], + }, +]); diff --git a/test/tests/operations/SymmetricDifference.mjs b/test/tests/operations/SymmetricDifference.mjs new file mode 100644 index 00000000..a2ef1562 --- /dev/null +++ b/test/tests/operations/SymmetricDifference.mjs @@ -0,0 +1,56 @@ +/** + * Symmetric difference tests. + * + * @author d98762625 + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister"; + +TestRegister.addTests([ + { + name: "Symmetric Difference", + input: "1 2 3 4 5\n\n3 4 5 6 7", + expectedOutput: "1 2 6 7", + recipeConfig: [ + { + op: "Symmetric Difference", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Symmetric Difference: wrong sample count", + input: "1 2\n\n3 4 5\n\n3 4 5 6 7", + expectedOutput: "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?", + recipeConfig: [ + { + op: "Symmetric Difference", + args: ["\n\n", " "], + }, + ], + }, + { + name: "Symmetric Difference: item delimiter", + input: "a_b_c_d_e\n\nc_d_e_f_g", + expectedOutput: "a_b_f_g", + recipeConfig: [ + { + op: "Symmetric Difference", + args: ["\n\n", "_"], + }, + ], + }, + { + name: "Symmetric Difference: sample delimiter", + input: "a_b_c_d_eAAAAAc_d_e_f_g", + expectedOutput: "a_b_f_g", + recipeConfig: [ + { + op: "Symmetric Difference", + args: ["AAAAA", "_"], + }, + ], + }, +]);