daily-word.ts 1.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. /**
  2. * Compute the daily word ID deterministically from the current date.
  3. * Timezone: America/New_York (EST/EDT).
  4. *
  5. * Formula: daysSinceEpoch mod targetCount (0-indexed)
  6. * Epoch: 2026-01-01 (arbitrary fixed reference)
  7. *
  8. * Uses Intl.DateTimeFormat to extract the date in the target timezone,
  9. * avoiding toLocaleString+re-parse pitfalls that break when the server
  10. * timezone differs from America/New_York.
  11. */
  12. const EPOCH_DATE = new Date('2026-01-01T00:00:00-05:00'); // EST
  13. function getDateInTz(date: Date, tz: string): string {
  14. // en-CA locale gives YYYY-MM-DD format
  15. return new Intl.DateTimeFormat('en-CA', {
  16. timeZone: tz,
  17. year: 'numeric',
  18. month: '2-digit',
  19. day: '2-digit',
  20. }).format(date);
  21. }
  22. function daysBetween(a: string, b: string): number {
  23. // Parse YYYY-MM-DD as UTC midnight for consistent arithmetic
  24. const da = new Date(a + 'T00:00:00Z').getTime();
  25. const db = new Date(b + 'T00:00:00Z').getTime();
  26. return Math.floor((da - db) / 86_400_000);
  27. }
  28. function getDaysSinceEpoch(now: Date): number {
  29. const nyDate = getDateInTz(now, 'America/New_York');
  30. const epochDate = getDateInTz(EPOCH_DATE, 'America/New_York');
  31. return daysBetween(nyDate, epochDate);
  32. }
  33. export function getDailyWordId(targetCount: number, now: Date = new Date()): number {
  34. if (targetCount <= 0) {
  35. throw new Error('targetCount must be positive');
  36. }
  37. const days = getDaysSinceEpoch(now);
  38. return days % targetCount;
  39. }