Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/producthunt

src/today.ts

78 lines · 2.3 KB · TypeScript
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(),
	}
}