plotly.js icon indicating copy to clipboard operation
plotly.js copied to clipboard

Persian calendar leap year issue

Open Vahid-Taheri opened this issue 7 months ago • 15 comments

Hello, In the Persian calendar(Jalali), 1403 was a leap year, and the 12th month had 30 days. In other words, 2025/03/20 should be converted to 1403/12/30, but it is converted to 1404/01/01.

I tried to find the reason, and I found out that plotly.js is using world-calendars package to convert calendars, and the main issue is related to this package, but it hasn't been updated for 8 years.

I want to change xaxis type to categorical data because I need data type features like changing format based on resolution, avoiding repetitive labels, etc.

Also, I tried to fix world-calendar and rebuild plotly.js and patch changes using patch-package, but it didn't work.

Is there any solution to use other libraries for converting dates to Persian with coverage of features, or fixing world-calendar issue?

Vahid-Taheri avatar May 07 '25 13:05 Vahid-Taheri

Hi @Vahid-Taheri, thanks for pointing this out! world-calendars is a package we created specifically for plotly.js, though apparently it's still housed in my personal GH account (@gvwilson FYI I'm happy to keep it or transfer to the plotly org at your discretion). It hasn't been updated in 8 years because nobody has reported any issues with it during that time, until now. Originally the project was codegen'd from this JQuery plugin by @kbwood, which I see has had some updates since we pulled it in 2016, so perhaps your issue has already been fixed upstream and we should just rebuild and pull it through? Alternatively we could break that link and inline the code in plotly.js, or we could move the codegen into plotly.js - though there are apparently a few non-plotly.js dependents on world-calendars so the best overall would be to fix it in place.

alexcjohnson avatar May 07 '25 15:05 alexcjohnson

FYI back in 2016 when we pulled the original I apparently didn't find it published on NPM so I copied the source code into my repo. It now apparently is published: https://www.npmjs.com/package/kbw-calendars - but I only see up to v2.1.0 published there while the latest in https://github.com/kbwood/calendars is v2.2.0, so I guess best is to still copy in the source code. Also note that in v2.1.0 kbwood reorganized the repo, so there will likely be a bunch of work to get the codegen working again.

alexcjohnson avatar May 07 '25 16:05 alexcjohnson

Hmm I was optimistic that the new upstream version would fix the issue you identified @Vahid-Taheri, especially since the latest version over there is just two months old (suspiciously close to the date this discrepancy occurs!) but it seems the leapYear code has not changed since then. Here's the current kbwood code for Persian leap year and the corresponding world-calendars code

@Vahid-Taheri is that the same code you were trying to edit? Do you have a proposed update that would give the correct leap years in all cases? Or a reference you can point us to for precisely how to decide which years are leap years?

Ideally I guess this means we should open a PR upstream to fix the bug there before we update world-calendars.

alexcjohnson avatar May 07 '25 16:05 alexcjohnson

