Google script - optimization (populate multiple columns if cell value) - google-apps-script

I have a script that populates with today's date column J when column A is filled.
function Populate() {
var sheetNameToWatch = "MASTER";
var columnNumberToWatch = /* column A */ 1;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveCell();
var val=sheet.getActiveCell().getValue()
if (sheet.getName() == sheetNameToWatch && range.getColumn() == columnNumberToWatch && val!= "" ) {
var targetCell = sheet.getRange(range.getRow(), range.getColumn()+9
);
targetCell.setValue("" + Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd"));
}
}
I think it's quite slow and I also would like to fill more columns at once, aside from the date on Column J:
Column I: "No payment"
Column L: "PENDING"
In order to fill multiple columns and try to make it work faster, I've also tested another version:
function Populate2(e)
{
var sheet = e.source.getActiveSheet();
if (sheet.getName() !== 'MASTER'|| e.range.getColumn() !== 1)
{
return;
}
for(var i=0;i<e.range.getNumRows();i++)
{
var offset=e.range.getRow() + i;
sheet.getRange('I'+ offset).setValue("No Payment");
sheet.getRange('J'+ offset).setValue(new Date());
sheet.getRange('L'+ offset).setValue("PENDING");
}
}
The last version has the problem that even if I clean the column A, the values are filled.
Couldn't figure out which version - if any - would be the best approach to improve regarding efficiency, and how.
Can anyone give me a hand?
Thank you in advance.

I believe your goal is as follows.
You want to reduce the process cost of your script.
When the values of column "A" are removed, you want to clear the columns "I", "J", and "L".
In this case, how about the following modification?
Modified script:
function Populate2(e) {
var range = e.range;
var sheet = e.source.getActiveSheet();
if (sheet.getSheetName() !== 'MASTER' || range.columnStart !== 1) {
return;
}
var values = range.getDisplayValues();
var { noPayment, date, pending, clear } = values.reduce((o, [a], i) => {
var row = range.rowStart + i;
if (a == "") {
o.clear.push(...["I", "J", "L"].map(e => e + row));
} else {
o.noPayment.push("I" + row);
o.date.push("J" + row);
o.pending.push("L" + row);
}
return o;
}, { noPayment: [], date: [], pending: [], clear: [] });
if (noPayment.length > 0) {
sheet.getRangeList(noPayment).setValue("No Payment");
sheet.getRangeList(date).setValue(new Date());
sheet.getRangeList(pending).setValue("PENDING");
}
if (clear.length > 0) {
sheet.getRangeList(clear).clearContent();
}
}
In this modification, the values are put to the cells using the range list. And also, the cells are cleared using the range list.
Note:
From your question, this modified script supposes that your function Populate2 is installed as OnEdit trigger. Please be careful about this.
I think that in your script, onEdit simple trigger might be also used. But, I'm not sure about your actual situation. So I used Populate2 in your script.
References:
getRangeList(a1Notations)
Class RangeList

Try this:
function Populate2(e) {
//e.source.toast('Entry');
var sh = e.range.getSheet();
if (sh.getName() !== 'Master' || e.range.columnStart !== 1) { return; }
//e.source.toast('flag1')
let n = e.range.rowEnd - e.range.rowStart + 1;
let dt = new Date();
let i = [...Array.from(new Array(n).keys(),x => ["No Payment"])];
let j = [...Array.from(new Array(n).keys(),x => [dt])];
let l = [...Array.from(new Array(n).keys(),x => ["PENDING"])];
sh.getRange(e.range.rowStart, 9, n).setValues(i);
sh.getRange(e.range.rowStart, 10, n).setValues(j);
sh.getRange(e.range.rowStart, 12, n).setValues(l);
}
or
function Populate2(e) {
//e.source.toast('Entry');
var sh = e.range.getSheet();
if (sh.getName() !== 'Master' || e.range.columnStart !== 1) { return; }
//e.source.toast('flag1')
let n = e.range.rowEnd - e.range.rowStart + 1;
let dt = new Date();
let i = [...Array.from(new Array(n).keys(),x => ["No Payment",dt])];
let l = [...Array.from(new Array(n).keys(),x => ["PENDING"])];
sh.getRange(e.range.rowStart, 9, n, 2).setValues(i);
sh.getRange(e.range.rowStart, 12, n).setValues(l);
}
Array.from()
Array Constructor

Related

When info is pasted into Cell 'J3' A time stamp is placed in 'AA1', and A time stamp is placed in the next empty cell of column 'AI'

I'm trying to make a script that when info is pasted into Cell 'J3' it will make a time stamp in cell 'AA1' and additionally it will also make a time stamp in column 'AI' in the first cell that doesn't already have a time stamp.
This would be so the sheet makes a time stamp that gets overridden each time that info is pasted into the sheet, but also make a static time stamp separately of each time info is pasted into the sheet.
I have a script that makes the timestamp that only updates when info is pasted into 'J3'
function:
onEdit(e) {
const sh = e.range.getSheet();
if (e.range.columnStart == 10 && e.range.rowStart == 3 && !e.value) {
sh.getRange("AA1").setValue(new Date());
}
}
I also have a script that takes info from one tab and copies it over to another tab on the first available row.
function Delete() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Testing'), true);
jumpToFirstEmptyBasic()
spreadsheet.getRange('Live Data!S2:AE1001').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Live Data'), true);
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A2:M1001').activate();
spreadsheet.getActiveRangeList().clear({contentsOnly: true, skipFilteredRows: true});
spreadsheet.getRange('Live Data!A2')
};
function jumpToFirstEmptyBasic() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Testing"); // << Change the sheet name to your own sheet.
var lastRow = sheet.getLastRow();
Logger.log(lastRow);
sheet.getRange(lastRow+1,1).activate();
};
Over all, I'm trying to add some of the seconds code to the first so that it has a second function of making a list of time stamps on the same tab whenever info is pasted into the 'J3' cell.
So far this is what I have put together, but it doesn't work, and I'm not experienced enough with python to find out why:
function onEdit(e) {
const sh = e.range.getSheet();
if (e.range.columnStart == 10 && e.range.rowStart == 3 && !e.value) {
sh.getRange("AA1").setValue(new Date());
jumpToFirstEmptyBasic()
sh.getRange(lastRow).setValue(new Date());
}
function jumpToFirstEmptyBasic() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var lastRow = ss.getLastRow();
Logger.log(lastRow);
ss.getRange(lastRow+1,1).activate();
};
}
Try this:
function onEdit(e) {
e.source.toast("Entry");
const sh = e.range.getSheet();
if (e.range.columnStart == 10 && e.range.rowStart == 3) {
e.source.toast("Gate1");
let lr = Number(getColumnHeight(27, sh, e.source));
sh.getRange(Number(lr) + 1, 27).setValue(new Date());
sh.getRange("AA1").setValue(new Date();)
e.source.toast(`The are ${Number(lr) + 1} timestamps on this page.`);
}
}
function getColumnHeight(col, sh, ss) {
var ss = ss || SpreadsheetApp.getActive();
var sh = sh || ss.getActiveSheet();
var col = col || sh.getActiveCell().getColumn();
var rcA = [];
if (sh.getLastRow()){ rcA = sh.getRange(1, col, sh.getLastRow(), 1).getValues().flat().reverse(); }
let s = 0;
for (let i = 0; i < rcA.length; i++) {
if (rcA[i].toString().length == 0) {
s++;
} else {
break;
}
}
return rcA.length - s;
//const h = Utilities.formatString('col: %s len: %s', col, rcA.length - s);
//Logger.log(h);
//SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(h).setWidth(150).setHeight(100), 'Col Length')
}
This may also work for you. But I find it unreliable at times and I'm not sure why.
function onEdit(e) {
e.source.toast("Entry");
const sh = e.range.getSheet();
if (e.range.columnStart == 10 && e.range.rowStart == 3) {
e.source.toast("Gate1");
let lr = Number(sh.getRange("AA1").getDataRegion(SpreadsheetApp.Dimension.ROWS).getHeight());
sh.getRange(Number(lr) + 1, 27).setValue(new Date());
sh.getRange("AA1").setValue(new Date());
e.source.toast(`The are ${Number(lr) + 1} timestamps on this page.`);
}
}
But if it works for you it will relieve you of using the helper function getColumnHeight();

