I have a google sheet for my business that creates a prep list for events.
After tackling an automatic clearing issue for over 25 hours(serious);I got some help from your fantastic community, modified the code to work, see below: (If Fx is empty, clear the cell to the left):
function cgar() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('auto');
var values = sheet.getRange("F11:F20").getValues();
var ranges = values.reduce((ar, [f], i) => {
if (f == "") ar.push("E" + (i + 11));
return ar;
}, []);
sheet.getRangeList(ranges).clearContent();
}
Is there a way for me to skip a specific cell, either based on location or value = "Garnish"?
If I change the code to take the whole range of "F5:F24" instead of the individual groups "F11:F20" it clears the header rows of the merged cell E10:F10 & E21:F21.
2.
var syr1 = ("D6")
var syr2 = ("D7")
var syr1isblank = SpreadsheetApp.getActiveSheet().getRange(syr1).isBlank()
var syr2isblank = SpreadsheetApp.getActiveSheet().getRange(syr2).isBlank()
if (syr1isblank == true) {
SpreadsheetApp.getActiveSheet().getRange("C16:D23").moveTo(sheet.getRange(syr1))
}
else if (syr2isblank == true && syr1isblank == false ) {
SpreadsheetApp.getActiveSheet().getRange("C16:D23").moveTo(sheet.getRange(syr2))
}
I have been using if and else if to move the group ranges that are below the cleared ones to the newly created space above, but it's clunky:
Is there a way that I can also modify this to scan for empty range, I think the process would be like this:
first empty cell spotted
Mark the cell to the left to a variable?
Scan the range f.x. E11:F20 for lastRow after it has been cleared by
the function above
Move the range to the variable marked cell above.
Image of the sheet in question: https://i.stack.imgur.com/ND2DC.png
I would greatly appreciate any assistance, thank you.
Related
I have script for Google Sheets that I collected on interwebs and got some help here. No I have 2 onEdit in conflict. I overcome that by creating script Trigger for onEdit2. It works but I don't think it is the best solution. Could you help get those two separated onEdit with if functions into one, please?
//Dependent Dropdown list
function onEdit(e){ // Function that runs when we edit a value in the table.
masterSelector(master1,master2,master3,master4);
var activeCell = e.range; // It returns the coordinate of the cell that we just edited.
var val = activeCell.getValue(); // Returns the value entered in the column we just edited.
var r = activeCell.getRow(); // returns the row number of the cell we edit.
var c = activeCell.getColumn(); // returns the column number of the cell we edit.
var wsName = activeCell.getSheet().getName();
if (wsName === masterWsName && c === firstLevelColumn && r > masterNumberOfHeaderRows) { // the if delimits the section sensitive to modification and action of the onEdit.
applyFirstLevelValidation(val,r);
} else if (wsName === masterWsName && c === secondLevelColumn && r > masterNumberOfHeaderRows){
applySecondLevelValidation(val,r);
}
} // end of onEdit
// addRow by checkboxes
function onEdit2(e) {
masterSelector(master1,master2,master3,master4);
//IF the cell that was edited was in column 4 = D and therefore a checkbox AND if the cell edited was checked (not unchecked):
if (e.range.columnStart === 4 && e.range.getValue() === true) {
var sheet = SpreadsheetApp.getActiveSheet(),
row = sheet.getActiveCell()
.getRow(),
//(active row, from column, numRows, numColumns)
rangeToCopy = sheet.getRange(row, 1, 1, 30);
sheet.insertRowAfter(row);
rangeToCopy.copyTo(sheet.getRange(row + 1, 1));
//Reset checked boxes in column 4
sheet.getRange(row,4,2,1).setValue(false);
}
}
Whole script is here, if needed.
A script cannot contain two functions with the same name. Rename your first onEdit function to onEdit1 (actually it will be better to assign a descriptive name) and the second function as onEdit2, then put them both in one function named onEdit and pass the parameter e to both of them:
function onEdit(e){
onEdit1(e);
onEdit2(e);
}
Related:
Two OnEdit functions not working together
Best Practices for Multiple OnEdit Functions
How to run multiple onEdit functions in the same google script (google sheets)?
Bracketing multiple onEdit functions
I am trying to find an empty cell in a specific column in google sheets. I am familiar with getLastRow(), but it returns the last row in whole sheet. I want to get the first empty cell in a specific column. So, I used a for loop and if. But I don't know why it is not working. The problem is that for loop does not return anything.
I am getting 10 rows of the column H (position 8) in the sheet test (line2). first 5 rows already have content. Data will be added to this cell later using another code. so, I want to be able to find the first empty cell in this column to be able to put the new data.
var sheettest = SpreadsheetApp.getActive().getSheetByName("test");
var columntest = sheettest.getRange(4, 8, 10).getValues();
for(var i=0; columntest[i]=="" ; i++){
if (columntest[i] ==""){
var notationofemptycell = sheettest.getRange(i,8).getA1Notation();
return notationofemptycell
}
}
As I said, I want to find the first empty cell in that column. I defined the for loop to go on as long as the cell is not empty. then if the cell is empty, it should return the A1 notation of that cell.
It seems the for loop does go on until it find the empty cell, because I get no definition for the var "notationofemptycell" in debugging.
This may be faster:
function getFirstEmptyCellIn_A_Column() {
var rng,sh,values,x;
/* Ive tested the code for speed using many different ways to do this and using array.some
is the fastest way - when array.some finds the first true statement it stops iterating -
*/
sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('test');
rng = sh.getRange('A:A');//This is the fastest - Its faster than getting the last row and
//getting a specific range that goes only to the last row
values = rng.getValues(); // get all the data in the column - This is a 2D array
x = 0;
values.some(function(ca,i){
//Logger.log(i)
//Logger.log(ca[0])
x = i;//Set the value every time - its faster than first testing for a reason to set the value
return ca[0] == "";//The first time that this is true it stops looping
});
Logger.log('x: ' + x)//x is the index of the value in the array - which is one less than
//the row number
return x + 1;
}
You need to loop through the length of columntest. Try this:
function myFunction() {
var sheettest = SpreadsheetApp.getActive().getSheetByName("test");
var lr=sheettest.getLastRow()
var columntest = sheettest.getRange(4, 8, lr).getValues();
for(var i=0; i<columntest.length ; i++){
if (columntest[i] ==""){
var notationofemptycell = sheettest.getActiveCell().getA1Notation();
Logger.log( notationofemptycell)
break;//stop when first blank cell on column H os found.
}
}
}
I have a Google Sheet that has 10k+ rows of data. While it should be rare, there could be instances of duplicate data being entered into the tab, and I have written a script to search for and remove those duplicates. For a while, this script has been running nicely and doing exactly what I expected it to do. But now that the tab has grown to over 10k rows, the script is exceeding the 6 minute time limit.
I've based this function on this tutorial.
// remove duplicates on Ship Details Complete
function duplicateShipDetailsComplete() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("Shipment Details Complete");
var sourceRange = sourceSheet.getRange(2, 1, sourceSheet.getLastRow(), 16)
var sourceData = sourceRange.getValues();
var keepData = new Array();
var deleteCount = 0;
for(i in sourceData) { // look for duplicates
var row = sourceData[i];
var duplicate = false; // initialize as not a duplicate
for(j in keepData) { // compare the current row in data to the rows in newData
if(row[2] == keepData[j][2] // duplicate Partner Invoice?
&& row[4] == keepData[j][4] // duplicate vPO?
&& row[5] == keepData[j][5] // duplicate SKU?
&& row[7] == keepData[j][7]) { // duplicate qty?
duplicate = true; // only if ALL criteria are duplicate, set row as a duplicate
}
}
if(!duplicate) { // If the row is NOT a duplicate
keepData.push(row); // add to newData
} else {
deleteCount++; // keep track of duplicates being deleted
}
}
sourceRange.clear();
sourceSheet.getRange(2, 1, keepData.length, keepData[0].length).setValues(keepData); // paste the keepData into the Working sheet
return deleteCount;
}
I've thought about breaking it up into pieces; process 1/3 of the data in each of 3 different calls. But this is actually being called from a different function that emails the returned deleteCount value, if it's greater than 0.
// Nightly email after checking for duplicates in Ship Details Complete
function sendEmailShipDetails() {
var deleted = duplicateShipDetailsComplete();
var update = parseFloat(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dept/Class").getRange(1,6).getValue()).toFixed(2);
if(deleted > 0) {
MailApp.sendEmail(
"me#myoffice.com",
"Shipment Details Cleaned Up",
deleted + " Shipment Detail line(s) were deleted from Complete as duplicates.\n" +
"Updated Value: " + update + "\n"
);
}
}
Even without that email function, it's exceeding the limit when I call duplicateShipDetailsComplete() directly. I suppose I could write three different functions (first 1/3, second 1/3, third 1/3) and update a cell somewhere with the results for each, and then call the email function separately to get that value. I'd feel a little better about that if I could write 1 function and pass parameters to it, but this is all coming from a Time Based Trigger, and you can't pass parms from those. But before I started to do that, I thought I'd check to see if someone had other suggestions on how I could make the existing code more efficient. Or, see if someone had totally different ideas on how I can do this.
Thanks
I'm creating a 2-dimensional heat map which has functionality when you click on any pixel. It grabs data associated with the index of every pixel (including adjacent pixels) and plots it. It currently looks like this:
The problem that I'm encountering is when I click on a left or right edge pixel, since it grabs data from adjacent pixels, it can retrieve data from the opposite side of the graph since it is all within a one-dimensional array. I am trying to create a conditional which checks if the clicked pixel is an edge case, and then configures the magnified graph accordingly to not show points from the other side of the main graph. This is the code I have so far:
// pushes all dataMagnified arrays left and right of i to magMainStore
var dataGrabber = function(indexGrabbed, arrayPushed) {
// iterates through all 5 pixels being selected
for (var b = -2; b <= 2; b++) {
var divValue = toString(i / cropLength + b);
// checks if selected index exists, and if it is not in the prior row, or if it is equal to zero
if (dataMagnified[indexGrabbed + b] != undefined && (& divValue.indexOf(".")!=-1)) {
dataMagnified[indexGrabbed + b].forEach(function(z) {
arrayPushed.push(z);
})
}
}
};
I am trying to get the same result as if I had a two dimensional array, and finding when the adjacent values within a single array is undefined. This is the line where I'm creating a conditional for that
if (dataMagnified[indexGrabbed + b] != undefined && (& divValue.indexOf(".")!=-1)) {
The second condition after the and is my attempts so far trying to figure this out. I'm unsure if I can even do this within a for loop that iterates 5 times or if I have to create multiple conditions for this. In addition, here's an image displaying what I'm trying to do:
Thank you!
Your approach looks overly complex and will perform rather slowly. For example, converting numbers to strings to be able to use .indexOf() to find a decimal point just for the sake of checking for integer numbers doesn't seem right.
A much simpler and more elegant solution might be the following function which will return the selection range bounded by the limits of the row:
function getBoundedSelection(indexGrabbed, selectionWidth) {
return dataMagnified.slice(
Math.max(Math.floor(indexGrabbed/cropLength) * cropLength, indexGrabbed - selectionWidth),
Math.min(rowStartIndex + cropLength, indexGrabbed + selectionWidth)
);
}
Here, to keep it as flexible as possible, selectionWidth determines the width of the selected range to either side of indexGrabbed. This would be 2 in your case.
As an explanation of what this does, I have broken it down:
function getBoundedSelection(indexGrabbed, selectionWidth) {
// Calculate the row indexGrabbed is on.
var row = Math.floor(indexGrabbed/cropLength);
// Determine the first index on that row.
var rowStartIndex = row * cropLength;
// Get the start index of the selection range or the start of the row,
// whatever is larger.
var selStartIndex = Math.max(rowStartIndex, indexGrabbed - selectionWidth);
// Determine the last index on that row
var rowEndIndex = rowStartIndex + cropLength;
// Get the end index of the selection range or the end of the row,
//whatever is smaller.
var selEndIndex = Math.min(rowEndIndex, indexGrabbed + selectionWidth);
// Return the slice bounded by the row's limits.
return dataMagnified.slice(selStartIndex, selEndIndex);
}
So I discovered that since the results of the clicked position would create a variable start and end position in the for loop, the only way to do this was as follows:
I started the same; all the code is nested in one function:
var dataGrabber = function(indexGrabbed, arrayPushed) {
I then create a second function that takes a start and end point as arguments, then passes them as the for loop starting point and ending condition:
var magnifyCondition = function (start, end) {
for (var b = start; b <= end; b++) {
if (dataMagnified[indexGrabbed + b] != undefined) {
dataMagnified[indexGrabbed + b].forEach(function (z) {
arrayPushed.push(z);
})
}
}
};
After that, I created 5 independent conditional statements since the start and end points can't be easily iterated through:
if (((indexGrabbed - 1) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-1, 2);
}
else if ((indexGrabbed / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(0, 2);
}
else if (((indexGrabbed + 1) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-2, 0);
}
else if (((indexGrabbed + 2) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-2, 1);
}
else {
magnifyCondition(-2, 2);
}
};
Lastly, I pass the index grabbed (i of the on clicked function) and an arbitrary array where the values get stored.
dataGrabber(i, magMainStore);
If there's a better way instead of the if statements, please let me know and I'd be happy to organize it better in the future!
function onEdit() {
var openRequests = SpreadsheetApp.getActive().getSheetByName('Open Requests');
var lastRowOpen = openRequests.getLastRow();
var closedRequests = SpreadsheetApp.getActive().getSheetByName('Closed Requests');
var lastRowClose = closedRequests.getLastRow();
var closed = openRequests.getRange(2,8,lastRowOpen,1).getValues();
for (var i = 0; i < lastRowOpen; i++)
{
if (closed[i][0].toString() == 'Yes')
{
var line = i+2;
if (closedRequests.getLastRow() == 1)
{
openRequests.getRange(line,1,1,9).copyTo(closedRequests.getRange(2,1,1,9));
closedRequests.getRange(2,9,1,1).setValue(new Date());
openRequests.deleteRow(line);
}
else
{
openRequests.getRange(line,1,1,9).copyTo(closedRequests.getRange(lastRowClose+1,1,1,9));
closedRequests.getRange(lastRowClose+1,9,1,1).setValue(new Date());
openRequests.deleteRow(line);
}
}
}
}
I have set up a trigger to run onEdit. What it does is check a column called Closed to see if it says Yes. The Closed column has a data validation drop down menu with the value Yes in it.
So when I click on the drop down menu and select Yes, it should copy the whole row to another sheet called Closed Requests then delete that row from the spreadsheet called Open Requests.
The issue I am having is that about 50% of the time, it deletes the row I select Yes to but it ALSO deletes the row below it (and about 50% of the time when this happens, only some times does the second deleted row show up in Closed Requests, the other times the whole row just disappears forever unless I undo).
From what I can tell, the deleteRow() function deletes the whole row and shifts all rows below it up a row to fill in the blank. So the row below the one meant to be deleted gets shifted up to the same row and also gets deleted. I don't know why the function is getting called twice though.
I tried adding some delays but it does not seem to be working.
function onEdit(e) {
var eRange = e.source.getActiveRange();
var openRequests = SpreadsheetApp.getActive().getSheetByName('Open Requests');
var closedRequests = SpreadsheetApp.getActive().getSheetByName('Closed Requests');
var nextRowClose = (closedRequests.getLastRow()?closedRequests.getLastRow()+1:2);
if(eRange.getSheet().getName()=="Open Requests" && eRange.getColumn()==8 && eRange.getValue()=="Yes") {
openRequests.getRange(eRange.getRow(), 1, 1, 9)
.copyTo(closedRequests.getRange(nextRowClose, 1));
closedRequests.getRange(nextRowClose, 9).setValue(new Date());
openRequests.deleteRow(eRange.getRow());
}
}
Could try iterating backwards as it was mentioned to me. Throwing in a SpreadsheetApp.flush() after the delete may help too.
#Jack, I have a similar use case to you. My code is the backwards one that BryanP discusses. My code is more or less here: "Batch removal of task items where status = 'Done'". It is because I remove them in a batch that I use the backwards method whereby the removal of a row with a higher row number will not disturb the row number of any rows with a lower row number.
But you are not removing rows in batch mode, so maybe backwards shouldn't make a difference (perhaps unless two users use the sheet and delete at the same time?)
So thought I'd try your code. I shoe horned your code into the onedit() function that is already present on my spreadsheet (which is used to colour rows red after a period of inactivity, and to put in a timestamp once the task is actually attended).
Then to test I used a copy of one of our spreadsheet which had already 50 rows/tasks in it. I manually filled in the required cells in a row and selected Done from the cell with the dropdown (I changed your code to expect "Done" rather than "Yes"). I repeated this for 20 rows.
The Result: Your code succeeded as you had expected it to every one of the 20 times ... no double deletes, always copying data across. It worked for me without introducing delays nor SpreadsheetApp.flush().
I don't have a solid suggestion I am afraid. In passing I mention the known fault where the spreadsheet has not properly refreshed itself, so does not show the deleted rows; this can be checked for by manually refreshing the spreadsheet when this fault appears. (However, the indications of this fault does not seem to logically fit with your report about the double copying over of two sequential rows.)
Thread lock? Sounds like a thread lock problem. Try:
function onEdit() {
// ****** add lock code
var lock = LockService.getPublicLock();
var hasMutex = lock.tryLock(100);
if(hasMutex==false) {
return;
}
// *** end
var openRequests = SpreadsheetApp.getActive().getSheetByName('Open Requests');
var lastRowOpen = openRequests.getLastRow();
var closedRequests = SpreadsheetApp.getActive().getSheetByName('Closed Requests');
var lastRowClose = closedRequests.getLastRow();
var closed = openRequests.getRange(2,8,lastRowOpen,1).getValues();
for (var i = 0; i < lastRowOpen; i++)
{
if (closed[i][0].toString() == 'Yes')
{
var line = i+2;
if (closedRequests.getLastRow() == 1)
{
openRequests.getRange(line,1,1,9).copyTo(closedRequests.getRange(2,1,1,9));
closedRequests.getRange(2,9,1,1).setValue(new Date());
openRequests.deleteRow(line);
}
else
{
openRequests.getRange(line,1,1,9).copyTo(closedRequests.getRange(lastRowClose+1,1,1,9));
closedRequests.getRange(lastRowClose+1,9,1,1).setValue(new Date());
openRequests.deleteRow(line);
}
}
}
// ****** add lock code
lock.releaseLock();
// *** end
}
Questions:
1) how many people were using the spreadsheet at the time.
2) how often does it happen.