I have written a custom function for Google Sheets in Apps Script. The goal is to have a sheet which automatically calculates who owes how much money to whom (e.g. to split a bill).
My sheet looks like this:
The first bill (Restaurant) is to be split among all 5 and the second bill is to be split among all 5 except Peter, because there is no 0 in B3.
The input for my Apps Script function will be cells B1 to F3 (thus, values AND names). The function works fine - it calculates the correct results. I open that spreadsheet via browser (sheets.google.com) AND via my phone app (Google Sheets). However, on my phone it often happens that the result cell (with the formula =calc_debt(B1:F3)) only displays "Loading ...". What's the problem?
For the sake of completeness, here is custom function's code:
function calc_debt(input) {
var credit = [0, 0, 0, 0, 0]; // credit[0] = Peter, credit[1] = Mark ...
for (var i = 1; i < input.length; i++) { // starting at i = 1 to skip the first row, which is the names!
// first: calculate how much everybody has to pay
var sum = 0;
var people = 0;
for (var j = 0; j <= 4; j++) {
if (input[i][j] !== "") {
sum += input[i][j];
people += 1;
}
}
var avg_payment = sum / people;
// second: calculate who has payed too much or too little
for (var j = 0; j <= 4; j++) {
if (input[i][j] !== "") {
credit[j] += input[i][j] - avg_payment;
}
}
}
// this function is needed later
function test_zero (value) {
return value < 0.00001;
};
var res = ""; // this variable will contain the result string, something like "Peter to Mark: 13,8 | Katy to ..."
while (!credit.every(test_zero)) {
var lowest = credit.indexOf(Math.min.apply(null, credit)); // find the person with the lowest credit balance (will be minus!)
var highest = credit.indexOf(Math.max.apply(null, credit)); // find the person with the highest credit balance (will be plus!)
var exchange = Math.min(Math.abs(credit[lowest]), Math.abs(credit[highest])); // find out by how much we can equalize these two against each other
credit[lowest] += exchange;
credit[highest] -= exchange;
res += input[0][lowest] + " to " + input[0][highest] + ": " + exchange.toFixed(2) + " | "; // input[0] = the row with the names.
}
return res;
}
I'm having a similar issue in the android app that loading a custom formula sometimes just shows 'Loading...', while in the web it always works fine. I've found a workaround to load the formulas in the android app:
Menu - > Export - > Save as - > PDF.
This will take a moment and behind the modal loading indicator you will see that the formulars eventually resolve. You can wait for the export to finish or cancel it as soon as you see your formular was resolved.
Also making the document available offline via the menu toggle could resolve the formulars.
Another thing you could do is using caching in your script. So whenever you use the web version to render more complex formulars the results are being stored and immediately loaded for the mobile app. Unfortunately, the Google cache is limited in time and does invalidate after a few hours. See here for more information:
https://developers.google.com/apps-script/reference/cache/
This two things work quite well. However, I'm searching for a better solution. Let me know if you find one.
Solved follow the solution provided here ..
Menu - > Export - > Save as - > PDF
This forces the script to run on mobile and be readable by the mobile sheet
Related
I've been trying to figure out how to get a Google AppsScript to pull in an API for keyword rank tracking directly within Google Sheets.
The loop is required to dynamically pull in information from column A and output the keyword ranking position into column G.
The keywords are in cells A2-A100. The ranking position (which is the only thing we are pulling from the API) we are popping into the corresponding row in column G, starting from G2. For testing purposes, we've got the loop set from 1 to 3.
We're at a bit of a loss as to why this isn't working as expected, and would really appreciate a nudge in the right direction!
The issue is that the very first result always returns 'keyword = undefined' within the API, and returning a result of '-1', meaning that the first row is not read. We've tried updating the r to 0, to 2, and changing the r references to no avail.
This makes us think that there must be something wrong with the loop, rather than the rest of the code, but please do correct me if this is not the case.
The script we've gotten so far is;
function callAPI() {
//New loop
for (r = 1; r <= 3; r++) {
{
//Find keyword, encode query and url
var query = keyword;
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(response);
//Get column value for keyword
var sheet = SpreadsheetApp.getActiveSheet();
var keyword = sheet.getRange(1 + r, 1).getValue();
}
//Set value of column
var results = sheet.getRange(1 + r, 7).setValue(response);
}
}
Additional edit:
So this is crystal clear, the desired input is;
keyword in A2 is read using the API and the output found (ranking position) is fed into G2.
the loop should then read A3, find the corresponding ranking position within the API, and adds that value to G3
rinse and repeat until the end of the loop.
Hopefully this is enough to go on, would really appreciate any advice, thank you!
Basically from TheMaster's comments you switch up your statements to this:
function callAPI() {
var sheet = SpreadsheetApp.getActiveSheet();
var keywords = sheet.getRange(2,1,3).getValues();
var responses = [];
//New loop
for (r = 0; r <= 2; r++) {
//Find keyword, encode query and url
var query = keywords[r][0];
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(resp);
responses.push([resp]);
}
//Set value of column
sheet.getRange(2,7,3).setValues(responses);
}
Note that I moved the sheet declaration outside the loop, it needs to be only called once.
EDIT: I updated the code to follow best practices in the tag info page. Note the usage of arrays as return values of getValues() and parameter of setValues().
I'm setting up a fairly complex Google sheet and trying to automate some routine interpolation with a script. I now have a script that works, but I want to optimise it.
Let me briefly describe the set up with some (simple) example data:
A B C D E
1 Lookup date Result Recorded date Value
2 17/8/2018 - 31/12/2018 210
3 31/12/2018 210 31/3/2019 273
4 14/2/2019 241.5 12/6/2019 411
5 31/3/2019 273
6 12/6/2019 411
7 1/7/2019 411
In this example, I have a small number of recorded values (columns D and E) and I want to compute the value for any date (column A). Column B is the output of the script. The problem is that my script is very slow, taking quite a while on my laptop (sometimes I must refresh the page), and never fully executing on my iPad.
I think part of this may be the volume of requests: I run this script for about 200 cells in my sheet.
I will briefly explain the script (full javascript code below).
It creates a custom function getvalue(x, y, lookupdate) which, for a given x-range (col. D) y-range (col. E) and "lookup date" (eg A4) will return the correct result (eg B4). This result is either:
blank if the lookup date occurs before the first recorded date
the exact value if the lookup date equals a recorded date
an interpolated value if the lookup date is in between two recorded dates
the final recorded value if the lookup date is beyond the range of the recorded dates
Now I have optimised this somewhat. In my implementation, I actually run it as an array for 100 cells in column A (only some of which actually need to run the script). I have another simple system that basically auto-populates the date in column A as a binary flag to say the script needs to run. So using ISBLANK() as a switch, my array formula for cells B3:B103 is:
=ArrayFormula(IF(ISBLANK(A3:A103),"",getvalue(D:D,E:E,A3:A103)))
Even though the array covers 100 cells, only about 50 of them are "activated" with a date in the A column, so only about 50 of them actually need to run the getvalue function. However, as a final complication, I am actually doing this to calculate four different values for each "lookup date", running four different arrays in four columns, so that's what I say the script runs approx. 200 times.
Here is my actual script:
function getvalue(x, y, lookupdate) {
/// LOOKUP AN ARRAY
if (lookupdate.map) {
return lookupdate.map(function(v) {
return getvalue(x, y, v);
});
}
/// GET RID OF EMPTY CELLS IN COLUMN
var xf = x.filter(function(el) {
return el != "";
});
var yf = y.filter(function(el) {
return el != "";
});
/// GET RID OF HEADER ROW
xf.shift()
yf.shift()
/// SAVE THE FIRST AND LAST VALUES
var firstx = xf[0][0]
var firsty = yf[0][0]
var lastx = xf[xf.length - 1][0]
var lasty = yf[yf.length - 1][0]
/// FIGURE OUT WHAT TO RETURN
if (lookupdate < firstx) {
return "";
} else if (lookupdate.valueOf() == firstx.valueOf()) {
return firsty;
} else if (lookupdate > lastx) {
return lasty;
} else {
var check = 0, index;
for(var i = 0, iLen = xf.length; i < iLen; i++) {
if(xf[i][0] == lookupdate) {
return yf[i][0];
} else {
if(xf[i][0] < lookupdate && ((xf[i][0] - check) < (lookupdate - check))) {
check = xf[i][0];
index = i;
}
}
}
var xValue, yValue, xDiff, yDiff, xInt;
yValue = yf[index][0];
xDiff = xf[index+1][0] - check;
yDiff = yf[index+1][0] - yValue;
xInt = lookupdate - check;
return (xInt * (yDiff / xDiff)) + yValue;
}
}
The error message on the iPad is simply the cells never move past "Loading...", and on the laptop it takes much longer than expected.
The most confusing thing is that I think it has gotten worse since I set it up as an array. I previously had it where all 400 cells would run the ISBLANK() check, and then for the approx 200 triggered cells, they would individually run the script. This at least would load on the iPad.
I read on here and on general Google support that scripts will run a lot faster if you batch operations, but it seems to have gotten slower since moving from 200 single cells to 4 arrays.
Does this need to be optimised further, or is there some other reason it might be stalling on my iPad?
Is it even possible to optimise it down and do this in a single call, instead of in 4 arrays?
Accounting for the case
else if (lookupdate.valueOf() == firstx.valueOf()) return firsty;
is superfluous because it is covered already by if(xf[i][0] == lookupdate)
(xf[i][0] - check) < (lookupdate - check) can be simplified to xf[i][0] < lookupdate
You are using pure javascript code, but keep in mind that App Script has many additional functions which are handy when working with Spreadsheet.
https://developers.google.com/apps-script/reference/spreadsheet/
So, e.g. for running your function only for the populated range functions like getDataRange() or getRange() in combination with getNextDataCell() and getLastRow() will be very useful for you.
The main important point - the functionality of your script. Are you assuming that there is an approximately linear relationship between Recorded date and value, and thus interpolate the value for not recorded dates?
In this case the statistically most precise way (and the programmatically simplest one) would be to calculate your slope between the first and last x and y respectively. That is:
Result=first_y+((y_last-y_first)/(x_last-x_first)*(Lookup_Date-first_x))
If this approach is suitable for you, your code would simplify and would look in App Script something like:
function myFunction() {
var ss=SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var Result_Range=ss.getRange("A2:B")
var limit=Result_Range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getLastRow()
var Result_values=Result_Range.getValues();
var valueRange=ss.getRange("D1:E");
var values=valueRange.getValues();
var last_Index=valueRange.getNextDataCell(SpreadsheetApp.Direction.DOWN).getLastRow()
var last_y=values[last_Index-1][1];
var last_x=values[last_Index-1][0].valueOf();
var first_y=values[1][1];
var first_x=values[1][0].valueOf();
var slope=(last_y-first_y)/(last_x-first_x);
for(var i=1;i<limit;i++)
{
Result_Range.getCell(i,2).setValue(first_y+(slope*(Result_values[i-1][0].valueOf()-first_x)))
Logger.log(i)
Logger.log(Result_values[i][0].valueOf()-first_x)
}
}
I've been struggling with a little project of mine for a while now, and was looking for some assistance. The key issue I believe is simply me not being familiar with array script language and how to approach this. I've tried a few things after researching on here a bit and reading through the Best Practices section, but haven't been able to get it functioning adequately.
My script needs to be able to collect 200 rows x 200 columns of data from a spreadsheet, and depending on the number within each cell, it needs to select the corresponding number of columns next to that number and colour them in.
This was really simple to do with my basic programming knowledge, by just getting it to select each cell, check the number, select that range with an offset and then change the colour and move onto the next cell, however my code is incredibly slow because it does everything within the sheet without batching the data, and can't complete the full range within Google Script's time allowance. Any assistance on speeding it up would be greatly appreciated, as I haven't been able to get this working using arrays.
Here's the code I'm working with currently:
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var selection=ss.getRange("Sheet2!H2:FC140");
var columns=selection.getNumColumns();
var rows=selection.getNumRows();
for (var column=1; column < columns; column++) {
for (var row=1; row < rows; row++) {
var cell=selection.getCell(row,column);
var cellvalue=cell.getValue();
if (cellvalue >= 1) {
var range=cell.offset(0,0,1,cellvalue);
range.setBackground("blue");
}
else {;}
}
}
}
Here's a public spreadsheet with confidential info removed and the sheet I'm targeting is Sheet2. Any assistance I could get on this would be greatly appreciated! Thanks
https://docs.google.com/spreadsheets/d/1Oe0aacfSBMmHpZvGPmjay5Q1bqBebnGQV4xlsK8juxk/edit#gid=0
You need to get rid of the repeated calls to range.getValue(). You can get all of the values for the range in one call & then iterate over that array in-script.
For your script it would look something like this:
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var selection=ss.getRange("Sheet2!H1:FC140"); // <= include header, but we'll skip it when we get to stepping over the array
var values = selection.getValues(); // <= get all of the values now in one call
for (var r=1; r < values.length; r++) {
for (var c=0; c < values[r].length; c++) {
if (values[r][c] >= 1) {
var range=sheet.getRange(r+1, c+8, 1, values[r][c]); // r+1 & c+8 because of array offsets
range.setBackground("blue");
}
else {;}
}
}
}
Take a look at Google's documentation: range.GetValues() https://developers.google.com/apps-script/reference/spreadsheet/range#getValues()
How about following sample script? If this is not your expectation, I would like to modify this script.
Sample script :
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var data = sheet.getRange("H2:FC140").getValues();
data.forEach(function(e1, i1){
e1.forEach(function(e2, i2){
if (e2 >= 1) {
sheet.getRange(i1+2, i2+8).offset(0,0,1,e2).setBackground("blue");
}
})
})
}
Result (sample) :
If I misunderstand your question, I'm sorry.
I'm working with ui-scroll from angular-ui (great job, the component is awesome!).
Everything works well for the most part. Data is loaded from custom datasource as I scroll down. However, when I scroll back to the top, I end up with a lot of whitespace within the viewport.
Upon inspection, it looks like ui-scroll is adding a sub-div and setting the height dynamically, but for some reason this isn't getting set back to 0 when I scroll to the top of the viewport.
I'm guessing this has to do with the way my datasource is serving data. I'm also not wrapping my head around the negative indexing. Could someone explain how I should be accounting for a negative index in the datasource.get() function when I'm mapping to a standard pagination service (index + offset, etc)?
Alright, I think I found the solution... Hopefully this will help someone else because I couldn't really find a working example in the demos provided by the angular-ui team.
Essentially, when you receive a negative index, you have to 1) clamp the index to 0 and then adjust the count variable by the difference.
Here's some sample code:
dataSource.get = function (index, count, successCallback)
{
// Index provided is 1-based, we'll convert to 0-based and clamp at 0
var realIndex = Math.max(1, index) - 1;
if (index < 0) {
count -= 1 - index;
}
if (realIndex < 0 ||
count == 0 ||
realIndex >= vm.searchResultCount)
{
successCallback([]);
return;
}
...
Make a pagination call to a service that expects the first
page of results to be at index 0.
Ui-scroll datasource implementation in case of limited data array may look like this
var min = 1, max = 100;
dataSource.get = function(index, count, success) {
var start = Math.max(min, index);
var end = Math.min(index + count - 1, max);
if (start <= end) {
getDataAsync(start, start + end + 1).then(success);
}
else {
success([]);
}
};
This allows you to deal with data arrays starting with min index. Also you may easily remove the limitation on bottom via var end = index + count - 1.
But I also would like to say, that it may be a back end responsibility. Look at this sample: https://rawgit.com/angular-ui/ui-scroll/master/demo/append/append.html – data array is limited from 1 to 50 while the datasource (skipping details) is just
get: function (index, count, success) {
Server.request(index, count).then(success);
}
I want to write a google spreadsheet with two main parts: an individual order form to calculate price & a system to count the total orders.
I made a simplified version here: https://docs.google.com/spreadsheets/d/1_vRnX-qT3-0puYhtBCclYIVu504A-donE4nU8rQn41k/edit?usp=sharing
After noting an individual order and the order is paid, the green button is clicked. The button activates the payment-script.
The payment script needs to add the values of the individual order to the total orders and afterwards empty the individual orders.
function payment() {
var individualOrder = SpreadsheetApp.getActiveSheet().getRange('E3:E6').getValues();
var currentTotal = SpreadsheetApp.getActiveSheet().getRange('O3:O6').getValues();
var newTotal = [0,0,0,0];
for (var i = 0; i < 4; i++) {
newTotal[i] = currentTotal[i] + individualOrder[i];
}
SpreadsheetApp.getActiveSheet().getRange('O3:O6').setValues([newTotal]);
// Reset individual order after payment
SpreadsheetApp.getActiveSheet().getRange('E3:E6').setValue(0);
}
EDIT: Now it does work to replace the new Total values by using the code below, but there is another problem. When summing the array values 1 + 0, it cocatenates them to 10 instead of using math-values. Any ideas?
function payment() {
var individualOrder = SpreadsheetApp.getActiveSheet().getRange('E3:E6').getValues();
var currentTotal = SpreadsheetApp.getActiveSheet().getRange('O3:O6').getValues();
var newTotal = currentTotal;
Logger.log(individualOrder);
Logger.log(currentTotal);
for (var i = 0; i < 4; i++) {
newTotal[i] = [currentTotal[i] + individualOrder[i]];
}
Logger.log(newTotal);
SpreadsheetApp.getActiveSheet().getRange('O3:O6').setValues(newTotal);
// Reset individual order after payment
SpreadsheetApp.getActiveSheet().getRange('E3:E6').setValue(0);
}
If all you want to do is set 'individualOrder' to 0 try:
function payment() {
var individualOrder = SpreadsheetApp.getActiveSheet().getRange('E3:E6').getValues();
for (var i = 0; i <individualOrder.length ; i++) {
individualOrder[i]=[0];
}
SpreadsheetApp.getActiveSheet().getRange('E3:E6').setValues(individualOrder);
}
I solved it by making the sum in the spreadsheet and doing a .getValues on that range and using this in .setValues. A lot easier, only 2 lines of code.