13

Searching on both StackOverflow and CodeReview sites, the Method described in this Question/Post has not been attempted; which is to convert the 5 Islamic Calendars' Dates to any of the 18 available calendars in Javascript without the use of External Date Libraries or using complex math/astronomical formulas.

Hope this post complies with StackOverflow Can I answer my own question? recommended policy guidelines for sharing new conceptual code with the community.

Javascript Intl.DateTimeFormat()

We all know that Javascript has a built-in method ( the Intl.DateTimeFormat() ) to convert Gregorian Dates into various calendars' dates (18 World Calendars) including the formatting of the output string.

However, Javascript does not provide a built-in method for the reverse operation, i.e. converting the 18 world calendar dates back into Gregorian Dates (including the Islamic Calendars Dates) or into other calendars. For such purposes, you will need to use External Date Libraries to do the conversion such as 'moment.js' and many others.

The method/code used here does not use external libraries and uses the Javascript Calendar Conversion by Target Approximation Method. The flowchart below summarises the concept of the Method which can be applied to convert other Calendars to the remaining Calendars.

The short Javascript function below provides the facilities to convert any of the five (5) Islamic (Hijri) Calendars' Dates (from Islamic year -280,803 AH to +281,510 AH) into any of the following 18 Javascript Calendars with options for formatting the resulting output:

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

