如何将JS日期(以编程方式)转换为Google表格序列号,反之亦然?

时间:2019-08-05 14:02:28

标签: javascript date datetime google-sheets google-sheets-api

所以,我有一个JS日期

> new Date()
Mon Aug 05 2019 06:55:46 GMT-0700 (Pacific Daylight Time

在Google Sheets API中,默认的dateTime渲染选项为DateTimeRenderOption.SERIAL_NUMBER,根据documentation所述

  

指示日期,时间,日期时间和持续时间字段以“序列号”格式的双精度形式输出,这在Lotus 1-2-3中很普遍。值的整数部分(小数点左边)计算自1899年12月30日以来的天数。小数部分(小数点右边)将时间计算为天数的一部分。例如,1900年1月1日中午为2.5,因为1899年12月30日之后是2天,所以为2;而由于中午为半天,则为0.5。 1900年2月1日下午3点将为33.625。这正确地将1900年视为not年。

我想知道如何使用API​​ /库将JS Date转换为SERIAL_NUMBER并从SERIAL_NUMBER转换为JS Date对象吗?

5 个答案:

答案 0 :(得分:0)

这将为您获取序列号。您可能会或可能不想修剪

function createSerialNum() {
    var oneDay = 24 * 60 * 60 * 1000;
    var firstDate = new Date(1899, 11, 30);
    var secondDate = new Date();
    console.log(secondDate);
    var secondDateMidnight = new Date(secondDate.getFullYear(), secondDate.getMonth(), secondDate.getDate());
    var diff = secondDate.getTime() - secondDateMidnight.getTime();
    var left = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / (oneDay))) - 1;
    var right = diff / oneDay;
    var result = left + right;
    console.log(result);
    return result;
}

这会将其转换为日期

function createDateFromSerial(serialNum){
    serialNum = String(serialNum).split(".");
    var ogDate;
    var oneDay = 24 * 60 * 60 * 1000;
    var firstDate = new Date(1899, 11, 30);  
    var days = serialNum[0];
    var ms = serialNum[1] * oneDay;
    ms = String(ms).substring(0, 8);

    firstDate.setDate(days);

    ogDate = new Date(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate(), 0, 0, 0, ms);
    console.log(ogDate);
    return ogDate;
}

答案 1 :(得分:0)

(此算法与在摄氏和华氏温度之间转换的算法非常相似,您可能对此不太熟悉。)

用于JavaScript日期的比例和您为Google表格描述的比例之间存在三个主要区别:
1)JS日期是基于毫秒的,而Google表格日期是基于天的,
2)JS日期采用1970年1月1日(世界标准时间上午12:00:00)等于零,而Google表格日期为此使用1900年12月30日,并且
3)Google Sheets中的Date(和Time)构造函数期望其输入的时区与UTC匹配,而JavaScript的Date构造函数使用脚本运行于任何位置的时区。

要在两种格式之间进行转换-以及调整时区处理-我们需要“扩展”比例(使用乘法/除法)以将天数转换为毫秒数,并且我们还需要“移动”比例(使用加/减)以说明对“零”的不同解释。

这详细显示了如何完成此操作:
(请注意,此脚本尚未使用Google表格进行过测试,但此处显示的一个示例值除外。)

// Defines constants in milliseconds (JavaScript's unit for dates)
const MS_PER_MINUTE        = 60000; // Assumes no leap year adjustment in this minute
const MS_PER_DAY           = 86400000; // Assumes no leap year adjustment in this day
const MS_PER_70YEARS_2DAYS = 2209161599801; // Diff between gSheets' & unix's "zero"

// Defines functions to build JS UTC Date objects
/* (The first function makes a preliminary Date object, calculates the difference 
between the user's timezone and UTC, and use this to make the final Date object) */
function newUTCDateFromComponents(YYYY, MM, DD = 1, hh = 0, mm = 0, ss = 0){
  // Takes 2 to 6 arguments (the components of a Date), returns a UTC Date object
  const date = new Date(YYYY, MM, DD, hh, mm, ss);
  const offsetMillisecs = date.getTimezoneOffset() * MS_PER_MINUTE;
  return new Date(date.getTime() - offsetMillisecs);
}
function newUTCDateFromTimestamp(timestamp){
  // Takes a timestamp, returns a UTC Date object
  const date = new Date(timestamp);
  return date;    
}

// Defines functions to convert between JavaScript timestamps and gSheets dates
function gsheetsDateToJsTimestamp(days){
  const jsTimestamp = (days * MS_PER_DAY) - MS_PER_70YEARS_2DAYS;
  return jsTimestamp;
}
function jsUTCTimestampToGsheetsDate(millisecs){
  let days = (millisecs + MS_PER_70YEARS_2DAYS) / MS_PER_DAY;
  days = Math.ceil(days * 100000) / 100000; // Rounds the result up to 5 digits
  return days;
}

