Calendar.getEvents() returned array order - google-apps-script

I've searched online and I've looked at the Class Calendar API reference, found here:
https://developers.google.com/apps-script/reference/calendar/calendar
I notice from running a script I've created that the elements of CalendarEvent[] returned by getEvents(startTime,endTime) seem to be in chronological order. Is this always true?
Essentially, am I guaranteed that the following code
events[i].getStartTime().getTime() <= events[i+1].getStartTime().getTime()
will always be true for 0 <= i < (events.length - 1)?
I'm interested in this because I'm creating a script, which merges two (or more) distinct calendars into one and also returns all time slots which are either unallocated (i.e. no event scheduled) or overlap more than one event. Knowing that the elements within a CalendarEvent[] are chronologically ordered makes this task significantly easier (and computationally less expensive).
TIA for any assistance,
S

From my experience, yes. It was always in this order.
Though I checked the docs and they don't mention anything about it.
So to be safe, you can either use advanced services to sort by the date https://developers.google.com/google-apps/calendar/v3/reference/events/list
or use vanilla javascript to sort them.

My take on this is no, the array doesn't guarantee it will be ordered
An event will be returned if it starts during the time range, ends during the time range, or encompasses the time range. If no time zone is specified, the time values are interpreted in the context of the script's time zone, which may be different from the calendar's time zone.
If the data isn't complete it may hinder with what you handle it. Its still best for you to implement a sort

I was having this problem as well. Instead of going with the overkill Calendar Advanced Service, I wrote a simple sorter for arrays of CalendarEvent objects.
// copy this to the bottom of your script, then call it on your array of CalendarEvent objects that you got from the CalendarApp
//
// ex:
// var sortedEvents = sortArrayOfCalendarEventsChronologically(events);
// or
// events = sortArrayOfCalendarEventsChronologically(events);
function sortArrayOfCalendarEventsChronologically(array) {
if (!array || array.length == 0) {
return 0;
}
var temp = [];
for (var i in array) {
var startTime = new Date(array[i].getStartTime());
var startTimeMilli = startTime.getTime();
for (var j in temp) {
var iteratorStartTime = temp[j].getStartTime();
var iteratorStartTimeMilli = iteratorStartTime.getTime();
if (startTimeMilli < iteratorStartTimeMilli) {
break;
}
}
temp.splice(j, 0, array[i]);
}
return temp;
}
https://gist.github.com/xd1936/0d2b2222c068e4cbbbfc3a84edf8f696

Related

Error Cannot convert Array to (class)[] in making a recurring calendar event with Google Apps Script

I am trying to create a recurring event in a Google Calendar but I keep getting the following error: Cannot convert Array to (class)[]
The problem lies in that I am trying to grab data from a cell to fill in the class. The code is the following:
var recur4 = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeeks([rep]);
var ne4 = c.createAllDayEventSeries(title, start, recur4, options);
Now, the variable rep is equal to cell H2 which has the following text in it: 31,36
When I put Logger.log(rep); it outputs 31,36 so there is no problem there either.
When I take out rep and put in 31,36 in the brackets, the script works perfectly and adds the events to the calendar, so I know that the problem is not anywhere else in the script.
I suppose that the problem has to do with the formatting in the cell, but I have no idea. Any help would be appreciated.
UPDATE
OK so based on the comment below, I changed the script to the following:
var sp = rep.split(",");
for(var i=0; i<sp.length; i++) { sp[i] = +sp[i]; }
var recur4 = CalendarApp.newRecurrence().addWeeklyRule().onlyOnWeeks(sp);
var ne4 = c.createAllDayEventSeries(title, start, recur4, options);
This got rid of the error, BUT now it is adding events every Friday. In the debugger, it now shows that the array is an integer array and comes out like this: [31,36] which should represent the two weeks I need, but something still does not work and the recur4 remains as undefined instead of an object.
UPDATE
Based on the comments that people gave below, the final script that worked fine was the following:
var recur4 = CalendarApp.newRecurrence().addYearlyRule().onlyOnWeeks(rep.split(",")).onlyOnWeekday(CalendarApp.Weekday.FRIDAY);
var ne4 = c.createEventSeries(title, start, stop, recur4, options);
The issue you have with your EventRecurrence specification is that you are specifying that this event should repeat weekly, but then use a restriction that is incompatible with a weekly restriction.
If you describe your condition with words, note that you cannot avoid saying "year". This is a strong indication that perhaps your recurrence period is incorrect.
E.g. "repeat this event every week, on weeks 31 and 36 of the year" vs. "repeat this event every year, on weeks 31 and 36"
Indeed, changing your restriction from weekly to yearly results in a valid RecurrenceRule:
var recur = CalendarApp.newRecurrence()
.addYearlyRule()
.onlyOnWeeks(rep.split(",").map(
function (week) {
return parseInt(week, 10);
})
);
References:
onlyOnWeeks
addYearlyRule
PS: the EventRecurrence and RecurrenceRule classes are pretty much interchangeable.

