@@ -7,14 +7,15 @@ import chalk from 'chalk';
77import { error , info , warn } from './util/logging' ;
88import schedule , { SCHOOL_START , SCHOOL_END_EXCLUSIVE , PeriodObj } from '@watt/shared/data/schedule' ;
99import { numToWeekday } from '@watt/shared/util/schedule' ;
10+ import { DateTime } from 'luxon' ;
1011
1112
1213// Constants
1314const EARLIEST_AM_HOUR = 6 ;
1415
1516const timeGetterRegex = / \( ? ( 1 ? \d ) (?: : ( \d { 2 } ) ) ? * (?: a m ) ? * [ - – ] * ( 1 ? \d ) (?: : ( \d { 2 } ) ) ? * ( n o o n | p m ) ? \) ? / ;
1617const gradeGetterRegex = / (?< ! P e r i o d | # ) \d + ( \/ \d + ) * (? ! (?: s t | n d | r d | t h ) \s + P e r i o d ) / i;
17- const altScheduleRegex = / s t a f f p d | a l t . s c h e d | s c h e d u l e | e x t e n d e d / i; // /schedule|extended|lunch/i
18+ const altScheduleRegex = / s t a f f p d | a l t . s c h e d | s c h e d u l e | e x t e n d e d | f i r s t d a y | f i n a l s / i; // /schedule|extended|lunch/i
1819const noSchoolRegex = / h o l i d a y | n o \s ( s t u d e n t s | s c h o o l ) | b r e a k | d e v e l o p m e n t / i;
1920const primeReplacesSelfRegex = / P R I M E ( r e p l a c e s | i n s t e a d o f ) S E L F | N o S E L F , e x t r a P R I M E / i;
2021// const selfStudyHallRegex = /9\/10 (SELF|Study Hall), 11\/12 (SELF|Study Hall)/i;
@@ -84,6 +85,8 @@ function parseAlternate(summary: string | undefined, description: string | undef
8485
8586 // Parse away HTML tags, entities, and oddities
8687 description = description
88+ . replace ( / ( \) : .+ ?\d ( [ A - Z a - z ] ) ) / g, '$1\n$2' )
89+ . replace ( / ( \) | ] ) ( [ ^ \s : ] ) / g, '$1\n$2' )
8790 . replace ( / \n \( / g, '(' ) // https://github.com/GunnWATT/watt/pull/73#discussion_r756519526
8891 . replace ( / < \/ ? ( p | d i v | b r ) .* ?> | \) , ? * (? = [ A - Z \d ] ) / g, '\n' )
8992 . replace ( / < .* ?> / g, '' ) // Remove all html tags
@@ -113,19 +116,29 @@ function parseAlternate(summary: string | undefined, description: string | undef
113116 const endTime = eH * 60 + eM ;
114117
115118 for ( const raw of names ) {
116- const grades = raw . match ( gradeGetterRegex ) ?. [ 0 ] . split ( '/' ) . map ( grade => Number ( grade ) ) ;
117- const name = raw . replace ( gradeGetterRegex , '' ) . trim ( ) ;
119+ let grades = raw
120+ . match ( gradeGetterRegex ) ?. [ 0 ]
121+ . split ( '/' )
122+ . map ( grade => Number ( grade ) ) ;
123+
124+ if ( grades ?. some ( grade => grade < 9 || grade > 12 ) ) {
125+ warn ( `[${ chalk . underline ( date ) } ] Invalid grades: ${ grades . join ( ', ' ) } in "${ chalk . cyan ( raw ) } "` ) ;
126+ grades = grades . filter ( grade => grade >= 9 && grade <= 12 ) ;
127+ grades = grades . length ? grades : undefined ;
128+ }
129+
130+ const name = raw . trim ( ) ;
118131 if ( ! name ) continue ;
119132
120- // Support both "Period 5" (standard) and "5th Period" (11/2/2022 schedule)
121- const isNumberPeriod = name . match ( / P e r i o d ( \d ) | ( \d ) (?: s t | n d | r d | t h ) P e r i o d / i) ;
122- const isStaffPrep = name . match ( / C o l l a b o r a t i o n | P r e p | M e e t i n g s ? | T r a i n i n g | M t g s | P L C / i) ;
133+ // Support both "Period 5" (standard) and "5th Period" (11/2/2022 schedule) and "(No) Zero Period" (8/21/2025 schedule)
134+ const isStaffPrep = name . match ( / C o l l a b o r a t i o n | P r e p | M e e t i n g s ? | T r a i n i n g | M t g s | P L C | S t a f f P D / i) ;
135+ const isNumberPeriod = name . match ( / (?< ! \b N o \s ) Z e r o P e r i o d | P e r i o d ( \d ) | ( \d ) (?: s t | n d | r d | t h ) (?: P e r i o d ) ? / i) ;
123136
124137 let fname = name ;
125138 let newEndTime = endTime ;
126139
127140 if ( isNumberPeriod ) {
128- fname = isNumberPeriod [ 1 ] ?? isNumberPeriod [ 2 ] ;
141+ fname = isNumberPeriod [ 1 ] ?? isNumberPeriod [ 2 ] ?? '0' ;
129142 } else if ( name . match ( / O f f i c e H o u r s | T u t o r i a l / i) ) {
130143 fname = "O" ;
131144 warn ( `[${ chalk . underline ( date ) } ] Parsed deprecated period Office Hours` ) ;
@@ -207,34 +220,37 @@ function parseAlternate(summary: string | undefined, description: string | undef
207220 const prev : { [ key : string ] : PeriodObj [ ] | null } = JSON . parse ( readFileSync ( './output/alternates.json' ) . toString ( ) ) ;
208221
209222 // Fetch iCal source, parse
210- const raw = await ( await fetch ( 'https://gunn.pausd.org/cf_calendar/feed.cfm?type=ical&feedID=3FC31A8EAE8A4918B2E582A69B519816 ' ) ) . text ( ) ;
223+ const raw = await ( await fetch ( 'https://gunn.pausd.org/fs/calendar-manager/events.ics?calendar_ids[]=51 ' ) ) . text ( ) ;
211224 const calendar = Object . values ( ical . parseICS ( raw ) ) ;
212225
213226 const fAlternates : { [ key : string ] : PeriodObj [ ] } = { } ;
214- let firstAlternate = new Date ( ) ;
227+ let firstAlternate = DateTime . now ( ) ;
215228
216229 // Populate `fAlternates` with unparsed day objects from iCal fetch
217230 for ( const event of calendar ) {
218- const startDateObj = event . start ! ;
219- const endDateObj = event . end ;
231+ let startDateObj = DateTime . fromJSDate ( event . start ! ) . setZone ( 'America/Los_Angeles' ) ;
232+ const endDateObj = DateTime . fromJSDate ( event . end ! ) . setZone ( 'America/Los_Angeles' ) ;
233+
234+ // Invalid events - courtesy of the July 2025 Gunn website update...
235+ if ( ! startDateObj || ! endDateObj ) continue ;
220236
221237 // If the alternate schedule does not lie within the school year, skip it
222- if ( startDateObj < SCHOOL_START . toJSDate ( ) || startDateObj >= SCHOOL_END_EXCLUSIVE . toJSDate ( ) )
238+ if ( startDateObj < SCHOOL_START || startDateObj >= SCHOOL_END_EXCLUSIVE )
223239 continue ;
224240
225- const schedule = parseAlternate ( event . summary , event . description , startDateObj . toISOString ( ) . slice ( 0 , 10 ) )
241+ const schedule = parseAlternate ( event . summary , event . description , startDateObj . toFormat ( 'yyyy-MM-dd' ) )
226242 if ( ! schedule ) continue ;
227243
228244 // If an end date exists, add all dates between the start and end dates with the alternate schedule
229245 if ( endDateObj ) {
230- while ( startDateObj . toISOString ( ) . slice ( 5 , 10 ) !== endDateObj . toISOString ( ) . slice ( 5 , 10 ) ) {
231- fAlternates [ startDateObj . toISOString ( ) . slice ( 5 , 10 ) ] = schedule ;
232- startDateObj . setUTCDate ( startDateObj . getUTCDate ( ) + 1 ) ;
246+ while ( startDateObj . toFormat ( 'yyyy-MM-dd' ) !== endDateObj . toFormat ( 'yyyy-MM-dd' ) ) {
247+ fAlternates [ startDateObj . toFormat ( 'MM-dd' ) ] = schedule ;
248+ startDateObj = startDateObj . plus ( { days : 1 } ) ;
233249 }
234250 }
235251
236252 if ( startDateObj < firstAlternate ) firstAlternate = startDateObj ;
237- fAlternates [ startDateObj . toISOString ( ) . slice ( 5 , 10 ) ] = schedule ;
253+ fAlternates [ startDateObj . minus ( { days : 1 } ) . toFormat ( 'MM-dd' ) ] = schedule ;
238254 }
239255
240256 const alternates : { [ key : string ] : PeriodObj [ ] | null } = { } ;
@@ -246,8 +262,8 @@ function parseAlternate(summary: string | undefined, description: string | undef
246262 let [ month , day ] = date . split ( '-' ) . map ( x => Number ( x ) ) ;
247263 if ( month > 6 ) month -= 12 ; // Hackily account for our truncated ISO key format making 12-03 appear greater than 04-29
248264
249- const firstMonth = firstAlternate . getMonth ( ) + 1 ;
250- if ( month < firstMonth || ( month === firstMonth && day < firstAlternate . getDate ( ) ) )
265+ const firstMonth = firstAlternate . month + 1 ;
266+ if ( month < firstMonth || ( month === firstMonth && day < firstAlternate . day ) )
251267 alternates [ date ] = schedule ;
252268 }
253269
0 commit comments