Function Keeps Looping After Last Row - google-apps-script

I am using the following code to fill a Google Docs template with data pulled from a spreadsheet.
function createBulkMembershipCards() {
const template = DriveApp.getFileById("--------");
const docFolder = DriveApp.getFolderById("----------");
const pdfFolder = DriveApp.getFolderById("----------------");
const libroSoci = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("LibroSoci");
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-1,19).getDisplayValues();
data.forEach(row => {
createMembershipCard (row[3],row[4],row[0],row[1], row[6],template,docFolder,pdfFolder);
});
}
function createMembershipCard (name,surname,msnumber,timestamp, email,template,docFolder,pdfFolder) {
const file = template.makeCopy(docFolder);
const docFile = DocumentApp.openById(file.getId());
const body = docFile.getBody();
body.replaceText("{name}", name);
body.replaceText("{surname}", surname);
body.replaceText("{msnumber}", msnumber);
body.replaceText("{timestamp}", timestamp);
body.replaceText("{email}", "<<"+email+">>");
docFile.saveAndClose();
docFile.setName(msnumber+" "+name+" "+surname);
const pdfBlob = docFile.getAs(MimeType.PDF);
pdfFolder.createFile(pdfBlob).setName(msnumber+" "+name+" "+surname);
}
I do not understand why, even though I used "getLastRow", the function keeps going on indefinitely after the last populated row.
Please not that I put 352 as the starting row because I want to extract data from that row on.
Is anyone able to help?
Thank you!

Let's say getLastRow() returns 1000. Then you are getting 999 rows. What you want is 1000-351 rows.
Change
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-1,19).getDisplayValues();
To
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-351,19).getDisplayValues();

Related

Importing Importing API data via importJSON

