-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Set operations #281
Set operations #281
Changes from 8 commits
7d15bfe
20e54a8
2c68be3
208cb05
e8bb9e2
951568c
f3610e7
e403adb
7ce1bf1
f491461
5f93c66
03ecaa8
852c95a
adc4f78
543dce5
76f27db
bbc580e
955a082
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -331,6 +331,7 @@ const Categories = [ | |
"Extract EXIF", | ||
"Numberwang", | ||
"XKCD Random Number", | ||
"Set Operations" | ||
] | ||
}, | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would make more sense to have separate operations for each function (i.e. one op for 'Union', one for 'Intersection', one for 'Set Difference'...). This is what we've done for the arithmetic ops ('Sum', 'Mean' etc.). |
||
module: "Default", | ||
description: "Performs set operations", | ||
inputType: "string", | ||
outputType: "html", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why this is |
||
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 | ||
} | ||
] | ||
} | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import Utils from "../Utils.js"; | ||
|
||
/** | ||
* Set operations. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd probably rename this file to |
||
* | ||
* @author d98762625 [d98762625@gmail.com] | ||
* @copyright Crown Copyright 2018 | ||
* @license APache-2.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo - should be a lowercase 'p' |
||
* | ||
* @namespace | ||
*/ | ||
class SetOps { | ||
|
||
/** | ||
* Set default options for operation | ||
*/ | ||
constructor() { | ||
this._sampleDelimiter = "\\n\\n"; | ||
this._operation = ["Union", "Intersection", "Set Difference", "Symmetric Difference", "Cartesian Product", "Power Set"]; | ||
this._itemDelimiter = ","; | ||
} | ||
|
||
/** | ||
* Get operations array | ||
* @returns {String[]} | ||
*/ | ||
get OPERATION() { | ||
return this._operation; | ||
} | ||
|
||
/** | ||
* Get sample delimiter | ||
* @returns {String} | ||
*/ | ||
get SAMPLE_DELIMITER() { | ||
return this._sampleDelimiter; | ||
} | ||
|
||
/** | ||
* Get item delimiter | ||
* @returns {String} | ||
*/ | ||
get ITEM_DELIMITER() { | ||
return this._itemDelimiter; | ||
} | ||
|
||
|
||
/** | ||
* Run the configured set operation. | ||
* | ||
* @param {String} input | ||
* @param {String[]} args | ||
* @returns {html} | ||
*/ | ||
runSetOperation(input, args) { | ||
const [sampleDelim, itemDelimiter, operation] = args; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat! I might start using this notation in other ops. |
||
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.bind(undefined, itemDelimiter), | ||
}[operation] | ||
.apply(this, 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); | ||
} | ||
|
||
/** | ||
* 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); | ||
} | ||
|
||
/** | ||
* 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; | ||
}); | ||
} | ||
|
||
/** | ||
* 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)); | ||
} | ||
|
||
/** | ||
* Return the cartesian product of the two inputted sets. | ||
* | ||
* @param {Object[]} a | ||
* @param {Object[]} b | ||
* @returns {String[]} | ||
*/ | ||
runCartesianProduct(a, b) { | ||
return Array(Math.max(a.length, b.length)) | ||
.fill(null) | ||
.map((item, index) => `(${a[index] || undefined},${b[index] || undefined})`); | ||
} | ||
|
||
/** | ||
* Return the power set of the inputted set. | ||
* | ||
* @param {Object[]} a | ||
* @returns {Object[]} | ||
*/ | ||
runPowerSet(delimiter, 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(delimiter)).sort((a, b) => a.length - b.length); | ||
} | ||
} | ||
|
||
export default new SetOps(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add these operations to the 'Arithmetic/Logic' category.