23分钟阅读

关于DateTime操作的权威指南

Punit是一位多才多艺的软件工程师和企业家。他从事大数据和实时3D仿真工作,并且是MEAN堆栈专家。

作为软件开发人员,您无法逃避日期操纵。开发人员构建的几乎每个应用程序都将具有一些组件,需要从用户那里获取日期/时间,并将其存储在数据库中并显示给用户。

向任何程序员询问他们处理日期和时区的经验,他们可能会分享一些战争故事。处理日期和时间字段当然不是火箭科学,但通常很乏味且容易出错。

有数百篇关于该主题的文章,但是,大多数要么过于学术化,专注于细微的细节,要么过于零散,仅提供简短的代码片段,而没有太多解释。这份有关DateTime操作的深入指南应该可以帮助您了解与时间和日期相关的编程概念和最佳实践,而不必浏览有关该主题的大量信息。

在本文中,我将帮助您清楚地考虑日期和时间字段,并提出一些最佳实践,以帮助您避免日期/时间的麻烦。在这里,我们将探讨正确处理日期和时间值所必需的一些关键概念,便于存储DateTime值并通过API传输它们的格式等。

对于初学者而言,生产代码的正确答案几乎总是使用适当的库,而不是滚动自己的库。本文讨论的DateTime计算的潜在困难只是冰山一角,但无论有无库,了解它们仍然有帮助。

如果正确理解日期时间库,它们将对您有所帮助

日期库以多种方式帮助您简化生活。它们极大地简化了日期解析,日期算术和逻辑运算以及日期格式。您可以为前端和后端找到可靠的日期库,从而为您完成大部分繁重的工作。

但是,我们经常使用日期库而没有考虑日期/时间实际如何工作。日期/时间是一个复杂的概念。即使对日期库的帮助,由于对其理解不正确而出现的错误也很难理解和修复。作为程序员,您需要了解基础知识,并能够体会到日期库为最大程度地解决问题而解决的问题。

此外,日期/时间库只能带您走这么远。通过允许您访问方便的数据结构来表示DateTime,所有日期库都可以工作。如果您要通过REST API发送和接收数据,则最终将需要将日期转换为字符串,反之亦然,因为JSON没有用于表示DateTime的本机数据结构。我在这里概述的概念将帮助您避免执行这些从日期到字符串和从字符串到日期的转换时可能出现的一些常见问题。

笔记: 即使我用过 的JavaScript 正如本文讨论的编程语言一样,这些是通用概念,它们在很大程度上适用于几乎所有编程语言及其日期库。因此,即使您以前从未编写过JavaScript代码,也可以随时继续阅读,因为我几乎不假设本文具有JavaScript的相关知识。

标准化时间

DateTime是一个非常特定的时间点。让我们考虑一下。在我撰写本文时,笔记本电脑上的时钟显示为7月21日下午1点29分。这就是我们所说的“本地时间”,即我在周围的挂钟和手表上看到的时间。

请花几分钟,如果我让我的朋友在下午3:00在附近的咖啡馆见我,我大概可以在那个时候见到她。同样,如果我说例如“让我们在一个半小时内见面”,也不会造成任何混乱。我们通常以这种方式与居住在同一城市或时区的人们谈论时间。

让我们考虑另一种情况:我想告诉居住在瑞典乌普萨拉的一位朋友,我想在下午5点与他交谈。我给他发了一条消息:“嗨,安东,让我们在下午5点谈谈。”我立即得到答复,“您的时间还是我的时间?”

安东告诉我,他住在中欧时区UTC + 01:00。我住在UTC + 05:45。这意味着当我居住的时间是下午5点时,它就是下午5点-UTC上午5:45 = 11:15,相当于乌普萨拉上午11:15 AM UTC + 01:00 = 12:15 PM我们。

另外,请注意时区(欧洲中部时间)和时区偏移量(UTC + 05:45)之间的时差。各国出于政治原因也可以决定更改夏令时的时区偏移量。几乎每年,至少一个国家/地区的规则都会发生变化,这意味着遵守这些规则的任何代码都必须保持最新状态-值得考虑一下您的应用程序每一层代码库所依赖的代码。

