如何格式化日期范围,例如 1 月 1 日 - 1 月 5 日

使用 JavaScript 以符合本地习惯的格式显示日期范围,并智能去除冗余

介绍

日期范围在网络应用中随处可见。预订系统显示从 1 月 1 日到 1 月 5 日的可用性,活动日历显示多日会议,分析仪表板显示特定时间段的数据,报告涵盖财务季度或自定义日期范围。这些范围传达了某些内容适用于两个端点之间的所有日期。

当您通过将两个格式化日期用连字符连接来手动格式化日期范围时,会生成不必要冗长的输出。从 2024 年 1 月 1 日到 2024 年 1 月 5 日的范围不需要在第二个日期中重复月份和年份。输出 "2024 年 1 月 1 日 - 5 日" 通过省略冗余部分更简洁地传达了相同的信息。

JavaScript 提供了 Intl.DateTimeFormatformatRange() 方法来自动处理日期范围格式化。此方法确定在范围的每个部分中包含哪些日期组件,应用特定语言环境的分隔符和格式约定,同时去除不必要的重复。

为什么日期范围需要智能格式化

不同的日期范围需要不同的详细程度。同一个月内的范围需要的信息比跨越多个年份的范围少。最佳格式取决于起始日期和结束日期之间哪些日期组件不同。

对于同一天内的范围,您只需显示时间差:"上午 10:00 - 下午 2:00"。重复显示两个时间的完整日期并不会增加信息量。

对于同一个月内的范围,您只需显示一次月份并列出两个日期:"2024 年 1 月 1 日 - 5 日"。重复 "1 月" 会使输出更难阅读而不会增加清晰度。

对于同一年内跨越不同月份的范围,您需要显示两个月份,但可以省略第一个日期的年份:"2024 年 12 月 25 日 - 2025 年 1 月 2 日" 需要完整信息,但 "2024 年 1 月 15 日 - 2 月 20 日" 在某些语言环境中可以省略第一个日期的年份。

对于跨越多个年份的范围,您必须为两个日期都包含年份:"2023 年 12 月 1 日 - 2024 年 3 月 15 日"。

手动实现这些规则需要检查哪些日期组件不同并相应地构造格式字符串。不同的语言环境也会以不同的方式应用这些规则,使用不同的分隔符和排序约定。formatRange() 方法封装了这些逻辑。

使用 formatRange 格式化日期范围

formatRange() 方法接受两个 Date 对象并返回一个格式化的字符串。创建一个带有所需语言环境和选项的 Intl.DateTimeFormat 实例,然后使用开始日期和结束日期调用 formatRange()

const formatter = new Intl.DateTimeFormat("en-US");

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// 输出: "1/1/24 – 1/5/24"

格式化器将默认的美式英语日期格式应用于两个日期,并用短横线连接它们。对于同一个月内的日期,输出仍然显示两个完整的日期,因为默认格式非常简短。

您可以使用与常规日期格式化相同的选项来指定要包含的日期组件。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// 输出: "January 1 – 5, 2024"

现在,格式化器智能地省略了第二个日期的月份和年份,因为它们与第一个日期相同。输出仅显示结束日期的日期数字,使范围更易读。

formatRange 如何优化日期范围输出

formatRange() 方法会检查两个日期并确定哪些组件不同。它仅在范围的每个部分中包含必要的组件。

对于同一个月和年份内的日期,第二部分仅显示结束日期。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "January 1 – 5, 2024"

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 0, 20)
));
// 输出: "January 15 – 20, 2024"

两个范围都只显示一次月份和年份,并用范围分隔符连接日期数字。

对于同一年中不同月份的日期,两个月份都会显示,但年份只显示一次。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// 输出: "January 15 – February 20, 2024"

格式化器包括两个月份名称,但将年份放在末尾,适用于整个范围。

对于跨年份的日期,两个完整的日期都会显示。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2023, 11, 25),
  new Date(2024, 0, 5)
));
// 输出: "December 25, 2023 – January 5, 2024"

每个日期都包含其完整的年份,因为年份不同。格式化器无法省略任何一个年份,否则会造成歧义。

格式化日期和时间范围

当您的格式包含时间组件时,formatRange() 方法会对时间字段应用相同的智能省略。

对于同一天的时间,仅输出中时间组件不同。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 14, 30);

console.log(formatter.formatRange(start, end));
// 输出: "2024年1月1日 上午10:00 – 下午2:30"

日期只显示一次,后跟时间范围。格式化器识别出重复显示结束时间的完整日期和时间不会增加有用信息。

对于不同日期的时间,完整的日期和时间值都会显示。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 2, 14, 30);

console.log(formatter.formatRange(start, end));
// 输出: "2024年1月1日 上午10:00 – 2024年1月2日 下午2:30"

由于它们表示不同的日期,因此日期和时间都会显示。格式化器无法安全地省略任何组件。

