Skip to content
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

Merged
merged 18 commits into from
Apr 11, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/config/Categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ const Categories = [
"Extract EXIF",
"Numberwang",
"XKCD Random Number",
"Set Operations"
Copy link
Member

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.

]
},
{
Expand Down
24 changes: 24 additions & 0 deletions src/core/config/OperationConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";


/**
Expand Down Expand Up @@ -4018,6 +4019,29 @@ const OperationConfig = {
inputType: "string",
outputType: "number",
args: []
},
"Set Operations": {
Copy link
Member

Choose a reason for hiding this comment

The 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is html? It looks like your output is always just a simple string.

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
}
]
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/core/config/modules/Default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";


/**
Expand Down Expand Up @@ -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),


/*
Expand Down
202 changes: 202 additions & 0 deletions src/core/operations/SetOperations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import Utils from "../Utils.js";

/**
* Set operations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably rename this file to SetOps.js to fit with the convention (e.g. BitwiseOps.js)

*
* @author d98762625 [d98762625@gmail.com]
* @copyright Crown Copyright 2018
* @license APache-2.0
Copy link
Member

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The 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();
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import "./tests/operations/OTP.js";
import "./tests/operations/Regex.js";
import "./tests/operations/StrUtils.js";
import "./tests/operations/SeqUtils.js";
import "./tests/operations/SetOperations.js";


let allTestsPassing = true;
Expand Down
Loading