import { PRODUCTHUNT_DAY_TIMEZONE } from './setup.ts'
import { requireString } from './types.ts'
export type ProductHuntDayBounds = {
timeZone: string
day: string
postedAfter: string
postedBefore: string
}
function pad(value: number): string {
return String(value).padStart(2, '0')
}
function zonedParts(
date: Date,
timeZone: string,
): { year: number; month: number; day: number; hour: number; minute: number; second: number } {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
}).formatToParts(date)
const get = (type: Intl.DateTimeFormatPartTypes) => {
const part = parts.find((item) => item.type === type)
return Number(part?.value ?? '0')
}
return {
year: get('year'),
month: get('month'),
day: get('day'),
hour: get('hour'),
minute: get('minute'),
second: get('second'),
}
}
function instantForLocalMidnight(ymd: string, timeZone: string): Date {
const guess = new Date(`${ymd}T00:00:00.000Z`)
let low = guess.getTime() - 36 * 60 * 60 * 1000
let high = guess.getTime() + 36 * 60 * 60 * 1000
for (let i = 0; i < 40; i += 1) {
const mid = Math.floor((low + high) / 2)
const parts = zonedParts(new Date(mid), timeZone)
const stamp = `${parts.year}-${pad(parts.month)}-${pad(parts.day)}T${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}`
if (stamp >= `${ymd}T00:00:00`) high = mid
else low = mid + 1
}
return new Date(high)
}
/**
* Product Hunt daily ranking uses the Pacific calendar day.
* Returns an exclusive `[postedAfter, postedBefore)` UTC window.
*/
export function todayBounds(
timeZone = PRODUCTHUNT_DAY_TIMEZONE,
now = new Date(),
): ProductHuntDayBounds {
const zone = requireString(timeZone, 'timeZone')
const parts = zonedParts(now, zone)
const day = `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`
const start = instantForLocalMidnight(day, zone)
const nextParts = zonedParts(new Date(start.getTime() + 36 * 60 * 60 * 1000), zone)
const nextDay = `${nextParts.year}-${pad(nextParts.month)}-${pad(nextParts.day)}`
const end = instantForLocalMidnight(nextDay, zone)
return {
timeZone: zone,
day,
postedAfter: start.toISOString(),
postedBefore: end.toISOString(),
}
}