Files
Jyotisha/frontend/src/lib/birth-profile-timezone.ts
T
2026-07-30 04:09:51 +08:00

108 lines
3.9 KiB
TypeScript

type RecordValue = Record<string, unknown>;
export class BirthProfileTimezoneError extends Error {
constructor() {
super("Unable to resolve historical birth timezone offset");
this.name = "BirthProfileTimezoneError";
}
}
function record(value: unknown): RecordValue | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as RecordValue
: null;
}
function valueFor(profile: RecordValue, snakeKey: string, camelKey: string): unknown {
return profile[snakeKey] ?? profile[camelKey];
}
function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function calendarDate(value: unknown): string | null {
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString().slice(0, 10);
return text(value);
}
export function finiteBirthNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
export function durableBirthCoordinate(value: unknown): number | null {
const coordinate = finiteBirthNumber(value);
if (coordinate === null) return null;
const rounded = Math.round(coordinate * 1_000_000) / 1_000_000;
return Object.is(rounded, -0) ? 0 : rounded;
}
export function birthProfileReferenceTime(value: unknown, preferredTime?: string | null): string {
const preferred = text(preferredTime)?.slice(0, 5);
if (preferred && /^([01]\d|2[0-3]):[0-5]\d$/.test(preferred)) return preferred;
const profile = record(value);
const reported = text(profile ? valueFor(profile, "reported_birth_time", "reportedTime") : null)?.slice(0, 5);
if (reported && /^([01]\d|2[0-3]):[0-5]\d$/.test(reported)) return reported;
const direct = text(profile ? valueFor(profile, "birth_time", "time") : null)?.slice(0, 5);
if (direct && /^([01]\d|2[0-3]):[0-5]\d$/.test(direct)) return direct;
const period = text(profile ? valueFor(profile, "birth_time_period", "birthTimePeriod") : null);
return {
early_morning: "06:00",
morning: "10:00",
afternoon: "15:00",
evening: "20:30",
late_night: "23:30",
}[period ?? ""] ?? "12:00";
}
type ResolveBirthTimezoneOptions = Readonly<{
fetchImpl?: typeof fetch;
apiBase?: string;
preferredTime?: string | null;
}>;
export async function resolveMissingBirthTimezoneOffset(
value: unknown,
options: ResolveBirthTimezoneOptions = {},
): Promise<unknown> {
const profile = record(value);
if (!profile) return value;
const existingOffset = finiteBirthNumber(valueFor(profile, "timezone_offset", "timezoneOffset"));
if (existingOffset !== null) return value;
const latitude = finiteBirthNumber(profile.latitude);
const longitude = finiteBirthNumber(profile.longitude);
const birthDate = calendarDate(valueFor(profile, "birth_date", "date"));
const timezoneId = text(valueFor(profile, "timezone_id", "timezoneId"));
if (latitude === null || longitude === null || !birthDate || !timezoneId) return value;
const fetchImpl = options.fetchImpl ?? fetch;
const apiBase = options.apiBase ?? process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
let response: Response;
try {
response = await fetchImpl(`${apiBase}/api/location/timezone`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
latitude,
longitude,
birthDate,
birthTime: birthProfileReferenceTime(profile, options.preferredTime),
}),
cache: "no-store",
});
} catch {
throw new BirthProfileTimezoneError();
}
if (!response.ok) throw new BirthProfileTimezoneError();
const payload = record(await response.json().catch(() => null));
const timezoneOffset = finiteBirthNumber(payload?.timezoneOffset);
if (payload?.available !== true || timezoneOffset === null) {
throw new BirthProfileTimezoneError();
}
return {
...profile,
timezone_offset: timezoneOffset,
timezoneOffset,
};
}