//
// Examples
//
  // JS to gSheets
  const jsUTCDateSource = newUTCDateFromComponents(2019, 0, 1, 16, 20); // 2019-JAN-01, 4:20PM
  const jsUTCDateStringSource = jsUTCDateSource.toUTCString();
  const jsUTCMillisecSource = jsUTCDateSource.getTime();
  const gsheetsDateResult = jsUTCTimestampToGsheetsDate(jsUTCMillisecSource);
  console.log(`${jsUTCDateStringSource}
from JS (milliseconds since Jan 1, 1970 began): ${jsUTCMillisecSource}
to Google Sheets (days since Dec 30, 1899 began): ${gsheetsDateResult}`);
  console.log(`-----------`);

  // gSheets to JS
  const gsheetsDateSource = 43466.68056; // 2019-JAN-01, 4:20PM
  const jsMillisecsResult = gsheetsDateToJsTimestamp(gsheetsDateSource);
  const jsUTCDateResult = newUTCDateFromTimestamp(jsMillisecsResult);
  const jsUTCMillisecsResult = jsUTCDateResult.getTime();
  const jsUTCDateStringResult = jsUTCDateResult.toUTCString();
  console.log(`from Google Sheets: ${gsheetsDateSource}
to JS: ${jsUTCMillisecsResult} // off by a fraction of a second (see below)
human-readable string: ${jsUTCDateStringResult}`);
  console.log(`NOTE: The result is off by a fraction of a second due to rounding 
  but evaluates to the same human-readable string.`);

答案 2 :(得分:0)

在进行转换之前,首先要注意序列号格式。

串行格式

如文档中所述,序列号分为两部分。小数点前的整数表示从1899年12月30日开始的日期,小数表示一天中的时间,没有时区。也就是说,无论用户机器或工作表设置的时区,夏令时或其他任何时间,0.5始终表示12:00(正午)的时间。

时区:

序列号没有时区。这意味着无论工作表的时区如何,单元格值0始终将呈现为“ 12/30/1899 0:00:00”。 工作表的时区仅影响NOW()或TODAY()之类的函数,更改时区将更改其数字输出(而不更改TIME()函数的数字输出)。

例如:

  • 将单元格设置为公式= NOW(),然后格式化该单元格以将其呈现为数字。
  • 将工作表的时区更改为不同于以前的时区+6小时。
  • 对工作表进行更改,以便重新计算NOW()值。
  • 请注意,数值将变化约+0.25,以表示6个小时的差异。
  • 还请注意,其他任何日期时间单元格都不会随时区更改而更改其显示。

通过spreadsheets.values.get获取数据时,默认的valueRenderOptionValueRenderOption.FORMATTED_VALUE,这意味着dateTimeRenderOption的默认DateTimeRenderOption.SERIAL_NUMBER被忽略。

格式

