Skip to content

Commit

Permalink
Update Hijri date calculation from anchor
Browse files Browse the repository at this point in the history
  • Loading branch information
AmrSaber committed Sep 12, 2024
1 parent 2805594 commit 3e7a247
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 686 deletions.
Binary file modified bun.lockb
Binary file not shown.
16 changes: 16 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,19 @@ export const UK_ID = 53;
export const CAMBRIDGE_ID = 20245;

export const UK_TIME_ZONE = 'Europe/London';

// In order
export const HIJRI_MONTHS = [
'muharram',
'safar',
'rabi-i',
'rabi-ii',
'jumada-i',
'jumada-ii',
'rajab',
"sha'ban",
'ramadan',
'shawwal',
"dhul-qa'dah",
'dhul-hijjah',
];
2 changes: 1 addition & 1 deletion src/lib/server/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Record = {
expires_at: number | null;
};

const CACHE_VERSION = 1;
const CACHE_VERSION = 2;
const CACHE_VERSION_KEY = 'cache::version';

export class SqliteCache {
Expand Down
19 changes: 8 additions & 11 deletions src/lib/services.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import memoize from 'lodash.memoize';
import { MyMasjidApi, OwnApi } from './api';
import type { HijriDate, HijriDateAnchor } from './types';
import { getDayOfYear } from './utils';
import { getHijriDateFromAnchor } from './utils';
import { LocalStorageCache } from './utils/cache';

export const getCountries = memoize(MyMasjidApi.getCountries);
Expand All @@ -12,19 +12,16 @@ export const getTimings = memoize(MyMasjidApi.getTimings);

export async function getHijriDate(countryId: number): Promise<HijriDate> {
const cacheKey = `api::hijri-date::${countryId}`;
const cachedDate = LocalStorageCache.get<HijriDateAnchor>(cacheKey);
let cachedDate = LocalStorageCache.get<HijriDateAnchor>(cacheKey);
if (cachedDate?.gregorianDate == null) cachedDate = null; // TODO: remove in later version - this is here for backward compatibility

const currentDayOfYear = getDayOfYear(new Date());
if (cachedDate != null) {
const { hijriDate, gregorianDayOfYear } = cachedDate;
const daysDiff = currentDayOfYear - gregorianDayOfYear;
hijriDate.day += daysDiff;
const currentDate = new Date().toISOString();

if (daysDiff >= 0 && hijriDate.day <= 29) return hijriDate;
}
let hijriDate = getHijriDateFromAnchor(cachedDate, currentDate);
if (hijriDate != null) return hijriDate;
hijriDate = await OwnApi.getHijriDate(countryId)!;

const hijriDate = await OwnApi.getHijriDate(countryId)!;
const dateAnchor: HijriDateAnchor = { hijriDate, gregorianDayOfYear: currentDayOfYear };
const dateAnchor: HijriDateAnchor = { hijriDate, gregorianDate: currentDate };
LocalStorageCache.set(cacheKey, dateAnchor);

return hijriDate;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export type HijriDate = {

export type HijriDateAnchor = {
hijriDate: HijriDate;
gregorianDayOfYear: number;
gregorianDate: string; // ISO-date
};
1 change: 1 addition & 0 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './api';
export * from './misc';
export * from './timing';
1 change: 1 addition & 0 deletions src/lib/types/misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Nullable<T> = T | null | undefined;
4 changes: 3 additions & 1 deletion src/lib/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Nullable } from '$lib/types';

export type CachedMeta = { expiresAt?: number };

export type Cache = {
has: (key: string) => boolean;
set: (key: string, value: unknown) => void;
get: <T>(key: string) => T | undefined;
get: <T>(key: string) => Nullable<T>;
delete: (key: string) => void;
};

Expand Down
136 changes: 136 additions & 0 deletions src/lib/utils/dates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { HijriDateAnchor } from '$lib/types';
import { describe, expect, test } from 'bun:test';
import { getHijriDateFromAnchor, getHijriMonthFromNumber as getMonth } from './dates';

describe('Dates utils tests', () => {
describe('getHijriDateFromAnchor', () => {
function getAnchor({ month = 0, day = 15, gregorianDate = '2024-01-01' } = {}): HijriDateAnchor {
return { hijriDate: { day, month: getMonth(month), year: 1446 }, gregorianDate };
}

test('Null anchor', () => {
expect(getHijriDateFromAnchor(null, '2024-01-01')).toBeNil();
});

test('End of gregorian year', () => {
const anchor = getAnchor({ gregorianDate: '2024-12-31' });
const hijriDate = getHijriDateFromAnchor(anchor, '2025-01-05');

expect(hijriDate).not.toBeNil();
expect(hijriDate?.day).toEqual(anchor.hijriDate.day + 5);
});

describe('Invalid dates', () => {
test('Current day is in the past', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, '1990-01-01');

expect(hijriDate).toBeNil();
});

const invalidDateValues = ['not-a-date', 'not a date', 123, null, undefined];

test('Invalid saved gregorian date', () => {
const targetDate = '2024-01-10';

invalidDateValues.forEach((value) => {
const anchor = getAnchor();

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
anchor.gregorianDate = value;
expect(getHijriDateFromAnchor(anchor, targetDate)).toBeNil();
});
});

test('Invalid current gregorian date', () => {
const anchor = getAnchor();

invalidDateValues.forEach((value) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(getHijriDateFromAnchor(anchor, value)).toBeNil();
});
});
});

describe('Middle of hijri month', () => {
test('To same day', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, anchor.gregorianDate);

expect(hijriDate).toEqual(anchor.hijriDate);
});

test('To middle of same month', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, '2024-01-11');

expect(hijriDate).toEqual({ ...anchor.hijriDate, day: anchor.hijriDate.day + 10 });
});

test('To end of same month', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, '2024-01-16');

expect(hijriDate).toBeNil();
});