Where Column Y Is Empty Fill In The Value From Column X

I'm new at this so thank you for your help and your patience.
Example: I have a shared spreadsheet with a protected range 'Y:Y' where colleagues update line items. One line item we want to flag when they change it is in column 'X:X'.
My desire is to set up a trigger that runs every 24 hours and it copies the values in column 'X:X' and pastes them into empty or blank values in column 'Y:Y'.
Then a simple DAX formula would compare X to Y. This would simplify a script I'm currently using that runs every time there's an edit.
function OnEdit() {
var s = SpreadsheetApp.getActiveSheet();
if( s.getName() == "PTL" ) { //checks that we're on PTL or not
if( s.getActiveCell().getColumn() == 7 ) { //checks that the cell being edited is in column G
var modify = s.getActiveCell().offset(0, 7);
modify.setValue(new Date());
var baseline = s.getActiveCell().offset(0, 6);
if( baseline.getValue() === '' ) //checks if the adjacent cell is empty or not?
baseline.setValue(new Date());
}
}
}
Try it this way:
function OnEdit(e) {
var sh = e.range.getSheet();
if (sh.getName() == "PTL" && e.range.columnStart == 7) {
e.range.offset(0,7).setValue(new Date());
if (e.range.offset(0, 6).getValue() == '')
e.range.offset(0,6).setValue(new Date());
}
}
function notsurewhatyouwant() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('PTL');
const xy = sh.getRange(1, 24, sh.getLastRow(), 2).getValues();
xy.forEach((r, i) => {
if (r[1] == null) {
sh.getRange(i+1,25).setValue(r[0]);
}
});
}
function createTrigger() {
if(ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "notsurewhatyouwant").length == 0) {
ScriptApp.newTrigger('notsurewhatyouwant').timeBased().everyDays(1).atHour(0).create();
}
}

