Create a TextItem for each product in a spreadsheet and to set the title of this item as "(Name of the product) (remains : (number remaining))" - google-apps-script

I want to make a Google Form for online order which would display the number of remaining units for each product, updated every time an order is passed.
As a first step, I try to create a TextItem for each product in a spreadsheet and to set the title of this item as "(Name of the product) (remains : (number remaining))"
var wsStocks = SpreadsheetApp.openById(ssID).getSheetByName("Stocks");
var form = FormApp.openById(formID);
function myFunction(){
var Products = wsStocks.getRange(1,1,wsStocks.getLastRow(),1).getValues();
for(j=1;j<Products .length+1;j++){
form.addTextItem().setTitle(wsStocks.getRange(j,1).getValue().toString()+" (remains: "+wsStocks.getRange(j,2).getValue().toString()+")");
};
}
When I run the code, the correct number of items are created, but the title seems to be attributed randomly: sometimes it is the expected title, sometimes it remains blank ("Question"). If I run several times the code, the distribution of correct and blank titles changes (sometimes the first and fourth are correct, the other ones are blank, sometimes it's just the second...)
I can check with Logger.log that "Products" does contain the list of products' names, and that the expression given as argument to SetTitle is indeed what I expect. I have no clue what's going on : /

Explanation / Issue:
It is not a good practice to use getRange and getValue within a for loop. You can construct the 2D array Products to include both column A and B and then you can just index both columns directly. I also used template literals to simplify your string expression:
var Products = wsStocks.getRange(1,1,wsStocks.getLastRow(),2).getValues();
for(j=0;j<Products.length;j++){
form.addTextItem().setTitle( `${Products[j][0]} (remains: ${Products[j][1]})` );
};
To answer your question, the issue with your current solution is that you are iteratively adding questions and setting their titles. However, this process needs some time to be completed and the for loop is always faster. You just need to slow down the requests.
Unlike SpreadsheetApp, FormApp does not support the flush method. But there is a workaround which you can use to slow down the requests and that is the sleep method. Of course, this approach will overall slow down your algorithm and if you specify a big enough time interval, your script might not be able to finish in time.
You need to choose the time interval wisely. In the following example, I used 4 seconds, but feel free to try a smaller or bigger time interval depending on the number of requests you need to process. For example, if you still see that you are getting wrong titles, increase that number.
Solution / Workaround:
var wsStocks = SpreadsheetApp.openById(ssID).getSheetByName("Stocks");
var form = FormApp.openById(formID);
function myFunction(){
var Products = wsStocks.getRange(1,1,wsStocks.getLastRow(),2).getValues();
for(j=0;j<Products.length;j++){
form.addTextItem().setTitle( `${Products[j][0]} (remains: ${Products[j][1]})` );
Utilities.sleep(4*1000); // 4 second delay
};
}

Related

Google Apps Script Reading and Writing Data from/to Sheets in a loop does not work

I was trying to use Google Sheets as my inventory manager. Every time I got bulk orders, I run them through a for loop and decrease the corresponding stock from another sheet. So I'm using "setValue" function to write data to Sheets.
The problem here is, I'm reading the current stock in a loop and writing the decreased stock in the same loop. However, "setValue" doesn't seem to work until the loop fully ends.
For example,
Milk = 5
>>New orders arrived with this order (1 milk, 1 bread, 1 coffee, 1 milk)
So here I will have a loop of 4 for each item.
For index 0, I read current stock from stock sheet (5). Then I check new order quantity (1). So setValue(4).
For index 1,...
For index 2,...
For index 3, I read current stock from the same stock sheet. It supposed to be 4, but it reads 5. The data supposed to be written 3 recursions before.
So the end result becomes;
Milk = 4
But it supposed to be
Milk = 3
Hope I was able to tell you the problem. So setValue function runs but it does not affect the corresponding sheet until the loop is over. That's why I cannot use it as a live database. Have you ever encountered a problem like this before?
Code looks like this
for(ordersArray){
for(stocksArray){
read stock from sheets
calculate new stock quantity
write new quantity with setValue //here this suppose to write on each loop but it writes only at the end of the loop
}
}
I played around with this idea to see if I could easily reproduce the problems you are seeing with the way I tend to work on a spreadsheet. I have eight columns and 1000 rows of fictitious data and I performed this "=SQRT(POW(SUM(A2:H2),2))" calculation on each row. To make it more challenging I'm reloading the array of data with semi random numbers and immediately reading the data with no SpreadsheetApp.flush() and then after the flush reading again and then comparing the last twenty-one rows of sums to see if they are not the same.
function readandwriterealyfast() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getActiveSheet();
const rg=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn()-1);
let vs=rg.getValues();
vs.forEach(function(r,i,A){
let n=Math.floor(Math.random()*100);
r.forEach(function(c,j){
A[i][j]=i+n+1;
});
});
rg.setValues(vs);
let sums=sh.getRange(sh.getLastRow()-20,sh.getLastColumn(),21,1).getValues();
SpreadsheetApp.flush();
let sums2=sh.getRange(sh.getLastRow()-20,sh.getLastColumn(),21,1).getValues();
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(JSON.stringify(sums) + '<br />' + JSON.stringify(sums2)), "Sums");
//=SQRT(POW(SUM(A2:H2),2))
}
And these are the sums in JSON format.
[[8080],[8048],[8376],[8008],[8136],[8024],[7880],[7920],[8048],[8328],[7992],[8528],[8344],[8336],[8312],[8128],[8328],[8424],[8072],[8680],[8688]]
[[8080],[8048],[8376],[8008],[8136],[8024],[7880],[7920],[8048],[8328],[7992],[8528],[8344],[8336],[8312],[8128],[8328],[8424],[8072],[8680],[8688]]
And I checked the data in the spreadsheet and it agrees with them. I'm not trying to say that it doesn't happen. But I find it remarkably fast when you tend to minimize the amount of cell functions utilized in your spreadsheets. And in my case I avoid the use of cell functions almost totally because I don't like them. (That's my personal opinion)
You might want to get a consultant to help you with your project. There may be something that can be done to provide your with a better outcome without too much additional effort.

