如何将任何一个伊斯兰教(伊斯兰历)的五个日期转换为18个世界日历之一,而无需使用外部库或复杂的天文公式

14

通过搜索StackOverflow和CodeReview网站,本问题/帖子中描述的方法尚未尝试过,该方法是在JavaScript中将5个伊斯兰历日转换为任何18种可用日历之一,而不使用外部日期库或复杂的数学/天文公式。

希望此帖符合StackOverflow我能回答自己的问题吗?推荐的政策准则,与社区共享新的概念代码。

Javascript Intl.DateTimeFormat()

我们都知道JavaScript有一个内置的方法(Intl.DateTimeFormat())可以将公历日期转换为各种日历日期(18种世界日历),包括输出字符串的格式化。

然而,JavaScript没有提供内置方法来完成反向操作,即将18种世界日历日期(包括伊斯兰历日期)转换回公历日期或其他日历。对于这种目的,您需要使用外部日期库进行转换,例如'moment.js'等许多其他库。

此处使用的方法/代码不使用外部库,并使用JavaScript Calendar Conversion by Target Approximation Method。下面的流程图总结了该方法的概念,该方法可应用于将其他日历转换为其余日历。

下面的短Javascript函数提供了将五个(5)伊斯兰(希吉)历日期(从伊斯兰年-280,803 AH到+281,510 AH)转换为以下任何18种Javascript日历的工具,其中包括格式化输出的选项:

"buddhist",“chinese”,“coptic”,“dangi”,“ethioaa”,“ethiopic”,“gregory”,“hebrew”,“indian”,“islamic”,“islamic-umalqura”,“islamic-tbla”,“islamic-civil”,“islamic-rgsa”,“iso8601”,“japanese”,“persian”,“roc”,“islamicc”。

该方法不使用外部库,也不使用复杂的数学或天文公式,仅依赖于JavaScript内置的日历转换算法,这些算法又基于ICU代码[https://icu.unicode.org/]。

这种方法确保输出始终准确,并完全符合JavaScript引擎输出的要求。

虽然一些外部库非常好,但它们随着时间的推移往往会失去更新和支持。一个例子是最近关于'moment.js'库的情况。

语法

函数语法为:

hijriToCalendars(year, month, day, [options])

该函数在最简单的形式下默认将转换为最常见和最近使用的伊斯兰教历法'islamic-umalqura'

它还将默认使用ISO日期格式将其转换为Gregorian日历。

例如:将伊斯兰教日期21 Rajab 1443(即21/07/1443)转换为公历日期。

hijriToCalendars(1443,7,21);

output: 2022-02-22T00:00:00.000Z    // default output Gregorian ISO format

如何将伊斯兰历转换为其他日历(例如“波斯”日历):

 hijriToCalendars(1443,7,21, { toCal: "persian" });

 output: 12/3/1400 AP

要将格式应用于输出,可以使用JavaScript的Intl.DateTimeFormat()方法中的'dateStyle'选项。

例如:使用完整的dateStyle将伊斯兰日期转换为波斯日期

 hijriToCalendars(1443,7,21, { toCal: "persian", dateStyle: "full" });

 output: Tuesday, Esfand 3, 1400 AP

示例:使用阿拉伯语区域设置将伊斯兰日期转换为希伯来语

 hijriToCalendars(1443,7,21,{ toCal:"hebrew", dateStyle: "full", locale:"ar"})

 output: الثلاثاء، ٢١ آذار الأول ٥٧٨٢ ص

以上方法也可用于其他18个日历。

新增功能是无需转换即可将伊斯兰日期格式化为任何可用的'dateStyles''locales'之一。

只需将'toCal'设置为与输入的伊斯兰日历相同,对于默认值使用:'islamic-umalqura'

例:使用波斯语区域设置格式化伊斯兰日期

 hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"fa"}));

 output:  سه‌شنبه ۲۱ رجب ۱۴۴۳ ه‍.ق.          // mind the RTL requirements

示例:在印地语区域设置中格式化伊斯兰教日期

 hijriToCalendars(1443,7,21,{ toCal : "islamic-umalqura", dateStyle : "full", locale : "hi"}));

 output: AH मंगलवार, 21 रजब 1443

默认的输入伊斯兰日历是 'islamic-umalqura',但是你可以使用 'fromCal' 选项将其更改为五种伊斯兰日历中的任何一种。

例子:将伊斯兰-民用日期 21 Rajab 1443(即 21/07/1443)转换为公历日期。

 hijriToCalendars(1443,7,21, {fromCal : "islamic-civil" });

 output: 2022-02-23T00:00:00.000Z
从上面可以看出,islamic-civil日期与islamic-umalqura日期相差1天。 您可以使用Intl.DateTimeFormat()中提供的所有选项来格式化输出日期。

无效的伊斯兰教日期

如果将无效的伊斯兰教日期传递给函数,将生成错误消息Invalid islamic-xxxxx date!。 无效的伊斯兰教日期是指日期在月份或日期/月份不正确的日期。 例如,伊斯兰教日期1443/2/30是无效的,因为希吉历的第二个月(“ Safar”月)始终为29天,不能是30天。 另外,例如,伊斯兰教日期1442/12/30会抛出错误,因为1442年是非闰年,并且第12个月只有29天。

这种方法可靠和准确吗?

