Google Apps Script: how to copy array of objects to range? - javascript

I have an array of objects called membership:
[{name: 'Linus Pauling', address: '1805 Main Street', phone: '(615) 555-1010',
email: 'linus.pauling#gmail.com' },
{name: 'Maury Povich', address: '382 North Street', phone: '(423) 555-1997',
email: 'maury#themauryshow.org'}]
(Although only 4 are shown here, I really have ten key/value pairs per member.)
What is the best way to copy bits of this array to a range of cells in Google Sheets? I am trying to find a method to call the object values directly, and use the .SetValues method to copy them en masse, as opposed to one at a time.
To get all the members' names in column A, I've tried:
sheet.getRange(1,1,membership.length,1).setValues(membership[{member.name}]);
...which gives Missing : after property ID.
Or:
sheet.getRange(1,1,membership.length,1).setValues([membership[name]]);
...which gives ReferenceError: "name" is not defined.
Or:
sheet.getRange(1,1,membership.length,1).setValues([member.name]);
...which gives the "Cannot convert Array to Object[][]" error.
I apologize for the newbie question. I have seen answers about how to copy values from a multidimensional array to a sheet's range, but not an array of objects.

Are you looking to produce a table in the form:
name | Address | phone | email
-----+---------+-------+------
.... | .... | .... | ....
etc?
If so, then the following snippet may help. The key thing to point out here is that you can't expect a given order when iterating through an object. i.e. Just because your representation of membership lists name first, doesn't mean that if you were to use a for ... in loop you could guarantee that name would come back first - Objects in JavaScript are unordered.
To ensure a given order, the snippet I list defines an array headings in which you specify the order of the columns you want in the Sheet. This is used to guarantee the column order in the output:
var membership = [
{
name: 'Linus Pauling', address: '1805 Main Street', phone: '(615) 555-1010',
email: 'linus.pauling#gmail.com'
},
{
name: 'Maury Povich', address: '382 North Street', phone: '(423) 555-1997',
email: 'maury#themauryshow.org'
}
];
// Headings in the column order that you wish the table to appear.
var headings = ['name', 'address', 'phone', 'email'];
var outputRows = [];
// Loop through each member
membership.forEach(function(member) {
// Add a new row to the output mapping each header to the corresponding member value.
outputRows.push(headings.map(function(heading) {
return member[heading] || '';
}));
});
// Write to sheets
if (outputRows.length) {
// Add the headings - delete this next line if headings not required
outputRows.unshift(headings);
SpreadsheetApp.getActiveSheet().getRange(1, 1, outputRows.length, outputRows[0].length).setValues(outputRows);
}
Output is :

How about following script? This script is a container bound script of spreadsheet. There are 2 patterns. The arrangement of data is different for between pattern 1 and 2.
Pattern 1 is
Pattern 2 is
function main() {
var data = [{name: 'Linus Pauling', address: '1805 Main Street', phone: '(615) 555-1010',
email: 'linus.pauling#gmail.com' }, {name: 'Maury Povich', address: '382 North Street', phone: '(423) 555-1997',
email: 'maury#themauryshow.org'}];
var sheet = SpreadsheetApp.getActiveSheet();
var result = pattern1(data);
var result = pattern2(data);
sheet.getRange('a1').offset(0, 0, result.length, result[0].length).setValues(result);
}
function pattern1(data){
var ar = [];
for (var i in data){
for (var key in data[i]){
ar.push([key, data[i][key]]);
}
}
return ar;
}
function pattern2(data){
var ar = [];
var keys = [];
var values = [];
for (var i in data){
for (var key in data[i]){
if (i == 0) keys.push(key);
values.push(data[i][key]);
}
if (i == 0){
ar.push(keys);
keys = [];
}
ar.push(values);
values = [];
}
return ar;
}
If my understanding for your questions was wrong, I apologize.

See the script at the bottom of the Simple Mail Merge Tutorial for some useful code to retrieve data. Copy all the code from this comment down:
//////////////////////////////////////////////////////////////////////////////////////////
//
// The code below is reused from the 'Reading Spreadsheet data using JavaScript Objects'
// tutorial.
//
/////////
/////////////////////////////////////////////////////////////////////////////////
// getRowsData iterates row by row in the input range and returns an array of objects.
Once you have, the getRowsData returns not only the data, but a second item which is a list of the Headers. Using this, you can modfy the code from #Bardy to allow for the Headers in any order, and also other possible columns in between.