How to fix the "Service invoked too many times for one day: urlfetch" error?

I am getting the following error in my Google sheet:
Service invoked too many times for one day: urlfetch
I know for a fact I am not making 100k calls, but I do have quite a few custom functions in my sheet. I tried to make a new sheet and copy/paste the script into that one, but I still get the same error. I then switched my account, made a new sheet, added the code, and I still got the error.
Is this just because I am on the same computer? Is Google smart enough to realize I am the same person trying to do it? I highly doubt that, so I am wondering why it would be throwing this error, even after switching accounts and making a new sheet.
In addition to that, is there any way to make sure I don't go over the limit in the future? This error sets me back at least a day with what I was working on. I do plan to write a script to just copy/paste the imported HTML as values into another sheet, but until I get that working, I need a temporary fix.
Sample code:
function tbaTeamsAtEvent(eventcode){
return ImportJSON("https://www.thebluealliance.com/api/v3/event/" + eventcode + "/teams?X-TBA-Auth-Key=" + auth_key);
}
function ImportJSONForTeamEvents(url, query, options){
var includeFunc = includeXPath_;
var transformFunc = defaultTransform_;
var jsondata = UrlFetchApp.fetch(url);
var object = JSON.parse(jsondata.getContentText());
var newObject = [];
for(var i = 0; i < object.length; i++){
var teamObject = {};
teamObject.playoff = object[i].alliances
newObject.push(teamObject);
}
return parseJSONObject_(object, query, "", includeFunc, transformFunc);
}
That is one "set" of code that is used for a specific function. I am pulling two different functions multiple times. I have about 600 of one function, and 4 of another. That would only be just over a thousand calls if all were run simultaneously.
I should note that I also have another sheet in my drive that automatically updates every hour with a UrlFetch. I do no believe this should affect this though, due to the very low pull rate.
I had a similar issue even though I was only calling two fetch calls in my functions and each function per data row. It exponentially grew, and with my data changing, every recalculate call also called those functioned, which VERY quickly hit the max.
My solution? I started using the Cache Service to temporarily store the results of the fetch calls, even if only for a few seconds, to allow for all the cells triggered by the same recalculation event to propagate using only the single call. This simple addition saved me thousands of fetch calls each time I accessed my sheets.
For reference:
https://developers.google.com/apps-script/reference/cache?hl=en

Comparing strings in google scripts

