0
votes

Attached is a Test Sheet which shows results and expected results. The Test sheet has all of the functions needed to run the sheet.

The main function is supposed to return a range of dates generated from a starting date. Each date should be three months ahead of its preceding date.

The test uses two versions of the function, one using UTC date methods on a date object, the other using normal date methods on a date object.

The sheet named quarter in the "workbook" has the results. Sheet processing tests relies on these Named Ranges: StartDateQ, StartDateWeird.

Anyone who seeks to solve this need only look at the sheet named quarter, the scripts page holding the functions and have working knowledge of Google Apps Script and the Google Spreadsheet API.

The Problem

The array of returned dates are not as expected. In some cases, the dates look right but some of the dates have unwanted time data that can be seen only by selecting a cell. In some cases, the dates are not right, being off by a day.

Why is this a Problem?

In an actual spreadsheet, formulas that rely on date matching fail since some of the visual dates have associated actual time data. Since the time data doesn't match, even though visually, the dates look the same, matching fails. Matching failures break the sheet.

The Algorithm

  1. If there is at least a start date to process, get the number of quarters to generate dates using countAge().
  2. Initialize date object with start date sent from spreadsheet.
  3. Make an empty array to hold dates to return to the spreadsheet.
  4. For the tally of quarters, loop through one count at a time.
  5. Insert the latest date to the array.
  6. Move the date along to the next quarter until there are no more quarters for which to generate a date.
  7. Return the results to the spreadsheet

Functions in Use (internal API)

is(obj, type) //compare if object is the right type

countAge(dateString,dateString2) //count years between dates, aka age

TEST VERSIONS

quartersUTC(startDate,endDate) // custom function to get quarter dates from two dates — UTC Version

quartersPlain(startDate,endDate) // custom function to get quarter dates from two dates — Plain Version

COMMENTARY

Since neither test function relies on "date math", but instead relies on date setters, e.g.,

not this:

d = new Date(); d.setMonth(d.getMonth()) + 3;

but this: if (latestDate.getMonth() == 0) { latestDate.setMonth(3,1) }

it does not seem obvious why the programming logic fails to produce the expected results.

THE FUNCTION

This is the plain version. The UTC version uses .setUTCmonth()

function quartersPlain(startDate,endDate)  {

// Check if there is a date to process, if not, exit.

  if (!is(startDate,"Date")) {
      return "Missing date";
  }


// Let's get the number of quarters  


  /* If there isn't an end date, we want the tally from the start date to now, aka the true age
     otherwise, we want want the tally between two dates.
  */
  var tally = 0; 
  if (!is(endDate,"Date")) {
    tally = (countAge(startDate) * 4) + 3
  } 
  else {

    if (new Date(endDate) > new Date(startDate)) {
      tally = (countAge(startDate,endDate) * 4)
    }
    else {
      tally = (countAge(endDate,startDate) * 4)
      startDate = endDate
    }
  }
  // testing ...
  // return tally



/* 
1. Initialize date object with start date sent from spreadsheet.
2. Make an empty array to hold dates to return to the spreadsheet.
3. For the tally of quarters, loop through one count at a time.
4. Insert the latest date to the array.
5. Move the date along to the next quarter.
*/

  // it's a logical fail needing to assign the date twice, kludgy 
  var latestDate = new Date(startDate);

  /* testing ...
  latestDate.setFullYear(latestDate.getUTCFullYear());
  latestDate.setUTCMonth(latestDate.getUTCMonth());
  latestDate.setDate(latestDate.getUTCDate());  
  */

  var dates = [ ]; 

  for (var loop = 0; loop < tally; loop++) {

    dates.push(new Date(latestDate));


    if (latestDate.getMonth() == 0) {
        latestDate.setMonth(3,1)
    }
    else if (latestDate.getMonth() == 1) {
        latestDate.setMonth(4,1)
    }
    else if (latestDate.getMonth() == 2) {
        latestDate.setMonth(5,1)
    }    
    else if (latestDate.getMonth() == 3) {
        latestDate.setMonth(6,1)
    }    
    else if (latestDate.getMonth() == 4) {
        latestDate.setMonth(7,1)
    }    
    else if (latestDate.getMonth() == 5) {
        latestDate.setMonth(8,1)
    }    
    else if (latestDate.getMonth() == 6) {
        latestDate.setMonth(9,1)
    }    
    else if (latestDate.getMonth() == 7) {
        latestDate.setMonth(10,1)
    }    
    else if (latestDate.getMonth() == 8) {
        latestDate.setMonth(11,1)
    }    
    else if (latestDate.getMonth() == 9) {
        latestDate.setMonth(0,1)
        latestDate.setFullYear(latestDate.getFullYear() + 1)
    }    
    else if (latestDate.getMonth() == 10) {
        latestDate.setMonth(1,1)
        latestDate.setFullYear(latestDate.getFullYear() + 1)
    } 
    else if (latestDate.getMonth() == 11) {
        latestDate.setMonth(2,1)
        latestDate.setFullYear(latestDate.getFullYear() + 1)
    }           

  }

   return dates;

}