Related

Match Entire Cell for Multiple Find and Replace in Google Sheets via Google Apps Script

First question here - I'm trying to use the Multiple Find and Replace in Google App Scripts for Google Sheets from this thread, however, I need to do an exact match on the cell. I did some research and see mentions of the class TextFinder and method matchExactCell, but I am stumped on where to add it.
When the script is run multiple times, then the first name in the replace is appended multiple times so the replaced cell reads: "John John John Smith" if script is run 3 times.
Any recommendations are appreciated! Thanks!
function runReplaceInSheet(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
// get the current data range values as an array
// Fewer calls to access the sheet -> lower overhead
var values = sheet.getDataRange().getValues();
// Replace Names
replaceInSheet(values, 'Smith', 'John Smith');
replaceInSheet(values, 'Doe', 'Jane Doe');
// Write all updated values to the sheet, at once
sheet.getDataRange().setValues(values);
}
function replaceInSheet(values, to_replace, replace_with) {
//loop over the rows in the array
for(var row in values){
//use Array.map to execute a replace call on each of the cells in the row.
var replaced_values = values[row].map(function(original_value) {
return original_value.toString().replace(to_replace,replace_with);
});
//replace the original row values with the replaced values
values[row] = replaced_values;
}
}
You can try with this little modification that does not use the function "replace" but compares the whole value with "replace_to" and returns "replace_with" if it's equal or "original_values" if it's not:
function replaceInSheet(values, to_replace, replace_with) {
//loop over the rows in the array
for(var row in values){
//use Array.map to execute a replace call on each of the cells in the row.
var replaced_values = values[row].map(function(original_value) {
if(original_value == to_replace) {return replace_with}
else {return original_value};
});
//replace the original row values with the replaced values
values[row] = replaced_values;
}
}
As another approach, from I did some research and see mentions of the class TextFinder and method matchExactCell, but I am stumped on where to add it., if you want to use TextFinder, how about the following sample script?
Sample script:
function sample1() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var obj = [
{ find: "Smith", replacement: "John Smith" },
{ find: "Doe", replacement: "Jane Doe" }
];
var range = sheet.getDataRange();
obj.forEach(({ find, replacement }) => range.createTextFinder(find).matchEntireCell(true).replaceAllWith(replacement));
}
Although the process cost of TextFinder is low, in this case, the replacement is run in a loop. If you want to reduce the process cost more, how about the following sample script? In this sample, Sheets API is used. By this, the process cost is lower a little than that of the above. Before you use this script, please enable Sheets API at Advanced Google services.
function sample2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetId = ss.getSheetByName("Sheet1").getSheetId();
var obj = [
{ find: "Smith", replacement: "John Smith" },
{ find: "Doe", replacement: "Jane Doe" }
];
var requests = obj.map(({ find, replacement }) => ({ findReplace: { find, replacement, range: { sheetId }, matchEntireCell: true } }));
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
References:
createTextFinder(findText) of Class Range
Method: spreadsheets.batchUpdate
FindReplaceRequest

Why can't I access variable inside a nested function?