这是另一个很好的理由,我们建议在大多数情况下,仅前端处理时区。如果没有,那么数据库引擎使用的规则与您的前端或后端的规则不匹配怎么办?

相对于用户和相对于普遍接受的标准来说,管理两个不同时间版本的问题非常困难,在编程世界中,精度是关键,甚至一秒钟也可以带来巨大的差异,这一点尤其困难。解决这些问题的第一步是将DateTime存储在UTC中。

格式标准化

标准化时间很棒,因为我只需要存储UTC时间,而且只要我知道用户的时区,就可以随时将其转换为他们的时间。相反,如果我知道用户的本地时间并知道他们的时区,则可以将其转换为UTC。

但是日期和时间可以用许多不同的格式指定。对于日期,您可以输入“ 7月30日”或“ 7月30日”或“ 7/30”(或30/7,具体取决于您的住所)。目前,您可以输入“ 9:30 PM”或“ 2130”。

全世界的科学家齐心协力解决这个问题,并决定采用一种格式来描述程序员真正喜欢的时间,因为时间短而精确。我们将其称为“ ISO日期格式”,它是ISO-8601扩展格式的简化版本,看起来像这样:

该图显示了称为ISO日期格式的ISO-8601扩展格式的简化版本。

对于00:00或UTC,我们改用“ Z”,这表示Zulu时间,UTC的另一个名称。

的JavaScript中的日期处理和算术

在开始最佳实践之前,我们将学习使用JavaScript进行日期操作以掌握语法和一般概念。尽管我们使用JavaScript,但是您可以轻松地使这些信息适应您喜欢的编程语言。

我们将使用日期算法来解决大多数开发人员遇到的与日期有关的常见问题。

我的目标是让您轻松地从字符串创建日期对象并从其中提取组件。这是日期库可以为您提供帮助的东西,但是总要了解它在后台的完成方式总是更好的。

一旦弄清了日期/时间,我们就可以更轻松地思考我们面临的问题,提取最佳实践并向前迈进。如果您想跳到最佳做法,请随时这样做,但是我强烈建议您至少略过下面的日期算术部分。

的JavaScript日期对象

Programming languages contain useful constructs to make our lives easier. The JavaScript Date 目的 is one such thing. It offers convenient methods to get the current date and time, store a date in a variable, perform date arithmetic, and format the date based on the user’s locale.

由于浏览器实现之间的差异以及对夏令时(DST)的错误处理,不建议针对关键任务应用程序使用Date对象,您可能应该使用类似DateTime库 Luxon,date-fns或dayjs. (Whatever you use, avoid the once-popular Moment.js—often simply called moment, as it appears in code—since it’s now deprecated.)

但是出于教育目的,我们将使用Date()对象提供的方法来学习JavaScript如何处理DateTime。

获取当前日期

const currentDate = new Date();

如果你 don’t pass anything to the Date constructor, the date object returned contains the current date and time.

然后,可以将其格式化为仅提取日期部分,如下所示:

const currentDate = new Date();

const currentDayOfMonth = currentDate.getDate();
const currentMonth = currentDate.getMonth(); // Be careful! January is 0, not 1
const currentYear = currentDate.getFullYear();

const dateString = currentDayOfMonth + "-" + (currentMonth + 1) + "-" + currentYear;
// "27-11-2020"

注意:“一月为零”陷阱是常见的,但不是普遍的。在开始使用任何语言(或配置格式:例如,cron特别是基于1的语言)的文档之前,都应仔细检查。

获取当前时间戳

如果你 instead want to get the current time stamp, you can create a new Date object and use the getTime() method.

const currentDate = new Date();
const timestamp = currentDate.getTime();

在JavaScript中,时间戳记是自1970年1月1日以来经过的毫秒数。