This is my code:
function pricesV2(){
var url='https://prices.runescape.wiki/api/v1/osrs/mapping'
var data = JSON.parse(UrlFetchApp.fetch(url).getContentText())
let myItems = new Map()
let myItem = new Map1()
json=eval('data')
json.forEach(function(elem){myItems.set(elem.id.toString(),elem.name)})
json.forEach(function(elem){myItem.set(elem.id.toString(),elem.examine)})
var url='https://prices.runescape.wiki/api/v1/osrs/latest'
var data = JSON.parse(UrlFetchApp.fetch(url).getContentText())
var result = []
result.push(['#','name','examine','high','low','lowTime', 'highTime'])
for (let p in eval('data.data')) {
try{result.push([p,myItems.get(p),myItem.get(p),data.data.item(p).high,data.data.item(p).low,convertTimestamp(data.data.item(p).lowTime),convertTimestamp(data.data.item(p).highTime)])}catch(e){}
}
return result
}
This is maybe important to know the variables of the API:
function prices(url){
//var url='https://prices.runescape.wiki/api/v1/osrs/latest'
var data = JSON.parse(UrlFetchApp.fetch(url).getContentText())
var result = []
result.push(['#','high','low','highTime','lowTime'])
for (let p in eval('data.data')) {
try{result.push([p,data.data.item(p).high,data.data.item(p).low,data.data.item(p).lowTime, ,data.data.item(p).highTime])}catch(e){}
}
return result
}
function naming(url){
//var url='https://prices.runescape.wiki/api/v1/osrs/mapping'
var data = JSON.parse(UrlFetchApp.fetch(url).getContentText())
var result = []
result.push(["id","name","examine","members","lowalch","limit","value","highalch"])
json=eval('data')
json.forEach(function(elem){
result.push([elem.id.toString(),elem.name,elem.examine,elem.members,elem.lowalch,elem.limit,elem.value,elem.highalch])
})
return result
}
These are 2 API combined (Importing API data via importJSON, solution did work out for 1 element, (element.name)). But when I want to add more from mapping it is giving an error. Could someone help me out? I want to combine all results in one table.
I believe your goal is as follows.
You want to integrate 2 returned data (JSON data) with the value of id.
From your reply of The colums doesn't need in this specific order., you are not required to check the order of columns.
You want to run the script as a custom function.
From your showing script, I thought that you might have wanted to use this script as a custom function.
In this case, how about the following sample script?
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet. And, please put a custom function =SAMPLE() to a cell. By this, the script is run.
function SAMPLE() {
const url1 = "https://prices.runescape.wiki/api/v1/osrs/mapping";
const url2 = "https://prices.runescape.wiki/api/v1/osrs/latest";
const [res1, res2] = [url1, url2].map(url => JSON.parse(UrlFetchApp.fetch(url).getContentText()));
const head = [...Object.keys(res1[0]), ...Object.keys(res2.data[Object.keys(res2.data)[0]])];
const obj1 = res1.reduce((o, e) => (o[e.id] = e, o), {});
const obj2 = Object.entries(res2.data).reduce((o, [k, v]) => (o[k] = v, o), {});
const keys = Object.keys(obj1).map(e => Number(e)).sort((a, b) => a - b);
const values = [head, ...keys.map(k => {
const o = Object.assign(obj1[k], obj2[k]);
return head.map(h => o[h] || "");
})];
return values;
}
Testing:
When this script is run, the following result is obtained.
Note:
If you want to set the specific order of the columns, please modify head in the above script.
When the custom function of =SAMPLE() is put to a cell, if an error occurs, please reopen Spreadsheet and test it again.
If you want to directly put the values to the Spreadsheet instead of the custom function, please modify the script.
References:
Custom Functions in Google Sheets
map()
reduce()
Added:
From the following 3 new questions,
Now how can I change like the top row to- > id, name, examine, members, lowalch, highalch, limit, high, low, lowtime, hightime? How can this be done in the function head, can't edit them individualy?
And also how can I format/convert highTime and lowTime to time (hh:mm:ss)?
From The colums doesn't need in this specific order., I didn't check the order of the column. In that case, as I have already mentioned in my answer, please modify head as follows. About your 2nd new question, in this case, please parse the unix time as follows.
So, when these new 2 questions are reflected in my sample script, it becomes as follows.
Sample script:
function SAMPLE() {
const url1 = "https://prices.runescape.wiki/api/v1/osrs/mapping";
const url2 = "https://prices.runescape.wiki/api/v1/osrs/latest";
const [res1, res2] = [url1, url2].map(url => JSON.parse(UrlFetchApp.fetch(url).getContentText()));
const head = ['id', 'name', 'examine', 'members', 'lowalch', 'highalch', 'limit', 'high', 'low', 'lowTime', 'highTime'];
const obj1 = res1.reduce((o, e) => (o[e.id] = e, o), {});
const obj2 = Object.entries(res2.data).reduce((o, [k, v]) => (o[k] = v, o), {});
const keys = Object.keys(obj1).map(e => Number(e)).sort((a, b) => a - b);
const timeZone = Session.getScriptTimeZone();
const values = [head, ...keys.map(k => {
const o = Object.assign(obj1[k], obj2[k]);
return head.map(h => o[h] ? (['lowTime', 'highTime'].includes(h) ? Utilities.formatDate(new Date(o[h] * 1000), timeZone, "HH:mm:ss") : o[h]) : "");
})];
return values;
}
Note:
About your following 3rd question,
How can this database also be added <prices.runescape.wiki/api/v1/osrs/volumes>?
I think that this is a new question. In this case, please post it as a new question.

Randomize rows when google doc is opened