如果您认为内部的Javascript ICU代码是可靠和准确的,则此方法仅使用从Javascript引擎内部生成的日期。除非它们位于Javascript引擎本身内部,否则不需要担心外部不正确的数学或代码错误/缺陷。 下面是一个具有示例测试用例以进行转换和格式化的函数,可运行并进行测试。 提供了用于将伊斯兰教日期(1443/7/21)转换为所有其他日历格式的示例。 欢迎提出任何改进/建议/替代方案。

/**********************************************************************************
* @function  : hijriToCalendars(year, month, day, [options])
*
* @purpose   : Converts Islamic (Hijri) Date to other Calendars' Dates.
*              Handles Hijri dates from -280,803 AH to +281,510 AH.
*              Handles all 5 Islamic Calendar Types.
*              Uses the 'JS Calendar Conversion by Target Approximation' Method.
*              No external libraries or complex mathematical/astronomical formulas.
*
* @version   : 1.00
* @author    : Mohsen Alyafei
* @date      : 21 Feb 2022
* @licence   : MIT
* @param     : year   : (numeric) [required] Hijri year  (-280803 to 281510)
* @param     : month  : (numeric) [required] hijri month (1 to 12) note: months is standard 1 based
* @param     : day    : (numeric) [required] hijri day   (1 to 29/30)
* @param     : options: Object with the following optional parameters:
*
*              'fromCal': Specifies the the type of input Islamic Calendar with 5 options:
*                         - 'islamic-umalqura' (default)
*                         - 'islamic-civil'
*                         - 'islamic-tbla'
*                         - 'islamic-rgsa'
*                         - 'islamic'
*
*              'toCal' : Specifies the the type of output Calendar to convert to with 19 Calendars:
*                        - "gregory" : (default)
*                        - "buddhist", "chinese", "coptic", "dangi", "ethioaa", "ethiopic",
*                          "hebrew", "indian", "islamic", "islamic-umalqura", "islamic-tbla",
*                          "islamic-civil", "islamic-rgsa", "iso8601", "japanese", "persian", "roc".
*
*               'dateStyle' Same as used in the Intl.DateTimeFormat() constructor.
*                           If not stated, default output is in Gregorian ISO Format: YYYY:MM:DDTHH:mm:ss.sssZ
*
*               'locale' The BCP 47 language tag for formatting (default is 'en').
*
*               Other options: As used in the Intl.DateTimeFormat() constructor.
*
* @returns   : Return the date in the calendar and format of the specified options.
***********************************************************************************/



//**********************************************************************************
function hijriToCalendars(year, month, day, op={}) {
 op.fromCal ??= "islamic-umalqura";   //
let   gD      = new Date(Date.UTC(2000,0,1));
      gD      = new Date(gD.setUTCDate(gD.getUTCDate() +
                ~~(227022+(year+(month-1)/12+day/354)*354.367)));
const gY      = gD.getUTCFullYear(gD)-2000,
      dFormat = new Intl.DateTimeFormat('en-u-ca-' + op.fromCal, {dateStyle:'short', timeZone:'UTC'});
      gD      = new Date(( gY < 0 ? "-" : "+")+("00000" + Math.abs(gY)).slice(-6)+"-"+("0" + (gD.getUTCMonth(gD)+1)).slice(-2)+"-" + ("0" + gD.getUTCDate(gD)).slice(-2));
let [iM,iD,iY]= [...dFormat.format(gD).split("/")], i=0;
      gD      = new Date(gD.setUTCDate(gD.getUTCDate() +
                ~~(year*354+month*29.53+day-(iY.split(" ")[0]*354+iM*29.53+iD*1)-2)));
while (i < 4) {
   [iM,iD,iY] = [...dFormat.format(gD).split("/")];
   if (iD == day && iM == month && iY.split(" ")[0] == year) return formatOutput(gD);
   gD = new Date(gD.setUTCDate(gD.getUTCDate()+1)); i++;
   }
throw new Error("Invalid "+op.fromCal+" date!");
function formatOutput(gD){
return "toCal"in op ? (op.calendar= op.toCal,
    new Intl.DateTimeFormat(op.locale ??= "en", op).format(gD)) : gD;
}
}
//**********************************************************************************





//==========================================================
// Test Units
//==========================================================
console.log("=".repeat(60));
console.log("Convert the Hijri (Islamic) Date '1443-07-21' to other calendars:");
console.log("input to function ==>: hijriToCalendars(1443,7,21, option)");
console.log("=".repeat(60));

console.log("Default (Gregory) ISO format     : ",hijriToCalendars(1443,7,21)); // convert default islamic-umalqura date to default gregorian date
console.log("Gregory 'full' format            : ",hijriToCalendars(1443,7,21,{toCal:"gregory",dateStyle:"full"}));

console.log("Persian no format                : ",hijriToCalendars(1443,7,21,{toCal:"persian"}));
console.log("Persian 'medium' format          : ",hijriToCalendars(1443,7,21,{toCal:"persian",dateStyle:"medium"}));
console.log("Persian 'full' format            : ",hijriToCalendars(1443,7,21,{toCal:"persian",dateStyle:"full"}));
console.log("Persian 'full' format 'fa' locale: ",hijriToCalendars(1443,7,21,{toCal:"persian",dateStyle:"full",locale:"fa"}));

