Im trying to make a spreadsheet to manage my money/expenses and have run into a problem trying to automate my process
I want to make a piece of code that runs every time a cell has been edited in the sheet.
When triggered, i want it to calculate ssum, lsum and betal(in the loop), and then put it into 3 different cells. The code behaves as expected, but the onedit trigger doesnt work.
This is my code:
function regnudbetalprocent() {
var betal = 0;
var i = 1;
var app = SpreadsheetApp;
var ss = app.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var sum = activeSheet.getRange(18, 5).getValue();
var ssum;
var lsum;
var ssumori = activeSheet.getRange(3, 8).getValue();
var lsumori = activeSheet.getRange(4, 8).getValue();
var fuld = activeSheet.getRange(18, 2).getValue();
while(betal < sum){
ssum = ((ssumori - fuld / 2) / 100) * i;
lsum = ((lsumori - fuld / 2) / 100) * i;
betal = ssum + lsum;
i++;
}
if (betal > sum) {
var output = [
[ssum,lsum],
["Samlet",betal]
]
return output;
}
}
The output variable sets the neighbouring cells accordingly from where the function is called
I've tried with setValue and clearContent, but it i cant edit outside the cell from where the function is called. I've used Edit -> current project's triggers to add an onEdit trigger, which increments each time i edit the sheet, but nothing happens.. I'm burned out
Can someone guide me? how do i get what i want?
The values for using with the calculation are the constant cells of "B18", "E18", "H3" and "H4".
You want to run the script when one of above cells is edited. You want to use OnEdit event trigger.
You want to put the result value to the cells of "D21", "E21" and "E22".
Your calculation has no issues.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Modification points:
In your script, I think that the simple trigger can be used instead of the installable trigger.
From your replying, about "Exception: You do not have permission to call setValue (line 7).", I think that you might be using the function of regnudbetalprocent() as the custom function. In this case, such error occurs.
You can know the coordinate of the edited cell using the event object.
When above points are reflected to your script, it becomes as follows.
Modified script:
The function of onEdit is used as the simple trigger. So in order to run the script, please manually edit one of cells "B18", "E18", "H3" and "H4". By this, the script is run and retrieved values from the cells of "B18", "E18", "H3" and "H4", and the calculated result is put to the cells of "D21:E22".
function onEdit(e) {
var a1Notation = e.range.getA1Notation(); // Added
if (a1Notation != "B18" && a1Notation != "E18" && a1Notation != "H3" && a1Notation != "H4") return; // Added
var betal = 0;
var i = 1;
var app = SpreadsheetApp;
var ss = app.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var sum = activeSheet.getRange(18, 5).getValue();
var ssum;
var lsum;
var ssumori = activeSheet.getRange(3, 8).getValue();
var lsumori = activeSheet.getRange(4, 8).getValue();
var fuld = activeSheet.getRange(18, 2).getValue();
while(betal < sum){
ssum = ((ssumori - fuld / 2) / 100) * i;
lsum = ((lsumori - fuld / 2) / 100) * i;
betal = ssum + lsum;
i++;
}
if (betal > sum) { // Modified
var output = [[ssum,lsum], ["Samlet",betal]];
var r = e.range.getSheet().getRange("D21:E22");
r.clearContent(); // This line might not be required.
r.setValues(output);
SpreadsheetApp.flush(); // This line might not be required.
}
}
References:
Simple Triggers
Event Objects
offset(rowOffset, columnOffset, numRows, numColumns)
Related
I'm working on a script that takes a list of tasks from Google Sheets and automatically creates events on Google Calendars, putting the event on the Calendar of the team member to whom the task is applied.
All that is working, now my issue is that I want to be able to run the script over the whole range of cells as tasks are added without creating duplicate events.
I tried using the method described here:
https://stackoverflow.com/a/57785525/12412425
BUT this only checks the event/task name, which doesn't work for me because I will likely be assigning tasks with the same name, but different dates, etc.
My thought was to make an extra column where, after an event is created, the code writes an "X" in the cell, and as the code executes, it checks this cell, and if it already has an "X", the row will be skipped. But I'm struggling to wrap my head around how to identify the cell I need to write to.
Basically, I would want to iterate through the rows of the specified range, and if the "CalendarID" cell contains a value, you can assume an event was created, therefore in the next cell over, write an "X". The rest would just be a simple if-statement to check for the "X".
My input data
Can anyone advise on how to do this? Or an alternate strategy?
(This is my first attempt at using Apps Script/JS)
Many thanks.
Current code:
function scheduleTasks() {
var spreadsheet = SpreadsheetApp.getActiveSheet();
var myRange = spreadsheet.getRange("E9:H").getValues();
for (x=0; x<myRange.length;x++)
{
var myRow = myRange[x];
var calendarID = myRow[3];
if(calendarID){
var eventCal = CalendarApp.getCalendarById(calendarID);
var task= myRow[0];
var startTime = myRow[1];
var endTime = myRow[2];
var existingEvents=Calendar.Events.list(calendarID);
var eventArray=[];
existingEvents.items.forEach(function(e){eventArray.push(e.summary)});
if(eventArray.indexOf(task)==-1){
eventCal.createEvent(task, startTime, endTime);
}else{
Logger.log('event exists already');
}
}
}
Where (I think) I'm trying to go with it (See comments)
function scheduleTasks() {
var spreadsheet = SpreadsheetApp.getActiveSheet();
var myRange = spreadsheet.getRange("E9:H").getValues();
for (x=0; x<myRange.length;x++)
{
var myRow = myRange[x];
var calendarID = myRow[3];
if(calendarID){
//if cell in "Scheduled" column, row x is false
var eventCal = CalendarApp.getCalendarById(calendarID);
var task= myRow[0];
var startTime = myRow[1];
var endTime = myRow[2];
eventCal.createEvent(task, startTime, endTime);
//write "X" in cell in "Scheduled" column, Row x
}
}
}
In your situation, how about the following modification?
Modified script 1:
In this sample script, the value of X is put in the "Scheduled" column.
function scheduleTasks() {
var spreadsheet = SpreadsheetApp.getActiveSheet();
var myRange = spreadsheet.getRange("E9:I" + spreadsheet.getLastRow()).getValues();
var rangeList = [];
for (x = 0; x < myRange.length; x++) {
var myRow = myRange[x];
var calendarID = myRow[3];
var scheduled = myRow[4];
if (calendarID && scheduled != "X") {
var eventCal = CalendarApp.getCalendarById(calendarID);
var task = myRow[0];
var startTime = myRow[1];
var endTime = myRow[2];
eventCal.createEvent(task, startTime, endTime);
rangeList.push(`I${x + 9}`)
}
}
if (rangeList.length == 0) return;
spreadsheet.getRangeList(rangeList).setValue("X");
}
When this script is run, when "calendarID" is existing and "Scheduled" column has no value of "X", the script in the if statement is run. In this case, in order to put the value of "X", I used RangeList.
Modified script 2:
In this sample script, the value of the event ID is put in the "Scheduled" column.
function scheduleTasks() {
var spreadsheet = SpreadsheetApp.getActiveSheet();
var lastRow = spreadsheet.getLastRow();
var myRange = spreadsheet.getRange("E9:I" + lastRow).getValues();
var scheduledAr = [];
for (x = 0; x < myRange.length; x++) {
var myRow = myRange[x];
var calendarID = myRow[3];
var scheduled = myRow[4];
if (calendarID && !scheduled) {
var eventCal = CalendarApp.getCalendarById(calendarID);
var task = myRow[0];
var startTime = myRow[1];
var endTime = myRow[2];
var event = eventCal.createEvent(task, startTime, endTime);
scheduledAr.push([event.getId()]);
} else {
scheduledAr.push([scheduled]); // Modified
}
}
spreadsheet.getRange("I9:I" + lastRow).setValues(scheduledAr);
}
When this script is run, when "calendarID" is existing and "Scheduled" column has no value, the script in the if statement is run. In this case, in order to put the value of event ID, I used setValues.
References:
getRangeList(a1Notations)
setValues(values)
https://i.stack.imgur.com/7VAJk.png
i want to copy data from "dB" sheet A5:A29 and paste to correct column.
so i use the script to find the correct column.
there range B2:CX2 have 0(not-correct) or 1(correct) value, so i use 'for' & 'if'
BUT!! It's too delay!!
i use console.time() and i get 25909ms(timecheck2 value) !!!
please help me.....
here is my code
function save(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('dB');
console.time("timecheck1");
//find last row
var copyrangeO = sheet.getRange(5,1,25,1).getValues();
var lastrowO = copyrangeO.filter(String).length;
var copyrange = sheet.getRange(5,1,lastrowO,1);
console.timeEnd("timecheck1");
//my dB data start "B2".
var cv = 1;
//find correct value(1). B2 ~ CX2 (#100)
console.time("timecheck2");
for (var i=2; i<101;i++){
if(sheet.getRange(2,i).getValue()===1){
cv = i;
}
}
console.timeEnd("timecheck2");
//if data isn't correct, cv===1. so error msg print.
console.time("timecheck3");
if(cv ===1){
Browser.msgBox("ERROR")
}else {
//data copy and paste.
var columnToCheck = sheet.getRange(4,cv,1000).getValues();
var lastrow = getLastRowSpecial(columnToCheck);
var pasterange = sheet.getRange(lastrow+4,cv);
copyrange.copyTo(pasterange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
Browser.msgBox(lastrowO + " saved!");
}
console.timeEnd("timecheck3");
}
Issue:
If I understand your situation correctly, you want to find the cell in B2:CX2 in which the value is 1, but the script is taking too much time for this.
The problem here is that you are using getRange and getValue in a loop (sheet.getRange(2,i).getValue()===1). This greatly increases the amount of calls to the Sheets service, which slows down your script, as you can see at Minimize calls to other services.
Solution:
In that case, I'd suggest doing the following:
Get the values from all columns at once using getValues().
Use findIndex to get the column index for which value is 1.
In order to do that, replace this:
var cv = 1;
//find correct value(1). B2 ~ CX2 (#100)
console.time("timecheck2");
for (var i=2; i<101;i++){
if(sheet.getRange(2,i).getValue()===1){
cv = i;
}
}
With this:
var ROW_INDEX = 2;
var FIRST_COLUMN = 2; // Column B
var LAST_COLUMN = 102; // Column CX
var columnValues = sheet.getRange(ROW_INDEX, FIRST_COLUMN, 1, LAST_COLUMN-FIRST_COLUMN+1).getValues()[0];
var cv = columnValues.findIndex(columnValue => columnValue === 1) + FIRST_COLUMN;
Note:
If there's no cell in the range with value 1, findIndex returns -1 which, added to FIRST_COLUMN, results in 1. That's appropriate for your current script, but won't work if the FIRST_COLUMN stops being 2, so be careful with this (either change the condition if(cv ===1){ to something less strict, or don't assign the resulting value to cv if findIndex returns -1).
The function will spend most of its time in the for loop because it repeats the Range.getValue() call many times. You can speed things up quite a bit by getting all values with one Range.getValues() call, like this:
let cv = 1;
console.time("timecheck2");
sheet.getRange('B2:B100').getValues().flat()
.some((value, index) => (cv = 2 + index) && value === 1);
console.timeEnd("timecheck2");
Note that this is not a cleanest way of finding cv, but it should help illustrate why you have a performance issue. You may want to do a complete rewrite of the code, using declarative instead of imperative style.
Try this:
I don't know what you're doing in the save because to did not supply the helper function code.
function save(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('dB');
var vs0 = sh.getRange(5,1,25,1).getValues();
var lr0 = vs0.filter(String).length;
var crg = sh.getRange(5,1,lr0,1);
var cv = 1;
const vs1 = sh.getRange(2,2,1,99).getValues().forEach((c,i) => {
if(c == 1)cv = i + 2
})
if(cv == 1){
Browser.msgBox("ERROR")
}else {
var vs2 = sh.getRange(4,cv,1000).getValues();
var lastrow = getLastRowSpecial(vs2);
var drg = sh.getRange(lastrow+4,cv);
crg.copyTo(drg, SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
Browser.msgBox(lr0 + " saved!");
}
}
There are two ways that i am able to add an auto increment column. By auto-increment, i mean that if column B has a value, column A will be incremented by a numeric value that increments based on the previous rows value.
The first way of doing this is simple, which is to paste a formula like the one below in my first column:
=IF(ISBLANK(B1),,IF(ISNUMBER(A1),A1,0)+1)
The second way i have done this is via a GA script. What i found however is performance using a GA script is much slower and error prone. For example if i pasted values quickly in the cells b1 to b10 in that order, it will at times reset the count and start at 1 again for some rows. This is because the values for the previous rows have not yet been calculated. I assume that this is because the GA scripts are probably run asynchronously and in parallel. My question is..is there a way to make sure each time a change happens, the execution of this script is queued and executed in order?
OR, is there a way i should write this script to optimize it?
function auto_increment_col() {
ID_COL = 1;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
//only increment column 1 for sheets in this list
var auto_inc_sheets = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("auto_inc_sheets").getValues();
auto_inc_sheets = auto_inc_sheets.map(function(row) {return row[0];});
var is_auto_inc_sheet = auto_inc_sheets.indexOf(spreadsheet.getSheetName()) != -1;
if (!is_auto_inc_sheet) return;
var worksheet = spreadsheet.getActiveSheet();
var last_row = worksheet.getLastRow();
var last_inc_val = worksheet.getRange(last_row, ID_COL).getValue();
//if auto_inc column is blank and the column next to auto_inc column (col B) is not blank, then assume its a new row and increment col A
var is_new_row = last_inc_val == "" && worksheet.getRange(last_row, ID_COL+1).getValue() != "";
Logger.log("new_row:" + is_new_row + ", last_inc_val:" + last_inc_val );
if (is_new_row) {
var prev_inc_val = worksheet.getRange(last_row-1, ID_COL).getValue();
worksheet.getRange(last_row, ID_COL).setValue(prev_inc_val+1);
}
}
There is my vision of auto increment https://github.com/contributorpw/google-apps-script-snippets/tree/master/snippets/spreadsheet_autoincrement
The main function of this is
/**
*
* #param {GoogleAppsScript.Spreadsheet.Sheet} sheet
*/
function autoincrement_(sheet) {
var data = sheet.getDataRange().getValues();
if (data.length < 2) return;
var indexCol = data[0].indexOf('autoincrement');
if (indexCol < 0) return;
var increment = data.map(function(row) {
return row[indexCol];
});
var lastIncrement = Math.max.apply(
null,
increment.filter(function(e) {
return isNumeric(e);
})
);
lastIncrement = isNumeric(lastIncrement) ? lastIncrement : 0;
var newIncrement = data
.map(function(row) {
if (row[indexCol] !== '') return [row[indexCol]];
if (row.join('').length > 0) return [++lastIncrement];
return [''];
})
.slice(1);
sheet.getRange(2, indexCol + 1, newIncrement.length).setValues(newIncrement);
}
But you have to open the snippet for details because this doesn't work without locks.
so I have a new problem.
So far my script here can loop a sheet and find the text "Bank", it will set the background color to red and it will take the value from another cell as marked and log it. Once it have done that once it will crash, so the loop will break, I have no idea why?
function sortBank() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var rowsDeleted = 0
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
if (row[8].indexOf('Bank') > -1) { /** Set the Job prefix **/
sheet.getRange(parseInt(i)+1,9).setBackgroundColor("#f44336");
var values = sheet.getRange(parseInt(i),2).getValues();
Logger.log(values[0][0]);
}
}
};
You may look at similar questions:
Google Script - Internal Error after 15 seconds
Google script - Exceeded maximum execution time , help optimize
Google sheet script, times out. Need a new way or flip it upside down
Basic solution is to use getValues() one time and then loop values in 2d array:
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var data = rows.getValues();
for (var i = 0; i < numRows; i++)
{
var j = SomeValue; // column number - 1
var row = data[i]; // row from origonal data range
var value = row[j]; // value from data
// some other code...
}
See more info about your problem here:
Your scripts will run faster if you can find ways to minimize the
calls the scripts make to those services.
I have this script in Google Spreadsheet and it fetches all the rows marked "READY" just fine, then it sets the value in column "W"(23) to "SENT", and then I am trying to avoid fetching duplicates, by marking the column as "SENT" but then when I run the code again, it ignores the "SENT" that it just pasted? What is wrong here?
var ss = SpreadsheetApp.openById("12y85GmJ94s6k3213j2nGK8rFr0GOfd_Emfk8WHu_MUQ");
var stitchSheet = ss.getSheetByName("Sheet8");
var orderSheet = ss.getSheetByName("Sheet1");
var SENT = "SENT";
function getOrders() {
var range = orderSheet.getDataRange();
var orders = range.getValues();
for (var i = 1; i < orders.length; i++) {
var row = orders[i];
var status = row[1];
var order = row[4];
var name = row[5];
var system = row[22];
if(system != SENT){
if(status.toString() === 'READY'){
orderSheet.getRange(i,23).setValue(SENT);
stitchSheet.appendRow([order,name]);
}
}
}
}
Your code is fine, so there must be a logical error somewhere. I've noticed that you made var i in the for loop 1. I do not know if this is intentional or not, but the index of arrays pretty much always starts with 0 in most programming languages, which means that you'll start at row 2 of your sheet, not row 1.
Finding logical errors
To find logical errors you need to learn how to use the debugger console in the script editor.
Put breakpoints on the lines I mark with a star below:
var ss = SpreadsheetApp.openById("12y85GmJ94s6k3213j2nGK8rFr0GOfd_Emfk8WHu_MUQ");
var stitchSheet = ss.getSheetByName("Sheet8");
var orderSheet = ss.getSheetByName("Sheet1");
var SENT = "SENT";
function getOrders() {
var range = orderSheet.getDataRange();
* var orders = range.getValues();
for (var i = 1; i < orders.length; i++) {
* var row = orders[i];
var status = row[1];
var order = row[4];
var name = row[5];
* var system = row[22];
if(system != SENT){
if(status.toString() === 'READY'){
orderSheet.getRange(i,23).setValue(SENT);
stitchSheet.appendRow([order,name]);
}
}
}
}
Start the debugger and it will stop at the first breakpoint. Inspect what value range has. Expanding This should let you find orderSheet and SENT (since they are outside the function and should be in the scope). If not you've got a problem here.
Go to the next breakpoint and inspect orders, it should have an array of arrays now. You can inspect that you've got the right values, if not, skip forward to the next breakpoint and look at what row is.