How to count for specific string and color? - google-apps-script

I've been looking into how to count cells with the countif function, and how to count cells that are colored using scripts and custom functions (like this thing: http://pastebin.com/4Yr095hV), but how would i count cells with a specific string AND color?
Example, I want to count every cell containing the word "one" that has a fill color of white.
EDIT: I was told to add what i had so far, but I am not sure what was meant by that. For counting cells with a specific string I used:
=COUNTIF(A1:A247,"string")
and for counting cells that are colored i used this what was on this page: https://webapps.stackexchange.com/questions/23881/google-spreadsheet-calculating-shaded-cells
but i still don't know how to combine these two TOGETHER.
EDIT: For those looking for this answer, I've found a way to utilize the script Tom posted, and adjusted a line within it.
For Tom's script to work with "wildcards", i used something called .indexOf to always look for any cells containing the string (effectively treating it as if there is always a star before and after the string). On line 32 of his script, I altered it to this:
.map (function(e,i,a) { if (e.toString().toUpperCase().indexOf(this.toString().toUpperCase()) >= 0){ return 1 } else { return 0 } },str))
So now whenever I want to look for a White cell containing the string "Apple1", it will count it regardless of if it's written as "OrangeApple1B" or whatever. And the casing doesn't matter since it seems like this script always converts the given string to Upper Case anyways.
I am still trying to find out how to incorporate this on a totally different spreadsheet though (using something like IMPORTRANGE to count cells on a TOTALLY DIFFERENT SHEET using this script)...

function countIfStringAndColor(r, str, color) {
var COLORS = {
"BLACK":"#000000",
"DARK GRAY 4":"#434343",
"DARK GRAY 3":"#666666",
"DARK GRAY 2":"#999999",
"DARK GRAY 1":"#B7B7B7",
"GRAY":"#CCCCCC"
};
var range = SpreadsheetApp
.getActive()
.getActiveSheet()
.getRange(r.toString());
color = color.indexOf("#") == 0 ? color : COLORS[color.toString().toUpperCase()];
return range
.getBackgrounds()
.reduce(function(a,b) { return a.concat(b) })
.map (function(e,i,a) { return e.toString().toUpperCase() === this.toString().toUpperCase(); },color)
.map(function(e,i,a) { return [e, this[i]] },
range
.getValues()
.reduce(function(a,b) { return a.concat(b) })
.map (function(e,i,a) { return e.toString().toUpperCase() === this.toString().toUpperCase() },str))
.filter(function(e,i,a) {return a[i][0] && a[i][1] })
.length;
}
METHOD OF OPERATION
The function takes three arguments: Range (String), String, String
The associative array 'COLORS' is supplied to convert the common names of colors to hex format. There are about 90 more colors in the list that I didn't supply for space reasons. I can get you the full list if you would like.
Grabbing the Range.
Checks to see if color is already in hex format. If not it tries to find a common name key in COLORS and return the hex value. From here out everything is toString() and toUpperCase() to help prevent errors.
The code from here out is one chain of array manipulation that will produce the solution for the function to return.
Grab the needed background colors.
.reduce, coupled with .concat (both Array Methods), is used to flatten the background color array. It changes it from a rectangular array of arrays to a one dimensional list.
.map goes through each element of the array and applies the given function. In this case we are seeing if the array element (e) is the same as the color supplied. Take note of how 'color' is called outside the closing curly bracket. It is the 'thisArg', and the 'this' inside the function is an image of it. The array is now reduced to a series of true/false elements.
This map is used to combine the two arrays, 'color' and 'str'. The indented part right below is the same steps we used to get 'color' to a series of true/false elements, but now applied to 'str'. All those operations are performed while 'str' is being called as the thisArg for the current map function. The map function then returns a single array of the form [color,str] which is made up of many elements of [true,false] [true,true] [false,false] pairs.
We are only interested in the solutions where both 'color' and 'str' are true, so we can use .filter to remove all the other elements, leaving use with an array of only [true, true] pairs.
Each [true, true] pair is a unique solution to the equation. We can just grab the length of the array to see how many solutions we have found! This is the value that is passed to the return at the beginning.

Related

How to clear cells if ISBLANK?