The Persian calendar was correct as is (according to https://www.iranchamber.com/calendar/converter/iranian_calendar_converter.php and https://www.parstimes.com/persian/calendar/). I have added a new "iranian" calendar to provide one that matches the alternative version with 1403 being a leap year (http://www.time.ir/).

kbwood avatar May 08 '25 01:05 kbwood

Hi @alexcjohnson, thanks for your early answer. In Jalali calendar, often leap years repeats every 4 years. 1395 was leap year, 1399 is leap year, 1403 is leap year too. By this pattern the next leap year should be 1407, but it is not ! because of that scientists !! 🙄😂 The next leap year is 1408. If you want to read more, this link is a good source.

About algorithms, I found two algorithms that cover this issue.
1- ngb-calendar-persian 2- Persian-leap npm package

Vahid-Taheri avatar May 08 '25 09:05 Vahid-Taheri

Thank you @kbwood - and I don't think we've corresponded before but let me say thank you for creating and maintaining your calendar plugin, that's been the basis of Plotly's international calendar support for nearly a decade now!

It does look like your leap year logic in iranian.js matches that in ng2-datepicker-jalali. The persian-leap package uses a bit different logic so it's a bit hard to match up but it also has two different calculations available, presumably corresponding to the distinction between your Persian and Iranian calendars. Do we know if both of these calendars are in use, and if so in what contexts? We can certainly follow your lead and include both, but because they're so similar it would be useful to give users clear guidance on their usage. I see on your docs that the Iranian calendar is "also known as Solar Hijri calendar" whereas the Persian is "also known as Jalali calendar" but the implication of this discussion is that @Vahid-Taheri considers your Iranian/Solar Hijri to be the Jalali calendar so I gather there's still some confusion.

alexcjohnson avatar May 08 '25 13:05 alexcjohnson

@alexcjohnson, both are the same (or maybe a little different). We mostly use Jalali calendar. moment-jalaali and date-fns-jalali are two common npm packages we use, and both of them are trustworthy sources. Their leap year implementation is the same as the algorithms we checked before.

Vahid-Taheri avatar May 08 '25 15:05 Vahid-Taheri

Thank you @Vahid-Taheri for raising this issue. And thank you to everyone else for addressing it. @kbwood, where and how can we access the new "iranian" calendar that you have added? I can't seem to find it in the plotly codebase. Also I noticed there is a "persian" calendar option. What is the difference between that and the "jalali" option?

hmanz avatar May 12 '25 11:05 hmanz

@hmanz we (Plotly) will need to rebuild using the new version of @kbwood's calendar code in order to get the new iranian calendar. And again that may take some time due to the altered structure so bear with us.

But I'm still unclear on the usage of these calendars and how we can help our users to pick the correct one for their needs. @Vahid-Taheri what do you mean by "mostly use Jalali"? Are there specific cases where the other one is used?

alexcjohnson avatar May 12 '25 12:05 alexcjohnson

@alexcjohnson and @hmanz, Jalali, Persian, Iranian, and Solar Hijri are all the same, and in global convention and language, we use the Jalali name for it. Still, in the native language, we call it Solar Hijri (هجری شمسی). This article may help you in this subject.

It was strange for me to see both in world-calendar package. I think it's better to fix one and refer another to that to keep safe usage for users who are using each one.

Vahid-Taheri avatar May 12 '25 13:05 Vahid-Taheri

The new calendar is available in my Calendars project. I'm don't fully understand the difference between the two calendars but they correspond to two different implementations as mentioned in my previous post.

kbwood avatar May 13 '25 01:05 kbwood

Hi everyone - so is the best solution here for plotly.js to switch to @kbwood's project (https://github.com/kbwood/calendars) rather than continuing with @alexcjohnson's (originally https://github.com/alexcjohnson/world-calendars, but I'm in the process of moving it to https://github.com/plotly/world-calendars)? Fewer packages => less confusion ?

gvwilson avatar May 14 '25 18:05 gvwilson

It isn't really possible for us to use kbwood/calendars directly, as it's structured as a jQuery plugin. But world-calendars is codegen'd from kbwood/calendars, we just have to rerun the codegen on the new version, and make any updates necessary for that to work. And perhaps also find a way to either pull the codegen input directly from kbwood/calendars or automate copying it into the world-calendars repo, I originally just copied the relevant files manually.

I'm kind of in the same boat as @kbwood in that I don't really understand the distinction between persian and iranian, it kind of sounds like only iranian is in common use but perhaps we keep both and just add an explanatory note to our docs for both, something like "persian and iranian differ only in leap year algorithm. iranian is the calendar in current common use in Iran."?

alexcjohnson avatar May 14 '25 19:05 alexcjohnson

I have another repository for the world calendars that removes the jQuery dependency: https://github.com/kbwood/world-calendars. It is written in TypeScript however.

kbwood avatar May 14 '25 23:05 kbwood

Oh that's cool, thanks for pointing that out! We should be able to use kbwood/world-calendars directly rather than doing codegen from your jQuery package. I'm not sure if plotly.js has any TypeScript dependencies right now but that should not be a blocker.

I'm curious what you plan for these projects going forward. Clearly it's not ideal to be maintaining two parallel codebases, perhaps there's a route to calendars becoming a thin jQuery wrapper around world-calendars?

alexcjohnson avatar May 15 '25 13:05 alexcjohnson

IMHO. the Jalali, Persian and Iranian should provide identical results.

In current Jalali/Persian the leap year is computed by a function similar to this:

var leapYear = function(year) {
	return (
		(
			(
				(
					(year - (year > 0 ? 474 : 473)) % 2820) +
					474 + 38
				) * 682
			) % 2816
		) < 682;
};

Unfortunately it didn't work as expected for the year 1403 SH.

I tested replacing the function with the following code:

var leapYear = function(year) {
    // diff between approximate mean tropical year and 365
    var c = 0.242197;

    // 475 S.H. (1096 A.D.) possibly when the solar calendar is adjusted by Omar Khayyam (https://en.wikipedia.org/wiki/Omar_Khayyam)
    var x = year - 475;

    var v0 = c * x;
    var v1 = c * (x + 1);

    var r0 = v0 - Math.floor(v0);
    var r1 = v1 - Math.floor(v1);
    return r0 > r1;
};

And it produced identical results between 475 SH and 1530 SH whereas 1403 & 1404 are fixed by my algorithm.

var leapYear1 = function(year) {
	return (
		(
			(
				(
					(year - (year > 0 ? 474 : 473)) % 2820) +
					474 + 38
				) * 682
			) % 2816
		) < 682;
};

var leapYear2 = function(year) {
    // diff between approximate mean tropical year and 365
    var c = 0.242197;

    // 475 S.H. (1096 A.D.) possibly when the solar calendar is adjusted by Omar Khayyam (https://en.wikipedia.org/wiki/Omar_Khayyam)
    var x = year - 475;

    var v0 = c * x;
    var v1 = c * (x + 1);

    var r0 = v0 - Math.floor(v0);
    var r1 = v1 - Math.floor(v1);
    return r0 > r1;
};

var x = [];
var y1 = [];
var y2 = [];
var t1 = [];
var t2 = [];
for (var i = 1350; i < 1450; i++) {
	x.push(i)
	y1.push(leapYear1(i) === true ? 1 : 0)
	y2.push(leapYear2(i) === true ? 1 : 0)

	t1.push(leapYear1(i) === true ? i : '')
	t2.push(leapYear2(i) === true ? i : '')
}

Plotly.newPlot(gd, [{
	name: 'Before',
	x: x,
	y: y1,
	text: t1,
	mode: 'markers+lines',
	// textposition: 'bottom'
}, {
	name: 'After',
	x: x,
	y: y2,
	text: t2,
	mode: 'markers+lines+text',
	textposition: 'top'
}]);

Image

To improve/simplify the Persian/Jalali methods, other changes may also required namely to convert dates to milliseconds. The other implementation named Iranian seems to works correctly between 1403-1404 SH but I fell it could be simplified.

archmoj avatar Jun 30 '25 22:06 archmoj

Both https://www.iranchamber.com/calendar/converter/iranian_calendar_converter.php and http://www.time.ir/ purport to be Iranian calendars, yet they disagree on whether 1403 and 1404 are leap years. What's the difference? Which is correct?

kbwood avatar Jul 03 '25 08:07 kbwood

@kbwood FYI - I opened PRs to fix the Persian/Jalali implementation concerning year 1403/1404 leap year mismatch. I also tested your other implementation named Iranian and noticed following differences as listed below.

@Vahid-Taheri Wondering if you could have access to a newspaper from 1864 AD (1243 SH) at the National Library and Archives of Iran to look at the dates? That could possibly help resolve the disagreement between the two implementations.

  Iranian  |Persian/Jalali
1502-12-30 | 1503-01-01
1469-12-30 | 1470-01-01
1436-12-30 | 1437-01-01
1403-12-30 | 1404-01-01
1243-01-02 | 1243-01-01
1210-01-02 | 1210-01-01
1144-01-02 | 1144-01-01
1115-01-02 | 1115-01-01
1111-01-02 | 1111-01-01
1082-01-02 | 1082-01-01
1049-01-02 | 1049-01-01
1016-01-02 | 1016-01-01
0987-01-02 | 0987-01-01
0983-01-02 | 0983-01-01
0954-01-02 | 0954-01-01
0950-01-02 | 0950-01-01
0921-01-02 | 0921-01-01
0917-01-02 | 0917-01-01
0888-01-02 | 0888-01-01
0884-01-02 | 0884-01-01
0859-01-02 | 0859-01-01
0855-01-02 | 0855-01-01
0851-01-02 | 0851-01-01
0826-01-02 | 0826-01-01
0822-01-02 | 0822-01-01
0818-01-02 | 0818-01-01
0793-01-02 | 0793-01-01
0789-01-02 | 0789-01-01
0760-01-02 | 0760-01-01
0756-01-02 | 0756-01-01
0731-01-02 | 0731-01-01
0727-01-02 | 0727-01-01
0723-01-02 | 0723-01-01
0719-01-02 | 0719-01-01
0698-01-02 | 0698-01-01
0694-01-02 | 0694-01-01
0690-01-02 | 0690-01-01
0686-01-02 | 0686-01-01
0665-01-02 | 0665-01-01
0661-01-02 | 0661-01-01
0657-01-02 | 0657-01-01
0632-01-02 | 0632-01-01
0628-01-02 | 0628-01-01
0624-01-02 | 0624-01-01
0603-01-02 | 0603-01-01
0599-01-02 | 0599-01-01
0595-01-02 | 0595-01-01
0591-01-02 | 0591-01-01
0570-01-02 | 0570-01-01
0566-01-02 | 0566-01-01
0562-01-02 | 0562-01-01
0558-01-02 | 0558-01-01
0537-01-02 | 0537-01-01
0533-01-02 | 0533-01-01
0529-01-02 | 0529-01-01
0525-01-02 | 0525-01-01
0504-01-02 | 0504-01-01
0500-01-02 | 0500-01-01
0496-01-02 | 0496-01-01
0492-01-02 | 0492-01-01
0475-01-02 | 0475-01-01

archmoj avatar Jul 04 '25 17:07 archmoj

@archmoj I have a problem with the leapfrog for a program I'm working on and you said it was solved. Do I need to update now to solve it or is there another way? I would appreciate an explanation.

fakharpoor05 avatar Jul 07 '25 06:07 fakharpoor05

@archmoj I have a problem with the leapfrog for a program I'm working on and you said it was solved. Do I need to update now to solve it or is there another way? I would appreciate an explanation.

@fakharpoor05 The proposed bug fix PR is not merged yet waiting for reviews. By the way you may try installing it using the following command:

npm install https://github.com/archmoj/world-calendars#0faeec8544d289c94845a73fcbb813b9bb423599

archmoj avatar Jul 07 '25 13:07 archmoj

I re-opened new PR https://github.com/plotly/world-calendars/pull/1 after world-calendars repository is moved to plotly org.

archmoj avatar Jul 11 '25 15:07 archmoj

@archmoj i update plotly.js to last version. But the problem is still there and it hasn't been fixed. Of course, I went and read the list of changes in the latest update, but there was no indication that this problem had been fixed.

fakharpoor05 avatar Aug 09 '25 10:08 fakharpoor05

@fakharpoor05 could you please open a new issue and describe what isn't working with steps to reproduce?

camdecoster avatar Aug 11 '25 16:08 camdecoster