Google Apps Script - Function Changes Cell Value Dynamically Based On Other Cells - google-apps-script

This will likely be an easy question to answer but its an issue I run into often. Most built in google sheet functions change dynamically when cell values are updated, I need my function to do the same rather than needing to run the function to check the dates. The following script is likely messy to anyone who knows what they're doing, but it works. It is built to scan if todays date matches any in the range named "Dates" and return the value in that objects column and row of the active cell. However, this only activates when I first plug the function into the cell. I need it to change the cells value whenever one of those dates matches todays date or when none of them match todays date.
function getClass() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var dateRanges = ss.getRangeByName("Dates");
var dateValues = dateRanges.getValues();
var studentRow = sheet.getActiveCell().getRow();
var todaysDate = Utilities.formatDate(new Date(),"00:00:00", "MM/dd/yyyy");
var datesWithCells = [];
for (i=0;i<=dateRanges.getNumColumns() -1;i++){
var date = [Utilities.formatDate(dateValues[0][i], "00:00:00", "MM/dd/yyyy")];
var col = [dateRanges.getColumn() + i]
datesWithCells.push([col,date]);
};
for (i=0;i<=dateRanges.getNumColumns() -1;i++){;
var dataCol = datesWithCells[i][0];
if (datesWithCells[i][1] == todaysDate){
return sheet.getRange(studentRow,dataCol).getValue();
} else {
return "N/A";
};
};
}
Image shows the cells correctly displaying N/A as no dates match todays date. If todays date did match, it would not change value because the function has already ran and will not update this value as needed

It is unclear why you would want to do that with a custom function, because a plain vanilla spreadsheet formula would seem to suffice:
=hlookup(today(), D2:Z, row() - row(D2) + 1, false)
To answer your question, you should not hard-code the location of the data in the custom function. Instead, pass the values to the custom function through parameters. The formula that uses a custom function will update its result automatically when its parameters change.

Related

Conditionally edit cells based of the value in cells in other rows - Google Sheet using Google Script

I've got limited knowledge of google script and I'm trying to get better every day. I'm trying to conditionally set the value of 2 cells based on the value in cells contained in other rows. I've got a sample sheet (here) with appointments on it. You'll also see the output desired on the output sheet.
When two or more appointments are taken by the same person based on his email address I want to write Yes in column Duplicate for every duplicate appointments but the most recent (based on Column E, that is the date when the appointment was created) and that are greater than the current date (if the appointment is already in the past no need to do anything). I also want to set the value of the column L to "Not Coming" which is a cell containing a data validation that I already automated on my main spreadsheet.
Here is the script that I already designed based on other questions answered here on stackoverflow. I'm not really familiar with indexes and how to proceed with them. The script runs without errors but nothing happens.
var currentDate = new Date()
// Master
var sheetMaster = ss.getSheets()[0];
var allValues=sheetMaster.getRange(2,1,sheetMaster.getLastRow()-1,sheetMaster.getLastColumn()).getValues();
var emailValues=sheetMaster.getRange(2,3,sheetMaster.getLastRow()-1,3).getValues();
var dateCreatedAtValues=sheetMaster.getRange(2,5,sheetMaster.getLastRow()-1,5).getValues();
var duplicateColumn=sheetMaster.getRange(2,11,sheetMaster.getLastRow()-1,11);
var eM=[];//emails
var dA=[];//dates
var eR=[];//entire rows
var dC=[];//duplicateColumn Yes or empty
function analyzeDuplicateEntries() {
for(var i=0;i<emailValues.length;i++) {
var idx=eM.indexOf(emailValues[i][0]);
if(idx==-1) {
eM.push(emailValues[i][0]);
dA.push(dateCreatedAtValues[i][0]);
eR.push(allValues[i]);
}
else if(new Date(dateCreatedAtValues[i][0]).valueOf() > new Date(dA[idx]).valueOf() && new Date(dateCreatedAtValues[i][0]).valueOf()> currentDate) {
duplicateColumn[i][0].setValue("Yes");
}
}
} ```
You are retrieving the wrong column and set the values to a range cell incorrectly
var mailValues=sheetMaster.getRange(2,3,sheetMaster.getLastRow()-1,3).getValues(); will return columns 3 to 5 (see documentation, while your emails are in column B, that is column 2.
Pay attention that the first parameter in getRange(row, column, numRows, numColumns) is the number of the columns to retrieve, rather than the last column
Mind that to use setValue on a range that contains more than one cell, you need to retrieve first the respective cell with getCell()
Take thereby into consideration that the cell indexes start with 1 (opposed to array indexes that start with 0`).
A simple (not optimal) way to rewrite your code would be:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var currentDate = new Date()
// Master
var sheetMaster = ss.getSheets()[0];
var allValues=sheetMaster.getRange(2,1,sheetMaster.getLastRow()-1,sheetMaster.getLastColumn()).getValues();
var emailValues=sheetMaster.getRange(2,2,sheetMaster.getLastRow()-1,1).getValues();
var dateCreatedAtValues=sheetMaster.getRange(2,5,sheetMaster.getLastRow()-1,1).getValues();
var duplicateColumn=sheetMaster.getRange(2,11,sheetMaster.getLastRow()-1,1);
var eM=[];//emails
var dA=[];//dates
var eR=[];//entire rows
var dC=[];//duplicateColumn Yes or empty
function analyzeDuplicateEntries() {
for(var i=0;i<emailValues.length;i++) {
var idx=eM.indexOf(emailValues[i][0]);
if(idx==-1) {
eM.push(emailValues[i][0]);
dA.push(dateCreatedAtValues[i][0]);
eR.push(allValues[i]);
}
else if(new Date(dateCreatedAtValues[i][0]).valueOf() > new Date(dA[idx]).valueOf() && new Date(dateCreatedAtValues[i][0]).valueOf()> currentDate) {
duplicateColumn.getCell(i+1, 1).setValue("Yes");
}
}
}

