Apps Script Google Sheets Birthday Reminder Range in Array - arrays

I'm trying to write an automatic birthday reminder for my team.
It's supposed to check if a persons birthday is today and if so, send a mail to everyone else in the team.
In Google Sheets, the four columns are: name, surname, e-mail and birthday. First row are headers.
This is what I got so far (mostly copied):
`
function main() {
// Load the sheet that contains the birthdays.
var sheet = SpreadsheetApp.getActive().getSheetByName("Geburtstage");
// Get the last row in the sheet that has data.
var numRows = sheet.getLastRow();
// Load data in the first two columns from the second row till the last row.
// Remember: The first row has column headers so we don’t want to load it.
var range = sheet.getRange(2, 1, numRows - 1, 4).getValues();
// Use a for loop to process each row of data
for(var index in range) {
// For each row, get the person’s name and their birthday
var row = range[index];
var name = row[0];
var birthday = row[3];
// Check if the person’s birthday is today
if(isBirthdayToday(birthday)) {
//If yes, send an email reminder
emailReminder(name);
}
}
}
// Check if a person’s birthday is today
function isBirthdayToday(birthday) {
var today = new Date();
if((today.getDate() === birthday.getDate()) &&
(today.getMonth() === birthday.getMonth())) {
return true;
} else {
return false;
}
}
// Function to send the email reminder
function emailReminder(name) {
var subject = "Geburtstagerinnerung: " + name;
var recipient = Session.getActiveUser().getEmail();
var body = name + " hat heute Geburtstag!";
MailApp.sendEmail(recipient, subject, body);
}
`
All that is missing is to replace the recipient by all e-mails in the third column, except for the person whos birthday it is.
My idea is, to save the range of the third column (e-mails) to an array, drop the e-mail whos birthday it is, and pass it to recipient as a comma separated string.
Afterwards reset the array in case two people have the same birthday.
My problem is, that I have no idea what I'm doing and all the solutions I found are overly complicated.

Try this:
function main() {
const dt = new Date();
const dtv = new Date(dt.getFullYear(),dt.getMonth(),dt.getDate()).valueOf();
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
const vs = sh.getRange(2, 1, sh.getLastRow() - 1, 4).getValues();
let recipients;
vs.forEach((R,i) => {
let d = new Date(R[3]);
let dv = new Date(d.getFullYear(),d.getMonth(),d.getDate()).valueOf();
if(dv == dtv) {
recipients = vs.filter((r,j) => j != i ).map( r => r[2]).flat().join(",")
GmailApp.sendEmail(recipients,`Todays ${R[0]} Birthday`,"");
}
});
}

Related

comparing json array rows with value on destination rows (apps script)

