Split out common calendar package
2
.gitignore
vendored
|
@ -1,5 +1,5 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
node_modules
|
||||
/.pnp
|
||||
.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?
|
||||
|
||||
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)
|
||||
Currently, the following calendars are supported:
|
||||
* [French Republican Calendar](frcal/README.md)
|
||||
|
|
|
@ -83,7 +83,7 @@ def main():
|
|||
'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:])]
|
||||
}
|
||||
with open('src/cal.json', 'w') as f:
|
||||
with open('src/french/cal.json', 'w') as f:
|
||||
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 ruralName from './rural-days.json';
|
||||
import {jdnGregorian} from '../gregorian';
|
||||
|
||||
// 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 Day =
|
||||
export type FrenchMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
|
||||
export type FrenchDay =
|
||||
1
|
||||
| 2
|
||||
| 3
|
||||
|
@ -37,12 +38,12 @@ export type Day =
|
|||
|
||||
export type FrenchDate = {
|
||||
year: number,
|
||||
month: Month,
|
||||
day: Day,
|
||||
month: FrenchMonth,
|
||||
day: FrenchDay,
|
||||
};
|
||||
|
||||
const monthNames: {
|
||||
[key in Month]: string
|
||||
[key in FrenchMonth]: string
|
||||
} = {
|
||||
1: 'Vendémiaire',
|
||||
2: 'Brumaire',
|
||||
|
@ -75,24 +76,12 @@ data.leap.forEach(leap => {
|
|||
});
|
||||
|
||||
export const endYear = startYear + leaps.length - 1;
|
||||
|
||||
export function frSupportedYear(year: number): boolean {
|
||||
return startYear <= year && year <= endYear;
|
||||
}
|
||||
|
||||
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 frJDN(year: number, month: Month, day: Day): number {
|
||||
export function frJDN(year: number, month: FrenchMonth, day: FrenchDay): number {
|
||||
const dy = year - startYear;
|
||||
const dd = month * 30 + day - 31;
|
||||
return startJD + 365 * dy + leaps[dy] + dd;
|
||||
|
@ -102,18 +91,7 @@ export function frIsLeap(year: number): boolean {
|
|||
return !!data.leap[year - startYear];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export const endJD = frJDN(endYear, 13, frIsLeap(endYear) ? 6: 5);
|
||||
export const startGregorian = jdnGregorian(startJD);
|
||||
export const endGregorian = jdnGregorian(endJD);
|
||||
export const endJD = frJDN(endYear, 13, frIsLeap(endYear) ? 6 : 5);
|
||||
|
||||
export function jdnFrench(jdn: number): FrenchDate {
|
||||
let lo = 0;
|
||||
|
@ -130,35 +108,16 @@ export function jdnFrench(jdn: number): FrenchDate {
|
|||
const dd = jdn - (startJD + 365 * lo + leaps[lo]);
|
||||
return {
|
||||
year: startYear + lo,
|
||||
month: Math.floor(dd / 30) + 1 as Month,
|
||||
day: dd % 30 + 1 as Day,
|
||||
month: Math.floor(dd / 30) + 1 as FrenchMonth,
|
||||
day: dd % 30 + 1 as FrenchDay,
|
||||
}
|
||||
}
|
||||
|
||||
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('.');
|
||||
}
|
||||
|
||||
export function monthName(month: Month): string {
|
||||
export function monthName(month: FrenchMonth): string {
|
||||
return monthNames[month];
|
||||
}
|
||||
|
||||
export function dateName(month: Month, day: Day): string | null {
|
||||
export function dateName(month: FrenchMonth, day: FrenchDay): string | null {
|
||||
if (month === 13) {
|
||||
switch (day) {
|
||||
case 1:
|
||||
|
@ -180,10 +139,13 @@ export function dateName(month: Month, day: Day): string | null {
|
|||
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];
|
||||
if (!rural)
|
||||
return null;
|
||||
const [name, title] = rural;
|
||||
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"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
@ -44,6 +44,8 @@
|
|||
]
|
||||
},
|
||||
"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 {Calendar} from './Calendar';
|
||||
import {
|
||||
dateJDN,
|
||||
endGregorian,
|
||||
endJD,
|
||||
FrenchMonth,
|
||||
frSupportedYear,
|
||||
gregorianJDN,
|
||||
jdnFrench,
|
||||
Month,
|
||||
startGregorian,
|
||||
startJD
|
||||
} from './dates';
|
||||
startJD,
|
||||
} from '@common/french';
|
||||
import {dateJDN, gregorianJDN} from '@common/gregorian';
|
||||
import {TimeOfDay} from './TimeOfDay';
|
||||
|
||||
type YearMonth = {
|
||||
year: number;
|
||||
month: Month;
|
||||
month: FrenchMonth;
|
||||
}
|
||||
|
||||
function parseURL(): YearMonth | null {
|
||||
|
@ -27,7 +26,7 @@ function parseURL(): YearMonth | null {
|
|||
const year = +match[1];
|
||||
if (!frSupportedYear(year) || month < 1 || month > 13)
|
||||
return null;
|
||||
return {year: year, month: month as Month};
|
||||
return {year: year, month: month as FrenchMonth};
|
||||
}
|
||||
|
||||
type AppState = YearMonth & {
|
|
@ -3,26 +3,26 @@ import './Calendar.scss';
|
|||
import {
|
||||
dateName,
|
||||
dateRuralName,
|
||||
Day,
|
||||
decadeNames,
|
||||
endYear,
|
||||
FrenchDay,
|
||||
FrenchMonth,
|
||||
frIsLeap,
|
||||
frJDN,
|
||||
jdnFrench,
|
||||
jdnGregorian,
|
||||
jdnLongCount,
|
||||
Month,
|
||||
monthName,
|
||||
startYear
|
||||
} from './dates';
|
||||
startYear,
|
||||
} from '@common/french';
|
||||
import {jdnGregorian} from '@common/gregorian';
|
||||
import {jdnLongCount} from '@common/longCount';
|
||||
|
||||
type MonthProps = {
|
||||
year: number;
|
||||
month: Month;
|
||||
month: FrenchMonth;
|
||||
};
|
||||
|
||||
type DateProps = MonthProps & {
|
||||
day: Day;
|
||||
day: FrenchDay;
|
||||
};
|
||||
|
||||
function DecadeName({name}: { name: string }): JSX.Element {
|
||||
|
@ -54,7 +54,7 @@ function NormalMonth({year, month, todayJDN}: MonthProps & { todayJDN: number })
|
|||
<div className="Month-decades">{
|
||||
Array.from(Array(3).keys()).map(i => <div key={i} className="Month-decade">{
|
||||
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 === 4 && <div className="Month-decadeSplitter-medium"/>}
|
||||
</React.Fragment>)
|
||||
|
@ -75,7 +75,7 @@ function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number })
|
|||
const leap = frIsLeap(year);
|
||||
return <div className="ComplementaryDays">{
|
||||
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 % 2 === 1 && <div className="ComplementaryDays-splitter"/>}
|
||||
</React.Fragment>)
|
||||
|
@ -84,7 +84,7 @@ function ComplementaryDays({year, todayJDN}: { year: number, todayJDN: number })
|
|||
|
||||
export type CalendarProps = MonthProps & {
|
||||
todayJDN: number;
|
||||
onSwitch?: (year: number, month: Month) => void,
|
||||
onSwitch?: (year: number, month: FrenchMonth) => void,
|
||||
};
|
||||
|
||||
type CalendarState = {
|
||||
|
@ -131,7 +131,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
|||
month = 13;
|
||||
}
|
||||
|
||||
this.props.onSwitch && this.props.onSwitch(year, month as Month);
|
||||
this.props.onSwitch && this.props.onSwitch(year, month as FrenchMonth);
|
||||
}
|
||||
|
||||
prevYear = () => {
|
||||
|
@ -165,7 +165,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
|||
}
|
||||
|
||||
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) => {
|
||||
|
@ -210,7 +210,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
|
|||
<select className="Calendar-month-input form-control" onChange={this.monthChange}
|
||||
value={this.props.month}>{
|
||||
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>;
|
||||
})
|
||||
}</select>
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import './TimeOfDay.scss';
|
||||
import {dateJDN, gregorianJDN} from './dates';
|
||||
import {dateJDN, gregorianJDN} from '@common/gregorian';
|
||||
|
||||
type TimeStamp = {
|
||||
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();
|
||||
});
|
||||
});
|