I keep getting this red lightbulb message in the sheets script editor every time it runs. Is there something wrong with the code? It seems to work and doesn't take very long to execute.
Method Range.getValue is heavily used by the script
The script uses a method which is considered expensive. Each invocation generates a time consuming call to a remote server. That may have critical impact on the execution time of the script, especially on large data. If performance is an issue for the script, you should consider using another method, e.g. Range.getValues()
function Merge() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Expense Index');
var lastrow=sheet.getLastRow();
var range = sheet.getRange("A1:F"+(lastrow+1));
var k=3;
for (var i = 4; i <=lastrow+1; i++) {
var val1=range.getCell(i-1, 1).getValue();
var val2=range.getCell(i, 1).getValue();
if(val1!==val2){
if(!(sheet.getRange('A'+k+':A'+(i-1)).isPartOfMerge())){
sheet.getRange('A'+k+':A'+(i-1)).mergeVertically();
range.getCell((i-1), 6).setValue('=SUM(E'+k+':E'+(i-1)+')');
}
k=i;
}
}
sheet.getRange("A1:F"+(lastrow+1)).setHorizontalAlignment('center');
sheet.getRange("A1:F"+(lastrow+1)).setVerticalAlignment('middle')
}
Google Apps Script has best practices recommended by the development team. Making repeat range calls is expensive in operation. It's much better to batch your range data and then iterate the Object that is returned.
Right now, you're requesting 2 Objects on each loop. It's more efficient to get all the data as a single object. So, change:
var range = sheet.getRange("A1:F"+(lastrow+1));
to
// range.getValues() returns a 2D array you can loop through in one call.
var range = sheet.getRange("A1:F"+(lastrow+1)).getValues();
and loop through the object using bracket notation.
Related
This is definitely a pretty basic question, but I can't seem to find a solution by myself, nor the answer in the depths of the internet.
Java script skill: Newbie
I am getting Google Forms Item Id's so that I could then edit/delete etc. even after somebody edits outside of my script.
To get them Ids I am using a 'for loop', which comfortably gets them for me in a string. Unfortunately I don't know how to save that result in a variable - always getting a syntax error.
I am also not happy with saving the logger.log in my google sheet.
Any ideas?
This is my code:
function GetItems3(){
var formId = "your-id";
var Form = FormApp.openById(formId);
var ajtems = Form.getItems();
for(var i=0;i<items.length;i++)
{ajtems[i].getId().toString()
Logger.log(ajtems[i].getId().toString())
};
//That saves the logger log in my sheet (DMsheet is a global var)
var A = DMsheet.getRange(15, 6, ajtems.length, 1).setValue(A);}
Thanks in advanc3
There are several things wrong from your code.
1) There is no need to use the .toString() method because getId() already returns the id as a String.
2) You didn't define your ajtems var.
3) You can't declare a var like A and use it at the same line you are declaring it because it hasn't been set yet.
4) You didn't declare DMsheet, this variable should contain your sheet and you would get it using the Class Sheet.
5) You said in your post "Java skill: Newbie". Java and JavaScript are not the same.
This code will help you to solve your issue:
function GetItems3(){
var formId = "your-id";
var Form = FormApp.openById(formId);
var items = Form.getItems();
// Get a sheet from the Spreadsheet you are running your script
var dmSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
// Create an empty array to save the Form's items
var myArray = [];
for(var i=0;i<items.length;i++){
Logger.log(items[i].getId());
// Sotre the title items in the array
myArray.push(items[i].getTitle());
};
// Set the values in your sheet
dmSheet.getRange(1, 1, 1, items.length).setValues([myArray]);
}
I really recommend you to take your time to read the Apps Script docs. It will help you a lot to improve your coding and clear your doubts on how to use Apps Script in the best way possible.
I am working on a project where I take multiple column/row inventory sheets and turn them into a multi-row/2-column format for order picking.
I have a switch for selecting the appropriate inventory sheet and a map() function that copies the imported information from the inventory DataRange().
However, not all the data is in consistent columns. What I would like to do is find an expression that maps the next column in if the column it was mapping has a zero or "" value.
I won't give you the full body of code unless you need it, but hopefully just the important parts.
This is what I have:
var source = SpreadsheetApp.openById("1xixIOWw2yGd1aX_2HeguZnt8G_UfiFOfG-W6Fk8OSTs"); //This sheet
var srcSht = SpreadsheetApp.getActive();
var sourceMenu = srcSht.getRange('A1');//This is the cell cotaining the dropdown
var menuTest = sourceMenu.getValue();
// Variable for the vars sheet. If it doesn't exist, create it
var varsTest = source.getSheetByName('vars');
if (!varsTest){source.insertSheet('vars');}
var importedA1 = varsTest.getDataRange().getA1Notation();
varsTest.clearContents();
var t1Imp = '=ImportRange("test1_Id", "Stock!A1:F11")';
var varsData = varsTest.getRange('A1');// This is the cell we fill with the importRange formula
varsData.setValue(t1Imp);
var imported = varsTest.getDataRange().getValues();
var newOrder = imported.map(function(item) {
if (item[4] !== NaN){return [[item[0]],[item[4]]];};
if (item[4] === NaN){return [[item[0]],[item[3]]];};}
var orderRange = source.getSheetByName('Sheet1').getRange(10,1,newOrder.length, newOrder[0].length);
orderRange.setValues(newOrder);
Logger.log("\t" + newOrder);
Logger.log(newOrder):
[(timestamp omitted)] items1,order,caramel,6,c&c,2,mint,3,PB,0,,,items2,,caramel,,strawberry,,mint,,PB,
It seems to be skipping the if statements, or I told it that I mean to test the index as NaN, which will obviously never be true.
I also tried replacing 'NaN' with 'undefined'. Same result. I tried finding the item[4].Values, but it gave me an error. I also tried the same logic using filter() instead of map() but it copied the entire data set.
I pull these values onto a new 'vars' sheet in the workbook (to minimize calls to the web service):
test1
reduce them to the first and last columns, then output:
test
The cells in the 'order' column for the second set of items in the 'test' sheet are blank. The values for that order column should be in item[3] of that array, but I can't get the script to identify that that the blank cells are blank.
I am new to Google Apps Script and JS, but I am watching a lot of tuts and learning by doing. If I find a solution, I will post it.
Thank you StackOverflow, I could not have learned as much as I have without this community!
I have a working function that does what I want. In short:
I had to create a duplicate of the order column in a new column, so that all the values would line up. It's not technically a JS answer, but was the simplest and follows good spreadsheet rules.
function rmZeroOrderPS(item){
var source = SpreadsheetApp.openById("<sheetId>"); //This sheet
var varsTest = source.getSheetByName('vars');
var imported = varsTest.getDataRange().getValues();
var i=-1;
while (i <= imported.length){
if(item[8]!= 0) {return [item[0],item[8]]};
i+=1;
};
I had this problem which Cooper helped me to solve it (thanks again for that), but now I'm struggling with a different one. The following script will count how many times a client code will appear on another Spreadsheet using as a second condition yesterday date.
function countSheets()
{
var vA = appSh();
var td = Utilities.formatDate(subDaysFromDate(new Date(),2), Session.getScriptTimeZone(), "dd/MM/yyyy");
var mbs=getAllSheets();
//var s='';
for (var i=2;i<vA.length;i++)
{
var d = Utilities.formatDate(new Date(vA[i][12]), Session.getScriptTimeZone(), "dd/MM/yyyy");
for(var key in mbs)
{
if(vA[i][0]==key && d==td)
{
mbs[key]+=1;
}
}
}
return mbs;
}
Then I have the below code which will search in the main spreadsheet (a table) a string and when was found will return row number, also will search for the date yesterday and return the column number. Based on these information I'll get the range where I need to paste the count result from the first script.
function runScript()
{
var ss=SpreadsheetApp.openById('ID');
var mbs=countSheets();
for(var key in mbs)
{
var sh=ss.getSheetByName(key);
var rg=sh.getDataRange();
var vA=rg.getValues();
for(var i=0;i<vA.length;i++)
{
if(vA[i][1]=='Total Number of Applications')
{
var nr=i;
break;//by terminating as soon as we find a match we should get improved performance. Which is something you cant do in a map.
}
}
if(typeof(nr)!='undefined')//If we don't find a match this is undefined
{
var today=subDaysFromDate(new Date(),2).setHours(0,0,0,0);
for(var i=0;i<vA[3].length;i++)
{
if(vA[3][i])//Some cells in this range have no contents
{
if(today.valueOf()==new Date(vA[3][i]).valueOf())
{
sh.getRange(nr+1,i+1,1,1).setValue(Number(mbs[key]));
}
}
}
}
}
return sh;
}
PROBLEM: I have 24 rows on the main Spreadsheet. So I will need to write the same script 24 times. As example, I need to count Total Number of Applications, Total Number of Calls, Number of Live Adverts and so on. If I do this it will exceed execution time since each script takes on average 25 seconds to run.
I did some researches on this website and internet and read about storing values and re-use them over and over. At the moment my script will have to go every time through the same file and count for each condition.
Q1: Is there any chance to create another array that contain all those strings from the second script?
Q2: How to use PropertiesService or anything else to store data and don't have to run over and over getValues() ? I've read Google Documentation but couldn't understand that much from it.
I hope it all make sense and can fix this problem.
My best regards,
Thank you!
My Approach to your Problem
You probably should write it for a couple of rows and then look at the two of them and see what is unique to each one. What is unique about each one is what you have to figure out how to store or access via an external function call.
The issue of time may require that you run these functions separately. I have a dialog which I use to load databases which does exactly that. It loads 800 lines and waits for 10 seconds then loads another 800 lines and wait for ten seconds and keeps doing that until there are no more lines. True it takes about 10 minutes to do this but I can be doing something else while it's working so I don't really care how long it takes. I do care about minimizing my impact to the Google Server though and so I don't run something like this just for fun.
By the way the 10 second delay is external to the gs function.
I'm working on a project with Google's Blockly, but parts of the documentation are incomprehensible. Can someone help me understand the end condition of the following for loop (xml = allXml[i])?
var allXml = Blockly.Xml.workspaceToDom(workspace);
var allCode = [];
for (var i = 0, xml; xml = allXml[i]; i++) {
var headless = new Blockly.Workspace();
Blockly.Xml.domToWorkspace(headless, xml);
allCode.push(Blockly.JavaScript.workspaceToCode(headless));
headless.dispose();
}
I imagine the loop will exit when allXml[i] is undefined, but how can you iterate through an XML object like this? It seems to always be returning undefined and skipping the loop entirely.
Thanks for you help
Definitions for most of the function can be found at https://code.google.com/p/blockly/source/browse/trunk/core/xml.js?r=1614
And the doc page I pulled this from is https://developers.google.com/blockly/custom-blocks/code-structure?hl=en
I could not find this code in the repo on github, so I guess it is a bit older example in the docs.
But if you will have a look at the Blockly.Xml.workspaceToDom() function's implementation, you will see a very similar thing there.
var blocks = workspace.getTopBlocks(true);
for (var i = 0, block; block = blocks[i]; i++) {
var element = Blockly.Xml.blockToDom_(block);
//...
xml.appendChild(element);
}
The idea here is to iterate over all branches of code. The top block has no top connection (it starts a new branch). The getTopBlocks() returns an array {!Array.<!Blockly.Block>}.
Considering that the documentation is showing it in a section about parallel execution, I think it will be related to the fact, that you can have more unconnected branches of code... and the exact implementation just changed over time.
As per the code, return type of Blockly.Xml.workspaceToDom(workspace) is {!Element}.
It basically returns a DOM node created using goog.dom.createDom('xml');. And for each top level block it is appending a DOM node to it.
So basically the code snippet in question is looping through all the DOM nodes corresponding to the top level blocks.
The following code is working but extremely slow. Up till the search function all goes well. First, the search function returns a sequence and not an array (why?!). Second, the array consists of nodes and I need URI's for the delete. And third, the deleteDocument function takes a string and not an array of URI's.
What would be the better way to do this? I need to delete year+ old documents.
Here I use xdmp.log in stead of document.delete just te be safe.
var now = new Date();
var yearBack = now.setDate(now.getDate() - 365);
var date = new Date(yearBack);
var b = cts.jsonPropertyRangeQuery("Dtm", "<", date);
var c = cts.search(b, ['unfiltered']).toArray();
for (i=0; i<fn.count(c); i++) {
xdmp.log(fn.documentUri(c[i]), "info");
};
Doing the same with cts.uris:
var now = new Date();
var yearBack = now.setDate(now.getDate() - 365);
var date = new Date(yearBack);
var b = cts.jsonPropertyRangeQuery("Dtm", "<", date);
var c = cts.uris("", [], b);
while (true) {
var uri = c.next();
if (uri.done == true){
break;
}
xdmp.log(uri.value, "info");
}
HTH!
Using toArray will work but is most likely were your slowness is. The cts.search() function returns an iterator. So All you have to do is loop over it and do your deleting until there is no more items in it. Also You might want to limit your search to 1,000 items. A transaction with a large number of deletes will take a while and might time out.
Here is an example of looping over the iterator
var now = new Date();
var yearBack = now.setDate(now.getDate() - 365);
var date = new Date(yearBack);
var b = cts.jsonPropertyRangeQuery("Dtm", "<", date);
var c = cts.search(b, ['unfiltered']);
while (true) {
var doc = c.next();
if (doc.done == true){
break;
}
xdmp.log(fn.documentUri(doc), "info");
}
here is an example if you wanted to limit to the first 1,000.
fn.subsequence(cts.search(b, ['unfiltered']), 1, 1000);
Several things to consider.
1) If you are searching for the purpose of deleting or anything that doesnt require the document body, using a search that returns URIs instead of nodes can be much faster. If that isnt convenient then getting the URI as close to the search expression can achieve similar results. You want to avoid having the server have to fetch and expand the document just to get the URI to delete it.
2) While there is full coverage in the JavaScript API's for all MarkLogic features, the JavaScript API's are based on the same underlying functions that the XQuery API's use. Its useful to understand that, and take a look at the equivalent XQuery API docs to get the big picture. For example Arrays vs Iterators - If the JS search API's returned Arrays it could be a huge performance problem because the underlying code is based on 'lazy evaluation' of sequences. For example a search could return 1 million rows but if you only look at the first one the server can often avoid accessing the remaining 999,999,999 documents. Similarly, as you iterate only the in scope referenced data needs to be in available. If they had to be put into an array then all results would have to be pre-fetched and put put in memory upfront.
3) Always keep in mind that operations which return lists of things may only be bounded by how big your database is. That is why cts.search() and other functions have built in 'pagination'. You should code for that from the start.
By reading the users guides you can get a better understanding of not only how to do something, but how to do it efficiently - or even at all - once your database becomes larger than memory. In general its a good idea to always code for paginated results - it is a lot more efficient and your code will still work just as well after you add 100 docs or a million.
4) take a look at xdmp.nodeUrl https://docs.marklogic.com/xdmp.nodeUri,
This function, unlike fn.documentUri(), will work on any node even if its not document node. If you can put this right next to the search instead of next to the delete then the system can optimize much better. The examples in the JavaScript guide are a good start https://docs.marklogic.com/guide/getting-started/javascript#chapter
In your case I suggest something like this to experiment with both pagination and extracting the URIs without having to expand the documents ..
var uris = []
for (var result of fn.subsequence(cts.search( ... ), 1 , 100 )
uris.push(xdmp.nodeUri(result))
for( i in uris )
xdmp.log( uris[i] )