I have a google sheet with a timer set for 5am and everyday I want the sheet to:
Refresh, which includes some links to external data, so I add a 10 second pause. There are approximately 100 rows.
I successfully wrote a formula in column U to tell me if data is imported correctly, and if not it returns the text "ERROR".
Search for "ERROR" in column U.
If there are any "ERRORS" in column U, refresh the page again and wait 10 seconds.
Repeat until there are no "ERRORS"
Only when there are no "ERRORS" in column U, copy the values in column V and paste-values in column W.
I think I am close... I just can't figure out how to tell it to re-run the "IF" portion if still finds "ERRORS". Any input is appreciated!
Here is a link to the file -> https://docs.google.com/spreadsheets/d/1GFN3tXRlqxo9J9iNpZMPk-e-WlNGVjJ4zSqZ1_aEE_U
function HardKeyValues() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Players'), true);
SpreadsheetApp.flush();
Utilities.sleep(10000);
var findVal = spreadsheet.getRange('U:U').getValue()
if(findVal.match('ERROR')){
SpreadsheetApp.flush();
Utilities.sleep(10000)}
else{
spreadsheet.getRange('W:W').activate();
spreadsheet.getRange('V:V').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);}
}
I would not actually use the sleep timer this way
But if I did I would write it like this:
function HardKeyValues() {
const ss = SpreadsheetApp.getActive();
const psh = ss.getSheetByName('Players');
SpreadsheetApp.flush();
Utilities.sleep(10000);
const vs = psh.getRange('U1:U' + psh.getLastRow()).getDisplayValues().flat();
if (vs.find(e => e.includes("ERROR"))) {
SpreadsheetApp.flush();
Utilities.sleep(10000);
} else {
sh.getRange("V1:V" + sh.getLastRow()).copyTo(sh.getRange("W1:W" + sh.getLastRow(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false));
}
}
Using indeterminate ranges like "W:W" often leads to many nulls at the end of the array.
What about using a while loop ?
function HardKeyValues() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Players'), true);
SpreadsheetApp.flush();
Utilities.sleep(10000);
var findVal = spreadsheet.getRange('U:U').getValue()
while (findVal.match('ERROR')){
SpreadsheetApp.flush();
Utilities.sleep(10000)}
findVal = spreadsheet.getRange('U:U').getValue()
}
spreadsheet.getRange('W:W').activate();
spreadsheet.getRange('V:V').copyTo(spreadsheet.getActiveRange(),
SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}
You could maybe try using a for loop, just make sure to have a variable that can eventually meet the requirement for the for loop to stop, otherwise, it will turn into a forever loop ;)
Related
I am new to coding Scripts but have managed to copy and edit (bodge...) a script that will onEdit, put a custom timestamp into the cell next to the cell where the edit took place. The script targets x2 onEdit columns (3,6), timestamping the cell offset(-1).
The workbook has developed to include multiple sheets now, most of which this script is not appropriate to run on, but I cannot figure out how to specify which sheets it should run on.
The below is the script I have cobbled together which works across all sheets, which I would now like to restrict to specific sheets only.
function onEdit(e) {
var colsToWatch = [3,6],
offset = [-1,-1],
ind = colsToWatch.indexOf(e.range.columnStart);
if (ind === -1 || e.range.rowStart === 1) return;
e.range.offset(0, offset[ind])
.setValue(!e.value ? null : Utilities.formatDate(new Date(), "GMT+1", "dd MMM yyyy HH:mm.ss"))
}
Please can someone help me by providing the code to run the scripts on specific sheets only. I would also be grateful for any suggestions of a simpler script to achieve the same aim, especially if it eliminates the timestamp being overwritten if the onEdit cell is subsequently edited after the initial edit.
Many thanks!!
function onEdit(e) {
const sh = e.range.getSheet();
const shts = ['Sheet1','Sheet2'];
const idx = shts.indexOf(sh.getName());
if(~idx ) {
//Whatever code you put here will only run on Sheet1 or Sheet2
}
}
This works for me (on on Sheet1 and Sheet2):
function onEdit(e) {
//e.source.toast('entry');
const sh = e.range.getSheet();
const shts = ['Sheet1','Sheet2'];
const idx = shts.indexOf(sh.getName());
if(~idx ) {
let rg = sh.getRange(e.range.rowStart,9);
if(rg.isBlank()) {
rg.setValue(new Date());
}
}
}
JavaScript Reference
I've been trying for hours to make the following Google Apps Script work. What it needs to do, is send emails (from an html-template) to anyone that:
has a complete Event Schedule (which is completed if they have been
assigned to at least 4 events, which is counted in column Q);
has NOT been sent an email earlier (which is kept track of in column
R);
The script keeps track of errors in column S, i.e. if there's no email address provided.
It appears it only works:
if I comment out
data = data.filter(function(r){ return r[17] == true & r[16] > 3});
or if I comment out
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors);
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
How can I get this script to work properly?
A copy of the Google Sheet I'm referring to is this one:
https://docs.google.com/spreadsheets/d/1sbOlvLVVfiQMWxNZmtCLuizci2cQB9Kfd8tYz64gjP0/edit?usp=sharing
This is my code so far:
function SendEmail(){
var voornaam = 3;
var achternaam = 4;
var email = 5;
var event1 = 9;
var event2 = 10;
var event3 = 11;
var event4 = 12;
var event5 = 13;
var event6 = 14;
var event7 = 15;
var emailTemp = HtmlService.createTemplateFromFile("email");
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Events Day 1");
var datum = ws.getRange(1,3).getValue();
var spreker = ws.getRange(1,6).getValue();
var data = ws.getRange("A3:R" + ws.getLastRow()).getValues();
data = data.filter(function(r){ return r[17] == false && r[16] > 3}); //Either this needs to be commented out...
let errors = [];
let mailSucces = [];
data.forEach(function(row){
try{
emailTemp.voornaam = row[voornaam];
emailTemp.email = row[email];
emailTemp.datum = datum;
emailTemp.spreker = spreker;
emailTemp.event1 = row[event1];
emailTemp.event2 = row[event2];
emailTemp.event3 = row[event3];
emailTemp.event4 = row[event4];
emailTemp.event5 = row[event5];
emailTemp.event6 = row[event6];
emailTemp.event7 = row[event7];
var htmlMessage = emailTemp.evaluate().getContent();
GmailApp.sendEmail(
row[email],
"Here you go! Your personal schedule for the event of " + datum,
"Your emailprogramm doesn't support html.",
{
name: "Event Organisation Team", htmlBody: htmlMessage, replyTo: "info#fakeemail.com"
});
errors.push([""]);
mailSucces.push(["TRUE"]);
}
catch(err){
errors.push(["Error: no message sent."]);
mailSucces.push(["False"]);
}
}); //close forEach
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors); //or this and the next line need to be commented out.
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
}
Edit I have been trying and thinking en trying... but still haven't found out how to make it work. But I also got understanding of why it's not working; I just don't know how to get it fixed.
Let me elaborate on the problem a bit more:
The problem is, that within the forEach loop the range is a filtered variant of the data, pulled from the spreadsheet with getValues. Therefore, writing data back with ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces); results in mismatched checkmarks in te spreadsheet.
So, somehow I need to put the range of the previous used filter data = data.filter(function(r){ return r[17] == false & r[16] > 3}); in a variable...? I guess?
Furthermore, I don't think it's wise to use setValue within the loop, because (from what I understand from my searching on the topic) this results in a slow script, because every loop the script makes an API call to write in the spreadsheet. Hence the errors.push and mailSucces.push, and my attempt to do a setValue at the end, after the loop is finished.
Can someone help me to finish this problem?
The problem is different size of the range you write to and data you are writing in.
Try replacing:
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors);
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
With:
ws.getRange(3, 19, errors.length, 1).setValues(errors);
ws.getRange(3, 18, mailSucces.length, 1).setValues(mailSucces);
You should use this variation of getRange
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow,-column,-numrows,-numcolumns
Your data has non-fixed number of rows and fixed number of columns (1). In general case your data will be matrix of X rows and Y columns. For that purpose you can make it completely dynamic:
sheet.getRange(startRow, startColumn, data.length, data[0].length)
Just make sure data.length is > 0 before you do this, otherwise data[0].length will break.
Edit:
I started writing a comment but it got too long. There are couple of things that may go wrong with sending emails. First thing I noticed is that you use & in filter, but in AppsScript/JavaScript/C-like-languages, you should use && for logical AND. Now the email: you only detect the code break with the catch block. At this point you don't know why the code breaks it could be anything. With GmailApp I recommend you to use createDraft while developing, then when all ok replace it with sendEmail for the final version, both functions have the exact same parameters, thank you Google devs ;-).
To find out the exact problem you should get the error message on break and display it. err.stack should tell you pretty much everything:
catch(err){
Logger.log(err.stack); // Added
errors.push(["Error: no message sent."]);
mailSucces.push(["False"]);
}
Run the sendEmail function from the code editor and you should see the Log for each catch(err) pass.
For some reason, my code that was working as of yesterday has quit working today. I can't seem to identify what I did to cause the issue and could use some more eyes on it. It was a really simple process but I am extremely new to the language, so I'm sure I'm missing something.
The issue is that the for loop used to update/paste to cell B2 on the sheet, but now it doesn't do that until after I cancel the code or it ends (using whatever the most recent value for num was). It's causing the information to not be updated and so all I get is the information associated with whatever is in cell B2 pasted all the way down through the end of the rows. It used to wait until B2 was updated before copying and pasting to the next row, which took a while but I still had more than enough time before the 5 minute limit since I'm not running a ton of data. My only thought is that I must've made a minor edit that changed this, but I can't track it down in version history. Any help would be greatly appreciated.
function CopyandPasteNewData2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('sheet1');
var lastRow = sh.getLastRow();
var num = 6;
for(var i = 6; i<=lastRow; i++)
{
num;
sh.getRange('A'+num).copyTo(sh.getRange('B2'), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false)
sh.getRange('D2:H2').copyTo(sh.getRange('B'+num+':F'+num), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false)
num++;
}
};
When you iteratively modify the values of a sheet it is always a good practice to use flush() to apply all pending Spreadsheet changes.
Please try the following small modification:
function CopyandPasteNewData2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('sheet1');
var lastRow = sh.getLastRow();
var num = 6;
for(var i = 6; i<=lastRow; i++)
{
num;
sh.getRange('A'+num).copyTo(sh.getRange('B2'), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false)
sh.getRange('D2:H2').copyTo(sh.getRange('B'+num+':F'+num), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false)
SpreadsheetApp.flush(); // <- new code
num++;
}
};
This is the script that I am using currently. I want it to apply to all sheets in my document automatically. Please help. The following is the code.
/**
* Creates a Date Stamp if a column is edited.
*/
//CORE VARIABLES
// The column you want to check if something is entered.
var COLUMNTOCHECK = 1;
// Where you want the date time stamp offset from the input location. [row, column]
var DATETIMELOCATION = [0,1];
// Sheet you are working on
var SHEETNAME = '46'
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
//checks that we're on the correct sheet.
if( sheet.getSheetName() == SHEETNAME ) {
var selectedCell = ss.getActiveCell();
//checks the column to ensure it is on the one we want to cause the date to appear.
if( selectedCell.getColumn() == COLUMNTOCHECK) {
var dateTimeCell = selectedCell.offset(DATETIMELOCATION[0],DATETIMELOCATION[1]);
dateTimeCell.setValue(new Date());
}
}
}
I want the script to execute on every sheet so that whenever I add a new sheet the script runs automatically and helps me add a time stamp to column two when I add a value to column 1.
Please help.
I am a complete noob here and as such some detailed explanation will help me immensely.
Basically, just remove the if statement and the code will apply to EVERY sheet.
In the answer given below, I've also used some of the objects returned by the onEdit trigger.
/**
* Creates a Date Stamp if a column is edited.
*/
//CORE VARIABLE
// The column you want to check if something is entered.
var COLUMNTOCHECK = 1;
function onEdit(e) {
var sh = e.range.getSheet();
if (e.range.columnStart == COLUMNTOCHECK){
// edited the right column do something
var targetcell = sh.getRange(e.range.rowStart,2);
targetcell.setValue(new Date());
}
else {
// do nothing
}
}
I have a small script and what I'm trying to do is to write one value from 'Sheet 1' to 'Sheet 2'. Wait for the results to load and compare the cells to see if it is above 10% or not. I have some =importhtml functions in the spreadsheet and it takes along time to load. I've tried sleep, utilities sleep, and flush. None have been working, maybe because I might be putting it in the wrong area..
function compareCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var listSheet = ss.getSheetByName('Stocks');
var dataSheet = ss.getSheetByName('Summary');
var listSheetLastRow = listSheet.getLastRow();
var currRow = 1;
for (i = 1; i <= listSheetLastRow; i++) {
if (listSheet.getRange(1, 3).getValue() == 'asd') {
var ticker = listSheet.getRange(currRow, 1).getValue();
dataSheet.getRange(5, 4).setValue(ticker);
var value1 = dataSheet.getRange(15, 4).getValue();
var value2 = dataSheet.getRange(22, 4).getValue();
SpreadsheetApp.flush();
if (value1 > 0.10 && value2 > 0.10) {
listSheet.getRange(currRow, 8).setValue('True');
listSheet.getRange(currRow, 9).setValue(value1);
listSheet.getRange(currRow, 10).setValue(value2);
} else {
listSheet.getRange(currRow, 8).setValue('False');
}
} else {
Browser.msgBox('Script aborted');
return null;
}
currRow++;
}
}
If it is not important that you use the =IMPORTHTML() function in your sheet, the easiest way to do this will be to use UrlFetchApp within Apps Script. Getting the data this way will cause your script to block until the HTML response is returned. You can also create a time-based trigger so your data is always fresh, and the user will not have to wait for the URL fetch when looking at your sheet.
Once you get the HTML response, you can do all of the same processing you'd do in Sheet1 within your script. If that won't work because you have complex processing in Sheet1, you can:
use UrlFetchpApp.fetch('http://sample.com/data.html') to retrieve your data
write the data to Sheet1
call SpreadsheetApp.flush() to force the write and whatever subsequent processing
proceed as per your example above
By handling these steps sequentially in your script you guarantee that your later steps don't happen before the data is present.
I had a similar problem but came up with a solution which uses a while loop which forces the script to wait until at least 1 extra column or 1 extra row has been added. So for this to work the formula needs to add data to at least one extra cell other than the one containing the formula, and it needs to extend the sheet's data range (number of rows or columns), for example by adding the formula to the end of the sheet, which looks like what you are doing. Every 0.5 seconds for 10 seconds it checks if extra cells have been added.
dataSheet.getRange(5, 4).setValue(ticker);
var wait = 0;
var timebetween = 500;
var timeout = 10000;
var lastRow = dataSheet.getLastRow();
var lastColumn = dataSheet.getLastColumn();
while (dataSheet.getLastColumn() <= lastColumn && dataSheet.getLastRow() <= lastRow){
Utilities.sleep(timebetween);
wait += timebetween;
if (wait >= timeout){
Logger.log('ERROR: Source data for ' + ticker + ' still empty after ' + timeout.toString() + ' seconds.');
throw new Error('Source data for ' + ticker + ' still empty after ' + timeout.toString() + ' seconds.');
}
}
In case if you are getting these two values (
var value1 = dataSheet.getRange(15, 4).getValue();
var value2 = dataSheet.getRange(22, 4).getValue();
) after the =importhtml call, you have to add sleep function before these two lines of code.
You also can have a loop until you get some values into the range from =importhtml call and add some sleep in the loop. Also note that as of April 2014 the limitation of script runtime is 6 minutes.
I also found this link which might be helpful.
Hope that helps!