3

除非我使用 .toLocaleDateString() 方法,否则下面的函数会返回一个比应有的日期少一天的日期。我做错了什么还是这是一个缺陷?

    function myDate() {
  var sDate = '05/10/2012';
  var parts = sDate.split('/');
  var d = new Date( parts[2], parts[1]-1, parts[0]);
  Logger.log(d);
  Logger.log(d.toLocaleDateString());
};

记录器返回:

2012 年 10 月 4 日星期四 16:00:00 PDT

2012 年 10 月 5 日 00:00:00 BST

我在英国,因此使用英国日期格式。我检查了我的项目属性的时区设置为“(GMT+00:00) 伦敦”,那么为什么 Logger 会在 PDT 中显示第一个日期?这就是日期错误的原因吗?我已经在一个独立的项目中重现了这个问题。这是它的链接。

我想将字符串变量转换为日期对象以进行一些日期数学运算,因此必须使用 .toLocaleDateString() 方法转换回字符串并没有帮助。

我已经检查了一致性,认为也许我可以通过测试其他日期来解决它。奇怪的是,如果我将 sDate 的值更改为 01/01/2012 和 04/03/2012 之间的任何值,它会准确返回。从 2012 年 5 月 3 日起,每天下降。对于 2013 年的日期,它会正确返回,直到 29/04/2013 再次开始下降一天。所以并不一致。这似乎与此处报告的问题相似。

4

5 回答 5

3

我发现 getValues() 函数在旧 DST 规则和新 DST 规则之间的范围内读取的日期减少了 1 小时。因此,日期“2012 年 10 月 30 日”的隐式时间是 00:00:00。但是,当读入时,它减少了一个小时,在前一天登陆,即 2012 年 10 月 29 日 23:00:00。

这两个时间范围之间的任何日期,不包括开始日,但包括结束日,目前都会表现出这种行为,至少由 getValues() 函数读取:

  • 在十月的最后一个星期日和十一月的第一个星期日之间
  • 在三月的第二个星期天和四月的第一个星期日之间。

我最终编写了可以动态计算当前年份的这些日期的代码,如果日期落在目标范围内,我只需将其增加一个小时。这可能是修复它的漫长道路,但它确实有效。

这是我的代码:

/*
The old rules for DST stated that the time change occurred on the last Sunday in October, 
which would be 10/28/2012.  So, when you type in 10/29/2012, the timestamp associated with the date (00:00:00) is being decremented by an hour (23:00:00), 
which drops it into the previous day.  The new rules for DST states that DST ends on the 1st Sunday in November, which is 11/04/2012.  Also, the DST rules
for springtime are also an impacted range of dates that exhibit this behavior, between the second sunday in March and the first sunday in April.

Note:  Running this function from the script editor does not produce the strange DST behavior.  It seems to be an issue with the getValues() function on a Range object.
*/
function fixDateDSTProblem(lstrDate){
  var d = new Date(lstrDate);
  //Example ranges affected
  //10/29/2012 - 11/04/2012
  //03/11/2013 - 04/07/2013

  if(isNaN(d.getMonth())){
    //return what was passed in if it's not a date or null.
    return lstrDate;
  }
  else if(isAffectedDate(d)){
    //increment by one hour
    return new Date(d.getTime() + (60 * 60 * 1000));
  }
  else{
    //date is not affected, simply return it as it was passed.
    return lstrDate;
  }
}



//Check to see if a date is within a beginning and end date
function dateWithin(beginDate,endDate,checkDate) {
    var b,e,c;
    b = Date.parse(beginDate);
    e = Date.parse(endDate);
    c = Date.parse(checkDate);
  //slightly modified this for the date checking since beginning date is not inclusive, so needs to be > instead of >=
    if((c <= e && c > b)) {
            return true;
    }
    return false;
}