The method does not use external libraries and does not use complex mathematical or astronomical formulas and relies solely on the Javascript built-in calendar conversion algorithms which are in turn based on the ICU code [https://icu.unicode.org/].

This approach ensures that the output is always accurate and fully compliant with the Javascript engine output.

While some external libraries are very good, they tend over time to lose the update and support; an example is the recent case of the 'moment.js' library.

Syntax

The function Syntax is:

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

In its simplest form, the function defaults to converting the 'islamic-umalqura' calendar which is the most common and recent Islamic Calendar used.

It will also default to convert into the Gregorian calendar using the ISO Date Format.

Example: Convert the Islamic Date 21 Rajab 1443 (i.e. 21/07/1443) to Gregorian.

hijriToCalendars(1443,7,21);

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

To to convert Islamic Date to another calendar (say 'Persian' calendar):

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

 output: 12/3/1400 AP

To add formatting to the output, use the 'dateStyle' options as in the Javascript Intl.DateTimeFormat() method.

Example: Convert Islamic Date to Persian Date with full dateStyle

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

 output: Tuesday, Esfand 3, 1400 AP

Example: Convert an Islamic Date into Hebrew with Arabic Locale

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

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

The above can be done for all other 18 Calendars.

An added feature is the ability to format the Islamic Date into any of the available 'dateStyles' and 'locales' without conversion.

To do that specify the 'toCal' to be the same as the Islamic input calendar, for the default use: 'islamic-umalqura'.

Example: Use Persian Locale to Format an Islamic Date

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

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

Example: Format an Islamic Date in the Hindi Locale

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

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

The default input Islamic Calendar is 'islamic-umalqura', however, you can change that to any of the five (5) Islamic Calendars using the 'fromCal' option.

Example: Convert the Islamic-Civil Date 21 Rajab 1443 (i.e. 21/07/1443) to Gregorian.

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

 output: 2022-02-23T00:00:00.000Z

You can see from the above that the islamic-civil date differs by 1 day from the islamic-umalqura date.

You can use all the options available in the Intl.DateTimeFormat() for formatting the output date.

Invalid Islamic Dates

If an invalid Islamic Date is passed to the function an error Invalid islamic-xxxxx date! will be generated.

Invalid Islamic Dates are dates that have incorrect days in the month or incorrect days or months.

For example, the Islamic Date 1443/2/30 is invalid because month 2 of the Hijri Calendar (month "Safar") is always 29 days and cannot be 30 days.

Also, for example, the Islamic date 1442/12/30 will throw an error because the year 1442 is a non-leap year and month 12 is only 29 days.

Is this Method Reliable and Accurate?

If you consider the internal Javascript ICU code to be reliable and accurate, then this Method solely uses the date generated from inside the Javascript Engine. There is no concern about external incorrect math or errors/bugs in the code unless they are inside the Javascript Engine itself.

Below is the function with example test cases for conversion and formatting which can be run and tested.

Examples are given for converting an Islamic Date (1443/7/21) which happens to be today's date into all other Calendars' formats.

Any improvements/suggestions/alternatives are appreciated.

/**********************************************************************************
* @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 Calendar Conversion by Target Approximation Method - Flowchart

enter image description here

Justin Grant
  • 44,807
  • 15
  • 124
  • 208
Mohsen Alyafei
  • 4,765
  • 3
  • 30
  • 42
  • There is no requirement to convert to Julian dates as interim step as the javascript engine built in code takes care of it all – Mohsen Alyafei Feb 22 '22 at 16:38
  • re: "This post is made in compliance with StackOverflow Can I answer my own question?", you should ask a real question (e.g. "How can I convert between an Islamic date and a Gregorian date in Javascript?") and then post your answer above as the answer to that question. Your current "question" is really an answer. – Justin Grant Feb 22 '22 at 19:32
  • I agree Justin. Please feel free to edit it. I respect your views. – Mohsen Alyafei Feb 22 '22 at 19:37
  • 1
    Unfortunately I can't edit an answer that doesn't exist! :-) I'd suggest you copy the content of this question into an answer, and then replace the question content with the question that you're answering. – Justin Grant Feb 22 '22 at 19:38
  • The problem always was if you post a question. All will ask what have you tried or is it homework. If you post a question with answer they complain too. Never happy even if it is a new concept. – Mohsen Alyafei Feb 22 '22 at 19:42
  • Under all circumstances I hope you like my new method. – Mohsen Alyafei Feb 22 '22 at 19:47
  • 1
    Calendars are messy. The [Gregorian](https://en.wikipedia.org/wiki/Gregorian_calendar) is no different, and was only introduced in October 1582 as an adjustment to the Julian calendar, which had excess leap days. At this time, Thu 04 Oct 1582 (Julian) was immediately followed by Fri 15 Oct 1582 (Gregorian). Thus, Gregorian dates prior to this never existed. Quick testing shows that the Javascript Date object does not take this into account, returning "Gregorian" dates within this 10 day period, and reporting 04 Oct 1582 as a Monday rather than Thursday. – Trentium Feb 23 '22 at 00:59
  • 1
    @Trentium—ECMAScript uses the [proleptic Gregorian calendar](https://262.ecma-international.org/#sec-time-values-and-time-range) based on an epoch of 1 Jan 1970. The Gregorian calendar was adopted at different times by different jurisdictions (e.g. Russia adopted it in 1918). Extrapolating Gregorian dates to times before its introduction is a perfectly reasonable method of providing a calendar standard that other calendars can aligned with, in the same way that UTC can be extended to times before its introduction in 1960 to define time offsets. – RobG Feb 23 '22 at 05:31
  • I agree with RobG that we can now use the reference point for programming as the gregorian date epoch given by javascript and calculate any calendar to that point then look it up with a fast loop to match. – Mohsen Alyafei Feb 23 '22 at 17:54
  • @MohsenAlyafei and I don't agree. If attempting to convert an Hijri date to a contemporaneous date prior to ~1582, additional considerations come into play. Specifically, [adjustments to the Javascript Date object implementation of the proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) are required to accurately translate from the Hijri date to the contemporaneous Julian date. And this further breaks down prior to 622AD when the Hijri calendar didn't even exist... Calendars are messy... Unless you also introduce a proleptic Hijri calendar... ; -) – Trentium Feb 24 '22 at 02:06
  • @MohsenAlyafei could you please explain why 4 iterations ? – Mohammed Saber Mohammed Mar 18 '23 at 20:17

3 Answers3

3

An engineer, physicist, and mathematician where on a train rolling through the English countryside, when the engineer looked out the window, and seeing a lone black sheep on the hillside, exclaimed, "Hey, look, England's got black sheep!" The physicist quickly admonished him, "All we can ascertain is that England has one black sheep." Whereupon the mathematician admonished them both, proclaiming, "All we can conclude is that England has at least one sheep that's black on one side..."

My apologies in arrears, as I'm a mathematician...

After some research, I believe the long term answer lies with the ECMAScript proposal dubbed "Temporal" which is currently (as of Thu 24 Feb 2022) in Stage 3 of Active Proposals. The full Stage 3 Draft of the Temporal proposal provides a bit of background on the motivation of the proposal along with the entire technical specification, with the Temporal documentation providing a more understandable purpose and operational use. This latter document is a good place to start...

A couple of notables drawn from the documentation:

  • Temporal.Instant abstracts a point in time without regard to calendar or location. Ie, this is the absolute time which all calendars and time zones must reference to render the date and time in the specified locale.

  • Temporal.Calendar is a representation of a calendar system, which provides the details on the calendar and methods by which to operate on the calendar. A number of prebuilt calendars exist, such as 'gregory', 'islamic', 'hebrew', etc. Additionally, the Temporal proposal also makes it possible to implement your own calendar.

That being said, the CookBook examples all make use of modern time frames, so am actually interested in attempting to implement the Julian calendar from 4AD to 1752AD during which time it was employed. The concept being that contemporaneous dates referencing the Julian calendar can be entered directly as the Julian date and easily manipulated and compared with dates from other calendars, with confidence that the dates are referencing the same underlying Temporal.Instant frame of reference... Eg, what was the Islamic date when William Shakespeare was born?

Being an ECMAScript stage 3 proposal, Temporal is still experimental but encouraged to be exercised in a non production environment, reporting any bugs. As the Temporal.Instant is the underlying point in time regardless of calendar or location, the conversion between calendars becomes a natural consequence of presenting Temporal.Instant via different calendars.

<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>

(Updated the Code Snippet to better exemplify the conversion between timeZones and calendars, in addition to various means of displaying the dates.)

Trentium
  • 3,419
  • 2
  • 12
  • 19
  • Thanks. Will the suffix `[u-ca=identifier]` always be there after the calendar date? – Mohsen Alyafei Feb 25 '22 at 09:02
  • What is the purpose of `Object.assign( Intl, temporal.Intl );` ? – Mohsen Alyafei Feb 25 '22 at 11:17
  • 1
    @MohsenAlyafei, I simply included `Object.assign( Intl, ...` as that statement was included in the source of the [CookBook](https://tc39.es/proposal-temporal/docs/cookbook.html). Thanks for questioning that, as it wasn't necessary. – Trentium Feb 25 '22 at 23:09
  • 1
    @MohsenAlyafei, the `Temporal` specification as I currently understand replaces the suffix with both the timezone and the calendar. I have updated the Code Snippet to better exemplify conversions `toZonedDateTimeISO()` and `withCalendar()`, and included an example which displays the dual suffix. – Trentium Feb 25 '22 at 23:21
1

What follows is a refactoring of the OPs code with the following aims:

  1. Try and improve readability and simplicity - for instance:
    • Math.trunc instead of ~~
    • avoid nesting uneccessary Date instantiation and assignment such as gD = new Date(gD.setUTCDate(gD.getUTCDate() + 1)) when gD.setUTCDate(gD.getUTCDate() + 1) has the same outcome.
    • Map the date values straight to and from numbers rather than strings. The parseInt function will automatically convert "1444 AH" to the number 1444
  2. Better support of JSDoc params
  3. Clearer error messages if the function is called with incorrect arguments
  4. Simplify concerns - return only the new Date object. Formatting the new Date object to a different calendar seems a separate concern and to belong in another function if at all (given the ease of the consuming code applying Intl.DateTimeFormat with whatever options suit).
// taken from https://stackoverflow.com/questions/71222556/how-to-convert-any-of-the-5-islamic-hijri-calendars-dates-to-any-of-18-world
// 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!')
}
Brent
  • 4,611
  • 4
  • 38
  • 55
1

I made the following code. Basic but worked for me. You may need to test it more.

{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);
    }
};

Another Method which works by capitalizing on the Date Object

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;
    }
}