I am trying to create a petition with a google form and doc. I've found an app script that will take the responses from the form and input them into a table on the doc. To help reduce the bias early respondents may face, I am trying to develop a script that will randomize the rows of the table every time the document is opened. Trying to scramble the signatures so that the same signatures aren't always at the top (can't be targeted).
Can anyone help me with this?
Below is the code I am using to populate the table from the form
//Since there could be a bunch of people submitting, we lock the script with each execution
//with a 30 second timeout so nothing gets overwritten
const lock = LockService.getScriptLock();
lock.waitLock(30000);
//Here we read the variables from the form submission event
const date = new Date(e.values[0]).toLocaleDateString();
//of you can use toLocaleString method if you want the time in the doc
const name = e.values[1];
const employeeNumber = e.values[2];
var form = FormApp.openById('');
const num = form.getResponses().length;
var num1 = num.toString()
//Next format those values as an array that corresponds to the table row layout
//in your Google Doc
const tableCells = [num1,name,employeeNumber]
//Next we open the letter and get its body
const letter = DocumentApp.openById('')
const body = letter.getBody();
//Next we get the first table in the doc and append an empty table row
const table = body.getTables()[0]
const tableRow = table.appendTableRow()
//Here we loop through our table cells from above and add
// a table cell to the table row for each piece of data
tableCells.forEach(function(cell, index){
let appendedCell = tableRow.appendTableCell(cell)
})
//here we save and close our letter and then release a lock
letter.saveAndClose();
lock.releaseLock();
}
Got this from a helpful vimeo tutorial by Abhishek Narula and Rebekah Modrak.
Randomize row in a google document table
First function Creates a table in a google document from a table in a spreadsheet
function creatTable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT]=DocumentApp.HorizontalAlignment.RIGHT;
const ss = SpreadsheetApp.openById("ssid");
const sh = ss.getSheetByName('Sheet0');
const tA = sh.getDataRange().getValues();
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
body.clear();
let n = body.getNumChildren();
for(let i =0;i<n-1;i++) {
if(i==0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
let table=body.appendTable(tA).setAttributes(style1);
}
Second function reads the table from the document and randomizes the rows it and reloads the table. This function actually searches through all of the children to find a table. If there is more than one table it will randomize all of them so it will need to be modified to integrate with your specific document.
This now removes the first line and then randomizes the array and then adds the first line back
function readandrandomizetable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.RIGHT;
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
let vs = [];
let fl = '';
[...Array.from(new Array(body.getNumChildren()).keys())].forEach(idx => {
let ch = body.getChild(idx);
if (ch.getType() == DocumentApp.ElementType.TABLE) {
Logger.log(`This is the index I want: ${idx}`);
let tbl = ch.asTable();
[...Array.from(new Array(tbl.getNumRows()).keys())].forEach(ridx => {
let s = tbl.getRow(ridx).getText().split('\n');
//Logger.log(JSON.stringify(s));
vs.push(s);
});
Logger.log(JSON.stringify(vs));
fl = vs.shift();
vs.sort((a, b) => {
vA = Math.floor(Math.random() * 100);
vB = Math.floor(Math.random() * 100);
return vA - vB;
});
vs.unshift(fl);
}
});
Logger.log(JSON.stringify(vs));
body.clear();
let n = body.getNumChildren();
for (let i = 0; i < n - 1; i++) {
if (i == 0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
body.appendTable(vs).setAttributes(style1);
}

Google Apps Script: Use getFolderById for multiple variables depending on value

I think I have a very simple issue with Google Apps Script, but I already tried to google the solution for 1.5hrs without success. I guess I search for the wrong terms.
Here my code:
function folderLocations(){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder;
}
function createNewGoogleDocs() {
//ID of Google Docs Template, what sheet to use + save all values as 2D array
const googleDocTemplate = DriveApp.getFileById('xxxxxxxxxx_XznDn-i0WVtIM');
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Current Overview');
const rows = sheet.getDataRange().getValues();
//Start processing each spreadsheet row
rows.forEach(function(row, index){
//Destination folder ID (can differ from each person)
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
// Set custom file name and create file
const copy = googleDocTemplate.makeCopy(`${row[15]} - ${row[3]} Quarterly Review` , destinationFolder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// Replace placeholders with real values
body.replaceText('%NAME%', row[3]);
body.replaceText('%QUARTER%', row[15]);
body.replaceText('%ANSWER_1%', row[16]);
body.replaceText('%ANSWER_2%', row[17]);
[...]
doc.saveAndClose();
})
}
All working fine! BUT: What I want is to "dynamically" change the folder, depending on the value of a cell. It's not always "Chris"...:
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
E.g.: If row[4] == Michael, then use the folder ID of "Michael". Somehow I can't get it to work to be "dynamically". 😔
I already tried all this, none working:
const destinationFolder = DriveApp.getFolderById(folderLocations().row[4]);
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));
const destinationFolder = DriveApp.getFolderById(folderLocations().`${row[4]}`);
const destinationFolder = DriveApp.getFolderById(folderLocations().toString(row[4]));
etc.
👆🏻 I know what I try to do here is embarrassing. But I am normally not a developer and nobody at my company is familiar with Google Apps Script. That's the last bit I am missing, rest I put together myself using Google.
Thank you SOO much! 🙏🏻
You don't even need a function. Just an object is enough:
const folderLocations = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
var id = folderLocations['Chris'];
console.log(id); // 1AEKHiI8iZKjHs-xxxxxxxxxx
const destinationFolder = DriveApp.getFolderById(folderLocations[row[4]]);
This did the trick :
function folderLocations(person){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder[person];
}
...further below:
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));

