diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 6f04267f..7fc999a1 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -331,6 +331,7 @@ const Categories = [ "Extract EXIF", "Numberwang", "XKCD Random Number", + "Set Operations" ] }, { diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 6a8f5d3b..d572d649 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -38,6 +38,7 @@ import StrUtils from "../operations/StrUtils.js"; import Tidy from "../operations/Tidy.js"; import Unicode from "../operations/Unicode.js"; import URL_ from "../operations/URL.js"; +import SetOps from "../operations/SetOperations.js"; /** @@ -4018,6 +4019,29 @@ const OperationConfig = { inputType: "string", outputType: "number", args: [] + }, + "Set Operations": { + module: "Default", + description: "Performs set operations", + inputType: "string", + outputType: "html", + args: [ + { + name: "Sample delimiter", + type: "binaryString", + value: SetOps.SAMPLE_DELIMITER + }, + { + name: "Item delimiter", + type: "binaryString", + value: SetOps.ITEM_DELIMITER + }, + { + name: "Operation", + type: "option", + value: SetOps.OPERATION + } + ] } }; diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js index d59b7b21..bbdafc25 100644 --- a/src/core/config/modules/Default.js +++ b/src/core/config/modules/Default.js @@ -30,6 +30,7 @@ import Tidy from "../../operations/Tidy.js"; import Unicode from "../../operations/Unicode.js"; import UUID from "../../operations/UUID.js"; import XKCD from "../../operations/XKCD.js"; +import SetOps from "../../operations/SetOperations.js"; /** @@ -164,6 +165,7 @@ OpModules.Default = { "Windows Filetime to UNIX Timestamp": Filetime.runFromFiletimeToUnix, "UNIX Timestamp to Windows Filetime": Filetime.runToFiletimeFromUnix, "XKCD Random Number": XKCD.runRandomNumber, + "Set Operations": SetOps.runSetOperation.bind(SetOps), /* diff --git a/src/core/operations/SetOperations.js b/src/core/operations/SetOperations.js new file mode 100644 index 00000000..c3f7caeb --- /dev/null +++ b/src/core/operations/SetOperations.js @@ -0,0 +1,181 @@ +import Utils from "../Utils.js"; + +/** + * + */ +class SetOps { + /** + * + */ + constructor() { + this._sampleDelimiter = "\\n\\n"; + this._operation = ["Union", "Intersection", "Set Difference", "Symmetric Difference", "Cartesian Product", "Power Set"]; + this._itemDelimiter = ","; + } + + /** + * + */ + get OPERATION() { + return this._operation; + } + + /** + * + */ + get SAMPLE_DELIMITER() { + return this._sampleDelimiter; + } + + /** + * + */ + get ITEM_DELIMITER() { + return this._itemDelimiter; + } + + + /** + * + * @param {*} input + * @param {*} args + */ + runSetOperation(input, args) { + const [sampleDelim, itemDelimiter, operation] = args; + const sets = input.split(sampleDelim); + + if (!sets || (sets.length !== 2 && operation !== "Power Set") || (sets.length !== 1 && operation === "Power Set")) { + return "Incorrect number of sets, perhaps you need to modify the sample delimiter or add more samples?"; + } + + if (this._operation.indexOf(operation) === -1) { + return "Invalid 'Operation' option."; + } + + let result = { + Union: this.runUnion, + Intersection: this.runIntersect, + "Set Difference": this.runSetDifference, + "Symmetric Difference": this.runSymmetricDifference, + "Cartesian Product": this.runCartesianProduct, + "Power Set": this.runPowerSet(itemDelimiter), + }[operation] + .apply(null, sets.map(s => s.split(itemDelimiter))); + + // Formatting issues due to the nested characteristics of power set. + if (operation === "Power Set") { + result = result.map(i => `${i}\n`).join(""); + } else { + result = result.join(itemDelimiter); + } + + return Utils.escapeHtml(result); + } + + /** + * + * @param {*} a + * @param {*} a + */ + runUnion(a, b) { + + const result = {}; + + /** + * + * @param {*} r + */ + const addUnique = (hash) => (item) => { + if (!hash[item]) { + hash[item] = true; + } + }; + + a.map(addUnique(result)); + b.map(addUnique(result)); + + return Object.keys(result); + } + + /** + * + * @param {*} a + * @param {*} b + */ + runIntersect(a, b) { + return a.filter((item) => { + return b.indexOf(item) > -1; + }); + } + + /** + * + * @param {*} a + * @param {*} b + */ + runSetDifference(a, b) { + return a.filter((item) => { + return b.indexOf(item) === -1; + }); + } + + /** + * + * @param {*} a + * @param {*} b + */ + runSymmetricDifference(a, b) { + /** + * + * @param {*} refArray + */ + const getDifference = (refArray) => (item) => { + return refArray.indexOf(item) === -1; + }; + + return a + .filter(getDifference(b)) + .concat(b.filter(getDifference(a))); + } + + /** + * + * @param {*} a + * @param {*} b + */ + runCartesianProduct(a, b) { + return Array(Math.max(a.length, b.length)) + .fill(null) + .map((item, index) => `(${a[index] || undefined},${b[index] || undefined})`); + } + + /** + * + * @param {*} a + */ + runPowerSet(delimiter) { + return function(a) { + /** + * + * @param {*} dec + */ + const toBinary = (dec) => (dec >>> 0).toString(2); + + const result = new Set(); + const maxBinaryValue = parseInt(Number(a.map(i => "1").reduce((p, c) => p + c)), 2); + const binaries = [...Array(maxBinaryValue + 1).keys()] + .map(toBinary) + .map(i => i.padStart(toBinary(maxBinaryValue).length, "0")); + + 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(delimiter)).sort((a, b) => a.length - b.length); + }; + } +} + +export default new SetOps(); \ No newline at end of file