function isAffectedDate(targetDate){
  var yearNum = targetDate.getFullYear();

  //Get the last Sunday in October
  var lastSunOctDateStr = getLastOccurrenceDate(0, 10, yearNum);

  //Get the first Sunday in November
  var firstSunNovDateStr = getOccurrenceDate(1, 0, 11, yearNum);

  //Get the second Sunday in March
  var secondSunMarDateStr = getOccurrenceDate(2, 0, 3, yearNum);

  //Get the first Sunday in April
  var firstSunAprDateStr = getOccurrenceDate(1, 0, 4, yearNum);  


  //if the date is between the last sunday in october and the first sunday in november
  // or if the date is between the second sunday in march and the first sunday and april, fix it up!
  if(dateWithin(lastSunOctDateStr, firstSunNovDateStr, targetDate) || 
     dateWithin(secondSunMarDateStr, firstSunAprDateStr, targetDate)){
    return true;
  }
  return false;
}



function getOccurrenceDate(numOccurrence, dayIndex, monthCalendar, yearNum){
  //"Get date of first occurrence of Monday in June 2013"
  //"Get date of the second occurrence of Sunday in April 2013"
  //dayIndex:  Sunday = 0, Saturday = 6
  var monthIndex = monthCalendar - 1;
  var numFirstXDay = null;
  var firstDay = new Date(monthCalendar+"/01/"+yearNum);
  var numDayOfWeek = firstDay.getDay();
  if(numDayOfWeek == dayIndex){
    numFirstXDay = 1;
  }
  else if(numDayOfWeek > dayIndex){  
    numFirstXDay = 1+(6-numDayOfWeek)+1+dayIndex+(7*(numOccurrence-1));
  }
  else if(numDayOfWeek < dayIndex){
    numFirstXDay = 1+(dayIndex - numDayOfWeek)+(7*(numOccurrence-1));
  }

  return monthCalendar+"/"+numFirstXDay+"/"+yearNum; 
}



function getLastOccurrenceDate(dayIndex, monthCalendar, yearNum){
  //Example:  "Get date of last occurrence of Monday in June 2013"
  var monthIndex = monthCalendar - 1;
  var numLastXDay = null;
  //TODO:  Handle Leap Year!
  var monthMaxDaysArray = {
    '1':'31',
    '2':'28',
    '3':'31',
    '4':'30',
    '5':'31',
    '6':'30',
    '7':'31',
    '8':'31',
    '9':'30',
    '10':'31',
    '11':'30',
    '12':'31'}    

  var lastDay = new Date(monthCalendar + "/"+monthMaxDaysArray[monthCalendar]+"/" + yearNum);
  var numDayOfWeek = lastDay.getDay();
  if(numDayOfWeek == dayIndex){ 
    numLastXDay = 31;
  }
  else if(numDayOfWeek > dayIndex){    
   numLastXDay = monthMaxDaysArray[monthCalendar] - (numDayOfWeek - dayIndex); 
  }
  else if(numDayOfWeek < dayIndex){
    numLastXDay = (monthMaxDaysArray[monthCalendar] - numDayOfWeek) - (6 - (dayIndex-1));
  }

  return monthCalendar + "/" + numLastXDay + "/" + yearNum;
}
于 2012-10-08T06:34:35.397 回答
2

记录器始终在 PDT 中记录日期。这是同一个时间实例,只是以不同的方式表示。您会看到 + 夏令时的差异。

于 2012-10-05T14:36:56.627 回答
1

即使科里的回答不需要任何确认(他知道他在说什么;)让我添加一些实用的细节......

  • 即使您看到的记录器值看起来错误,您对日期对象 ( getDay, getTime, getFulYear...) 所做的每个 javascript 操作都将返回正确的值,因此如果您需要操作日期,则无需转换为字符串并返回数字
  • 如果这些日期显示在电子表格中,则电子表格区域设置将用于显示所需的值。
  • 有时您会在记录器中看到其他格式的值...不要问何时或为什么,但有时会发生在我身上(我在 GMT+1 比利时)
  • 您在 10 月和 4 月注意到的日期变化确实发生在夏令时开始和停止时,当您需要在一个时期的日历事件中定义一个日期时,这可能会变得很棘手(例如,今天我在 12 月在那里创建了一个事件将相差 1 小时),因此当您在用户界面中读取日期对象时需要使用正确的 GMT+0X00 值utilities.formatDate
  • 如果由于某种原因您显示了从夏令时切换日之前开始到之后结束的日期列表,并且您使用utilities.formatDate,最好使用一个变量来动态地使用“GMT+0X00”:我只需使用类似这样的东西就可以得到它:

* EDIT*因为几个星期以来语法发生了变化,这个字符串的格式必须不同,所以我相应地更新了这个代码。(见问题 2204

`var FUS1=new Date().toString().substr(25,6)+":00";// FUS1 gets the GMT+02:00 or GMT+01:00 string`

希望它变得足够清楚

于 2012-10-05T15:49:45.703 回答
0

很抱歉迟到了几个月才入党……

这些答案的问题在于,他们都没有明确表明这是一个BUG。或者,更准确地说,这是谷歌认定的“正确”行为(例如,参见 Issue 1271,没有采取任何行动就关闭了),而这显然是完全错误的做法。

考虑一下。我想将某人的生日放在电子表格中,并且我有一个很大的列表。然后我想读出该值并将其放入一个表格中:

function getAge(ss, index) {
  var ages = ss.getRangeByName("birthDates").getValues();
  return ages[index];
}
...
app.createLabel(getAge(ss, index));

该表格现在包含错误的出生日期为 4 月至 10 月之间出生的每个人。谷歌已经决定他们实际上是在一天前出生的。或者尝试读出年龄,并将它们放入另一个电子表格中,无需处理,两个电子表格具有相同的时区设置。同样的故事,当人们发邮件告诉我我弄错了他们的生日时,这让我看起来非常愚蠢。

应该这样做:如果有人手动指定一个日期,例如“06/06/1997”,然后用 读取该日期getValue,他们希望得到“06/06/1997”,而不是前一天。他们真的不希望您尝试第二次猜测他们:“好吧,这个日期恰好是在 DST 期间,所以他们真正希望读回的getValue是更正到非 DST 的内容,所以我们为什么不减去一个一个小时,并在前一天给他们?”

这很疯狂。如果 Excel 这样做,那将是一个笑柄。请修复它。

编辑

Serge,这很有趣,但是您的电子表格显示了一种解决方法,它提取 TZ,然后根据该 TZ 显示。但这只是避免了开始时这是错误的问题。考虑一下。电子表格几乎总是包含固定日期的周年纪念日——某人的生日、战斗的日期、约会的时间/日期等等。这些独立于 TZ. 如果您在比利时的生日是 1960 年 7 月 6 日,那么如果您碰巧住在加利福尼亚,那也是 7 月 6 日。如果您出生于上午 00:30,您的护照仍显示为 7 月 6 日,即使在 CET 和 PDT 中该时间实际上是 7 月 5 日。对于周年纪念日或约会,没有人在乎时区。在电子表格中,只有一小部分用户关心 TZ。我无法立即想到一个真正重要的用例。

Lotus 1-2-3 做对了。Excel 做对了。日期是一个整数序列号,没有时区信息。将日期从一个电子表格复制到另一个电子表格,它总是正确的。使用 VB/whatever 从电子表格中获取日期,将其放入另一个电子表格中,它总是正确的(我认为)。尽管我不想这么说,Excel 定义了电子表格,而且他们做得很好。除了VB。

在 GAS 中,如果我使用 复制日期getValues,这是错误的。谷歌对此过于聪明了。好的,有一个复杂的解决方法涉及查找创建电子表格的 TZ,假设您输入的日期实际上是相对于 UTC 的纪元日期,并根据电子表格的当前 TZ 设置显示它(至少,我认为这就是你正在做的)。但那又怎样?而且,它总是有效吗?如果您将电子表格邮寄给住在 PDT 的人怎么办?我相信你可以让它工作,但这没有抓住重点——这只是一种变通方法,我认为它可能很脆弱(请注意,有3 个TZ 设置:Google 帐户设置、工作表设置和脚本设置,你必须考虑所有这些 - 见 ahab')。根本问题是 Google 电子表格存储相对于特定 TZ 的日期(底层格式 ms 是相对于 Unix 纪元吗??)。这是错误的,现在无法修复,并且只是与日期处理有关的一大堆蠕虫的一部分。

作为一个短期修复,我可能会在电子表格中的所有日期上添加 12 小时。作为一个长期解决方案,我可能应该将所有日期作为文本字符串处理,并自己在 JavaScript 中完成。我已经不得不用“整数”毫秒数替换所有经过的时间,以处理与 GAS 日期中丢失的毫秒数有关的问题。继续,说服我我错了。

由 Serge 编辑: 您好,为了便于阅读,我冒昧地在您的编辑中回答(对此感到抱歉)。

我理解你的观点,但我不同意。

您引用的 UI 示例确实需要一种解决方法,因为这是纯 javascript,而不是电子表格日期......也就是说,Excel 电子表格和 Google 电子表格的工作方式相同(第二天是 day+1,时间 = 小数,参考日期 0 = 1899 年 12 月 31 日))只要您留在电子表格世界中,当您退出电子表格并转到 Javascript 时,问题就来了(参考日期 = 01/01/1970,以毫秒为单位)...

如果他们选择不关心 TZ 并在任何时区保持日期“静态”,那将会有很多其他问题:例如,如果你想在比利时的我和华盛顿的奥巴马总统之间在 Skype 上安排约会怎么办? ? 如果我们决定在星期一早上 8 点进行谈话,是在比利时还是在华盛顿早上 8 点?

使用谷歌解决方案,他只需要早起一点,但我们都会在那里,因为日期和时间会显示相同的“时刻”,按照你建议的方式,我们不会有任何交谈的机会(那将是多么可悲^^)。

还有很多情况显然是更好的选择:与日历服务通信也是一个很好的例子。

静态 excel 解决方案效果很好,因为这样的文档没有以相同的方式共享,并且不会将日期转换为 Javascript 日期对象。Excel SS 与计算机用户相关,Google SS 使用时区设置保持与用户计算机的这种关系,因为它(本质上)与用户计算机无关。我几乎没有希望说服你 (;-) 但我希望这至少能让事情变得更清楚......(无论如何我不是谷歌工程师,也无法以任何方式影响他们^^)。

再编辑...

这是一个小测试表,其 TZ 设置设置为 GMT-8,它获取另一个测试表的值 (设置为 GMT+2 比利时冬季时间),并使用这个简单的脚本以正确的方式显示它:

function timeCorrection() {
  var thisSheet = SpreadsheetApp.getActiveSheet();
  var otherSS = SpreadsheetApp.openById('0AnqSFd3iikE3dENrc2h1M0ktQTlSNWpNUi1TS0lrNlE');
  var otherSheet = otherSS.getSheetByName('Sheet1');
  var otherValue = otherSheet.getRange(1,1).getValue();
  var tz = otherSS.getSpreadsheetTimeZone();
  var thisSheetValue = new Date(Utilities.formatDate(otherValue,tz,'MM/dd/yyyy HH:mm:ss'));
  thisSheet.getRange(1,1).setValue(thisSheetValue);
}

就这么简单 ;-) ,“外国工作表”的设置被设置为与其脚本设置相同的值,这是在温哥华创建的新创建工作表的默认值。

于 2013-01-26T22:13:28.663 回答
0

这不是错误...

我可以为程序化程度较低的问题提供另一种解决方案吗?我隔离了这个问题并花了一天时间试图弄清楚为什么我的脚本会在前一天和下午 23:00 提取日期。

然后,我意识到电子表格不是逗号分隔的数据库,而是具有自动格式化功能。因此,当我在电子表格中选择包含日期的列并从菜单中选择格式>数字>纯文本时,问题就解决了。现在我的脚本运行完美,没有太多的方法。将此应用于整个列甚至会影响新添加的单元格。所以这是一个甜蜜的解决方案。

因为这是stackoverflow,所以这里是java解决方案(我检查它是否有效):

sheet.getRange(1,n).setNumberFormat('@STRING@');

我从 christian.simms 中获取,来自这里: Format a Google Sheets cell in plaintext via Apps Script

于 2016-03-15T19:36:19.900 回答