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.
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.
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 convert an Islamic Date to another calendar (say to '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.
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.
If you consider the internal Javascript ICU C++ 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;}
}
What follows is a refactoring of the OPs code with the following aims:
Math.trunc
instead of ~~
gD = new Date(gD.setUTCDate(gD.getUTCDate() + 1))
when gD.setUTCDate(gD.getUTCDate() + 1)
has the same outcome."1444 AH"
to the number 1444
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!')
}