How can I group exact string together? - google-apps-script

I'm working with a google sheet that contains a column of unique project numbers followed by a column with the equipment description used in that project. I have set up my code to send an automatic email with this information to a list of recipients. What I want to do that I don't know how to is what code can I use that will read the project number column and when it finds two project numbers exactly the same, it will group the equipment together into a list that will be sent in that automatic email. I know the list can be a variable but I just don't know how to make it look for spa project numbers to join the equipment together without changing anything in the google sheet. Thank y'all!

I'm agree with Cooper. To build an object is a most obvious way to get unique values (ID, etc) from any list:
var data = [
[1,'a'],
[2,'b'],
[3,'c'],
[1,'d']
]
var obj = {};
data.forEach(x => {
try { obj[x[0]].push(x[1]) } // try to add the value to an existed array
catch(e) { obj[x[0]] = [x[1]] } // or make an array if there is no existed one
});
console.log(obj); // out: [ {1:[a,d]}, {2:[b]}, {3:[c]} ]
var id = 1
console.log('All the values from ID '+ id + ': ' + obj[id].join(', ')); // out: a, d

Related

Using for and if loops in Google Apps Script

Dear programming Community,
at first I need to state, that I am not quite experienced in VBA and programming in general.
What is my problem? I have created a topic list in google sheets in order to collect topics for our monthly meeting among members in a little dance club. That list has a few columns (A: date of creation of topic; B: topic; C: Name of creator; ...). Since it is hard to force all the people to use the same format for the date (column A; some use the year, others not, ...), I decided to lock the entire column A (read-only) and put a formular there in all cells that looks in the adjacent cell in column B and sets the current date, if someone types in a new topic (=if(B2="";"";Now()). Here the problem is, that google sheets (and excel) does then always update the date, when you open the file a few days later again. I tried to overcome this problem by using a circular reference, but that doesn't work either. So now I am thinking of creating a little function (macro) that gets triggered when the file is closed.
Every cell in Column B (Topic) in the range from row 2 to 1000 (row 1 is headline) shall be checked if someone created a new topic (whether or not its empty). If it is not empty, the Date in the adjacent cell (Column A) shall be copied and reinserted just as the value (to get rid of the formular in that cell). Since it also can happen, that someone has created a topic, but a few days later decides to delete it again, in that case the formular for the date shall be inserted again. I thought to solve this with an If-Then-Else loop (If B is not empty, then copy/paste A, else insert formula in A) in a For loop (checking rows 1 - 1000). This is what I have so far, but unfortunately does not work. Could someone help me out here?
Thanks in advance and best regards,
Harry
function NeuerTest () {
var ss=SpreadsheetApp.getActive();
var s=ss.getSheetByName('Themenspeicher');
var thema = s.getCell(i,2);
var datum = s.getCell(i,1);
for (i=2;i<=100;i++) {
if(thema.isBlank){
}
else {
datum.copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}}
}
The suggested approach is to limit the calls to the Spreadsheet API, therefore instead of getting every cell, get all the data at once.
// this gets all the data in the Sheet
const allRows = s.getDataRange().getValues()
// here we will store what is written back into the sheet
const output = []
// now go through each row
allRows.forEach( (row, ind) => {
const currentRowNumber = ind+1
// check if column b is empty
if( !row[1] || row[1]= "" ){
// it is, therefore add a row with a formula
output.push( ["=YOUR_FORMULA_HERE"] )
} else {
// keep the existing value
output.push( [row[0]] )
}
})
Basically it could be something like this:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Themenspeicher');
var range = sheet.getRange('A2:B1000');
var data = range.getValues(); // <---- or: range.getDisplayValues();
for (let row in data) {
var formula = '=if(B' + (+row+2) + '="";"";Now())';
if (data[row][1] == '') data[row][0] = formula;
}
range.setValues(data);
}
But actual answer depends on what exactly you have, how your formula looks like, etc. It would be better if you show a sample of your sheet (a couple of screenshots would be enough) 'before the script' and 'after the script'.

How to extract and display words from a long list of words from a text string in google sheet