Trying to make a very basic function in google scripts. Basically, I want to read a value from cell J5 and increment the value in L5 until J5 says "Good!" (which would happen based on other stuff in my spreadsheet). I can't seem to test the string though.. when running, the code just seems to stop randomly or run forever (I also need the code to not touch the spreadsheet the moment it says "Good!", otherwise.. everything gets altered since the formulas run again).
function makeSelections() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var testcell = ss.getRange('J5')
var testcellValue = testcell.getValue();
for (var i = 0; testcellValue != "Good!"; i++) {
ss.getRange('L5').setValue(i);
testcellValue = testcell.getValue();
}
}
EDIT: As zbnrg pointed out, it appears to be a problem with the spreadsheet being way too slow to keep up with the script. Can anyone help with code to check a range (A2-A14) for no duplications (i.e. all unique entries)?
The recalculation of the "other stuff" in the spreadsheet is slower than the for-loop; you might pause the calculation and let the spreadsheet recalculate. Within the for-loop include:
Utilities.sleep(1000); //1000 milliseconds for 1 sec, 2000 for 2, etc.
Javascript: fast, Google Spreadsheet recalc: slow, unpredictable
Update for second part of question
Quick and dirty way to randomize a list without fussing with scripts:
Use =rand(), this generates a random number between 0 and 1:
Insert a column before the names in the A column.
Put the formula =rand() into A2 and drag it the length of the names
Sort both of these rows by the values in column A. (Doesn't matter if it is ascending or descending, since we just want a randomized list).
There you go, randomized names.
Then delete the row of random numbers.

Why does my Apps Script reference the wrong row?

The Goal
I am trying to create a spreadsheet using some custom functions. The purpose for the sheet is to keep score in a quizzing competition. The top row has the question number, the second row the score, and the third number the number of fouls for that question.
The Problem
After noticing some problems with the score calculation, I was able to narrow the problem down to the part of the code where I add up the fouls that occurred prior to the current question. It seems that no matter what I do, the code sums over the question row, not the foul row.
Attempted Solutions
The extremely strange thing is that no matter what I change the reference cells to, it always produces the same result (i.e. it still references the question row same as it always has).
Example
I'm not sure if that makes any sense, but I've made an example sheet here so you can see what I'm talking about and try to figure out what is going on. Keep in mind that I'm well aware that I could accomplish what I'm trying to do in the example sheet with a simple built-in formula, but since there's no way to use worksheet formulas on the Apps Script side, I have to create my own version of it. I've made the example editable for anyone with the link, so you should have full access to it; but if you have problems, just let me know, and I'll see what I can do to fix it.
In your For loop, you are summing the indexes rather than the values:
Try:
for (var PrevValue in PrevValues[0]) {
Sum = Sum + Number(PrevValues[0][PrevValue]);
}
EDIT:
You'll also need to account for the case where you pass in a single cell rather than a range (=mySum($B4:B4)), because in that case the value is passed directly instead of an array.
if(PrevValues instanceof Array){
for (var PrevValue in PrevValues[0]) {
Sum = Sum + Number(PrevValues[0][PrevValue]);
}
}else
Sum = PrevValues;

How to write a simple JQuery integer division function

My webpage displays realtime data that is updated every 10 seconds from our PI server(historian server). I was recently tasked to take two peices of data that are allready displayed on my page and divide them, displaying the new result on a graph. I have very little experience with AJAX/JQuery, so I was wondering if someone out there can lend me a hand?
Clarification:
Hypethetically-
Say I have "Number of Grades" and "Sum of Grades" displayed on my page as basic integers. They are updated every 10 seconds from the server. I want to take those two pieces of live data, divide them, and display the result (in this case it would be the overall average grade). So, my page will display Number of Grades, Sum of Grades, and Average Grade all updated in real time, every 10 seconds.
I am unclear as to whether JQuery can simply take those values off the server and perform division on them and return a value, or if other steps need to be taken to achieve this. I'm completely shooting in the dark here so sorry in advance for any vagueness or lack of required information.Thank you. Some example code is given below:
<span class = 'PIdata' data-tag = 'NumOfGrades' data-on_pi_change = 'NumOfGradeChange'></span>
<span class = 'PIdata' data-tag = 'SumOfGrades' data-on_pi_change = 'SumOfGradeChange'></span>
I want to display a value that divides NumOfGrades by SumOfGrades.
var sumOfGrades = parseFloat($.('#SumOfGradesId').text());
var numberOfGrades = parseFloat($.('#NumberOfGradesId').text());
var result = fn(sumOfGrades , numberOfGrades);
$.('#AverageGradeId').text(result);
This will set the required value.
Simple Maths with jQuery - division
The above link seems to have the answer to your question! Basically just access numbers by their id you set, get the value, parse them into integers or floats, perform your calculation, then store the value into the spot you would like it to appear!