Using DocumentApp's append functions to 'MailMerge' data into single Google Document

Update: I have updated my code with some of the suggestions as well as a feature that allows for easy multiple markers and changed the arrayOfData into a 2D Array or strings. Has similar runtimes, if not slightly slower - 50pg avg: 12.499s, 100pg avg: 21.688s, per page avg: 0.233s.
I am writing a script that takes some data and a template and performs a 'mail merge' type function into another document. The general idea for this is easy and I can do it no problem.
However, I am currently 'mail merging' many rows (150-300+) of ~5 columns of data into predefined fields of a single page template (certificates) into a single document. The result is a single Google Document with 150 - 300 pages of the certificate pages. The alternative is to generate many documents and, somehow, combine them.
Is This a Good/Efficient Way of Doing This?
It took me a while to work out put together this example from the documentation alone as I couldn't find anything online. I feel like there should be a simpler way to do this but can not find anything close to it (ie. appending a Body to a Body). Is this the best way to do get this functionality right now?
Edit: What about using bytes from the Body's Blob? I'm not experienced with this but would it work faster? Though then the issue becomes replacing text without generating many Documents before converting to Blobs?
*Note: I know Code Review exists, but they don't seem to have many users who understand Google Apps Script well enough to offer improvement. There is a much bigger community here! Please excuse it this time.
Here Is My Code (Updated Feb 23, 2018 # 3:00PM PST)
Essentially it takes each child element of the Body, replaces some fields, then detects its type and appends it using the appropriate append function.
/* Document Generation Statistics:
* 50 elements replaced:
* [12.482 seconds total runtime]
* [13.272 seconds total runtime]
* [12.069 seconds total runtime]
* [12.719 seconds total runtime]
* [11.951 seconds total runtime]
*
* 100 elements replaced:
* [22.265 seconds total runtime]
* [21.111 seconds total runtime]
*/
var TEMPLATE_ID = "Document_ID";
function createCerts(){
createOneDocumentFromTemplate(
[
['John', 'Doe'], ['Jane', 'Doe'], ['Jack', 'Turner'], ['Jordan', 'Bell'],['Lacy', 'Kim']
],
["<<First>>","<<Last>>"]);
}
function createOneDocumentFromTemplate(arrayOfData, arrayOfMarkers) {
var file = DriveApp.getFileById(TEMPLATE_ID).makeCopy("Certificates");
var doc = DocumentApp.openById(file.getId());
var body = doc.getBody();
var fixed = body.copy();
body.clear();
var copy;
for(var j=0; j<arrayOfData.length;j++){
var item = arrayOfData[j];
copy = fixed.copy();
for (var i = 1; i < copy.getNumChildren() - 1; i++) {
for(var k=0; k<arrayOfMarkers.length; k++){
copy.replaceText(arrayOfMarkers[k], item[k]);
}
switch (copy.getChild(i).getType()) {
case DocumentApp.ElementType.PARAGRAPH:
body.appendParagraph(copy.getChild(i).asParagraph().copy());
break;
case DocumentApp.ElementType.LIST_ITEM:
body.appendListItem(copy.getChild(i).asListItem().copy());
break;
case DocumentApp.ElementType.TABLE:
body.appendTable(copy.getChild(i).asTable().copy());
break;
}
}
}
doc.saveAndClose();
return doc;
}
Gist
This is more of a Code Review question, but no, as written, I don't see any way to make it more efficient. I run a similar script for creating documents at work, though mine creates separate PDF files to share with the user rather than creating something we would print. It may save you time and effort to look into an AddOn like docAppender (if you're coming from a form) or autoCrat.
A couple of suggestions:
I'm more of a for loop person because it's easier to log errors on particular rows with the indexing variable. It's also more efficient if you're pulling from a spreadsheet where some rows could be skipped (already merged, let's say). Using forEach gives more readable code and is good if you always want to go over the entire array, but is less flexible with conditionals. Using a for loop will also allow you to set a row as merged with a boolean variable in the last column.
The other thing I can suggest would be to use some kind of time-based test to stop execution before you time the script out, especially if you're merging hundreds of rows of data.
// Limit script execution to 4.5 minutes to avoid execution timeouts
// #param {Object} - Date object from loop
// return Boolean
function isTimeUp_(starttime) {
var now = new Date();
return now.getTime() - starttime.getTime() > 270000; // 4.5 minutes
}
Then, inside your function:
var starttime = new Date();
replace.forEach(...
// include this line somewhere before you begin writing data
if (isTimeUp_(starttime )) {
Logger.log("Time up, finished on row " + i);
break;
}
... // rest of your forEach()

Why is my caching inconsistent?

I am trying setup caching for a spreadsheet custom funciton but the results seem to be inconsistent/unexpected. Sometimes I get the cached results, sometimes it refreshes the data. I've set the timeout to 10 seconds, and when I refresh within 10 seconds, sometimes it grabs new data, sometimes it caches. Even after waiting more than 10 seconds since last call, sometimes I get the cached results. Why is there so much inconsistency in the spreadsheet function? (or am I just doing something wrong?). When I call the function directly within the actual script, it seems to be much more consistent but sometimes I get inconsistenties/unexpected results.
function getStackOverflow(){
var cache = CacheService.getPublicCache();
var cached = cache.get("stackoverflow");
if(cached != null) {
Logger.log('this is cached');
return 'this is cached version';
}
// Fetch the data and create an object.
var result = UrlFetchApp.fetch('http://api.stackoverflow.com/1.1/tags/google-apps-script/top-answerers/all-time');
var json = Utilities.jsonParse(result.getContentText()).top_users;
var rows = [],data;
for (i = 0; i < json.length; i++) {
data = json[i].user;
rows.push(data.display_name);
}
Logger.log("This is a refresh");
cache.put("stackoverflow",JSON.stringify(rows),10);
return rows;
}
You cant use custom functions like that. Its documented.
Custom functions must be deterministic, they have always the same output given fhe same input (in your case none since you are passing no parameters.
the spreadsheet will remember the values for each input set, basically like a second layer of cache that yiu have no control.

Need a way to parametize an array sort function, with parameters set in a handler

I'm writing a web app that displays a subset of rows from a spreadsheet worksheet. For the convenience of users, the data is presented in a grid, each row selected from the spreadsheet forms a row in the grid. The number of rows relevant to each user grows over time.
The header of the grid is a set of buttons, which allow the user to control the sort order of the data. So if the user clicks the button in the header of the first column, the array that populates the grid is sorted by the first column and so forth. If you click the same column button twice, the sort order reverses.
I used script properties to communicate the selected sort field and order between the handler responding to the button presses, and the order function called by the sort.
So in doGet():
// Sort Field and Order
ScriptProperties.setProperties({"sortField": "date","sortOrder": "asc"});
And in the button handler:
function sortHandler(event) {
var id = event.parameter.source;
if (id === ScriptProperties.getProperty("sortField")) {
ScriptProperties.setProperty("sortOrder", (ScriptProperties.getProperty("sortOrder") === "asc")?"desc":"asc");
} else {
ScriptProperties.setProperties({"sortField": id, "sortOrder": "asc"});
}
var app = UiApp.getActiveApplication();
app.remove(app.getElementById("ScrollPanel"));
createForm_(app);
return app;
}
And the order function itself (this is invoked by a sort method on the array: array.sort(order); in the code that defines the grid):
function orderFunction(a, b) {
var sortParameter = ScriptProperties.getProperties();
var asc = sortParameter.sortOrder === "asc";
switch(sortParameter.sortField) {
case "date":
var aDate = new Date(a.date);
var bDate = new Date(b.date);
return (asc)?(aDate - bDate):(bDate - aDate);
case "serviceno":
return (asc)?(a.serviceno-b.serviceno):(b.serviceno-a.serviceno);
default: // lexical
var aLex = String(a[sortParameter.sortField]).toLowerCase();
var bLex = String(b[sortParameter.sortField]).toLowerCase();
if (aLex < bLex) {
return (asc)?-1:1;
} else if (aLex > bLex) {
return (asc)?1:-1;
} else {
return 0;
}
}
}
The fly in the ointment with this design is Google. Once the array gets to a certain size, the sort fails with an error that the Properties service is being called too frequently. The message suggests inserting a 1s delay using Utilities.sleep(), but the grid already takes a long time to render already - how is it going to go if the array takes 1s to decide the order of two values?
I tried reimplementing with ScriptDB, but that suffers the same problem, calls to ScriptDB service are made too frequently for Google's liking.
So how else can I implement this, without the orderFunction accessing any App Script services?
If you are looking for temporary storage, I prefer CacheService to ScriptProperties.
Use
CacheService.getPrivateCache().put and
CacheService.getPrivateCache().get
You could also use a hidden widget to pass information between the doGet an handler functions, it will also be much faster than calling the scriptProperties service (have a look at the execution transcript to see how long it takes, you'll be surprised)
On the other hand if you really want to keep using it you can also try to (slightly) reduce the number of calls to the scriptProperties service, for example in your sortHandler(event) function :
function sortHandler(event) {
var id = event.parameter.source;
var sortField = ScriptProperties.getProperty("sortField");
var sortOrder = ScriptProperties.getProperty("sortOrder");
if (id === sortField) {
ScriptProperties.setProperty("sortOrder", (sortOrder === "asc")?"desc":"asc");
} else {
ScriptProperties.setProperties({"sortField": id, "sortOrder": "asc"});
}
var app = UiApp.getActiveApplication();
app.remove(app.getElementById("ScrollPanel"));
createForm_(app);
return app;
}
...how else can I implement this?
Use HTMLService with jQuery DataTable which does all the ordering that you can imagine and a whole lot more without scripting button press logic and the like. There is an example of the spreadsheet to basic table here http://davethinkingaloud.blogspot.co.nz/2013/03/jsonp-and-google-apps-script.html

Script to permute columns, rows or any ranges

EDIT: I changed the code to include possibility of providing ranges by name (in A1 notation) as this could be potentially more efficient than providing Range object (if the range ends up not moved) and for sure is easier to use in simple cases. Idea by AdamL (see answers bellow).
In some spreadsheets I need to permute rows or columns. Requiring user to do this manually isn't very nice. So making proper commands in menu which would run script seemed a reasonable solution.
Oddly I wasn't able to find any function (either build in or wrote by someone else) which would permute rows/columns. So I wrote one myself and then considered publishing it. But since my experience with JavaScript and Google Apps Script is low I wanted to have someone else check on this function. Also I have some questions. So here we go.
// Parameters:
// - ranges An Array with ranges which contents are to be permuted.
// All the ranges must have the same size. They do not have to be
// vectors (rows or columns) and can be of any size. They may come from
// different sheets.
// Every element of the array must be either a Range object or a string
// naming the range in A1 notation (with or without sheet name).
// - permutation An Array with 0-based indexes determining desired permutation
// of the ranges. i-th element of this array says to which range
// should the contents of i-th range be moved.
// - temp A range of the same size as the ranges in "ranges". It is used to
// temporarily store some ranges while permuting them. Thus the initial
// contents of this range will be overwritten and its contents on exit is
// unspecified. Yet if there is nothing to be moved ("ranges" has less
// than 2 elements or all ranges are already on their proper places) this
// range will not be used at all.
// It is advised to make this range hidden so the "garbage" doesn't
// bother user.
// This can be either a Range object or a string naming the range in A1
// notation (with or without sheet name) - just as with the "ranges".
// - sheet An optional Sheet object used to resolve range names without sheet
// name. If none is provided active sheet is used. Note however that it
// may cause issues if user changes the active sheet while the script is
// running. Thus if you specify ranges by name without sheet names you
// should provide this argument.
//
// Return Value:
// None.
//
// This function aims at minimizing moves of the ranges. It does at most n+m
// moves where n is the number of permuted ranges while m is the number of
// cycles within the permutation. For n > 0 m is at least 1 and at most n. Yet
// trivial 1-element cycles are handled without any moving (as there is nothing
// to be moved) so m is at most floor(n/2).
//
// For example to shift columns A, B and C by 1 in a cycle (with a temp in
// column D) do following:
//
// permuteRanges(
// ["A1:A", "B1:B", "C1:C"],
// [1, 2, 0],
// "D1:D",
// SpreadsheetApp.getActiveSheet()
// );
function permuteRanges(ranges, permutation, temp, sheet) {
// indexes[i] says which range (index of ranges element) should be moved to
// i-th position.
var indexes = new Array(permutation.length);
for(var i = 0; i < permutation.length; ++i)
indexes[permutation[i]] = i;
// Generating the above array is linear in time and requires creation of a
// separate array.
// Yet this allows us to save on moving ranges by moving most of them to their
// final location with only one operation. (We need only one additional move
// to a temporary location per each non-trivial cycle.)
// Range extraction infrastructure.
// This is used to store reference sheet once it will be needed (if it will be
// needed). The reference sheet is used to resolve ranges provided by string
// rather than by Range object.
var realSheet;
// This is used to store Range objects extracted from "ranges" on
// corresponding indexes. It is also used to store Range object corresponding
// to "temp" (on string index named "temp").
var realRanges;
// Auxiliary function which for given index obtains a Range object
// corresponding to ranges[index] (or to temp if index is "temp").
// This allows us to be more flexible with what can be provided as a range. So
// we accept both direct Range objects and strings which are interpreted as
// range names in A1 notation (for the Sheet.getRange function).
function getRealRange(index) {
// If realRanges wasn't yet created (this must be the first call to this
// function then) create it.
if(!realRanges) {
realRanges = new Array(ranges.length);
}
// If we haven't yet obtained the Range do it now.
if(!realRanges[index]) {
var range;
// Obtain provided range depending on whether index is "temp" or an index.
var providedRange;
if(index === "temp") {
providedRange = temp;
} else {
providedRange = ranges[index];
}
// If corresponding "ranges" element is a string we have to obtain the
// range from a Sheet...
if(typeof providedRange === "string") {
// ...so we have to first get the Sheet itself...
if(!realSheet) {
// ...if none was provided by the caller get currently active one. Yet
// note that we do this only once.
if(!sheet) {
realSheet = SpreadsheetApp.getActiveSheet();
} else {
realSheet = sheet;
}
}
range = realSheet.getRange(providedRange);
} else {
// But if the corresponding "ranges" element is not a string then assume
// it is a Range object and use it directly.
range = providedRange;
}
// Store the Range for future use. Each range is used twice (first as a
// source and then as a target) except the temp range which is used twice
// per cycle.
realRanges[index] = range;
}
// We already have the expected Range so just return it.
return realRanges[index];
}
// Now finally move the ranges.
for(var i = 0; i < ranges.length; ++i) {
// If the range is already on its place (because it was from the start or we
// already moved it in some previous cycle) then don't do anything.
// Checking this should save us a lot trouble since after all we are moving
// ranges in a spreadsheet, not just swapping integers.
if(indexes[i] == i) {
continue;
}
// Now we will deal with (non-trivial) cycle of which the first element is
// i-th. We will move the i-th range to temp. Then we will move the range
// which must go on the (now empty) i-th position. And iterate the process
// until we reach end of the cycle by getting to position on which the i-th
// range (now in temp) should be moved.
// Each time we move a range we mark it in indexes (by writing n on n-th
// index) so that if the outer for loop reaches that index it will not do
// anything more with it.
getRealRange(i).moveTo(getRealRange("temp"));
var j = i;
while(indexes[j] != i) {
getRealRange(indexes[j]).moveTo(getRealRange(j));
// Swap index[j] and j itself.
var old = indexes[j];
indexes[j] = j;
j = old;
}
getRealRange("temp").moveTo(getRealRange(j));
// No need to swap since j will not be used anymore. Just write to indexes.
indexes[j] = j;
}
}
The questions are:
Is this properly implemented? Can it be improved?
How about parameters validation? Should I do it? What should I do if they are invalid?
I wasn't sure whether to use copyTo or moveTo. I decided on moveTo as it seemed to me more what I intended to do. But now in second thoughts I think that maybe copyTo would be more efficient.
Also I noticed that the Range moved from not always is cleared. Especially when in Debugger.
Undo/redo seems to be an issue with this function. It seems that every moveTo is a separate operation (or even worse, but maybe that was just a low responsiveness of the Google Docs when I was testing) on the spreadsheet and undoing the permutation is not a single action. Can anything be done about it?
The documentation I wrote for the function claims that it works across different sheets or even different spreadsheets. I haven't actually checked that ;) but Google Apps Script documentation doesn't seem to deny it. Will it work that way?
I'm not sure whether this is a proper place to ask such questions (since this is not truly a question) but since Google Apps Script community support is moving to Stack Overflow I didn't knew where else to ask.
Don't you think it might be more efficient in terms of execution speed to do it with arrays ?
try this for example : (I added logs everywhere to show what happens)
(Note also that sheets are limited to 255 columns... take care of the list length)
function permutation() {
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var lr = ss.getLastRow()
var lc=ss.getLastColumn();
var data = sh.getRange(1,1,lr,lc).getValues()
Logger.log(data)
var temp2= new Array();
var h=data.length
Logger.log(h)
var w=data[0].length
Logger.log(w)
for(nn=0;nn<w;++nn){
var temp1= new Array();
for (tt=0;tt<h;++tt){
temp1.push(data[tt][nn])
}
temp2.push(temp1)
}
Logger.log(temp2)
Logger.log(temp2.length)
Logger.log(temp2[0].length)
sh.getRange(1,1,lr,lc).clear()
sh.getRange(1,1,lc,lr).setValues(temp2)
}
best regards,
Serge
Adam, from my limited experience on the Apps Script GPF, I have learned that it is best to limit get and set calls as much as possible (and you could include moveTo/copyTo in that as well).
Do you think it would be better to pass the range names, rather than the ranges, as parameters (and to that end, you might need a mechanism to pass sheet names and spreadsheet keys as well, to support your requirement of working across different sheets/spreadsheets), and then trivial "getRange's" can be avoided as well as a trivial "moveTo's".
Also, if you are just transferring values only, it would probably be better to not move it to a temporary range but rather assign those arrays to a variable in the script which can then be later "set" in the correct spot. But if you need to copy over formats or formulae, that's a different story.