Find Value in Google Sheet App Script - google-apps-script

I've been searching around for a script that finds text within a range. I found a couple and modified this code for my particular needs. All I'm doing is creating a "login" type system. The user inputs their email (E5) on one spreadsheet and then when I hit the button it runs the script to searches another spreadsheet that contains emails (A1:A) and sub status in B - so total range is (A1:B).
Problem is, the script only works if the email matches cell A1. So if Briansmith#hotmail.com is in (E5) or what the user types in, and on the other spreadsheet in A1 Briansmith#hotmail.com is there - it works. But if I moved it down to A5 or something else than A1, it no longer works. I'm confused why the for loop isn't continuing to go down the column to look for matching text.
Here's the code I'm using:
function loginFunction() {
var s = SpreadsheetApp.getActiveSheet();
//Username is in (E5)
var email = s.getRange("E5").getValue();
var data = SpreadsheetApp.openById('Deleted for privacy').getRange("A1:B")
var value = data.getValues();
for (var i = 0; i < value.length; i++) {
if (value[i][0] === email) {
if (value[i][1] === "yes") {
//Email is located (col A) and sub (col B) is yes
Logger.log('Yes');
return SpreadsheetApp.getUi().alert('Connected!');
} else {
//Email is located (col A) and sub (col B) is no
Logger.log('Not Sub');
return SpreadsheetApp.getUi().alert('No Subscription Found');
}
} else {
Logger.log('False');
return SpreadsheetApp.getUi().alert('Not Connected');
//Email not found
}
}
}

