I have this script working perfectly for tabs 3-15. I would like it to also run in tabs 80 until the end of the sheet.
There are just over 100 tabs and if the script runs through all of the tabs it takes way too long to run. I have tried changing line 6 to (var i=3;i<15,i<80<totalSheets;i++) but it seems to still go through all of them when I do that.
All help greatly appreciated, thanks.
function hidecolumns() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var totalSheets = sheets.length;
for(var i=3;i<15;i++)
{
var first_row = sheets[i].getRange(3,1,1,sheets[i].getMaxColumns()).getValues().flat();
first_row.forEach((fr,j)=>{
if(fr==0){
sheets[i].hideColumns(j+1);
}
else {
sheets[i].showColumns(j+1);
}
})
}
}
I believe your goal is as follows.
You want to reduce the process cost of your script.
In this case, how about using Sheets API? When Sheets API is used for your script, it becomes as follows.
Modified script:
This script uses Sheets API. So, please enable Sheets API at Advanced Google services.
function hidecolumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
var sheets = ss.getSheets().filter((_, i) => (i >= 3 && i < 15) || i > 80); // Or when you want to use all sheets, you can also use ss.getSheets()
var requests = sheets.flatMap(s => {
var sheetId = s.getSheetId();
var values = ss.getRange(`'${s.getSheetName()}'!A3:3`).getValues()[0];
return values.map((r, i) => ({
updateDimensionProperties: {
properties: { hiddenByUser: r == 0 },
range: { sheetId, startIndex: i, endIndex: i + 1, dimension: "COLUMNS" },
fields: "hiddenByUser"
}
}));
});
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
}
In this case, from I have tried changing line 6 to (var i=3;i<15,i<80<totalSheets;i++), the sheet indexes of 3 - 14 (from i=3;i<15) and 81 - end (from i<80<totalSheets)are used.
I cannot understand your 1st index and end index you want to use. If you want to 3 - 15 and 80 - end, please modify ss.getSheets().filter((_, i) => (i >= 3 && i < 15) || i > 80) to ss.getSheets().filter((_, i) => (i >= 3 && i <= 15) || i >= 80).
References:
Method: spreadsheets.batchUpdate
UpdateDimensionPropertiesRequest
Hide columns on many selected sheets
function hidecolumns() {
const ss = SpreadsheetApp.getActiveSheet();
ss.getSheets().filter((sh, i) => (i > 2 && i < 16) || (i > 79) ).forEach(sh => {
let first_row = sh.getRange(3, 1, 1, sh.getMaxColumns()).getValues().flat();
first_row.forEach((fr, j) => {
if (fr == 0) {
sh.hideColumns(j + 1);
}
else {
sh.showColumns(j + 1);
}
});
})
}
Related
I am working on creating a script that needs to push rows from one sheet of a google workbook to another based on how the row is categorized by an entry in another column. This needs to be also adaptable to have it push to a different google workbook in some cases in the future. I have tried multiple iterations of the following script and it will pull the rows over and then updated background colors, but it is just iterating through all the data and pulling over everything instead of just those rows with an "X" in the relevant column.
What I'd like it to do is pull only those on the "Feedback" tab which are not set to green as the cell color in column F, and those with an "X" in column F, then to have it set the cell to green once it has pulled so that it won't pull the same information next time I run the script. I want to then have it do the same thing using column G.
Here is a test doc I have been testing with.
https://docs.google.com/spreadsheets/d/1JLyEuVijQ8MvfOKrtbRD_YKmRDnTCxf7qCSw9Ggty_Y/edit#gid=384324173
This is the code I have currently:
function oneFeedback() {
var sss = SpreadsheetApp.getActiveSpreadsheet();
var ss = sss.getSheetByName("Feedback");
var s = ss.getSheetName();
var data = ss.getDataRange().getValues();
var bostab = sss.getSheetByName("1");
if(s !== "Feedback"){
Browser.msgBox("This option isn't available for this sheet.")
}
else
{
for(var i = 2; i < data.length; i++){
if(ss.getRange(i+1,6).getBackground !== "#d9ead3"){
if(ss.getRange(i+1,6) !== ""){
var values = ss.getRange(i+1,1,1,5).getValues();
bostab.insertRowBefore(2);
bostab.getRange(2,2,1,5).setValues(values).setFontColor("#000000");
ss.getRange(i+1,6).setBackground("#d9ead3");
}
}
Browser.msgBox("Complete")
}
}
}
The script is set to run from selecting a menu item in the "Extras" menu that is being created using the "Code.gs" script on this doc.
Modification points:
In your script, getBackground of ss.getRange(i+1,6).getBackground might be getBackground().
When getValues() and setValues() are used in a loop, the process cost will become high. Ref (Author: me)
Only column "F" is used.
When these points are reflected in your script, how about the following modification?
Modified script:
function oneFeedback() {
// Ref: https://stackoverflow.com/a/53678158
const columnIndexToLetter_ = index => (a = Math.floor(index / 26)) >= 0 ? columnIndexToLetter_(a - 1) + String.fromCharCode(65 + (index % 26)) : "";
// Retrieve source sheet.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const srcSheet = ss.getSheetByName("Feedback");
if (!srcSheet) Browser.msgBox("This option isn't available for this sheet.");
// Retrieve source values.
const range = srcSheet.getDataRange();
const [header, ...values] = range.getValues();
const [, ...backgrounds] = range.getBackgrounds();
// Create an object for putting to destination sheets.
const offset = 5; // This is from your question.
const dstSheets = header.splice(offset);
const obj = dstSheets.reduce((o, e) => (o[e] = [], o), {});
const res = values.reduce((o, r, i) => {
dstSheets.forEach((h, j) => {
const idx = offset + j;
if (r[idx] == "X" && backgrounds[i][idx] != "#d9ead3") {
o[h].push(r);
o.ranges.push(`${columnIndexToLetter_(idx)}${i + 2}`);
}
});
return o;
}, { ...obj, ranges: [] });
// Put values to destination sheets.
dstSheets.forEach(e => {
const v = res[e];
if (v.length > 0) {
const dstSheet = ss.getSheetByName(e);
dstSheet.getRange(dstSheet.getLastRow() + 1, 1, v.length, v[0].length).setValues(v);
}
});
// Set background colors of source cells.
if (res.ranges.length == 0) return;
srcSheet.getRangeList(res.ranges).setBackground("#d9ead3");
}
When this script is run, I thought that your goal might be able to be achieved.
References:
reduce()
forEach()
setBackground(color) of Class RangeList
I need to create a time trigger that requires 3 things.
Runs every 60 minutes (originally set for 800, but needs to run more often)
Only runs on rows that have been timestamped over 1 hour
Only runs on a row if Col13 is TRUE
I have created a simple version that runs perfect, but i dont understand how to integrate with time conditions i need. (Row 2 is an example row, does not need to move)
function TimeTrigger(){
ScriptApp.newTrigger('MoveChecked')
.timeBase()
.atHour(8)
.everyDays(1)
.create();
}
function MoveChecked(){
const sh = SpreadsheetApp.getActive();
const ss = sh.getSheetByName("Shipped_Log");
const outSheet = sh.getSheetByName("Master_Adjustment_Log");
let data = ss.getRange(2,1,ss.getLastRow()-1,23).getValues();
let out = [];
for (let i = 0; i<data.length; i++){
if (data[i][11]== true){
out.push(data[i]);
ss.deleteRow(i+2);
data.splice(i,1);
i--;
}
}
outSheet.getRange(outSheet.getLastRow()+1,1,out.length,23).setValues(out);
}
spreadsheet for context
Movechecked on a one hour trigger
I assumed timestamp is column 1 but it can be changed easily
function TimeTrigger() {
if (ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "MoveChecked").length == 0) {
ScriptApp.newTrigger('MoveChecked').timeBased().everyHours(1).create()
}
}
function MoveChecked() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Shipped_Log");
const osh = ss.getSheetByName("Master_Adjustment_Log");
let vs = sh.getRange(2, 1, ss.getLastRow() - 1, sh.getLastColumn()).getValues();
let dt = new Date();
let thv = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), dt.getHours() - 1).valueOf()
let out = [];
let d = 0;
vs.forEach((r, i) => {
let ctv = new Date(r[0]).valueOf()
if (r[12] == "TRUE" && ctv > thv) {
out.push(r);
sh.deleteRow(i + 2 - d++);
}
})
osh.getRange(osh.getLastRow() + 1, 1, out.length, out[0].length).setValues(out);
}
Beginning of... I added this after posting the question below
For example, entering "ulianababenko" into either A2 or B2 would cause both A2 and B2 to display "ulianababenko". Yes. A2 and B2 would display the same text.
However, A2 would link to behance.net/ulianababenko whereas B2 would link to behance.net/ulianababenko/moodboards. In other words, A2 would link to the first URL; and B2 would link to the second URL.
End of... I added this after posting the question below
Normally in an ordinary Google Sheet, when I press Ctrl + z repeatedly, I am able to undo many steps back.
However, in this Google Sheet --> Behance.net- sample spreadsheet, when I change, for example, A3 from test1 to Ben and then change A4 from test2 to Jacob, and then press Ctrl + z once I am able to undo Jacob (in A4). However, when I press Ctrl + z a second time, I am unable to undo Ben (in A3).
In such a case, of course, I would like to be able to undo Ben (in A3).
The Google sheet linked to above contains the following Apps Script....
function onEdit(e){
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
myFunction(sh,sh.getRange(i,j),sh.getRange(i,j).getValue())
}
}
}
function myFunction(sheet,range,value){
if (sheet.getName() == "RemcoE33" && range.getColumn() < 3 && range.getRow() > 1) {
const urls = [`https://www.behance.net/${value}`,`https://www.behance.net/${value}/moodboards`];
const richTextValues = [];
console.log(urls);
urls.forEach(url => {
richTextValues.push(SpreadsheetApp.newRichTextValue()
.setText(value)
.setLinkUrl(url)
.build())
});
sheet.getRange(range.getRow(), 1, 1, 2).setRichTextValues([richTextValues]);
}
}
I tried the following but it caused the original scrip to stop working properly
function onEdit(e){
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
myFunction(sh,sh.getRange(i,j),sh.getRange(i,j).getValue())
}
}
}
function myFunction(sheet,range,value){
if (sheet.getName() == "RemcoE33" && range.getColumn() < 3 && range.getRow() > 1) {
const urls = [`https://www.behance.net/${value}`,`https://www.behance.net/${value}/moodboards`];
const richTextValues = [];
console.log(urls);
urls.forEach(url => {
richTextValues.push(SpreadsheetApp.newRichTextValue()
.setText(value)
.setLinkUrl(url)
.build())
});
sheet.getRange(range.getRow(), 1, 1, 2).setRichTextValues([richTextValues]);
}
}
function onEdit(e){
if (!e.oldValue.includes("https://www.behance.net")) {
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
myFunction(sh,sh.getRange(i,j),sh.getRange(i,j).getValue())
}
}
}
}
I tried the following on 29 January 2022 but it caused the original scrip to stop working properly
function onEdit(e){
if (!e.oldValue.includes("https://www.behance.net")) {
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
myFunction(sh,sh.getRange(i,j),sh.getRange(i,j).getValue())
}
}
}
}
function myFunction(sheet,range,value){
if (sheet.getName() == "RemcoE33" && range.getColumn() < 3 && range.getRow() > 1) {
const urls = [`https://www.behance.net/${value}`,`https://www.behance.net/${value}/moodboards`];
const richTextValues = [];
console.log(urls);
urls.forEach(url => {
richTextValues.push(SpreadsheetApp.newRichTextValue()
.setText(value)
.setLinkUrl(url)
.build())
});
sheet.getRange(range.getRow(), 1, 1, 2).setRichTextValues([richTextValues]);
}
}
Issue:
An onEdit function that changes the value that was just edited doesn't work well with undoing changes.
Explanation:
For every edit your users make, there's an additional edit made by your script: turning the new values to rich text values.
Therefore, in order to undo each manual edit, you should undo the action twice (one for the manual edit and one for the script edit). But, each time you undo an action via ctrl + z, that "undo" counts as a further edit and your script gets triggered again, making in its turn an additional change to the spreadsheet.
That is, every time you try to undo a change, the script makes an additional change. So, if you want to undo multiple actions, you'll have to press ctrl + z many times and fast enough so as to not give the script time to change the value again.
Workaround:
As a workaround for your specific case, I'd suggest checking whether the old value (pre-edit) contains the string https://www.behance.net (using the event object property e.oldValue). If I understand your workflow correctly, that will mean the script is triggering due to an "undo" action, and so it should not make an additional edit.
function onEdit(e){
try {
if (!e.oldValue.includes("https://www.behance.net")) {
update(e);
}
} catch(err) {
update(e);
}
}
function update(e) {
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
myFunction(sh,sh.getRange(i,j),sh.getRange(i,j).getValue())
}
}
}
function myFunction(sheet,range,value){
if (sheet.getName() == "RemcoE33" && range.getColumn() < 3 && range.getRow() > 1) {
const urls = [`https://www.behance.net/${value}`,`https://www.behance.net/${value}/moodboards`];
const richTextValues = [];
console.log(urls);
urls.forEach(url => {
richTextValues.push(SpreadsheetApp.newRichTextValue()
.setText(value)
.setLinkUrl(url)
.build())
});
sheet.getRange(range.getRow(), 1, 1, 2).setRichTextValues([richTextValues]);
}
}
I would like to find if a certain value is in a range using app scripts for google sheets.
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getDataRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
If I have my range rangeBikeNumbers, how can I check if the number "42" for example is in that range. I have searched for hours now and have beeb unable to find any answer to this. indexOf only seems to return -1, regardless of whether or not the value is in the range.
var indexDataNumber = values.indexOf(42); for example always ends up being -1
I believe your goal as follows.
You want to check whether the value of 42 is existing in the range of A5:A5000.
In this case, I would like to propose to use TextFinder. Because when TexiFinder is used, the process cost is low. Ref By the way, getDataRange has not arguments. From your script, I thought that you might want var rangeBikeNumbers = sheet.getRange("A5:A5000");.
When this is reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var find = rangeBikeNumbers.createTextFinder("42").matchEntireCell(true).findNext();
if (find) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
Note:
About var indexDataNumber = values.indexOf(42); for example always ends up being -1, I think that the reason of this issue is due to that values is 2 dimensional array. If you want to use this, you can also use the following script.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
var find = values.map(([e]) => e).indexOf(42); // of values.flat().indexOf(42);
if (find > -1) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
References:
Benchmark: Process Costs for Searching Values in Spreadsheet using Google Apps Script
getDataRange()
getRange(a1Notation)
createTextFinder(findText)
Select any active range that you wish to search and it will search for the seed in that at range. The seed is currently defaulted to 42 but you can change it.
function findSeedInRange(seed = 42) {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
})
})
if(!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
}
Here's another approach:
function findSeedInRange() {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const resp = ui.prompt('Enter Seed', 'Enter Seed', ui.ButtonSet.OK_CANCEL)
if (resp.getSelectedButton() == ui.Button.OK) {
var seed = parseInt(resp.getResponseText());
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
});
});
if (!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
} else {
ui.alert('Operation cancelled.')
}
}
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]) };
}
}
}