console.log("Hebrew no format                 : ",hijriToCalendars(1443,7,21,{toCal:"hebrew"}));
console.log("Hebrew 'full' format             : ",hijriToCalendars(1443,7,21,{toCal:"hebrew",dateStyle:"full"}));
console.log("Hebrew 'full' format 'ar' locale : ",hijriToCalendars(1443,7,21,{toCal:"hebrew",dateStyle:"full",locale:"ar"}));

console.log("Indian no format                 : ",hijriToCalendars(1443,7,21,{toCal:"indian"}));
console.log("Indian 'medium' format           : ",hijriToCalendars(1443,7,21,{toCal:"indian",dateStyle:"medium"}));
console.log("Indian 'full' format             : ",hijriToCalendars(1443,7,21,{toCal:"indian",dateStyle:"full"}));
console.log("Indian 'full' format 'hi' locale : ",hijriToCalendars(1443,7,21,{toCal:"indian",dateStyle:"full",locale:"hi"}));
console.log("Indian 'full' format 'in' locale : ",hijriToCalendars(1443,7,21,{toCal:"indian",dateStyle:"full",locale:"in"}));
console.log("Chinese no format                : ",hijriToCalendars(1443,7,21,{toCal:"chinese"}));
console.log("Chinese 'full' format            : ",hijriToCalendars(1443,7,21,{toCal:"chinese",dateStyle:"full"}));
console.log("Chinese 'full' format 'zh' locale: ",hijriToCalendars(1443,7,21,{toCal:"chinese",dateStyle:"full",locale:"zh-CN"}));

console.log("Coptic 'full' format             : ",hijriToCalendars(1443,7,21,{toCal:"coptic",dateStyle:"full"}));
console.log("Coptic 'full' format 'ar' locale : ",hijriToCalendars(1443,7,21,{toCal:"coptic",dateStyle:"full",locale:"ar"}));

console.log("Dangi (Korean) 'full' format     : ",hijriToCalendars(1443,7,21,{toCal:"dangi",dateStyle:"full"}));
console.log("R.O.C. (Minguo) 'full' format    : ",hijriToCalendars(1443,7,21,{toCal:"roc",dateStyle:"full"}));
console.log("Japanese 'full' format           : ",hijriToCalendars(1443,7,21,{toCal:"japanese",dateStyle:"full"}));
console.log("Ethioaa 'full' format            : ",hijriToCalendars(1443,7,21,{toCal:"ethioaa",dateStyle:"full"}));
console.log("Ethiopic 'full' format           : ",hijriToCalendars(1443,7,21,{toCal:"ethiopic",dateStyle:"full"}));
console.log("Buddhist 'full' format           : ",hijriToCalendars(1443,7,21,{toCal:"buddhist",dateStyle:"full"}));