I have a google sheet which contains 2 columns in Sheet 1
Column one contains text strings. I want to extract "job titles" from those text strings in the next column. List of all jobs title is in another sheet name "Data Lookup"
If any exact match job title (lower case or upper case or in any case) is present in "Data Lookup" sheet and in A1:A1795 range then show in sheet 1 and if exact match is not found then find any partial match job title.
For this purpose I made the tried following custom code.
function findtitle(text, list){
  var result = 'Not Found';
  list = list.flat();
  list.forEach( str => {    
if(text.includes(str)){      
result = str;      
return result;    
}  
});  
return result;
}
It is finding only partial match words and sometimes it is not finding any word. while job title is present in Data lookup sheet.
i have attached a sample google sheet also
https://docs.google.com/spreadsheets/d/17AZ2Cuk4gcAiXm_yo3_IK-WeYqO5g7n5HBumCjNknFU/edit?usp=sharing
for example
in sheet 1 cell number A43 contains a text string word " Project manager" and in
Cell no. A470 in Sheet "Data Lookup"  project manager" is present but it is showing only manager.
How to fix this issue.
There are a few of problems with your script.
Sometimes matches are not exact, for instance you may have extra spaces, as pointed out by Goran, so those matches are missed
You only return one title per job description, but they often mention several
Your function only returns one value, so you have to repeat it multiple times for every job description.
I built the following one.
What it does is:
convert each job title to a regular expression for better matching
returns multiple job titles per job description
accepts and returns a range of values
const getJobs = (jobDescriptions, jobs) => {
const escape = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const toRe = str => new RegExp(escape(str).replace(/ +/g, ' +'), 'i');
return jobDescriptions.map(jd => {
const result = [
jobs
.reduce((acc, job) => {
const re = toRe(job[0]);
if (re.test(jd)) acc.push(job[0]);
return [...new Set(acc)];
}, [])
.join(', '),
];
return '' !== result[0] ? result : ['Not Found'];
});
};
I put it in cell C2 in your sheet, you can check out the result.
Notice you also have duplicates in your Data Lookup sheet, I remove them from the output.
There is a double space in Project manager in Cell A43 content, this is why it is not found, but you would probably have unexpected results in other occasions, because your function returns a first matched position.
Proper solution would be:
/** *
* #customfunction
*/
function findtitle2(text, jobs){
jobs = jobs.flat();
const allMatches = jobs.map( job => text.indexOf(job)>=0? job:null).filter(m=>m);
if(allMatches.length>0){
// sort by size
const sortedMatches = allMatches.sort((m1,m2)=>m2.length-m1)
// return the longest match
return sortedMatches[0]
} else{
return 'Not Found'
}
}

Split Gmail thread and label by date in a google script

Hy,
I have a server sending me several log mails by day and I want to automaticly label this mails.
I can't touch the server configuration to adapt the mail subject, so the work must be done by "receiver".
The Subject is still same so gmail merge them in a thread of 100, but I want to split them by date. So One Date, one thread. In addition, I want label them whith a nested label: "Server1" -> "Date"
I've only found a way to add label to the thread in globality and no way to split them.
Is it even possible?
After a new look on my issue, perhaps add the date at the message subject can split threads.
Like:
function AddLogSubjectADate() {
var threads = GmailApp.search('from:sender#server.com has:nouserlabels');
threads.forEach(function(messages){
messages.getMessages().forEach(function(msg){
var date = msg.getDate();
var date_of_mail = Utilities.formatDate(date, "GMT+1", "yyyy/MM/dd")
var subj = msg.getSubject()
var newsubj = subj + date_of_mail
//A way to modify subject
});
});
}
But I didn't find a way to change the subject.
Post Scriptum
I don't think it's relevant, but here is my previous work. but it add label to the thread. Like I said I haven't find a way to split threads.
function AddLogLabelbyDate() {
var today = new Date();
var tomorrow = new Date();
var yesterday = new Date();
tomorrow.setDate(today.getDate()+1);
yesterday.setDate(today.getDate()-1);
var date_today = Utilities.formatDate(today, "GMT+1", "yyyy/MM/dd")
var date_tomorrow = Utilities.formatDate(tomorrow, "GMT+1", "yyyy/MM/dd")
var date_yesterday = Utilities.formatDate(yesterday, "GMT+1", "yyyy/MM/dd")
var threads = GmailApp.search('from:sender#server.com has:nouserlabels before:'+ date_tomorrow +' after:'+ date_yesterday +'');
label.addToThreads(threads);
}
Per the API documentation, Gmail follows some rules about thread grouping:
In order to be part of a thread, a message or draft must meet the following criteria:1. The requested threadId must be specified on the Message or Draft.Message you supply with your request.2. The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard.3. The Subject headers must match.
So, you can prevent the automatic grouping into a given conversation thread by modifying any of those 3 parameters.
Alternately, you can apply per-message conversation labels, though this will not really help you if you use "Conversation View" UI.
Both of these methods require the use of the Gmail REST API, for which Apps Script provides an "advanced service" client library. The native GmailApp does not provide a method for per-message thread alteration, or for manipulating messages in the manner needed.
Thread Separation
If you wanted to disable the conversation grouping, in theory you could do this:
Message#get to obtain a full message representation
Modify one of the properties Gmail uses to perform thread grouping
Message#insert or import to create the new message on the server
Message#delete to remove the original
Message#get to get the inserted message metadata, after Gmail has given it a threadId.
Get the remaining messages that should share that new threadId, modify them appropriately, and insert.
Repeat.
I haven't tested that approach, hence my "in theory" comment.
Per-message labeling
The relevant API methods include Gmail.User.Labels.list, Gmail.User.Messages.list, Gmail.User.Messages.modify, and Gmail.User.Messages.batchModify. You'll probably want to use the list and messages.batchModify methods, since you seem to have a large number of messages for which you'd like to make alterations. Note, there are non-trivial rate limits in place, so working in small batches is liable to be most resource-efficient.
This is likely to be the simplest method to implement, since you don't have to actually create or delete messages - just search for messages that should have a given label, add (or create and add) it to them, and remove any non-desired labels. To start you off, here are some minimal examples that show how to work with the Gmail REST API. I expect you will need to refer to the API documentation when you use this information to construct your actual script.
An example Labels#list:
function getLabelsWithName(labelName) {
const search = Gmail.Users.Labels.list("me");
if (!search.labels || !search.labels.length)
return [];
const matches = search.labels.filter(function (label) {
// Return true to include the label, false to omit it.
return label.name === labelName;
});
return matches;
}
An example Messages#list:
function getPartialMessagesWithLabel(labelResource) {
const options = {
labelIds: [ labelResource.id ],
fields: "nextPageToken,messages(id,threadId,labelIds,internalDate)"
};
const results = [];
// Messages#list is paginated, so we must page through them to obtain all results.
do {
var search = Gmail.Users.Messages.list("me", options);
options.pageToken = search.nextPageToken;
if (search.messages && search.messages.length)
Array.prototype.push.apply(results, search.messages);
} while (options.pageToken);
return results;
}
An example Messages#batchModify:
function batchAddLabels(messageArray, labels) {
if (!messageArray || !messageArray.length || !messageArray[0].id)
throw new Error("Missing array of messages to update");
if (!labels || !labels.length || !labels[0].id)
throw new Error("Missing array of label resources to add to the given messages");
const requestMetaData = {
"addLabelIds": labels.map(function (label) { return label.id; }),
"ids": messageArray.map(function (msg) { return msg.id; }) // max 1000 per request!
};
Gmail.Users.Messages.batchModify(requestMetaData, "me");
}
Additional Resources:
Message Searches
"fields" parameter