I've use this script before, and work well, here's script, This script copies the values and paste to last row in google sheet,
function doPost(request = {}) {
const { parameter, postData: { contents, type } = {} } = request; //request data
const { dataReq = {} } = JSON.parse(contents); //content
const { fname = {} } = JSON.parse(contents); //function name
const response = {
status: "function not found: " + fname, // prepare response in function not found
data2: dataReq
}
switch (fname) { //function selection
case 'pasteData':
var output = JSON.stringify(pasteDAta(dataReq)) //call function with data from request
break
default:
var output = JSON.stringify(response)
break
}
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JSON); //response to frontend
}
function pasteDAta(dataReq) {
const id = '1_27rjNQmlXrwVKpLWUbGrJYPJufGRa7Dk-XEKcNAHr0'; //id of Google Sheet
var sheet = SpreadsheetApp.openById(id).getSheetByName('Sheet1'); //sheet
var headings = sheet.getDataRange().getValues()[0]; //Headers
var values = dataReq.map((a) => {
let holder = [];
for (x in headings) {
let output = (headings[x] in a) ? a[headings[x]] : '';
holder.push(output);
}
return holder;
});
var len = values.length;
sheet.getRange(sheet.getLastRow() + 1, 1, len, values[0].length).setValues(values);
return "Numbers of sheets added: " + len;
}
I want this script to be able to check the row values ​​in column b (source [json]), if the value in the source rows in column b is the same as the value in the destination rows in column b (google sheet) then the values ​​are replaced all but if not, the values ​​are copied to the last rows. If it is possible, can anyone give me a modified working script?
Example
First condition; Before (Destination Sheet)
Date
Code
Name
Grade
02/04/21
Math1
John
80
02/04/21
Math2
John
80
Expected results
After replacing (from JSON)- if Column B (Code) is the same as the source
Date
Code
Name
Grade
02/04/21
Math1
Dare
78
02/04/21
Math2
Brian
90
Second condition; Before (Destination Sheet)
Date
Code
Name
Grade
02/04/21
Bio1
Anton
78
02/04/21
Bio2
Julian
65
Expected results
After after appending to last row (from JSON)- if Column B isn't same as the source
Date
Code
Name
Grade
02/04/21
Bio1
Anton
78
02/04/21
Bio2
Julian
65
02/04/21
Math1
Dare
78
02/04/21
Math2
Brian
90
Try to change the function pasteDAta() this way:
function pasteDAta(dataReq) {
const id = "1_27rjNQmlXrwVKpLWUbGrJYPJufGRa7Dk-XEKcNAHr0";
var sheet = SpreadsheetApp.openById(id).getSheetByName("Sheet1");
// get header and the rest data from the sheet
var [ headings, ...data ] = sheet.getDataRange().getValues();
var values = dataReq.map((a) => {
let holder = [];
for (x in headings) {
let output = headings[x] in a ? a[headings[x]] : "";
holder.push(output);
}
return holder;
});
var len = values.length;
var new_values = []; // values to add at the bottom of the sheet
var col_b = data.map(x => x[1]); // get column B from the data
values.forEach(row => {
// find an index of the row with the same value in cell B
var row_index = col_b.indexOf(row[1]);
// if nothing was found add the row to the new values
if (row_index == -1) new_values.push(row);
// else change the found row on the sheet
else sheet.getRange(row_index + 2, 1, 1, row.length).setValues([row]); // '+2' due the header
})
// add the new values at the bottom of the sheet
if (new_values.length > 0)
sheet.getRange(sheet.getLastRow() + 1, 1, len, new_values[0].length).setValues(new_values);
return "Numbers of sheets added: " + len;
}

How do you use multiple scripts in the same spreadsheet?