如果你 don’t intend to support <IE8, you can use Date.now() to directly get the time stamp without having to create a new Date object.

解析日期

将字符串转换为JavaScript日期对象的方式不同。

Date对象的构造函数接受多种日期格式:

const date1 = new Date("Wed, 27 July 2016 13:30:00");
const date2 = new Date("Wed, 27 July 2016 07:45:00 UTC");
const date3 = new Date("27 July 2016 13:30:00 UTC+05:45");

请注意,您不需要包括星期几,因为JS可以确定任何日期的星期几。

您还可以将年,月,日,小时,分钟和秒作为单独的参数传递:

const date = new Date(2016, 6, 27, 13, 30, 0);

当然,您始终可以使用ISO日期格式:

const date = new Date("2016-07-27T07:45:00Z");

但是,如果您未明确提供时区,则会遇到麻烦!

const date1 = new Date("25 July 2016");
const date2 = new Date("July 25, 2016");

这两种方式都会给您当地时间2016年7月25日00:00:00。

如果你 use the ISO format, even if you give only the date and not the time and time zone, it will automatically accept the time zone as UTC.

这意味着:

new Date("25 July 2016").getTime() !== new Date("2016-07-25").getTime()
new Date("2016-07-25").getTime() === new Date("2016-07-25T00:00:00Z").getTime()

格式化日期

Fortunately, modern JavaScript has some convenient internationalization functions built into the standard Intl namespace that make date formatting a straightforward operation.

For this we’ll need two objects: a Date and an Intl.DateTimeFormat, initialized with our output preferences. Supposing we’d like to use the American (M/D/YYYY) format, this would look like:

const firstValentineOfTheDecade = new Date(2020, 1, 14); // 1 for February
const enUSFormatter = new Intl.DateTimeFormat('en-US');
console.log(enUSFormatter.format(firstValentineOfTheDecade));
// 2/14/2020

If instead we wanted the Dutch (D/M/YYYY) format, we would just pass a different culture code to the DateTimeFormat constructor:

const nlBEFormatter = new Intl.DateTimeFormat('nl-BE');
console.log(nlBEFormatter.format(firstValentineOfTheDecade));
// 14/2/2020

或更长格式的美国格式,其中阐明了月份名称:

const longEnUSFormatter = new Intl.DateTimeFormat('en-US', {
    year:  'numeric',
    month: 'long',
    day:   'numeric',
});
console.log(longEnUSFormatter.format(firstValentineOfTheDecade));
// February 14, 2020

Now, if we wanted a proper ordinal format on the day of the month—that is, “14th” instead of just “14”—this unfortunately needs a bit of a workaround, because day’s only valid values as of this writing are "numeric" or "2-digit". Borrowing Flavio Copes的版本Mathias Bynens的代码 to leverage another part of Intl for this, we can customize the day of the month output via formatToParts():

const pluralRules = new Intl.PluralRules('en-US', {
    type: 'ordinal'
})
const suffixes = {
    'one': 'st',
    'two': 'nd',
    'few': 'rd',
    'other': 'th'
}
const convertToOrdinal = (number) => `${number}${suffixes[pluralRules.select(number)]}`
// At this point:
// convertToOrdinal("1") === "1st"
// convertToOrdinal("2") === "2nd"
// etc.

const extractValueAndCustomizeDayOfMonth = (part) => {
    if (part.type === "day") {
        return convertToOrdinal(part.value);
    }
    return part.value;
};
console.log(
    longEnUSFormatter.formatToParts(firstValentineOfTheDecade)
    .map(extractValueAndCustomizeDayOfMonth)
    .join("")
);
// February 14th, 2020

Unfortunately, formatToParts isn’t supported by Internet Explorer (IE) at all as of this writing, but all other desktop, mobile, and back-end (i.e. Node.js) technologies 确实有支持。对于那些需要支持IE且绝对需要使用序号的用户,下面的边注(或更好的是,适当的日期库)提供了答案。

