Changing Cell Value based off of Date- App script build? - google-apps-script

I was wondering if anyone could help build a script for a Macro Extension on my google sheet. We use sheets to track application statuses and would like an automation to be set.
My Goal:
I would like any application that is still in the "Pre-IC" agreement (column N) after 30 days of their application date(column H) to be automatically changed to "Delayed" in column N.
I have attached a screenshot to this, please let me know if further information is needed in order to build this script -- I'm a newbie at this I really need support

Description
You can use a Time Driven Trigger to run unattended periodically. You do this from the Script Editor Triggers.
The following script can be set to run daily or more frequently as needed.
I have created a simple mock up of your sheet with only 2 columns that you were interested in.
Code.gs
function statusTimer() {
try {
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
let values = sheet.getRange(7,1,sheet.getLastRow()-6,sheet.getLastColumn()).getValues();
let today = new Date();
for( let i=0; i<values.length; i++ ) {
if( values[i][1] === "Pre IC Agreement" ) {
if( today.valueOf()-values[i][0].valueOf() > (30*24*60*60*1000) ) { // valueOf is in milliseconds
values[i][1] = "Delayed";
}
}
}
values = values.map( row => [row[1]] ); // extract only 2nd column
// Note my test sheet has only 2 columns. You need to adjust getRange() for your case
sheet.getRange(7,2,values.length,1).setValues(values);
}
catch(err) {
console.log(err);
}
}
Reference
Installable Trigger
Spreadsheet Service
Date Object
Array.map()

Try
function myFunction() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1');
var d = new Date();
var oneMonthAgo = new Date();
oneMonthAgo.setDate(d.getDate() - 30)
const date = sh.getRange('H7:H'+sh.getLastRow()).getValues()
const status = sh.getRange('N7:N'+sh.getLastRow()).getValues()
status.forEach(function(r,i){
if (r[0] == 'Pre-IC Agreement' && date[i][0] < oneMonthAgo){
r[0] = 'Delayed'
}
})
sh.getRange('N7:N'+sh.getLastRow()).setValues(status)
}

Related

Runtime limit exceeded when I try to sum the values of a column according to the rows that satisfy the desired filters