So I have been using the geolocation to find the user location and then find the distance to it and a csv list of locations. I want to save the distance to the json object, but can't access it in my nested function.
function onLocationFound(e) {
console.log(e);
L.marker([e.latitude, e.longitude], {icon: home_marker}).bindPopup("You are here!").addTo(m);
console.log(typeof(e.latitude));
console.log(typeof(e.longitude));
$.get('vac_sites.csv', function (csvString) {
var data = Papa.parse(csvString, { header: true, dynamicTyping: true }).data;
console.log(data)
for (let i = 0; i < data.length; i++) {
//get distance and then use dynamic variable names, make them into js objects, then store in dic with distance
var row = data[i];
console.log(row)
var myRoute = L.Routing.osrmv1({
serviceUrl: 'https://xxx.xxx.xxx:443/route/v1'
});
var self_loc = new L.Routing.Waypoint;
self_loc.latLng = L.latLng([e.latitude, e.longitude]);
var site_loc = new L.Routing.Waypoint;
site_loc.latLng = L.latLng([row.Latitude, row.Longitude]);
myRoute.route([self_loc, site_loc], function(err, routes) {
distance = routes[0].summary.totalDistance;
console.log('routing distance: ' + distance);
row.distance = distance
console.log(row)
});
When I open the console, it appears to have created a new json object and added row to it.
How can I access the original row variable and add the distance to it? Is it a problem with function scope?
EDIT:
When I open console I get this for the first console.log(row):
...
{Name: "Cate Pharmacy", Address: "500 N Missouri Ave, Corning, Arkansas", Telephone: "(870) 857-6766", Website: "www.catepharmacy.com/", Latitude: 36.4155144, …}
...
I want to add a key and value pair for this that is the distance of the route in the form distance: xxxxx.
Desired result is:
...
{Name: "Cate Pharmacy", Address: "500 N Missouri Ave, Corning, Arkansas", Telephone: "(870) 857-6766", Website: "www.catepharmacy.com/", Latitude: 36.4155144, distance: xxxxxx, …}
...
But instead at the second console.log(row) I get this:
{Name: null, distance: 265184.8}
Talking on the chat we solved the problem.
It was narrowed down to changing var row; to let row;
It sounds like row is leaked into your function somehow. You should read this to understand the differences between the two variable declaration keywords.
In a nutshell, var is bound to the immediate function body while let is bound to the immediate closing block. That could be what caused it. Otherwise, I don't know.
It's best to use let because var is almost always unnecessary and can cause problems.

Javascript ForEach on Array of Arrays

I am looping through a collection of blog posts to firstly push the username and ID of the blog author to a new array of arrays, and then secondly, count the number of blogs from each author. The code below achieves this; however, in the new array, the username and author ID are no longer separate items in the array, but seem to be concatenated into a single string. I need to retain them as separate items as I need to use both separately; how can I amend the result to achieve this?
var countAuthors = [];
blogAuthors = await Blog.find().populate('authors');
blogAuthors.forEach(function(blogAuthor){
countAuthors.push([blogAuthor.author.username, blogAuthor.author.id]);
})
console.log(countAuthors);
// Outputs as separate array items, as expected:
// [ 'author1', 5d7eed028c298b424b3fb5f1 ],
// [ 'author2', 5dd8aa254d74b30017dbfdd3 ],
var result = {};
countAuthors.forEach(function(x) {
result[x] = (result[x] || 0) + 1;
});
console.log(result);
// Username and author ID become a single string and cannot be accessed as separate array items
// 'author1,5d7eed028c298b424b3fb5f1': 15,
// 'author2,5dd8aa254d74b30017dbfdd3': 2,
Update:
Maybe I can explain a bit further WHY on what to do this. What I am aiming for is a table which displays the blog author's name alongside the number of blogs they have written. However, I also want the author name to link to their profile page, which requires the blogAuthor.author.id to do so. Hence, I need to still be able to access the author username and ID separately after executing the count. Thanks
You could use String.split().
For example:
let result = 'author1,5d7eed028c298b424b3fb5f1'.split(',')
would set result to:
['author1' , '5d7eed028c298b424b3fb5f1']
You can then access them individually like:
result[1] //'5d7eed028c298b424b3fb5f1'
Your issue is that you weren't splitting the x up in the foreach callback, and so the whole array was being converted to a string and being used as the key when inserting into the results object.
You can use array destructuring to split the author name and blog id, and use them to optionally adding a new entry to the result object, and then update that result.
countAuthors = [
['author1', 'bookId1'],
['author2', 'bookId2'],
['author1', 'bookId3'],
['author1', 'bookId4'],
['author2', 'bookId5']
]
var result = {};
countAuthors.forEach(([author, id]) => {
if (result[author] === undefined) {
result[author] = {count: 0, blogIds: []};
}
result[author].count += 1;
result[author].blogIds.push(id);
});
console.log(result);

IndexedDB Get next unique index based on predicate

Is there a way to retrieve the next unique index in a store based on a predicate on the record. For example if I have a book store full of objects like so:
{name: 'Hello Kitty', author: 'Me', pages: 5}
Would it be possible to return the next unique index on author, but base the uniqueness on the highest number of pages?
index.openKeyCursor('author', IDBCursor.nextunique).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// How to filter the record by highest number of pages?
cursor.continue();
}
};
This is a bit tricky, but you can do. I will illustrate with my library https://bitbucket.org/ytkyaw/ydn-db but you can use IndexedDB API.
First you have to use compound index (only Firefox and Chrome supported) using array keyPath. Database schema for ydn-db is
var schema = {
stores: [{
name: 'book',
indexes: [{
name: 'author, pages',
keyPath: ['author', 'pages']
}]
}
};
var db = new ydn.db.Storage('db name', schema);
The index, 'author, pages' is sorted by author and then by pages. Then we prepare cursor or create iterator in ydn-db.
var iter = new ydn.db.IndexValueIterator('book', 'author, pages');
By default, order is in ascending. Here we want descending order to get highest pages value. This inadvertently make author to sort in descending order, but there is no way to avoid it.
iter = iter.reverse().unique(); // essentially 'PREV_UNIQUE'
Then, we open the iterator giving rise to cursor with descending ordering. The first cursor is what we want. On next iteration, we skip duplicate author name. This is done by using cursor.continue(next_key) method. next_key is given, such that it won't repeat what already got by giving lowest possible value with known author key.
db.open(function(cursor) {
var book = cursor.getValue();
console.log(book);
var effective_key = cursor.getKey();
var author_key = effective_key[0];
var next_key = [author_key];
return next_key; // continue to this or lower than this key.
}, iter);
Note that, we just need to iterate only unique author and no buffer memory require, and hence scalable.

Better data structure to handle this array

I have an array of data get from the server(ordered by date):
[ {date:"2012-8", name:"Tokyo"}, {date:"2012-3", name:"Beijing"}, {date:"2011-10", name:"New York"} ]
I'd like to :
get the name of the first element whose date is in a given year, for example, given 2012, I need Tokyo
get the year of a given name
change the date of a name
which data structure should I use to make this effective ?
because the array could be large, I prefer not to loop the array to find something
Since it appears that the data is probably already sorted by descending date you could use a binary search on that data to avoid performing a full linear scan.
To handle the unstated requirement that changing the date will then change the ordering, you would need to perform two searches, which as above could be binary searches. Having found the current index, and the index where it's supposed to be, you can use two calls to Array.splice() to move the element from one place in the array to another.
To handle searches by name, and assuming that each name is unique, you should create a secondary structure that maps from names to elements:
var map = {};
for (var i = 0, n = array.length; i < n; ++i) {
var name = array[i].name;
map[name] = array[i];
}
You can then use the map array to directly address requirements 2 and 3.
Because the map elements are actually just references to the array elements, changes to those elements will happen in both.
Assuming you are using unique cities, I would use the city names as a map key:
cities = {
Tokyo: {
date: "2012-8"
},
New York: {
date: "2011-10"
}
}
To search by date:
function byDate(date) {
for(el in cities) {
if(cities.hasOwnProperty(el) && cities[el].date === date)
return el;
}
}
Just for the record: without redesigning your date structure you could use sorting combined with the Array filter or map method:
function sortByDate(a,b){
return Number(a.date.replace(/[^\d]+/g,'')) >
Number(b.date.replace(/[^\d]+/g,''));
}
var example = [ {date:"2012-8", name:"Tokyo"},
{date:"2012-3", name:"Beijing"},
{date:"2011-10", name:"New York"} ]
.sort(sortByDate);
//first city with year 2012 (and the lowest month of that year)
var b = example.filter(function(a){return +(a.date.substr(0,4)) === 2012})[0];
b.name; //=> Beijing
//year of a given city
var city = 'Tokyo';
var c = example.filter(function(a){return a.city === city;})[0];
c.year; //=> 2012
//change year of 'New York', and resort data
var city = 'New York', date = '2010-10';
example = example.map(
function(a){if (a.name === city) {a.date = date;} return a;}
).sort(sortByDate);

Categories

Resources