feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
2026-08-15 22:27:52 +02:00
parent e074f04bdf
commit d4d99a7473
18854 changed files with 2615729 additions and 173735 deletions

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar';
const BUDDHIST_ERA_START = -543;
/**
* The Buddhist calendar is the same as the Gregorian calendar, but counts years
* starting from the birth of Buddha in 543 BC (Gregorian). It supports only one
* era, identified as 'BE'.
*/
export class BuddhistCalendar extends GregorianCalendar {
identifier: CalendarIdentifier = 'buddhist';
fromJulianDay(jd: number): CalendarDate {
let gregorianDate = super.fromJulianDay(jd);
let year = getExtendedYear(gregorianDate.era, gregorianDate.year);
return new CalendarDate(
this,
year - BUDDHIST_ERA_START,
gregorianDate.month,
gregorianDate.day
);
}
toJulianDay(date: AnyCalendarDate): number {
return super.toJulianDay(toGregorian(date));
}
getEras(): string[] {
return ['BE'];
}
getDaysInMonth(date: AnyCalendarDate): number {
return super.getDaysInMonth(toGregorian(date));
}
balanceDate(): void {}
}
function toGregorian(date: AnyCalendarDate) {
let [era, year] = fromExtendedYear(date.year + BUDDHIST_ERA_START);
return new CalendarDate(era, year, date.month, date.day);
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {Mutable} from '../utils';
const ETHIOPIC_EPOCH = 1723856;
const COPTIC_EPOCH = 1824665;
// The delta between Amete Alem 1 and Amete Mihret 1
// AA 5501 = AM 1
const AMETE_MIHRET_DELTA = 5500;
function ceToJulianDay(epoch: number, year: number, month: number, day: number): number {
return (
epoch + // difference from Julian epoch to 1,1,1
365 * year + // number of days from years
Math.floor(year / 4) + // extra day of leap year
30 * (month - 1) + // number of days from months (1 based)
day -
1 // number of days for present month (1 based)
);
}
function julianDayToCE(epoch: number, jd: number) {
let year = Math.floor((4 * (jd - epoch)) / 1461);
let month = 1 + Math.floor((jd - ceToJulianDay(epoch, year, 1, 1)) / 30);
let day = jd + 1 - ceToJulianDay(epoch, year, month, 1);
return [year, month, day];
}
function getLeapDay(year: number) {
return Math.floor((year % 4) / 3);
}
function getDaysInMonth(year: number, month: number) {
// The Ethiopian and Coptic calendars have 13 months, 12 of 30 days each and
// an intercalary month at the end of the year of 5 or 6 days, depending whether
// the year is a leap year or not. The Leap Year follows the same rules as the
// Julian Calendar so that the extra month always has six days in the year before
// a Julian Leap Year.
if (month % 13 !== 0) {
// not intercalary month
return 30;
} else {
// intercalary month 5 days + possible leap day
return getLeapDay(year) + 5;
}
}
/**
* The Ethiopic calendar system is the official calendar used in Ethiopia.
* It includes 12 months of 30 days each, plus 5 or 6 intercalary days depending
* on whether it is a leap year. Two eras are supported: 'AA' and 'AM'.
*/
export class EthiopicCalendar implements Calendar {
identifier: CalendarIdentifier = 'ethiopic';
fromJulianDay(jd: number): CalendarDate {
let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd);
let era = 'AM';
if (year <= 0) {
era = 'AA';
year += AMETE_MIHRET_DELTA;
}
return new CalendarDate(this, era, year, month, day);
}
toJulianDay(date: AnyCalendarDate): number {
let year = date.year;
if (date.era === 'AA') {
year -= AMETE_MIHRET_DELTA;
}
return ceToJulianDay(ETHIOPIC_EPOCH, year, date.month, date.day);
}
getDaysInMonth(date: AnyCalendarDate): number {
return getDaysInMonth(date.year, date.month);
}
getMonthsInYear(): number {
return 13;
}
getDaysInYear(date: AnyCalendarDate): number {
return 365 + getLeapDay(date.year);
}
getMaximumMonthsInYear(): number {
return 13;
}
getMaximumDaysInMonth(): number {
return 30;
}
getYearsInEra(date: AnyCalendarDate): number {
// 9999-12-31 gregorian is 9992-20-02 ethiopic.
// Round down to 9991 for the last full year.
// AA 9999-01-01 ethiopic is 4506-09-30 gregorian.
return date.era === 'AA' ? 9999 : 9991;
}
getEras(): string[] {
return ['AA', 'AM'];
}
}
/**
* The Ethiopic (Amete Alem) calendar is the same as the modern Ethiopic calendar,
* except years were measured from a different epoch. Only one era is supported: 'AA'.
*/
export class EthiopicAmeteAlemCalendar extends EthiopicCalendar {
identifier: CalendarIdentifier = 'ethioaa'; // also known as 'ethiopic-amete-alem' in ICU
fromJulianDay(jd: number): CalendarDate {
let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd);
year += AMETE_MIHRET_DELTA;
return new CalendarDate(this, 'AA', year, month, day);
}
getEras(): string[] {
return ['AA'];
}
getYearsInEra(): number {
// 9999-13-04 ethioaa is the maximum date, which is equivalent to 4506-09-29 gregorian.
return 9999;
}
}
/**
* The Coptic calendar is similar to the Ethiopic calendar.
* It includes 12 months of 30 days each, plus 5 or 6 intercalary days depending
* on whether it is a leap year. Two eras are supported: 'BCE' and 'CE'.
*/
export class CopticCalendar extends EthiopicCalendar {
identifier: CalendarIdentifier = 'coptic';
fromJulianDay(jd: number): CalendarDate {
let [year, month, day] = julianDayToCE(COPTIC_EPOCH, jd);
let era = 'CE';
if (year <= 0) {
era = 'BCE';
year = 1 - year;
}
return new CalendarDate(this, era, year, month, day);
}
toJulianDay(date: AnyCalendarDate): number {
let year = date.year;
if (date.era === 'BCE') {
year = 1 - year;
}
return ceToJulianDay(COPTIC_EPOCH, year, date.month, date.day);
}
getDaysInMonth(date: AnyCalendarDate): number {
let year = date.year;
if (date.era === 'BCE') {
year = 1 - year;
}
return getDaysInMonth(year, date.month);
}
isInverseEra(date: AnyCalendarDate): boolean {
return date.era === 'BCE';
}
balanceDate(date: Mutable<AnyCalendarDate>): void {
if (date.year <= 0) {
date.era = date.era === 'BCE' ? 'CE' : 'BCE';
date.year = 1 - date.year;
}
}
getEras(): string[] {
return ['BCE', 'CE'];
}
getYearsInEra(date: AnyCalendarDate): number {
// 9999-12-30 gregorian is 9716-02-20 coptic.
// Round down to 9715 for the last full year.
// BCE 9999-01-01 coptic is BC 9716-06-15 gregorian.
return date.era === 'BCE' ? 9999 : 9715;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {mod, Mutable} from '../utils';
const EPOCH = 1721426; // 001/01/03 Julian C.E.
export function gregorianToJulianDay(
era: string,
year: number,
month: number,
day: number
): number {
year = getExtendedYear(era, year);
let y1 = year - 1;
let monthOffset = -2;
if (month <= 2) {
monthOffset = 0;
} else if (isLeapYear(year)) {
monthOffset = -1;
}
return (
EPOCH -
1 +
365 * y1 +
Math.floor(y1 / 4) -
Math.floor(y1 / 100) +
Math.floor(y1 / 400) +
Math.floor((367 * month - 362) / 12 + monthOffset + day)
);
}
export function isLeapYear(year: number): boolean {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
export function getExtendedYear(era: string, year: number): number {
return era === 'BC' ? 1 - year : year;
}
export function fromExtendedYear(year: number): [string, number] {
let era = 'AD';
if (year <= 0) {
era = 'BC';
year = 1 - year;
}
return [era, year];
}
const daysInMonth = {
standard: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
leapyear: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
/**
* The Gregorian calendar is the most commonly used calendar system in the world. It supports two
* eras: BC, and AD. Years always contain 12 months, and 365 or 366 days depending on whether it is
* a leap year.
*/
export class GregorianCalendar implements Calendar {
identifier: CalendarIdentifier = 'gregory';
fromJulianDay(jd: number): CalendarDate {
let jd0 = jd;
let depoch = jd0 - EPOCH;
let quadricent = Math.floor(depoch / 146097);
let dqc = mod(depoch, 146097);
let cent = Math.floor(dqc / 36524);
let dcent = mod(dqc, 36524);
let quad = Math.floor(dcent / 1461);
let dquad = mod(dcent, 1461);
let yindex = Math.floor(dquad / 365);
let extendedYear =
quadricent * 400 + cent * 100 + quad * 4 + yindex + (cent !== 4 && yindex !== 4 ? 1 : 0);
let [era, year] = fromExtendedYear(extendedYear);
let yearDay = jd0 - gregorianToJulianDay(era, year, 1, 1);
let leapAdj = 2;
if (jd0 < gregorianToJulianDay(era, year, 3, 1)) {
leapAdj = 0;
} else if (isLeapYear(year)) {
leapAdj = 1;
}
let month = Math.floor(((yearDay + leapAdj) * 12 + 373) / 367);
let day = jd0 - gregorianToJulianDay(era, year, month, 1) + 1;
return new CalendarDate(era, year, month, day);
}
toJulianDay(date: AnyCalendarDate): number {
return gregorianToJulianDay(date.era, date.year, date.month, date.day);
}
getDaysInMonth(date: AnyCalendarDate): number {
return daysInMonth[isLeapYear(date.year) ? 'leapyear' : 'standard'][date.month - 1];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getMonthsInYear(date: AnyCalendarDate): number {
return 12;
}
getDaysInYear(date: AnyCalendarDate): number {
return isLeapYear(date.year) ? 366 : 365;
}
getMaximumMonthsInYear(): number {
return 12;
}
getMaximumDaysInMonth(): number {
return 31;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getYearsInEra(date: AnyCalendarDate): number {
return 9999;
}
getEras(): string[] {
return ['BC', 'AD'];
}
isInverseEra(date: AnyCalendarDate): boolean {
return date.era === 'BC';
}
balanceDate(date: Mutable<AnyCalendarDate>): void {
if (date.year <= 0) {
date.era = date.era === 'BC' ? 'AD' : 'BC';
date.year = 1 - date.year;
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {mod, Mutable} from '../utils';
const HEBREW_EPOCH = 347997;
// Hebrew date calculations are performed in terms of days, hours, and
// "parts" (or halakim), which are 1/1080 of an hour, or 3 1/3 seconds.
const HOUR_PARTS = 1080;
const DAY_PARTS = 24 * HOUR_PARTS;
// An approximate value for the length of a lunar month.
// It is used to calculate the approximate year and month of a given
// absolute date.
const MONTH_DAYS = 29;
const MONTH_FRACT = 12 * HOUR_PARTS + 793;
const MONTH_PARTS = MONTH_DAYS * DAY_PARTS + MONTH_FRACT;
function isLeapYear(year: number) {
return mod(year * 7 + 1, 19) < 7;
}
// Test for delay of start of new year and to avoid
// Sunday, Wednesday, and Friday as start of the new year.
function hebrewDelay1(year: number) {
let months = Math.floor((235 * year - 234) / 19);
let parts = 12084 + 13753 * months;
let day = months * 29 + Math.floor(parts / 25920);
if (mod(3 * (day + 1), 7) < 3) {
day += 1;
}
return day;
}
// Check for delay in start of new year due to length of adjacent years
function hebrewDelay2(year: number) {
let last = hebrewDelay1(year - 1);
let present = hebrewDelay1(year);
let next = hebrewDelay1(year + 1);
if (next - present === 356) {
return 2;
}
if (present - last === 382) {
return 1;
}
return 0;
}
function startOfYear(year: number) {
return hebrewDelay1(year) + hebrewDelay2(year);
}
function getDaysInYear(year: number) {
return startOfYear(year + 1) - startOfYear(year);
}
function getYearType(year: number) {
let yearLength = getDaysInYear(year);
if (yearLength > 380) {
yearLength -= 30; // Subtract length of leap month.
}
switch (yearLength) {
case 353:
return 0; // deficient
case 354:
return 1; // normal
case 355:
return 2; // complete
}
}
function getDaysInMonth(year: number, month: number): number {
// Normalize month numbers from 1 - 13, even on non-leap years
if (month >= 6 && !isLeapYear(year)) {
month++;
}
// First of all, dispose of fixed-length 29 day months
if (month === 4 || month === 7 || month === 9 || month === 11 || month === 13) {
return 29;
}
let yearType = getYearType(year);
// If it's Heshvan, days depend on length of year
if (month === 2) {
return yearType === 2 ? 30 : 29;
}
// Similarly, Kislev varies with the length of year
if (month === 3) {
return yearType === 0 ? 29 : 30;
}
// Adar I only exists in leap years
if (month === 6) {
return isLeapYear(year) ? 30 : 0;
}
return 30;
}
/**
* The Hebrew calendar is used in Israel and around the world by the Jewish faith.
* Years include either 12 or 13 months depending on whether it is a leap year.
* In leap years, an extra month is inserted at month 6.
*/
export class HebrewCalendar implements Calendar {
identifier: CalendarIdentifier = 'hebrew';
fromJulianDay(jd: number): CalendarDate {
let d = jd - HEBREW_EPOCH;
let m = (d * DAY_PARTS) / MONTH_PARTS; // Months (approx)
let year = Math.floor((19 * m + 234) / 235) + 1; // Years (approx)
let ys = startOfYear(year); // 1st day of year
let dayOfYear = Math.floor(d - ys);
// Because of the postponement rules, it's possible to guess wrong. Fix it.
while (dayOfYear < 1) {
year--;
ys = startOfYear(year);
dayOfYear = Math.floor(d - ys);
}
// Now figure out which month we're in, and the date within that month
let month = 1;
let monthStart = 0;
while (monthStart < dayOfYear) {
monthStart += getDaysInMonth(year, month);
month++;
}
month--;
monthStart -= getDaysInMonth(year, month);
let day = dayOfYear - monthStart;
return new CalendarDate(this, year, month, day);
}
toJulianDay(date: AnyCalendarDate): number {
let jd = startOfYear(date.year);
for (let month = 1; month < date.month; month++) {
jd += getDaysInMonth(date.year, month);
}
return jd + date.day + HEBREW_EPOCH;
}
getDaysInMonth(date: AnyCalendarDate): number {
return getDaysInMonth(date.year, date.month);
}
getMonthsInYear(date: AnyCalendarDate): number {
return isLeapYear(date.year) ? 13 : 12;
}
getDaysInYear(date: AnyCalendarDate): number {
return getDaysInYear(date.year);
}
getMaximumMonthsInYear(): number {
return 13;
}
getMaximumDaysInMonth(): number {
return 30;
}
getYearsInEra(): number {
// 6239 gregorian
return 9999;
}
getEras(): string[] {
return ['AM'];
}
balanceYearMonth(date: Mutable<AnyCalendarDate>, previousDate: AnyCalendarDate): void {
// Keep date in the same month when switching between leap years and non leap years
if (previousDate.year !== date.year) {
if (isLeapYear(previousDate.year) && !isLeapYear(date.year) && previousDate.month > 6) {
date.month--;
} else if (
!isLeapYear(previousDate.year) &&
isLeapYear(date.year) &&
previousDate.month > 6
) {
date.month++;
}
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {
fromExtendedYear,
GregorianCalendar,
gregorianToJulianDay,
isLeapYear
} from './GregorianCalendar';
// Starts in 78 AD,
const INDIAN_ERA_START = 78;
// The Indian year starts 80 days later than the Gregorian year.
const INDIAN_YEAR_START = 80;
/**
* The Indian National Calendar is similar to the Gregorian calendar, but with
* years numbered since the Saka era in 78 AD (Gregorian). There are 12 months
* in each year, with either 30 or 31 days. Only one era identifier is supported: 'saka'.
*/
export class IndianCalendar extends GregorianCalendar {
identifier: CalendarIdentifier = 'indian';
fromJulianDay(jd: number): CalendarDate {
// Gregorian date for Julian day
let date = super.fromJulianDay(jd);
// Year in Saka era
let indianYear = date.year - INDIAN_ERA_START;
// Day number in Gregorian year (starting from 0)
let yDay = jd - gregorianToJulianDay(date.era, date.year, 1, 1);
let leapMonth: number;
if (yDay < INDIAN_YEAR_START) {
// Day is at the end of the preceding Saka year
indianYear--;
// Days in leapMonth this year, previous Gregorian year
leapMonth = isLeapYear(date.year - 1) ? 31 : 30;
yDay += leapMonth + 31 * 5 + 30 * 3 + 10;
} else {
// Days in leapMonth this year
leapMonth = isLeapYear(date.year) ? 31 : 30;
yDay -= INDIAN_YEAR_START;
}
let indianMonth: number;
let indianDay: number;
if (yDay < leapMonth) {
indianMonth = 1;
indianDay = yDay + 1;
} else {
let mDay = yDay - leapMonth;
if (mDay < 31 * 5) {
indianMonth = Math.floor(mDay / 31) + 2;
indianDay = (mDay % 31) + 1;
} else {
mDay -= 31 * 5;
indianMonth = Math.floor(mDay / 30) + 7;
indianDay = (mDay % 30) + 1;
}
}
return new CalendarDate(this, indianYear, indianMonth, indianDay);
}
toJulianDay(date: AnyCalendarDate): number {
let extendedYear = date.year + INDIAN_ERA_START;
let [era, year] = fromExtendedYear(extendedYear);
let leapMonth: number;
let jd: number;
if (isLeapYear(year)) {
leapMonth = 31;
jd = gregorianToJulianDay(era, year, 3, 21);
} else {
leapMonth = 30;
jd = gregorianToJulianDay(era, year, 3, 22);
}
if (date.month === 1) {
return jd + date.day - 1;
}
jd += leapMonth + Math.min(date.month - 2, 5) * 31;
if (date.month >= 8) {
jd += (date.month - 7) * 30;
}
jd += date.day - 1;
return jd;
}
getDaysInMonth(date: AnyCalendarDate): number {
if (date.month === 1 && isLeapYear(date.year + INDIAN_ERA_START)) {
return 31;
}
if (date.month >= 2 && date.month <= 6) {
return 31;
}
return 30;
}
getYearsInEra(): number {
// 9999-12-31 gregorian is 9920-10-10 indian.
// Round down to 9919 for the last full year.
return 9919;
}
getEras(): string[] {
return ['saka'];
}
balanceDate(): void {}
}

View File

@@ -0,0 +1,248 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
const CIVIL_EPOC = 1948440; // CE 622 July 16 Friday (Julian calendar) / CE 622 July 19 (Gregorian calendar)
const ASTRONOMICAL_EPOC = 1948439; // CE 622 July 15 Thursday (Julian calendar)
const UMALQURA_YEAR_START = 1300;
const UMALQURA_YEAR_END = 1600;
const UMALQURA_START_DAYS = 460322;
function islamicToJulianDay(epoch: number, year: number, month: number, day: number): number {
return (
day +
Math.ceil(29.5 * (month - 1)) +
(year - 1) * 354 +
Math.floor((3 + 11 * year) / 30) +
epoch -
1
);
}
function julianDayToIslamic(calendar: Calendar, epoch: number, jd: number) {
let year = Math.floor((30 * (jd - epoch) + 10646) / 10631);
let month = Math.min(
12,
Math.ceil((jd - (29 + islamicToJulianDay(epoch, year, 1, 1))) / 29.5) + 1
);
let day = jd - islamicToJulianDay(epoch, year, month, 1) + 1;
return new CalendarDate(calendar, year, month, day);
}
function isLeapYear(year: number): boolean {
return (14 + 11 * year) % 30 < 11;
}
/**
* The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
* world. The civil variant uses simple arithmetic rules rather than astronomical calculations to
* approximate the traditional calendar, which is based on sighting of the crescent moon. It uses
* Friday, July 16 622 CE (Julian) as the epoch. Each year has 12 months, with either 354 or 355
* days depending on whether it is a leap year. Learn more about the available Islamic calendars
* [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
*/
export class IslamicCivilCalendar implements Calendar {
identifier: CalendarIdentifier = 'islamic-civil';
fromJulianDay(jd: number): CalendarDate {
return julianDayToIslamic(this, CIVIL_EPOC, jd);
}
toJulianDay(date: AnyCalendarDate): number {
return islamicToJulianDay(CIVIL_EPOC, date.year, date.month, date.day);
}
getDaysInMonth(date: AnyCalendarDate): number {
let length = 29 + (date.month % 2);
if (date.month === 12 && isLeapYear(date.year)) {
length++;
}
return length;
}
getMonthsInYear(): number {
return 12;
}
getDaysInYear(date: AnyCalendarDate): number {
return isLeapYear(date.year) ? 355 : 354;
}
getMaximumMonthsInYear(): number {
return 12;
}
getMaximumDaysInMonth(): number {
return 30;
}
getYearsInEra(): number {
// 9999 gregorian
return 9665;
}
getEras(): string[] {
return ['AH'];
}
}
/**
* The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
* world. The tabular variant uses simple arithmetic rules rather than astronomical calculations to
* approximate the traditional calendar, which is based on sighting of the crescent moon. It uses
* Thursday, July 15 622 CE (Julian) as the epoch. Each year has 12 months, with either 354 or 355
* days depending on whether it is a leap year. Learn more about the available Islamic calendars
* [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
*/
export class IslamicTabularCalendar extends IslamicCivilCalendar {
identifier: CalendarIdentifier = 'islamic-tbla';
fromJulianDay(jd: number): CalendarDate {
return julianDayToIslamic(this, ASTRONOMICAL_EPOC, jd);
}
toJulianDay(date: AnyCalendarDate): number {
return islamicToJulianDay(ASTRONOMICAL_EPOC, date.year, date.month, date.day);
}
}
// Generated by scripts/generate-umalqura.js
const UMALQURA_DATA =
'qgpUDckO1AbqBmwDrQpVBakGkgepC9QF2gpcBS0NlQZKB1QLagutBa4ETwoXBYsGpQbVCtYCWwmdBE0KJg2VDawFtgm6AlsKKwWVCsoG6Qr0AnYJtgJWCcoKpAvSC9kF3AJtCU0FpQpSC6ULtAW2CVcFlwJLBaMGUgdlC2oFqworBZUMSg2lDcoF1gpXCasESwmlClILagt1BXYCtwhbBFUFqQW0BdoJ3QRuAjYJqgpUDbIN1QXaAlsJqwRVCkkLZAtxC7QFtQpVCiUNkg7JDtQG6QprCasEkwpJDaQNsg25CroEWworBZUKKgtVC1wFvQQ9Ah0JlQpKC1oLbQW2AjsJmwRVBqkGVAdqC2wFrQpVBSkLkgupC9QF2gpaBasKlQVJB2QHqgu1BbYCVgpNDiULUgtqC60FrgIvCZcESwalBqwG1gpdBZ0ETQoWDZUNqgW1BdoCWwmtBJUFygbkBuoK9QS2AlYJqgpUC9IL2QXqAm0JrQSVCkoLpQuyBbUJ1gSXCkcFkwZJB1ULagVrCisFiwpGDaMNygXWCtsEawJLCaUKUgtpC3UFdgG3CFsCKwVlBbQF2gntBG0BtgimClINqQ3UBdoKWwmrBFMGKQdiB6kLsgW1ClUFJQuSDckO0gbpCmsFqwRVCikNVA2qDbUJugQ7CpsETQqqCtUK2gJdCV4ELgqaDFUNsga5BroEXQotBZUKUguoC7QLuQXaAloJSgukDdEO6AZqC20FNQWVBkoNqA3UDdoGWwWdAisGFQtKC5ULqgWuCi4JjwwnBZUGqgbWCl0FnQI=';
let UMALQURA_MONTHLENGTH: Uint16Array;
let UMALQURA_YEAR_START_TABLE: Uint32Array;
function umalquraYearStart(year: number): number {
return UMALQURA_START_DAYS + UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START];
}
function umalquraMonthLength(year: number, month: number): number {
let idx = year - UMALQURA_YEAR_START;
let mask = 0x01 << (11 - (month - 1));
if ((UMALQURA_MONTHLENGTH[idx] & mask) === 0) {
return 29;
} else {
return 30;
}
}
function umalquraMonthStart(year: number, month: number): number {
let day = umalquraYearStart(year);
for (let i = 1; i < month; i++) {
day += umalquraMonthLength(year, i);
}
return day;
}
function umalquraYearLength(year: number): number {
return (
UMALQURA_YEAR_START_TABLE[year + 1 - UMALQURA_YEAR_START] -
UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START]
);
}
/**
* The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
* world. The Umalqura variant is primarily used in Saudi Arabia. It is a lunar calendar, based on
* astronomical calculations that predict the sighting of a crescent moon. Month and year lengths
* vary between years depending on these calculations. Learn more about the available Islamic
* calendars
* [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
*/
export class IslamicUmalquraCalendar extends IslamicCivilCalendar {
identifier: CalendarIdentifier = 'islamic-umalqura';
constructor() {
super();
if (!UMALQURA_MONTHLENGTH) {
UMALQURA_MONTHLENGTH = new Uint16Array(
Uint8Array.from(atob(UMALQURA_DATA), c => c.charCodeAt(0)).buffer
);
}
if (!UMALQURA_YEAR_START_TABLE) {
UMALQURA_YEAR_START_TABLE = new Uint32Array(UMALQURA_YEAR_END - UMALQURA_YEAR_START + 1);
let yearStart = 0;
for (let year = UMALQURA_YEAR_START; year <= UMALQURA_YEAR_END; year++) {
UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START] = yearStart;
for (let i = 1; i <= 12; i++) {
yearStart += umalquraMonthLength(year, i);
}
}
}
}
fromJulianDay(jd: number): CalendarDate {
let days = jd - CIVIL_EPOC;
let startDays = umalquraYearStart(UMALQURA_YEAR_START);
let endDays = umalquraYearStart(UMALQURA_YEAR_END);
if (days < startDays || days > endDays) {
return super.fromJulianDay(jd);
} else {
let y = UMALQURA_YEAR_START - 1;
let m = 1;
let d = 1;
while (d > 0) {
y++;
d = days - umalquraYearStart(y) + 1;
let yearLength = umalquraYearLength(y);
if (d === yearLength) {
m = 12;
break;
} else if (d < yearLength) {
let monthLength = umalquraMonthLength(y, m);
m = 1;
while (d > monthLength) {
d -= monthLength;
m++;
monthLength = umalquraMonthLength(y, m);
}
break;
}
}
return new CalendarDate(this, y, m, days - umalquraMonthStart(y, m) + 1);
}
}
toJulianDay(date: AnyCalendarDate): number {
if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
return super.toJulianDay(date);
}
return CIVIL_EPOC + umalquraMonthStart(date.year, date.month) + (date.day - 1);
}
getDaysInMonth(date: AnyCalendarDate): number {
if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
return super.getDaysInMonth(date);
}
return umalquraMonthLength(date.year, date.month);
}
getDaysInYear(date: AnyCalendarDate): number {
if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
return super.getDaysInYear(date);
}
return umalquraYearLength(date.year);
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from the TC39 Temporal proposal.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {GregorianCalendar} from './GregorianCalendar';
import {Mutable} from '../utils';
const ERA_START_DATES = [
[1868, 9, 8],
[1912, 7, 30],
[1926, 12, 25],
[1989, 1, 8],
[2019, 5, 1]
];
const ERA_END_DATES = [
[1912, 7, 29],
[1926, 12, 24],
[1989, 1, 7],
[2019, 4, 30]
];
const ERA_ADDENDS = [1867, 1911, 1925, 1988, 2018];
const ERA_NAMES = ['meiji', 'taisho', 'showa', 'heisei', 'reiwa'];
function findEraFromGregorianDate(date: AnyCalendarDate) {
const idx = ERA_START_DATES.findIndex(([year, month, day]) => {
if (date.year < year) {
return true;
}
if (date.year === year && date.month < month) {
return true;
}
if (date.year === year && date.month === month && date.day < day) {
return true;
}
return false;
});
if (idx === -1) {
return ERA_START_DATES.length - 1;
}
if (idx === 0) {
return 0;
}
return idx - 1;
}
function toGregorian(date: AnyCalendarDate) {
let eraAddend = ERA_ADDENDS[ERA_NAMES.indexOf(date.era)];
if (!eraAddend) {
throw new Error('Unknown era: ' + date.era);
}
return new CalendarDate(date.year + eraAddend, date.month, date.day);
}
/**
* The Japanese calendar is based on the Gregorian calendar, but with eras for the reign of each
* Japanese emperor. Whenever a new emperor ascends to the throne, a new era begins and the year
* starts again from 1. Note that eras before 1868 (Gregorian) are not currently supported by this
* implementation.
*/
export class JapaneseCalendar extends GregorianCalendar {
identifier: CalendarIdentifier = 'japanese';
fromJulianDay(jd: number): CalendarDate {
let date = super.fromJulianDay(jd);
let era = findEraFromGregorianDate(date);
return new CalendarDate(
this,
ERA_NAMES[era],
date.year - ERA_ADDENDS[era],
date.month,
date.day
);
}
toJulianDay(date: AnyCalendarDate): number {
return super.toJulianDay(toGregorian(date));
}
balanceDate(date: Mutable<AnyCalendarDate>): void {
let gregorianDate = toGregorian(date);
let era = findEraFromGregorianDate(gregorianDate);
if (ERA_NAMES[era] !== date.era) {
date.era = ERA_NAMES[era];
date.year = gregorianDate.year - ERA_ADDENDS[era];
}
// Constrain in case we went before the first supported era.
this.constrainDate(date);
}
constrainDate(date: Mutable<AnyCalendarDate>): void {
let idx = ERA_NAMES.indexOf(date.era);
let end = ERA_END_DATES[idx];
if (end != null) {
let [endYear, endMonth, endDay] = end;
// Constrain the year to the maximum possible value in the era.
// Then constrain the month and day fields within that.
let maxYear = endYear - ERA_ADDENDS[idx];
date.year = Math.max(1, Math.min(maxYear, date.year));
if (date.year === maxYear) {
date.month = Math.min(endMonth, date.month);
if (date.month === endMonth) {
date.day = Math.min(endDay, date.day);
}
}
}
if (date.year === 1 && idx >= 0) {
let [, startMonth, startDay] = ERA_START_DATES[idx];
date.month = Math.max(startMonth, date.month);
if (date.month === startMonth) {
date.day = Math.max(startDay, date.day);
}
}
}
getEras(): string[] {
return ERA_NAMES;
}
getYearsInEra(date: AnyCalendarDate): number {
// Get the number of years in the era, taking into account the date's month and day fields.
let era = ERA_NAMES.indexOf(date.era);
let cur = ERA_START_DATES[era];
let next = ERA_START_DATES[era + 1];
if (next == null) {
// 9999 gregorian is the maximum year allowed.
return 9999 - cur[0] + 1;
}
let years = next[0] - cur[0];
if (date.month < next[1] || (date.month === next[1] && date.day < next[2])) {
years++;
}
return years;
}
getDaysInMonth(date: AnyCalendarDate): number {
return super.getDaysInMonth(toGregorian(date));
}
getMinimumMonthInYear(date: AnyCalendarDate): number {
let start = getMinimums(date);
return start ? start[1] : 1;
}
getMinimumDayInMonth(date: AnyCalendarDate): number {
let start = getMinimums(date);
return start && date.month === start[1] ? start[2] : 1;
}
}
function getMinimums(date: AnyCalendarDate) {
if (date.year === 1) {
let idx = ERA_NAMES.indexOf(date.era);
return ERA_START_DATES[idx];
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {mod} from '../utils';
const PERSIAN_EPOCH = 1948320;
// Number of days from the start of the year to the start of each month.
const MONTH_START = [
0, // Farvardin
31, // Ordibehesht
62, // Khordad
93, // Tir
124, // Mordad
155, // Shahrivar
186, // Mehr
216, // Aban
246, // Azar
276, // Dey
306, // Bahman
336 // Esfand
];
/**
* The Persian calendar is the main calendar used in Iran and Afghanistan. It has 12 months
* in each year, the first 6 of which have 31 days, and the next 5 have 30 days. The 12th month
* has either 29 or 30 days depending on whether it is a leap year. The Persian year starts
* around the March equinox.
*/
export class PersianCalendar implements Calendar {
identifier: CalendarIdentifier = 'persian';
fromJulianDay(jd: number): CalendarDate {
let daysSinceEpoch = jd - PERSIAN_EPOCH;
let year = 1 + Math.floor((33 * daysSinceEpoch + 3) / 12053);
let farvardin1 = 365 * (year - 1) + Math.floor((8 * year + 21) / 33);
let dayOfYear = daysSinceEpoch - farvardin1;
let month = dayOfYear < 216 ? Math.floor(dayOfYear / 31) : Math.floor((dayOfYear - 6) / 30);
let day = dayOfYear - MONTH_START[month] + 1;
return new CalendarDate(this, year, month + 1, day);
}
toJulianDay(date: AnyCalendarDate): number {
let jd = PERSIAN_EPOCH - 1 + 365 * (date.year - 1) + Math.floor((8 * date.year + 21) / 33);
jd += MONTH_START[date.month - 1];
jd += date.day;
return jd;
}
getMonthsInYear(): number {
return 12;
}
getDaysInMonth(date: AnyCalendarDate): number {
if (date.month <= 6) {
return 31;
}
if (date.month <= 11) {
return 30;
}
let isLeapYear = mod(25 * date.year + 11, 33) < 8;
return isLeapYear ? 30 : 29;
}
getMaximumMonthsInYear(): number {
return 12;
}
getMaximumDaysInMonth(): number {
return 31;
}
getEras(): string[] {
return ['AP'];
}
getYearsInEra(): number {
// 9378-10-10 persian is 9999-12-31 gregorian.
// Round down to 9377 to set the maximum full year.
return 9377;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.
import {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar';
import {Mutable} from '../utils';
const TAIWAN_ERA_START = 1911;
function gregorianYear(date: AnyCalendarDate) {
return date.era === 'minguo' ? date.year + TAIWAN_ERA_START : 1 - date.year + TAIWAN_ERA_START;
}
function gregorianToTaiwan(year: number): [string, number] {
let y = year - TAIWAN_ERA_START;
if (y > 0) {
return ['minguo', y];
} else {
return ['before_minguo', 1 - y];
}
}
/**
* The Taiwanese calendar is the same as the Gregorian calendar, but years
* are numbered starting from 1912 (Gregorian). Two eras are supported:
* 'before_minguo' and 'minguo'.
*/
export class TaiwanCalendar extends GregorianCalendar {
identifier: CalendarIdentifier = 'roc'; // Republic of China
fromJulianDay(jd: number): CalendarDate {
let date = super.fromJulianDay(jd);
let extendedYear = getExtendedYear(date.era, date.year);
let [era, year] = gregorianToTaiwan(extendedYear);
return new CalendarDate(this, era, year, date.month, date.day);
}
toJulianDay(date: AnyCalendarDate): number {
return super.toJulianDay(toGregorian(date));
}
getEras(): string[] {
return ['before_minguo', 'minguo'];
}
balanceDate(date: Mutable<AnyCalendarDate>): void {
let [era, year] = gregorianToTaiwan(gregorianYear(date));
date.era = era;
date.year = year;
}
isInverseEra(date: AnyCalendarDate): boolean {
return date.era === 'before_minguo';
}
getDaysInMonth(date: AnyCalendarDate): number {
return super.getDaysInMonth(toGregorian(date));
}
getYearsInEra(date: AnyCalendarDate): number {
return date.era === 'before_minguo' ? 9999 : 9999 - TAIWAN_ERA_START;
}
}
function toGregorian(date: AnyCalendarDate) {
let [era, year] = fromExtendedYear(gregorianYear(date));
return new CalendarDate(era, year, date.month, date.day);
}