Let's say I want to filter the rows of a worksheet that have:
car in Column AF
home in Column E
work in Column B
And sum the values of Column V to know if it is above zero or not:
function fix_value(pg,sum,cell) {
var ss = SpreadsheetApp.getActive().getSheetByName(pg).getRange(cell);
if (sum > 0) {
ss.setValue('on');
} else {
ss.setValue('off');
}
}
function main_event() {
var vls = SpreadsheetApp.getActive().getSheetByName('Sheet111').getRange('A14:A17').getValues();
var comb = SpreadsheetApp.openById('XXXXXXXXXXXXXXXXXXXXXXXXXX');
var rgs = comb.getSheetByName('Historic');
var rgs_vls = rgs.getRange('A3:AF').getValues();
var sum = 0;
for (var i = 0; i < rgs_vls.length; i++) {
if (
rgs_vls[i][31] == vls[0][0] &&
rgs_vls[i][4] == vls[1][0] &&
rgs_vls[i][1] == vls[2][0]
) {
sum += rgs_vls[i][21];
}
}
fix_value('Sheet111',sum,'A10');
}
But my worksheet is very big and this analysis and this looping need to be run every 5 minutes.
As it takes a long time to finish the process, at certain times of the day the code ends up overflowing the execution time limit.
How to proceed in this case to end the high execution time problem?
This works for me:
function main_event() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1');
var vs = sh.getRange('A14:A17').getValues().flat();
var ass = SpreadsheetApp.getActive();
var ash = ass.getSheetByName('Sheet0');
var avs = ash.getRange('A3:AF'+ash.getLastRow()).getValues();
let sum = 0;
avs.forEach((r,i) => {
if(r[31] == vs[0] && r[4] == vs[1] && r[1] == vs[2]) {
sum += r[21];
}
});
if(sum > 0 ) {
sh.getRange("A10").setValue('on')
} else {
sh.getRange("A10").setValue('off');
}
}
In this answer, please think of this as my challenge about whether the process cost can be reduced. From currently the page has 288324 rows and 32 columns, how about the following modifications? In this modification, I modified your script by using the following references.
Modified script 1:
In this script, Sheets API is used. So, please enable Sheets API at Advanced Google services.
function main_event2() {
var spreadsheetId = "###"; // Please set spreadsheet ID of "Historic" sheet.
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet111");
var [[a], [b], [c]] = sheet.getRange('A14:A16').getValues();
var [bb, ee, afaf, vv] = Sheets.Spreadsheets.Values.batchGet(spreadsheetId, { ranges: ["'Historic'!B3:B", "'Historic'!E3:E", "'Historic'!AF3:AF", "'Historic'!V3:V"] }).valueRanges;
var sum = afaf.values.reduce((res, [e], i) => {
if (e == a && ee.values[i][0] == b && bb.values[i][0] == c) {
res += Number(vv.values[i]);
}
return res;
}, 0);
sheet.getRange("A10").setValue(sum > 0 ? "on" : "off");
}
When this script is run for the sample sheet with 288324 rows and 32 columns, the process cost was about 20 seconds. But, I cannot know your actual situation. So, please test it using your Spreadsheet.
Modified script 2:
In this script, the values are retrieved using the query language.
function main_event3() {
var spreadsheetId = "###"; // Please set spreadsheet ID of "Historic" sheet.
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet111");
var [[a], [b], [c]] = sheet.getRange('A14:A16').getValues();
var query = `SELECT V WHERE B='${c}' AND E='${b}' AND AF='${a}'`;
var url = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?sheet=Historic&tqx=out:csv&tq=${encodeURIComponent(query)}`;
var res = UrlFetchApp.fetch(url, { headers: { authorization: "Bearer " + ScriptApp.getOAuthToken() } });
var [, ...ar] = Utilities.parseCsv(res.getContentText());
var sum = ar.reduce((res, e) => res += Number(e), 0);
sheet.getRange("A10").setValue(sum > 0 ? "on" : "off");
}
When this script is run for the sample sheet with 288324 rows and 32 columns, the process cost was about 15 seconds. But, I cannot know your actual situation. So, please test it using your Spreadsheet.
References:
Benchmark: Reading and Writing Spreadsheet using Google Apps Script (Author: me)
Benchmark: Conditional Branch using Google Apps Script (Author: me)
Benchmark: Process Costs for Retrieving Values from Arrays for Spreadsheet using Google Apps Script (Author: me)
Method: spreadsheets.values.batchGet
How to speed ​up the search data in sheet

Delete columns before today's date

I've been struggling with this for a couple weeks now. I have posted a question here a couple days a go but still i didn't got and solution yet. Probably because in fact it was a 2-3 tasks question and not so well explained by my fault. Thus i decided to post another one with a one (of the 2-3) more simple task.
So, i have the script below which deletes in the sample sheet the columns dates before today's date. The script works fine but i have two issues.
First... If the number of the old dates/columns is one it works fine. It deletes only the day before and NOT the first 3 columns (A,B & C in the sample sheet) which i want to keep. If they are more the one old dates then the script deletes several columns/dates before and after today's date. What i want is to delete all the columns/dates ONLY before today's date and NOT the first 3 columns (A,B & C in the sample sheet) which i want to keep.
The second issue is kinda weird. Currently i am running the script from a custom menu ("My Tools" in the sample sheet). Now, when run the script from the custom menu or with the "Run" button in script editor it works. When i try to add a time-driven trigger (not programmatically. i don't know how) so to run it once every 24 hours, then i get a script error with this message: Exception: Range not found at deleteColumnsDate(Code:14:49).
This is a sample of my working sheet: https://docs.google.com/spreadsheets/d/1NJvcLxwc96411-Sl_aTu1d-J5gQvqlZjPUQbWZoRqBM/edit?usp=sharing
And this is the script that i am currently using:
function onOpen()
{
var ui = SpreadsheetApp.getUi();
ui.createMenu('My Tools')
.addItem('Delete Older Dates','deleteColumnsDate')
.addToUi();
}
function deleteColumnsDate(row)
{
var row = (typeof(row) !== 'undefined') ? row : '3';
var day = 86400000;
var today = new Date().getTime();
var rng = SpreadsheetApp.getActiveSheet().getRange(row + ':' + row);
var rngA = rng.getValues();
for(var i = 1; i < rngA[0].length ;i++)
{
if(isDate(rngA[0][i]) && (((today - new Date(rngA[0][i]).getTime())/day) > 1 ))
{
SpreadsheetApp.getActiveSheet().deleteColumns(i + 1);
}
}
}
function isDate (x)
{
return (null != x) && !isNaN(x) && ("undefined" !== typeof x.getDate);
}
So, what am i doing wrong here?. Is it possible to make this process to run fully automatically every 24 hours?
Thank you in advance
Nessus
When you put a time trigger, you can't use getActiveSheet(). Instead you should use SpreadsheetApp.openById( [ID] )
You will find your ID inside URL of the document like : docs.google.com/spreadsheets/d/abc1234567/edit#gid=0
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#openbyidid
EDIT:
As Cooper mentionned it, I forgot something.
Try this way :
var ss = SpreadsheetApp.openById( [SHEET ID] );
var sh = ss.getSheetByName( [SHEET NAME] ); //delete column here
var rng = sh.getRange(row + ':' + row);
Complete function:
function deleteColumnsDate() {
var day = 86400000;
var today = new Date().getTime();
var ss = SpreadsheetApp.openById(" [SHEET ID]");
var sh = ss.getSheetByName(" [SHEET NAME] ");
var rng = sh.getRange('A1:Z1'); // change if you need larger
var rngA = rng.getValues();
var nb_to_delete = 0;
for(var i = 1; i < rngA[0].length ;i++) { // start colonne B
if(isDate(rngA[0][i]) && (((today - new Date(rngA[0][i]).getTime())/day) > 1 )){
nb_to_delete++;
}
}
sh.deleteColumns(2, nb_to_delete);
}

How to get a total across all sheets for cells based on criteria from another cell?

I have a budget spreadsheet with tabs for every pay period. These tabs are created as needed and don't have names I can easily know in advance. For instance, one will be "10/15 - 10/28" because that's the pay period. Next month I create a new one with "10/29 - 11/11." I'd like to be able to sum a value across all sheets. For example, every sheet has a row named "Save," some sheets have a row named "Rent", but not every sheet will contain rows with those names and when they do they won't always be in the same cell number.
Sample sheet
I've seen some examples where there's a bunch of SUMIFs and every sheet is manually named but I'd much rather not have to do that because this sheet gets copied fairly often and the sheet names will never be the same.
=SUMIFS('Tab 1' !A1:A10, 'Tab 1'!B1:B10, "Rent")
+SUMIFS('Tab 2' !A1:A10, 'Tab 2'!B1:B10, "Rent")
+SUMIFS('Tab 3' !A1:A10, 'Tab 3'!B1:B10, "Rent")
Is this possible with either a standard formula or a script?
Sample Data
Desired final tab
Column 1's values are known in advance so those can be hardcoded. For instance, there will never be a random "yet more stuff" appear which I wouldn't sum up by adding a new row to the final tab.
While there's another answer that works for this, I think the use of text finders and getRange, getValue and setFormula in loops is not the best approach, since it greatly increases the amount of calls to the spreadsheet service, slowing down the script (see Minimize calls to other services).
Method 1. onEdit trigger:
An option would be to use an onEdit trigger to do the following whenever a user edits the spreadsheet:
Loop through all sheets (excluding Totals).
For each sheet, loop through all data.
For each row, check if the category has been found previously.
If it has not been found, add it (and the corresponding amount) to an array storing the totals (called items in the function below).
If it has been found, add the current amount to the previous total.
Write the resulting data to Totals.
It could be something like this (check inline comments for more details):
const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;
function onEdit(e) {
const ss = e.source;
const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}
Method 2. Custom function:
Another option would be to use an Apps Script Custom Function.
In this case, writing the data via setValues is not necessary, returning the results would be enough:
const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;
function CALCULATE_TOTALS() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
return items;
}
Once the script is saved, you can use this function the same you would use any sheets built-in function:
The problem with this approach is that the formula won't recalculate automatically when changing any of the source data. In order to do that, see the above method.
Method 3. onSelectionChange trigger:
From your comment:
I'd love to be able to trigger it when the totals sheet is opened but that doesn't appear to be possible
You can do this by using an onSelectionChange trigger in combination with PropertiesService.
The idea would be that, every time a user changes cell selection, the function should check whether current sheet is Totals and whether the previously active sheet is not Totals. If that's the case, this means the user just opened the Totals sheet, and the results should update.
It could be something like this:
function onSelectionChange(e) {
const range = e.range;
const sheet = range.getSheet();
const sheetName = sheet.getName();
const previousSheetName = PropertiesService.getUserProperties().getProperty("PREVIOUS_SHEET");
if (sheetName === TOTAL_SHEET_NAME && previousSheetName !== TOTAL_SHEET_NAME) {
updateTotals(e);
}
PropertiesService.getUserProperties().setProperty("PREVIOUS_SHEET", sheetName);
}
function updateTotals(e) {
const ss = e.source;
const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}
Note: Please notice that, in order for this trigger to work, you need to refresh the spreadsheet once the trigger is added and every time the spreadsheet is opened (ref).
Reference:
onEdit(e)
Custom Functions in Google Sheets
onSelectionChange(e)
I wrote 2 scripts:
budgetTotal which takes a budgetCategory parameter, for example "Rent", and loops through all the sheets in the file to sum up the amounts listed on each sheet for that category.
budgetCreation which looks at your Totals sheet and writes these budgetTotal formulas in for each category you have listed.
I ran into a challenge which was, as I added new sheets the formulas wouldn't be aware and update the totals. So, what I did was create a simple button that executes the budgetCreation script. This way, as you add new payroll weeks you just need to press the button and - voila! - the totals update.
There might be a better way to do this using onEdit or onChange triggers but this felt like a decent starting place.
Here's a copy of the sheet with the button in place.
const ws=SpreadsheetApp.getActiveSpreadsheet()
const ss=ws.getActiveSheet()
const totals=ws.getSheetByName("Totals")
function budgetCreation(){
var budgetStart = totals.createTextFinder("Category").findNext()
var budgetStartRow = budgetStart.getRow()+1
var budgetEndRow = ss.getRange(budgetStart.getA1Notation()).getDataRegion().getLastRow()
var budgetCategoies = budgetEndRow - budgetStartRow + 1
ss.getRange(budgetStartRow,2,budgetCategoies,1).clear()
for (i=0; i<budgetCategoies; i++){
var budCat = ss.getRange(budgetStartRow+i,1).getValue()
var budFormula = `=budgetTotal(\"${budCat}\")`
ss.getRange(budgetStartRow+i,2).setFormula(budFormula)
}
}
function budgetTotal(budgetCategory) {
var sheets = ws.getSheets()
var total = 0
for (i=0; i<sheets.length; i++){
if (sheets[i].getName() != totals.getName()){
var totalFinder = sheets[i].createTextFinder(budgetCategory).findNext()
if (totalFinder == null){
total = 0
} else {
var totalValueFinder = sheets[i].getRange(totalFinder.getRow(),totalFinder.getColumn()+1).getValue()
total += totalValueFinder
}
}
}
return total
}