如果你 need to support older browsers like IE before version 11, date formatting in JavaScript is tougher because there were no standard date-formatting functions like strftime in Python or PHP.

In PHP for example, the function strftime("Today is %b %d %Y %X", mktime(5,10,0,12,30,99)) gives you Today is Dec 30 1999 05:10:00.

您可以使用 字母的不同组合 preceded by % to get the date in different formats. (Careful, not every language assigns the same meaning to each letter—特别是,“ M”和“ m”可能会交换几分钟和几个月。)

如果你 are sure of the format you want to use, it is best to extract individual bits using the JavaScript functions we covered above and create a string yourself.

var currentDate = new Date();
var date = currentDate.getDate();
var month = currentDate.getMonth(); 
var year = currentDate.getFullYear();

我们可以用MM / DD / YYYY格式获取日期

var monthDateYear  = (month+1) + "/" + date + "/" + year;

该解决方案的问题在于,它可能会给日期提供不一致的长度,因为一个月中的某些月份和日期是单位数字,而另一些则是两位数。例如,如果您要在表格列中显示日期,则可能会出现问题,因为日期没有对齐。

我们可以通过使用“ pad”函数来解决此问题,该函数添加前导0。

function pad(n) {
    return n<10 ? '0'+n : n;
}

现在,使用以下命令以MM / DD / YYYY格式获取正确的日期:

var mmddyyyy = pad(month + 1) + "/" + pad(date) + "/" + year;

如果我们改为使用DD-MM-YYYY,则过程类似:

var ddmmyyyy = pad(date) + "-" + pad(month + 1) + "-" + year;

让我们开始之前,尝试以“月日期,年”格式打印日期。我们将需要一个月索引到名称的映射:

var monthNames = [
    "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
];

var dateWithFullMonthName = monthNames[month] + " " + pad(date) + ", " + year;

Some people like to display the date as 1st January, 2013. No problem, all we need is a helper function ordinal that returns 1st for 1, 12th for 12, and 103rd for 103, etc., and the rest is simple:

var ordinalDate = ordinal(date) + " " + monthNames[month] + ", " + year;

很容易从date对象确定星期几,因此我们将其添加到:

var daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];


ordinalDateWithDayOfWeek = daysOfWeek[currentDate.getDay()] + ", " + ordinalDate;

这里最重要的是,一旦您从日期中提取了数字,那么格式主要与字符串有关。

更改日期格式

一旦知道如何解析日期并设置日期格式,将日期从一种格式更改为另一种格式,只需将两者结合即可。

例如,如果您的日期格式为2013年7月21日,并且想要将格式更改为2013年7月21日,则可以这样实现:

const myDate = new Date("Jul 21, 2013");
const dayOfMonth = myDate.getDate();
const month = myDate.getMonth();
const year = myDate.getFullYear();

function pad(n) {
    return n<10 ? '0'+n : n
}

const ddmmyyyy = pad(dayOfMonth) + "-" + pad(month + 1) + "-" + year;
// "21-07-2013"

使用JavaScript Date对象的本地化功能

The date formatting methods we discussed above should work in most applications, but if you really want to localize the formatting of the date, I suggest you use the Date object’s toLocaleDateString() method:

const today = new Date().toLocaleDateString('en-GB', {  
    day:   'numeric',
    month: 'short',
    year:  'numeric',
});

…gives us something like 26 Jul 2016.

Changing the locale to ‘en-US’ gives “Jul 26, 2016” instead. Notice how the formatting changed, but the display options were still kept the same—a very useful feature. As shown in the previous section, the newer Intl.DateTimeFormat-based technique works very similarly to this, but lets you reuse a formatter object so that you only need to set options once.

With toLocaleDateString(), it is a good habit to always pass the formatting options, even if the output looks fine on your computer. This can protect the UI from breaking in unexpected locales with really long month names or looking awkward because of short ones.

如果我想要整个月为“ July”,我要做的就是将选项中的month参数更改为“ long”。 JavaScript为我处理了一切。对于美国英语,我现在是2016年7月26日。

