Javascript Intl.DateTimeFormat() 'islamic' 和 'ar-SA' 之间的伊斯兰(Hijri)日历输出存在差异

Discrepancy in Javascript Intl.DateTimeFormat() Outputs for the Islamic (Hijri) Calendar between 'islamic' and 'ar-SA'

2022 年 3 月 3 日是回历月(今年回历 1443 AH)的结束日;即 30 Rajab 1443 AH.

根据所有网站、应用程序、MS Office 和 Windows 日历,回历 1443 年回历为 30 天。

当使用 javascript Intl.DateTimeFormat() 使用 islamic 日历选项显示 3 March 2022 的伊斯兰(回历)日期时, 它将给出伊斯兰回历日期 (1 Shaʻban 1443 AH)。此结果是 Rajab 月后的一天(即下个月的 1 号),它计算出 Rajab 月为 29 天而不是 30 天。

但是,如果传递给 Intl.DateTimeFormat() 的选项是 ar-SA(即阿拉伯-沙特阿拉伯),它将给出正确的结果。 这很奇怪,因为 ar-SA 语言环境默认使用伊斯兰 (Hijri) 日历。

这是 error/bug 还是 javascript 的正确内部工作原理?

除了使用 'ar-SA' 语言环境(但不使用外部库)之外,是否有更可靠的方法来获取 Javascript 中的伊斯兰日期?

参见下面的代码示例:

我已经在节点和 chrome 中对此进行了测试,它给出了相同的结果差异。

let date = new Date("2022-03-03");
let options = {year:'numeric', month:'long',day:'numeric'};

let format = new Intl.DateTimeFormat('ar-SA-u-nu-latn', options);
console.log("3 March 2022 with 'ar-SA'         :"+format.format(date)+ " ==> means: 30 Rajab 1443 AH");

format = new Intl.DateTimeFormat('en-u-ca-islamic-nu-latn', options);
console.log("3 March 2022 with Islamic Calendar: "+format.format(date));

您看到的“相差一个”日期问题可能有以下三个原因:

  1. 日期初始化和日期格式化之间的时区不匹配
  2. 使用错误的伊斯兰日历(JS 实现通常提供 5 种不同的伊斯兰日历!)
  3. 用于 JS 日历计算的 ICU 库中的错误

我将在下面逐一介绍。

1.日期初始化和日期格式之间的时区不匹配

off-by-one-day 错误的最常见原因是(如 @RobG 在他上面的评论中指出的那样)声明 Date 值时使用的时区与时区不匹配在您想要的日历中格式化时使用。

当您使用 ISO 8601 字符串初始化 Date 实例时,Date 实例中存储的实际值是自 1970 年 1 月 1 日 UTC 以来的毫秒数。根据您的系统时区,new Date('2022-02-03') 可以是您系统时区的 2 月 3 日或 2 月 2 日。避免此问题的一种方法是在格式化时也使用 UTC:

new Date('2022-03-03').toLocaleDateString('en-US');
// outputs '3/2/2022' when run in San Francisco

new Date('2022-03-03').toLocaleDateString('en-US', { timeZone: 'UTC' });
// outputs '3/3/2022' as expected. UTC is used both for declaring and formatting.

new Date('2022-03-03').toLocaleDateString(
  'en-SA-u-ca-islamic-umalqura',
  { timeZone: 'UTC', month: 'long', day: 'numeric', year: 'numeric' }
);
// outputs 'Rajab 30, 1443 AH' as expected

请注意,我在上面使用了 Date.toLocaleDateString,但结果与您使用 Intl.DateTimeFormat.format 时的结果相同。两种方法的参数和实现相同。

2。使用错误的伊斯兰日历(JS 实现通常提供 5 种不同的伊斯兰日历!)

第二个更微妙的问题是 JavaScript 使用了多种伊斯兰历法。支持的日历列表在这里:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendars。摘自该页面,以下是支持的伊斯兰日历变体:

  • islamic
    • Islamic calendar
  • islamic-umalqura
    • Islamic calendar, Umm al-Qura
  • islamic-tbla
    • Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch)
  • islamic-civil
    • Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch)
  • islamic-rgsa
    • Islamic calendar, Saudi Arabia sighting

(还有一个已弃用的 islamicc 日历,但应改用 islamic-civil。)

ar-SA 语言环境的默认日历是 islamic-umalqura 日历,而不是 islamic 日历。验证:

new Intl.DateTimeFormat('ar-SA').resolvedOptions();
// {
//   calendar: "islamic-umalqura"
//   day: "numeric"
//   locale: "ar-SA"
//   month: "numeric"
//   numberingSystem: "arab"
//   timeZone: "America/Los_Angeles"
//   year: "numeric"
// }

不同的伊斯兰历变化会产生不同的日历日期。例如:

calendars = ["islamic", "islamic-umalqura", "islamic-tbla", "islamic-civil", "islamic-rgsa"];
options = { timeZone: 'UTC', month: 'long', day: 'numeric', year: 'numeric' };
date = new Date('2022-03-03');
calendars.forEach(calendar => {
  formatted = date.toLocaleDateString(`en-SA-u-ca-${calendar}`, options);
  console.log(`${calendar}: ${formatted}`);
});
// The code above outputs the following:
//   islamic: Shaʻban 1, 1443 AH
//   islamic-umalqura: Rajab 30, 1443 AH
//   islamic-tbla: Rajab 30, 1443 AH
//   islamic-civil: Rajab 29, 1443 AH
//   islamic-rgsa: Shaʻban 1, 1443 AH

3。用于JS的日历计算的ICU库中的错误

