I have a spreadsheet, and I would like to use google scripts (JS based) to put all of that data into an array and then search through that array for information. The code I have below does not work, and returns nothing in the logger:
function openNCR() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var NCRData = ss.getSheets()[1];
var data = NCRData.getDataRange.getValues();
var search = 'Open NCR'
var target_rows = []
data.forEach(function(element, index) {
if (element == search) {
target_rows.push(index)
}
})
Logger.log(target_rows);
}
Yet when I make the change below in terms of how to pull the information, the code works great! What is the key difference here in the above and below? Doesn't getDataRange.getValues pull all the values within that sheet? If that is the case, then why do I need to define a specific column to search through as per below (column 3 is the column that contains the information "Open NCR" or "Closed NCR")?
function openNCR() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var NCRData = ss.getSheets()[1];
var data = NCRData.getRange(1, 3, NCRData.getLastRow()).getValues()
var search = 'Open NCR'
var target_rows = []
data.forEach(function(element, index) {
if (element == search) {
target_rows.push(index)
}
})
Logger.log(target_rows);
}
.getDataRange() is a method from the class 'Sheet' and thus requires brackets.
var data = NCRData.getDataRange().getValues();
Related
I have a project that I've been working on for a bit. I've received some excellent help here, and I think I'm almost done and just need one more bit of help to get it working.
The script looks at a Google Sheet and takes a place name entered in Column A and uses the Google Places API to find requested information about it (address, phone number, etc.)
The last bit of help that I need will be able to implement the cell input component. The last user to help me said that
function writeToSheet(){
var ss = SpreadsheetApp.getActiveSheet();
var data = COMBINED2("Food");
var placeCid = data[4];
var findText = ss.createTextFinder(placeCid).findAll();
if(findText.length == 0){
ss.getRange(ss.getLastRow()+1,1,1, data.length).setValues([data])
}
}
would be able use TextFinder to check if the place url exists in the Sheet. If the result of TextFinder is 0, it will call COMBINED2() to get the place information and populate the Sheet with writeToSheet()
They noted that
You can use a cell input in your COMBINED2 by using
ss.getRange(range).getValue()
Not having a coding background, I have been able to stitch most of this together on my own, but I could use a bit of help in adding that capability to my code. Any help or guidance would be great.
Here is the code in full:
// This location basis is used to narrow the search -- e.g. if you were
// building a sheet of bars in NYC, you would want to set it to coordinates
// in NYC.
// You can get this from the url of a Google Maps search.
const LOC_BASIS_LAT_LON = "40.74516247433546, -73.98621366765816"; // e.g. "37.7644856,-122.4472203"
function COMBINED2(text) {
var API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx';
var baseUrl = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json';
var queryUrl = baseUrl + '?input=' + text + '&inputtype=textquery&key=' + API_KEY + "&locationbias=point:" + LOC_BASIS_LAT_LON;
var response = UrlFetchApp.fetch(queryUrl);
var json = response.getContentText();
var placeId = JSON.parse(json);
var ID = placeId.candidates[0].place_id;
var fields = 'name,formatted_address,formatted_phone_number,website,url,types,opening_hours';
var baseUrl2 = 'https://maps.googleapis.com/maps/api/place/details/json?placeid=';
var queryUrl2 = baseUrl2 + ID + '&fields=' + fields + '&key='+ API_KEY + "&locationbias=point:" + LOC_BASIS_LAT_LON;
if (ID == '') {
return 'Give me a Google Places URL...';
}
var response2 = UrlFetchApp.fetch(queryUrl2);
var json2 = response2.getContentText();
var place = JSON.parse(json2).result;
var weekdays = '';
place.opening_hours.weekday_text.forEach((weekdayText) => {
weekdays += ( weekdayText + '\r\n' );
} );
var data = [
place.name,
place.formatted_address,
place.formatted_phone_number,
place.website,
place.url,
weekdays.trim()
];
return data;
}
function getColumnLastRow(range){
var ss = SpreadsheetApp.getActiveSheet();
var inputs = ss.getRange(range).getValues();
return inputs.filter(String).length;
}
function writeToSheet(){
var ss = SpreadsheetApp.getActiveSheet();
var data = COMBINED2("Food");
var placeCid = data[4];
var findText = ss.createTextFinder(placeCid).findAll();
if(findText.length == 0){
ss.getRange(ss.getLastRow()+1,1,1, data.length).setValues([data])
}
}
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("Custom Menu")
.addItem("Get place info","writeToSheet")
.addToUi();
}
Update
Here is a link to a Shared Sheet in case anyone wants to work on it with me.
https://docs.google.com/spreadsheets/d/1KGsk6nkin1CUgpjfHU_AdhF17T_Eh41_g4MLb1CG_Tk/edit#gid=2100307022
Here is what I might not have articulated properly.
I wanted to be able to enter the names of places in Column A
Then, I want to be able to run the function with the custom menu feature. If TextFinder does not find the Place URL for the given place, it will look up the data and write it to the Sheet.
I wanted to limit the number of API calls with this and to make sure the data was written to the Sheet so that it does not need to be pulled each time the Sheet is reopened.
Finished Product
Big thanks to Lamblichus for sticking this out with me. I hope this helps other people some day.
Here is the finished code:
// This location basis is used to narrow the search -- e.g. if you were
// building a sheet of bars in NYC, you would want to set it to coordinates
// in NYC.
// You can get this from the url of a Google Maps search.
const LOC_BASIS_LAT_LON = "ENTER_GPS_COORDINATES_HERE"; // e.g. "37.7644856,-122.4472203"
function COMBINED2(text) {
var API_KEY = 'ENTER_API_KEY_HERE';
var baseUrl = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json';
var queryUrl = baseUrl + '?input=' + text + '&inputtype=textquery&key=' + API_KEY + "&locationbias=point:" + LOC_BASIS_LAT_LON;
var response = UrlFetchApp.fetch(queryUrl);
var json = response.getContentText();
var placeId = JSON.parse(json);
var ID = placeId.candidates[0].place_id;
var fields = 'name,formatted_address,formatted_phone_number,website,url,types,opening_hours';
var baseUrl2 = 'https://maps.googleapis.com/maps/api/place/details/json?placeid=';
var queryUrl2 = baseUrl2 + ID + '&fields=' + fields + '&key='+ API_KEY + "&locationbias=point:" + LOC_BASIS_LAT_LON;
if (ID == '') {
return 'Give me a Google Places URL...';
}
var response2 = UrlFetchApp.fetch(queryUrl2);
var json2 = response2.getContentText();
var place = JSON.parse(json2).result;
var weekdays = '';
if (place.opening_hours && place.opening_hours.weekday_text) {
place.opening_hours.weekday_text.forEach((weekdayText) => {
weekdays += ( weekdayText + '\r\n' );
} );
}
var data = [
place.name,
place.formatted_address,
place.formatted_phone_number,
place.website,
place.url,
weekdays.trim()
];
return data;
}
function writeToSheet() {
const sheet = SpreadsheetApp.getActiveSheet();
const FIRST_ROW = 2;
const sourceData = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 6)
.getValues().filter(row => String(row[0]));
for (let i = 0; i < sourceData.length; i++) {
const sourceRow = sourceData[i];
if (sourceRow[4] === "") {
const text = sourceRow[0];
const data = COMBINED2(text);
sheet.getRange(FIRST_ROW+i, 2, 1, data.length).setValues([data]);
}
}
}
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("Custom Menu")
.addItem("Get place info","writeToSheet")
.addToUi();
}
Desired goal:
If I understand you correctly, for each value in column A, you want to retrieve some related data from Maps API and paste it to columns B-F, if column E is not currently populated.
Issues:
You are only providing the last value from column A to COMBINED2, but you want to loop through all values in column A and fetch the desired information for all of them (as long as the Place URL -column E- is not already populated).
If you want to avoid calling Maps API if the Place URL is not populated, using TextFinder after calling Maps API doesn't make sense; you don't limit your calls to the API if you do that. If you just want to check whether the Place URL column is populated, I'd suggest checking whether the cell is empty or not, and calling Maps API if it's empty.
Proposed workflow:
Retrieve all values from the sheet, including not just column A but also E (for practical purposes, all 6 columns are fetched in the sample below, since it can be done in one call), using Range.getValues().
Iterate through the rows (for example, using for), and for each row, check that the cell in E is populated.
If the cell in E (Place URL) is empty, use the value in A as the parameter for COMBINED2 and write the resulting data to columns B-F, as you are currently doing.
Code sample:
function writeToSheet() {
const sheet = SpreadsheetApp.getActiveSheet();
const FIRST_ROW = 2;
const sourceData = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 6)
.getValues().filter(row => String(row[0]));
for (let i = 0; i < sourceData.length; i++) {
const sourceRow = sourceData[i];
if (sourceRow[4] === "") {
const text = sourceRow[0];
const data = COMBINED2(text);
sheet.getRange(FIRST_ROW+i, 2, 1, data.length).setValues([data]);
}
}
}
Update:
For names in which Places API doesn't return opening_hours, consider checking if this exists first:
function COMBINED2(text) {
// ... REST OF YOUR FUNCTION ...
var weekdays = '';
if (place.opening_hours && place.opening_hours.weekday_text) {
place.opening_hours.weekday_text.forEach((weekdayText) => {
weekdays += ( weekdayText + '\r\n' );
} );
}
var data = [
place.name,
place.formatted_address,
place.formatted_phone_number,
place.website,
place.url,
weekdays.trim()
];
return data;
}
By using the event trigger function...
function onEdit(e){
SpreadsheetApp.getActiveSheet().getRange(insert your range in A1 format).setValue("anything you want to add into the cell")
}
function onEdit(e){
var ss = SpreadsheetApp.getActiveSheet();
var data = COMBINED2("Food");
var placeCid = data[4];
var findText = ss.createTextFinder(placeCid).findAll();
if(findText.length == 0){
ss.getRange(ss.getLastRow()+1,1,1, data.length).setValues([data])
}
}
u need to specifically tell google apps script that the function is as such so that your function will execute when a event object known as e has happened.
You can read more about it on Simple Triggers
I created a function that search's my inbox in Gmail and then classifies each massage in 3 categories (name, email and body). Then it pastes it in 3 columns in a Google Sheet. But, when I run it will only paste the name and email on the first 2 columns. The problems is with the body. Even though, when i run Logger.log(d.getPlainBody()); it shows the body text I am looking for.
The code I am using:
// extract emails from label in Gmail
function extractEmails() {
// get the spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
// get all email threads that match label from Sheet
var threads = GmailApp.getInboxThreads();
// get all the messages for the current batch of threads
var messages = GmailApp.getMessagesForThreads(threads);
var emailArray = [];
// get array of email addresses
messages.forEach(
function(message) {
message.forEach(
function(d) {
emailArray.push(d.getFrom(),d.getTo(),d.getPlainBody());
Logger.log(d.getPlainBody());
});
});
// de-duplicate the array
var uniqueEmailArray = emailArray.filter(function(item, pos) {
return emailArray.indexOf(item) == pos;
});
var cleanedEmailArray = uniqueEmailArray.map(
function(el) {
var matches = el.match(/\s*"?([^"]*)"?\s+<(.+)>/);
if (matches) {
name = matches[1];
email = matches[2];
body = matches[3];
}
return [name, email, body];
});
// clear any old data
sheet.getRange(2,1,sheet.getLastRow(), 3).clearContent();
// paste in new names and emails
var printing = sheet.getRange(2 ,1,cleanedEmailArray.length, 3);
printing.setValues(cleanedEmailArray);
}
Your logic seems faulty, especially on the map function. You are actually matching every element, not per message. And your regex is only capturing 2 patterns so it was expected for matches[3] to return blank.
I modified your code a little bit. Here are they:
Passing variables as an element of an array (message), and removed getTo as you are not using it. But if you are, feel free to add it again.
// get array of email addresses
messages.forEach(function(message) {
message.forEach(function(d) {
// Push the data as array to easily process them
// Removed getTo as you only are getting name and email from getFrom data based on your regex
emailArray.push([d.getFrom(),d.getPlainBody()]);
});
});
Map function was replaced with a simpler forEach. This is possible since we made the message data as array above.
var cleanedEmailArray = [];
uniqueEmailArray.forEach(function(message){
// match name and email from pattern "name <email>" in d.getFrom()
// only pass match[1] and match[2], since match[0] = message[0]
[, name, email] = message[0].match(/\s*"?([^"]*)"?\s+<(.+)>/);
// get body from d.getPlainBody()
body = message[1];
cleanedEmailArray.push([name, email, body])
});
(Nitpick) You were not using printing so I combined the 2 lines
// paste in new names and emails
sheet.getRange(2 ,1,cleanedEmailArray.length, 3).setValues(cleanedEmailArray);
This is what your code will look like:
Code:
function extractEmails() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var threads = GmailApp.getInboxThreads();
var messages = GmailApp.getMessagesForThreads(threads);
var emailArray = [], cleanedEmailArray = [];
messages.forEach(function(message) {
message.forEach(function(d) {
emailArray.push([d.getFrom(),d.getPlainBody()]);
});
});
var uniqueEmailArray = emailArray.filter(function(item, pos) {
return emailArray.indexOf(item) == pos;
});
uniqueEmailArray.forEach(function(message){
[, name, email] = message[0].match(/\s*"?([^"]*)"?\s+<(.+)>/);
body = message[1];
cleanedEmailArray.push([name, email, body])
});
sheet.getRange(2,1,sheet.getLastRow(), 3).clearContent();
sheet.getRange(2 ,1,cleanedEmailArray.length, 3).setValues(cleanedEmailArray);
}
Output:
If anyone comes here, I have modified a bit of the code from NaziA and created an InboxScraper(The article has step by step with many images). I leave the code below. From the google app script connected to your Google Sheets it will access your Gmail and Scrape email (Column A) and body (Column B). I have posted images in an article.
function extractEmails() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var threads = GmailApp.getInboxThreads();
var messages = GmailApp.getMessagesForThreads(threads);
var emailArray = [], cleanedEmailArray = [];
messages.forEach(function(message) {
message.forEach(function(d) {
emailArray.push([d.getFrom(),d.getPlainBody()]);
});
});
var uniqueEmailArray = emailArray.filter(function(item, pos) {
return emailArray.indexOf(item) == pos;
});
uniqueEmailArray.forEach(function(message){
[, name, email] = message[0].match(/\s*"?([^"]*)"?\s+<(.+)>/);
body = message[1];
cleanedEmailArray.push([name, email, body])
});
sheet.getRange(2,1,sheet.getLastRow(), 3).clearContent();
sheet.getRange(2 ,1,cleanedEmailArray.length, 3).setValues(cleanedEmailArray);
}
I have done a bit of research on how I can perform a rowcall using google app script but have a little challenge and will appreciate any assitance on this.
So this code looks at the first column and gets the values to be used in renaming new tabs
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getActiveSheet();
var sheet1 = ss.getSheetByName("main")
var getNames = sheet1.getRange("A2:A").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
templateSheet.copyTo(ss).setName(getNames[i]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
}
}
}
The sheet
What I would like to do and is becoming a challenge, is While creating a new tab with a name then, I would like to copy the entire row to the new tab/sheet. e.g for the sheet Levi only the raw with Levi Data be copied to the sheet.
At the moment my code copies the entire source sheet to the new tabs/sheets. I will really appreciate any help with this
Proposed solution:
Now you are using the main sheet as a template so when you use it with the function .copyTo you will copy the whole content.
You will have to get the whole row corresponding to the index of the given name.
Approach
You will need an extra filtering to get the correct row values you want to put in the new sheet.
I will filter the name column (column A) and get the index of the name in the loop.
(I am assuming you can have some gaps so the index of the for loop would not be enough).
Once i found the corresponding index i will need to increment it by one because row indexing starts from 1 in Google Spreadsheets.
Now i can easily get the row using the function .getRange(row, column, numRows, numColumns).
I am using the function .getLastColumn() to compute the numColumns parameter.
Now I can use the function .appendRow() to insert the row in the new sheet I just created with the .insertSheet() function.
Sample Code:
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getActiveSheet();
var sheet1 = ss.getSheetByName("main")
var getNames = sheet1.getRange("A2:A").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
//The copyTo function will copy the entire sheet
//templateSheet.copyTo(ss).setName(getNames[i]);
var rowIndex = sheet1.getRange("A:A").getValues().flatMap(value => value[0]).indexOf(getNames[i]) + 1;
var rowValues = sheet1.getRange(rowIndex, 1, 1, sheet1.getLastColumn()).getValues();
ss.insertSheet(getNames[i]).appendRow(rowValues[0]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
}
}
}
Edit
In the case the names are repeated you will have to filter the column and extract the corresponding indexes.
First you will have to get a set for your getNames variable. (otherwise you will have repetitions).
var getNames = [...new Set(sheet1.getRange("A2:A").getValues().filter(String).toString().split(","))];
Then you will have to map the row indexes to the names in the column A.
Now you can filter by the getNames values and you will obtain the row indexes.
In the end you can append to the new sheet the rows at the corresponding indexes.
var rowIndexes = sheet1.getRange("A:A").getValues()
.map((value, index) => [value[0], (index + 1)])
.filter(value => value[0] === getNames[i]);
var namedSheet = ss.insertSheet(getNames[i]);
rowIndexes.map(index => {
var rowValues = sheet1.getRange(index[1], 1, 1, sheet1.getLastColumn()).getValues();
namedSheet.appendRow(rowValues[0]);
});
References:
Class Sheet
Following a previous question
I want to classify text entries by adding a tag in the next column.
I could do it using regex but it will take too much time writing all conditions like :
if(String(data[i][0]).match(/acme|brooshire|dillons|target|heb|costco/gi))
{
labValues[i][0]='Supermarket';
}
Instead I created a named list with all stores names (in another sheet).
If an entry matches a term in the list, the next column is set to "Supermarket".
I am using this script below... No bugs but nothing happens when executed !
function tagStore() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange('A2:A655')
var store = range.getValues();
var tag = sheet.getRange('B2:B655');
var tagvalues= tag.getValues();
var storeList= SpreadsheetApp.getActive().getRangeByName("store_list");
for (var i = 0; i<store.length; i++)
{
if(String(store[i][0]).match(storeList))
{
tagvalues[i][0]='Supermarket';
}
}
tag.setValues(tagvalues);
}
Edit:
It is important to use a Regex as the "store" Values are not exactly the same as the "store_list".
Store Values : ["Acme Store", "HEB PLaza", "Dillons Group"...]
Store_List : [acme, heb, dillons...]
Instead of trying to go with the regEx approach there is a more straightforward approach by retrieving the range as a list.
// For a Column
var storeList = SpreadsheetApp.getActive().getRangeByName("store_list").getValues().map(function(r){return r[0];});
// For a Row
var storeList = SpreadsheetApp.getActive().getRangeByName("store_list").getValues()[0];
And then look if the values you are looking for are in this list with indexOf().
Try this:
function tagStore() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange('A2:A655')
var store = range.getValues();
var tag = sheet.getRange('B2:B655');
var tagvalues= tag.getValues();
var storeList= SpreadsheetApp.getActive().getRangeByName("store_list").getValues().map(function(r){return r[0];});//if it's a column
//var storeList=SpreadsheetApp.getActive().getRangeByName("store_list").getValues()[0];//if it's a row
for (var i=0;i<store.length; i++) {
if(storeList.indexOf(store[i][0])!=-1) {
tagvalues[i][0]='Supermarket';
}
}
tag.setValues(tagvalues);
}
I've got this working code:
function hUpdate() {
var ss = SpreadsheetApp.getActiveSheet();
var hRowNum = ss.getLastRow();
var hNew = ss.getRange(hRowNum,3).getValue(); // value being compared
var hCompare = ss.getRange(hRowNum-1,3).getValue();
if (hNew == hCompare)
{hNew = 'same';}
else
{hNew = 'different';}
return hNew;
}
What I really want to do is compare hNew with all values in previous rows of the same column. I know I have to use an array but I'm stuck with the actual coding.