I want to add clearing function. If "B1" is blank, then clear C1,D1,E1,F1;
But I don't know how I can do it. I tried custom function in formatting, but not works.
Can u help me?
The simple answer to your question is as follows:
function myFunction() {
let your_sheet = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/your_sheet_id').getSheetByName('Sheet1')
let cell_check = your_sheet.getRange('B1');
if(cell_check.isBlank()) {
your_sheet.getRange('C1:F1').clearContent();
}else{
console.log('not empty')
}
}
But I would probably advise considering the use of numbered ranges instead of named ranges as it will be easer to reason about at scale if you are running through multiple rows.
function myNumberedRangeFunction() {
let your_sheet = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/your_sheet_id').getSheetByName('Sheet1');
let cell_check = your_sheet.getRange(1,2);
/* getRange(
1, the first row,
2, is the second column
)
*/
if(cell_check.isBlank()) {
your_sheet.getRange(1,3,1,4).clearContent();
/* getRange(
1, the first row,
3, the third column,
1, get one row,
4, get four columns
)
*/
}else{
console.log('not empty')
}
}
Typically, if I am manipulating a large spreadsheet, I will pull the entire spreadsheet data_table = your_sheet.getRange(1,1,1,1).isBlank() ? [] : Array.from(your_sheet.getRange(1,1,your_sheet.getLastRow(),your_sheet.getLastColumn()).getValues()); and convert it to a table array that I can analyze and manipulate all at once and then just place the changes all at once with
your_sheet.getRange(1,1,data_table.length,data_table[0].length).setValues(data_table)
You can hide, but not clear, the values in columns C:F with this conditional formatting custom formula rule:
=isblank($B1)
The rule should set the text color to match the cell fill color, making the value in the cell "invisible".

Search Array of Strings with Non-sensitivity and Non-exact Match

Notice: I have made a few changes to the original question as my problem was not with commas within string.
I have a function I've been working on to exclude a cell value from a new array that contains a string I am searching for. I am doing this in order to put together a list for .setHiddenValues, since .setVisibleValues is not supported/implemented yet.
Here are my requirements for the sake of clarity:
Currently working:
Able to handle numbers as well as strings
Can search for lowercase and uppercase. visibleValueStr is user inputted so it can't be so sensitive.
colValueArr may have strings with commas within.
Still working on:
visibleValueStr can be a single value or array.
Case sensitivity("apple" to match "Apple")
Not exact matches("apple" to match "apple and banana")
Here is the function I currently have with the above met/unmet conditions:
function getHiddenValueArray(colValueArr,visibleValueArr){
var flatUniqArr = colValueArr.map(function(e){return e[0].toString();})
.filter(function(e,i,a){
return (a.indexOf(e.toString())==i && visibleValueArr.indexOf(e.toString()) == -1);
})
return flatUniqArr;
}
Please let me know what other info I need. I will update this question as I continue to do my research in the meanwhile.
Clarification from comments:
User inputs input(s) on HTML form and the variable is passed on as visibleValueArr.
When using Logger.log(visibleValueArr).
[apple, banana]
When using Logger.log(colValueArr).
[[Apple],[apple][apple][apple and banana],[apple],[banana, and apple],
[apple, and banana],[orange],[orange, and banana],[kiwi],[kiwi, and orange],
[strawberry]]
So when I use:
SpreadsheetApp.newFilterCriteria().setHiddenValues(newArray).build();
newArray should be the hidden values. In this case it should be:
orange
kiwi
kiwi, and orange
strawberry
Basically anything that does not contain what visibleValueArr is.
Instead, it returns all values back, hiding them all.
When I use [Apple, Banana] the "Apple" and "Banana" values are left out of newArray as they should be, but "Apple and Banana" and "Apple, and Banana" are not"
In addition, I would also like to understand what the e,i,a in function(e,i,a) represent. I'm trying to apply .toLowerCase() in different places to see if that resolves part of my issue but I'm not sure where to do it.
Issues:
Case sensitivity("apple" to match "Apple")
Not exact matches("apple" to match "apple and banana")
Solution:
Use regex-search with case insensitivity
Modified Script:
function getHiddenValueArray(colValueArr,visibleValueArr){
/*colValueArr = [["Apple"],["apple"],["orange"],["Apple, and Banana"]];
visibleValueArr = ['apple','banana'];*/
var flatUniqArr = colValueArr.map(function(e){return e[0].toString();})
.filter(function(e,i,a){
return (a.indexOf(e)==i && !(visibleValueArr.some(function(f){
return e.search(new RegExp(f,'i'))+1;
})));
});
//Logger.log(flatUniqArr); will log orange
return flatUniqArr;
}
References:
String#search
Array#some
Array#filter
Array#map

