Split out common calendar package
2
.gitignore
vendored
|
@ -1,5 +1,5 @@
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
|
|
64
README.md
|
@ -1,61 +1,7 @@
|
||||||
# French Republican Calendar
|
# qcal
|
||||||
|
|
||||||

|
This is **@quantum5**'s collection of calendar webapps, intend to cover all
|
||||||
|
calendars from major cultures.
|
||||||
|
|
||||||
## What is this?
|
Currently, the following calendars are supported:
|
||||||
|
* [French Republican Calendar](frcal/README.md)
|
||||||
The [French Republican calendar][0] was a calendar created and implemented
|
|
||||||
during the French Revolution.
|
|
||||||
|
|
||||||
It was also frequently known as the *French Revolutionary Calendar*, but this
|
|
||||||
was a misnomer: year 1 of the calendar started on 22 September 1792, the day
|
|
||||||
after the [abolition of the monarchy][1] and the founding of the [French
|
|
||||||
First Republic][2].
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
|
|
||||||
A year consists of 12 months of 30 days each, divided into three *décades* of
|
|
||||||
10 days each, followed by 5 complementary days (6 in leap years).
|
|
||||||
|
|
||||||
The year starts on the day of the autumnal equinox at the Paris Observatory
|
|
||||||
(longitude 2°20′14.03″ E). A leap year follow directly from this definition:
|
|
||||||
a year is a leap year when the next autumnal equinox happens 366 days later
|
|
||||||
instead of the normal 365. By this definition, the year will **never** drift
|
|
||||||
with respect to the seasons.
|
|
||||||
|
|
||||||
The 12 months are: *Vendémiaire*, *Brumaire*, *Frimaire*, *Nivôse*,
|
|
||||||
*Pluviôse*, *Ventôse*, *Germinal*, *Floréal*, *Prairial*, *Messidor*,
|
|
||||||
*Thermidor*, *Fructidor.*
|
|
||||||
|
|
||||||
The complementary days are: *la Fête de la Vertu*, *la Fête du Génie*,
|
|
||||||
*la Fête du Travail*, *la Fête de l'Opinion*, *la Fête des Récompenses,*
|
|
||||||
and *la Fête de la Révolution* (leap years only).
|
|
||||||
|
|
||||||
## What's so special about this version?
|
|
||||||
|
|
||||||
Most versions of the calendar floating around doesn't use the original
|
|
||||||
definition above.
|
|
||||||
|
|
||||||
Most versions uses the so-called *Romme* method for leap years, using the
|
|
||||||
same leap year rules as the Gregorian calendar, i.e. every year divisible
|
|
||||||
by four, except century years not divisible by 400. This method might make
|
|
||||||
sense, except years 3, 7, and 11 were leap years under the original rules
|
|
||||||
and were observed as such in real life, but the *Romme* method instead makes
|
|
||||||
years 4, 8, 12 leap years instead.
|
|
||||||
|
|
||||||
This version uses the original rules. The [JPL's DE440 and DE441
|
|
||||||
ephemerides][3] were used to calculate the exact timings of the autumnal
|
|
||||||
equinoxes between the Gregorian years 13201 BCE and 17191 CE (corresponding
|
|
||||||
to the French Republican years -14991 to 15399). The times were then converted
|
|
||||||
to UT1+00:09:21, the exact local time at the Paris Observatory. UT1 was chosen
|
|
||||||
to keep track of the Earth's rotation without having to worry about the issues
|
|
||||||
posed by leap seconds in UTC. Note that due to the uncertainty over [ΔT][4] —
|
|
||||||
the difference between UT1 and Terrestrial Time (TT) used in the ephemerides —
|
|
||||||
it is theoretically possible for there to be inaccuracies when the equinox
|
|
||||||
occurs very close to midnight.
|
|
||||||
|
|
||||||
[0]: https://en.wikipedia.org/wiki/French_Republican_calendar
|
|
||||||
[1]: https://en.wikipedia.org/wiki/Proclamation_of_the_abolition_of_the_monarchy
|
|
||||||
[2]: https://en.wikipedia.org/wiki/French_First_Republic
|
|
||||||
[3]: https://ssd.jpl.nasa.gov/planets/eph_export.html
|
|
||||||
[4]: https://en.wikipedia.org/wiki/%CE%94T_(timekeeping)
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ def main():
|
||||||
'start_year': fr_year(int(equinoxes[0].ut1_calendar()[0])),
|
'start_year': fr_year(int(equinoxes[0].ut1_calendar()[0])),
|
||||||
'leap': [int(paris_jdn(b) - paris_jdn(a) == 366) for a, b in zip(equinoxes, equinoxes[1:])]
|
'leap': [int(paris_jdn(b) - paris_jdn(a) == 366) for a, b in zip(equinoxes, equinoxes[1:])]
|
||||||
}
|
}
|
||||||
with open('src/cal.json', 'w') as f:
|
with open('src/french/cal.json', 'w') as f:
|
||||||
json.dump(result, f, separators=(',', ':'))
|
json.dump(result, f, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
5
common/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
4883
common/package-lock.json
generated
Normal file
32
common/package.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "qcal_common",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.2",
|
||||||
|
"@testing-library/react": "^12.1.2",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/node": "^16.11.24",
|
||||||
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"ts-jest": "^29.1.0"
|
||||||
|
}
|
||||||
|
}
|
93
common/src/french/index.test.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import {
|
||||||
|
dateName,
|
||||||
|
dateRuralName,
|
||||||
|
frIsLeap,
|
||||||
|
frJDN,
|
||||||
|
jdnFrench,
|
||||||
|
monthName
|
||||||
|
} from './index';
|
||||||
|
|
||||||
|
describe('frJDN', () => {
|
||||||
|
it('works for sample dates', () => {
|
||||||
|
expect(frJDN(1, 1, 1)).toBe(2375840);
|
||||||
|
expect(frJDN(8, 2, 18)).toBe(2378444);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works in years starting/ending near midnight', () => {
|
||||||
|
expect(frJDN( 111, 1, 1)).toBe(2416017); // equinox 1902-09-23T23:55:19 UT1
|
||||||
|
expect(frJDN( 206, 1, 1)).toBe(2450715); // equinox 1997-09-22T23:55:46 UT1
|
||||||
|
expect(frJDN(2490, 1, 1)).toBe(3284926); // equinox 4281-09-20T23:50:38 UT1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frIsLeap', () => {
|
||||||
|
it('works for sample dates', () => {
|
||||||
|
expect(frIsLeap(1)).toBeFalsy();
|
||||||
|
expect(frIsLeap(8)).toBeFalsy();
|
||||||
|
expect(frIsLeap(3)).toBeTruthy();
|
||||||
|
expect(frIsLeap(7)).toBeTruthy();
|
||||||
|
expect(frIsLeap(11)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works in years starting/ending near midnight', () => {
|
||||||
|
expect(frIsLeap(110)).toBeTruthy();
|
||||||
|
expect(frIsLeap(205)).toBeTruthy();
|
||||||
|
expect(frIsLeap(2489)).toBeFalsy();
|
||||||
|
expect(frIsLeap(111)).toBeFalsy();
|
||||||
|
expect(frIsLeap(206)).toBeFalsy();
|
||||||
|
expect(frIsLeap(2490)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('jdnFrench', () => {
|
||||||
|
it('works for sample dates', () => {
|
||||||
|
expect(jdnFrench(2375840)).toEqual({year: 1, month: 1, day: 1});
|
||||||
|
expect(jdnFrench(2378444)).toEqual({year: 8, month: 2, day: 18});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works in years starting/ending near midnight', () => {
|
||||||
|
expect(jdnFrench(2416017)).toEqual({year: 111, month: 1, day: 1});
|
||||||
|
expect(jdnFrench(2450715)).toEqual({year: 206, month: 1, day: 1});
|
||||||
|
expect(jdnFrench(3284926)).toEqual({year: 2490, month: 1, day: 1});
|
||||||
|
expect(jdnFrench(2416016)).toEqual({year: 110, month: 13, day: 6});
|
||||||
|
expect(jdnFrench(2450714)).toEqual({year: 205, month: 13, day: 6});
|
||||||
|
expect(jdnFrench(3284925)).toEqual({year: 2489, month: 13, day: 5});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('monthName', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(monthName(1)).toBe('Vendémiaire');
|
||||||
|
expect(monthName(12)).toBe('Fructidor');
|
||||||
|
expect(monthName(13)).toBe('Jours Complémentaires');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dateName', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(dateName(1, 1)).toBe('1 Vendémiaire');
|
||||||
|
expect(dateName(2, 18)).toBe('18 Brumaire');
|
||||||
|
expect(dateName(3, 11)).toBe('11 Frimaire');
|
||||||
|
expect(dateName(8, 16)).toBe('16 Floréal');
|
||||||
|
expect(dateName(12, 30)).toBe('30 Fructidor');
|
||||||
|
expect(dateName(13, 1)).toBe('La Fête de la Vertu');
|
||||||
|
expect(dateName(13, 6)).toBe('La Fête de la Révolution');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for non-existent complimentary days', () => {
|
||||||
|
expect(dateName(13, 7)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dateRuralName', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(dateRuralName(1, 1)).toEqual({name: 'Raisin', title: 'Grape'});
|
||||||
|
expect(dateRuralName(1, 30)).toEqual({name: 'Tonneau', title: 'Barrel'});
|
||||||
|
expect(dateRuralName(12, 1)).toEqual({name: 'Prune', title: 'Plum'});
|
||||||
|
expect(dateRuralName(12, 30)).toEqual({name: 'Panier', title: 'Pack Basket'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for complimentary days', () => {
|
||||||
|
expect(dateRuralName(13, 1)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,10 @@
|
||||||
import data from './cal.json';
|
import data from './cal.json';
|
||||||
import ruralName from './rural-days.json';
|
import ruralName from './rural-days.json';
|
||||||
|
import {jdnGregorian} from '../gregorian';
|
||||||
|
|
||||||
// Month 13 is for the complementary days
|
// Month 13 is for the complementary days
|
||||||
export type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
|
export type FrenchMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
|
||||||
export type Day =
|
export type FrenchDay =
|
||||||
1
|
1
|
||||||
| 2
|
| 2
|
||||||
| 3
|
| 3
|
||||||
|
@ -37,12 +38,12 @@ export type Day =
|
||||||
|
|
||||||
export type FrenchDate = {
|
export type FrenchDate = {
|
||||||
year: number,
|
year: number,
|
||||||
month: Month,
|
month: FrenchMonth,
|
||||||
day: Day,
|
day: FrenchDay,
|
||||||
};
|
};
|
||||||
|
|
||||||
const monthNames: {
|
const monthNames: {
|
||||||
[key in Month]: string
|
[key in FrenchMonth]: string
|
||||||
} = {
|
} = {
|
||||||
1: 'Vendémiaire',
|
1: 'Vendémiaire',
|
||||||
2: 'Brumaire',
|
2: 'Brumaire',
|
||||||
|
@ -75,24 +76,12 @@ data.leap.forEach(leap => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const endYear = startYear + leaps.length - 1;
|
export const endYear = startYear + leaps.length - 1;
|
||||||
|
|
||||||
export function frSupportedYear(year: number): boolean {
|
export function frSupportedYear(year: number): boolean {
|
||||||
return startYear <= year && year <= endYear;
|
return startYear <= year && year <= endYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gregorianJDN(year: number, month: number, day: number): number {
|
export function frJDN(year: number, month: FrenchMonth, day: FrenchDay): number {
|
||||||
const g = year + 4716 - (month <= 2 ? 1 : 0);
|
|
||||||
const f = (month + 9) % 12;
|
|
||||||
const e = Math.floor(1461 * g / 4) + day - 1402;
|
|
||||||
const J = e + Math.floor((153 * f + 2) / 5);
|
|
||||||
const dg = 38 - Math.floor(Math.floor((g + 184) / 100) * 3 / 4);
|
|
||||||
return J + dg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateJDN(date: Date) {
|
|
||||||
return gregorianJDN(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function frJDN(year: number, month: Month, day: Day): number {
|
|
||||||
const dy = year - startYear;
|
const dy = year - startYear;
|
||||||
const dd = month * 30 + day - 31;
|
const dd = month * 30 + day - 31;
|
||||||
return startJD + 365 * dy + leaps[dy] + dd;
|
return startJD + 365 * dy + leaps[dy] + dd;
|
||||||
|
@ -102,18 +91,7 @@ export function frIsLeap(year: number): boolean {
|
||||||
return !!data.leap[year - startYear];
|
return !!data.leap[year - startYear];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jdnGregorian(jdn: number): Date {
|
export const endJD = frJDN(endYear, 13, frIsLeap(endYear) ? 6 : 5);
|
||||||
const e = 4 * (jdn + 1401 + Math.floor(Math.floor((4 * jdn + 274277) / 146097) * 3 / 4) - 38) + 3;
|
|
||||||
const h = 5 * Math.floor((e % 1461 + 1461) % 1461 / 4) + 2;
|
|
||||||
const day = Math.floor((h % 153 + 153) % 153 / 5) + 1;
|
|
||||||
const month = (Math.floor(h / 153) + 2) % 12 + 1;
|
|
||||||
const year = Math.floor(e / 1461) - 4716 + Math.floor((14 - month) / 12);
|
|
||||||
return new Date(year, month - 1, day);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const endJD = frJDN(endYear, 13, frIsLeap(endYear) ? 6: 5);
|
|
||||||
export const startGregorian = jdnGregorian(startJD);
|
|
||||||
export const endGregorian = jdnGregorian(endJD);
|
|
||||||
|
|
||||||
export function jdnFrench(jdn: number): FrenchDate {
|
export function jdnFrench(jdn: number): FrenchDate {
|
||||||
let lo = 0;
|
let lo = 0;
|
||||||
|
@ -130,35 +108,16 @@ export function jdnFrench(jdn: number): FrenchDate {
|
||||||
const dd = jdn - (startJD + 365 * lo + leaps[lo]);
|
const dd = jdn - (startJD + 365 * lo + leaps[lo]);
|
||||||
return {
|
return {
|
||||||
year: startYear + lo,
|
year: startYear + lo,
|
||||||
month: Math.floor(dd / 30) + 1 as Month,
|
month: Math.floor(dd / 30) + 1 as FrenchMonth,
|
||||||
day: dd % 30 + 1 as Day,
|
day: dd % 30 + 1 as FrenchDay,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jdnLongCount(jdn: number): string | null {
|
export function monthName(month: FrenchMonth): string {
|
||||||
let z = jdn - 584283;
|
|
||||||
if (z < 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const parts = [z % 20, Math.floor(z / 20) % 18];
|
|
||||||
z = Math.floor(z / 360);
|
|
||||||
while (z > 0) {
|
|
||||||
parts.push(z % 20);
|
|
||||||
z = Math.floor(z / 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parts.length < 5) {
|
|
||||||
parts.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.reverse().join('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function monthName(month: Month): string {
|
|
||||||
return monthNames[month];
|
return monthNames[month];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateName(month: Month, day: Day): string | null {
|
export function dateName(month: FrenchMonth, day: FrenchDay): string | null {
|
||||||
if (month === 13) {
|
if (month === 13) {
|
||||||
switch (day) {
|
switch (day) {
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -180,10 +139,13 @@ export function dateName(month: Month, day: Day): string | null {
|
||||||
return `${day} ${monthNames[month]}`;
|
return `${day} ${monthNames[month]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateRuralName(month: Month, day: Day): {name: string, title: string} | null {
|
export function dateRuralName(month: FrenchMonth, day: FrenchDay): { name: string, title: string } | null {
|
||||||
const rural = ruralName[month * 30 + day - 31];
|
const rural = ruralName[month * 30 + day - 31];
|
||||||
if (!rural)
|
if (!rural)
|
||||||
return null;
|
return null;
|
||||||
const [name, title] = rural;
|
const [name, title] = rural;
|
||||||
return {name, title};
|
return {name, title};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const startGregorian = jdnGregorian(startJD);
|
||||||
|
export const endGregorian = jdnGregorian(endJD);
|
39
common/src/gregorian.test.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import {gregorianJDN, jdnGregorian} from './gregorian';
|
||||||
|
|
||||||
|
describe('gregorianJDN', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(gregorianJDN(2000, 1, 1)).toBe(2451545);
|
||||||
|
expect(gregorianJDN(-4713, 11, 24)).toBe(0);
|
||||||
|
expect(gregorianJDN(11917, 9, 18)).toBe(6073915);
|
||||||
|
expect(gregorianJDN(-28565, 6, 17)).toBe(-8711925);
|
||||||
|
expect(gregorianJDN(-26650, 4, 13)).toBe(-8012550);
|
||||||
|
expect(gregorianJDN(17430, 3, 8)).toBe(8087303);
|
||||||
|
expect(gregorianJDN(3395, 7, 18)).toBe(2961257);
|
||||||
|
expect(gregorianJDN(4579, 3, 11)).toBe(3393575);
|
||||||
|
expect(gregorianJDN(-14851, 11, 22)).toBe(-3702831);
|
||||||
|
expect(gregorianJDN(8824, 11, 28)).toBe(4944292);
|
||||||
|
expect(gregorianJDN(19720, 8, 14)).toBe(8923868);
|
||||||
|
expect(gregorianJDN(7504, 7, 22)).toBe(4462042);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('jdnGregorian', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(jdnGregorian(0)).toEqual(new Date(-4713, 10, 24));
|
||||||
|
expect(jdnGregorian(2299160)).toEqual(new Date(1582, 9, 14));
|
||||||
|
expect(jdnGregorian(2299161)).toEqual(new Date(1582, 9, 15));
|
||||||
|
expect(jdnGregorian(2361221)).toEqual(new Date(1752, 8, 13));
|
||||||
|
expect(jdnGregorian(2361222)).toEqual(new Date(1752, 8, 14));
|
||||||
|
expect(jdnGregorian(2451545)).toEqual(new Date(2000, 0, 1));
|
||||||
|
expect(jdnGregorian(-8512316)).toEqual(new Date(-28019, 11, 20));
|
||||||
|
expect(jdnGregorian(-8534852)).toEqual(new Date(-28080, 3, 8));
|
||||||
|
expect(jdnGregorian(2653462)).toEqual(new Date(2552, 9, 30));
|
||||||
|
expect(jdnGregorian(3271156)).toEqual(new Date(4244, 0, 8));
|
||||||
|
expect(jdnGregorian(-666477)).toEqual(new Date(-6537, 1, 23));
|
||||||
|
expect(jdnGregorian(2397854)).toEqual(new Date(1852, 11, 31));
|
||||||
|
expect(jdnGregorian(-1211235)).toEqual(new Date(-8029, 7, 26));
|
||||||
|
expect(jdnGregorian(-91680)).toEqual(new Date(-4964, 10, 20));
|
||||||
|
expect(jdnGregorian(-5605876)).toEqual(new Date(-20061, 6, 14));
|
||||||
|
expect(jdnGregorian(-295121)).toEqual(new Date(-5521, 10, 19));
|
||||||
|
});
|
||||||
|
});
|
21
common/src/gregorian.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export function gregorianJDN(year: number, month: number, day: number): number {
|
||||||
|
const g = year + 4716 - (month <= 2 ? 1 : 0);
|
||||||
|
const f = (month + 9) % 12;
|
||||||
|
const e = Math.floor(1461 * g / 4) + day - 1402;
|
||||||
|
const J = e + Math.floor((153 * f + 2) / 5);
|
||||||
|
const dg = 38 - Math.floor(Math.floor((g + 184) / 100) * 3 / 4);
|
||||||
|
return J + dg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateJDN(date: Date) {
|
||||||
|
return gregorianJDN(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jdnGregorian(jdn: number): Date {
|
||||||
|
const e = 4 * (jdn + 1401 + Math.floor(Math.floor((4 * jdn + 274277) / 146097) * 3 / 4) - 38) + 3;
|
||||||
|
const h = 5 * Math.floor((e % 1461 + 1461) % 1461 / 4) + 2;
|
||||||
|
const day = Math.floor((h % 153 + 153) % 153 / 5) + 1;
|
||||||
|
const month = (Math.floor(h / 153) + 2) % 12 + 1;
|
||||||
|
const year = Math.floor(e / 1461) - 4716 + Math.floor((14 - month) / 12);
|
||||||
|
return new Date(year, month - 1, day);
|
||||||
|
}
|
62
common/src/longCount.test.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import {jdnLongCount} from './longCount';
|
||||||
|
|
||||||
|
describe('jdnLongCount', () => {
|
||||||
|
it('works for normal dates', () => {
|
||||||
|
expect(jdnLongCount(2456282)).toEqual('12.19.19.17.19');
|
||||||
|
expect(jdnLongCount(2459413)).toEqual('13.0.8.12.10');
|
||||||
|
expect(jdnLongCount(1708616)).toEqual('7.16.3.2.13');
|
||||||
|
expect(jdnLongCount(1787816)).toEqual('8.7.3.2.13');
|
||||||
|
expect(jdnLongCount(1709981)).toEqual('7.16.6.16.18');
|
||||||
|
expect(jdnLongCount(1725275)).toEqual('7.18.9.7.12');
|
||||||
|
expect(jdnLongCount(1727095)).toEqual('7.18.14.8.12');
|
||||||
|
expect(jdnLongCount(1731775)).toEqual('7.19.7.8.12');
|
||||||
|
expect(jdnLongCount(1734635)).toEqual('7.19.15.7.12');
|
||||||
|
expect(jdnLongCount(1751618)).toEqual('8.2.2.10.15');
|
||||||
|
expect(jdnLongCount(1758818)).toEqual('8.3.2.10.15');
|
||||||
|
expect(jdnLongCount(1767234)).toEqual('8.4.5.17.11');
|
||||||
|
expect(jdnLongCount(1773428)).toEqual('8.5.3.3.5');
|
||||||
|
expect(jdnLongCount(1778230)).toEqual('8.5.16.9.7');
|
||||||
|
expect(jdnLongCount(1780300)).toEqual('8.6.2.4.17');
|
||||||
|
expect(jdnLongCount(2283483)).toEqual('11.16.0.0.0');
|
||||||
|
expect(jdnLongCount(584283)).toEqual('0.0.0.0.0');
|
||||||
|
expect(jdnLongCount(728283)).toEqual('1.0.0.0.0');
|
||||||
|
expect(jdnLongCount(872283)).toEqual('2.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1016283)).toEqual('3.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1160283)).toEqual('4.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1304283)).toEqual('5.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1448283)).toEqual('6.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1592283)).toEqual('7.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1736283)).toEqual('8.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1880283)).toEqual('9.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2024283)).toEqual('10.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2168283)).toEqual('11.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2312283)).toEqual('12.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2456283)).toEqual('13.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2600283)).toEqual('14.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2744283)).toEqual('15.0.0.0.0');
|
||||||
|
expect(jdnLongCount(2888283)).toEqual('16.0.0.0.0');
|
||||||
|
expect(jdnLongCount(3032283)).toEqual('17.0.0.0.0');
|
||||||
|
expect(jdnLongCount(3176283)).toEqual('18.0.0.0.0');
|
||||||
|
expect(jdnLongCount(3320283)).toEqual('19.0.0.0.0');
|
||||||
|
expect(jdnLongCount(3464283)).toEqual('1.0.0.0.0.0');
|
||||||
|
expect(jdnLongCount(1941383)).toEqual('9.8.9.13.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works for insane dates in the future', () => {
|
||||||
|
expect(jdnLongCount(591279564516)).toEqual('1.5.13.5.5.4.0.11.13.13');
|
||||||
|
expect(jdnLongCount(570988471138)).toEqual('1.4.15.12.19.13.13.3.8.15');
|
||||||
|
expect(jdnLongCount(166410754861)).toEqual('7.4.9.1.6.3.13.14.18');
|
||||||
|
expect(jdnLongCount(176632006419)).toEqual('7.13.6.10.7.1.19.4.16');
|
||||||
|
expect(jdnLongCount(652557304645)).toEqual('1.8.6.9.2.3.17.16.10.2');
|
||||||
|
expect(jdnLongCount(140305417242)).toEqual('6.1.15.16.19.2.7.1.19');
|
||||||
|
expect(jdnLongCount(805888002058)).toEqual('1.14.19.11.2.0.8.0.8.15');
|
||||||
|
expect(jdnLongCount(176433890202)).toEqual('7.13.3.1.11.5.16.7.19');
|
||||||
|
expect(jdnLongCount(331888546361)).toEqual('14.8.1.18.17.10.5.13.18');
|
||||||
|
expect(jdnLongCount(657363764536)).toEqual('1.8.10.12.11.2.1.14.0.13');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for dates before inception of calendar', () => {
|
||||||
|
expect(jdnLongCount(0)).toBeNull();
|
||||||
|
expect(jdnLongCount(584282)).toBeNull();
|
||||||
|
});
|
||||||
|
})
|
18
common/src/longCount.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export function jdnLongCount(jdn: number): string | null {
|
||||||
|
let z = jdn - 584283;
|
||||||
|
if (z < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const parts = [z % 20, Math.floor(z / 20) % 18];
|
||||||
|
z = Math.floor(z / 360);
|
||||||
|
while (z > 0) {
|
||||||
|
parts.push(z % 20);
|
||||||
|
z = Math.floor(z / 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (parts.length < 5) {
|
||||||
|
parts.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.reverse().join('.');
|
||||||
|
}
|
61
frcal/README.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# French Republican Calendar
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
|
||||||
|
The [French Republican calendar][0] was a calendar created and implemented
|
||||||
|
during the French Revolution.
|
||||||
|
|
||||||
|
It was also frequently known as the *French Revolutionary Calendar*, but this
|
||||||
|
was a misnomer: year 1 of the calendar started on 22 September 1792, the day
|
||||||
|
after the [abolition of the monarchy][1] and the founding of the [French
|
||||||
|
First Republic][2].
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
|
||||||
|
A year consists of 12 months of 30 days each, divided into three *décades* of
|
||||||
|
10 days each, followed by 5 complementary days (6 in leap years).
|
||||||
|
|
||||||
|
The year starts on the day of the autumnal equinox at the Paris Observatory
|
||||||
|
(longitude 2°20′14.03″ E). A leap year follow directly from this definition:
|
||||||
|
a year is a leap year when the next autumnal equinox happens 366 days later
|
||||||
|
instead of the normal 365. By this definition, the year will **never** drift
|
||||||
|
with respect to the seasons.
|
||||||
|
|
||||||
|
The 12 months are: *Vendémiaire*, *Brumaire*, *Frimaire*, *Nivôse*,
|
||||||
|
*Pluviôse*, *Ventôse*, *Germinal*, *Floréal*, *Prairial*, *Messidor*,
|
||||||
|
*Thermidor*, *Fructidor.*
|
||||||
|
|
||||||
|
The complementary days are: *la Fête de la Vertu*, *la Fête du Génie*,
|
||||||
|
*la Fête du Travail*, *la Fête de l'Opinion*, *la Fête des Récompenses,*
|
||||||
|
and *la Fête de la Révolution* (leap years only).
|
||||||
|
|
||||||
|
## What's so special about this version?
|
||||||
|
|
||||||
|
Most versions of the calendar floating around doesn't use the original
|
||||||
|
definition above.
|
||||||
|
|
||||||
|
Most versions uses the so-called *Romme* method for leap years, using the
|
||||||
|
same leap year rules as the Gregorian calendar, i.e. every year divisible
|
||||||
|
by four, except century years not divisible by 400. This method might make
|
||||||
|
sense, except years 3, 7, and 11 were leap years under the original rules
|
||||||
|
and were observed as such in real life, but the *Romme* method instead makes
|
||||||
|
years 4, 8, 12 leap years instead.
|
||||||
|
|
||||||
|
This version uses the original rules. The [JPL's DE440 and DE441
|
||||||
|
ephemerides][3] were used to calculate the exact timings of the autumnal
|
||||||
|
equinoxes between the Gregorian years 13201 BCE and 17191 CE (corresponding
|
||||||
|
to the French Republican years -14991 to 15399). The times were then converted
|
||||||
|
to UT1+00:09:21, the exact local time at the Paris Observatory. UT1 was chosen
|
||||||
|
to keep track of the Earth's rotation without having to worry about the issues
|
||||||
|
posed by leap seconds in UTC. Note that due to the uncertainty over [ΔT][4] —
|
||||||
|
the difference between UT1 and Terrestrial Time (TT) used in the ephemerides —
|
||||||
|
it is theoretically possible for there to be inaccuracies when the equinox
|
||||||
|
occurs very close to midnight.
|
||||||
|
|
||||||
|
[0]: https://en.wikipedia.org/wiki/French_Republican_calendar
|
||||||
|
[1]: https://en.wikipedia.org/wiki/Proclamation_of_the_abolition_of_the_monarchy
|
||||||
|
[2]: https://en.wikipedia.org/wiki/French_First_Republic
|
||||||
|
[3]: https://ssd.jpl.nasa.gov/planets/eph_export.html
|
||||||
|
[4]: https://en.wikipedia.org/wiki/%CE%94T_(timekeeping)
|
5
frcal/config-overrides.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const {aliasWebpack, aliasJest} = require('react-app-alias-ex');
|
||||||
|
|
||||||
|
const options = {};
|
||||||
|
module.exports = aliasWebpack(options);
|
||||||
|
module.exports.jest = aliasJest(options);
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
14193
package-lock.json → frcal/package-lock.json
generated
|
@ -20,9 +20,9 @@
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-scripts build",
|
"build": "react-app-rewired build",
|
||||||
"test": "react-scripts test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -44,6 +44,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bootstrap": "^5.1.9"
|
"@types/bootstrap": "^5.1.9",
|
||||||
|
"react-app-alias-ex": "^2.1.0",
|
||||||
|
"react-app-rewired": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
@ -1,21 +1,20 @@
|
||||||
import React, {FormEvent} from 'react';
|
import React, {FormEvent} from 'react';
|
||||||
import {Calendar} from './Calendar';
|
import {Calendar} from './Calendar';
|
||||||
import {
|
import {
|
||||||
dateJDN,
|
|
||||||
endGregorian,
|
endGregorian,
|
||||||
endJD,
|
endJD,
|
||||||
|
FrenchMonth,
|
||||||
frSupportedYear,
|
frSupportedYear,
|
||||||
gregorianJDN,
|
|
||||||
jdnFrench,
|
jdnFrench,
|
||||||
Month,
|
|
||||||
startGregorian,
|
startGregorian,
|
||||||
startJD
|
startJD,
|
||||||
} from './dates';
|
} from '@common/french';
|
||||||
|
import {dateJDN, gregorianJDN} from '@common/gregorian';
|
||||||
import {TimeOfDay} from './TimeOfDay';
|
import {TimeOfDay} from './TimeOfDay';
|
||||||
|
|
||||||
type YearMonth = {
|
type YearMonth = {
|
||||||
year: number;
|
year: number;
|
||||||
month: Month;
|
month: FrenchMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseURL(): YearMonth | null {
|
function parseURL(): YearMonth | null {
|
||||||
|
@ -27,7 +26,7 @@ function parseURL(): YearMonth | null {
|
||||||
const year = +match[1];
|
const year = +match[1];
|
||||||
if (!frSupportedYear(year) || month < 1 || month > 13)
|
if (!frSupportedYear(year) || month < 1 || month > 13)
|
||||||
return null;
|
return null;
|
||||||
return {year: year, month: month as Month};
|
return {year: year, month: month as FrenchMonth};
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppState = YearMonth & {
|
type AppState = YearMonth & {
|
|
@ -3,26 +3,26 @@ import './Calendar.scss';
|
||||||
import {
|
import {
|
||||||
dateName,
|
dateName,
|
||||||
dateRuralName,
|
dateRuralName,
|
||||||
Day,
|
|
||||||
decadeNames,
|
decadeNames,
|
||||||
endYear,
|
endYear,
|
||||||
|
FrenchDay,
|
||||||
|
FrenchMonth,
|
||||||
frIsLeap,
|
frIsLeap,
|
||||||
frJDN,
|
frJDN,
|
||||||
jdnFrench,
|
jdnFrench,
|
||||||
jdnGregorian,
|
|
||||||
jdnLongCount,
|
|
||||||
Month,
|
|
||||||
monthName,
|
monthName,
|
||||||
startYear
|
startYear,
|
||||||
} from './dates';
|
} from '@common/french';
|
||||||
|
import {jdnGregorian} from '@common/gregorian';
|
||||||
|
import {jdnLongCount} from '@common/longCount';
|
||||||
|
|
||||||
type MonthProps = {
|
type MonthProps = {
|
||||||
year: number;
|
year: number;
|
||||||
month: Month;
|
month: FrenchMonth;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DateProps = MonthProps & {
|
type DateProps = MonthProps & {
|
||||||
day: Day;
|
day: FrenchDay;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DecadeName({name}: { name: string }): JSX.Element {
|
function DecadeName({name}: { name: string }): JSX.Element {
|
||||||
|
@ -54,7 +54,7 @@ function NormalMonth({year, month, todayJDN}: MonthProps & { todayJDN: number })
|
||||||
<div className="Month-decades">{
|
<div className="Month-decades">{
|
||||||
Array.from(Array(3).keys()).map(i => <div key={i} className="Month-decade">{
|
Array.from(Array(3).keys()).map(i => <div key={i} className="Month-decade">{
|
||||||
Array.from(Array(10).keys()).map(j => <React.Fragment key={j}>
|
Array.from(Array(10).keys()).map(j => <React.Fragment key={j}>
|
||||||
<NormalDay year={year} month={month} day={i * 10 + j + 1 as Day} todayJDN={todayJDN}/>
|
<NormalDay year={year} month={month} day={i * 10 + j + 1 as FrenchDay} todayJDN={todayJDN}/>
|
||||||
{j % 2 === 1 && <div className="Month-decadeSplitter-small"/>}
|
{j % 2 === 1 && <div className="Month-decadeSplitter-small"/>}
|
||||||
{j === 4 && <div className="Month-decadeSplitter-medium"/>}
|
{j === 4 && <div className="Month-decadeSplitter-medium"/>}
|
||||||
</React.Fragment>)
|
</React.Fragment>)
|
||||||
|
@ -75,7 +75,7 @@ function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number })
|
||||||
const leap = frIsLeap(year);
|
const leap = frIsLeap(year);
|
||||||
return <div className="ComplementaryDays">{
|
return <div className="ComplementaryDays">{
|
||||||
Array.from(Array(6).keys()).map(i => <React.Fragment key={i}>
|
Array.from(Array(6).keys()).map(i => <React.Fragment key={i}>
|
||||||
{(i < 5 || leap) && <ComplementaryDay year={year} month={13} day={i + 1 as Day} todayJDN={todayJDN}/>}
|
{(i < 5 || leap) && <ComplementaryDay year={year} month={13} day={i + 1 as FrenchDay} todayJDN={todayJDN}/>}
|
||||||
{i === 5 && !leap && <div className="ComplementaryDay-fake"/>}
|
{i === 5 && !leap && <div className="ComplementaryDay-fake"/>}
|
||||||
{i % 2 === 1 && <div className="ComplementaryDays-splitter"/>}
|
{i % 2 === 1 && <div className="ComplementaryDays-splitter"/>}
|
||||||
</React.Fragment>)
|
</React.Fragment>)
|
||||||
|
@ -84,7 +84,7 @@ function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number })
|
||||||
|
|
||||||
export type CalendarProps = MonthProps & {
|
export type CalendarProps = MonthProps & {
|
||||||
todayJDN: number;
|
todayJDN: number;
|
||||||
onSwitch?: (year: number, month: Month) => void,
|
onSwitch?: (year: number, month: FrenchMonth) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type CalendarState = {
|
type CalendarState = {
|
||||||
|
@ -131,7 +131,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
||||||
month = 13;
|
month = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSwitch && this.props.onSwitch(year, month as Month);
|
this.props.onSwitch && this.props.onSwitch(year, month as FrenchMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevYear = () => {
|
prevYear = () => {
|
||||||
|
@ -165,7 +165,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
monthChange = (event: any) => {
|
monthChange = (event: any) => {
|
||||||
this.goToNormalized(this.props.year, +event.target.value as Month);
|
this.goToNormalized(this.props.year, +event.target.value as FrenchMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
yearChange = (event: any) => {
|
yearChange = (event: any) => {
|
||||||
|
@ -210,7 +210,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
||||||
<select className="Calendar-month-input form-control" onChange={this.monthChange}
|
<select className="Calendar-month-input form-control" onChange={this.monthChange}
|
||||||
value={this.props.month}>{
|
value={this.props.month}>{
|
||||||
Array.from(Array(13).keys()).map(i => {
|
Array.from(Array(13).keys()).map(i => {
|
||||||
const month = i + 1 as Month;
|
const month = i + 1 as FrenchMonth;
|
||||||
return <option key={i} value={month}>{monthName(month)}</option>;
|
return <option key={i} value={month}>{monthName(month)}</option>;
|
||||||
})
|
})
|
||||||
}</select>
|
}</select>
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './TimeOfDay.scss';
|
import './TimeOfDay.scss';
|
||||||
import {dateJDN, gregorianJDN} from './dates';
|
import {dateJDN, gregorianJDN} from '@common/gregorian';
|
||||||
|
|
||||||
type TimeStamp = {
|
type TimeStamp = {
|
||||||
hour: number,
|
hour: number,
|
1
frcal/src/cal.json
Normal file
27
frcal/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.paths.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
8
frcal/tsconfig.paths.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@common/*": ["../common/src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,195 +0,0 @@
|
||||||
import {
|
|
||||||
dateName,
|
|
||||||
dateRuralName,
|
|
||||||
frIsLeap,
|
|
||||||
frJDN,
|
|
||||||
gregorianJDN,
|
|
||||||
jdnFrench,
|
|
||||||
jdnGregorian,
|
|
||||||
jdnLongCount,
|
|
||||||
monthName
|
|
||||||
} from './dates';
|
|
||||||
|
|
||||||
describe('gregorianJDN', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(gregorianJDN(2000, 1, 1)).toBe(2451545);
|
|
||||||
expect(gregorianJDN(-4713, 11, 24)).toBe(0);
|
|
||||||
expect(gregorianJDN(11917, 9, 18)).toBe(6073915);
|
|
||||||
expect(gregorianJDN(-28565, 6, 17)).toBe(-8711925);
|
|
||||||
expect(gregorianJDN(-26650, 4, 13)).toBe(-8012550);
|
|
||||||
expect(gregorianJDN(17430, 3, 8)).toBe(8087303);
|
|
||||||
expect(gregorianJDN(3395, 7, 18)).toBe(2961257);
|
|
||||||
expect(gregorianJDN(4579, 3, 11)).toBe(3393575);
|
|
||||||
expect(gregorianJDN(-14851, 11, 22)).toBe(-3702831);
|
|
||||||
expect(gregorianJDN(8824, 11, 28)).toBe(4944292);
|
|
||||||
expect(gregorianJDN(19720, 8, 14)).toBe(8923868);
|
|
||||||
expect(gregorianJDN(7504, 7, 22)).toBe(4462042);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('frJDN', () => {
|
|
||||||
it('works for sample dates', () => {
|
|
||||||
expect(frJDN(1, 1, 1)).toBe(2375840);
|
|
||||||
expect(frJDN(8, 2, 18)).toBe(2378444);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works in years starting/ending near midnight', () => {
|
|
||||||
expect(frJDN( 111, 1, 1)).toBe(2416017); // equinox 1902-09-23T23:55:19 UT1
|
|
||||||
expect(frJDN( 206, 1, 1)).toBe(2450715); // equinox 1997-09-22T23:55:46 UT1
|
|
||||||
expect(frJDN(2490, 1, 1)).toBe(3284926); // equinox 4281-09-20T23:50:38 UT1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('frIsLeap', () => {
|
|
||||||
it('works for sample dates', () => {
|
|
||||||
expect(frIsLeap(1)).toBeFalsy();
|
|
||||||
expect(frIsLeap(8)).toBeFalsy();
|
|
||||||
expect(frIsLeap(3)).toBeTruthy();
|
|
||||||
expect(frIsLeap(7)).toBeTruthy();
|
|
||||||
expect(frIsLeap(11)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works in years starting/ending near midnight', () => {
|
|
||||||
expect(frIsLeap(110)).toBeTruthy();
|
|
||||||
expect(frIsLeap(205)).toBeTruthy();
|
|
||||||
expect(frIsLeap(2489)).toBeFalsy();
|
|
||||||
expect(frIsLeap(111)).toBeFalsy();
|
|
||||||
expect(frIsLeap(206)).toBeFalsy();
|
|
||||||
expect(frIsLeap(2490)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('jdnFrench', () => {
|
|
||||||
it('works for sample dates', () => {
|
|
||||||
expect(jdnFrench(2375840)).toEqual({year: 1, month: 1, day: 1});
|
|
||||||
expect(jdnFrench(2378444)).toEqual({year: 8, month: 2, day: 18});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works in years starting/ending near midnight', () => {
|
|
||||||
expect(jdnFrench(2416017)).toEqual({year: 111, month: 1, day: 1});
|
|
||||||
expect(jdnFrench(2450715)).toEqual({year: 206, month: 1, day: 1});
|
|
||||||
expect(jdnFrench(3284926)).toEqual({year: 2490, month: 1, day: 1});
|
|
||||||
expect(jdnFrench(2416016)).toEqual({year: 110, month: 13, day: 6});
|
|
||||||
expect(jdnFrench(2450714)).toEqual({year: 205, month: 13, day: 6});
|
|
||||||
expect(jdnFrench(3284925)).toEqual({year: 2489, month: 13, day: 5});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('jdnGregorian', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(jdnGregorian(0)).toEqual(new Date(-4713, 10, 24));
|
|
||||||
expect(jdnGregorian(2299160)).toEqual(new Date(1582, 9, 14));
|
|
||||||
expect(jdnGregorian(2299161)).toEqual(new Date(1582, 9, 15));
|
|
||||||
expect(jdnGregorian(2361221)).toEqual(new Date(1752, 8, 13));
|
|
||||||
expect(jdnGregorian(2361222)).toEqual(new Date(1752, 8, 14));
|
|
||||||
expect(jdnGregorian(2451545)).toEqual(new Date(2000, 0, 1));
|
|
||||||
expect(jdnGregorian(-8512316)).toEqual(new Date(-28019, 11, 20));
|
|
||||||
expect(jdnGregorian(-8534852)).toEqual(new Date(-28080, 3, 8));
|
|
||||||
expect(jdnGregorian(2653462)).toEqual(new Date(2552, 9, 30));
|
|
||||||
expect(jdnGregorian(3271156)).toEqual(new Date(4244, 0, 8));
|
|
||||||
expect(jdnGregorian(-666477)).toEqual(new Date(-6537, 1, 23));
|
|
||||||
expect(jdnGregorian(2397854)).toEqual(new Date(1852, 11, 31));
|
|
||||||
expect(jdnGregorian(-1211235)).toEqual(new Date(-8029, 7, 26));
|
|
||||||
expect(jdnGregorian(-91680)).toEqual(new Date(-4964, 10, 20));
|
|
||||||
expect(jdnGregorian(-5605876)).toEqual(new Date(-20061, 6, 14));
|
|
||||||
expect(jdnGregorian(-295121)).toEqual(new Date(-5521, 10, 19));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('jdnLongCount', () => {
|
|
||||||
it('works for normal dates', () => {
|
|
||||||
expect(jdnLongCount(2456282)).toEqual('12.19.19.17.19');
|
|
||||||
expect(jdnLongCount(2459413)).toEqual('13.0.8.12.10');
|
|
||||||
expect(jdnLongCount(1708616)).toEqual('7.16.3.2.13');
|
|
||||||
expect(jdnLongCount(1787816)).toEqual('8.7.3.2.13');
|
|
||||||
expect(jdnLongCount(1709981)).toEqual('7.16.6.16.18');
|
|
||||||
expect(jdnLongCount(1725275)).toEqual('7.18.9.7.12');
|
|
||||||
expect(jdnLongCount(1727095)).toEqual('7.18.14.8.12');
|
|
||||||
expect(jdnLongCount(1731775)).toEqual('7.19.7.8.12');
|
|
||||||
expect(jdnLongCount(1734635)).toEqual('7.19.15.7.12');
|
|
||||||
expect(jdnLongCount(1751618)).toEqual('8.2.2.10.15');
|
|
||||||
expect(jdnLongCount(1758818)).toEqual('8.3.2.10.15');
|
|
||||||
expect(jdnLongCount(1767234)).toEqual('8.4.5.17.11');
|
|
||||||
expect(jdnLongCount(1773428)).toEqual('8.5.3.3.5');
|
|
||||||
expect(jdnLongCount(1778230)).toEqual('8.5.16.9.7');
|
|
||||||
expect(jdnLongCount(1780300)).toEqual('8.6.2.4.17');
|
|
||||||
expect(jdnLongCount(2283483)).toEqual('11.16.0.0.0');
|
|
||||||
expect(jdnLongCount(584283)).toEqual('0.0.0.0.0');
|
|
||||||
expect(jdnLongCount(728283)).toEqual('1.0.0.0.0');
|
|
||||||
expect(jdnLongCount(872283)).toEqual('2.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1016283)).toEqual('3.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1160283)).toEqual('4.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1304283)).toEqual('5.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1448283)).toEqual('6.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1592283)).toEqual('7.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1736283)).toEqual('8.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1880283)).toEqual('9.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2024283)).toEqual('10.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2168283)).toEqual('11.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2312283)).toEqual('12.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2456283)).toEqual('13.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2600283)).toEqual('14.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2744283)).toEqual('15.0.0.0.0');
|
|
||||||
expect(jdnLongCount(2888283)).toEqual('16.0.0.0.0');
|
|
||||||
expect(jdnLongCount(3032283)).toEqual('17.0.0.0.0');
|
|
||||||
expect(jdnLongCount(3176283)).toEqual('18.0.0.0.0');
|
|
||||||
expect(jdnLongCount(3320283)).toEqual('19.0.0.0.0');
|
|
||||||
expect(jdnLongCount(3464283)).toEqual('1.0.0.0.0.0');
|
|
||||||
expect(jdnLongCount(1941383)).toEqual('9.8.9.13.0');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works for insane dates in the future', () => {
|
|
||||||
expect(jdnLongCount(591279564516)).toEqual('1.5.13.5.5.4.0.11.13.13');
|
|
||||||
expect(jdnLongCount(570988471138)).toEqual('1.4.15.12.19.13.13.3.8.15');
|
|
||||||
expect(jdnLongCount(166410754861)).toEqual('7.4.9.1.6.3.13.14.18');
|
|
||||||
expect(jdnLongCount(176632006419)).toEqual('7.13.6.10.7.1.19.4.16');
|
|
||||||
expect(jdnLongCount(652557304645)).toEqual('1.8.6.9.2.3.17.16.10.2');
|
|
||||||
expect(jdnLongCount(140305417242)).toEqual('6.1.15.16.19.2.7.1.19');
|
|
||||||
expect(jdnLongCount(805888002058)).toEqual('1.14.19.11.2.0.8.0.8.15');
|
|
||||||
expect(jdnLongCount(176433890202)).toEqual('7.13.3.1.11.5.16.7.19');
|
|
||||||
expect(jdnLongCount(331888546361)).toEqual('14.8.1.18.17.10.5.13.18');
|
|
||||||
expect(jdnLongCount(657363764536)).toEqual('1.8.10.12.11.2.1.14.0.13');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null for dates before inception of calendar', () => {
|
|
||||||
expect(jdnLongCount(0)).toBeNull();
|
|
||||||
expect(jdnLongCount(584282)).toBeNull();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('monthName', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(monthName(1)).toBe('Vendémiaire');
|
|
||||||
expect(monthName(12)).toBe('Fructidor');
|
|
||||||
expect(monthName(13)).toBe('Jours Complémentaires');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('dateName', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(dateName(1, 1)).toBe('1 Vendémiaire');
|
|
||||||
expect(dateName(2, 18)).toBe('18 Brumaire');
|
|
||||||
expect(dateName(3, 11)).toBe('11 Frimaire');
|
|
||||||
expect(dateName(8, 16)).toBe('16 Floréal');
|
|
||||||
expect(dateName(12, 30)).toBe('30 Fructidor');
|
|
||||||
expect(dateName(13, 1)).toBe('La Fête de la Vertu');
|
|
||||||
expect(dateName(13, 6)).toBe('La Fête de la Révolution');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null for non-existent complimentary days', () => {
|
|
||||||
expect(dateName(13, 7)).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('dateRuralName', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(dateRuralName(1, 1)).toEqual({name: 'Raisin', title: 'Grape'});
|
|
||||||
expect(dateRuralName(1, 30)).toEqual({name: 'Tonneau', title: 'Barrel'});
|
|
||||||
expect(dateRuralName(12, 1)).toEqual({name: 'Prune', title: 'Plum'});
|
|
||||||
expect(dateRuralName(12, 30)).toEqual({name: 'Panier', title: 'Pack Basket'});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null for complimentary days', () => {
|
|
||||||
expect(dateRuralName(13, 1)).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|