COUNTIF Statements: Range Across All Sheets + Cell Reference as Criterion - google-sheets

1) Range Across All Sheets:
I've googled everything but nothing. Basically, I need a formula that looks for the same range across all sheets.
My current formula looks like this:
=COUNTIF(Aug_15!$G:$G, "Shaun")+countif(July_15!$G:$G, "Shaun)+countif(June_15!$G:$G, "Shaun")+countif(May_15!$G:$G, "Shaun")+COUNTIF(Apr_15!$G:$G, "Shaun")+COUNTIF(Mar_15!$G:$G, "Shaun")
The issue I have is, as a month passes, a new sheet for the month is created. So this lowers the automation dramatically as you have to edit the formula every month. I'm basically looking for something that will search G:G across all sheets for that criteria.
So in my imaginary world, it would look something like this:
=COUNTIF(ALLSHEETS!$G:$G, "Shaun")
2) Cell Reference as Criterion
I'm trying to make the criteria look for something from another cell. For example, I'd replace "Shaun" with the cell L3. But it doesn't work! It searches for literally the two characters L and 3!
Is there anyway to make the criteria a value from another cell?
Many Thanks,
Shaun.

As Akshin Jalilov noticed, you will need a script to achieve that. I happen to have written a custom function for that scenario some time ago.
/**
* Counts the cells within the range on multiple sheets.
*
* #param {"A1:B23"} range The range to monitor (A1Notation).
* #param {"valueToCount"} countItem Either a string or a cell reference
* #param {"Sheet1, Sheet2"} excluded [Optional] - String that holds the names of the sheets that are excluded (comma-separated list);
* #return {number} The number of times the item appears in the range(s).
* #customfunction
*/
function COUNTALLSHEETS(range, countItem, excluded) {
try {
var count = 0,
ex = (excluded) ? Trim(excluded.split()) : false;
SpreadsheetApp.getActive()
.getSheets()
.forEach(function (s) {
if (ex && ex.indexOf(s.getName()) === -1 || !ex) {
s.getRange(range)
.getValues()
.reduce(function (a, b) {
return a.concat(b);
})
.forEach(function (v) {
if (v === countItem) count += 1;
});
};
});
return count;
} catch (e) {
throw e.message;
}
}
function Trim(v) {
return v.toString().replace(/^\s\s*/, "")
.replace(/\s\s*$/, "");
}
You can use the custom function in your spreadsheet like this:
=COUNTALLSHEETS("B2:B10", "Shaun")
or when 'Shaun' is in C2
=COUNTALLSHEETS("B2:B3", C2)
There is an optional parameter allowing you to provide a string with comma-separated sheet names you wish to exclude from the count. Don't use this paramater if you want to count ALL sheets.
See if that works for you ?

1) Range Across All Sheets:
The only way you can do that is via script, otherwise Spreadsheet functions cannot dynamically read sheets in the spreadsheet.
2) Cell Reference as Criterion
If the value of L3 is "Shaun" you can do this:
=COUNTIF(Aug_15!$G:$G, L3)
Make sure that you don't put L3 in quotes.

Related

Formula to use to achieve the header into each row [duplicate]

