@@ -20,6 +20,23 @@ export interface FreebuffModelOption {
2020export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT'
2121export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1'
2222export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
23+ const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York'
24+ const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles'
25+
26+ interface ZonedDateParts {
27+ year : number
28+ month : number
29+ day : number
30+ weekday : string
31+ hour : number
32+ minute : number
33+ minutes : number
34+ }
35+
36+ interface LocalTimeFormatOptions {
37+ locale ?: string
38+ timeZone ?: string
39+ }
2340
2441export const FREEBUFF_MODELS = [
2542 {
@@ -71,29 +88,163 @@ export function getFreebuffModel(id: string): FreebuffModelOption {
7188 )
7289}
7390
74- function getZonedParts (
75- date : Date ,
76- timeZone : string ,
77- ) : { weekday : string ; minutes : number } {
91+ function getZonedParts ( date : Date , timeZone : string ) : ZonedDateParts {
7892 const parts = new Intl . DateTimeFormat ( 'en-US' , {
7993 timeZone,
94+ year : 'numeric' ,
95+ month : '2-digit' ,
96+ day : '2-digit' ,
8097 weekday : 'short' ,
8198 hour : '2-digit' ,
8299 minute : '2-digit' ,
83100 hourCycle : 'h23' ,
84101 } ) . formatToParts ( date )
85- const value = ( type : string ) => parts . find ( ( part ) => part . type === type ) ?. value
102+ const value = ( type : string ) =>
103+ parts . find ( ( part ) => part . type === type ) ?. value
104+ const year = Number ( value ( 'year' ) ?? 0 )
105+ const month = Number ( value ( 'month' ) ?? 1 )
106+ const day = Number ( value ( 'day' ) ?? 1 )
86107 const hour = Number ( value ( 'hour' ) ?? 0 )
87108 const minute = Number ( value ( 'minute' ) ?? 0 )
88109 return {
110+ year,
111+ month,
112+ day,
89113 weekday : value ( 'weekday' ) ?? '' ,
114+ hour,
115+ minute,
90116 minutes : hour * 60 + minute ,
91117 }
92118}
93119
120+ function addDaysToYmd (
121+ year : number ,
122+ month : number ,
123+ day : number ,
124+ days : number ,
125+ ) : Pick < ZonedDateParts , 'year' | 'month' | 'day' > {
126+ const next = new Date ( Date . UTC ( year , month - 1 , day ) )
127+ next . setUTCDate ( next . getUTCDate ( ) + days )
128+ return {
129+ year : next . getUTCFullYear ( ) ,
130+ month : next . getUTCMonth ( ) + 1 ,
131+ day : next . getUTCDate ( ) ,
132+ }
133+ }
134+
135+ function getUtcForZonedTime (
136+ parts : Pick < ZonedDateParts , 'year' | 'month' | 'day' > ,
137+ timeZone : string ,
138+ hour : number ,
139+ minute : number ,
140+ ) : Date {
141+ let guess = new Date (
142+ Date . UTC ( parts . year , parts . month - 1 , parts . day , hour , minute ) ,
143+ )
144+
145+ for ( let i = 0 ; i < 3 ; i ++ ) {
146+ const actual = getZonedParts ( guess , timeZone )
147+ const desiredUtc = Date . UTC (
148+ parts . year ,
149+ parts . month - 1 ,
150+ parts . day ,
151+ hour ,
152+ minute ,
153+ )
154+ const actualUtc = Date . UTC (
155+ actual . year ,
156+ actual . month - 1 ,
157+ actual . day ,
158+ actual . hour ,
159+ actual . minute ,
160+ )
161+ guess = new Date ( guess . getTime ( ) + ( desiredUtc - actualUtc ) )
162+ }
163+
164+ return guess
165+ }
166+
167+ function isWeekend (
168+ parts : Pick < ZonedDateParts , 'year' | 'month' | 'day' > ,
169+ ) : boolean {
170+ const weekday = new Date (
171+ Date . UTC ( parts . year , parts . month - 1 , parts . day ) ,
172+ ) . getUTCDay ( )
173+ return weekday === 0 || weekday === 6
174+ }
175+
176+ function getNextFreebuffDeploymentStart ( now : Date ) : Date {
177+ const easternNow = getZonedParts ( now , FREEBUFF_EASTERN_TIMEZONE )
178+
179+ for ( let offset = 0 ; offset < 8 ; offset ++ ) {
180+ const day = addDaysToYmd (
181+ easternNow . year ,
182+ easternNow . month ,
183+ easternNow . day ,
184+ offset ,
185+ )
186+ if ( isWeekend ( day ) ) continue
187+ const candidate = getUtcForZonedTime ( day , FREEBUFF_EASTERN_TIMEZONE , 9 , 0 )
188+ if ( candidate . getTime ( ) > now . getTime ( ) ) return candidate
189+ }
190+
191+ return getUtcForZonedTime (
192+ addDaysToYmd ( easternNow . year , easternNow . month , easternNow . day , 8 ) ,
193+ FREEBUFF_EASTERN_TIMEZONE ,
194+ 9 ,
195+ 0 ,
196+ )
197+ }
198+
199+ function getCurrentFreebuffDeploymentEnd ( now : Date ) : Date {
200+ const pacificNow = getZonedParts ( now , FREEBUFF_PACIFIC_TIMEZONE )
201+ return getUtcForZonedTime ( pacificNow , FREEBUFF_PACIFIC_TIMEZONE , 17 , 0 )
202+ }
203+
204+ function isSameLocalDay ( left : Date , right : Date , timeZone ?: string ) : boolean {
205+ const formatter = new Intl . DateTimeFormat ( 'en-CA' , {
206+ timeZone,
207+ year : 'numeric' ,
208+ month : '2-digit' ,
209+ day : '2-digit' ,
210+ } )
211+ return formatter . format ( left ) === formatter . format ( right )
212+ }
213+
214+ function formatLocalTime (
215+ date : Date ,
216+ referenceNow : Date ,
217+ options : LocalTimeFormatOptions = { } ,
218+ ) : string {
219+ const shouldShowWeekday = ! isSameLocalDay (
220+ date ,
221+ referenceNow ,
222+ options . timeZone ,
223+ )
224+ return new Intl . DateTimeFormat ( options . locale , {
225+ timeZone : options . timeZone ,
226+ weekday : shouldShowWeekday ? 'short' : undefined ,
227+ hour : 'numeric' ,
228+ minute : '2-digit' ,
229+ } ) . format ( date )
230+ }
231+
232+ export function getFreebuffDeploymentAvailabilityLabel (
233+ now : Date = new Date ( ) ,
234+ options : LocalTimeFormatOptions = { } ,
235+ ) : string {
236+ if ( isFreebuffDeploymentHours ( now ) ) {
237+ const closesAt = getCurrentFreebuffDeploymentEnd ( now )
238+ return `until ${ formatLocalTime ( closesAt , now , options ) } local`
239+ }
240+
241+ const opensAt = getNextFreebuffDeploymentStart ( now )
242+ return `opens ${ formatLocalTime ( opensAt , now , options ) } local`
243+ }
244+
94245export function isFreebuffDeploymentHours ( now : Date = new Date ( ) ) : boolean {
95- const eastern = getZonedParts ( now , 'America/New_York' )
96- const pacific = getZonedParts ( now , 'America/Los_Angeles' )
246+ const eastern = getZonedParts ( now , FREEBUFF_EASTERN_TIMEZONE )
247+ const pacific = getZonedParts ( now , FREEBUFF_PACIFIC_TIMEZONE )
97248 if ( eastern . weekday === 'Sat' || eastern . weekday === 'Sun' ) return false
98249 return eastern . minutes >= 9 * 60 && pacific . minutes < 17 * 60
99250}
0 commit comments