You're using return after all three condition checks. You should continue the loop:
Try modifying from:
} else {
Logger.log('False');
return SpreadsheetApp.getUi().alert('Not Connected');
//Email not found
to
} else if(i==value.length-1){ //if i is the last value
Logger.log('False');
return SpreadsheetApp.getUi().alert('Not Connected');
//Email not found

Related

Sending email reminders for Google Sheet tasks

Updated the code based on suggestions below, The email does not contain the summary, any help fix this would be appreciated! The test file is attached below,
function sendEmail(){
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("2021-12 {3600950}").activate();
var ss =
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
//data on sheet, filter for any row where the status is still "assigned"
var data = ss.getDataRange().getValues()
var assigned = data.reduce(function(acc, curr) {
if(curr[5] === "assigned") {
acc.push(curr)
}
return acc
},[])
// unique list of emails
var Assignee = [0]
var Summary = [2]
var compareAssignee = []
for (i=0; i<assigned.length; i++){
compareAssignee.push(assigned[i][0])
}
//loop unique emails, all the tasks for each of those users, and send a simple email with the tasks
for (var i=0; i<Assignee.length; i++){
var Summary = assigned.reduce(function(acc, curr) {
if(curr[0] === Assignee[i])
{
acc.push(String.fromCharCode() + "pending task: " + curr[2] +
Summary[2])
//this puts a line break before each task, so when the email is
sent each one is on its own line.
}
return acc
},[])
console.log(Summary)
MailApp.sendEmail
MailApp.sendEmail(compareAssignee[0],"pending RPP task(s)",Summary[2])
}
}
function scheduleEmails(){
// Schedules for the first of every month
ScriptApp.newTrigger("sendEmail")
.timeBased()
.onMonthDay(28)
.atHour(1)
.create();
}
You want to send an email at the end of every month to users who have tasks that are still in "assigned" status.
The sendEmail script below finds all the tasks for each user and sends an email to them listing each of their tasks that are still "assigned". EDIT: You indicated in a comment above that emails are in Col 1, tasks are in Col 3 and status is in Col6. I updated the code to reflect that below.
Check out this sample email to see the results.
The second function creates a trigger that runs sendEmail every month. You indicated you wanted to send the email on the last day of the month, but it seems Google has a hard time with that. Some other folks came up with workarounds. I like this one: send the reminder on the 1st of the month, but at 1 in the morning! You can see more of their work here
function sendEmail(){
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("status").activate();
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
//grab all the data in the sheet, and then filter for any row where the status is still "assigned"
var data = ss.getDataRange().getValues()
var assigned = data.reduce(function(acc, curr) {
if(curr[5] === "assigned") {
acc.push(curr)
}
return acc
},[])
// From all the tasks still in "assigned" status, get a unique list of emails.
var uniqueEmails = []
var compareEmails = []
for (i=0; i<assigned.length; i++){
compareEmails.push(assigned[i][0])
}
uniqueEmails = [...new Set(compareEmails)]
//loop through the unique emails, grab all the tasks for each of those users, and send a simple email with the tasks listed.
for (var i=0; i<uniqueEmails.length; i++){
var tasksPerUser = assigned.reduce(function(acc, curr) {
if(curr[0] === uniqueEmails[i]) {
acc.push(String.fromCharCode(10) + "pending task: " + curr[2]) //this puts a line break before each task, so when the email is sent each one is on its own line.
}
return acc
},[])
console.log(tasksPerUser)
MailApp.sendEmail(uniqueEmails[i],"pending tasks",tasksPerUser)
}
}
function scheduleEmails(){
// Schedules for the first of every month
ScriptApp.newTrigger("sendEmail")
.timeBased()
.onMonthDay(1)
.atHour(1)
.create();
}

Google (Docs) Apps Script - Can't check if cursor on named range

I am inserting text into a document and each text insertion is added to a named range so that I can look them all up with getNamedRanges(NAME) and getNamedRangesById(ID).
Now I need to check if the current cursor position is on a named range and I have yet to figure out how.
This post is similar:
How to determine the named range from position in Google Docs through Google Apps Script
But when the cursor is on a namedrange cursor.getElement() returns Text object, not the named range.
How can I determine if the cursor is currently positioned on a named range?
You want to confirm whether the current cursor position is inside in the namedRange on Google Document.
You want to achieve this using Google Apps Script.
Workaround:
In this workaround, I checked whether the cursor position is included in the namedRange by comparing the indexes of paragraph of both the namedRange and the cursor position.
Flow:
The flow of the script is as follows.
Retrieve the indexes of paragraph of the namedRange.
I this sample script, from your question, the namedRange ID is used.
In this case, there might be multiple paragraphs including table, list and so on. So all indexes in the namedRange are retrieved.
Retrieve the index of paragraph of the cursor position.
Retrieve the index of paragraph of the selected range.
This sample script also checks whether the selected range is in the namedRange. Because when the text is selected, cursor becomes null.
If the cursor or selected range are staying in the namedRange, myFunction() returns true.
If the cursor or selected range are not staying in the namedRange, myFunction() returns false.
Also you can confirm it at the log.
Sample script:
Before you use this script, please set the namedRange ID.
function myFunction() {
var nameRangeId = "###"; // Please set namedRange ID here.
var getIndex = function(doc, e) {
while (e.getParent().getType() != DocumentApp.ElementType.BODY_SECTION) e = e.getParent();
return doc.getBody().getChildIndex(e);
};
var doc = DocumentApp.getActiveDocument();
// For namedRange
var namedRange = doc.getNamedRangeById(nameRangeId);
if (namedRange) {
var indexOfNamedRange = namedRange.getRange().getRangeElements().map(function(e) {return getIndex(doc, e.getElement())});
} else {
throw new Error("No namedRange.");
}
var name = namedRange.getName();
// For cursor
var cursor = doc.getCursor();
if (cursor) {
var indexOfCursor = getIndex(doc, cursor.getElement());
if (~indexOfNamedRange.indexOf(indexOfCursor)) {
Logger.log("Inside of %s", name);
return true;
}
Logger.log("Outside of %s", name);
return false;
}
// For select
var select = doc.getSelection();
if (select) {
var indexOfSelect = select.getRangeElements().map(function(e) {return getIndex(doc, e.getElement())});
if (indexOfSelect.some(function(e) {return ~indexOfNamedRange.indexOf(e)})) {
Logger.log("Inside of %s", name);
return true;
}
Logger.log("Outside of %s", name);
return false;
}
throw new Error("No cursor and select.");
}
Note:
In this script, when the text is selected on Document, the cursor position cannot be retrieved. So I added the function to check the selected range. If you don't want to check the selected range, please remove the script of // For select.
In this script, even only one index of selected range are included in the namedRange, true is returned. About this, please modify for your situation.
In the current stage, this script doesn't suppose about the header and footer sections.
References:
getNamedRangeById()
getCursor()
getSelection()
getChildIndex()
Added:
I had understood that from this situation, OP has set the named range to the paragraph. When I proposed a sample script for this, I thought that I correctly understood OP's goal. But, from gaspar's following comment,
this only shows whether the cursor is in the same element as the named range, but in case of named range partial text it gives a false positive finding if the cursor is in the same element but not in the same text part
If OP sets the part of the paragraph as the named range, and OP wants to check whether the cursor is included in the named range, the sample script is as follows.
Sample script:
function myFunction() {
var nameRangeId = "###"; // Please set namedRange ID here.
var getIndex = function (doc, e) {
while (e.getParent().getType() != DocumentApp.ElementType.BODY_SECTION) e = e.getParent();
return doc.getBody().getChildIndex(e);
};
var doc = DocumentApp.getActiveDocument();
// For namedRange
var namedRange = doc.getNamedRangeById(nameRangeId);
if (namedRange) {
var indexOfNamedRange = namedRange.getRange().getRangeElements().map(e => ({ idx: getIndex(doc, e.getElement()), start: e.getStartOffset(), end: e.getEndOffsetInclusive() }));
} else {
throw new Error("No namedRange.");
}
var name = namedRange.getName();
// For cursor
var cursor = doc.getCursor();
if (cursor) {
var indexOfCursor = getIndex(doc, cursor.getElement());
var offset = cursor.getOffset();
if (indexOfNamedRange.some(({ idx, start, end }) => idx == indexOfCursor && ((start == -1 && end == -1) || (offset > start && offset < end)))) {
Logger.log("Inside of %s", name);
return true;
}
Logger.log("Outside of %s", name);
return false;
}
// For select
var select = doc.getSelection();
if (select) {
var indexOfSelect = select.getRangeElements().map(e => ({ idx: getIndex(doc, e.getElement()), start: e.getStartOffset(), end: e.getEndOffsetInclusive() }));
if (indexOfSelect.some(e => indexOfNamedRange.some(({ idx, start, end }) => idx == e.idx && ((start == -1 && end == -1) || ((e.start > start && e.start < end) || (e.end > start && e.end < end)))))) {
Logger.log("Inside of %s", name);
return true;
}
Logger.log("Outside of %s", name);
return false;
}
throw new Error("No cursor and select.");
}
When I posted my answer, Google Apps Script cannot use V8 runtime. But, now, V8 runtime can be used. So I modified the script using V8 runtime. Please be careful about this.
The solution proposed in the post to which you refer implies looping through your range of interest and checking if one of your range elements equals the element on which the cursor lies.
The code should look like this:
function myFunction() {
var doc = DocumentApp.getActiveDocument();
var cursor = doc.getCursor();
var el=cursor.getElement().asText().getText();
var range;
//specify the name of the range of interest in getNamedRanges()
doc.getNamedRanges('testy').forEach(function(rangeEntry){
(rangeEntry.getRange().getRangeElements().forEach(function(element){
var child=element.getElement().getText();
if(child==el){
Logger.log("Cursor is on named range "+rangeEntry.getName())
}
}))
})
}
Since I also needed this and none of the previous answers work correctly (I tested them all; see my comments), I wrote my own function that actually works, see below.
const cursorIndex = getIndex(cursor.getElement())
let found = false;
let rangeIndex = 0;
// note: to search for any named range, just omit the name
// (hence just give "doc.getNamedRanges()")
// then you can get the name of the found named range via "getName()"
for (const rangeEntry of doc.getNamedRanges('the_named_range_name')) {
for (const element of rangeEntry.getRange().getRangeElements()) {
rangeIndex = getIndex(element.getElement());
if (cursorIndex === rangeIndex) {
if (element.isPartial()) {
let cursorOffset = cursor.getSurroundingTextOffset()
if (cursorOffset >= element.getStartOffset() && cursorOffset <= element.getEndOffsetInclusive() + 1) {
found = true;
break;
}
} else {
found = true;
break;
}
}
}
if (found || rangeIndex > cursorIndex) {
break;
}
}
if (found) {
DocumentApp.getUi().alert("There is a named range here.");
}
(With small modifications, this could also be an answer to this question.)

Importing data from different Spreadsheets - cache issue google app script

*,
Specs:
import data from a target sheet (A) to another (B) in a different Google Spreadsheet;
data on B sheet need to be filtered/sorted by user without affecting A sheet
when A data change, B data should update too (live or at least on refresh/push a button)
(optional) import B sheet notes into A sheet
Structure of A sheet (and then B sheet which is a mirror basically) is a list of items where every item has a column "ID".
Originally I tried IMPORTRANGE which works great with live updates, but unfortunately on B sheet user cannot use native filters to sort/filter data.
I wrote this custom function:
function importSingleItemData(idItem) {
//vars for debugging
//var idItem = 1;
// Id of spreadsheet where data are contained
var inKey = "xxxxx";
// Actual code
var outData;
var idItemColumn;
var ss = SpreadsheetApp.openById(inKey); // target sheet
// 1. Import idItemColumn
if (ss) {
idItemColumn = ss.getRange("sheet1!A1:A500").getValues();
// 2. find id_property row
for (var i = 0; i < idItemColumn.length; i++){
if(idItemColumn[i][0] == idItem){
var idFound = idItemColumn[i][0];
// 3. import property availability range
var row = i+1;
var RangeString = "sheet1!B"+row + ":AM"+row;
var range = ss.getRange(RangeString);
// copy formatting
// range.copyFormatToRange(range.getGridId(), 3, 4,5,7); !not working
outData = range.getValues();
break;
}
}
return outData;
}
}
Where I try to locate the Id of the item and import the interested data of that row. Then I apply it on B sheet using =importSingleItemData(A1) where A1 contains the id of item =1; A2 = 2, etc like
ID ItemData
1 =importSingleItemData(A1)
2 =importSingleItemData(A2)
...
This works great, the problem is that it does not update data on B sheet when A changes. I read a few posts on stackoverflow about this caching beaviour and tried a few things with no luck (like adding time to import, which is no longer supported), also tried setValue method which does not work with custom function.
I was now thing some combination of VLookup/Hlookup with IMPORTRANGE, not sure whether this will work.
Any tips how to sort this out guys?
Thanks in advance!!
If your working with alot of data between two different areas and matching up alot of info. I made script based vlookup. It maybe helpful in the future.
//-------------------------------------------------(Script Vlookup)------------------------------------------------//
/*
Benefit of this script is:
-That google sheets will not continually do lookups on data that is not changing with using this function
-Unlike Vlookup you can have it look at for reference data at any point in the row. Does not have to be in the first column for it to work like Vlookup.
Useage:
var LocNum = SpreadsheetApp.openById(SheetID).getSheetByName('Sheet1').getRange('J2:J').getValues();
FinderLookUpReturnArrayRange_(LocNum,0,'Data','A:G',[3],'test',1,1,'No');
-Loads all Locations numbers from J2:J into a variable
--looks for Location Numbers in Column 0 of Referance sheet and range eg "Data!A:G"
---Returns results to Column 3 of Target Sheet and range eg "test!A1" or "1,1"
*/
function FinderLookUpReturnArrayRange_(Search_Key,SearchKey_Ref_IndexOffSet,Ref_Sheet,Ref_Range,IndexOffSetForReturn,Set_Sheet,Set_PosRow,Set_PosCol,ReturnMultiResults)
{
var twoDimensionalArray = [];
var data = SpreadsheetApp.getActive().getSheetByName(Ref_Sheet).getRange(Ref_Range).getValues(); //Syncs sheet by name and range into var
for (var i = 0, Il=Search_Key.length; i<Il; i++) // i = number of rows to index and search
{
var Sending = []; //Making a Blank Array
var newArray = []; //Making a Blank Array
var Found ="";
for (nn=0,NNL=data.length;nn<NNL;nn++) //nn = will be the number of row that the data is found at
{
if(Found==1 && ReturnMultiResults=='No') //if statement for found if found = 1 it will to stop all other logic in nn loop from running
{
break; //Breaking nn loop once found
}
if (data[nn][SearchKey_Ref_IndexOffSet]==Search_Key[i]) //if statement is triggered when the search_key is found.
{
var newArray = [];
for (var cc=0,CCL=IndexOffSetForReturn.length;cc<CCL;cc++) //cc = numbers of columns to referance
{
var iosr = IndexOffSetForReturn[cc]; //Loading the value of current cc
var Sending = data[nn][iosr]; //Loading data of Level nn offset by value of cc
if(isEmpty_(Sending)==true) //if statement for if one of the returned Column level cells are blank
{
var Sending = "#N/A"; //Sets #N/A on all column levels that are blank
}
if (CCL>1) //if statement for multi-Column returns
{
newArray.push(Sending);
if(CCL-1 == cc) //if statement for pulling all columns into larger array
{
twoDimensionalArray.push(newArray);
Logger.log(twoDimensionalArray);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
else if (CCL<=1) //if statement for single-Column returns
{
twoDimensionalArray.push(Sending);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
}
if(NNL-1==nn && isEmpty_(Sending)==true) //following if statement is for if the current item in lookup array is not found. Nessessary for data structure.
{
for(var na=0,NAL=IndexOffSetForReturn.length;na<NAL;na++) //looping for the number of columns to place "#N/A" in to preserve data structure
{
if (NAL<=1) //checks to see if it's a single column return
{
var Sending = "#N/A";
twoDimensionalArray.push(Sending);
}
else if (NAL>1) //checks to see if it's a Multi column return
{
var Sending = "#N/A";
newArray.push(Sending);
}
}
if (NAL>1) //checks to see if it's a Multi column return
{
twoDimensionalArray.push(newArray);
}
}
}
}
if(typeof Set_PosRow != "number") //checks to see if what kinda of variable Set_PosRow is. if its anything other than a number it will goto next avaible row
{
var Set_PosRow = getFirstEmptyRowUsingArray_(Set_Sheet); //for usage in a database like entry without having to manually look for the next level.
}
for (var l = 0,lL=Search_Key.length; l<lL; l++) //Builds 2d Looping-Array to allow choosing of columns at a future point
{
if (CCL<=1) //checks to see if it's a single column return for running setValue
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow + l,Set_PosCol).setValue(twoDimensionalArray[l]);
}
}
if (CCL>1) //checks to see if it's a multi column return for running setValues
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow,Set_PosCol,twoDimensionalArray.length,twoDimensionalArray[0].length).setValues(twoDimensionalArray);
}
SpreadsheetApp.flush();
}
//*************************************************(Script Vlookup)*************************************************//
And some helper Functions
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//Copy this block of fucnctions as they are used in the Vlookup Script
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//-------------------------------------------------(Find Last Row on Database)------------------------------------------------//
function getFirstEmptyRowUsingArray_(sheetname)
{
var data = SpreadsheetApp.getActive().getSheetByName(sheetname).getDataRange().getValues();
for(var n = data.length ; n<0 ; n--)
{
if(isEmpty_(data[n][0])=false)
{
n++;
break;
}
}
n++
return (n);
}
//*************************************************(Find Last Row on Database)*************************************************//
//-------------------------------------------------(Blank Array Extractor/Rebuilder)------------------------------------------------//
function cleanArray_(actual)
{
var newArray = new Array();
for(var i = 0; i<actual.length; i++)
{
if (isEmpty_(actual[i]) == false)
{
newArray.push(actual[i]);
}
}
return newArray;
}
//*************************************************(Blank Array Extractor/Rebuilder)*************************************************//
//-------------------------------------------------(Even/Odd)------------------------------------------------//
function isEven_(value) {
if (value%2 == 0)
return true;
else
return false;
}
//*************************************************(Even/Odd)*************************************************//
//-------------------------------------------------(Array Col Sum Agent)------------------------------------------------//
function SumColArray_(sumagent)
{
var newArray = new Array();
for(var i = 0; i<sumagent.length; i++)
{
var totalsum = 0
var CleanForSum = cleanArray_(sumagent[i]);
for(var d = 0; d<CleanForSum.length; d++)
{
totalsum += CleanForSum[d];
}
newArray.push(Math.round(totalsum));
}
return newArray;
}
//*************************************************(Array Col Sum Agent)*************************************************//
//-------------------------------------------------(Empty String Check)------------------------------------------------//
function isEmpty_(string)
{
if(!string) return true;
if(string == '') return true;
if(string === false) return true;
if(string === null) return true;
if(string == undefined) return true;
string = string+' '; // check for a bunch of whitespace
if('' == (string.replace(/^\s\s*/, '').replace(/\s\s*$/, ''))) return true;
return false;
}
//*************************************************(Empty String Check)*************************************************//
Eventually I sorted out with native functions filtering on a single row by id
=IFERROR(FILTER(IMPORTRANGE("key";"sheet1!B1:AN300");IMPORTRANGE("key";"sheet1!A1:A300") = id))

Identifying Form destination (Spreadsheet AND SHEET)

I'm working on a script that interacts with Google Form' response sheet.
FormApp.getActiveForm().getDestinationId()
give me the spreadsheet id, but I don't find a way to get the sheet itself. User can change its name and position, so I need to get its id, like in
Sheet.getSheetId()
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form:
Form.getItems().length
and then search for gridItems, add the number of rows in each and add them minus one:
+ gridItem.getRows().length - 1
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.
Thank you
#tehhowch came very close to the correct answer, but there is a problem with the code: there is no guarantee that form.getPublishedUrl() and sheet.getFormUrl() will return exactly the same string. In my case, form.getPublishedUrl() returned a URL formed as https://docs.google.com/forms/d/e/{id}/viewform and sheet.getFormUrl() returned https://docs.google.com/forms/d/{id}/viewform. Since the form id is part of the URL, a more robust implementation would be:
function get_form_destination_sheet(form) {
const form_id = form.getId();
const destination_id = form.getDestinationId();
if (destination_id) {
const spreadsheet = SpreadsheetApp.openById(destination_id);
const matches = spreadsheet.getSheets().filter(function (sheet) {
const url = sheet.getFormUrl();
return url && url.indexOf(form_id) > -1;
});
return matches.length > 0 ? matches[0] : null;
}
return null;
}
There is now a way to verify which sheet in a Google Sheets file with multiple linked forms corresponds to the current Form - through the use of Sheet#getFormUrl(), which was added to the Sheet class in 2017.
function getFormResponseSheet_(wkbkId, formUrl) {
const matches = SpreadsheetApp.openById(wkbkId).getSheets().filter(
function (sheet) {
return sheet.getFormUrl() === formUrl;
});
return matches[0]; // a `Sheet` or `undefined`
}
function foo() {
const form = FormApp.getActiveForm();
const destSheet = getFormResponseSheet_(form.getDestinationId(), form.getPublishedUrl());
if (!destSheet)
throw new Error("No sheets in destination with form url '" + form.getPublishedUrl() + "'");
// do stuff with the linked response destination sheet.
}
If you have unlinked the Form and the destination spreadsheet, then obviously you won't be able to use getDestinationId or getFormUrl.
I needed this also, and remarkably there is still no apps script method that facilitates it. In the end I set about finding a reliable way to determine the sheet id, and this is what I ended up with by way of programmatic workaround:
Add a temporary form item with a title that's a random string (or something similarly suitable)
Wait for the new corresponding column to be added to the destination sheet (typically takes a few seconds)
Look though each sheet in the destination until you find this new form item title string in a header row
Delete the temporary form item that was added
Wait for the corresponding column in the sheet to unlink from the form and become deletable (typically takes a few seconds)
Delete the column corresponding to the temporary form item
Return the sheet ID
I'm sure some won't like this approach because it modifies the form and spreadsheet, but it does work well.
With the necessary wait times included it takes about 12 seconds to perform all the look up / clean up operations.
Here's my code for this method in case anyone else might like to use it.
// Takes Apps Script 'Form' object as single paramater
// The second parameter 'obj', is for recursion (do not pass a second parameter)
// Return value is either:
// - null (if the form is not linked to any spreadsheet)
// - sheetId [int]
// An error is thrown if the operations are taking too long
function getFormDestinationSheetId(form, obj) {
var obj = obj || {}; // Initialise object to be passed between recursions of this function
obj.attempts = (obj.attempts || 1);
Logger.log('Attempt #' + obj.attempts);
if (obj.attempts > 14) {
throw 'Unable to determine destination sheet id, too many failed attempts, taking too long. Sorry!';
}
obj.spreadsheetId = obj.spreadsheetId || form.getDestinationId();
if (!obj.spreadsheetId) {
return null; // This means there actually is no spreadsheet destination set at all.
} else {
var tempFormItemTitle = '### IF YOU SEE THIS, PLEASE IGNORE! ###';
if (!obj.tempFormItemId && !obj.sheetId) { // If the sheet id exists from a previous recusion, we're just in a clean up phase
// Check that temp item does not already exist in form
form.getItems(FormApp.ItemType.TEXT).map(function(textItem) {
var textItemTitle = textItem.getTitle();
Logger.log('Checking against form text item: ' + textItemTitle);
if (textItemTitle === tempFormItemTitle) {
obj.tempFormItemId = textItem.getId();
Logger.log('Found matching form text item reusing item id: ' + obj.tempFormItemId);
}
return 0;
}); // Note: Just using map as handy iterator, don't need to assign the output to anything
if (!obj.tempFormItemId) {
Logger.log('Adding temporary item to form');
obj.tempFormItemId = form.addTextItem().setTitle(tempFormItemTitle).getId();
}
}
obj.spreadsheet = obj.spreadsheet || SpreadsheetApp.openById(obj.spreadsheetId);
obj.sheets = obj.sheets || obj.spreadsheet.getSheets();
obj.sheetId = obj.sheetId || null;
var sheetHeaderRow = null;
for (var i = 0, x = obj.sheets.length; i < x; i++) {
sheetHeaderRow = obj.sheets[i].getSheetValues(1, 1, 1, -1)[0];
for (var j = 0, y = sheetHeaderRow.length; j < y; j++) {
if (sheetHeaderRow[j] === tempFormItemTitle) {
obj.sheetId = obj.sheets[i].getSheetId();
Logger.log('Temporary item title found in header row of sheet id: ' + obj.sheetId);
break;
}
}
if (obj.sheetId) break;
}
// Time to start cleaning things up a bit!
if (obj.sheetId) {
if (obj.tempFormItemId) {
try {
form.deleteItem(form.getItemById(obj.tempFormItemId));
obj.tempFormItemId = null;
Logger.log('Successfully deleted temporary form item');
} catch (e) {
Logger.log('Tried to delete temporary form item, but it seems it was already deleted');
}
}
if (obj.sheetId && !obj.tempFormItemId && !obj.tempColumnDeleted) {
try {
obj.sheets[i].deleteColumn(j + 1);
obj.tempColumnDeleted = true;
Logger.log('Successfully deleted temporary column');
} catch (e) {
Logger.log('Could not delete temporary column as it was still attached to the form');
}
}
if (!obj.tempFormItemId && obj.tempColumnDeleted) {
Logger.log('Completed!');
return obj.sheetId;
}
}
SpreadsheetApp.flush(); // Just in case this helps!
// Normally this process takes three passes, and a delay of 4.5 secs seems to make it work in only 3 passes most of the time
// Perhaps if many people are submitting forms/editing the spreadsheet, this delay would not be long enough, I don't know.
obj.delay = ((obj.delay || 4500));
// If this point is reached then we're not quite finished, so try again after a little delay
Logger.log('Delay before trying again: ' + obj.delay / 1000 + ' secs');
Utilities.sleep(obj.delay);
obj.attempts++;
return getFormDestinationSheetId(form, obj);
}
}
To get the spreadsheet, once you have the DestinationID, use SpreadsheetApp.openById(). Once you have that, you can retrieve an array of sheets, and get the response sheet by index, regardless of its name.
var destId = FormApp.getActiveForm().getDestinationId();
var ss = SpreadsheetApp.openById(destId);
var respSheet = ss.getSheets()[0]; // Forms typically go into sheet 0.
...
From this point, you can manipulate the data in the spreadsheet using other Spreadsheet Service methods.
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form... (but that doesn't match the spreadsheet)
You're right - the number of current items does not equal the number of columns in the spreadsheet. The number of columns each response takes up in the destination sheet includes any questions that have been deleted from the form, and excludes items that are not questions. Also, the order of the columns in the spreadsheet is the order that questions were created in - as you re-arrange your form or insert new questions, the spreadsheet column order does not reflect the new order.
Assuming that the only columns in the spreadsheet are from forms, here's how you could make use of them:
...
var data = respSheet.getDataRange().getValues(); // 2d array of form responses
var headers = data[0]; // timestamp and all questions
var numColumns = headers.length; // count headers
var numResponses = data.length - 1; // count responses
And your last point is correct, you need to correlate names.
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.

Auto Email Google App Script: do while condition

I been working on this script for long time, it is Google App script, it sends email alert automatically to email provided, and I have script triggered at running every 1 minute.
so if cell total is greater than 201 it will send email to user.
but the problem is it send email every minute script is ran.
I need help with coding where if email has been sent already once, it will not send again unless cell value is less than 201 again and goes back to greater than 201,
I was thinking of making a cell which will contain text "Sent" or "Not Sent"
if it says "Not Sent" let the email code run if total is greater than 201
if is says "Sent" and total is greater than 201 don't let email code run..
I know I am not every clear but It has been very hard to get help on this.
Here is the code.
and if this works I'm sure lots of people can use this script for their use.
function sendEmail(email_address, email_subject, email_message) {
MailApp.sendEmail(email_address, email_subject, email_message);
}
function test_sendEmail() {
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.setActiveCell('A2');
var criterion_cutoff = 201;
var i = 0;
var addr;
var subj;
var msg;
do {
addr = cell.offset(i,0).getValue();
subj = cell.offset(i,1).getValue();
msg = cell.offset(i,2).getValue();
criterion = cell.offset(i,3).getValue();
if(criterion == criterion_cutoff) {
sendEmail(addr,subj,msg);
// Browser.msgBox('Sending email to: ' + addr);
}
i++;
} while( cell.offset(i, 0).getValue().length > 0 )
Browser.msgBox('Done!');
}
so I was think of adding if else condition outside of do while
if(SpreadsheetApp.getActiveSheet().getRange(21,6).getValue() != 'Not Sent') {
do {
//same stuff as above
} while(condition)
}
else
//don't know wht else to do in else condtion so just using googleclock
SpreadsheetApp.getActiveSheet().getRange(2,7).setValue('=GoogleClock()')
I guess I've figured it out on my own. I want to share my solution so other can use it.
so I have added 2 if else condtion
first one will check if cell value is < 201
if it is less than 201 than set somecell example F5 = Not Sent
another if else loop that will check
if value of F5 != Sent and cell value == 201
than it will run the email code
and right after running that email code it will set cell F5 to Sent
so next time loop runs again it will not send email again unless value of cell is
but this has one minor issue it will only work for 1st row out of 3 rows, you can make it for all 3 rows by adding more if conditions, I don't need it so I am not worried about it.
function sendEmail(email_address, email_subject, email_message) {
MailApp.sendEmail(email_address, email_subject, email_message);
}
function test_sendEmail() {
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.setActiveCell('A2');
var criterion_cutoff = 201;
var i = 0;
var addr;
var subj;
var msg;
if((SpreadsheetApp.getActiveSheet().getRange(2,4).getValue() < '201')) {
SpreadsheetApp.getActiveSheet().getRange(2,6).setValue('Not Sent');
}
else
if((SpreadsheetApp.getActiveSheet().getRange(2,6).getValue() != 'Sent') && (SpreadsheetApp.getActiveSheet().getRange(2,4).getValue() == '201')) {
do {
addr = cell.offset(i,0).getValue();
subj = cell.offset(i,1).getValue();
msg = cell.offset(i,2).getValue();
criterion = cell.offset(i,3).getValue();
if(criterion == criterion_cutoff) {
sendEmail(addr,subj,msg);
// Browser.msgBox('Sending email to: ' + addr);
SpreadsheetApp.getActiveSheet().getRange(2,6).setValue('Sent');
}
i++;
} while( cell.offset(i, 0).getValue().length > 0 )
Browser.msgBox('Done!');
}
}