在不同语言环境中格式化日期范围

范围格式会根据每个语言环境的日期组件顺序、分隔符以及省略哪些组件的约定进行调整。

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "2024年1月1日 – 5日"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "1.–5. Januar 2024"

const jaFormatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(jaFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "2024年1月1日~5日"

英文将月份放在首位,年份显示在最后。德文将日期数字放在首位并用句点分隔,然后是月份名称和年份。日文使用年-月-日的顺序,并用波浪号(~)作为范围分隔符。每种语言环境都会根据其约定决定哪些组件显示一次,哪些显示两次。

这些差异也适用于跨月份的范围。

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// 输出: "2024年1月15日 – 2月20日"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// 输出: "15. Januar – 20. Februar 2024"

const frFormatter = new Intl.DateTimeFormat("fr-FR", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(frFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// 输出: "15 janvier – 20 février 2024"

所有三种语言环境都显示了两个月份的名称,但它们相对于日期数字的位置不同。格式化器会自动处理这些差异。

使用不同样式格式化日期范围

您可以使用 dateStyle 选项来控制整体格式的长度,就像单个日期格式化一样。

const shortFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short"
});

console.log(shortFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "1/1/24 – 1/5/24"

const mediumFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium"
});

console.log(mediumFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "Jan 1 – 5, 2024"

const longFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "long"
});

console.log(longFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "January 1 – 5, 2024"

const fullFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "full"
});

console.log(fullFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// 输出: "Monday, January 1 – Friday, January 5, 2024"

short 样式生成数字日期,并且不会应用智能省略,因为格式已经很紧凑。mediumlong 样式会缩写或拼写出月份,并省略冗余的部分。full 样式会为两个日期都包含星期名称。

使用 formatRangeToParts 进行自定义样式

formatRangeToParts() 方法返回一个对象数组,表示格式化范围的各个组成部分。这使您可以对范围输出的各个部分进行样式化或操作。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

console.log(parts);

输出是一个对象数组,每个对象包含 typevaluesource 属性。

[
  { type: "month", value: "January", source: "startRange" },
  { type: "literal", value: " ", source: "startRange" },
  { type: "day", value: "1", source: "startRange" },
  { type: "literal", value: " – ", source: "shared" },
  { type: "day", value: "5", source: "endRange" },
  { type: "literal", value: ", ", source: "shared" },
  { type: "year", value: "2024", source: "shared" }
]

type 属性标识组成部分:月份、日期、年份或文字文本。value 属性包含格式化的文本。source 属性指示该组成部分属于开始日期、结束日期,还是两者共享。

您可以使用这些部分创建带有不同组件样式的自定义 HTML。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

let html = "";

parts.forEach(part => {
  if (part.type === "month") {
    html += `<span class="month">${part.value}</span>`;
  } else if (part.type === "day") {
    html += `<span class="day">${part.value}</span>`;
  } else if (part.type === "year") {
    html += `<span class="year">${part.value}</span>`;
  } else if (part.type === "literal" && part.source === "shared" && part.value.includes("–")) {
    html += `<span class="separator">${part.value}</span>`;
  } else {
    html += part.value;
  }
});

console.log(html);
// 输出: <span class="month">January</span> <span class="day">1</span><span class="separator"> – </span><span class="day">5</span>, <span class="year">2024</span>

此技术在保留特定于区域设置的格式的同时,允许对不同组件进行自定义视觉样式。

当日期相等时会发生什么

当您为 startend 参数传递相同的日期时,formatRange() 会输出一个格式化的单一日期,而不是一个范围。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2024, 0, 1);

console.log(formatter.formatRange(date, date));
// 输出: "2024年1月1日"

格式化器会识别出具有相同起点和终点的范围实际上并不是一个真正的范围,因此将其格式化为单一日期。即使 Date 对象是不同的实例但具有相同的值,这种行为仍然适用。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 10, 0);

console.log(formatter.formatRange(start, end));
// 输出: "2024年1月1日"

尽管这些是单独的 Date 对象,但它们表示相同的日期和时间。由于范围在格式选项的精度级别上持续时间为零,格式化器会输出单一日期。由于格式中不包含时间组件,因此时间部分无关紧要,日期被视为相等。

何时使用 formatRange 而非手动格式化

在向用户显示日期范围时使用 formatRange()。这适用于预订日期范围、事件持续时间、报告周期、可用时间窗口或任何其他时间跨度。该方法确保了正确的特定语言环境格式化和最佳的组件省略。

当需要显示多个不相关的日期时,请避免使用 formatRange()。例如,像 "1月1日, 1月15日, 2月1日" 这样的截止日期列表应该为每个日期分别使用常规的 format() 调用,而不是将它们视为范围。

在显示日期之间的比较或差异时,也应避免使用 formatRange()。如果您需要显示一个日期比另一个日期早多少或晚多少,这表示的是相对时间计算,而不是范围。