Adobe After Effects: Keep "Expression-Relations" when duplicating multiple layers

just wanted to ask, whether there is a way to keep the relations of expressions going when duplicating layers.
E.g. I have two layers, "LayerA" and "LayerB". Now I have an expression going on in "LayerB" saying, that its position always equals the position of "LayerA".
Now when I duplicate those two and get "LayerA 2" and "LayerB 2" I want the expression in "LayerB 2" to reference to "LayerA 2"'s position rather than "LayerA"'s position!
While it is no problem to simply change the expression when there is only one of them, it gets quite hard when you have multiple expressions going on ...
You might end up wanting to organize your comp differently, but, given your example (and exactly those name lengths), this position expression will work to find the appropriate 'target layer':
//base name to work from:
baseName = "Layer";
//length of that:
nameLen = baseName.length;
//this layer's name:
myName = thisLayer.name;
if (myName.length == nameLen) {
//if they are the same, then it is the original
// (non-duplicated) version
thisComp.layer("LayerA").transform.position;
} else {
//get tail string, the space and number:
tailStr = myName.substring(nameLen+1, myName.length);
//build new target layer name with "A":
targetName = myName.substring(0, (nameLen)) + "A" + tailStr
//new line pointing to target layer:
thisComp.layer(targetName).transform.position;
}

D3 reusable multi-line chart with JSON data