注意:如果希望浏览器自动使用用户的语言环境,则可以将“ undefined”作为第一个参数。

如果你 want to show the numeric version of the date and don’t want to fuss with MM/DD/YYYY vs. DD/MM/YYYY for different locales, I suggest the following simple solution:

const today = new Date().toLocaleDateString(undefined, {
    day:   'numeric',
    month: 'numeric',
    year:  'numeric',
});

On my computer, this outputs 7/26/2016. If you want to make sure that month and date have two digits, just change the options:

const today = new Date().toLocaleDateString(undefined, {
    day:   '2-digit',
    month: '2-digit',
    year:  'numeric',
});

This outputs 07/26/2016. Just what we wanted!

您还可以使用其他一些相关功能来本地化时间和日期的显示方式:

代码输出描述
now.toLocaleTimeString()
“ 4:21:38 AM” 显示唯一时间的本地化版本
now.toLocaleTimeString(undefined, {
    hour:   '2-digit',
    minute: '2-digit',
    second: '2-digit',
});
“ 04:21:38 AM” 根据提供的选项显示本地化时间
now.toLocaleString()
“ 2016年7月22日,上午4:21:38” 显示用户区域设置的日期和时间
now.toLocaleString(undefined, {
    day:    'numeric',
    month:  'numeric',
    year:   'numeric',
    hour:   '2-digit',
    minute: '2-digit',
});
“ 2016年7月22日,上午04:21” 根据提供的选项显示本地化的日期和时间

计算相对日期和时间

下面是向JavaScript日期添加20天的示例(例如,计算出已知日期之后20天的日期):

const myDate = new Date("July 20, 2016 15:00:00");
const nextDayOfMonth = myDate.getDate() + 20;
myDate.setDate(nextDayOfMonth);
const newDate = myDate.toLocaleString();

The original date object now represents a date 20 days after July 20 and newDate contains a localized string representing that date. On my browser, newDate contains “8/9/2016, 3:00:00 PM”.

To calculate relative time stamps with a more precise difference than whole days, you can use Date.getTime() and Date.setTime() to work with integers representing the number of milliseconds since a certain epoch—namely, January 1, 1970. For example, if you want to know when it’s 17 hours after right now:

const msSinceEpoch = (new Date()).getTime();
const seventeenHoursLater = new Date(msSinceEpoch + 17 * 60 * 60 * 1000);

比较日期

与日期相关的所有其他内容一样,比较日期也有其自身的陷阱。

首先,我们需要创建日期对象。幸运的是,<, >, <=, and >=所有工作。因此,比较2014年7月19日和2014年7月18日很容易:

const date1 = new Date("July 19, 2014");
const date2 = new Date("July 28, 2014");

if(date1 > date2) {
    console.log("First date is more recent");
} else {
    console.log("Second date is more recent");
}

检查相等性比较棘手,因为代表相同日期的两个日期对象仍然是两个不同的日期对象,并且将不相等。比较日期字符串不是一个好主意,因为例如“ 2014年7月20日”和“ 2014年7月20日”代表相同的日期,但具有不同的字符串表示形式。下面的代码段说明了第一点:

const date1 = new Date("June 10, 2003");
const date2 = new Date(date1);

const equalOrNot = date1 == date2 ? "equal" : "not equal";
console.log(equalOrNot);

This will output not equal.

这种特殊情况可以通过比较日期(它们的时间戳)的整数等效值来解决,如下所示:

date1.getTime() == date2.getTime()

我在很多地方都看到过此示例,但我不喜欢它,因为通常不会从另一个日期对象创建日期对象。因此,我认为该示例仅从学术角度而言很重要。此外,这要求两个Date对象都引用完全相同的秒,而您可能只想知道它们是否引用同一天,小时或分钟。

让我们看一个更实际的例子。您正在尝试比较用户输入的生日是否与从API获得的幸运日期相同。

const userEnteredString = "12/20/1989"; // MM/DD/YYYY format
const dateStringFromAPI = "1989-12-20T00:00:00Z";