出现意外日期的第三个可能原因是 JS 引擎中的日历计算代码存在错误。据我所知,所有主流浏览器都将日历计算委托给一个名为 ICU. If you're using the correct time zone and calendar variation and there's still a problem with the calculation, then you may want to try filing an issue in the ICU JIRA site: https://unicode-org.atlassian.net/jira/software/c/projects/ICU/issues/.

的库

顺便说一句,在回答这个问题时,我注意到 Intl.DateTimeFormat constructor where the list of supported calendars is wrong. I filed https://github.com/mdn/content/pull/12764 的 MDN 文档中有一个错误来修复内容。此 PR 已合并,但生产 MDN 站点可能需要一段时间才能使用固定内容更新。

根据Justin Grant下面提供的优秀信息和数据,我对五(5)个伊斯兰历的输出进行了比较:

  1. 伊斯兰
  2. islamic-umalqura
  3. islamic-tbla
  4. islamic-civil
  5. islamic-rgsa

输出比较是在 50 年(从 1980 年 1 月 1 日到 2030 年 12 月 31 日)期间进行的

结果输出如下:

============================================================
islamic diff from islamic-tbla           :6369  days (34.19%)
islamic diff from islamic-rgsa           :0     days ( 0.00%)
islamic diff from islamic-umalqura       :11425 days (61.33%)
islamic diff from islamic-civil          :15919 days (85.46%)
------------------------------------------------------------
islamic-tbla diff from islamic-rgsa      :6369  days (34.19%)
islamic-tbla diff from islamic-umalqura  :10777 days (57.85%)
islamic-tbla diff from islamic-civil     :18628 days (100.00%)
-------------------------------------------------------------
islamic-rgsa diff from islamic-umalqura  :11425 days (61.33%)
islamic-rgsa diff from islamic-civil     :15919 days (85.46%)
-------------------------------------------------------------
islamic-umalqura diff from islamic-civil :9049  days (48.58%)
=============================================================

结果给出了以下一般信息:

默认 islamic 日历下的日期始终与 islamic-rgsa 一致。不确定默认 islamic 使用与 islamic rgsa 相同的代码还是根据 region.

选择其他类型

islamic-civil 日历下的日期与所有其他日历格式的差异最大(在 85% 到 100% 之间)。

islamic-umalqura 日历中的日期与其他日历相差大约 50% 到 62%。

根据对一些伊斯兰网站和应用程序的观察,新的 islamic-umalqura 日历是最常用的伊斯兰日历。在欧洲和北美也是首选。

因为区域设置 ar-SA 默认使用 islamic-umalqura 日历,ar-SA 的日期输出(和月份长度)将不同于islamic 超过 61% 的时间使用日历。

下面提供了我使用过的测试代码,但请注意它需要大约25秒才能给出输出;敬请期待.

console.log("Comparing 50 years results under the 5 islamic calendar formats (from year 1980 to 2030)");
console.log("=".repeat(60));

let startDate = new Date("1980-01-01");  // date to start test from
let options   = {year: 'numeric', month: 'long', day: 'numeric'};
let d         = startDate;
let diff      = [0,0,0,0,0,0,0,0,0,0];  // array to hold the diff results
let totalDays = 18628;                  // total days approx 50 Gregorian years
for (let i=0; i<totalDays; i++) {
let dateIslamic     = new Intl.DateTimeFormat('en-u-ca-islamic'         ,options).format(d),
    dateIslamicTBLA = new Intl.DateTimeFormat('en-u-ca-islamic-tbla'    ,options).format(d),
    dateIslamicRGSA = new Intl.DateTimeFormat('en-u-ca-islamic-rgsa'    ,options).format(d),
    dateIslamicUMQ  = new Intl.DateTimeFormat('en-u-ca-islamic-umalqura',options).format(d),
    dateIslamicCIVL = new Intl.DateTimeFormat('en-u-ca-islamic-civil'   ,options).format(d);

if (dateIslamic != dateIslamicTBLA) diff[0]++;
if (dateIslamic != dateIslamicRGSA) diff[1]++;
if (dateIslamic != dateIslamicUMQ)  diff[2]++;
if (dateIslamic != dateIslamicCIVL) diff[3]++;

if (dateIslamicTBLA != dateIslamicRGSA) diff[4]++;
if (dateIslamicTBLA != dateIslamicUMQ)  diff[5]++;
if (dateIslamicTBLA != dateIslamicCIVL) diff[6]++;

if (dateIslamicRGSA != dateIslamicUMQ)  diff[7]++;
if (dateIslamicRGSA != dateIslamicCIVL) diff[8]++;

if (dateIslamicUMQ != dateIslamicCIVL)  diff[9]++;

d    = new Date(d.setDate(d.getDate() + 1)); // next day
}

console.log("islamic diff from islamic-tbla           :"+perc(0));
console.log("islamic diff from islamic-rgsa           :"+perc(1));
console.log("islamic diff from islamic-umalqura       :"+perc(2));
console.log("islamic diff from islamic-civil          :"+perc(3));
console.log("-".repeat(50));
console.log("islamic-tbla diff from islamic-rgsa      :"+perc(4));
console.log("islamic-tbla diff from islamic-umalqura  :"+perc(5));
console.log("islamic-tbla diff from islamic-civil     :"+perc(6));
console.log("-".repeat(50));
console.log("islamic-rgsa diff from islamic-umalqura  :"+perc(7));
console.log("islamic-rgsa diff from islamic-civil     :"+perc(8));
console.log("-".repeat(50));
console.log("islamic-umalqura diff from islamic-civil :"+perc(9));


function perc(n) {return + diff[n]+" days (" +((diff[n]/totalDays)*100).toFixed(2)+"%)";}