Sfoglia il codice sorgente

Merge branch 'd98762625-set-operations' into esm

n1474335 7 anni fa
parent
commit
23f69bd21d

+ 5 - 1
.gitignore

@@ -5,4 +5,8 @@ build
 docs/*
 !docs/*.conf.json
 !docs/*.ico
-.vscode
+.vscode
+src/core/config/modules/*
+src/core/config/OperationConfig.json
+src/core/operations/index.mjs
+

+ 1 - 1
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}"],

+ 2 - 1
package.json

@@ -117,6 +117,7 @@
     "start": "grunt dev",
     "build": "grunt prod",
     "test": "grunt test",
-    "docs": "grunt docs"
+    "docs": "grunt docs",
+    "lint": "grunt lint"
   }
 }

+ 5 - 3
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;

+ 6 - 0
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",

+ 0 - 537
src/core/config/OperationConfig.json

@@ -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.<br><br>This operation decodes data from an ASCII Base64 string back into its raw format.<br><br>e.g. <code>aGVsbG8=</code> becomes <code>hello</code>",
-        "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.<br><br>e.g. <code>ce 93 ce b5 ce b9 ce ac 20 cf 83 ce bf cf 85 0a</code> becomes the UTF-8 encoded string <code>Γειά σου</code>",
-        "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.<br><br>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.<br><br>This operation decodes data from an ASCII Base64 string back into its raw format.<br><br>e.g. <code>aGVsbG8=</code> becomes <code>hello</code>",
-        "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.<br><br>e.g. The UTF-8 encoded string <code>Γειά σου</code> becomes <code>ce 93 ce b5 ce b9 ce ac 20 cf 83 ce bf cf 85 0a</code>",
-        "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.<br><br>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
-            }
-        ]
-    }
-}

+ 0 - 30
src/core/config/modules/Compression.mjs

@@ -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;

+ 0 - 36
src/core/config/modules/Default.mjs

@@ -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;

+ 0 - 21
src/core/config/modules/OpModules.mjs

@@ -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;

+ 97 - 0
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;

+ 92 - 0
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;

+ 88 - 0
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;

+ 88 - 0
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;

+ 98 - 0
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;

+ 100 - 0
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;

+ 0 - 48
src/core/operations/index.mjs

@@ -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,
-};

+ 6 - 1
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 = {

+ 67 - 0
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: ["_", " "],
+            },
+        ],
+    },
+]);

+ 34 - 0
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: [" "],
+            },
+        ],
+    },
+]);

+ 56 - 0
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: ["===", ";"],
+            },
+        ],
+    },
+]);

+ 56 - 0
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", "-"],
+            },
+        ],
+    }
+]);

+ 67 - 0
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", " "],
+            },
+        ],
+    },
+]);

+ 56 - 0
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", "_"],
+            },
+        ],
+    },
+]);