I have a spreadsheet that tracks legal cases from start to finish. In that spreadsheet I used a script to create dependent dropdown menus on multiple rows (I'll share the script at the end). The main dropdown menu tells me what Department the case is in. The Departments are Transcription; Doctor; Scheduling; Records; QA; and Billing.
I also have a script that is supposed to moves an entire row of data to a separate tab labeled Billing when the department dropdown menu is set to Billing. I had this script working before I made the dependent dropdown menus and was just using data validation to create my dropdown menu.
Both of these scripts work separately but when I try to use them together the dependent dropdown menus quit working and when the department is set to Billing that row disappears like its supposed to except it doesn't show up on the Billing tab like its supposed to. I have no idea where it goes.
Can someone please tell me how to get both scripts to work at the same time? And, why the row of data disappears when the department is set to Billing but doesn't go to the Billing tab?
Dependent Dropdown Menu Script
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Database");
var wsOptions = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("options");
var options = wsOptions.getRange(2,1,wsOptions.getLastRow()-1,2).getValues();
function myFunction() {
var list = ["a","b","c","f"];
var cell = ws.getRange("J2");
applyValidationToCell(list,cell);
}
function onEdit(event){
var activeCell = event.range;
var value = activeCell.getValue();
var row = activeCell.getRow();
var column = activeCell.getColumn();
var wsName = activeCell.getSheet().getName();
if(wsName == "Database" && column === 5 && row > 1){
if(value === ""){
ws.getRange(row,10).clearContent();
ws.getRange(row,10).clearValidations();
} else{
ws.getRange(row,10).clearContent();
var filteredOptions = options.filter(function(options){ return options[0] === value });
var listToApply = filteredOptions.map(function(options){ return options[1] });
Logger.log(listToApply);
var cell = ws.getRange(row,10);
applyValidationToCell(listToApply,cell);
}
}
}
function applyValidationToCell(list,cell){
var rule = SpreadsheetApp
.newDataValidation()
.requireValueInList(list)
.setAllowInvalid(false)
.build();
cell.setDataValidation(rule);
}
Billing Script
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Database");
var wsOptions = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("options");
var options = wsOptions.getRange(2, 1,wsOptions.getLastRow()-1,2).getValues();
function myFunction() {
var list = ["a","b","g"];
var cell = ws.getRange("K2");
applyValidationToCell(list,cell);
}
function onEdit(e){
var activeCell = e.range;
var val = activeCell.getValue();
var r = activeCell.getRow();
var c = activeCell.getColumn();
var wsName = activeCell.getSheet().getName();
if(wsName == "Database" && c === 5 && r > 1){
var filteredOptions = options.filter(function(o){return o[0] === val});
var listToApply = filteredOptions.map(function(o){ return o[1]});
console.log(listToApply);
var cell = ws.getRange(r, 10);
applyValidationToCell(listToApply,cell);
}
}
function applyValidationToCell(list,cell){
var rule = SpreadsheetApp
.newDataValidation()
.requireValueInList(list)
.setAllowInvalid(false)
.build();
cell.setDataValidation(rule);
}
function onEdit(e) {
const src = e.source.getActiveSheet();
const r = e.range;
if (r.columnStart != 5|| r.rowStart == 2 || e.value == src.getName()) return;
const dest = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(e.value);
src.getRange(r.rowStart,1,1,30).moveTo(dest.getRange(dest.getLastRow()+1,1,1,30));
src.deleteRow(r.rowStart);
}
All functions have to have a unique name and all functions have access to all sheets. onedit triggers are generated on every user edit and it's up to the onEdit function to route edits from the appropriate sheets using information gathered from the event object. Note: Sheet Name = e.range.getSheet().getName() where e is populated by the event object in the function declaration function onEdit(e) {}
The if statement below limits access to edits that occur on Sheet name "Database" and e.range.columnStart == 5 and rows greater than one
if(wsName == "Database" && column === 5 && row > 1){
if(value === ""){
ws.getRange(row,10).clearContent();
ws.getRange(row,10).clearValidations();
} else{
ws.getRange(row,10).clearContent();
var filteredOptions = options.filter(function(options){ return options[0] === value });
var listToApply = filteredOptions.map(function(options){ return options[1] });
Logger.log(listToApply);
var cell = ws.getRange(row,10);
applyValidationToCell(listToApply,cell);
}
}
if you require actions from other sheets then you must add more if statements or other conditional logic to route the appropriate edit traffic to the the correct processing statements.

Need a custom function to repeat for each row shown and minimize API calls

EDIT: I need help combining functions into 1 and adding a trigger.
In my spreadsheet I have rows 4-100 for customer service calls that are filtered from a "ServiceData" worksheet by either choosing a "Service Month" or "Service Day" (ie. "7/11" shows only 5 rows where "July" would show 65 rows) . Each row item has corresponding Place IDs for origin/destination in column K and L with an order # (as in 1st, 2nd, 3rd... service call of the day) in column J .
TravelTime spreadsheet
I'm using the following custom function travelTime() in cells M4:M100 to calculate driving duration and distance between 2 place IDs:
function travelTime(origin,destination) {
var API_KEY = PropertiesService.getScriptProperties().getProperty('keyMaps');
var baseUrl = "https://maps.googleapis.com/maps/api/distancematrix/json? units=imperial&origins=";
var queryUrl = baseUrl + "place_id:" + origin + "&destinations=" +
"place_id:" + destination + "&mode=driving" + "&key=" + API_KEY;
var response = UrlFetchApp.fetch(queryUrl);
var json = response.getContentText();
var time = JSON.parse(json);
return [[ time.rows[0].elements[0].duration.text,
time.rows[0].elements[0].distance.text ]] ;
A major issue is that many unnecessary service calls are being made to the API when I'm making edits to the "ServiceData" spreadsheet (ie. service date changes when a particular day is over-scheduled) and not needing the travel time updated until I'm done working through a schedule .
After researching quite a bit there seems to be several options I could be using; caching, looping, arrays, and putting everything into a script then attach to a button to only run when ready. Considering I'm a newbie, putting all these options together are definitely beyond my skill level and could really use some help.
EDIT with new functions:
So after more researching I have been able to put together the following functions that when each are run independently work great. Now the problem I'm having is putting these all together in particular adjusting the original travelTime()into newTravelTime(). I have made an attempt towards the right direction below but can't figure out how to get the API call in there .
function newTravelTime() {//<--**having issues how to write this function
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("MonthlySA");
var sourceR = sourceSheet.getRange(4, 11, sourceSheet.getLastRow()-3, 4);
var sourceV = sourceR.getValues();
var array = [];
for (var i = 0; i < sourceV.length; i++) {
if (sourceV[i][2] == "") {
var origin = sourceV[i][0];//ori place IDs for API query
var destination = sourceV[i][1];//des place IDs API api query
}
array.push([sourceV[i][2]]);
}
sourceSheet.getRange(4, 13, array.length, 1).setValues(array);
I'd like to create a final getTravelTime() with all the functions and add an OnEdit trigger when either "Service Month" or "Service Day" changes in cells B1 or B2 to run them. If there is any advice with my functions themselves I would really appreciate some help, I am very new with this and trying.
///checks if origin/destination are already in the cacheSheet then return travel time to sourceSheet
function getCachedTravelTime() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("MonthlySA");
var sourceR = sourceSheet.getRange(4, 11, sourceSheet.getLastRow()-3, 4);
var sourceV = sourceR.getValues();
var cacheSheet = ss.getSheetByName("TravelTimeCache");
var cacheR = cacheSheet.getRange(2, 1, cacheSheet.getLastRow()-1, 4);
var cacheV = cacheR.getValues();
var array = [];
for (var i = 0; i < sourceV.length; i++) {
for (var j = 0; j < cacheV.length; j++) {
//if origin/destination columns from sourceSheet match columns on cacheSheet
if (sourceV[i][0]+sourceV[i][1] == cacheV[j][0]+cacheV[j][1]) {
sourceV[i][2] = cacheV[j][2]; //column with travel duration
sourceV[i][3] = cacheV[j][3]; //column with travel distance
}
}
array.push([sourceV[i][2], sourceV[i][3]]);
}
sourceSheet.getRange(4, 13, array.length, 2).setValues(array);
}
///if origin or destination are blank, label as 'missing value'
function missingOD() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("MonthlySA");
var sourceID = sourceSheet.getRange(4, 3, sourceSheet.getLastRow()-3, 12);
var sourceV = sourceID.getValues();
var array = [];
for (var i = 0; i < sourceV.length; i++) {
// if ID has a value
if (sourceV[i][0] != "") {
// if origin or destination is blank
if (sourceV[i][8] == "" || sourceV[i][9] == "") {
sourceV[i][10] = 'missing value';
}
}
array.push([sourceV[i][10]]);
}
sourceSheet.getRange(4, 13, array.length, 1).setValues(array);
}
///if cache not found - get the new travelTime for that origin/destination on sourceSheet...
function newTravelTime() {//<--
}
///...and store the new travelTime() in cacheSheet
function storeTravelTime() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("MonthlySA");
var sourceR = sourceSheet.getRange(4, 11, sourceSheet.getLastRow()-3, 4);
var sourceV = sourceR.getValues();
var cacheSheet = ss.getSheetByName("TravelTimeCache");
var cacheR = cacheSheet.getRange(2, 1, cacheSheet.getLastRow()-1, 4);
var cacheV = cacheR.getValues();
var array = [];
for (var i = 0; i < sourceV.length; i++) {
var duplicate = false;
for (var j = 0; j < cacheV.length; j++) {
if (sourceV[i][0]+sourceV[i][1] == cacheV[j][0]+cacheV[j][1]) {
duplicate = true;
}
}
if(!duplicate){ //if origin/destination columns from sourceSheet are NOT matched on cacheSheet
array.push([sourceV[i][0], sourceV[i][1], sourceV[i][2], sourceV[i][3]]);//columns with new data
}
}
//add new data to last row of cacheSheet
cacheSheet.getRange(cacheSheet.getLastRow()+1, 1, array.length, 4).setValues(array);
}
One of the easiest solution thats coming to mind is Caching. Instead of making API calls everytime check if we have already made that call previously.
Something like this
function getTravelTime(origin, destination) {
var travelTime = getTravelTimeFromPreviousCall(origin, destination);
if (travelTime != null) {
return travelTime;
} else {
var travelTime = fetchTravelTime(origin, destination);
storeTravelTime(origin, destination, travelTime);
return travelTime;
}
}
function fetchTravelTime(origin, destination) {
var API_KEY = PropertiesService.getScriptProperties().getProperty('keyMaps');
var baseUrl = "https://maps.googleapis.com/maps/api/distancematrix/json? units=imperial&origins=";
var queryUrl = baseUrl + "place_id:" + origin + "&destinations=" + "place_id:" + destination + "&mode=driving" + "&key=" + API_KEY;
var response = UrlFetchApp.fetch(queryUrl);
var json = response.getContentText();
var time = JSON.parse(json);
return time.rows[0].elements[0].duration.text;
}
For this we can define our cache something like :
A sheet with column -
origin
destination
travel time
And we need to define following functions:
getTravelTimeFromPreviousCall(origin, destination) : In this we need to check cache and return travel time for that origin & destination, if not found then return null
storeTravelTime(origin, destination, time) : This will only store travel time for future use in cache sheet
You can try something like this :
function getTravelTimeFromPreviousCall(origin, destination) {
var cacheSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(CACHE_SHEET_NAME);
var cacheData = sheet.getDataRange().getValues();
for (var i=0; i<cacheData.length; i++) {
if (cacheData[i][ORIGIN_COLUMN_INDEX]==origin && cacheData[i][DESTINATION_COLUMN_INDEX]==destination) {
return cacheData[i][TRAVEL_TIME_COLUMN_INDEX];
}
}
return null;
}
function storeTravelTime(origin, destination, travelTime) {
var cacheSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(CACHE_SHEET_NAME);
sheet.appendRow([origin, destination, travelTime]);
}
Please fix loop variable, array indexes & constants, as per your sheet.