How to find the last column using google apps script?

I want to copy the data from G11:G22 to the last column of the sheet starting from row 11 by using onEdit function. So, the script will be triggered once I select Transfer in cell F7. After running the script, the data should appear in range H11:H22 since column H is the last column in the sheet. How should I achieved that? This is what I have tried:
function onEdit(e){
var ss = SpreadsheetApp.getActiveSheet();
var lastrow = ss.getLastRow()
for( var k = 2; k <= lastrow ; k++) {
var m = 1;
while( sheet.getRange(k,m).isBlank() == false) {
m = m +1;
}
var lastcolumn = m
}
var destRange = ss.getRange(11,lastcolumn+1)
if (e.range.columnStart == 6 && e.range.rowStart == 7){
if (e.value == 'Transfer'){
var source = ss.getRange("G11:G22")
source.copyTo (destRange, {contentsOnly: true})
e.range.clearContent()
}
}
}
I think the problem comes from the For loop but I have no idea what is wrong. Hope to get some helps on this, thank you.
Based on your screenshot I suppose you can use getLastColumn()
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSheet();
var destRange = ss.getRange(11, ss.getLastColumn() + 1)
if (e.range.columnStart == 6 && e.range.rowStart == 7) {
if (e.value == 'Transfer') {
var source = ss.getRange("G11:G22")
source.copyTo(destRange, { contentsOnly: true })
e.range.clearContent()
}
}
}
No need for loop to get the last column if there is no more filled cels on the right side of the sheet.

From the drop-down list by selecting "delete" clearContent(); the cell