const dateFromUserEnteredString = new Date(userEnteredString)
const dateFromAPIString = new Date(dateStringFromAPI);

if (dateFromUserEnteredString.getTime() == dateFromAPIString.getTime()) {
    transferOneMillionDollarsToUserAccount();
} else {
    doNothing();
}

两者都表示同一个日期,但是很遗憾,您的用户将无法获得数百万美元。

这就是问题所在:JavaScript始终将时区假定为浏览器提供的时区,除非另有明确指定。

This means, for me, new Date ("12/20/1989") will create a date 1989-12-20T00:00:00+5:45 or 1989-12-19T18:15:00Z which is not the same as 1989-12-20T00:00:00Z in terms of time stamp.

无法仅更改现有日期对象的时区,因此我们的目标是现在创建一个新的日期对象,但要使用UTC而不是本地时区。

创建日期对象时,我们将忽略用户的时区并使用UTC。有两种方法可以做到这一点:

  1. 从用户输入日期创建ISO格式的日期字符串,并使用它创建Date对象。使用有效的ISO日期格式创建Date对象,同时非常清楚UTC与local的意图。
const userEnteredDate = "12/20/1989";
const parts = userEnteredDate.split("/");

const userEnteredDateISO = parts[2] + "-" + parts[0] + "-" + parts[1];

const userEnteredDateObj = new Date(userEnteredDateISO + "T00:00:00Z");
const dateFromAPI = new Date("1989-12-20T00:00:00Z");

const result = userEnteredDateObj.getTime() == dateFromAPI.getTime(); // true

如果您不指定时间,则该方法也适用,因为该时间默认为午夜(即00:00:00Z):

const userEnteredDate = new Date("1989-12-20");
const dateFromAPI = new Date("1989-12-20T00:00:00Z");
const result = userEnteredDate.getTime() == dateFromAPI.getTime(); // true

请记住:如果为日期构造函数传递了正确的ISO日期格式为YYYY-MM-DD的字符串,它将自动采用UTC。

  1. 的JavaScript提供了一个简洁的Date.UTC()函数,您可以使用该函数获取日期的UTC时间戳。我们从日期中提取组件并将它们传递给函数。
const userEnteredDate = new Date("12/20/1989");
const userEnteredDateTimeStamp = Date.UTC(userEnteredDate.getFullYear(), userEnteredDate.getMonth(), userEnteredDate.getDate(), 0, 0, 0);

const dateFromAPI = new Date("1989-12-20T00:00:00Z");
const result = userEnteredDateTimeStamp == dateFromAPI.getTime(); // true
...

找出两个日期之间的差异

您会遇到的常见情况是找到两个日期之间的差额。

我们讨论两个用例:

查找两个日期之间的天数

将两个日期都转换为UTC时间戳,找到以毫秒为单位的差并找到等效的天数。

const dateFromAPI = "2016-02-10T00:00:00Z";

const now = new Date();
const datefromAPITimeStamp = (new Date(dateFromAPI)).getTime();
const nowTimeStamp = now.getTime();

const microSecondsDiff = Math.abs(datefromAPITimeStamp - nowTimeStamp);

// Math.round is used instead of Math.floor to account for certain DST cases
// Number of milliseconds per day =
//   24 hrs/day * 60 minutes/hour * 60 seconds/minute * 1000 ms/second
const daysDiff = Math.round(microSecondsDiff / (1000 * 60 * 60  * 24));

console.log(daysDiff);

从用户的出生日期开始查找其年龄

const birthDateFromAPI = "12/10/1989";

注意:我们有非标准格式。阅读API文档,确定这是10月12日还是12月10日。相应地更改为ISO格式。

const parts = birthDateFromAPI.split("/");
const birthDateISO = parts[2] + "-" + parts[0] + "-" + parts[1];

const birthDate =  new Date(birthDateISO);
const today = new Date();

let age = today.getFullYear() - birthDate.getFullYear();