Automatically group new rows by time period as these are being added / recorded

So I've got this Google Sheets file where I'm using one of the sheets as a log / history for registering some values.
My time trigger which will call the function which records data to the log is set by the user. Regardless if the user chooses 1 minute interval or maybe 5, 10, 15 minutes and so on, soon the log sheet will be overpopulated. So I'd like a way for the record script I've got on the Script Editor to automatically group rows as these are being added/recorded on the log sheet automatically by the time trigger, by day and also by month simultaneously. This means Month groups and within these day subgroups and within each of these the 1 minute, or 5 etc, rows were recorded. This would improve navigation of the log sheet immensely. Is this possible?
I've got this record code on the script editor:
function RECORD_HISTORY() {
var historySheetName = "HISTORY";
var historySheet = getSheetWithName(historySheetName);
if (historySheet == null) {
historySheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(historySheetName, 1);
}
range = historySheet.getRange(2, 1, 1, 9);
var values = range.getValues();
values[0][0] = new Date();
historySheet.appendRow(values[0]);
}
function getSheetWithName(name) {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var idx in sheets) {
if (sheets[idx].getName() == name) {
return sheets[idx];
}
}
return null;
}
I know there's at least one function that has the power to edit groups etc:
shiftRowGroupDepth()
All the examples of this function were used in different situations so I couldn't figure out how to go about using this function for my needs. You guys have any idea how to go about to doing this?
Here's a dummy file for you to have a better idea of my log sheet:
https://docs.google.com/spreadsheets/d/1ExXtmQ8nyuV1o_UtabVJ-TifIbORItFMWjtN6ZlruWc/
EDIT:
I want this sheet / script to do everything automatically so neither I or the user need to open the sheet and group rows manually. I don't even want to open the file in order to press a button that calls a function that groups rows. What I want is a way or script that automatically creates groups of rows by month with its own row with the name of the month: AUGUST etc. and then within/below that row/group the days, and within these day groups, the rest of the rows with the hourly logs. This row grouping function should happen automatically, while the file is closed, and the data is being recorded to the log by the function that records data which is called through a time trigger.
I want the user to have no role whatsoever in handling information, grouping etc. The user should just open the spreadsheet in order to be able to get an easy fast interpretation of the data, and be able to navigate it easily.
Also I would like the rows to be automatically collapsed after they are created and not remain expanded because if would confuse the user.
Finally would this be easier to do if the code was combined with the recording function I wrote? "RECORD_HISTORY()"
Let me know if this was clear enough. Regarding the time zone, mine is GMT+01:00.
Thanks!
function groupRow() {
const timeZone = 'Your time zone';
const sheet = SpreadsheetApp.getActiveSheet();
const rowStart = 6;
const rows = sheet.getLastRow() - rowStart + 1;
const values = sheet.getRange(rowStart, 1, rows, 1).getValues().flat();
const o = {};
values.forEach((date, i) => {
const [m, d] = Utilities.formatDate(date, timeZone, 'yyyyMM,dd').split(',');
if (!o[m]) { o[m] = {}; }
if (!o[m][d]) { o[m][d] = []; }
o[m][d].push(rowStart + i);
});
// console.log(o);
for (const m in o) { o[m] = Object.values(o[m]); }
// console.log(o);
Object.values(o).forEach(m => {
for (const d of m) {
if (d.length === 1) { continue; }
const range = `${d[1]}:${d.slice(-1)[0]}`;
// console.log('d', range);
sheet.getRange(range).shiftRowGroupDepth(1);
}
const a = m.flat();
if (a.length === 1) { return; }
const range = `${a[1]}:${a.slice(-1)[0]}`;
// console.log('m', range);
sheet.getRange(range).shiftRowGroupDepth(1);
});
}