I'm trying to do some re-factoring on my charts to make them re-usable using this as a guide: http://bost.ocks.org/mike/chart/
I'm having problems drawing the lines in my multi-line graph though - specifically passing the data to the x and y values. If I hard code the element names it works, but if I try to use the xValue and yValue objects this does not work. I'm assuming that this is because I'm trying to call a function within the parameter of an other object, but I'm not sure how to get around this. In the exmaple Mike uses d[0] and d[1], but this won't work with JSON data (or I'm not sure how to make it work).
I've posted this JSFiddle so you can see the code. The problem lines are 125 to 131 which in turn is being called from line 165.
var main_line = d3.svg.line()
.interpolate("cardinal")
// Hard coding the elements works
//.x(function(d) { return main_x(d.date); })
//.y(function(d) { return main_y(d.buildFixTime); });
// Passing xValue and yValue does not work
.x(function(d) { return main_x(xValue); })
.y(function(d) { return main_y(yValue); });
http://jsfiddle.net/goodspeedj/fDyLY/
Thank you in advance.
You need to redefine your accessor method within .x() and .y(). The accessor method defines the way that a datum is pulled out of the data that is bound to the selection that you call the line generator on.
Suppose you have a relatively flat data structure such as the following.
data = [{x : 1, y : 2}, {x:1, y:3}, {x:4, y:5}];
You then bind the data to a selection with the following statement
d3.select("body").datum(data).append("path").attr("d",lineGenerator);
Quite a bit is going on underneath this statement. I'll give you a bit more of a walkthrough after showing you a commonly used example.
The important aspect to understand is that similarly to other calls in d3 such as
var exampleRectangles = d3.select("body")
.data(data).enter()
.append("rect")
.attr("width",2)
.attr("height", 3)
.attr("x",function(datum){return datum.x}) // pay attention to this line
.attr("y",0);
d3 is implicitly iterating over each element in your data. For each datum in your data array, in this case there is a total of three datum, you are going to add a rectangle to the dom.
In the line that I tell you to pay attention to you notice that you're defining an anonymous (unnamed) function. What is that datum parameter coming from? It's implicitly being passed to your anonymous function.
So each rectangle has it's own corresponding datum {x : 1, y : 2}, {x:1, y:3}, {x:4, y:5} respectively. Each rectangle's x coordinate is defined by the respective datum.x attribute. Under the sheets, d3 is implicitly looping over the data array that you've defined. A similar approach to the example d3 code could be written as above.
for (var i = 0; i < data.length; i++)
{
d3.select("body").append("rect")
.attr("width",2)
.attr("height", 3)
.attr("x",data[i].x)
.attr("y",0);
}
This follows from the notion of data driven documents (d3). For each item added (a rectangle in the above example a piece of data is tied to it. In the above example you see that there is something kind of similar to your .x() and .y() accessor functions :
.attr("x",function(datum){return datum.x})
This function is telling d3 how to filter over the total datum that's being passed to the .attr() accessor method.
So, you need to determine which data you need to get a hold of to make your .attr("d", lineGenerator)call make sense. The difference between your.datum(data)call and the typical.data(data)call is that instead of parceling the data that's being passed to.data(data)`, the whole array is given as a single piece of data to the line generator function (similar to main_line(data), wherein it will again implicitly loop over the points to construct your path.
So, what you need to do is determine what a single datum will be defined as for your function to operate on.
I'm not going to define that as I don't seem to know quite which information you are operating on, but I would hazard a guess at something like.
.x(xAccessor)
.y(yAccessor)
function xAccessor(datum)
{
return xScale(datum._id.month);
}
function yAccessor(datum)
{
return yScale(datum.buildFixTime);
}
The way you have it set up, xValue and yValue are functions; you have to actually execute them on something to get a value back.
.x(function(d) { return main_x( xValue(d) ); })
.y(function(d) { return main_y( yValue(d) ); });
If you weren't using a scale, you could use
.x(xValue)
.y(yValue);
but only because if you pass in a function d3 executes it for you with the data as a parameter. And that only works for d3 methods that expect functions as possible input -- the scale functions expect data values as input.
I wrote a long piece work for another user last week that you may find useful, explaining methods that accept functions as parameters.

getting a random element from an array of movieclips(or labels in a timeline) in Flash CC . Actionscript 3

I am making a pretty neat quiz-game in flashCC right now and I definitely need your help.
My skills are more on the design then the programming side. So to many of you this might seem a baby question (and asked many times before) but from all the answers I saw so far, I couldn't get any results for my project.
So here is the thing :
I need the EXACT script for creating an array (with movieclips inside? or instance names of mcs? How does this even work?)
and a method, to pick a random element of this array without repeats until the "game is over".
Paul
The easiest way to pick a random element from an array without repeating is to first sort the array with a "random" function, then pop or shift items out of it until the array is empty.
Let's say you have an array of items which can be filled with either instances or instance names, you've chosen instance names: :
var FirstArray:Array = ["blau", "orange", "green"];
Now, you'll need a random sort function:
// you do not need to modify this function in any way.
// the Array.sort method accepts a function that takes in 2 objects and returns an int
// this function has been written to comply with that
function randomSort(a:Object, b:Object):int
{
return Math.random() > .5 ? -1 : 1;
}
The way a sort function normally works is it compares two objects and returns -1 if the first item precedes the second item, 1 if the opposite is true, and 0 if they are the same.
So what we're doing in the function above is returning -1 or 1 randomly. This should get the array all jumbled up when you call:
FirstArray.sort(randomSort);
Now that the array is randomly sorted, you can begin pulling items from it like so:
if(FirstArray.length) // make sure there's at least one item in there
{
// since you are using instance names, you'll need to use that to grab a reference to the actual instance:
var currentQuizItem:MovieClip = this[FirstArray.pop()];
// if you had filled your array with the actual instances instead, you would just be assigning FirstArray.pop() to currentQuizItem
// every time you call pop on an array, you're removing the last item
// this will ensure that you won't repeat any items
// do what you need to do with your MovieClip here
}
else
{
// if there aren't any items left, the game is over
}
When strung together, the above code should be enough to get you up and running.
You could try something like:
var array:Array = [1, 2, 3, 4, 5];
var shuffledArray:Array = [];
while (array.length > 0)
{
shuffledArray.push(array.splice(Math.round(Math.random() * (array.length - 1)), 1)[0]);
}
trace('shuffledArray: ', shuffledArray, '\nrandom item: ', shuffledArray[0]);