test('To next month', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, '2024-02-01');

expect(hijriDate).toBeNil();
});

test('months jump', () => {
const anchor = getAnchor();
const hijriDate = getHijriDateFromAnchor(anchor, '2024-06-01');

expect(hijriDate).toBeNil();
});
});

describe('End of hijri month', () => {
test('To same day', () => {
const anchor = getAnchor({ day: 30 });
const hijriDate = getHijriDateFromAnchor(anchor, anchor.gregorianDate);

expect(hijriDate).toEqual(anchor.hijriDate);
});

test('To next month', () => {
const anchor = getAnchor({ day: 30 });
const hijriDate = getHijriDateFromAnchor(anchor, '2024-01-15');

expect(hijriDate).toEqual({ ...anchor.hijriDate, month: getMonth(1), day: 14 });
});

test('To end of next month', () => {
const anchor = getAnchor({ day: 30 });

// Up until 29th is known
expect(getHijriDateFromAnchor(anchor, '2024-01-30')).toEqual({
...anchor.hijriDate,
month: getMonth(1),
day: 29,
});

// 30 is unknown
expect(getHijriDateFromAnchor(anchor, '2024-01-31')).toBeNil();
});

test('To months jump', () => {
const anchor = getAnchor({ day: 30 });
expect(getHijriDateFromAnchor(anchor, '2024-06-01')).toBeNil();
});

test('On last day of year', () => {
const anchor = getAnchor({ day: 30, month: 11 });
const hijriDate = getHijriDateFromAnchor(anchor, '2024-01-15');

expect(hijriDate).toEqual({ day: 14, month: getMonth(0), year: anchor.hijriDate.year + 1 });
});
});
});
});
59 changes: 55 additions & 4 deletions src/lib/utils/dates.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,62 @@
import { HIJRI_MONTHS } from '$lib/constants';
import type { HijriDate, HijriDateAnchor, Nullable } from '$lib/types';
import { DateTime, Interval } from 'luxon';
import { assert } from './polyfills';

export const DAY_MS = 24 * 60 * 60 * 1000;

export function getDaysDiff(d1: Date, d2: Date): number {
const diffTime = Math.abs(d2.valueOf() - d1.valueOf());
return Math.ceil(diffTime / (24 * 60 * 60 * 1000));
}

export function getDayOfYear(date: Date) {
const diff = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0);
return diff / DAY_MS;
export function getHijriMonthNumber(month: string): number {
assert(HIJRI_MONTHS.includes(month), `Invalid Hijri month "${month}"`);
return HIJRI_MONTHS.indexOf(month);
}

