Google Spreadsheets: generate all combinations of 4 columns with 8 rows each - function

I'd like to create a function for Google Sheets that allows me to list all possible combinations of the numbers 1 to 8, concatenated 4 times (from 1111 to 8888, I think that is 8^4 = 4096).
(I add a screenshot for clarity).
So far I tried with:
=ArrayFormula(transpose(split(concatenate(A2:A9&B2:B9&C2:C9&D2:D9& char(9)),char(9))))
...but this gives me only 8 combinations: 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888.
I'm slightly bad at programming, specially with new languages, so any help is very much appreciated!

Here is a little custom function that creates all combinations of rows (it's just easier to write for rows):
function combinations(arr) {
return arr.reduce(function(prod, row) {
var out = [];
for (i in row) {
out = out.concat(prod.map(function(x) {
return x.concat(row[i]);
}));
}
return out;
}, [[]]);
}
Using it as =combinations(A2:D9) would create 4^8 combinations, each of length 8, and that is not what you want. But it's easy enough to transpose:
=combinations(transpose(A2:D9))
The above function returns combinations as a rectangular array, so in your example the output would be 4 columns wide. If you want to join the combinations in one cell (so the output is a single column), use this modified version:
function joincombinations(arr) {
return arr.reduce(function(prod, row) {
var out = [];
for (i in row) {
out = out.concat(prod.map(function(x) {
return x.concat(row[i]);
}));
}
return out;
}, [[]]).map(function(row) {
return row.join("");
});
}
Usage: =joincombinations(transpose(A2:D9))

Try
=arrayformula(if(row(A:A)>4096,"",int((row(A:A)-1)/512)+1&mod(int((row(A:A)-1)/64),8)+1&mod(int((row(A:A)-1)/8),8)+1&mod(int((row(A:A)-1)/1),8)+1))
(needs at least 4096 rows in the sheet).

Related

How to get a value after a character, from a string based on a text match using formula?

I got the following value:
tradicional;cropped$9$10;mullet$5$7
In cell A1, I can choose between tradicional, cropped and mullet. In cell A2, I pick 1, or 2.
If I pick cropped and 2, the value to be returned would be 10.
If I pick mullet and 1, the value to be returned would be 5.
If
I'd go for len and left, but I don't see how this is going to work using the matching criteria.
Here's a practical example: https://docs.google.com/spreadsheets/d/1dFzXmtKj15EzApTKUKv8yF7_mAIB1COPSgMLMMmFE4E/edit?usp=sharing
Appreciate your help.
Description
You can split the text string on semicolon ";" into 3 parts. The depending on which part you choose, you can split it on dollar sign "$" then you can get the "item" and return an integer. I leave it to you to figure out how to incorporate into your script.
Script (Test Case)
function makeAChoice() {
try {
console.log("You chose "+getChoice("cropped",2));
console.log("You chose "+getChoice("mullet",1));
console.log("You chose "+getChoice("somethingelse",1));
}
catch(err) {
console.log(err);
}
}
function getChoice(choice,item) {
try {
var text = "tradicional;cropped$9$10;mullet$5$7";
text = text.split(";");
text = text.filter( s => s.includes(choice) );
if( text.length < 1 ) throw "Error choice ["+choice+"] not found!";
text = text[0].split("$");
return parseInt(text[item]);
}
catch(err) {
console.log(err);
}
}
Console.log
8:32:38 AM Notice Execution started
8:32:38 AM Info You chose 10
8:32:38 AM Info You chose 5
8:32:38 AM Info Error choice [somethingelse] not found!
8:32:38 AM Info You chose undefined
8:32:38 AM Notice Execution completed
Reference
https://www.w3schools.com/jsref/jsref_split.asp
https://www.w3schools.com/jsref/jsref_filter.asp
https://www.w3schools.com/jsref/jsref_includes.asp
https://www.w3schools.com/jsref/jsref_parseint.asp
Per my comments to your original post, I feel that there is a lot we don't know about your bigger goal. But as you aren't able to provide that, this solution will work for your one exact example.
Place the following formula in C4:
=ArrayFormula(IFERROR(VLOOKUP(A4;SPLIT(FLATTEN(SPLIT(E4;";"));"$");B4+1;FALSE)))
(See the new sheet "Erik Help.")
The inner SPLIT splits the E4 string at every semicolon.
FLATTEN sends that all to one column.
The outer SPLIT then splits at each "$".
VLOOKUP can then try to find the Col-A term in the first column of the resulting virtual chart. If found, it will return the column value that matches the Col-B value + 1 (since column 1 of the virtual array is the labels, e.g., 'tradicional,' etc.).
If no match is found for both the Col-A and Col-B data, then IFERROR returns null.

Highcharts change value out of csv file before plotting as epoch time not in ms so it has to be multiplied by 1000

I found the posibillity to use a callback function after reading the csv file in the highcharts . But my knowledge ist not good enough to get the first value per line and multiply by 1000. I am starting from the code out of an example found: (https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/data/parsed/)
data: {
csv: document.getElementById('csv').innerHTML,
parsed: function (columns) {
// We want to keep the values since 1950 only
$.each(columns, function () {
// Keep the first item which is the series name, then remove the following 70
this.splice(1, 70);
});
}
},
I have to put my code instead of the "this.splice(1,70)" but no idea how.
My data look like:
Date,Temp,Press
1552417882, 25.54,980.23
1552417942, 25.60,980.19
1552418002, 25.60,980.17
1552418062, 25.58,980.14
1552418122, 25.53,980.12
1552418183, 25.48,980.06
1552418243, 25.48,980.06
1552418303, 25.47,980.04
1552418363, 25.47,980.02
And also the first line (description) should be unchanged.
Best regards
Albert
You should check the structure of the columns array and loop through the first inner array to recalculate the timestamp:
data: {
csv: document.getElementById('csv').innerHTML,
parsed: function(columns) {
columns[0].forEach(function(column, i) {
columns[0][i] *= 1000;
});
}
},
Live demo: https://jsfiddle.net/BlackLabel/zve9tsa8/
API Reference: https://api.highcharts.com/highcharts/data.parsed

How to add legend for a bar chart with different colors in dc.js?

Below is the code snippet for a barchart with colored bars:
var Dim2 = ndx.dimension(function(d){return [d.SNo, d.something ]});
var Group2 = Dim2.group().reduceSum(function(d){ return d.someId; });
var someColors = d3.scale.ordinal().domain(["a1","a2","a3","a4","a5","a6","a7","a8"])
.range(["#2980B9","#00FFFF","#008000","#FFC300","#FF5733","#D1AEF1","#C0C0C0","#000000"]);
barChart2
.height(250)
.width(1000)
.brushOn(false)
.mouseZoomable(true)
.x(d3.scale.linear().domain([600,800]))
.elasticY(false)
.dimension(Dim2)
.group(Group2)
.keyAccessor(function(d){ return d.key[0]; })
.valueAccessor(function(d){return d.value; })
.colors(someColors)
.colorAccessor(function(d){return d.key[1]; });
How do I add a legend to this chart?
Using composite keys in crossfilter is really tricky, and I don't recommend it unless you really need it.
Crossfilter only understands scalars, so even though you can produce dimension and group keys which are arrays, and retrieve them correctly, crossfilter is going to coerce those arrays to strings, and that can cause trouble.
Here, what is happening is that Group2.all() iterates over your data in string order, so you get keys in the order
[1, "a1"], [10, "a3"], [11, "a4"], [12, "a5"], [2, "a3"], ...
Without changing the shape of your data, one way around this is to sort the data in your legendables function:
barChart2.legendables = function() {
return Group2.all().sort((a,b) => a.key[0] - b.key[0])
.map(function(kv) {
return {
chart: barChart2,
name: kv.key[1],
color: barChart2.colors()(kv.key[1]) }; }) };
An unrelated problem is that dc.js takes the X domain very literally, so even though [1,12] contains all the values, the last bar was not shown because the right side ends right at 12 and the bar is drawn between 12 and 13.
So:
.x(d3.scale.linear().domain([1,13]))
Now the legend matches the data!
Fork of your fiddle (also with dc.css).
EDIT: Of course, you want the legend items unique, too. You can define uniq like this:
function uniq(a, kf) {
var seen = [];
return a.filter(x => seen[kf(x)] ? false : (seen[kf(x)] = true));
}
Adding a step to legendables:
barChart2.legendables = function() {
var vals = uniq(Group2.all(), kv => kv.key[1]),
sorted = vals.sort((a,b) => a.key[1] > b.key[1] ? 1 : -1);
// or in X order: sorted = vals.sort((a,b) => a.key[0] - b.key[0]);
return sorted.map(function(kv) {
return {
chart: barChart2,
name: kv.key[1],
color: barChart2.colors()(kv.key[1]) }; }) };
Note that we're sorting by the string value of d.something which lands in key[1]. As shown in the comment, sorting by x order (d.SNo, key[0]) is possible too. I wouldn't recommend sorting by y since that's a reduceSum.
Result, sorted and uniq'd:
New fiddle.

dc.js crossfilter without reduce

Is crossfilter manipulating my data?
Background
I have performed all the processing I need server side and just want to graph exactly what comes down the json pipe. So far I've get the graph working exactly how I want it to except for it seems my data is being manipulated.
Here's my crossfilter code:
ndx = crossfilter(rData);
runDimension = ndx.dimension(function (d) { return [+d.series, +d.norm_1]; });
runGroup = runDimension.group();
runGroup.reduceSum(function (d) { return d.value;});
Note: norm_1 is unique
Issues
Basically I'm noticing two issues:
I know for a fact that all my data will be between -1 and 1 (I've run several checks to test this), BUT when graphing it I see it dips down to -1.4 in some places.
My server sends exactly 1000 rows of data, but by breakpointing some of the dc.js code I can see it's only graphing 752 rows.
More Evidence
On my chart I've set the valueAccessor and added some checks to test the values going out of bounds, and I can see very clearly it goes out:
.valueAccessor(function (d) {
if (d.value > 1 || d.value < -1) {
console.log(d);
}
return d.value;
})
The data from the server requires a small amount formatting before going into crossfilter (it comes down as a table and needs to be split into series objects). I used this as an opportunity to test whether the data goes out of bounds, and I can see clearly it stays within bounds:
for (var i = 0; i < $scope.remoteData.rows.length; i++) {
for (var j = 0; j < $scope.remoteData.labels.length; j++) {
var label = $scope.remoteData.labels[j];
var value = $scope.remoteData.rows[i][label];
if (value > 1 || value < -1) {
console.log({
label: label,
i: i,
series: j,
norm_1: $scope.remoteData.rows[i].norm_1,
value: value,
});
}
rData.push({
series: j,
norm_1: $scope.remoteData.rows[i].norm_1,
value: value
})
}
}
Discussion
I suspect my problems have something to do with:
runGroup.reduceSum(function (d) { return d.value;});
Is this function adding together certain data points?
Sounds like you have some rows for which [+d.series, +d.norm_1] is not unique. And yes any rows with the same key will be added with reduceSum.
I'd suggest making your dimension key be something that's really unique.
If you don't have a unique key, with a little more work you could use the array indices themselves as the dimension key. It will mean you have to use both key and value accessors everywhere to look back in the original array.
Something like:
ndx = crossfilter(d3.range(0, rData.length));
runDimension = ndx.dimension(function(d) { return d; })
runGroup = runDimension.group().reduceSum(function(d) {
return rData[d].value;
})
chart.keyAccessor(function(kv) { return rData[kv.key].x; })
.valueAccessor(function(kv) { return rData[kv.key].y; })

Combining two items (recipe-like)

I'd like a user to be able to combine two items and if compatible will yield a new item. In this example, the item IDs will be saved as Strings.
I was wondering what the most efficient way to do this would be, while making sure that swapped order will always yield the same result, so the user could input the order:
item X + item Y = item Z
item Y + item X = item Z
I've tried using Dictionaries and Objects, but I just haven't been able to get anything to work. I've also tried some various libraries that include HashMap/HashSet but nothing is working. here's some pseduo-code:
itemRecipe1:HashSet = new HashSet();
itemRecipe1.add("2");//Add item with ID of 2
itemRecipe1.add("3");//Add item with ID of 3
inputRecipe:HashSet = new HashSet();
inputRecipe.add("3");//Add item with ID of 3 (swapped)
inputRecipe.add("2");//Add item with ID of 2 (swapped)
recipeList:HashMap = new HashMap();
receipeList.put(itemRecipe1, "11");//Recipe has been added, the result of the recipe should be item 11
//This should output as TRUE since the composition of itemRecipe1 and inputRecipe are the same, despite a different input order.
trace(receipeList.containsKey(inputRecipe));
If anyone has a solution for this issue, please elt me know as I am willing to implement any design I can get working. I just don't see how a Dictionary could work as the key order matters.
So you're trying to associate two or more objects with each other. The first thing you need is some primitive data you can use to represent each item uniquely, typically an ID. This should give you something like the following to begin with:
class Item {
public var _id:int;
public function Item(id:int) {
_id = id;
}
public function get id():int { return _id; }
}
Now you need some piece of data that establishes a relationship between multiple Items using this ID. That could be as simple as the following, with a little extra functionality thrown in to see if an input list of these IDs matches the relationship:
class ItemRelationship {
private var _items:Vector.<Item>;
public function ItemRelationship(items:Vector.<Item>) {
_items = items;
}
public function matches(ids:Vector.<int>):Boolean {
if (_items.length !== ids.length) {
return false;
}
for each (var item:Item in _items) {
var found:Boolean = false;
for each (var id:int in ids) {
if (item.id === id) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
public function get items():Vector.<Item> { return _items; }
}
This lets us do something like this, assuming we have a bunch of items (item1, item2, ...) with IDs.
var rel:ItemRelationship = new ItemRelationship(new <Item>[item1, item2]);
And then:
trace(rel.matches(new <int>[1,2])); // true
trace(rel.matches(new <int>[2,1])); // true
trace(rel.matches(new <int>[3,4])); // false
Now all we need is something that stores all of these relationships and lets us fetch one based on a list of input IDs:
class RelationshipCollection {
private var _relationships:Vector.<ItemRelationship>;
public function RelationshipCollection(relationships:Vector.<ItemRelationship>) {
_relationships = relationships;
}
public function find(ids:Vector.<int>):ItemRelationship {
for each(var relationship:ItemRelationship in _relationships) {
if (relationship.matches(ids)) return relationship;
}
return null;
}
}
Put a load of relationships in there:
var collection:RelationshipCollection = new RelationshipCollection(new <ItemRelationship>[
new ItemRelationship(new <Item>[item1, item4]),
new ItemRelationship(new <Item>[item2, item3])
]);
And give it a whirl:
trace(collection.find(new <int>[1, 3])); // null (no match)
trace(collection.find(new <int>[1, 4])); // works
trace(collection.find(new <int>[3, 2])); // works
trace(collection.find(new <int>[2, 3])); // works
Of course for the sake of readability you can rename each class to something more appropriate for its application e.g. Item => Potion, ItemRelationship => Recipe, RelationshipCollection => RecipeBook.
so the user could input the order
The first step is to limit the possible input. If you allow any type of input, you have to parse that input and things get complicated very quickly.
Create an input method that only allows the user to put two items together, say for example via drag and drop of the items to only 2 slots.
I just don't see how a Dictionary could work as the key order matters.
The important part is to design the keys well.
As #George Profenza pointed out in the comments, you could change your IDs to a different format. Instead of having 1, 2, 3, ... n you could use 1, 2, 4, ... 2^n. The advantage is that you can combine any two IDs uniquely via bitwise or operator (|). In the following example, two such IDs are combined (binary notation):
00001
| 10000
--------
10001
As you can see, each ID occupies a separate position in binary: the 1st position and the 5th. Combining both via or operator means that now both 1st and 5th position are 1. The order doesn't matter. If you use such IDs in the form of powers of 2 you can combine them regardless of the order to form pairs, which can then be used as keys to a dictionary.
Another solution is to simply sort the pair of IDs.
The combination 3-2 becomes 2-3 and the combination 2-3 stays 2-3. Both 2-3 and 3-2 lead to the same result.
You can then build your data structure accordingly, that is: the outer data structure is for the lower ID number and the nested, inner one is for the bigger ID number. Here's some pseudo code with generic objects:
var map:Object = {};
map["2"] = {"3":"combination 2-3"};
To access that, you'd do something like:
trace(map[Math.min(ID1, ID2)][Math.max(ID1, ID2)])
There's also the brute force way of doing it by storing both possible combinations in the data structure. The code for that could roughly look like that:
var map:Object = {};
map["2"] = {"3":"combination 2-3"};
map["3"] = {"2":"combination 2-3"};
Now both
trace(map[ID1][ID2]);
and
trace(map[ID2][ID1]);
Should yield the same result.