@@ -64,18 +64,44 @@ def get_date_range():
6464
6565
6666def get_user_timezone (file_content ):
67- yaml_match = re .search (r'---\s*\ntimezone:\s*(\S +)\s*\n---' , file_content )
67+ yaml_match = re .search (r'---\s*\ntimezone:\s*([^\n] +)\s*\n---' , file_content )
6868 if yaml_match :
69- timezone_str = yaml_match .group (1 )
69+ timezone_str = yaml_match .group (1 ).strip ()
70+ # 1) Try IANA timezone names directly (e.g., "Asia/Kolkata")
7071 try :
7172 return pytz .timezone (timezone_str )
7273 except pytz .exceptions .UnknownTimeZoneError :
73- try :
74- offset = int (timezone_str [3 :]) # Extract the offset value
75- return pytz .FixedOffset (offset * 60 ) # Offset in minutes
76- except ValueError :
77- logging .warning (f"Invalid timezone format: { timezone_str } . Using default { DEFAULT_TIMEZONE } ." )
78- return pytz .timezone (DEFAULT_TIMEZONE )
74+ pass
75+
76+ # 2) Support UTC/GMT fixed offsets like UTC+5:30, UTC-3, UTC+0530, GMT+9, etc.
77+ s = timezone_str .strip ().upper ().replace ("UTC " , "UTC" ).replace ("GMT " , "GMT" )
78+
79+ # Special case: plain UTC/GMT
80+ if s in ("UTC" , "GMT" , "Z" ):
81+ return pytz .UTC
82+
83+ # Match formats: UTC+H, UTC+HH, UTC+H:MM, UTC+HH:MM, UTC+HHMM (and GMT variants)
84+ m = re .match (r'^(?:UTC|GMT)\s*([+-])\s*(\d{1,2})(?::?(\d{2}))?$' , s )
85+ if m :
86+ sign = 1 if m .group (1 ) == '+' else - 1
87+ hours = int (m .group (2 ))
88+ minutes = int (m .group (3 )) if m .group (3 ) else 0
89+ total_minutes = sign * (hours * 60 + minutes )
90+ return pytz .FixedOffset (total_minutes )
91+
92+ # Match decimal hours like UTC+5.5 or UTC-3.75
93+ m = re .match (r'^(?:UTC|GMT)\s*([+-])\s*(\d{1,2})\.(\d+)$' , s )
94+ if m :
95+ sign = 1 if m .group (1 ) == '+' else - 1
96+ hours = int (m .group (2 ))
97+ frac = float ('0.' + m .group (3 ))
98+ minutes = int (round (frac * 60 ))
99+ total_minutes = sign * (hours * 60 + minutes )
100+ return pytz .FixedOffset (total_minutes )
101+
102+ logging .warning (f"Invalid timezone format: { timezone_str } . Using default { DEFAULT_TIMEZONE } ." )
103+ return pytz .timezone (DEFAULT_TIMEZONE )
104+
79105 return pytz .timezone (DEFAULT_TIMEZONE )
80106
81107
@@ -155,19 +181,26 @@ def get_user_study_status(nickname):
155181 file_content = file .read ()
156182 user_tz = get_user_timezone (file_content )
157183 logging .info (f"File content length for { nickname } : { len (file_content )} user_tz: { user_tz } " )
158- current_date = datetime .now (user_tz ). replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
184+ now_local = datetime .now (user_tz )
159185
160186 for date in get_date_range ():
161- # 直接比较 UTC 日期,避免时区转换问题
162- utc_date = date .astimezone (user_tz ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
163- user_current_date = current_date
164-
165- if utc_date .date () == user_current_date .date ():
166- user_status [date ] = "✅" if check_md_content (file_content , date , user_tz ) else " "
167- elif utc_date .date () > user_current_date .date ():
187+ # Treat each UTC program day as a 24h window [start_utc, next_start_utc)
188+ start_utc = date .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
189+ next_start_utc = (start_utc + timedelta (days = 1 ))
190+
191+ # Convert window edges into user's local time for boundary checks
192+ start_local = start_utc .astimezone (user_tz )
193+ end_local = next_start_utc .astimezone (user_tz )
194+
195+ if now_local < start_local :
196+ # Future day for this user
168197 user_status [date ] = " "
169- else :
198+ elif now_local >= end_local :
199+ # Past day: must be ✅ or ⭕️
170200 user_status [date ] = "✅" if check_md_content (file_content , date , user_tz ) else "⭕️"
201+ else :
202+ # In-progress (today for this user): show ✅ if already posted, else blank
203+ user_status [date ] = "✅" if check_md_content (file_content , date , user_tz ) else " "
171204 logging .info (f"Successfully processed file for user: { nickname } " )
172205 except FileNotFoundError :
173206 logging .error (f"Error: Could not find file { file_name } " )
@@ -285,21 +318,27 @@ def generate_user_row(user):
285318
286319 user_tz = get_user_timezone (file_content )
287320
288- user_current_day = datetime .now (user_tz ). replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
321+ now_local = datetime .now (user_tz )
289322 date_range = get_date_range ()
290323
291324 for i , date in enumerate (date_range ):
292- user_datetime = date .astimezone (pytz .UTC ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
325+ # UTC window for the program day
326+ start_utc = date .astimezone (pytz .UTC ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
327+ next_start_utc = start_utc + timedelta (days = 1 )
328+ # Localized boundaries
329+ start_local = start_utc .astimezone (user_tz )
330+ end_local = next_start_utc .astimezone (user_tz )
293331
294332 if is_eliminated :
295333 new_row += " |"
296334 continue
297335
298- if user_datetime .date () > user_current_day .date ():
336+ # Future day for this user
337+ if now_local < start_local :
299338 new_row += " |"
300339 continue
301340
302- days_from_start = (user_datetime .date () - START_DATE .date ()).days
341+ days_from_start = (start_utc .date () - START_DATE .date ()).days
303342 week_number = days_from_start // 7
304343
305344 cycle_start_day = week_number * 7
@@ -308,14 +347,15 @@ def generate_user_row(user):
308347 absent_count = 0
309348 for day_idx in range (cycle_start_day , min (cycle_end_day + 1 , i + 1 )):
310349 if day_idx < len (date_range ):
311- check_date = date_range [day_idx ].astimezone (pytz .UTC ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
312- check_date_local = check_date .astimezone (user_tz ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
313- if check_date .date () <= user_current_day .date ():
314- status = user_status .get (check_date , "⭕️" )
350+ check_start_utc = date_range [day_idx ].astimezone (pytz .UTC ).replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
351+ check_end_local = (check_start_utc + timedelta (days = 1 )).astimezone (user_tz )
352+ # Only count days that have fully ended in user's local time
353+ if now_local >= check_end_local :
354+ status = user_status .get (check_start_utc , "⭕️" )
315355 if status == "⭕️" :
316356 absent_count += 1
317357
318- current_status = user_status .get (user_datetime , "⭕️" )
358+ current_status = user_status .get (start_utc , "⭕️" )
319359
320360 if absent_count > 2 :
321361 is_eliminated = True
0 commit comments