`

1
20k cells? What? Neither sheet has 20k cells. Besides, the problem is well-displayed for anyone who can think and likes a challenge. It's ☑ minimal, ☑ complete, and ☑ verifiable. Any half-thinker can get an idea of what is going on by looking at the first 20 rows or so on each sheet. Anyone who is smart is welcome to answer. Good luck! - user4305191
Thorough question. I'll take a look later. Seriously though unless you are suggesting that you are a "half-thinker" yourself, be nice to those who voluntarily will be helping you out. - JSDBroughton
Seriously though, what is about you that has you suffering from the false belief that you can order others to be nice? What drives you to be such a meddler? Are you another one of these types who conforms eagerly to peer pressure, a follower, one who desperately needs to belong, in this case, conforming to the silliness of the Stack Overflow ethos? Either you want to answer the challenge or not. - user4305191

1 Answers

1
votes

The short answer is this is due to daylight-saving time. :-)

The start of the summer quarter, in a time-zone that uses daylight saving time, is one hour physically earlier than dates when no daylight saving is in effect.

So for example in my timeZone (AEST) [Australia = GMT+10 / GMT+11 for Daylight saving] when I look at your "starting quarter" date of 1-APR-1947 (17258 days since 1/1/900) it reports it in the script debugger as :

Tue Apr 01 1947 18:00:00 GMT+1000 (AEST)

(which suggests BTW that your local TimeZone is GMT-8 or West Coast USA , yes ?)

But if I point it to one of your dates during Daylight saving (eg 1/1/1960 - 21916 internally), it reports is as :

Fri Jan 01 1960 19:00:00 GMT+1100 (AEDT)

What this means is that when you calculate new dates in your function using .setMonth() or .setUTCMonth() these are being "clever" and taking daylight saving into consideration.. so sometimes in your result set you will get .04167 of a day extra (.04167 x 86400 seconds = 3600 seconds or 1 hour) .. which can be easily seen by adjusting the format of your date column to be "Date Time"

The simple solution to the problem this was causing you in a real spreadsheet is to use =INT() to just report the DATE component without any time.. after all, that is what you really are interested in here it seems.

Alternatively you could use .setHours(0) in your script functions in addition to your .setMonth() calls.

I was unable to see any "dates off by 1 day" in your sample data, but I could cause a similar outcome by using UTC times and then when they DISPLAY in local Time zone I can see eg 12/31/1949 23:00:00.. because during the UK Winter Australia is having Summer and is on daylight-saving, vica-versa in UK Summer/Australian Winter, and in between (spring and autumn) when neither have daylight saving, the quarter start dates have no time component... produces some interesting results :

10/1/1950 0:00:00 12/31/1950 23:00:00 4/1/1951 0:00:00 7/1/1951 1:00:00 10/1/1951 0:00:00

In your data you will also notice that the pattern of Jan/Apr being no time and Jul/Oct being +1 changed in 2007 when daylight saving was extended slightly. Other interesting (apparent) anomalies are because of the way they played around with daylight saving in the past.

By the way, if you wanted to make your original code more compact, you could do something like the following :

for (var loop = 0; loop < tally; loop++) {
    dates.push(new Date(latestDate));

    var nextMonth = (latestDate.getMonth() + 3) % 12;   // mod 12 
    latestDate.setMonth(nextMonth,1);
    if (nextMonth < 3) {
        latestDate.setFullYear(latestDate.getFullYear() + 1)
    }
 }

Oh, one final thing, I'd recommend that you change the title of your question to something like "Unexpected Time result using .setMonth() or .setUTCMonth() functions in Google Sheets / Script" as that better describes the problem IMO.

Cheers.