Date is copied as "Range" in Google Sheets Macro/Script

I am struggling with a potential minor issue in google Sheets. I simply want to copy a date value to another cell, without changing the date format, etc.
However, if I save the date in a var, the Logger finds "Range". I also tried formatting, but I ended up with wrong dates, current dates, or just nothing at all.
Hint: Cell 63,4 contains a date value, formatted as a date in the spreadsheet.
function test() {
var as = SpreadsheetApp.getActiveSpreadsheet();
var ss = as.getActiveSheet();
var start = ss.getRange(63,4);
var row = start.getRow();
var col = start.getColumn();
Logger.log(start);
start = new Date(0);
ss.getRange('C2').setValue(start);
...
}
After running this code, the log shows "RANGE".
Do you have an idea how to solve this? I think I am not saving the cell correctly, but I did not find any further ideas.
Explanation / issue:
That is because you are not getting the value and getRange
returns an instance of the range class.
Keep in mind that if the value is a date you might want to use
getDisplayValue().
Solution:
Replace:
var start = ss.getRange(63,4);
with:
var start = ss.getRange(63,4).getDisplayValue();
and remove that part:
start = new Date(0);
since this will overwrite the current value that is assigned to start which is the desired date you want to get.
Code snippet:
function test() {
var as = SpreadsheetApp.getActiveSpreadsheet();
var ss = as.getActiveSheet();
var start = ss.getRange(63,4).getDisplayValue(); // cell D63
ss.getRange('C2').setValue(start);
}

How do I write data in every last column?