I have to use the sheet a lot on phone,
but it's quite awkward because it often doesn't show the delete button right away and I have to select it after the three dots
to make this easier, i thought i would add the word delete to the data
if I have already written something into the cell then selecting delete will delete the contents of the cell
the tricky part is that if something is already in the cell and I select Delete then it will write it afterwards so e.g.
write "something" > choose "Delete" than got
something, Delete
so i want a match that finds the word delete anywhere in the cell
my thought is that there should be a if( .match(/^Delete/) and clearContent(); somewhere for the newvalue and the oldvalue part but i couldn't figure out how to keep the script working like it used to and even find delete cell too
unchanged script:
function onEdit(e) {
const magicCells = ['B3:U27', 'W3:AP27'];
const ss = SpreadsheetApp.getActive();
let intersect;
if (e.value === undefined
|| !e.oldValue
|| !magicCells.some(rangeA1 => (intersect = getRangeIntersection_(e.range,
ss.getRange(rangeA1))))) {
return;
}
var oldValue;
var newValue;
var activeCell = ss.getActiveCell();
if (activeCell.getColumn() > 1) {
newValue = e.value;
oldValue = e.oldValue;
if (!e.value) {
activeCell.setValue("");
} else {
if (!e.oldValue) {
activeCell.setValue(newValue);
} else {
if (newValue.indexOf(oldValue) < 0) {
activeCell.setValue(oldValue + ', ' + newValue);
} else {
activeCell.setValue(newValue);
};
};
};
};
}
function getRangeIntersection_(range, intersectingRange) {
// version 1.4, written by --Hyde, 19 October 2020
var sheet = range.getSheet();
if (sheet.getSheetId() !== intersectingRange.getSheet().getSheetId()) {
return null;
}
var rowStart = Math.max(range.getRow(), intersectingRange.getRow());
var rowEnd = Math.min(range.getLastRow(), intersectingRange.getLastRow());
if (rowStart > rowEnd) {
return null;
}
var columnStart = Math.max(range.getColumn(), intersectingRange.getColumn());
var columnEnd = Math.min(range.getLastColumn(), intersectingRange.getLastColumn());
if (columnStart > columnEnd) {
return null;
}
return {
sheet: sheet,
range: sheet.getRange(rowStart, columnStart, rowEnd - rowStart + 1, columnEnd - columnStart + 1),
rowStart: rowStart,
columnStart: columnStart,
rowEnd: rowEnd,
columnEnd: columnEnd,
numRows: rowEnd - rowStart + 1,
numColumns: columnEnd - columnStart + 1,
};
}
thanks in advance for your help!
You can refer to this minimum reproducible sample code:
function onEdit(e){
var ss = e.source;
var cell = e.range;
var row = cell.getRow();
var col = cell.getColumn();
Logger.log(row);
Logger.log(col);
Logger.log(cell.getDisplayValue());
//Check if modified cell is in sheet "dropdown menu delete cell with script - unsolved"
//Check if modified cell is within range B3:I27
if(ss.getActiveSheet().getName() == "dropdown menu delete cell with script - unsolved"
&& row >= 3 && row <= 27 && col >= 2 && col <= 9){
if(cell.getDisplayValue().includes('DeleteCell')){
cell.clearContent();
}
}
}
I did not check your existing getRangeIntersection_() to check if the modified cell is within the set of ranges you provided.
I manually checked the row and column index based on your sample sheet
Use String.includes() to check if a string exist within a string (this is in case sensitive)
Clear the content of the cell if it has "DeleteCell" in its value
(UPDATE)
Here is a sample script combined in your existing script (where new data can be appended to the old data if not "DeleteCell" value was selected):
function onEdit(e) {
const magicCells = ['B3:U27', 'W3:AP27'];
const ss = SpreadsheetApp.getActive();
if (e.value === undefined
|| !e.oldValue
|| !magicCells.some(rangeA1 => (intersect = getRangeIntersection_(e.range, ss.getRange(rangeA1))))) {
return;
}
var oldValue;
var newValue;
var activeCell = ss.getActiveCell();
if (activeCell.getColumn() > 1) {
newValue = e.value;
oldValue = e.oldValue;
if (!e.value) {
activeCell.setValue("");
} else {
if (!e.oldValue) {
activeCell.setValue(newValue);
} else {
if (newValue.indexOf(oldValue) < 0) {
if(newValue == 'DeleteCell'){
activeCell.clearContent();
}else{
activeCell.setValue(oldValue + ', ' + newValue);
}
} else {
activeCell.setValue(newValue);
};
};
};
};
}

Google Sheets range(s) to allow varying numbers of checkboxes

I have a sheet where I need to limit the number of checkboxes allowed within a range. Like this
H219 to H225 allows only one checkbox to be checked.
H228: H335 allows three checkboxes.
H340:H347 Allows two checkboxes.
This script works when I use it once, but when i add it multiple times and change the range it seems to stop working.
function onEdit(e) {
const sh=e.range.getSheet();
if(sh.getName()=='GOALS') {
const mcpr=1;
const mcpc=2;
const arrayrange='h219:h225';
const arg=sh.getRange(arrayrange);
const avs=arg.getValues();
const ulr=arg.getRow();
const ulc=arg.getColumn();
const lrr=ulr+arg.getHeight()-1;
const lrc=ulc+arg.getWidth()-1;
if(e.range.columnStart<=lrc && e.range.rowStart<=lrr && e.value=="TRUE") {
let rc=avs[e.range.rowStart-ulr].filter(function(e){return e;}).reduce(function(a,v){ if(v){return a+1;} },0);
if(rc>mcpr){e.range.setValue("FALSE");e.source.toast('Sorry maximum checks per row is ' + mcpr);};
let cc=avs.map(function(r,i){return r[e.range.columnStart-ulc];}).filter(function(e){return e}).reduce(function(a,v){if(v){return a+1;}},0);
if(cc>mcpc){e.range.setValue('FALSE');e.source.toast('Sorry maximum checks per column is ' + mcpc);};
}
}
}
//
function onEdit(e) {
const sh=e.range.getSheet();
if(sh.getName()=='GOALS') {
const mcpr=1;
const mcpc=3;
const arrayrange='h236:h244';
const arg=sh.getRange(arrayrange);
const avs=arg.getValues();
const ulr=arg.getRow();
const ulc=arg.getColumn();
const lrr=ulr+arg.getHeight()-1;
const lrc=ulc+arg.getWidth()-1;
if(e.range.columnStart<=lrc && e.range.rowStart<=lrr && e.value=="TRUE") {
let rc=avs[e.range.rowStart-ulr].filter(function(e){return e;}).reduce(function(a,v){ if(v){return a+1;} },0);
if(rc>mcpr){e.range.setValue("FALSE");e.source.toast('Sorry maximum checks per row is ' + mcpr);};
let cc=avs.map(function(r,i){return r[e.range.columnStart-ulc];}).filter(function(e){return e}).reduce(function(a,v){if(v){return a+1;}},0);
if(cc>mcpc){e.range.setValue('FALSE');e.source.toast('Sorry maximum checks per column is ' + mcpc);};
}
}
}
Thank you so much, I have searched far and wide and this was the best script i could find, i just need it to work in about 6 places within the same sheet, with each range allowing a different number of checkboxes.
I believe your current situation and goal as follows.
You have a Google Spreadsheet that the checkboxes are put to the cells H219:H225, H228:H335 and H340:H347.
You want to give the limitation to the number for checking the checkboxes in each range.
For example, H219:H225, H228:H335 and H340:H347 have the limitations of 1, 3 and 2, respectively.
You want to achieve this using Google Apps Script.
In this case, in order to achieve your goal, I would like to propose a sample script using an array including the ranges and limitations. The script is run by the OnEdit simple trigger.
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet and set the variables of obj and sheetName, and save it. When you use this script, please check the checkboxes in the ranges H219:H225, H228:H335 and H340:H347. By this, the script is run by the simple trigger of OnEdit.
function onEdit(e) {
// Please set the ranges and limitations.
const obj = [
{range: "H219:H225", limit: 1},
{range: "H228:H335", limit: 3},
{range: "H340:H347", limit: 2},
];
const sheetName = "Sheet1"; // Please set the sheet name of the sheet including the checkboxes.
const range = e.range;
const editedColumn = range.getColumn();
const editedRow = range.getRow();
const sheet = range.getSheet();
if (sheet.getSheetName() != sheetName) return;
obj.forEach(({range}, i) => {
const temp = sheet.getRange(range);
const startRow = temp.getRow();
const startColumn = temp.getColumn();
obj[i].startRow = startRow;
obj[i].endRow = startRow + temp.getNumRows() - 1;
obj[i].startColumn = startColumn;
obj[i].endColumn = startColumn + temp.getNumColumns() - 1;
});
for (let i = 0; i < obj.length; i++) {
if (editedRow >= obj[i].startRow && editedRow <= obj[i].endRow && editedColumn >= obj[i].startColumn && editedColumn <= obj[i].endColumn) {
const n = sheet.getRange(obj[i].range).getValues().filter(([h]) => h === true).length;
if (n == obj[i].limit + 1) {
range.uncheck();
// Browser.msgBox("Number of checked checboxes are over the limitation."); // If you want to open the dialog, you canm use this.
} else if (n > obj[i].limit + 1) {
Browser.msgBox("Checed checkboxes of existing checkboxes have already been over the limitation number of " + obj[i].limit);
}
break;
}
}
}
Result:
When above script is used, the following result is obtained.
Note:
This sample script is run by the OnEdit simple trigger. So when you directly run the script with the script editor, an error occurs. Please be careful this.
References:
Simple Triggers
Event Objects
I wonder if you could do something like this:
You can add a new section for every range trow is top row, brow is bottom row, lcol is left column and rcol is right column and they are arrays
function onEdit(e) {
const sh = e.range.getSheet();
const trow = [236];
const brow = [244];
const lcol = [8];
const rcol = [8];
const mcpr = [1];
const mcpc = [3];
if (sh.getName() == 'GOALS' && e.range.columnStart >= lcol[0] && e.range.columnStart <= rcol[0] && e.range.rowStart >= trow[0] && e.range.rowStart <= brow[0] && e.value == 'TRUE') {
let vs = sh.getRange(trow[0], lcol[0], brow[0] - trow[0] + 1, rcol[0] - lcol[0] + 1).getValues();
let rc = vs[e.range.rowStart - trow[0]].filter(e =>return e).reduce((a, v) => { if (v) return (a + 1); }, 0);
if (rc > mcpr[0]) { e.range.setValue("FALSE"); e.source.toast('Sorry maximum checks per row is ' + mcpr[0]); };
let cc = vs.map((r, i) => { return r[e.range.columnStart - lcol[0]] }).filter(e =>return e;).reduce((a, v) => { if (v) return a + 1; });
if (cc > mcpc[0]) { e.range.setValue('FALSE'); e.source.toast('Sorry maximum checks per column is ' + mcpc[0]) };
if (sh.getName() == 'GOALS' && e.range.columnStart >= lcol[1] && e.range.columnStart <= rcol[1] && e.range.rowStart >= trow[1] && e.range.rowStart <= brow[1] && e.value == 'TRUE') {
let vs = sh.getRange(trow[1], lcol[1], brow[1] - trow[1] + 1, rcol[1] - lcol[1] + 1).getValues();
let rc = vs[e.range.rowStart - trow[1]].filter(e =>return e).reduce((a, v) => { if (v) return (a + 1); }, 0);
if (rc > mcpr[1]) { e.range.setValue("FALSE"); e.source.toast('Sorry maximum checks per row is ' + mcpr[1]); };
let cc = vs.map((r, i) => { return r[e.range.columnStart - lcol[1]] }).filter(e =>return e;).reduce((a, v) => { if (v) return a + 1; });
if (cc > mcpc[1]) { e.range.setValue('FALSE'); e.source.toast('Sorry maximum checks per column is ' + mcpc[1]) };
}
}
}