DATE_TIME格式的单元格将根据电子表格的语言环境而不是用户计算机的语言环境(例如,如果电子表格设置为德语,则为“ 9/12/2020 8:15:40”或“ 12.9.2020 8:15:40”。如果您对日期所做的所有事情都显示了该日期,并且您不关心可能存在区域设置不匹配的情况,那么可以。请注意,不能在此字符串上使用Date.parse(),因为如果电子表格的语言环境不是英语,则该值将不一致。

如果将valueRenderOption更改为ValueRenderOption.UNFORMATTED_VALUE,我们将得到日期的SERIAL_NUMBER值,下面将给出公式。

写作

要使用JS日期写入工作表,我们不一定需要将其转换为Lotus串行格式。我们可以使用以下公式写入单元格:

`=DATE(${date.getFullYear()}, ${date.getMonth() + 1}, ${date.getDate()}) + TIME(${date.getHours()}, ${date.getMinutes()}, ${date.getSeconds()})`

这将具有类似以下内容的公式输出:=DATE(2020, 9, 12) + TIME(9, 15, 41) 无论时区如何,该单元格都将呈现为9/12/2020 9:15:41。

在该示例中,单元格使用日期对象的本地时间编写。我们可以通过以下方式将其写为日期对象的UTC时间:

`=DATE(${date.getUTCFullYear()}, ${date.getUTCMonth() + 1}, ${date.getUTCDate()}) + TIME(${date.getUTCHours()}, ${date.getUTCMinutes()}, ${date.getUTCSeconds()})`

您使用哪种取决于您的用例。使用当地时间意味着如果当地时间是3:15,则单元格将在工作表中呈现您期望的“ 3:15”。 使用UTC时间意味着如果本地时间是3:15,并且您的时区是-600GMT,则该单元格将显示为“ 9:15”,这可能会增加混乱。 但是,如果您发现两个日期之间的时差,那么使用本地时间可能会成为一个问题。以夏令时为例。例如,如果您在美国/芝加哥时区,夏令时结束于2020年11月1日凌晨2点,那么当地时间凌晨2点与凌晨1点之间是2小时。

(new Date(2020, 10, 1, 2)) - (new Date(2020, 10, 1, 1)) // 7200000 (2 hours) (note JS month is 0-indexed)

但是在Sheets中,没有夏令时,因此,无论电子表格设置的时区如何,将这两个日期相减将为1小时。

所以我们有一些选择:

  1. 将工作表的date_time值视为UTC时间。 优点:时代是明确的。应用程序可以在用户的​​时区中显示时间戳,并另存为UTC。 缺点:直接编辑工作表可能会造成混淆,因为您需要手动将当地时间转换为UTC。不能与NOW()/ TODAY()函数一起使用。
  2. 考虑工作表值没有时区。 优点:Times使用NOW()和TODAY()方法。直接编辑工作表时,始终将时间输入为用户的本地时间。 缺点:时区信息丢失。在计算时间之间的时差时,夏令时更改可能导致计算减少一个小时。
  3. 请考虑将工作表的值相对于设置工作表的时区。 优点:时代是明确的。时间使用NOW()和TODAY()方法。 缺点:工作表时区始终是标准时间,因此直接编辑工作表与#1存在相同的问题。在工作表的标准时间和用户的本地时间之间进行转换很困难,并且需要外部库。

无论选择哪种方法,我们都将从相同的公式开始。

最后,代码

(请注意,这与Jeremy Ring的回答非常相似,只不过这将处理DST更改)

/**
 * Converts a Lotus serial format to a JS UTC date.
 * @param serial {number}
 * @return {Date}
 */
function serialToDateUTC(serial) {
  const intPart = Math.floor(serial);
  const fracPart = serial - intPart;
  const time = Math.round(fracPart * DAY_TO_MS);
  const date = new Date(Date.UTC(1899, 11, 30 + intPart) + time);
  return date;
}

/**
 * Converts a JS Date to Lotus serial format.
 * @param date {Date}
 * @return {number}
 */
function utcDateToSerial(date) {
  const dateSansTime = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
  const intPart = Math.round((dateSansTime.getTime() - Date.UTC(1899, 11, 30)) / DAY_TO_MS);
  const fracPart = (date.getTime() - dateSansTime.getTime()) / DAY_TO_MS;
  return intPart + fracPart;
}

要显示UTC日期,请使用Date.toUTCString(或将Intl.DateTimeFormat设置为timeZone)。示例:

console.log(serialToDateUTC(44086.344212963).toUTCString()); // 9/12/2020 8:15:40

(选项1) 如果我们希望以UTC的形式存储在工作表中并在本地时间显示给用户,我们会使用这些UTC转换来读取/写入工作表,然后通过Date.toLocaleStringIntl.DateTimeFormat在本地显示 请注意,直接在Google文档中编辑时间时,此选项可能会造成混淆;时间将需要在UTC时区中输入。

(选项2) 如果我们忽略时区,则意味着我们正在考虑工作表中的日期/时间值对于用户始终是本地的,而不管他们的时区如何。 为此,如果我们有一个本地时间为4:24的日期,我们想将4:24存储在工作表中(使用公式,这就是=TIME(4, 24, 0)。 为此,我们可以使用先前发布的相同方法,但是要强制/假装本地时间是UTC时间。例如

const timestamp = new Date(); new Date(Date.UTC(timestamp.getFullHours(), timestamp.getMonth(), timestamp.getDate(), timestamp.getHours(), timestamp.getMinutes(), timestamp.getSeconds()));

这将使时差变得不稳定-考虑一个时间表应用程序,其中您通过Date.now()或new Date()来获取开始时间和结束时间,并使用公式结束时间减去开始时间作为持续时间单元格。如果我们跨越时间变化,将其强制为标准时间可能会损失或增加一个小时的时间差。

(选项3) 处理工作表时区本地的所有时间。 这要求我们获得两条信息。单元格数据和工作表的时区。 JavaScript本机无法在另一个时区中创建日期,也无法从任意时区获取时区偏移。为此,我们需要一个外部库,例如countries-and-timezones。 使用Google files.get api获取时区,然后从国家和时区查询中获取dstOffset(而不是utc偏移量)。使用该偏移量可以从本地时区的Date.getTimezoneOffset()调整工作表的时区。是的,有点讨厌。

答案 3 :(得分:0)

我一直在将它用于 Google Sheet API。对我来说效果很好。

// Hour format: HH:MM
// Date format: YYYY-MM-DD
function serialDate(hour, date) {

  let leftDate = date;
  leftDate = new Date(leftDate);
  leftDate = leftDate.getTime();

  let serialNumberReference = (new Date('1899-12-30')).getTime();

  leftDate = (leftDate - serialNumberReference) / (3600 * 24 * 1000);

  let rightDate = date;
  rightDate = (new Date(rightDate + ' ' + hour)).getTime() - (new Date(rightDate + ' 00:00').getTime());
  rightDate = rightDate / (3600 * 24 * 1000);

  return leftDate + rightDate;
}

答案 4 :(得分:-1)

日期至序列号:string

迄今为止的序列号:(date.getTime() - new Date(1899, 11, 30).getTime()) / (24 * 60 * 60 * 1000)