if(today.getMonth() < birthDate.getMonth()) {
    age--;
}

if(today.getMonth() == birthDate.getMonth() && today.getDate() < birthDate.getDate()) {
    age--;
}

我知道有更多简洁的方法可以编写此代码,但是由于逻辑的明确性,我喜欢以此方式编写代码。

避免约会地狱的建议

既然我们对日期算法感到满意,我们就可以理解要遵循的最佳实践以及遵循这些实践的原因。

从用户获取DateTime

如果你 are getting the date and time from the user, you are most probably looking for their local DateTime. We saw in the date arithmetic section that the Date constructor can accept the date in a number of different ways.

To remove any confusion, I always suggest creating a date using new Date(year, month, day, hours, minutes, seconds, milliseconds) format even if you already have the date in a valid parsable format. If all programmers in your team follow this simple rule, it will be extremely easy to maintain the code in the long run since it is as explicit as you can be with the Date constructor.

The cool part is that you can use the variations that allow you to omit any of the last four parameters if they are zero; i.e., new Date(2012, 10, 12) is the same as new Date(2012, 10, 12, 0, 0, 0, 0) because the unspecified parameters default to zero.

例如,如果使用的日期和时间选择器为您提供日期2012-10-12和时间12:30,则可以提取零件并创建一个新的Date对象,如下所示:

const dateFromPicker = "2012-10-12";
const timeFromPicker = "12:30";

const dateParts = dateFromPicker.split("-");
const timeParts = timeFromPicker.split(":");
const localDate = new Date(dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1]);

除非它采用ISO日期格式,否则请尝试避免从字符串创建日期。请改用Date(年,月,日,时,分,秒,微秒)方法。

仅获取日期

如果你 are getting only the date, a user’s birthdate for instance, it is best to convert the format to valid ISO date format to eliminate any time zone information that can cause the date to shift forward or backward when converted to UTC. For example:

const dateFromPicker = "12/20/2012";
const dateParts = dateFromPicker.split("/");
const ISODate = dateParts[2] + "-" + dateParts[0] + "-" + dateParts[1];
const birthDate = new Date(ISODate).toISOString();

In case you forgot, if you create a Date 目的 with the input in valid ISO date format (YYYY-MM-DD), it will default to UTC instead of defaulting to the browser’s time zone.

保存日期

始终将日期时间存储在UTC中。始终将ISO日期字符串或时间戳发送到后端。

在试图向用户显示正确的本地时间的痛苦经历之后,几代计算机程序员已经意识到了这个简单的事实。将本地时间存储在后端是个坏主意,最好让浏览器在前端处理到本地时间的转换。

同样,很明显,您绝不应向后端发送DateTime字符串,例如“ 1989年7月20日12:10 PM”。即使您也发送了时区,您也会加大其他程序员理解您的意图并正确解析和存储日期的工作量。

Use the toISOString() or toJSON() methods of the Date object to convert the local DateTime to UTC.

const dateFromUI = "12-13-2012";
const timeFromUI = "10:20";
const dateParts = dateFromUI.split("-");
const timeParts = timeFromUI.split(":");

const date = new Date(dateParts[2], dateParts[0]-1, dateParts[1], timeParts[0], timeParts[1]);

const dateISO = date.toISOString();

$.post("http://example.com/", {date: dateISO}, ...)

显示日期和时间

  1. 从REST API获取时间戳或ISO格式的日期。
  2. Create a Date object.
  3. Use the toLocaleString() or toLocaleDateString() and toLocaleTimeString() methods or a date library to display the local time.
const dateFromAPI = "2016-01-02T12:30:00Z";

const localDate = new Date(dateFromAPI);
const localDateString = localDate.toLocaleDateString(undefined, {  
    day:   'numeric',
    month: 'short',
    year:  'numeric',
});


const localTimeString = localDate.toLocaleTimeString(undefined, {
    hour:   '2-digit',
    minute: '2-digit',
    second: '2-digit',
});