//console.log("");
console.log("=".repeat(60));
console.log("Format the input Hijri Date in different locales without conversion:");
console.log("=".repeat(60));
console.log("Islamic-umalqura 'ar' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"ar"}));
console.log("Islamic-umalqura 'en' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"en"}));
console.log("Islamic-umalqura 'fa' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"fa"}));
console.log("Islamic-umalqura 'hi' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"hi"}));
console.log("Islamic-umalqura 'id' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"id"}));
console.log("Islamic-umalqura 'pa' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"pa"}));
console.log("Islamic-umalqura 'ma' locale   : ",hijriToCalendars(1443,7,21,{toCal:"islamic-umalqura",dateStyle:"full", locale:"ma"}));
console.log("Islamic-cvil 'ar' locale       : ",hijriToCalendars(1443,7,21,{toCal:"islamic-civil",dateStyle:"full", locale:"ar"}));

//console.log("");
console.log("=".repeat(60));
console.log("Convert Max Negative and Max Positive Hijri Dates to Gregorian");
console.log("=".repeat(60));
console.log("Maximum Negative Date : ",hijriToCalendars(-280803,12,22)); // max negative hijri date
console.log("Maximum Positive Date : ",hijriToCalendars(281510,12,29));  // max positive hijri date
//console.log("=".repeat(60));

// Other Test Cases
var r=0; // test tracker
r |= test(1,1,1,{},"0622-07-19T00:00:00.000Z");
r |= test(622,7,18,{},"1225-08-02T00:00:00.000Z");
r |= test(1443,7,21,{},"2022-02-22T00:00:00.000Z");
r |= test(1443,7,14,{},"2022-02-15T00:00:00.000Z");
r |= test(1443,9,1,{},"2022-04-02T00:00:00.000Z");
r |= test(2000,9,1,{},"2562-09-01T00:00:00.000Z");
r |= test(2100,9,1,{},"2659-09-10T00:00:00.000Z");
r |= test(2200,9,1,{},"2756-09-17T00:00:00.000Z");
r |= test(2300,9,1,{},"2853-09-25T00:00:00.000Z");
r |= test(2400,9,1,{},"2950-10-04T00:00:00.000Z");
r |= test(2443,9,1,{},"2992-06-22T00:00:00.000Z");
r |= test(3443,9,1,{},"3962-09-13T00:00:00.000Z");
r |= test(4443,9,1,{},"4932-12-03T00:00:00.000Z");
r |= test(5443,9,1,{},"5903-02-23T00:00:00.000Z");
r |= test(6443,9,1,{},"6873-05-14T00:00:00.000Z");
r |= test(6550,7,14,{},"6977-01-20T00:00:00.000Z");
r |= test(7443,9,1,{},"7843-08-05T00:00:00.000Z");
r |= test(8443,9,1,{},"8813-10-24T00:00:00.000Z");
r |= test(9443,9,1,{},"9784-01-14T00:00:00.000Z");
r |= test(10443,9,1,{},"+010754-04-06T00:00:00.000Z");
r |= test(150443,9,1,{},"+146585-06-23T00:00:00.000Z");
r |= test(1443,4,29,{fromCal:"islamic"},"2021-12-03T00:00:00.000Z");
r |= test(1443,7,21,{fromCal:"islamic-civil"},"2022-02-23T00:00:00.000Z");
r |= test(1443,7,21,{fromCal:"islamic"},"2022-02-22T00:00:00.000Z");
r |= test(102428,4,29,{fromCal:"islamic-civil"},"+099999-11-24T00:00:00.000Z");
r |= test(-640, 7, 20,{fromCal:"islamic-civil"},"0001-03-03T00:00:00.000Z");
r |= test(-2000,1,1,{},"-001319-02-16T00:00:00.000Z");
r |= test(-2020,1,1,{},"-001339-09-22T00:00:00.000Z");
r |= test(-6000,1,1,{fromCal:"islamic-tbla"},"-005200-03-27T00:00:00.000Z");
r |= test(-6000,1,1,{fromCal:"islamic-rgsa"},"-005200-03-24T00:00:00.000Z");
r |= test(-20000,1,1,{},"-018783-02-11T00:00:00.000Z");
r |= test(-60000,12,29,{fromCal:"islamic"},"-057591-02-03T00:00:00.000Z");
r |= test(-100000,12,20,{fromCal:"islamic"},"-096400-02-08T00:00:00.000Z");
r |= test(-116000,1,1,{},"-111925-09-16T00:00:00.000Z");
r |= test(-270000,12,29,{fromCal:"islamic"},"-261338-01-15T00:00:00.000Z");
r |= test(275000,12,29,{fromCal:"islamic"},"+267434-02-27T00:00:00.000Z");
r |= test(-200000,1,1,{},"-193424-12-23T00:00:00.000Z");
r |= test(-206779,3,1,{},"-200000-01-01T00:00:00.000Z");

if (r==0) console.log("✅ All Other Test Cases Passed.");
//============ test function ============
function test(Y,M,D,OP,should) {
let out = hijriToCalendars(Y,M,D,OP);
out=out.toISOString();
if (out !== should) {console.log(`${Y},${M},${D} Output   : ${out}\n${Y},${M},${D} Should be: ${should}`);return 1;}
}

Javascript目标逼近法日历转换 - 流程图

图片描述


我同意Justin的观点。请随意编辑它。我尊重你的看法。 - Mohsen Alyafei
1
很不幸,我无法编辑不存在的答案! :-) 我建议您将此问题的内容复制到答案中,然后用您正在回答的问题替换问题的内容。 - Justin Grant
1
日历很混乱。公历也不例外,它只是在1582年10月作为对于朱利安日历的调整而被引入,因为朱利安日历有过多的闰日。在这个时候,1582年10月4日(朱利安)紧接着就是1582年10月15日(公历)。因此,这之前的公历日期从未存在过。快速测试显示,Javascript Date对象没有考虑到这一点,在这10天的时间内返回“公历”日期,并将1582年10月4日报告为星期一而不是星期四。 - Trentium
1
@Trentium-ECMAScript使用以1970年1月1日为时期的普氏格里高利历。格里高利历在不同的管辖范围内采用的时间各不相同(例如,俄罗斯在1918年采用了这一历法)。将格里高利历日期推算至其引入之前的时间是提供一种可以与其他日历对齐的日历标准的完全合理方法,就像UTC可以扩展到其1960年引入之前的时间来定义时间偏移一样。 - RobG
1
@Zuhair 我已经尝试使用Temporal API,结果是一样的。事实上,Temporal API 给出了错误信息 day 30 does not exist in month 10 of year 1402。谢谢。 - undefined
显示剩余13条评论
3个回答

3
一位工程师、物理学家和数学家在英国乡间列车上行驶时,工程师看着窗外的山坡上有只孤独的黑绵羊,惊叫道:“嘿,看,英格兰有黑绵羊!”物理学家迅速警告他:“我们只能确定英格兰有一只黑绵羊。”于是数学家警告他们两个,宣称:“我们只能得出结论,即英格兰至少有一只一侧是黑色的羊...”抱歉,我是一名数学家...

经过一些研究,我认为长期的解决方案在名为"Temporal"的ECMAScript提案中,该提案目前(截至2022年2月24日星期四)处于第3阶段活动提案中。完整的Temporal提案第3阶段草案提供了一些背景介绍和整个技术规范,而Temporal文档则提供了更易理解的目的和操作用途。后者是一个不错的起点...

从文档中可以得出以下几点值得注意的内容:

  • Temporal.Instant 抽象了时间点,不考虑日历或位置。也就是说,所有日历和时区都必须参考这个绝对时间来渲染指定语言环境下的日期和时间。

  • Temporal.Calendar 是一个日历系统的表示,提供关于日历的详细信息和操作日历的方法。有许多预先构建好的日历,例如“格里高利”、“伊斯兰教”、“希伯来教”等。此外,Temporal 提案还可以实现自己的日历。

话虽如此,CookBook 的例子都使用现代时间框架,所以我真的很想尝试在公元4年至1752年期间实施朱利安日历。这段时间内一直使用该日历。其概念是可以将与朱利安日历相关的同时代日期直接输入为朱利安日期,并轻松地操作和比较来自其他日历的日期,有信心这些日期引用相同的基础Temporal.Instant参考框架...例如,威廉·莎士比亚出生时是伊斯兰历几号?
作为ECMAScript第3阶段提案,Temporal仍处于实验阶段,但鼓励在非生产环境中使用,报告任何错误。由于Temporal.Instant是无论日历或位置如何的基础时间点,因此通过不同的日历呈现Temporal.Instant,转换日历成为一种自然结果。

<script type='module'>

  import * as TemporalModule from 'https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.3.0/dist/index.umd.js'
  
  const Temporal = temporal.Temporal;

  let islamicDate = Temporal.ZonedDateTime.from( {
    year: 1440,
    monthCode: 'M06',
    day: 2,
    hour:12,
    minute: 45,
    timeZone: 'Asia/Dubai',
    calendar: 'islamic'
  } );
  
  let hebrewDate = Temporal.ZonedDateTime.from( islamicDate ).withCalendar( 'hebrew' );
  
  let gregoryDate = Temporal.ZonedDateTime.from( islamicDate ).withCalendar( 'gregory' );
  
  console.log( `Date in Islamic: ${ islamicDate.toPlainDate().toLocaleString( 'en-US', { calendar: 'islamic' } ) }` );
  console.log( `DateTime in Islamic in Dubai: ${ islamicDate.toLocaleString( 'en-US', { calendar: 'islamic' } ) }` );
  console.log( `DateTime in Islamic in London: ${ islamicDate.toInstant().toZonedDateTimeISO( 'Europe/London' ).toLocaleString('en-US', { calendar: 'islamic' } ) }` );  
  
  console.log( `Date in Hebrew: ${ hebrewDate.toPlainDate().toLocaleString( 'en-US', { calendar: 'hebrew' } ) }` );
  console.log( `DateTime in Hebrew in Dubai: ${ hebrewDate.toLocaleString( 'en-US', { calendar: 'hebrew' } ) }` );
  console.log( `DateTime in Hebrew in London: ${ hebrewDate.toInstant().toZonedDateTimeISO( 'Europe/London' ).toLocaleString('en-US', { calendar: 'hebrew' } ) }` );
  
  console.log( `Date in Gregorian: ${ gregoryDate.toPlainDate().toLocaleString( 'en-US' ) }` );
  console.log( `DateTime in Gregorian: ${ gregoryDate.toLocaleString( 'en-US' ) }` ); 
  
  console.log( `Islamic Date in English (United States) Locale:\n${ islamicDate.toPlainDate().toString() }` );
  console.log( `Islamic DateTime in English (United States) Locale:\n${ islamicDate.toString() }` );
  
</script>

更新了代码片段,更好地展示了时区和日历之间的转换,以及显示日期的各种方法。


谢谢。日历日期后面的后缀 [u-ca=identifier] 是否总是存在? - Mohsen Alyafei
Object.assign(Intl, temporal.Intl); 的目的是什么? - Mohsen Alyafei
1
@MohsenAlyafei,我只是包含了Object.assign(Intl, ...语句,因为这个语句在烹饪书的源代码中已经包含了。谢谢你的质疑,因为这并不是必要的。 - Trentium
1
@MohsenAlyafei,据我目前的理解,Temporal规范将后缀替换为时区和日历。我已更新代码片段以更好地说明toZonedDateTimeISO()withCalendar()的转换,并包括一个显示双重后缀的示例。 - Trentium

2
以下是对原始代码的重构,目标如下:
  1. 尝试提高可读性和简洁性-例如:
    • 使用Math.trunc代替~~
    • 避免嵌套不必要的日期实例化和赋值,例如gD = new Date(gD.setUTCDate(gD.getUTCDate() + 1)),当gD.setUTCDate(gD.getUTCDate() + 1)具有相同的结果时。
    • 将日期值直接映射为数字而不是字符串。 parseInt函数将自动将"1444 AH"转换为数字1444
  2. 更好地支持JSDoc参数
  3. 如果使用不正确的参数调用函数,则提供更清晰的错误消息
  4. 简化问题-仅返回新的Date对象。将新的日期对象格式化为不同的日历似乎是一个单独的问题,如果需要,应该属于另一个函数(考虑到消费代码使用适合的任何选项应用Intl.DateTimeFormat的便捷性)。
// taken from https://dev59.com/Tuk5XIcBkEYKwwoY2NHV
// watch the temporal ECMAscript proposal which will make much of this obsolete - https://github.com/tc39/proposal-temporal
const formatters = {}
function getFormatter (calendar) {
  const locale = 'en-u-ca-' + calendar
  let returnFormatter = formatters[locale]
  if (returnFormatter) return returnFormatter
  const support = ['islamic-umalqura', 'islamic-civil', 'islamic-tbla', 'islamic-rgsa', 'islamic']
  if (!support.includes(calendar)) throw new Error(`calendar must be one of '${support.join("', '")}'`)
  if (!Intl || typeof Intl.DateTimeFormat !== 'function') throw new Error('Intl.DateTimeFormat is not available in this environment')
  try {
    returnFormatter = new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeZone: 'UTC' })
  } catch (err) {
    throw new Error(`Intl.DateTimeFormat threw an error, usually because locale '${locale}' is unsupported`, { cause: err })
  }
  return (formatters[locale] = returnFormatter)
}
/**********************************************************************************
* @purpose   : Converts Islamic (Hijri) Date to a Javascript Date.
*              Handles all 5 Islamic Calendar Types.
*              Uses the 'JS Calendar Conversion by Target Approximation' Method.
* @warning     Uses Intl.DateTimeFormat which is not supported on android. Most polyfills only work with gregorian calendars, in which case this script will not work.
* @author    : Mohsen Alyafei (Feb 2022)
* @licence   : MIT
* @param {number} year Hijri year
* @param {number} month Hijri month (1 to 12) note: months is standard 1 based
* @param {number} day Hijri day (1 to 29/30)
* @param {('islamic-umalqura'|'islamic-civil'|'islamic-tbla'|'islamic-rgsa'|'islamic')} [fromCalendar] Specifies the the type of input Islamic Calendar. default 'islamic-umalqura'
* @returns A new JavaScript Date at UTC midnight corresponding to the provided Hijri year, month and day
*/
module.exports = function hijriToJSDate (year, month, day, fromCalendar) {
  'use strict'
  const dFormat = getFormatter(fromCalendar)
  let gD = new Date(Date.UTC(2000, 0, 1))
  gD.setUTCDate(gD.getUTCDate() + Math.trunc(227022 + (year + (month - 1) / 12 + day / 354) * 354.367))
  const gY = gD.getUTCFullYear() - 2000
  gD = new Date(Date.UTC(gY, gD.getUTCMonth(), gD.getUTCDate()))
  let [iM, iD, iY] = dFormat.format(gD).split('/').map(n => parseInt(n, 10))
  gD.setUTCDate(gD.getUTCDate() + Math.trunc(year * 354 + month * 29.53 + day - (iY * 354 + iM * 29.53 + iD * 1) - 2))
  for (let i = 0; i < 4; ++i) {
    [iM, iD, iY] = dFormat.format(gD).split('/').map(n => parseInt(n, 10))
    if (iD === day && iM === month && iY === year) return gD
    gD.setUTCDate(gD.getUTCDate() + 1)
  }
  throw new Error('Invalid ' + fromCalendar + ' date!')
}

请问您能否解释一下这段代码?还有这些数字是什么意思?比如227022、2000、29.53、354.367、354。 - Mohammed Saber Mohammed

1
我写了以下代码。基本但对我有效。你可能需要更多测试。
{const toDateValue = (year, month, day) => {
    return +year * ((19 * 354 + 11 * 355) / 30) + +month * 29.53055 + +day;
};

const getCurrentDate = () => {
    return new Date(
        new Date().getFullYear(),
        new Date().getMonth(),
        new Date().getDate()
    );
};

const getDateComponentFromDate = (date) => {
    return date
        .toLocaleDateString('en-u-ca-islamic-umalqura')
        .split(' ')[0]
        .split('/');
};

const calculateDiffFromCurrent = (year, month, day) => {
    const [currentMonth, currentDay, currentYear] = getDateComponentFromDate(
        getCurrentDate()
    );

    let currentDateValue = toDateValue(currentYear, currentMonth, currentDay);
    let estimateDateValue = toDateValue(year, month, day);

    return adjustDate(currentDateValue - estimateDateValue, year, month, day);
};

const checkAndAdjust = (estimatedDate, year, month, day, count = 0) => {
    const [estimatedMonth, estimatedDay, estimatedYear] =
        getDateComponentFromDate(estimatedDate);

    if (count > 20 ) return estimatedDate
     
     if (estimatedYear > year) {
        estimatedDate.setDate(estimatedDate.getDate() - 1);
    } else if (estimatedYear < year) {
        estimatedDate.setDate(estimatedDate.getDate() + 1);
    } else if (estimatedMonth > month) {
        estimatedDate.setDate(estimatedDate.getDate() - 1);
    } else if (estimatedMonth < month) {
        estimatedDate.setDate(estimatedDate.getDate() + 1);
    } else if (estimatedDay > day) {
        estimatedDate.setDate(estimatedDate.getDate() - 1);
    } else if (estimatedDay < day) {
        estimatedDate.setDate(estimatedDate.getDate() + 1);
    } else {
        return estimatedDate;
    }
     count += 1;

    return checkAndAdjust(estimatedDate, year, month, day, count);
};

const adjustDate = (diff, year, month, day) => {
    let estimatedDate = getCurrentDate();

    estimatedDate.setDate(-diff + estimatedDate.getDate());

    estimatedDate = checkAndAdjust(estimatedDate, year, month, day);

    return estimatedDate;
};

export const getRequiredDate = (year, month, day, gergInput = false) => {
    if (gergInput) {
        return new Date(year, month, day);
    } else {
        return calculateDiffFromCurrent(year, month, day);
    }
};

另一种利用日期对象的大写字母的方法

export class EnhancedDate extends Date {
    getWeek() {
        var onejan = new Date(this.getFullYear(), 0, 1);
        return Math.ceil(
            ((this - onejan) / 86400000 + onejan.getDay() + 1) / 7
        );
    }

    add(x, unit = 'd') {
        switch (unit) {
            case 'd':
                this.setDate(this.getDate() + x);
                break;
            case 'm':
                this.setMonth(this.getMonth() + x);
                break;
            case 'y':
                this.setFullYear(this.getFullYear() + x);
                break;
            case 'im':
                let days =
                    this.toLocaleString('en', {
                        calendar: 'islamic-umalqura',
                        day: 'numeric',
                    }) - 1;
                if (x > 0) {
                    while (x > 0) {
                        this.endOf('im').add(1);
                        x--;
                    }
                    this.add(days);
                } else if (x < 0) {
                    while (x <= 0) {
                        this.startOf('im').add(-1);
                        x++;
                    }
                    this.add(days + 1);
                }
                break;
            case 'iy':
                this.add(x * 12, 'im');
                break;

            default:
                break;
        }
        return this;
    }

    startOf(unit = 'm') {
        switch (unit) {
            case 'd':
                this.setHours(0, 0, 0, 0);
                break;
            case 'm':
                this.setDate(1);
                this.setHours(0, 0, 0, 0);
                break;
            case 'im':
                let diff =
                    +this.toLocaleString('en', {
                        calendar: 'islamic-umalqura',
                        day: 'numeric',
                    }) - 1;

                this.add(-diff);
                break;
            case 'y':
                this.setDate(1);
                this.setMonth(0);
                this.setHours(0, 0, 0, 0);
                break;
            case 'iy':
                let hijriMonth = this.toLocaleString('en', {
                    calendar: 'islamic-umalqura',
                    month: 'numeric',
                });

                this.startOf('im').startOf('d');
                while (hijriMonth !== '1') {
                    this.add(-1).startOf('im');
                    hijriMonth = this.toLocaleString('en', {
                        calendar: 'islamic-umalqura',
                        month: 'numeric',
                    });
                }
                break;

            default:
                break;
        }
        return this;
    }

    endOf(unit = 'm') {
        switch (unit) {
            case 'd':
                this.setHours(23, 59, 59, 999);
                break;
            case 'm':
                this.startOf('m').add(1, 'm').add(-1);
                this.setHours(0, 0, 0, 0);
                break;
            case 'im':
                this.startOf('im').add(30).startOf('im').add(-1);
                break;
            case 'y':
                this.setMonth(11);
                this.setDate(31);
                this.setHours(0, 0, 0, 0);
                break;
            case 'iy':
                let hijriMonth = this.toLocaleString('en', {
                    calendar: 'islamic-umalqura',
                    month: 'numeric',
                });

                this.endOf('im').startOf('d');
                while (hijriMonth !== '12') {
                    this.add(1).endOf('im');
                    hijriMonth = this.toLocaleString('en', {
                        calendar: 'islamic-umalqura',
                        month: 'numeric',
                    });
                }
                break;

            default:
                break;
        }
        return this;
    }

    isSame(date = new EnhancedDate()) {
        return this.valueOf() === date.valueOf();
    }

    isSameOrBefore(date = new EnhancedDate()) {
        return this.valueOf() <= date.valueOf();
    }

    isSameOrAfter(date = new EnhancedDate()) {
        return this.valueOf() >= date.valueOf();
    }

    isBefore(date = new EnhancedDate()) {
        return this.valueOf() < date.valueOf();
    }

    isAfter(date = new EnhancedDate()) {
        return this.valueOf() > date.valueOf();
    }

    format(dateString = '', lng) {
        let calendar = {
            calendar: 'gregory',
        };

        if (dateString.includes('i')) {
            calendar = {
                calendar: 'islamic-umalqura',
            };
            dateString = dateString.replace('i', '');
        }

        switch (dateString) {
            case 'YYYY':
                dateString = { year: 'numeric' };
                break;
            case 'YY':
                dateString = { year: '2-digit' };
                break;
            case 'M':
                dateString = { month: 'numeric' };
                break;
            case 'MM':
                dateString = { month: '2-digit' };
                break;
            case 'MMM':
                dateString = { month: 'short' };
                break;
            case 'MMMM':
                dateString = { month: 'long' };
                break;
            case 'D':
                dateString = { day: 'numeric' };
                break;
            case 'DD':
                dateString = { day: '2-digit' };
                break;

            case 'H':
                dateString = { hour: 'numeric' };
                break;
            case 'HH':
                dateString = { hour: '2-digit' };
                break;
            case 'H:M':
                dateString = { hour: 'numeric', minute: 'numeric' };
                break;
            case 'HH:MM':
                dateString = {
                    hour: '2-digit',
                    minute: '2-digit',
                };
                break;
            case 'H:M:S':
                dateString = {
                    hour: 'numeric',
                    minute: 'numeric',
                    second: 'numeric',
                };
                break;
            case 'HH:MM:SS':
                dateString = {
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit',
                };
                break;
            case 'WDD':
                dateString = { weekday: 'long' };
                break;
            case 'WD':
                dateString = { weekday: 'short' };
                break;

            default:
                dateString = {};
                break;
        }

        dateString = this.toLocaleString(lng, {
            ...calendar,
            ...dateString,
        })
            .replace(' AH', '')
            .replace(' هـ', '');

        return dateString;
    }

    print(str = '', lng = 'en') {
        str = str === '' ? 'MM/DD/YYYY HH:MM:SS' : str;

        let types =
            'WDD-WD-H:M:S-HH:MM:SS-H:M-HH:MM-iYYYY-iYY-iMMMM-iMMM-iMM-iM-iDD-iD-YYYY-YY-MMMM-MMM-MM-M-DD-D-HH-H'.split(
                '-'
            );

        let processing = str;
        types = types.filter((t) => {
            if (str.includes(t)) {
                str = str.replace(t, '');
                return t;
            }
        });

        types.forEach((t) => {
            processing = processing.replace(t, this.format(t, lng));
        });

        return processing;
    }

    getHijriDate(year, month = 1, day = 1) {
        let yearDiff =
            year -
            this.toLocaleString('en', {
                calendar: 'islamic-umalqura',
                year: 'numeric',
            }).split(' ')[0];

        this.add(yearDiff, 'iy');
        let monthDiff =
            month -
            this.toLocaleString('en', {
                calendar: 'islamic-umalqura',
                month: 'numeric',
            });
        this.add(monthDiff, 'im');

        let dayDiff =
            day -
            this.toLocaleString('en', {
                calendar: 'islamic-umalqura',
                day: 'numeric',
            });
        this.add(dayDiff);

        return this;
    }

    relativeDateFormat(number, select = 1) {
        let sentance = [];
        switch (select) {
            case 1:
                sentance = ['يوم', 'يومين', 'أيام'];
                break;
            case 2:
                sentance = ['شهر', 'شهرين', 'أشهر'];
                break;
            case 3:
                sentance = ['سنة', 'سنتين', 'سنوات'];
                break;

            default:
                break;
        }

        number = Math.abs(number);

        return `${number > 2 ? number : ''} ${
            number < 1
                ? ''
                : number == 1
                ? sentance[0]
                : number == 2
                ? sentance[1]
                : number < 11
                ? sentance[2]
                : sentance[0]
        }`;
    }

    arabicAnd(years, months, days = 0, select = 1) {
        return select === 1
            ? `${Math.abs(months) >= 1 && Math.abs(years) >= 1 ? 'و' : ''}`
            : `${
                    Math.abs(days) >= 1 &&
                    (Math.abs(years) >= 1 || Math.abs(months) >= 1)
                        ? 'و'
                        : ''
              }`;
    }

    findDifference(comparedDate) {
        comparedDate.setHours(0, 0, 0, 0);

        var currentYear = comparedDate.getFullYear();
        var currentMonth = comparedDate.getMonth();
        var currentDay = comparedDate.getDate();

        var year = this.getFullYear();
        var month = this.getMonth();
        var day = this.getDate();

        var differenceInYears = currentYear - year;
        var differenceInMonths = currentMonth - month;
        var differenceInDays = currentDay - day;

        if (comparedDate.valueOf() >= this.valueOf()) {
            if (differenceInDays < 0) {
                differenceInMonths -= 1;
                differenceInDays += 30;
            }

            if (differenceInMonths < 0) {
                differenceInYears -= 1;
                differenceInMonths += 12;
            }
        }

        var years = differenceInYears;
        var months = differenceInMonths;
        var days = differenceInDays;

        let message = 'هو تاريخ اليوم';
        if (comparedDate.valueOf() > this.valueOf()) {
            message = `قبل ${this.relativeDateFormat(
                years,
                3
            )} ${this.arabicAnd(years, months)} ${this.relativeDateFormat(
                months,
                2
            )} ${this.arabicAnd(
                years,
                months,
                days,
                2
            )} ${this.relativeDateFormat(days)}`;
        } else if (comparedDate.valueOf() < this.valueOf()) {
            message = `سيكون بعد ${this.relativeDateFormat(
                years,
                3
            )} ${this.arabicAnd(years, months)} ${this.relativeDateFormat(
                months,
                2
            )} ${this.arabicAnd(
                years,
                months,
                days,
                2
            )} ${this.relativeDateFormat(days)}`;
        }

        return message;
    }

    findHijriDifference(comparedDate) {
        comparedDate.startOf('d');

        var currentYear = +comparedDate.print('iYYYY');
        var currentMonth = +comparedDate.print('iM');
        var currentDay = +comparedDate.print('iD');

        var year = +this.print('iYYYY');
        var month = +this.print('iM');
        var day = +this.print('iD');

        var differenceInYears = currentYear - year;
        var differenceInMonths = currentMonth - month;
        var differenceInDays = currentDay - day;

        if (comparedDate.valueOf() >= this.valueOf()) {
            if (differenceInDays < 0) {
                differenceInMonths -= 1;
                differenceInDays += 30;
            }

            if (differenceInMonths < 0) {
                differenceInYears -= 1;
                differenceInMonths += 12;
            }
        }

        var years = differenceInYears;
        var months = differenceInMonths;
        var days = differenceInDays;

        let message = 'هو تاريخ اليوم';
        if (comparedDate.valueOf() > this.valueOf()) {
            message = `قبل ${this.relativeDateFormat(
                years,
                3
            )} ${this.arabicAnd(years, months)} ${this.relativeDateFormat(
                months,
                2
            )} ${this.arabicAnd(
                years,
                months,
                days,
                2
            )} ${this.relativeDateFormat(days)}`;
        } else if (comparedDate.valueOf() < this.valueOf()) {
            message = `سيكون بعد ${this.relativeDateFormat(
                years,
                3
            )} ${this.arabicAnd(years, months)} ${this.relativeDateFormat(
                months,
                2
            )} ${this.arabicAnd(
                years,
                months,
                days,
                2
            )} ${this.relativeDateFormat(days)}`;
        }

        return message;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接