export const DAY_MS = 24 * 60 * 60 * 1000;
export function getHijriMonthFromNumber(order: number): string {
assert(Number.isInteger(order) && order < 12 && order >= 0, `Invalid month number "${order}"`);
return HIJRI_MONTHS[order];
}

export function getHijriDateFromAnchor(
anchor: Nullable<HijriDateAnchor>,
currentGregorianIsoDate: string,
): Nullable<HijriDate> {
if (anchor == null) return null;
const { hijriDate, gregorianDate: savedGregorianIsoDate } = JSON.parse(JSON.stringify(anchor)) as HijriDateAnchor;

// Assert valid hijri date
assert(Number.isInteger(hijriDate.year), `Invalid hijri year ${hijriDate.year}`);
assert(HIJRI_MONTHS.includes(hijriDate.month), `Invalid hijri month ${hijriDate.month}`);
assert(
Number.isInteger(hijriDate.day) && hijriDate.day <= 30 && hijriDate.day > 0,
`Invalid hijri day ${hijriDate.day}`,
);

const savedGregorianDate = DateTime.fromISO(savedGregorianIsoDate).startOf('day');
const currentGregorianDate = DateTime.fromISO(currentGregorianIsoDate).startOf('day');
if (!savedGregorianDate.isValid || !currentGregorianDate.isValid) return null;

const interval = Interval.fromDateTimes(savedGregorianDate, currentGregorianDate);
if (!interval.isValid) return null;

const daysDiff = interval.length('days');

if (hijriDate.day == 30 && daysDiff > 0) {
hijriDate.day = daysDiff;

let finalMonthOrder = getHijriMonthNumber(hijriDate.month) + 1;
if (finalMonthOrder == 12) {
finalMonthOrder = 0;
hijriDate.year++;
}
hijriDate.month = getHijriMonthFromNumber(finalMonthOrder);
} else {
hijriDate.day += daysDiff;
}

if (daysDiff == 0 || hijriDate.day < 30) return hijriDate;
return null;
}
1 change: 1 addition & 0 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './dates';
export * from './objects';
export * from './polyfills';
export * from './timings';
4 changes: 4 additions & 0 deletions src/lib/utils/polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { browser } from '$app/environment';
import serverAssert from 'node:assert/strict';

export const assert = browser ? console.assert : serverAssert;
17 changes: 6 additions & 11 deletions src/routes/api/hijri-date/[country]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UK_ID, UK_TIME_ZONE } from '$lib/constants.js';
import { getCacheStore } from '$lib/server';
import type { HijriDate, HijriDateAnchor } from '$lib/types/api.js';
import { getHijriDateFromAnchor } from '$lib/utils/dates.js';
import { json } from '@sveltejs/kit';
import * as cheerio from 'cheerio';
import { DateTime } from 'luxon';
Expand All @@ -13,22 +14,16 @@ export async function GET({ params }) {
}

try {
const dayOfYear = DateTime.now().setZone(UK_TIME_ZONE).ordinal;

const cacheStore = await getCacheStore();
const cacheKey = `api::hijri-date::${country}`;
const cachedDate = cacheStore.get<HijriDateAnchor>(cacheKey);

if (cachedDate != null) {
const { hijriDate, gregorianDayOfYear } = cachedDate;
const daysDiff = dayOfYear - gregorianDayOfYear;
hijriDate.day += daysDiff;

if (daysDiff >= 0 && hijriDate.day <= 29) return json(hijriDate);
}
const currentDate = DateTime.now().setZone(UK_TIME_ZONE).toISO()!;
let hijriDate = getHijriDateFromAnchor(cachedDate, currentDate);
if (hijriDate != null) return json(hijriDate);

const hijriDate = await scrapMoonSightingUkHijriDate();
const anchor: HijriDateAnchor = { hijriDate, gregorianDayOfYear: dayOfYear };
hijriDate = await scrapMoonSightingUkHijriDate();
const anchor: HijriDateAnchor = { hijriDate, gregorianDate: currentDate };
cacheStore.set(cacheKey, anchor);

return json(hijriDate);
Expand Down
Loading

0 comments on commit 3e7a247

Please sign in to comment.