Build a list if condition is met in one of several ranges on other tabs

I'm trying to write a script that builds a list for each subject in a row based on whether or not that subject appears within a specific year range within each tab.
Here is a sample version of my data for reference: https://docs.google.com/spreadsheets/d/1fRhsIAeQd2qDIWILhvhnoTr1Po009RljdjrL9TYwncI/edit#gid=0.
I manually filled in some of the rows column B on the "History" tab, but this is what I want the script to produce (the cells highlighted red).
My best idea so far is to create an object for all of the names and use a for loop to go through each name... then to create separate objects for each role and year, and loop through those with a conditional check to search for the name. If the name appears in that year range, perhaps I can push some text to a new object?
I started writing a script for this but quickly realized it's going to be really painful and inefficient. Any ideas for how I can do this more efficiently? My data goes back several years for hundreds of people so manual updating just isn't an option anymore!
Thanks!!
function buildHistory() {
var ss = SpreadsheetApp.getActive();
var historySheet = ss.getSheetByName("History");
var names = historySheet.getRange(2, 1, historySheet.getLastRow(), 1).getValues();
var pastMems = ss.getSheetByName("Past Members");
var mems2015 = pastMems.getRange(3, 2, pastMems.getLastRow(), 1).getValues();
var mems2014 = pastMems.getRange(3, 5, pastMems.getLastRow(), 1).getValues();
for (var i = 0; i < names.length; i++) {
var name = names[i];
Logger.log(name);
for (var j = 0; j < mems2015.length; j++) {
if (mems2015[j] == name) {
Logger.log("Mem in 2015");
} else if (mems2014[j] == name) {
Logger.log("Mem in 2014");
}
}
}
}
This is the way I would structure the code. This example is incomplete. I did not try to run it. It's meant to show a basic strategy of how the program would flow, and how the data would be processed. Again, the code is incomplete. The code does have a way to dynamically determine the start column for each year in a sheet. It shows how to associate a name, with that person's history.
var masterObject = {};
var ss = SpreadsheetApp.getActiveSpreadsheet();
function masterFunction() {
initialPopulationOfMasterObject();
processPastVolunteersSheet();
processPastSponsersSheet();
addDataToHistory();
};
function initialPopulationOfMasterObject() {
var historySheet = ss.getSheetByName("History");
var names = historySheet.getRange(2, 1, historySheet.getLastRow(), 1).getValues();
var i=0;
for (i=0;i<names.length;i+=1) {
masterObject.thisPerson = names[i];
masterObject.theirHistory = []; //array for list of this person's history
};
};
function processPastVolunteersSheet() {
var volunteersSheet = ss.getSheetByName("Past Volunteers");
var numberOfYears = 3;
var yrs = 0, thisYrsData=[], startColumn;
var j=0, thisName="", thisYr="";
for (yrs=0;yrs<numberOfYears;yrs+=1) {
if (yrs=0) {
startColumn = 1;
thisYr = volunteersSheet.getRange(1, startColumn, 1,1).getValue();
} else {
startColumn = yrs*3;
thisYr = volunteersSheet.getRange(1, startColumn, 1,1).getValue();
};
thisYrsData=volunteersSheet.getRange(2, startColumn, volunteersSheet.getLastRow(),2).getValues();
for (j=0;j<thisYrsData;j+=1) {
masterObject[thisName].push("Volunteer (" + thisYr);
};
};
};

