Split Array Function on Email PlainBody() MSG using Google Script - google-apps-script

I use the following code below to grab emails, get the plainbody(), date and messageto bring them into Google sheet in COLUMN A.
Then in the spreadsheet I copy down the formula in COLUMN C
=ARRAYFORMULA(IFERROR(SPLIT(A3,"|")))
(Which I tested this works as well - not sure if one is preferred)
=SPLIT($A2,"|")
I'd like to combine these functions and have attempted the following
function getEmails() {
var ss = SpreadsheetApp.getActiveSheet();
var threads = GmailApp.search("is:starred in:TanWebOppCRM");
for (var i=0; i<threads.length; i++) {
var messages = threads[i].getMessages();
for (var j=0; j<messages.length; j++) {
var msg = messages[j].getPlainBody();
var dat = messages[j].getDate();
var msgParse = '=SPLIT($A2,"|")';
/*var msgParse = '=ARRAYFORMULA(IFERROR(SPLIT($A2,"|")))';*/
ss.appendRow([msg, dat, msgParse]);
}
threads[i].moveToTrash();
}
}
This DOES "WORK". For example - I know I just sent myself a test email that can be extracted. I know the next row that WILL be appended is Row 30 - and change the code
var msgParse = '=SPLIT($A2,"|")';
to
var msgParse = '=SPLIT($A30,"|")';
Then the extraction works, and you see the split happen.
If I was to import 2 new rows - obviously
$A30 /* stays A30 - so I tried-
var msgParse = '=SPLIT($A[j],"|")';
Just to see if the [j] dynamically changed - even though I know j is for message loop
The question...
how do I get the following line to increment to the append row value
so that each loop changes the row reference to the appended row - so if I was starting from row 30
var msgParse = '=SPLIT($A30,"|")';
var msgParse = '=SPLIT($A31,"|")';
var msgParse = '=SPLIT($A32,"|")';
Is what the incremental msgParse value data would be.

You can construct the formula via string concatenation.
If you always just append you can just get the last row and add 1 (because the new row will be one row below) and use that as the row reference:
var msgParse = '=SPLIT($A' + (ss.getLastRow() + 1) + ', "|")';

Since I'm new to Script - and I couldn't find this answer - I wanted to add my own answer.
#Robin definitely pointed me in the right direction - I didn't realize I can use string concatenation - however the solution provided did not increment and once that string concatenation was written - it remain for the rest of the FOR loop
The final piece was adding a counter - I could probably eliminate the first row = variable and there is probably a way to make this more efficient But it took in 20 sample emails from gmail and parsed them into spreadsheet in maybe 6 or 7 seconds.
function getEmails() {
var ss = SpreadsheetApp.getActiveSheet();
var threads = GmailApp.search("is:starred in:TannerWebOppCRM");
var row = ss.getLastRow();
for (var i=0; i<threads.length; i++) {
var messages = threads[i].getMessages();
var newrow = row + 1;
for (var j=0; j<messages.length; j++) {
var msg = messages[j].getPlainBody();
var dat = messages[j].getDate();
var msgParse ='=SPLIT($A'+ (newrow) + ', "|")';
ss.appendRow([msg, dat, msgParse]);
newrow = newrow + 1;
}
threads[i].moveToTrash();
}
}
To complete this post I wanted to include the update and addition to the answer I accepted because that technically answered my question - just didn't put it into context how to make it work in my script.

Related

More efficient way to format cells based on a given reference

I am creating a google sheet which contains fixtures and I would like to color all the cells featuring the fixtures which show how relatively easy/difficult the match is for a given team. This is my spreadsheet. The following is the code that I am currently using -
// Function to find the row number of a given fixture
function findRow(empName){
var ss=SpreadsheetApp.getActive();
//var fix = ss.getSheetByName('Fixtures');
var fdr = ss.getSheetByName('FDR');
var data = fdr.getDataRange().getValues();
for(var i = 0; i<data.length;i++){
if(data[i][1] == empName){ //[1] because column E
Logger.log((i+1))
return i+1;
}
}
}
// Function to color the given cell based on its fixture difficulty
function colorMeUp() {
var ss = SpreadsheetApp.getActive();
var fix = ss.getSheetByName('Fixtures');
var fdr = ss.getSheetByName('FDR');
for(var i = 4; i <= 23; i = i + 1){
for(var j = 3; j <= 40; j = j + 1){
var temp = fix.getRange(i,j).getValue(); // Find the fixture to color
var master = fix.getRange(i,2).getValue(); // Find the reference team
var rowNumMaster = findRow(master);
var rowNumTemp = findRow(temp);
if(rowNumTemp < 23){
rowNumMaster = rowNumMaster + 20;
}
var tempRating = fdr.getRange(rowNumTemp,4).getValue();
var masterRating = fdr.getRange(rowNumMaster,4).getValue();
var fixDiff = masterRating + (6-tempRating); // Calculate relative fixture difficulty
var rang = fdr.getRange(1+fixDiff,8).getBackground();
fix.getRange(i,j).setBackground(rang);
}
}
}
As you can see, this code uses the table given in the "FDR" sheet to assign a colour to each fixture given in the "Fixture" sheet. However, when I press the ugly blue button on the left side the process of it happening is painfully slow and it exceeded the maximum time. Is there any way to make this process go faster by tweaking this code/ using a different approach? I am very new to all this so don't really know what I can do to make this better/faster.
Try integrating if(data[i][1] == empName){ column two of data into a flatten array and then you can use indexOf instead of call a function twice in the center of a loop. So inotherword trash findRow and build it inside of colorMeUp
So something like this:
function colorMeUp() {
var ss = SpreadsheetApp.getActive();
var fix = ss.getSheetByName('Fixtures');
var fdr = ss.getSheetByName('FDR');
const sA = fdr.getRange(1,2,fdr.getLastRow()).getDisplayValues().flat();
for(var i = 4; i <= 23; i = i + 1){
for(var j = 3; j <= 40; j = j + 1){
var temp = fix.getRange(i,j).getValue(); // Find the fixture to color
var master = fix.getRange(i,2).getValue(); // Find the reference team
var rowNumMaster = sA.indexOf(master) + 1
var rowNumTemp = sA.indexOf(temp) + 1
if(rowNumTemp < 23){
rowNumMaster = rowNumMaster + 20;
}
var tempRating = fdr.getRange(rowNumTemp,4).getValue();
var masterRating = fdr.getRange(rowNumMaster,4).getValue();
var fixDiff = masterRating + (6-tempRating); // Calculate relative fixture difficulty
var rang = fdr.getRange(1+fixDiff,8).getBackground();
fix.getRange(i,j).setBackground(rang);
}
}
}
I threw this together rather quickly so it may not actually work but it should speed things up considerably using that approach. If you've done a few web apps then my guess is that you're a reasonable coder and should be able to integrate this into you code. Or it's possible that I'm totally FOS.
It would be really good to get rid of the getValue() in the loop in lieu of getValues() outside of the loop and use array indices to access the same data . Same with getbackground. I'd need to see your data to figure that out.
BTW I don't follow links to spreadsheets so giving me access to data involves post tables.'

Filter gmails using timestamp

Am new dabbling with Google Apps Script; would like to ask if I'm in the right direction, and how to I manipulate time within the script.
I'm struggling in trying to maniuplate time values in Google App Script, basically I am able to pull the timestamp of each email sent, but I only want to paste into the spreadsheet email information that were recent, e.g. within 30minutes from script run time. This is to avoid pulling duplicate information.
Not sure if there is a currentTime() function here, or I have to create a new Date() object and do some calculations from there. Tried a few variations and nothing seemed to work proper.
Would appreciate any help in getting towards the right direction in doing this thank you!
function getDetails(){
var DEST_URL = "SHEET_URL"; //redacted for sensitivity
var DEST_SHEETNAME = "Test";
var destss = SpreadsheetApp.openByUrl(DEST_URL);
var destSheet = destss.getSheetByName(DEST_SHEETNAME);
var threads = GmailApp.search("FILTERS"); //filter settings redacted for sensitivity
for(var i = 0; i < threads.length; i++){
var messages=threads[i].getMessages();
for(var j =0; j < 1; j++){ //only take first message in thread
var message = messages[j];
var subject = message.getSubject() ;
var sentTimeStamp = message.getDate();
if(sentTimeStamp is within last 30minutes as of script run time){ //this is where i need help
var delimitString = subject.split("is sent");
var detailName = delimitString[0];
var lastRow = destSheet.getLastRow();
destSheet.getRange(lastRow + 1,1).setValue(detailName);
destSheet.getRange(lastRow + 1,2),setValue(sentTimeStamp);
}
}
}
}
You can convert timeStamp into ms seconds and then compare to the value of "30 s ago"
Sample:
var sentTimeStamp = message.getDate();
var now = new Date();
var ThirtyMinutesAgo = now-30*60*1000;
if(sentTimeStamp.getTime() < ThirtyMinutesAgo){
...
}
References:
newDate()
getTime()
Another idea would be to query for emails that you received the last 30 minutes.
Explanation:
You can get the emails that you received the last 30 minutes ago as a query in the GmailApp.search function. See this link to see what filters you can use.
This will get the last emails with keyword "FILTERS" that you received the last 30 minutes.
var ThirtyMinutesAgo = new Date();
ThirtyMinutesAgo.setMinutes(ThirtyMinutesAgo.getMinutes() - 30);
const queryString = `"FILTERS" newer:${Math.round(ThirtyMinutesAgo.getTime()/1000)}`
const threads = GmailApp.search(queryString); // threads the last 30 minutes
This approach is more efficient for two reasons:
You have less data (threads) to iterate over with the for loop.
You don't need to apply and if statement on every thread.
Solution:
function getDetails(){
var DEST_URL = "SHEET_URL"; //redacted for sensitivity
var DEST_SHEETNAME = "Test";
var destss = SpreadsheetApp.openByUrl(DEST_URL);
var destSheet = destss.getSheetByName(DEST_SHEETNAME);
// var threads = GmailApp.search("FILTERS"); //filter settings redacted for sensitivity
// new code
var ThirtyMinutesAgo = new Date();
ThirtyMinutesAgo.setMinutes(ThirtyMinutesAgo.getMinutes() - 30);
const queryString = `"FILTERS" newer:${Math.round(ThirtyMinutesAgo.getTime()/1000)}`
const threads = GmailApp.search(queryString); // threads the last 30 minutes
//
for(var i = 0; i < threads.length; i++){
var messages=threads[i].getMessages();
for(var j =0; j < 1; j++){ //only take first message in thread
var message = messages[j];
var subject = message.getSubject() ;
var sentTimeStamp = message.getDate();
var delimitString = subject.split("is sent");
var detailName = delimitString[0];
var lastRow = destSheet.getLastRow();
destSheet.getRange(lastRow + 1,1).setValue(detailName);
destSheet.getRange(lastRow + 1,2),setValue(sentTimeStamp);
}
}
}
}

How to store data in Array using For loop in Google apps script - pass array by value

Edit: answer at the bottom
Im experiencing some weird behavior I came across when using google script.
I have a 2d array and I insert data into it using for loop.
I noticed that if use
appendRow(someArray[i])
INSIDE the for loop, everything works as expected.
But If try to access data or use appendRow outside of the for loop, it always gives me the data of the last row the ran in the for loop.
so:
appendRow(someArray[1])
gives same result as
appendRow(someArray[2]),appendRow(someArray[3])
when used OUTSIDE of the for loop.
Can anyone tell me whats causes it?
It also happens when im using setValue on a 2d array, I can use it outside of the loop, or all of the rows are identical.
I have spent 2 hours on this simple little thing, and finally understand what causing the problem but I still cant figure out how to fix it.
Im attaching a simple code that explains what the problem, please focus on the second FOR loop.
function myFunctionAppendRowInside() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getDataRange().getValues();
var newRow = data[5];
var arr = new Array(100,100);
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
arr[i] = newRow
}
for (var i = 0; i < 99; i++){
target.getRange(targetRow,1).setValue(evento[i]);
arr[i][0] = currentId + i;
arr[i][2] = productSku + i;
sheet.appendRow(arr[i]);
}
//All of them gives the same row, which is the one created in the last run of the FOR loop arr[98]
sheet.appendRow(arr[1]);
sheet.appendRow(arr[2]);
sheet.appendRow(arr[3]);
}
Please explain to me whats causes it and how to overcome it.
Edit : added my code which uses "setValues" , but still experiencing the same problem. My array is populated only with the LAST row created by the "for loop"
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet");
var data = sheet.getDataRange().getValues();
//Taking the 5th row, using it as a template for rest of rows
var newRow = data[5];
//2d Array
var arr = new Array(100,100);
//data for the only 2 columns who are going to change between rows
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
newRow[0] = currentId + i;
newRow[2] = productSku + i;
arr[i] = newRow;
}
newSheet.getRange(2, 1, arr.length, arr[0].length).setValues(arr);
}
Second Edit:
So the issue was that "getValues()" returns an array, which I needed to work with.
But Array are passed by reference and not by value, so any changes I made along the code, where changing the original array which I got from "getValues".
The solution:
iterate over the array received from "getValues", and copy the values one by one(cell by cell) to the a new array and only then manipulate it.
I also created a 2d array, which also requires running a "for loop"
Im attaching the working code which does:
1.copy row 13, which includes 51 columns from my original sheet.
2.create an empty 2d array (9999x51).
3. take row 13 and manipulate its columns based of current iteration (ie row '1' will include original data + '1'
The code :
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var data = sheet.getDataRange().getValues();
//create Array
var arr = [];
//turn it to a 2 dimension array
for (var i=0;i<10000;i++) {
arr[i] = [];
}
//starting point for our new id which will run from 30000-39999
var currentId = 30000;
//run on our 2 dimension array and manipulate data cell by cell
for (var i=0; i <= 9999; i++){
for (var j=0; j<= data[13].length - 1; j++){
if (j == 0){
var obj = currentId + i;
arr[i][j] = obj;
}else{
if (j == 2){
arr[i][j] = data[13][j] + i;
}else{
arr[i][j] = data[13][j];
}
}
}
}
//copy to new sheet
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet466");
newSheet.getRange(1, 1,10000, 51).setValues(arr);
}
I'm not sure what causes your problem. But instead of appending each row (which can get very slow) it is faster to get a range and set its value. For example:
sheet.getRange(1, 1, arr.length, arr[0].length).setValues(arr);
Where the two 1's are the starting position that you wish to use. For more info check out the documentation on getRange.

Protecting Cells Based on Contents of Other Cells in Google Sheets

I have a single worksheet that contains user entered responses in Columns C & D, Rows 3 - 20. The responses are time dependent and look at Column E Rows 3-20 to see if it is "Locked" or "Open".
Using protection, I lock the entire sheet for editing with the exception of C3:D20. The sheet is set to calculate every minute.
I am trying to write a script that checks the column E to see if it is set for locked or open. If it is set for locked, I would like to lock (protect) columns C&D in that row for editing from everyone but myself. I run the script every 5 minutes and I have the for loop and if statement handled, but when I go to use the RemoveEditors function it does 2 things:
Creates a new protected range (so after 5 minutes I have 1 additional protected range, 10 minutes, I have 2 additional, etc.)
Does not remove the other editors from those able to edit the cells.
I tried using Google's example code, but their code adds the current user as an editor, which is what I'm trying to avoid doing since then that editor can just remove the protection that the code is putting in place.
Any help you could provide would be appreciated.
Current Code is below:
function Lock_Cells() {
var sheet = SpreadsheetApp.getActive();
for (var i = 3; i <= 20; i++)
{
var Check_Cell = "E" + i;
var Temp = sheet.getRange(Check_Cell).getValue();
if (Temp == "Locked")
{
var Lock_Range = "C" + (i + 2) + ":D" + "i";
var protection = sheet.getRange(Lock_Range).protect();
var description = "Row " + i;
protection.setDescription(description);
var eds = protection.getEditors();
protection.removeEditors(eds);
}
}
}
To avoid creating a new set of protected ranges, you can add logic to check which rows are already locked. With that information you just need to skip those rows:
note: there was a mistake in this line: var Lock_Range = "C" + (i + 2) + ":D" + "i"; the variable i should not have quotation.
function Lock_Cells() {
var sheet = SpreadsheetApp.getActive();
var rows = get_protected_Rows();
for (var i =3; i <= 20; i++)
{
var Check_Cell = "E" + i;
var cell = sheet.getRange(Check_Cell);
var Temp = sheet.getRange(Check_Cell).getValue();
if (Temp == "Locked" && rows.indexOf(i) <0)
{
var Lock_Range = "C" + i + ":D" + i; //In this line you put "i"
.....
...
}
function get_protected_Rows()
{
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
var rows = [];
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
var anotation = protection.getRange().getRow();
rows.push(anotation);
}
return rows
}
You are right, when the code is executed by one of the users, the protection gives that user the ability to edit those rows. I would recommend that as the owner of the file, you also run a task to remove every other editor from those rows. The function would be very similar to the previous. And I know is not the best but it may help you with your use case.
function remove_editors()
{
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
var anotation = protection.getRange().getA1Notation();
var eds = protection.getEditors();
protection.removeEditors(eds);
}
}
By doing that i was able to restrict the permission to other users. Hope it helps.

Handling time duration in Google Sheets Api

I'm making a basic script that fetches time durations from external sheets, and sums them. What I have so far is:
function getHorasCasoUso() {
var libros = {
"key1" : "externalSheetURL1",
"key2" : "externalSheetURL2",
...
};
var horas_por_caso_uso = {};
for (var key in libros) {
var libro = SpreadsheetApp.openByUrl(libros[key]);
var sheets = libro.getSheets();
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
for (var j = 5; j < numRows; j++) {
var row = values[j];
var caso_uso = row[6];
var horas = row[4]; //The cell format is 'Duration'
if (!caso_uso)
continue;
if (!!horas_por_caso_uso[caso_uso])
horas_por_caso_uso[caso_uso] += horas;
else
horas_por_caso_uso[caso_uso] = horas;
}
}
}
var ss = SpreadsheetApp.getActiveSheet();
for (var key in horas_por_caso_uso) {
ss.appendRow([key, horas_por_caso_uso[key]]);
}
}
The problem is that the data stored in 'horas' is a string. I want to get the time duration in that cell. How can I do that?
Your issue seems quite similar to the one in this post but at a larger scale...
You should convert row[4] value to minutes (or seconds if you need this accuracy) before adding that value to the total counter.
If the cells are formatted as duration (as you say it is) it should work without changes.(see code at the end of this post)
If not, ie if these values are returned as strings then a simple string manipulation will do the job for you like this :
example : testString = 12:34
function toMinutes(value){
var minutes = Number(value.split(':')[0].replace(' ',''))*60+Number(value.split(':')[1].replace(' ',''));
return minutes;
}
(code working as a function) will return 754 (60*12+34)
usage in your code : var horas = toMinutes(row[4]);
function toMinutes(value){
var duration = new Date(value);
var min = duration.getMinutes()+60*duration.getHours();
return min;
}
You can eventually improve this function with a few error trapping features to handle cases where cell is empty of malformed... Since I don't know what the data look like I prefer let you do it yourself.