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.
Related
I have a simple Google sheet that records what sessions people are signed up for (3 concurrent sessions per day):
The same person cannot be in more than 1 session on a given day. I'd like to create a function in column B that checks for that situation and flags it, as in Susan, Keith, and Amy in the example above (I've highlighted in yellow the conditions that would trigger a flag).
If there were just one date, I'd use a countif (or maybe countifs?) to check for more than 1 TRUE for that date. But with multiple dates, I think some sort of iterative function or query is needed. I have a feeling I may be missing a simple formula, but it's eluding me. I may add more dates, so the solution needs to allow for n number of dates in the range.
UPDATE: My scenario has become a little more complex. I'm designating a potential role each person can play in each session and then using the checkboxes to indicate who is playing what role in each session. A given person can't be in more than 1 session per day (but a given person may be in 0 sessions on a given day). The below image shows this updated scenario, with the yellow highlights showing the conditions that I want flagged via the function in column B.
Here's a link to the Google sheet if you want to create a copy.
Given the use case provided, you can apply the formula below to B3 and drag the auto-complete handle:
=IF(ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "", "FLAG")
I'm converting the Boolean values to INT and summing them up. If the sum is equal to the count of unique days in the first row, then everything is fine, otherwise, FLAG!
In other words, if there are more (or less) checks than days, it should be flagged.
You can also set up a conditional formatting to paint the cell accordingly.
Alternatively, if you’d like to treat each scenario you can use =IFS() as below:
=IFS(ARRAYFORMULA(SUM(INT(C3:K3))) > COUNTUNIQUE($C$1:$1), "HIGHER", ARRAYFORMULA(SUM(INT(C3:K3))) < COUNTUNIQUE($C$1:$1), "LOWER", ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "OK")
References:
Sheets Functions documentation
IF
IFS
ARRAYFORMULA
SUM
INT
COUNTUNIQUE
EDIT:
Since the changes in the original scope significantly impacted my previous answer, here is a suggestion using a custom formula:
function checkFlags(){
var ss = SpreadsheetApp.getActive(); // get active Sheets
var ws = ss.getSheetByName("Sheet1"); // getting tab named "Sheet1"
var currentCellRange = ss.getActiveRange(); // getting active cell, in the context of a custom formula, it gets the one being calculated at the time
var rowIndex = currentCellRange.getRowIndex(); //getting current row number
var rowValues = ws.getRange(`${rowIndex}:${rowIndex}`).getValues()[0]; //getting row cells values
var sessionsList = []; //temp variable to store useful data from cells
for (var i = 0; i < rowValues.length; i++) { //reading cells on the row to create a date/flag array
var cell = rowValues[i]; //getting Range of current cell
if (typeof(cell) == 'boolean'){ //if the current cell has a boolean value, it is a session flag
var headerDate = ws.getRange(2, (i+2)).getDisplayValue(); //getting the header value on row 2 (current date for the session flag)
sessionsList.push({date: headerDate, session: cell});//storing date and session flag value on the temp variable
}
};
var groupBy = function(xs, key) { //handle function to proccess the sessionsList variable and group flag values by 'date'
return xs.reduce(function(rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
var tempGroupedArray = groupBy(sessionsList, 'date'); //grouping temp sessionsList by 'date'. This will return an array like [ { date: '<date>', session: true/false }, { date: '<date>', session: true/false }, ...]
for (dateFlags of Object.entries(tempGroupedArray)) {//looping through the `grouped by date` array
var tempCount = dateFlags[1].filter(x => x.session==true).length; //how many `trues` are for this date
if (tempCount > 1){ //if there is more than one session set as "true" for this date...
return 'FLAG'; //...immediately stop execution and return FLAG
}
};
//if it passed the loop above, it means there is no flags
return ''; //then return blank
}
NOTES: This custom formula will not update the result in the cell after a change on the flag values, you must delete/paste it to force if wanted.
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)
I'm pretty new to excel or google sheets. The work place, that I work at does not have anything stream lined.
I'm trying to create my own work book that I can refresh everyday I log in so that I can have a list of things that I need to work on for that day.
One of the functions that I would like to have is, whenever a new sheet is shared with me on Google Sheets, I want the URL for that sheet to populate in one of the cells in my workbook automatically and arranged based on timestamp.
I was trying to search for this on Google, but I read that: shared with me docs are not stored anywhere specifically.
Any help or pointing me in the right direction is highly appreciated.
It is easy to fetch the files that have been shared with you. For that, you can simply call the Drive API's Files: list method specifying in the q parameter the sharedWithMe attribute.
Afterwards, you can use the SpreadsheetApp class to gather a spreadsheet and insert data into it. In this case, you can simply make several calls of apendRow() to insert the results.
Finally, properties can be used to store the status of the script (last date checked), so that it can know where to resume from. In this case below, we'll be saving the date in the LAST_DATE property.
Code.gs
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var SHEET_NAME = 'YOUR_SHEET_NAME';
function myFunction() {
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
var lastDate = new Date(PropertiesService.getScriptProperties().getProperty('LAST_DATE'));
var currentDate = new Date();
var files = getFiles(lastDate);
for (var i=0; i<files.length; i++) {
var row = [
new Date(files[i].sharedWithMeDate),
files[i].title,
files[i].alternateLink,
files[i].sharingUser.emailAddress,
files[i].owners.map(getEmail).join(', ')];
sheet.appendRow(row);
}
console.log('lastDate: %s, currentDate: %s, events added: %d', lastDate, currentDate, files.length);
PropertiesService.getScriptProperties().setProperty('LAST_DATE', currentDate.toISOString());
}
function getEmail(user) {
return user.emailAddress;
}
function getFiles(lastSharedDate) {
var query = "sharedWithMe and mimeType = 'application/vnd.google-apps.spreadsheet'";
var res = Drive.Files.list({
q: query,
orderBy: "sharedWithMeDate desc",
fields: "*",
pageSize: 1000
});
// `query` parameter cannot compare sharedWithMeDate, so we do it afterwards
return res.items.filter(function (i) {
return (new Date(i.sharedWithMeDate) > lastSharedDate);
}).reverse();
}
You can set up the script to be ran periodically (i.e once a day, or more in case you'd need it) using Time-driven triggers.
I'm importing information into a Google Sheet that I'm hoping to print as a delivery list invoice.
The code below allows items marked "0" in Column H to disappear off the invoice, allowing the final product to appear cleaner.
I'm trying to figure out how to make the "Hide Row" function trigger for empty cells, so I don't have to manually enter in 0's.
As far as I can tell, the Filter function won't work because it won't update "OnEdit", only manually. I plan to repeat this for many sheets (one per customer) and require it to be automatic.
This is the code I have so far.
function onEdit() {
var s = SpreadsheetApp.getActive()
.getSheetByName('BELLTOWN');
s.showRows(1, s.getMaxRows());
s.getRange('H:H')
.getValues()
.forEach(function (r, i) {
if (r[0] !== '' && r[0].toString()
.charAt(0) == 0) s.hideRows(i + 1)
});
}
function getNote(cell) {
return SpreadsheetApp.getActiveSheet().getRange(cell).getComment();
}
Change
if (r[0] !== '' && r[0].toString()
to
if (r[0] == '' && r[0].toString()
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.