How to loop range of cells and set value in adjacent column

I'm learning Google Apps Scripts for use with Google Spreadsheets.
I have a list of URLs in one column and I want to write a script to get the title element from each URL and write it in the adjacent cell. I have accomplished this for one specific cell as per the following script:
function getTitles() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("url_list");
var range = sheet.getRange("G3");
var url = range.getValue();
var response = UrlFetchApp.fetch(url);
var doc = Xml.parse(response.getContentText(),true);
var title = doc.html.head.title.getText();
var output = sheet.getRange("H3").setValue(title);
Logger.log(title);
return title;
}
This gets the URL in G3, parses it, pulls the element and writes the output in H3.
Now that I have this basic building block I want to loop the entire G column and write the output to the adjacent cell but I'm stuck. Can anyone point me in the right direction?
May look something like this:
function getTitles() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("url_list");
var urls = sheet.getRange("G3:G").getValues();
var titleList = [], newValues = [],
response, doc, title;
for (var row = 0, var len = urls.length; row < len; row++) {
if (urls[row] != '') {
response = UrlFetchApp.fetch(urls[row]);
doc = Xml.parse(response.getContentText(),true);
title = doc.html.head.title.getText();
newValues.push([title]);
titleList.push(title);
Logger.log(title);
} else newValues.push([]);
}
Logger.log('newValues ' + newValues);
Logger.log('titleList ' + titleList);
// SET NEW COLUMN VALUES ALL AT ONCE!
sheet.getRange("H3").offset(0, 0, newValues.length).setValues(newValues);
return titleList;
}

Resources