您什么时候也应该存储本地时间?

“有时候 重要的是要知道时区 发生事件,并且转换到单个时区将无法消除该信息。

“如果您要进行市场推广,并且想知道哪些顾客在午餐时间下了订单,那么看起来似乎已经在格林尼治标准时间中午下达的订单实际上并没有在纽约早餐时提供太多帮助。”

如果你 come across this kind of situation, it would be wiser to save the local time as well. As usual, we would like to create the date in ISO format, but we have to find the time zone offset first.

The Date object’s getTimeZoneOffset() function tells us the number of minutes that when added to a given local time gives the equivalent UTC time. I suggest converting it to (+-)hh:mm format because it makes it more obvious that it is a time zone offset.

const now = new Date();
const tz = now.gettime zoneOffset();

在我的+05:45时区,我得到-345,这不仅是相反的符号,而且-345之类的数字可能会完全困扰后端开发人员。因此,我们将其转换为+05:45。

const sign = tz > 0 ? "-" : "+";

const hours = pad(Math.floor(Math.abs(tz)/60));
const minutes = pad(Math.abs(tz)%60);

const tzOffset = sign + hours + ":" + minutes;

现在,我们获取其余的值,并创建一个表示本地DateTime的有效ISO字符串。

const localDateTime = now.getFullYear() +
                      "-" +
                      pad(now.getMonth()+1) +
                      "-" +
                      pad(now.getDate()) +
                      "T" +
                      pad(now.getHours()) +
                      ":" +
                      pad(now.getMinutes()) +
                      ":" +
                      pad(now.getSeconds());

如果你 want, you can wrap the UTC and local dates in an object.

const eventDate = {
    utc: now.toISOString(),
    local: localDateTime,
    tzOffset: tzOffset,
}

Now, in the back end, if you wanted to find out if the event occurred before noon local time, you can parse the date and simply use the getHours() function.

const localDateString = eventDate.local;
const localDate = new Date(localDateString);

if(localDate.getHours() < 12) {
    console.log("Event happened before noon local time");
}

We didn’t use the tzOffset here, but we still store it because we might need it in the future for debugging purposes. You could actually just send the time zone offset and UTC time only. But I like to store the local time too because you will eventually have to store the date in a database and having the local time stored separately allows you to directly query based on a field rather than having to perform calculations to get the local date.

有时,即使已存储本地时区,您仍要显示特定时区中的日期。例如,如果事件是虚拟的,则在当前用户的时区中可能更有意义;如果不是虚拟事件,则在实际发生的时区中可能更有意义。无论如何,值得一看 既定的解决方案 用于使用显式时区名称进行格式化。

服务器和数据库配置

始终将服务器和数据库配置为使用UTC时区。 (请注意,UTC和GMT是 不一样的东西—例如,格林尼治标准时间(GMT)可能意味着在夏季切换到BST,而UTC则永远不会这样做。

我们已经看到时区转换可以带来多大的痛苦,尤其是在无意中时。始终发送UTC DateTime并将服务器配置为UTC时区可以使您的生活更轻松。您的后端代码将变得更加简单和整洁,因为它无需进行任何时区转换。来自世界各地服务器的DateTime数据可以轻松进行比较和排序。

后端中的代码应能够假定服务器的时区为UTC(但仍应进行检查以确保正确)。每次编写新的DateTime代码时,只需进行简单的配置检查,就无需考虑和编码转换。

是时候更好地处理日期了

日期操纵是一个难题。本文中的实际示例背后的概念不仅仅适用于JavaScript,而且仅仅是正确处理DateTime数据和计算的起点。另外,每个帮助程序库都将附带 自己的一套 的 nuances.

最重要的是:在后端使用ISO,并保留前端以为用户正确格式化内容。专业程序员将意识到其中的一些细微差别,并且将(更加确定地)在后端和前端使用受良好支持的DateTime库。数据库方面的内置函数是另外一回事了,但是希望本文能够提供足够的背景,以便在这种情况下做出更明智的决策。