Google Apps Script not transferring multiline data from Google Form

I have written the following code to generate a pdf from a form response. However, my multiline response (genRemarks) and my score (tSCR) are not being used with the template file I've created, while my timestamp (cDate) is.
function onSubmit(e) {
const rg = e.range;
const sh = rg.getSheet();
const cDate = e.namedValues['Timestamp'][0];
const cBodyComments = e.namedValues['genRemarks'][0];
//Consequential/Calculated Data
const cUCS = e.namedValues['Uniform Compliance Score'][0];
const cPCS = e.namedValues['Protocol Compliance Score'][0];
const cDCR = e.namedValues['Dignified Conduct Rating'][0];
const cESR = e.namedValues['Empathy/Sincerity Rating'][0];
const cSWS = e.namedValues['Structured Work Score'][0];
const cCCOR = e.namedValues['Chain of Command Observed Rating'][0];
const cWER = e.namedValues['Work Environment Rating'][0];
const cOTOTS = e.namedValues['On Task and On Time Score'][0];
var tSCR = cUCS + cPCS + cDCR + cESR + cSWS + cCCOR + cWER + cOTOTS;
const invoiceFolderID = '[omitted due to sensitive data]';
const invoiceFolder = DriveApp.getFolderById(invoiceFolderID);
const templateFileID = '[omitted due to sensitive data]';
const newFilename = 'OCSO Report_' + cDate;
const newReportFileID = DriveApp.getFileById(templateFileID).makeCopy(newFilename, invoiceFolder).getId();;
var document = DocumentApp.openById(newReportFileID);
var body = document.getBody();
//start template replacement
body.replaceText('{{Timestamp}}', cDate);
body.replaceText('{{genRemarks}}', cBodyComments);
body.replaceText('{{Score}}', tSCR);
document.saveAndClose();
}
Im a bit new to apps script so any help is welcomed
Try changing this: const cBodyComments = e.namedValues['genRemarks'][0]; to this: const cBodyComments = e.namedValues['genRemarks'][1]; or whatever based upon your observation of
function onFormSubmit(e) {
Logger.log(JSON.stringified(e));
}
I believe the reason I'm seeing them come in columns other than zero was because I edited the form several times and ended up with multiple columns with the same name so this may not be the issue. If this is isn't your problem please let me know and I'll delete my answer.

App script not depending on column position