I need to add a new column everyday and the data insertion will be done individually for the specific row comparing with the input... please tell me if it is correct or not.
will this piece of code work for adding new columns ever day and initialize all the entries by 0?
function trig(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1);
var trigger = builder.create();
}
function addcol(){
var cname = builder.atDate( day, month, year)
var column = eventRange.getLastColumn();
sheet.insertColumnAfter(column).setName(cname);
sheet.getRange("E1").setValue(new Date()).setNumberFormat('d/M/yyyy');
var col = [];
for(var n=0 ; n<s.getMaxRows();n++){
col.getLastColumn().push(['0']);
}
ss.getRange('N:N').setValues(col);
}
// now for the insertion part
here the sr will be compared to SRN from the sheet (E) and if it matches it will replace 0 with 1 in the last column added everyday. plese tell me will this work?
function doPost(e){
var action = e.parameter.action;
if(action == 'scanner'){
return scanner(e);
}
}
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for(i=1; i<=R; i++)
{
if (srn == sheet.getDataRange([i][2]))
{
sheet.getDataRange([i],[C]).push[(1)];
sheet.append([i],[C]);
return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
break;
}
}
}
Time-based trigger:
There are no event objects associated with time-based triggers, so variables like eventRange cannot work. It seems like you want to use variables in addcol that are defined in trig (e.g. builder). That is not possible. Also, if you want your function to run once a day, there is no need for lines like this: builder.atDate(day, month, year)). The trigger will be created by running this function once:
function createTrigger(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1).create();
}
Adding column with 0's:
There are many problems with the function addcol:
Several uninitialized variables are being used (s, builder, eventRange).
Unexisting methods are being used: e.g.: setNumberFormat is a method of the Range class, not of the Date object. You should use Utilities.formatDate(date, timeZone, format) to format dates. Also, you are using setName when inserting a new column, but that changes the sheet name. Is that what you want to do? And also, cname is assigned a trigger builder as value, which I seriously doubt is your purpose. The same way, an array col does not have a method getLastColumn().
You could use this addcol function instead (change your sheet name, currently set to Your sheet name, and the timeZone in formatDate, currently set to GMT:
function addcol() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Your sheet name"); // Change accordingly
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (sheet.getMaxColumns() === lastCol) sheet.insertColumnAfter(lastCol);
var newCol = sheet.getRange(1, lastCol + 1, lastRow, 1);
var values = [];
values.push([Utilities.formatDate(new Date(), "GMT", "d/M/yyyy")]); // Change accordingly
for (var i = 1; i < sheet.getLastRow(); i++) {
values.push([0]);
}
newCol.setValues(values);
}
Replacing 0's with 1's:
Assuming that you are getting the function scanner to run correctly and that the parameter e.parameter.sr is getting populated correctly, you can do the following:
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for (i=1; i<=R; i++) {
if (srn == sheet.getRange(i, 2).getValue()) {
sheet.getRange(i, C).setValue(1);
}
}
}
Here you were also using unexisting methods or providing incorrect parameters:
The method getDataRange doesn't allow any argument, you should be using getRange(row, column), and provide the row and column indexes separated by commas, not as if trying to access a 2D array.
break terminates the current loop, so only use it if you only want to update 1 cell. The same goes for return which finishes current function execution.
Reference:
Spreadsheet Service
Installable Triggers
Short
No
Long
There are several problems with the script:
getDataRange() expects no arguments passed (docs only say it is the same as using getRange(yourSheet.getLastRow(), yourSheet.getLastColumn()), not that you should do it). Certainly it does not expect instances of Array (bracket [] notation wraps C and i, which are of type Number into one). Moreover, it returns a Range, which at the time of writing does not have push() method.
getLastColumn() returns an instance of Number, and thus does not have a push() method as well. You are on the right track, though, since col is an Array, and you need to push() into it.
If you want the script to add a zero-filled column, don't get constant ranges: in current state, getRange('N:N') guarantees that each time you will re-initialize column N. Btw, same goes for getRange("E1").
You still haven't addressed issues listed in comments to your previous question.
Also, in your scanner function there is a syntax error: push[(1)] should be push([1]).
Also, the sheet variable is either undeclared or is declared globally, which is bad.
Notes
If you don't expect number of students to change dynamically, you can switch from getMaxRows() to getLastRow() to only zero-fill cells that are in range of cureent student info grid.
This question is a direct continuation of a currently closed one (please, always disclose that for reference at least).
How about skipping init to zero step at all? If cell is empty, getValue() / getValues() will return its value as an empty string, which is a falsy value, just as 0 is. If you want to count attendance at the end of period, a simple conditional will suffice to sum up.
The default MIME type for TextOutput instance obtained by createTextOutput() is plain text, so setting it to ContentService.MimeType.TEXT is an overkill in your case.
Reference
getDataRange() docs
getLastColumn() docs
getValue() docs
getValues() docs
Range docs
createTextOutput() docs
Falsy values explanation on MDN

range.getValues() With specific Date in Specific Cell

We are using a Google Script to import a Range from other Spreadsheet to another.
This helped us in the past but now the data is growing and we need to reduce the data that we import. (timeout problems)
We need to import the rows with a specific date on a specific column.
In this case, as you can see in the script below, we are importing cells from 'A1' to 'N last row' in the range variable.
What we need is that in the column 'H' from that range date is checked with something like "Date in column K >= Today()-90"
// iterate all the sheets
sourceSheetNames.forEach(function(sheetName, index) {
if (EXCLUDED_SHEETS.indexOf(sheetName) == -1) {
// get the sheet
var sheet = sourceSpreadSheet.getSheetByName(sheetName);
// selects the range of data that we want to pick. We know that row 1 is the header of the table,
// but we need to calculate which is the last row of the sheet. For that we use getLastRow() function
var lastRow = sheet.getLastRow();
// N is because we want to copy to the N column
var range = sheet.getRange('A1:N' + lastRow);
// get the values
var data = range.getValues();
data.forEach(function(value) {
value.unshift(sheetName);
});
}
});
To conditionally copy the only the rows that meet a criteria, you will want to push them to a new array if they qualify. This push would be added to your existing data.forEach() call:
...
var now = new Date();
var today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
var kept = [];
var data = range.getValues();
// Add qualifying rows to kept
data.forEach(function(row) {
var colHvalue = row[7];
var colKvalue = row[10];
if( /* your desired test */) {
// Add the name of the sheet as the row's 1st column value.
row.unshift(sheetName);
// Keep this row
kept.push(row);
}
});
/* other stuff with `kept`, like writing it to the new sheet */
You'll have to implement your specific test as you have not shared how time is stored in column H or K (e.g. days since epoch, iso time string, etc). Be sure to review the Date reference.
I've solved this in the past by adding a new column in the spreadsheet which calculates n days past an event.
=ARRAYFORMULA(IF(ISBLANK(K2:K),"",ROUNDDOWN(K2:K - NOW())))
The core of the function is the countdown calculation. For instance, today is Thursday, March 1. Subtracting it from a date in the future like Sunday, March 4, returns a whole integer: 3. I can test for that integer (or any integer) in a simple script.
In your script, add a conditional statement before executing the rest of the function:
// ...
if(someDate === -90) {
// do something...
}
This way, you're just checking the value of a cell rather than doing a calculation in a helper function. The only change (if you want a longer or shorter interval) is in the conditional test.

