title: Merging multiple Google calendar feeds into one JSON object in javascript
body: <p>I am trying to bring in the JSON feeds from multiple Google calendars so that I can sort the upcoming events and display the next X number in an "Upcoming Events" list.</p>
  
  <p>I have this working with Yahoo! pipes but I want to get away from using a 3rd party to aggregate. I think I am close, but I cannot get the JSON objects created correctly. I am getting the data into the array but not in JSON format, so I can't manipulate it. </p>
  
  <p>I have tried <code>var myJsonString = JSON.stringify(JSONData);</code> using <a href="https://github.com/douglascrockford/JSON-js" rel="nofollow">https://github.com/douglascrockford/JSON-js</a> but that just threw errors. I suspect because my variable is in the wrong starting format. I have tried just calling the feed like: <code>$.getJSON(url);</code> and creating a <code>function concant1()</code> to do the <code>JSONData=JSONData.concat(data);</code>, but it doesn't fire and I think it would produce the same end result anyway. I have also tried several other methods of getting the end result I want with varying degrees of doom. Here is the closest I have come so far:</p>
  
  <pre><code>var JSONData = new Array();
  var urllist = ["https://www.google.com/calendar/feeds/dg61asqgqg4pust2l20obgdl64%40group.calendar.google.com/public/full?orderby=starttime&amp;max-results=3&amp;sortorder=ascending&amp;futureevents=true&amp;ctz=America/New_York&amp;singleevents=true&amp;alt=json&amp;callback=concant1","https://www.google.com/calendar/feeds/5oc3kvp7lnu5rd4krg2skcu2ng%40group.calendar.google.com/public/full?orderby=starttime&amp;max-results=3&amp;sortorder=ascending&amp;futureevents=true&amp;ctz=America/New_York&amp;singleevents=true&amp;alt=json&amp;callback=concant1","http://www.google.com/calendar/feeds/rine4umu96kl6t46v4fartnho8%40group.calendar.google.com/public/full?orderby=starttime&amp;max-results=3&amp;sortorder=ascending&amp;futureevents=true&amp;ctz=America/New_York&amp;singleevents=true&amp;alt=json&amp;callback=concant1"];
  
  urllist.forEach(function addFeed(url){
      alert("The URL being used: "+ url);
      if (void 0 != JSONData){JSONData=JSONData.concat($.getJSON(url));}
      else{JSONData = $.getJSON(url);}
      alert("The count from concantonated JSONData: "+JSONData.length); 
  });
  
  document.write("The final count from JSONData: "+JSONData.length+"&lt;p&gt;");
  console.log(JSONData)
  </code></pre>
  
  <p><strong>UPDATE:</strong> 
  Now with full working source!! :) If anyone would like to make suggestions on how to improve the code's efficiency it would be gratefully accepted. I hope others find this useful.:</p>
  
  <pre><code>// GCal MFA - Google Calendar Multiple Feed Aggregator
  // Useage: GCalMFA(CIDs,n); 
  // Where 'CIDs' is a list of comma seperated Google calendar IDs in the format: id@group.calendar.google.com, and 'n' is the number of results to display.
  // While the contained console.log(); outputs are really handy for testing, you will probably want to remove them for regular usage
  // Author: Jeramy Kruser - http://jeramy.kruser.me
  
  // This is error-checking code for IE and can be removed
  // onerror=function (d, f, g){alert (d+ "\n"+ f+ "\n");}
  // This keeps IE from complaining about console.log and can be removed if all the console.log testing statements are removed
  // if (!window.console) {console = {log: function() {}};}
  
  // Add a tag to your page to identify it as js-enabled for CSS purposes
  document.body.className += ' js-enabled';
  
  // Global variables
  var urllist = [];
  var maxResults = 3; // The default is 3 results unless a value is sent
  var JSONData = {};
  var eventCount = 0;
  var errorLog = "";
  
  JSONData = { count: 0,
      value : {
      description: "Aggregates multiple Google calendar feeds into a single sorted list",
      generator: "StackOverflow communal coding - Thanks for the assist Patrick M",
      website: "http://jeramy.kruser.me",
      author: "Jeramy &amp; Kasey Kruser",
      items: []
  }};
  
  // Prototype forEach required for IE
  if ( !Array.prototype.forEach ) {
    Array.prototype.forEach = function(fn, scope) {
      for(var i = 0, len = this.length; i &lt; len; ++i) {
        fn.call(scope, this[i], i, this);
      }
    }
  }
  
  // For putting dates from feed into a format that can be read by the Date function for calculating event length.
  function parse (str) {
          // validate year as 4 digits, month as 01-12, and day as 01-31 
          str = str.match (/^(\d{4})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/);
          if (str) {
             // make a date
             str[0] = new Date ( + str[1], + str[2] - 1, + str[3]);
             // check if month stayed the same (ie that day number is valid)
             if (str[0].getMonth () === + str[2] - 1) {
                return str[0];
                }
          }
          return undefined;
   }
  
  //For outputting to HTML
  function output() {
      var months, day_in_ms, summary, i, item, eventlink, title, calendar, where,dtstart, dtend, endyear, endmonth, endday, startyear, startmonth, startday, endmonthdayyear, eventlinktitle, startmonthday, length, curtextval, k;
      // Array of month names from numbers for page display.
      months = {'0':'January', '1':'February', '2':'March', '3':'April', '4':'May', '5':'June', '6':'July', '7':'August', '8':'September', '9':'October', '10':'November', '11':'December'};
      // For use in calculating event length.
      day_in_ms = 24 * 60 * 60 * 1000;
      // Instantiate HTML Arrays.
      summary = [];
      for (i = 0; i &lt; maxResults; i+=1 ) {
          // console.log("i: "+i+" &lt; "+"maxResults: "+ maxResults);
          if (!(JSONData.value.items[i] === undefined)) {     
              item = JSONData.value.items[i];
              // Grabbing data for each event in the feed.
              eventlink = (item.link[0].href);
              title = item.title.$t; 
              // Only display the calendar title if there is more than one 
              calendar = "";
              if (urllist.length &gt; 1) {
                  calendar = '&lt;br /&gt;Calendar: &lt;a href="https://www.google.com/calendar/embed?src=' + item.gd$who[0].email + '&amp;ctz=America/New_York"&gt;' + item.gd$who[0].valueString + '&lt;\/a&gt; (&lt;a href="https://www.google.com/calendar/ical/' + item.gd$who[0].email + '/public/basic.ics"&gt;iCal&lt;\/a&gt;)'; 
              }
              // Grabbing event location, if entered.
              if ( item.gd$where[0].valueString !== "" ) {
                  where = '&lt;br /&gt;' + (item.gd$where[0].valueString); 
                  }
              else {
                  where = (""); 
                  }
              // Grabbing start date and putting in form YYYYmmdd. Subtracting one day from dtend without a specified end time (which contains colons) to fix Google's habit of ending an all-day event at midnight on the following day.
              dtstart = new Date(parse(((item.gd$when[0].startTime).substring(0,10)).replace(/-/g,"")));
              if((item.gd$when[0].endTime).indexOf(':') === -1) {
                    dtend = new Date(parse(((item.gd$when[0].endTime).substring(0,10)).replace(/-/g,"")) - day_in_ms);
                  }
              else {
                    dtend = new Date(parse(((item.gd$when[0].endTime).substring(0,10)).replace(/-/g,"")));
                  }
              // Put dates in pretty form for display.
              endyear = dtend.getFullYear();  
              endmonth = months[dtend.getMonth()];
              endday = dtend.getDate();
              startyear = dtstart.getFullYear();  
              startmonth = months[dtstart.getMonth()];
              startday = dtstart.getDate();
              //consolidate some much-used variables for HTML output.
              endmonthdayyear = endmonth + ' ' + endday + ', ' + endyear;
              eventlinktitle = '&lt;a href="' + eventlink + '"&gt;' + title + '&lt;\/a&gt;';
              startmonthday = startmonth + ' ' + startday;
              // Calculates the number of days between each event's start and end dates. 
              length = ((dtend - dtstart) / day_in_ms);
              // HTML for each event, depending on which div is available on the page (different HTML applies). Only one div can exist on any one page. 
              if (document.getElementById("homeCalendar") !== null ) {
                  // If the length of the event is greater than 0 days, show start and end dates. 
                  if ( length &gt; 0 &amp;&amp; startmonth !== endmonth &amp;&amp; startday === endday ) {       
                          summary[i] = ('&lt;h3&gt;' + eventlink + '"&gt;' + startmonthday + ', ' + startyear + ' - ' + endmonthdayyear + '&lt;\/a&gt;&lt;\/h3&gt;&lt;p&gt;' + title + '&lt;\/p&gt;'); }
                      // If the length of the event is greater than 0 and begins and ends within the same month, shorten the date display.
                      else if ( length &gt; 0 &amp;&amp; startmonth === endmonth &amp;&amp; startyear === endyear ) {        
                          summary[i] = ('&lt;h3&gt;&lt;a href="' + eventlink + '"&gt;' + startmonthday + '-' + endday + ', ' + endyear + '&lt;\/a&gt;&lt;\/h3&gt;&lt;p&gt;' + title + '&lt;\/p&gt;'); }
                      // If the length of the event is greater than 0 and begins and ends within different months of the same year, shorten the date display.
                      else if ( length &gt; 0 &amp;&amp; startmonth !== endmonth &amp;&amp; startyear === endyear ) {        
                          summary[i] = ('&lt;h3&gt;&lt;a href="' + eventlink + '"&gt;' + startmonthday + ' - ' + endmonthdayyear + '&lt;\/a&gt;&lt;\/h3&gt;&lt;p&gt;' + title + '&lt;\/p&gt;'); }
                      // If the length of the event is less than one day (length &lt; = 0), show only the start date.
                      else { 
                          summary[i] = ('&lt;h3&gt;&lt;a href="' + eventlink + '"&gt;' + startmonthday + ', ' + startyear + '&lt;\/a&gt;&lt;\/h3&gt;&lt;p&gt;' + title + '&lt;\/p&gt;'); }
              }        
              else if (document.getElementById("allCalendar") !== null ) {
                  // If the length of the event is greater than 0 days, show start and end dates. 
                   if ( length &gt; 0 &amp;&amp; startmonth !== endmonth &amp;&amp; startday === endday ) {           
                             summary[i] = ('&lt;li&gt;' + eventlinktitle + '&lt;br /&gt;' + startmonthday + ', ' + startyear + ' - ' + endmonthdayyear + where + calendar + '&lt;\/li&gt;'); } 
                        // If the length of the event is greater than 0 and begins and ends within the same month, shorten the date display. 
                        else if ( length &gt; 0 &amp;&amp; startmonth === endmonth &amp;&amp; startyear === endyear ) {           
                             summary[i] = ('&lt;li&gt;' + eventlinktitle + '&lt;br /&gt;' + startmonthday + '-' + endday + ', ' + endyear + where + calendar + '&lt;\/li&gt;'); } 
                        // If the length of the event is greater than 0 and begins and ends within different months of the same year, shorten the date display. 
                        else if ( length &gt; 0 &amp;&amp; startmonth !== endmonth &amp;&amp; startyear === endyear ) {           
                             summary[i] = ('&lt;li&gt;' + eventlinktitle + '&lt;br /&gt;' + startmonthday + ' - ' + endmonthdayyear + where + calendar + '&lt;\/li&gt;'); } 
                        // If the length of the event is less than one day (length &lt; = 0), show only the start date. 
                        else {  
                             summary[i] = ('&lt;li&gt;' + eventlinktitle + '&lt;br /&gt;' + startmonthday + ', ' + startyear + where + calendar + '&lt;\/li&gt;'); }
              } 
          }
          if (summary[i] === undefined) { summary[i] = "";}
          // console.log(summary[i]);
      }
      // console.log(JSONData);
      // Puts the HTML into the div with the appropriate id. Each page can have only one.
      if (document.getElementById("homeCalendar") !== null ) {
          curtextval = document.getElementById("homeCalendar");
          // console.log("homeCalendar: "+curtextval);
          }
      else if (document.getElementById("oneCalendar") !== null ) {
          curtextval = document.getElementById("oneCalendar");
          // console.log("oneCalendar: "+curtextval);
           }
      else if (document.getElementById("allCalendar") !== null ) {
          curtextval = document.getElementById("allCalendar");
          // console.log("allCalendar: "+curtextval.innerHTML);
           }
      for (k = 0; k&lt;maxResults; k+=1 ) { curtextval.innerHTML = curtextval.innerHTML + summary[k]; }
      if (JSONData.count === 0) {
          errorLog += '&lt;div id="noEvents"&gt;No events found.&lt;/div&gt;';
      }
      if (document.getElementById("homeCalendar") === null ) {
          curtextval.innerHTML = '&lt;ul&gt;' + curtextval.innerHTML + '&lt;\/ul&gt;'; 
          }
      if (errorLog !== "") {
          curtextval.innerHTML += errorLog;
          }
  }
  
  // For taking in each feed, breaking out the events and sorting them into the object by date
  function sortFeed(event) {  
      var tempEntry, i;
      tempEntry = event;
      i = 0;
      // console.log("*** New incoming event object #"+eventCount+" ***");
      // console.log(event.title.$t);
      // console.log(event);
      // console.log("i = " + i + " and maxResults " + maxResults);
      while(i&lt;maxResults) {
          // console.log("i = " + i + " &lt; maxResults " + maxResults);
          // console.log("Sorting event = " + event.title.$t + " by date of " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,""));
          if (JSONData.value.items[i]) {
              // console.log("JSONData.value.items[" + i + "] exists and has a startTime of " + JSONData.value.items[i].gd$when[0].startTime.substring(0,10).replace(/-/g,""));
              if (event.gd$when[0].startTime.substring(0,10).replace(/-/g,"")&lt;JSONData.value.items[i].gd$when[0].startTime.substring(0,10).replace(/-/g,"")) {
                  // console.log("The incoming event value of " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,"") + " is &lt; " + JSONData.value.items[i].gd$when[0].startTime.substring(0,10).replace(/-/g,""));
                  tempEntry = JSONData.value.items[i];
                  // console.log("Existing JSONData.value.items[" + i + "] value " + JSONData.value.items[i].gd$when[0].startTime.substring(0,10).replace(/-/g,"") + " stored in tempEntry");
                  JSONData.value.items[i] = event;
                  // console.log("Position JSONData.value.items[" + i + "] set to new value: " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,""));
                  event = tempEntry;
                  // console.log("Now sorting event = " + event.title.$t + " by date of " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,""));
              }
              else {
                  // console.log("The incoming event value of " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,"") + " is &gt; " + JSONData.value.items[i].gd$when[0].startTime.substring(0,10).replace(/-/g,"") + " moving on...");
              }
          }
          else {
              JSONData.value.items[i] = event;
              // console.log("JSONData.value.items[" + i + "] does not exist so it was set to the Incoming value of " + event.gd$when[0].startTime.substring(0,10).replace(/-/g,""));
              i = maxResults;
          }
          i += 1;
      }
  }
  
  // For completing the aggregation
  function complete(result) {
      // Track the number of calls completed back, we're not done until all URLs have processed
      if( complete.count === undefined ){
          complete.count = urllist.length;        
      }
      // console.log("complete.count = "+complete.count);
      // console.log(result.feed);
      if(result.feed.entry){
          JSONData.count = maxResults;
          // Check each incoming item against JSONData.value.items
          // console.log("*** Begin Sorting " + result.feed.entry.length + " Events ***");
          // console.log(result.feed.entry);
          result.feed.entry.forEach(
              function(event){
                  eventCount += 1;
                  sortFeed(event);                
              }
          );
      }
      if( (complete.count-=1)&lt;1 ) {
          // console.log("*** Done Sorting ***");
          output();
      }
  }
  
  // This is the main function. It takes in the list of Calendar IDs and the number of results to display
  function GCalMFA(list,results){
          var i, calPreProperties, calPostProperties1, calPostProperties2;
          calPreProperties = "https://www.google.com/calendar/feeds/";
          calPostProperties1 = "/public/full?max-results=";
          calPostProperties2 = "&amp;orderby=starttime&amp;sortorder=ascending&amp;futureevents=true&amp;ctz=America/New_York&amp;singleevents=true&amp;alt=json&amp;callback=?";
  
          if (list) {
              if (results) {
                  maxResults = results;
              }
              urllist = list.split(',');
              for (i = 0; i &lt; urllist.length; i+=1 ){
                  // console.log(urllist[i]);
                  if (urllist[i] === ""){ urllist.splice(i,1);}
                  else{
                  urllist[i] = calPreProperties + urllist[i] + calPostProperties1+maxResults+calPostProperties2;}
              }
              // console.log("There are " + urllist.length + " URLs");
              urllist.forEach(function addFeed(url){
                  $.getJSON(url, complete);   
              });
          }
          else {
              errorLog += '&lt;div id="noURLs"&gt;No calendars have been selected.&lt;/div&gt;';
              output();
          }
  }
  </code></pre>