I want to be able to insert columns in my data or rearrange column positions without affecting the appscript functions.
Instead of referring to column positions by number, I thought of referring to them by header values (I will not change the names).
I made a small function which will make it easy for writing the code.
The output of the function will be stored in the sheet and then I will copy paste it to my app script function.
Is my approach all right or is there a better way to do it?
function get_headers(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheets=ss.getSheets();
var fin=[];
var tsheet=ss.getSheetByName("Product");
var shtname=tsheet.getName();
var [hd_Product, ...data_Product] = tsheet.getDataRange().getDisplayValues();
for (j=0;j<hd_Product.length;j++) { fin.push("const "+shtname+"_"+hd_Product[j]+"=hd_product.indexOf('"+hd_Product[j]+ "');") };
Browser.msgBox(fin);
//tsheet.getRange(tsheet.getLastRow(), 1, fin.length, fin[0].length).setValues(fin);
The below code - for running it for all the shets is not woeking.
for (i=0;i<sheets.length;i++){
var shtname=sheets[i].getName();
var tsheet=ss.getSheetByName(shtname);
var [header, ...data] = tsheet.getDataRange().getDisplayValues();
for (j=0;j<header.length;j++) {
fin.push("const "+shtname+"_"+header[j]+"=header.indexOf('"+header[j]+ "');") ;
}//for j
Browser.msgBox(fin);
}//for i
//Browser.msgBox(fin);
}
//
//
function process(){
const Product_Code=hd_product.indexOf('Code');
const Product_Item=hd_product.indexOf('Item');
const Product_UOM=hd_product.indexOf('UOM');
const Product_CAT=hd_product.indexOf('CAT');
const Product_Price=hd_product.indexOf('Price');
const Product_Min_Stock=hd_product.indexOf('Min_Stock');
const Product_Lot=hd_product.indexOf('Lot');
const Product_Now_Stock=hd_product.indexOf('Now_Stock');
const Product_Upd_Date=hd_product.indexOf('Upd_Date');
const Product_Vendor1=hd_product.indexOf('Vendor1');
const Product_Vendor2=hd_product.indexOf('Vendor2');
const Product_Vendor3=hd_product.indexOf('Vendor3');
}
A better way is to associate DeveloperMetaData with that column:
const setup = () => {
const sh = SpreadsheetApp.getActive().getSheets()[0];
const setupHeaders = new Map([
['Code', 'A:A'],// associate 'Code' with range A:A/Col 1
['Item', 'B:B'],
]);
setupHeaders.forEach((range, header) =>
sh.getRange(range).addDeveloperMetadata('secretTag', header)
);
};
Once setup, the column can be moved anywhere and the associated metadata will also move. You can then retrieve the current column position using DeveloperMetaDataFinder, even if you change the headers name in the sheet:
const getDevMeta_ = () =>
new Map(
SpreadsheetApp.getActive()
.createDeveloperMetadataFinder()
.withKey('secretTag')
.withLocationType(SpreadsheetApp.DeveloperMetadataLocationType.COLUMN)
.find()
.map(md => [md.getValue(), md.getLocation().getColumn()])
);
const getColumnsForHeaders = (headers = ['Item', 'Code']) => {
const map = getDevMeta_();
return headers.map(header => map.get(header));
};
const test1 = () => console.log(getColumnsForHeaders(['Code', 'Item'])); //returns current location of code and item
References:
Developer metadata guide
function myFunction() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getActiveSheet();
const rg=sh.getDataRange();
const vA=rg.getValues()
//Just learn how to use the next 3 or 4 lines and it's pretty easy you can integrate them into any code
const hA=vA[0];//this is the header row with a [0] index at the end to flatten it out to a single dimension array.
const col={};//returns column from header name
const idx={};//returns index from header name
hA.forEach(function(e,i){col[e]=i+1;idx[e]=i;});//this is the loop to fill in the objects above depending upon whether you want indexes or columns
//Example of how to use them
vA.forEach(function(r,i){}) {
r[idx[headername]];//referring to an index with column header
sh.getRange(i+1,col[headername]).setValue();//reffering to a column with header name
}
How to get a column number by header value
An easy way to return the column number would be the following
function myFunction() {
var searchedHeader = "Hello World";
var column = returnColumn(searchedHeader);
}
function returnColumn(header) {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var tsheet=ss.getSheetByName("Product");
var headers = tsheet.getRange(1, 1, 1, tsheet.getLastColumn()).getValues();
var column = (headers[0].indexOf(header) + 1);
if (column != 0) {
return column;
}
}
You can use the dynamic variable column storing the column number to perform all kind of requests, e.g.
var range = tsheet.getRange(5, column);