Trigger importHTML in Google Apps Script

I'd like to know whether it is possible to write a Google Apps Script that will trigger daily importhtml in Google spreadsheets. For example let's assume I want to import updates of following table everyday at 1 pm.
=IMPORTHTML("https://en.wikipedia.org/wiki/Lubbock,_Texas","table",5)
I need this automatic checking for table updates even when my computer is offline.
Thank you
Yes, it is possible to write a script to make a copy of the table every day.
In short, you will have to make a copy of the imported table and paste it into a new Sheet. ImportHTML a gets updated every time you open/access the spreadsheet. So only way to store table is to make a copy and paste it into a new sheet.
Here is code that does just that:
function getImportData()
{
var ss = SpreadsheetApp.getActive()
var sheet =ss.getSheetByName("ImportSheet")
if (sheet == null)
{
sheet = ss.insertSheet("ImportSheet")
sheet.getRange(1,1).setValue( "=IMPORTHTML(\"https://en.wikipedia.org/wiki/Lubbock,_Texas\",\"table\",5)")
}
var dataRange = sheet.getDataRange()
//waitforLoading waits for the function importHTML to finish loading the table, only a problem if table takes a while to load
//for more discussion on this visit : http://stackoverflow.com/questions/12711072/how-to-pause-app-scripts-until-spreadsheet-finishes-calculation
var wait = waitForLoading(dataRange,sheet, 10)
var logSheet = ss.getSheetByName("ImportLogSheet")
if (logSheet == null)
{
logSheet = ss.insertSheet("ImportLogSheet")
}
var timeStampRow = []
timeStampRow[0] = new Date()
if(wait)
{
logSheet.appendRow(timeStampRow)
var lastRow = logSheet.getLastRow() + 1
var destinationRange = logSheet.getRange(lastRow,1)
dataRange.copyTo(destinationRange, {contentsOnly:true})
}
else {
timeStampRow[1] = "Scripted timeout waiting for the table to Load "
logSheet.appendRow(timeStampRow)
}
}
function waitForLoading(dataRange, sheet, maxWaitTimeInSec)
{
// This function is only required if it takes a while for the importHTML to load your data!
// if not you can skip this function
// Function looks to see if the value of the
for(i = 0; i< maxWaitTimeInSec ; i++)
{
var value = dataRange.getCell(1,1).getValue()
if(value.search("Loading") !== -1) {
Utilities.sleep(1000);
dataRange = sheet.getDataRange()
} else {
return true
}
}
return false
}
Edit: Forgot to mention about setting up triggers. You can setup any function to be triggered using time-driven triggers. Details on how to set it up is given here: https://developers.google.com/apps-script/guides/triggers/installable#managing_triggers_manually