Sheets function to add content to another sheet

I've been making slow but steady progress on this app that creates the daily bulletin for the school where I teach.
Data is submitted by staff via a form, and is then naturally in a sheet. I already created a script to purge old data from the sheet, thanks in part to help I've gotten here. An additional script orders content on the data sheet by bulletin category, creates a copy of a template sheet, names it by the desired date, puts the date at the top. That's about as far as I've gotten. It also adds the first category heading by default, which is mostly a test.
What I'm attempting to do now is loop through each row of the data sheet to determine if any of the three date columns contains the desired date (entered via a dialog box earlier in the script). If any of them match today's date, we then will check to see if the current category and the category in the row are the same. If they are not, we change the current category and add a new heading to the bulletin sheet. If they are the same, we get the announcement itself and add that to the bulletin sheet. I suspect I'll use embedded functions for these two purposes.
Right now I'm stuck on the loop portion. Again, this should cycle through each row of the data sheet. There are three columns containing the dates (C, D, E). If I can get it to recognize date matches from one of the cells in this range, I can move forward with the rest.
function writeBulletin() {
//get the bulletin date
var bullSheet = todayDay;
//make the bulletin sheet active
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(todayDate));
//set var for needed sheets
var responses = ss.getSheetByName("Form Responses 1")
var bulletin = ss.getSheetByName(todayDate)
//get the date from the sheet title and apply it to the date range
var dateCell = bulletin.getRange(3,1);
var sheetDate = bulletin.getName();
dateCell.setValue(sheetDate);
//works
//Now we start building the bulletin
//currentDataRow is a reference to the Responses sheet. Used in later for loop
var currentDataRow = 2;
var currentBulletinRow = 11;
var catCurrent = "01 Administration";
var catCurrentSS=catCurrent.substring(3,30);
var lastRow = responses.getLastRow(); //get last row of data sheet
var lastBull = bulletin.getLastRow(); //get last row of bulletin sheet
var nextBullRow = lastBull+2;
var testOutput = bulletin.getRange(6,3);
var nextBullItem = bulletin.getRange(nextBullRow,1);
nextBullItem.setValue(catCurrentSS);
//testOutput.setValue("dude"); //this works
if(responses.getRange(2,3).getValue()==todayDate) {
testOutput.setValue("dude");
}
//bulletin.getRange(2,3).setValue("dude"); //test row
for(var i = 2; i<=lastRow; i++) {
if(5>3) {
//if(responses.getRange(i,3).getValue()==sheetDate||responses.getRange(i,4).getValue()==sheetDate||responses.getRange(i,5).getValue()==sheetDate){
//bulletin.getRange(nextBullRow,3).setValue("dude");//works
bulletin.getRange(nextBullRow,1).setValue(responses.getRange(i,7).getValue());
nextBullRow+=2;
}
}
}
I did notice that my loop condition statement had a reversed inequality sign; however, fixing this did not seem to help.
jdv: Good point. fixed it now
Aside from the issue of repeatedly interacting with the Spreadsheet interface (the alternative being to read values from the Spreadsheet once, then work with the resulting javascript Array object), the issue is that you are comparing a Range object with a String:
var sheetDate = bulletin.getName();
...
if(responses.getRange(i, 3) == sheetDate || ..... ) {
This will not work :) You need to access the value of the Range:
if(responses.getRange(i, 3).getValue() == sheetDate || ... ) {
edit: as mentioned in comments, the values in these responses cells are interpreted as Date objects. Date comparisons are fun, because you get to play with time zones and/or format strings. I recommend avoiding needing to use dates in this manner, especially when starting out with scripts.
One possible fix for this new issue is to use the value from dateCell.getValue() after calling SpreadsheetApp.flush() (to ensure the writing of sheetDate is performed first). This will let the spreadsheet do the nasty work making the correct date:
dateCell.setValue(sheetDate);
SpreadsheetApp.flush();
// Construct the proper Date object from the sheetDate value
var compareDate = dateCell.getValue();
...
for(var i = 2; i <= lastRow; ++i) {
// Read into an array [[ 0: elem#(i,3), 1: elem#(i,4), 2: elem#(i,5), 3: elem#(i,6), 4: elem#(i,7) ]]
var row = responses.getRange(i, 3, 1, 5).getValues();
if(row[0][0] == compareDate || row[0][1] == compareDate || row[0][2] == compareDate) {
...