diff --git a/.gitignore b/.gitignore
index 31b15bd9..9ea869e3 100755
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,8 @@ build
docs/*
!docs/*.conf.json
!docs/*.ico
-.vscode
\ No newline at end of file
+.vscode
+src/core/config/modules/*
+src/core/config/OperationConfig.json
+src/core/operations/index.mjs
+
diff --git a/Gruntfile.js b/Gruntfile.js
index eb66cd95..57d36c2e 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -142,7 +142,7 @@ module.exports = function (grunt) {
configFile: "./.eslintrc.json"
},
configs: ["Gruntfile.js"],
- core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*"],
+ core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
web: ["src/web/**/*.{js,mjs}"],
node: ["src/node/**/*.{js,mjs}"],
tests: ["test/**/*.{js,mjs}"],
diff --git a/package.json b/package.json
index 119d915b..b0632b25 100644
--- a/package.json
+++ b/package.json
@@ -117,6 +117,7 @@
"start": "grunt dev",
"build": "grunt prod",
"test": "grunt test",
- "docs": "grunt docs"
+ "docs": "grunt docs",
+ "lint": "grunt lint"
}
}
diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs
index 14792157..006e431c 100755
--- a/src/core/Recipe.mjs
+++ b/src/core/Recipe.mjs
@@ -189,9 +189,11 @@ class Recipe {
}
// Present the results of the final operation
- // TODO try/catch
- output = await lastRunOp.present(output);
- dish.set(output, lastRunOp.presentType);
+ if (lastRunOp) {
+ // TODO try/catch
+ output = await lastRunOp.present(output);
+ dish.set(output, lastRunOp.presentType);
+ }
log.debug("Recipe complete");
return this.opList.length;
diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js
index b74e932a..70e4ea4b 100755
--- a/src/core/config/Categories.js
+++ b/src/core/config/Categories.js
@@ -119,6 +119,12 @@ const Categories = [
{
name: "Arithmetic / Logic",
ops: [
+ "Set Union",
+ "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
deleted file mode 100644
index 2f18363d..00000000
--- a/src/core/config/OperationConfig.json
+++ /dev/null
@@ -1,537 +0,0 @@
-{
- "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.",
- "inputType": "string",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Alphabet",
- "type": "binaryString",
- "value": "A-Z2-7="
- },
- {
- "name": "Remove non-alphabet chars",
- "type": "boolean",
- "value": true
- }
- ]
- },
- "From Base64": {
- "module": "Default",
- "description": "Base64 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.
This operation decodes data from an ASCII Base64 string back into its raw format.
e.g. aGVsbG8=
becomes hello
",
- "inputType": "string",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Alphabet",
- "type": "editableOption",
- "value": [
- {
- "name": "Standard: A-Za-z0-9+/=",
- "value": "A-Za-z0-9+/="
- },
- {
- "name": "URL safe: A-Za-z0-9-_",
- "value": "A-Za-z0-9-_"
- },
- {
- "name": "Filename safe: A-Za-z0-9+-=",
- "value": "A-Za-z0-9+\\-="
- },
- {
- "name": "itoa64: ./0-9A-Za-z=",
- "value": "./0-9A-Za-z="
- },
- {
- "name": "XML: A-Za-z0-9_.",
- "value": "A-Za-z0-9_."
- },
- {
- "name": "y64: A-Za-z0-9._-",
- "value": "A-Za-z0-9._-"
- },
- {
- "name": "z64: 0-9a-zA-Z+/=",
- "value": "0-9a-zA-Z+/="
- },
- {
- "name": "Radix-64: 0-9A-Za-z+/=",
- "value": "0-9A-Za-z+/="
- },
- {
- "name": "Uuencoding: [space]-_",
- "value": " -_"
- },
- {
- "name": "Xxencoding: +-0-9A-Za-z",
- "value": "+\\-0-9A-Za-z"
- },
- {
- "name": "BinHex: !-,-0-689@A-NP-VX-Z[`a-fh-mp-r",
- "value": "!-,-0-689@A-NP-VX-Z[`a-fh-mp-r"
- },
- {
- "name": "ROT13: N-ZA-Mn-za-m0-9+/=",
- "value": "N-ZA-Mn-za-m0-9+/="
- },
- {
- "name": "UNIX crypt: ./0-9A-Za-z",
- "value": "./0-9A-Za-z"
- }
- ]
- },
- {
- "name": "Remove non-alphabet chars",
- "type": "boolean",
- "value": true
- }
- ]
- },
- "From Hex": {
- "module": "Default",
- "description": "Converts a hexadecimal byte string back into its raw value.
e.g. ce 93 ce b5 ce b9 ce ac 20 cf 83 ce bf cf 85 0a
becomes the UTF-8 encoded string Γειά σου
",
- "inputType": "string",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Delimiter",
- "type": "option",
- "value": [
- "Space",
- "Comma",
- "Semi-colon",
- "Colon",
- "Line feed",
- "CRLF",
- "0x",
- "\\x",
- "None"
- ]
- }
- ]
- },
- "Gunzip": {
- "module": "Compression",
- "description": "Decompresses data which has been compressed using the deflate algorithm with gzip headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": []
- },
- "Gzip": {
- "module": "Compression",
- "description": "Compresses data using the deflate algorithm with gzip headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Compression type",
- "type": "option",
- "value": [
- "Dynamic Huffman Coding",
- "Fixed Huffman Coding",
- "None (Store)"
- ]
- },
- {
- "name": "Filename (optional)",
- "type": "string",
- "value": ""
- },
- {
- "name": "Comment (optional)",
- "type": "string",
- "value": ""
- },
- {
- "name": "Include file checksum",
- "type": "boolean",
- "value": false
- }
- ]
- },
- "ROT13": {
- "module": "Default",
- "description": "A simple caesar substitution cipher which rotates alphabet characters by the specified amount (default 13).",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Rotate lower case chars",
- "type": "boolean",
- "value": true
- },
- {
- "name": "Rotate upper case chars",
- "type": "boolean",
- "value": true
- },
- {
- "name": "Amount",
- "type": "number",
- "value": 13
- }
- ]
- },
- "ROT47": {
- "module": "Default",
- "description": "A slightly more complex variation of a caesar cipher, which includes ASCII characters from 33 '!' to 126 '~'. Default rotation: 47.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Amount",
- "type": "number",
- "value": 47
- }
- ]
- },
- "Raw Deflate": {
- "module": "Compression",
- "description": "Compresses data using the deflate algorithm with no headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Compression type",
- "type": "option",
- "value": [
- "Dynamic Huffman Coding",
- "Fixed Huffman Coding",
- "None (Store)"
- ]
- }
- ]
- },
- "Raw Inflate": {
- "module": "Compression",
- "description": "Decompresses data which has been compressed using the deflate algorithm with no headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Start index",
- "type": "number",
- "value": 0
- },
- {
- "name": "Initial output buffer size",
- "type": "number",
- "value": 0
- },
- {
- "name": "Buffer expansion type",
- "type": "option",
- "value": [
- "Adaptive",
- "Block"
- ]
- },
- {
- "name": "Resize buffer after decompression",
- "type": "boolean",
- "value": false
- },
- {
- "name": "Verify result",
- "type": "boolean",
- "value": false
- }
- ]
- },
- "Rotate left": {
- "module": "Default",
- "description": "Rotates each byte to the left by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Amount",
- "type": "number",
- "value": 1
- },
- {
- "name": "Carry through",
- "type": "boolean",
- "value": false
- }
- ]
- },
- "Rotate right": {
- "module": "Default",
- "description": "Rotates each byte to the right by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Amount",
- "type": "number",
- "value": 1
- },
- {
- "name": "Carry through",
- "type": "boolean",
- "value": false
- }
- ]
- },
- "Show Base64 offsets": {
- "module": "Default",
- "description": "When a string is within a block of data and the whole block is Base64'd, the string itself could be represented in Base64 in three distinct ways depending on its offset within the block.
This operation shows all possible offsets for a given string so that each possible encoding can be considered.",
- "inputType": "byteArray",
- "outputType": "html",
- "flowControl": false,
- "args": [
- {
- "name": "Alphabet",
- "type": "binaryString",
- "value": "A-Za-z0-9+/="
- },
- {
- "name": "Show variable chars and padding",
- "type": "boolean",
- "value": true
- }
- ]
- },
- "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.",
- "inputType": "byteArray",
- "outputType": "string",
- "flowControl": false,
- "args": [
- {
- "name": "Alphabet",
- "type": "binaryString",
- "value": "A-Z2-7="
- }
- ]
- },
- "To Base64": {
- "module": "Default",
- "description": "Base64 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.
This operation decodes data from an ASCII Base64 string back into its raw format.
e.g. aGVsbG8=
becomes hello
",
- "inputType": "ArrayBuffer",
- "outputType": "string",
- "flowControl": false,
- "args": [
- {
- "name": "Alphabet",
- "type": "editableOption",
- "value": [
- {
- "name": "Standard: A-Za-z0-9+/=",
- "value": "A-Za-z0-9+/="
- },
- {
- "name": "URL safe: A-Za-z0-9-_",
- "value": "A-Za-z0-9-_"
- },
- {
- "name": "Filename safe: A-Za-z0-9+-=",
- "value": "A-Za-z0-9+\\-="
- },
- {
- "name": "itoa64: ./0-9A-Za-z=",
- "value": "./0-9A-Za-z="
- },
- {
- "name": "XML: A-Za-z0-9_.",
- "value": "A-Za-z0-9_."
- },
- {
- "name": "y64: A-Za-z0-9._-",
- "value": "A-Za-z0-9._-"
- },
- {
- "name": "z64: 0-9a-zA-Z+/=",
- "value": "0-9a-zA-Z+/="
- },
- {
- "name": "Radix-64: 0-9A-Za-z+/=",
- "value": "0-9A-Za-z+/="
- },
- {
- "name": "Uuencoding: [space]-_",
- "value": " -_"
- },
- {
- "name": "Xxencoding: +-0-9A-Za-z",
- "value": "+\\-0-9A-Za-z"
- },
- {
- "name": "BinHex: !-,-0-689@A-NP-VX-Z[`a-fh-mp-r",
- "value": "!-,-0-689@A-NP-VX-Z[`a-fh-mp-r"
- },
- {
- "name": "ROT13: N-ZA-Mn-za-m0-9+/=",
- "value": "N-ZA-Mn-za-m0-9+/="
- },
- {
- "name": "UNIX crypt: ./0-9A-Za-z",
- "value": "./0-9A-Za-z"
- }
- ]
- }
- ]
- },
- "To Hex": {
- "module": "Default",
- "description": "Converts the input string to hexadecimal bytes separated by the specified delimiter.
e.g. The UTF-8 encoded string Γειά σου
becomes ce 93 ce b5 ce b9 ce ac 20 cf 83 ce bf cf 85 0a
",
- "inputType": "ArrayBuffer",
- "outputType": "string",
- "flowControl": false,
- "args": [
- {
- "name": "Delimiter",
- "type": "option",
- "value": [
- "Space",
- "Comma",
- "Semi-colon",
- "Colon",
- "Line feed",
- "CRLF",
- "0x",
- "\\x",
- "None"
- ]
- }
- ]
- },
- "Unzip": {
- "module": "Compression",
- "description": "Decompresses data using the PKZIP algorithm and displays it per file, with support for passwords.",
- "inputType": "byteArray",
- "outputType": "html",
- "flowControl": false,
- "args": [
- {
- "name": "Password",
- "type": "binaryString",
- "value": ""
- },
- {
- "name": "Verify result",
- "type": "boolean",
- "value": false
- }
- ]
- },
- "Zip": {
- "module": "Compression",
- "description": "Compresses data using the PKZIP algorithm with the given filename.
No support for multiple files at this time.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Filename",
- "type": "string",
- "value": "file.txt"
- },
- {
- "name": "Comment",
- "type": "string",
- "value": ""
- },
- {
- "name": "Password",
- "type": "binaryString",
- "value": ""
- },
- {
- "name": "Compression method",
- "type": "option",
- "value": [
- "Deflate",
- "None (Store)"
- ]
- },
- {
- "name": "Operating system",
- "type": "option",
- "value": [
- "MSDOS",
- "Unix",
- "Macintosh"
- ]
- },
- {
- "name": "Compression type",
- "type": "option",
- "value": [
- "Dynamic Huffman Coding",
- "Fixed Huffman Coding",
- "None (Store)"
- ]
- }
- ]
- },
- "Zlib Deflate": {
- "module": "Compression",
- "description": "Compresses data using the deflate algorithm adding zlib headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Compression type",
- "type": "option",
- "value": [
- "Dynamic Huffman Coding",
- "Fixed Huffman Coding",
- "None (Store)"
- ]
- }
- ]
- },
- "Zlib Inflate": {
- "module": "Compression",
- "description": "Decompresses data which has been compressed using the deflate algorithm with zlib headers.",
- "inputType": "byteArray",
- "outputType": "byteArray",
- "flowControl": false,
- "args": [
- {
- "name": "Start index",
- "type": "number",
- "value": 0
- },
- {
- "name": "Initial output buffer size",
- "type": "number",
- "value": 0
- },
- {
- "name": "Buffer expansion type",
- "type": "option",
- "value": [
- "Adaptive",
- "Block"
- ]
- },
- {
- "name": "Resize buffer after decompression",
- "type": "boolean",
- "value": false
- },
- {
- "name": "Verify result",
- "type": "boolean",
- "value": false
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/src/core/config/modules/Compression.mjs b/src/core/config/modules/Compression.mjs
deleted file mode 100644
index 61b2ed2d..00000000
--- a/src/core/config/modules/Compression.mjs
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
-* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateConfig.mjs
-*
-* @author n1474335 [n1474335@gmail.com]
-* @copyright Crown Copyright 2018
-* @license Apache-2.0
-*/
-import Gunzip from "../../operations/Gunzip";
-import Gzip from "../../operations/Gzip";
-import RawDeflate from "../../operations/RawDeflate";
-import RawInflate from "../../operations/RawInflate";
-import Unzip from "../../operations/Unzip";
-import Zip from "../../operations/Zip";
-import ZlibDeflate from "../../operations/ZlibDeflate";
-import ZlibInflate from "../../operations/ZlibInflate";
-
-const OpModules = typeof self === "undefined" ? {} : self.OpModules || {};
-
-OpModules.Compression = {
- "Gunzip": Gunzip,
- "Gzip": Gzip,
- "Raw Deflate": RawDeflate,
- "Raw Inflate": RawInflate,
- "Unzip": Unzip,
- "Zip": Zip,
- "Zlib Deflate": ZlibDeflate,
- "Zlib Inflate": ZlibInflate,
-};
-
-export default OpModules;
diff --git a/src/core/config/modules/Default.mjs b/src/core/config/modules/Default.mjs
deleted file mode 100644
index 8648aa8a..00000000
--- a/src/core/config/modules/Default.mjs
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
-* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateConfig.mjs
-*
-* @author n1474335 [n1474335@gmail.com]
-* @copyright Crown Copyright 2018
-* @license Apache-2.0
-*/
-import FromBase32 from "../../operations/FromBase32";
-import FromBase64 from "../../operations/FromBase64";
-import FromHex from "../../operations/FromHex";
-import ROT13 from "../../operations/ROT13";
-import ROT47 from "../../operations/ROT47";
-import RotateLeft from "../../operations/RotateLeft";
-import RotateRight from "../../operations/RotateRight";
-import ShowBase64Offsets from "../../operations/ShowBase64Offsets";
-import ToBase32 from "../../operations/ToBase32";
-import ToBase64 from "../../operations/ToBase64";
-import ToHex from "../../operations/ToHex";
-
-const OpModules = typeof self === "undefined" ? {} : self.OpModules || {};
-
-OpModules.Default = {
- "From Base32": FromBase32,
- "From Base64": FromBase64,
- "From Hex": FromHex,
- "ROT13": ROT13,
- "ROT47": ROT47,
- "Rotate left": RotateLeft,
- "Rotate right": RotateRight,
- "Show Base64 offsets": ShowBase64Offsets,
- "To Base32": ToBase32,
- "To Base64": ToBase64,
- "To Hex": ToHex,
-};
-
-export default OpModules;
diff --git a/src/core/config/modules/OpModules.mjs b/src/core/config/modules/OpModules.mjs
deleted file mode 100644
index 7c3f3b5e..00000000
--- a/src/core/config/modules/OpModules.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
-* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateConfig.mjs
-*
-* Imports all modules for builds which do not load modules separately.
-*
-* @author n1474335 [n1474335@gmail.com]
-* @copyright Crown Copyright 2018
-* @license Apache-2.0
-*/
-import DefaultModule from "./Default";
-import CompressionModule from "./Compression";
-
-const OpModules = {};
-
-Object.assign(
- OpModules,
- DefaultModule,
- CompressionModule,
-);
-
-export default OpModules;
diff --git a/src/core/operations/CartesianProduct.mjs b/src/core/operations/CartesianProduct.mjs
new file mode 100644
index 00000000..5cf546b0
--- /dev/null
+++ b/src/core/operations/CartesianProduct.mjs
@@ -0,0 +1,97 @@
+/**
+ * @author d98762625 [d98762625@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+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 = "Calculates the cartesian product of multiple sets of data, returning all possible combinations.";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Sample delimiter",
+ type: "binaryString",
+ value: "\\n\\n"
+ },
+ {
+ name: "Item delimiter",
+ type: "binaryString",
+ value: ","
+ },
+ ];
+ }
+
+ /**
+ * Validate input length
+ *
+ * @param {Object[]} sets
+ * @throws {Error} if fewer than 2 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 {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.sampleDelim, this.itemDelimiter] = args;
+ const sets = input.split(this.sampleDelim);
+
+ try {
+ this.validateSampleNumbers(sets);
+ } catch (e) {
+ return e;
+ }
+
+ return this.runCartesianProduct(...sets.map(s => s.split(this.itemDelimiter)));
+ }
+
+ /**
+ * Return the cartesian product of the two inputted sets.
+ *
+ * @param {Object[]} a
+ * @param {Object[]} b
+ * @param {Object[]} c
+ * @returns {string}
+ */
+ runCartesianProduct(a, b, ...c) {
+ /**
+ * https://stackoverflow.com/a/43053803/7200497
+ * @returns {Object[]}
+ */
+ const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e))));
+ /**
+ * https://stackoverflow.com/a/43053803/7200497
+ * @returns {Object[][]}
+ */
+ const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);
+
+ return cartesian(a, b, ...c)
+ .map(set => `(${set.join(",")})`)
+ .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..f3d32392
--- /dev/null
+++ b/src/core/operations/PowerSet.mjs
@@ -0,0 +1,92 @@
+/**
+ * @author d98762625 [d98762625@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+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 = "Calculates all the subsets of a set.";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Item delimiter",
+ type: "binaryString",
+ value: ","
+ },
+ ];
+ }
+
+ /**
+ * Generate the power set
+ *
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.itemDelimiter] = args;
+ // Split and filter empty strings
+ const inputArray = input.split(this.itemDelimiter).filter(a => a);
+
+ if (inputArray.length) {
+ return 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
new file mode 100644
index 00000000..4b0f3118
--- /dev/null
+++ b/src/core/operations/SetDifference.mjs
@@ -0,0 +1,88 @@
+/**
+ * @author d98762625 [d98762625@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Set Difference operation
+ */
+class SetDifference extends Operation {
+
+ /**
+ * Set Difference constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Set Difference";
+ this.module = "Default";
+ this.description = "Calculates the difference of two sets.";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Sample delimiter",
+ type: "binaryString",
+ value: "\\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 {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.sampleDelim, this.itemDelimiter] = args;
+ const sets = input.split(this.sampleDelim);
+
+ try {
+ this.validateSampleNumbers(sets);
+ } catch (e) {
+ return e;
+ }
+
+ return this.runSetDifference(...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;
+ })
+ .join(this.itemDelimiter);
+ }
+
+}
+
+export default SetDifference;
diff --git a/src/core/operations/SetIntersection.mjs b/src/core/operations/SetIntersection.mjs
new file mode 100644
index 00000000..e576be07
--- /dev/null
+++ b/src/core/operations/SetIntersection.mjs
@@ -0,0 +1,88 @@
+/**
+ * @author d98762625 [d98762625@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Set Intersection operation
+ */
+class SetIntersection extends Operation {
+
+ /**
+ * Set Intersection constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Set Intersection";
+ this.module = "Default";
+ this.description = "Calculates the intersection of two sets.";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Sample delimiter",
+ type: "binaryString",
+ value: "\\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 intersection operation
+ *
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.sampleDelim, this.itemDelimiter] = args;
+ const sets = input.split(this.sampleDelim);
+
+ try {
+ this.validateSampleNumbers(sets);
+ } catch (e) {
+ return e;
+ }
+
+ return this.runIntersect(...sets.map(s => s.split(this.itemDelimiter)));
+ }
+
+ /**
+ * Get the intersection of the two sets.
+ *
+ * @param {Object[]} a
+ * @param {Object[]} b
+ * @returns {Object[]}
+ */
+ runIntersect(a, b) {
+ return a
+ .filter((item) => {
+ return b.indexOf(item) > -1;
+ })
+ .join(this.itemDelimiter);
+ }
+
+}
+
+export default SetIntersection;
diff --git a/src/core/operations/SetUnion.mjs b/src/core/operations/SetUnion.mjs
new file mode 100644
index 00000000..56e12f4d
--- /dev/null
+++ b/src/core/operations/SetUnion.mjs
@@ -0,0 +1,98 @@
+/**
+ * @author d98762625 [d98762625@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Set Union operation
+ */
+class SetUnion extends Operation {
+
+ /**
+ * Set Union constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Set Union";
+ this.module = "Default";
+ this.description = "Calculates the union of two sets.";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Sample delimiter",
+ type: "binaryString",
+ value: "\\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 union operation
+ *
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.sampleDelim, this.itemDelimiter] = args;
+ const sets = input.split(this.sampleDelim);
+
+ try {
+ this.validateSampleNumbers(sets);
+ } catch (e) {
+ return e;
+ }
+
+ return this.runUnion(...sets.map(s => s.split(this.itemDelimiter)));
+ }
+
+ /**
+ * Get the union of the two sets.
+ *
+ * @param {Object[]} a
+ * @param {Object[]} b
+ * @returns {Object[]}
+ */
+ runUnion(a, b) {
+ const result = {};
+
+ /**
+ * Only add non-existing items
+ * @param {Object} hash
+ */
+ const addUnique = (hash) => (item) => {
+ if (!hash[item]) {
+ hash[item] = true;
+ }
+ };
+
+ a.map(addUnique(result));
+ b.map(addUnique(result));
+
+ return Object.keys(result).join(this.itemDelimiter);
+ }
+}
+
+export default SetUnion;
diff --git a/src/core/operations/SymmetricDifference.mjs b/src/core/operations/SymmetricDifference.mjs
new file mode 100644
index 00000000..bc2bb434
--- /dev/null
+++ b/src/core/operations/SymmetricDifference.mjs
@@ -0,0 +1,100 @@
+/**
+ * @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 = "Calculates 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 {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ [this.sampleDelim, this.itemDelimiter] = args;
+ const sets = input.split(this.sampleDelim);
+
+ try {
+ this.validateSampleNumbers(sets);
+ } catch (e) {
+ return e;
+ }
+
+ return 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
deleted file mode 100644
index 5b735963..00000000
--- a/src/core/operations/index.mjs
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
-* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateOpsIndex.mjs
-*
-* @author n1474335 [n1474335@gmail.com]
-* @copyright Crown Copyright 2018
-* @license Apache-2.0
-*/
-import FromBase32 from "./FromBase32";
-import FromBase64 from "./FromBase64";
-import FromHex from "./FromHex";
-import Gunzip from "./Gunzip";
-import Gzip from "./Gzip";
-import ROT13 from "./ROT13";
-import ROT47 from "./ROT47";
-import RawDeflate from "./RawDeflate";
-import RawInflate from "./RawInflate";
-import RotateLeft from "./RotateLeft";
-import RotateRight from "./RotateRight";
-import ShowBase64Offsets from "./ShowBase64Offsets";
-import ToBase32 from "./ToBase32";
-import ToBase64 from "./ToBase64";
-import ToHex from "./ToHex";
-import Unzip from "./Unzip";
-import Zip from "./Zip";
-import ZlibDeflate from "./ZlibDeflate";
-import ZlibInflate from "./ZlibInflate";
-
-export {
- FromBase32,
- FromBase64,
- FromHex,
- Gunzip,
- Gzip,
- ROT13,
- ROT47,
- RawDeflate,
- RawInflate,
- RotateLeft,
- RotateRight,
- ShowBase64Offsets,
- ToBase32,
- ToBase64,
- ToHex,
- Unzip,
- Zip,
- ZlibDeflate,
- ZlibInflate,
-};
diff --git a/test/index.mjs b/test/index.mjs
index 82b3c081..dd592f6f 100644
--- a/test/index.mjs
+++ b/test/index.mjs
@@ -48,7 +48,12 @@ import "./tests/operations/Base64";
import "./tests/operations/Rotate.mjs";
// import "./tests/operations/StrUtils.js";
// import "./tests/operations/SeqUtils.js";
-
+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..aafdb8b6
--- /dev/null
+++ b/test/tests/operations/CartesianProduct.mjs
@@ -0,0 +1,67 @@
+/**
+ * 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) (1,b) (1,c) (1,d) (1,e) (2,a) (2,b) (2,c) (2,d) (2,e) (3,a) (3,b) (3,c) (3,d) (3,e) (4,a) (4,b) (4,c) (4,d) (4,e) (5,a) (5,b) (5,c) (5,d) (5,e)",
+ 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) (1,b) (1,c) (1,d) (1,e) (2,a) (2,b) (2,c) (2,d) (2,e) (3,a) (3,b) (3,c) (3,d) (3,e) (4,a) (4,b) (4,c) (4,d) (4,e) (5,a) (5,b) (5,c) (5,d) (5,e) (6,a) (6,b) (6,c) (6,d) (6,e)",
+ 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) (1,b) (1,c) (1,d) (1,e) (1,f) (2,a) (2,b) (2,c) (2,d) (2,e) (2,f) (3,a) (3,b) (3,c) (3,d) (3,e) (3,f) (4,a) (4,b) (4,c) (4,d) (4,e) (4,f) (5,a) (5,b) (5,c) (5,d) (5,e) (5,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)-(1,b)-(1,c)-(1,d)-(1,e)-(2,a)-(2,b)-(2,c)-(2,d)-(2,e)-(3,a)-(3,b)-(3,c)-(3,d)-(3,e)-(4,a)-(4,b)-(4,c)-(4,d)-(4,e)-(5,a)-(5,b)-(5,c)-(5,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) (1,b) (1,c) (1,d) (1,e) (2,a) (2,b) (2,c) (2,d) (2,e) (3,a) (3,b) (3,c) (3,d) (3,e) (4,a) (4,b) (4,c) (4,d) (4,e) (5,a) (5,b) (5,c) (5,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/SetDifference.mjs b/test/tests/operations/SetDifference.mjs
new file mode 100644
index 00000000..3bc91d3f
--- /dev/null
+++ b/test/tests/operations/SetDifference.mjs
@@ -0,0 +1,56 @@
+/**
+ * Set Difference tests.
+ *
+ * @author d98762625
+ *
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister";
+
+TestRegister.addTests([
+ {
+ name: "Set Difference",
+ input: "1 2 3 4 5\n\n3 4 5 6 7",
+ expectedOutput: "1 2",
+ recipeConfig: [
+ {
+ op: "Set Difference",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Difference: wrong sample count",
+ input: "1 2 3 4 5_3_4 5 6 7",
+ expectedOutput: "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?",
+ recipeConfig: [
+ {
+ op: "Set Difference",
+ args: [" ", "_"],
+ },
+ ],
+ },
+ {
+ name: "Set Difference: item delimiter",
+ input: "1;2;3;4;5\n\n3;4;5;6;7",
+ expectedOutput: "1;2",
+ recipeConfig: [
+ {
+ op: "Set Difference",
+ args: ["\n\n", ";"],
+ },
+ ],
+ },
+ {
+ name: "Set Difference: sample delimiter",
+ input: "1;2;3;4;5===3;4;5;6;7",
+ expectedOutput: "1;2",
+ recipeConfig: [
+ {
+ op: "Set Difference",
+ args: ["===", ";"],
+ },
+ ],
+ },
+]);
diff --git a/test/tests/operations/SetIntersection.mjs b/test/tests/operations/SetIntersection.mjs
new file mode 100644
index 00000000..83809b6e
--- /dev/null
+++ b/test/tests/operations/SetIntersection.mjs
@@ -0,0 +1,56 @@
+/**
+ * Set Intersection tests.
+ *
+ * @author d98762625
+ *
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister";
+
+TestRegister.addTests([
+ {
+ name: "Set Intersection",
+ input: "1 2 3 4 5\n\n3 4 5 6 7",
+ expectedOutput: "3 4 5",
+ recipeConfig: [
+ {
+ op: "Set Intersection",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Intersection: only one set",
+ input: "1 2 3 4 5 6 7 8",
+ expectedOutput: "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?",
+ recipeConfig: [
+ {
+ op: "Set Intersection",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Intersection: item delimiter",
+ input: "1-2-3-4-5\n\n3-4-5-6-7",
+ expectedOutput: "3-4-5",
+ recipeConfig: [
+ {
+ op: "Set Intersection",
+ args: ["\n\n", "-"],
+ },
+ ],
+ },
+ {
+ name: "Set Intersection: sample delimiter",
+ input: "1-2-3-4-5z3-4-5-6-7",
+ expectedOutput: "3-4-5",
+ recipeConfig: [
+ {
+ op: "Set Intersection",
+ args: ["z", "-"],
+ },
+ ],
+ }
+]);
diff --git a/test/tests/operations/SetUnion.mjs b/test/tests/operations/SetUnion.mjs
new file mode 100644
index 00000000..e997b0d4
--- /dev/null
+++ b/test/tests/operations/SetUnion.mjs
@@ -0,0 +1,67 @@
+/**
+ * Set Union tests.
+ *
+ * @author d98762625
+ *
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister";
+
+TestRegister.addTests([
+ {
+ name: "Set Union: Nothing",
+ input: "\n\n",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ op: "Set Union",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Union",
+ input: "1 2 3 4 5\n\n3 4 5 6 7",
+ expectedOutput: "1 2 3 4 5 6 7",
+ recipeConfig: [
+ {
+ op: "Set Union",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Union: invalid sample number",
+ input: "1 2 3 4 5\n\n3 4 5 6 7\n\n1",
+ expectedOutput: "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?",
+ recipeConfig: [
+ {
+ op: "Set Union",
+ args: ["\n\n", " "],
+ },
+ ],
+ },
+ {
+ name: "Set Union: item delimiter",
+ input: "1,2,3,4,5\n\n3,4,5,6,7",
+ expectedOutput: "1,2,3,4,5,6,7",
+ recipeConfig: [
+ {
+ op: "Set Union",
+ args: ["\n\n", ","],
+ },
+ ],
+ },
+ {
+ name: "Set Union: sample delimiter",
+ input: "1 2 3 4 5whatever3 4 5 6 7",
+ expectedOutput: "1 2 3 4 5 6 7",
+ recipeConfig: [
+ {
+ op: "Set Union",
+ args: ["whatever", " "],
+ },
+ ],
+ },
+]);
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", "_"],
+ },
+ ],
+ },
+]);