Counting unique domain names from a column

I wrote a google apps script which fetches the G Suite (google apps) users from the AdminDirectory API
As on output, i get the domain name in front of every user (used replace to extract domain name from each user email id).
what i want to do-:
1. Count the number of users on each domain in one column, so end result should look like this-:
Column - I (exaxple)
domainA.com = 120 users
domainB.com = 28 users
etc....
Any help is appreciated.
I think there is probably a way to shorten this code, but something like this will work. You will have to figure out how to read the correct columns in and output to the correct columns, but hopefully, this will help out:
var emails = [
'someone#testA.com',
'someone#testB.com',
'someone#testA.com',
'someoneelse#testA.com'
];
function countDomains() {
var domainCounts = {};
var domains = emails.map(function(domain) {return domain.split('#')[1];});
domains.forEach(function(each) {
if(domainCounts.hasOwnProperty(each)) {
domainCounts[each]++;
} else {
domainCounts[each] = 1;
}
});
Logger.log(domainCounts);
}

Create student roster from Classroom

I'm trying to pull the student information from a google classroom roster. Here is what I have so far:
function studentRoster() {
var optionalArgs = {
pageSize: 2
};
var getStudents = Classroom.Courses.Students.list("757828465",optionalArgs).students;
Logger.log(getStudents);
}
Sandy's answer below helped solve part of my problem and I get this as a log (names, id's, emails and such changed):
[16-01-05 17:44:04:734 PST] [{profile={photoUrl=https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg, emailAddress=jsdoe#fjuhsd.org, name={givenName=John, familyName=Doe, fullName=John Doe}, id=108117124004883828162}, courseId=757828465, userId=108117124004883828162}, {profile={photoUrl=https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg, emailAddress=jhdoe#fjuhsd.org, name={givenName=Jane, familyName=Doe, fullName=Jane Doe}, id=115613162385930536688}, courseId=757828465, userId=115613162385930536688}]
So my question now is: How do I extract only certain pieces of this information (like full name and email)?
The end result will be pushing it to a google sheet.
The Collection sections under the Classroom API Reference can help clarify some of the object chaining required to access the fields you want.
The following code will generate a list of student names, their email, and associated course ID.
function listStudents() {
var optionalArgs = {
pageSize: 0 // Max output
};
var response = Classroom.Courses.Students.list(xxxxxxxxx; // Put CourseID here
var students = response.students;
if(students && students.length > 0){
for( i = 0; i < students.length; i++){
var student = students[i];
// fullName is normally prefixed with s/ use of .substring removes first two characters
Logger.log('%s %s %s', student.profile.name.fullName.substring(2), student.profile.emailAddress, student.courseId );
}
} else {
Logger.log('No students in this course');
}
}
Collection courses.students
The Quickstart example is using the list method, and the list method takes optional query parameters. One of them being pageSize.
Query parameters for List
You are using the get method.
Documentation - Get method
The only options that the get method has is for the Path. And there is only one setting, the id.
So, that object literal named optionalArgs, you don't need for what you are doing. optionalArgs is an object because it has curly braces, and has elements that are "key/value" pairs. It's "literal", because it's typed into the code, as opposed to building the object with code.
If you did a search, (open the search dialog with Ctrl + F), and searched for optionalArgs, you'll see that it's not being used anywhere. So, the optionalArgs object with the pageSize property is not needed when using the get method.