This question already has answers here:
Excel/Sheets combine row column content and find respecting value
(2 answers)
Closed 1 year ago.
I need to convert some data in my Google sheet. Attached is the screenshot on how I currently have the data and how I am looking to format the data into.
Derived from MattKing's answer, I've added transpose in conjunction with some manipulation on the concatenation part to follow the type of sorting your Required Data Format had. This should give you the same output you provided above.
Sample Data:
Formula:
=arrayformula(split(flatten(transpose(A2:A6&" "&B1:D1)&"|"&transpose(B2:D6)),"|"))
Where:
A2:A6 is the range of your project names
B1:D1 is the range of your headers
B2:D6 is the range of your dates
Result:
EDIT:
If you are expecting blank cells in your dates such as the sample below (as pgSystemTester mentioned in the comments section):
You need to add query and exclude those rows that doesn't have dates.
=arrayformula(query({split(flatten(transpose(A2:A6&" "&B1:D1)&"|"&transpose(B2:D6)),"|")}, "where Col2 is not null"))
Just to give another option, and spurred on by this (likely repeat) question
=LET(x, $I$2:$L$4,
myrows, ROWS(x),
mycols, COLUMNS(x),
mycount, SEQUENCE(myrows*mycols),
car, $H$2:$H$4, color, $I$1:$L$1,
mylist, car&" "&color,
mycolumn, INDEX(mylist, CEILING(mycount/mycols,1), IF(MOD(mycount,mycols)=0, mycols,MOD(mycount,mycols))),
mydata, INDEX(x, CEILING(mycount/mycols,1), IF(MOD(mycount,mycols)=0, mycols,MOD(mycount,mycols))),
IF(SEQUENCE(1,2)=1, mycolumn, mydata))
assuming your data starts in cell Sheet1!A2 and extends indefinitely down and indefinitely over, try this:
=ARRAYFORMULA(QUERY(SPLIT(FLATTEN(Sheet1!A3:A&" "&Sheet1!B2:2&"|"&OFFSET(Sheet1!B3,,,9^9,9^9)),"|",0,0),"where Col2 is not null",0))
You can see a sample sheet here, with several of answers illustrated on each tab.
Formula With No App Scripts
This is an update and includes a formula that was stolen from a different answer that appears to nail it. (Player() is officially the Jack Bauer of Spreadsheets... don't ask how he gets things done!).
=INDEX(QUERY(SPLIT(FLATTEN(IF(B2:E="",,A2:A&" "&B1:E1&"×"&B2:E)), "×"),
"where Col2 is not null"))
Make sure this is placed off to the right so that it doesn't spill into itself.
My Original App Scripts Solution
This is honestly not needed any more, but I worked on it for half an hour and maybe someone might find it helpful if they have trouble sleeping.
/**
* Creates 2 Columns of Data
*
* #param {range} theRangeValues The Table Range.
* #return The two columns of data with all combinations based on rows and columns
* #customfunction
*/
function buildTwoColumnsData(theRangeValues) {
var allValues = [];
var i = 1;
var cCell = theRangeValues[0][i];
while (cCell != '' && i < theRangeValues[0].length) {
var theEnd = cCell;
var g = 1;
var rCell = theRangeValues[g][0];
while (rCell != '' && g < theRangeValues.length) {
allValues.push([rCell + " " + theEnd, theRangeValues[g][i]])
g++;
if (g < theRangeValues.length) {
rCell = theRangeValues[g][0];
}
}
i++;
if (i < theRangeValues[0].length) {
cCell = theRangeValues[0][i];
}
}
return allValues;
}

Finding lowest value with no overlapping dates

I have a spreadsheet with criteria, a start and end date, and a value. The goal is to find the lowest value for each unique criteria and start date without overlapping dates (exclusive of end date). I made a pivot table to make it easier for myself but I know there is probably a way to highlight all valid rows that meet the above requirements with some formula or conditional formatting.
I have attached a google drive link where the spreadsheet can be found here and I have some images of the sheet as well. I know that it might be possible with conditional formatting but I just don't know how to combine everything I want it to do in a single formula.
Example below:
Row 2 is a valid entry because it has the lowest value for Item 1 starting on 03-15-2021, same with row 9.
Row 5 is valid because the start date does not fall within the date range of row 2 (exclusive of end date)
Row 7 is not valid because the start date is between the start and end date of row 6
You may add a bounded script to your project. Then you can call it either with a picture/drawing that has the function assigned (button-like), or adding a menu to Google Sheets.
From what you said in the question and the comments, this seems to do what you are trying. Notice that this requires the V8 runtime (which should be the default).
function validate() {
// Get the correct sheet
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
const sheet = spreadsheet.getSheetByName('Sheet1')
// Get the data
const length = sheet.getLastRow() - 1
const range = sheet.getRange(2, 1, length, 4)
const rows = range.getValues()
const data = Array.from(rows.entries(), ([index, [item, start, end, value]]) => {
/*
* Row Index
* 1 Criteria 1
* 2 Item 1 0
* 3 Item 1 1
* 4 Item 1 2
*
* row = index + 2
*/
return {
row: index + 2,
criteria: item,
start: start.getTime(),
end: end.getTime(),
value: value
}
})
// Sort the data by criteria (asc), start date (asc), value (asc) and end date (asc)
data.sort((a, b) => {
let order = a.criteria.localeCompare(b.criteria)
if (order !== 0) return order
order = a.start - b.start
if (order !== 0) return order
order = a.value - b.value
if (order !== 0) return order
order = a.end - b.end
return order
})
// Iterate elements and extract the valid ones
// Notice that because we sorted them, the first one of each criteria will always be valid
const valid = []
let currentCriteria
let currentValid = []
for (let row of data) {
if (row.criteria !== currentCriteria) {
// First of the criteria
valid.push(...currentValid) // Move the valids from the old criteria to the valid list
currentValid = [row] // The new list of valid rows is only the current one (for now)
currentCriteria = row.criteria // Set the criteria
} else {
const startDateCollision = currentValid.some(valid => {
row.start >= valid.start && row.start < valid.end
})
if (!startDateCollision) {
currentValid.push(row)
}
}
}
valid.push(...currentValid)
// Remove any old marks
sheet.getRange(2, 5, length).setValue('')
// Mark the valid rows
for (let row of valid) {
sheet.getRange(row.row, 5).setValue('Valid')
}
}
Algorithm rundown
We get the sheet that we have the data in. In this case we do it by name (remember to change it if it's not the default Sheet1)
We read the data and transform it in a more an array of objects, which for this case makes it easier to manage
We sort the data. This is similar to the transpose you made but in the code. It also forces a priority order and groups it by criteria
Iterate the rows, keeping only the valid:
We keep a list of all the valid ones (valid) and one for the current criteria only (currentValid) because we only have to check data collisions with the ones in the same criteria.
The first iteration will always enter the if block (because currentCriteria is undefined).
When changing criteria, we dump all the rows in currentValid into valid. We do the same after the loop with the last criteria
When changing criteria, the CurrentValid is an array with the current row as an element because the first row will always be valid (because of sorting)
For the other rows, we check if the starting date is between the starting and ending date of any of the valid rows for that criteria. If it's not, add it to this criteria's valid rows
We remove all the current "Valid" in the validity row and fill it out with the valids
The cornerstone of the algorithm is actually sorting the data. It allows us to not have to search for the best row, as it's always the next one. It also ensures things like that the first row of a criteria is always valid.
Learning resources
Javascript tutorial (W3Schools)
Google App Scripts
Overview of Google Apps Script
Extending Google Sheets
Custom Menus in Google Workspace
Code references
Class SpreadsheetApp
Class Sheet
Sheet.getRange (notice the 3 overloads)
let ... of (MDN)
Spread syntax (...) (MDN)
Arrow function expressions (MDN)
Array.from() (MDN)
Array.prototype.push() (MDN)
Array.prototype.sort() (MDN)
Date.prototype.getTime() (MDN)
String.prototype.localeCompare() (MDN)

Generate string like Q,R,S...AB where AB is last column in Google Sheet

I have a complex Google Sheet query that works great except when a Google Sheet doesn't have as many columns as I use in my formula.
Here's what the formula looks like now:
=sum(filter(query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='"&C2&"'",0),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) >=date(2017,1,1),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) <=date(2017,12,31)))
It works great. But the problem is I run it against many worksheets and some don't have e.g. column AG,AH and end at AF at which point I get an error.
So what I need is a way to generate the string Q,R,S....[Name of Last Column in Sheet] and then I can use that instead of my hard-coded Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH but I cannot figure out how to do that.
Any help is greatly appreciated. Thanks!
Per comments above, final formula was:
LEFT("Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK,A‌​L,AM,AN,AO,AP,AQ,AR,‌​AS,AT,AU,AV,AW,AX,AY‌​,AZ,BA,BB,BC ",2*Columns(INDIRECT(A2&"!1:1"))-33+IF(Columns(INDIRECT(A2&"‌​!1:1"))>26,Columns(I‌​NDIRECT(A2&"!1:1"))-‌​26,0))
where column A contains the list of worksheets (tabs) in the Google Sheet. Put this in B2, and then copied it down. I am not marking this as the correct answer since others gave a correct formula-based answer but this did the trick for me.
This can be done with built-in functions:
On a helper sheet, let say you name it, helper, fill up range with letters A to Z, let say A1:A26
Let say that on B1 you write the following formula:
=ArrayFormula({A1:A26;TRANSPOSE(SPLIT(JOIN(",",SUBSTITUTE(QUERY(TRANSPOSE(A1:A26)&A1:A26,,27)," ",",")),","))}) . This will create a list of column letter headers.
On each new worksheet use columns(1:1) to get the total number of columns.
To get your string of column headers, then you could use something like :
JOIN(",",OFFSET(helper!B1,16,0,columns(1:1)-16))
QUERY(helper!B:B,"select B limit "&columns(1:1)-7&" offset 7")
NOTE:
If you decide to have only one helper sheet and use it on several spreadsheets, then use
QUERY(IMPORTRANGE(your_url,"helper!B:B"),"select Col1 limit "&columns(1:1)-7&" offset 7")
This can be done with script. Without seeing you spreadsheet, it is hard to know exactly what you need, but this should be close. I get the variables from Sheet1 and return the formula to Sheet1. Adjust the sheet name to fit your needs. This will look at your data sheets based on the variable sheet name determine the last column. Determine the column letters and build the string the query needs. It then sets the new query formula. I added a menu to run it from.
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().addMenu(
'Create Data', [
{ name: 'Run', functionName: 'formula' },
]);
}
function formula(){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName("Sheet1") //sheet where variables are
var sheet=s.getRange("A2").getValue()//variable sheet name
var sel=makeString(sheet) //get the select string of column letters
//Create formula and return to Sheet1 A3
var f= s.getRange("A3").setFormula('=sum(filter(query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'"&C2&"\'",0),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) >=date(2017,1,1),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) <=date(2017,12,31)))')
}
function makeString(sht){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName(sht)
var lc=s.getLastColumn()
var rng=s.getRange(1, 17, 1, lc).getValues()
var str=''
var ltr=[]
for(var i=17;i<rng[0].length+1;i++){
ltr[i]= columnToLetter(i)
str=ltr.join(',')
}
var str1=str.substr(17)
return str1
}
function columnToLetter(column)
{
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
Let me know if you have any questions.

How to total data based on current date across sheets

UPDATE: Resolved by moving to Excel. :(
I have a Google spreadsheet that has a page for every week. I currently am using the following custom function to count the total amount of times the name appears across all sheets.
function COUNTALLSHEETS(range, countItem, excluded) {
try {
var count = 0,
ex = (excluded) ? Trim(excluded.split()) : false;
SpreadsheetApp.getActive()
.getSheets()
.forEach(function (s) {
if (ex && ex.indexOf(s.getName()) === -1 || !ex) {
s.getRange(range)
.getValues()
.reduce(function (a, b) {
return a.concat(b);
})
.forEach(function (v) {
if (v === countItem) count += 1;
});
};
});
return count;
} catch (e) {
throw e.message;
}
}
function Trim(v) {
return v.toString().replace(/^\s\s*/, "")
.replace(/\s\s*$/, "");
}
When I run the function using
=COUNTALLSHEETS("$A$1:$G$40", A2, "Rosters")
it returns the value across all sheets except those excluded by the third argument of the function. In this case, the sheet titled 'Rosters' isn't counted.
My problem is that every week, I have to update the sheets that are excluded. I currently have all sheets dated in the future excluded. So instead of the short call to the function earlier, it looks more like
=COUNTALLSHEETS("$A$1:$G$40", A2, "Rosters, 5-9Dec, 12-16Dec, ... etc.")
Is there any way to modify the code itself or the function call to exclude future sheets automatically? I'm looking to pull the current date, compare it to a sheet title (which I can change to fit a certain format if needed), and exclude any sheets dated in the future.
Please let me know any ideas; if I need to share a copy of the spreadsheet I can.
Do you need to regularly access the excluded sheets? What about hiding them, and only counting unhidden sheets? Might keep things more organized/manageable at the same time as solving your problem. Since I don't thoroughly know your use-case, this may not be the right direction, of course.

Google Sheets Sum values from ALL sheets in a workbook

So assuming I have a Sheets Workbook with random sheet names, "Bob", "Sally", "Billy", "John" or something similar that isn't sequential. And I have another sheet called "Totals". How can I sum the values from a particular cell, say "H4" from all of them? Specifically if I added new sheets after I created the formula without having to change the formula every time?
You may want to use a custom function for that. Add this to the scripteditor
function sumCellAS(cell) {
var val = [];
SpreadsheetApp.getActive()
.getSheets()
.forEach(function (s) {
val.push(Number(s.getRange(cell)
.getValue()))
});
return val.reduce(function (x, y) {
return x + y;
});
}
Then in your spreadsheet enter the formula =sumCellAS("H2"), passing in the cell to be summed, as a string.

Resources