@@ -294,15 +294,63 @@ def check_time_squareness(
294294 a_t = _round (actual , NDECIMALS )
295295 t_t = _round (theo , NDECIMALS )
296296
297- bad = np .where (a_t != t_t )[0 ]
298- if bad .size :
299- i = int (bad [0 ])
300- ctx .add_failure (
301- f"Mismatch at index { i } : expected { t_t [i ]:.{NDECIMALS }f} , got { a_t [i ]:.{NDECIMALS }f} . "
302- f"(table_id={ table_id } , frequency={ freq_id } , var={ target } , midpoint={ use_midpoint } )"
303- )
297+ # For monthly instantaneous data, allow three valid timestamp conventions,
298+ # but require consistency across the whole file (single convention per file):
299+ # - first day of each month at 00:00
300+ # - 15th day of each month at 00:00
301+ # - exact calendar-aware midpoint of each month interval
302+ allow_mon_point_midmonth = (
303+ instantaneous
304+ and (freq_id in {"mon" , "monPt" })
305+ and (inc_unit == "months" )
306+ and (inc_val == 1 )
307+ )
308+
309+ if allow_mon_point_midmonth :
310+ theo_mid = np .zeros (actual .size , dtype = float )
311+ theo_center = np .zeros (actual .size , dtype = float )
312+ cur = start_boundary
313+ for i in range (actual .size ):
314+ nxt = _add_time_increment (cur , inc_val , inc_unit , cal )
315+ mid = cftime .datetime (cur .year , cur .month , 15 , 0 , 0 , 0 , calendar = cal )
316+ theo_mid [i ] = float (cftime .date2num (mid , units = units , calendar = cal ))
317+ theo_center [i ] = _midpoint_num (cur , nxt , units , cal )
318+ cur = nxt
319+
320+ t_mid = _round (theo_mid , NDECIMALS )
321+ t_center = _round (theo_center , NDECIMALS )
322+
323+ # A file passes only if the full axis matches one convention end-to-end.
324+ full_match_start = np .array_equal (a_t , t_t )
325+ full_match_mid = np .array_equal (a_t , t_mid )
326+ full_match_center = np .array_equal (a_t , t_center )
327+
328+ if not (full_match_start or full_match_mid or full_match_center ):
329+ bad = np .where ((a_t != t_t ) & (a_t != t_mid ) & (a_t != t_center ))[0 ]
330+ if bad .size :
331+ i = int (bad [0 ])
332+ else :
333+ # Mixed-convention axis: every point matches at least one candidate,
334+ # but no single candidate matches the entire file.
335+ first_start = np .where (a_t != t_t )[0 ]
336+ i = int (first_start [0 ]) if first_start .size else 0
337+ ctx .add_failure (
338+ f"Mismatch at index { i } : expected { t_t [i ]:.{NDECIMALS }f} (month-start) "
339+ f"or { t_mid [i ]:.{NDECIMALS }f} (day-15) "
340+ f"or { t_center [i ]:.{NDECIMALS }f} (exact center), got { a_t [i ]:.{NDECIMALS }f} . "
341+ "The full file must consistently follow one of these conventions. "
342+ f"(table_id={ table_id } , frequency={ freq_id } , var={ target } , midpoint={ use_midpoint } )"
343+ )
304344 else :
305- if not ctx .messages :
306- ctx .add_pass ()
345+ bad = np .where (a_t != t_t )[0 ]
346+ if bad .size :
347+ i = int (bad [0 ])
348+ ctx .add_failure (
349+ f"Mismatch at index { i } : expected { t_t [i ]:.{NDECIMALS }f} , got { a_t [i ]:.{NDECIMALS }f} . "
350+ f"(table_id={ table_id } , frequency={ freq_id } , var={ target } , midpoint={ use_midpoint } )"
351+ )
352+
353+ if not ctx .messages :
354+ ctx .add_pass ()
307355
308356 return [ctx .to_result ()]
0 commit comments