[tz] strftime %s

Brooks Harris brooks at edlmax.com
Mon Jan 15 19:38:10 UTC 2024


You might want to have a look at __mktime_internal (). I discovered some 
time ago that mktime() did not return correct time_t values in some 
cases. For examples America/New_York, 1945-08-14 19:00:00, where the 
only difference is the Abbr (tm_zone), from EWT to EPT 
Africa/Johannesburg, 1944-03-19 01:00:00, where the Abbr (tm_zone) is 
the same, SAST to SAST __mktime_internal () was missing points near 
these transitions (and others) and so returning incorrect results. This 
was causing much confusion in my testing. The problem was that 
__mktime_internal () was comparing only isdstwhere it also needed to 
compare Abbr, as explained more in comments in the attached modified 
code. Attached is the __mktime_internal () code with my suggested 
modifications extracted from glibc-2.38\time\mktime.c as downloaded from 
https://mirrors.ibiblio.org/gnu/libc/glibc-2.38.tar.xz

See the two code blocks commented // Modified by Brooks Harris

Since making these changes I have not seen mktime() make any errors at 
many thousands of test points where localtime() was populating struct tm.

I hope this might be helpful.

-Brooks


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mm.icann.org/pipermail/tz/attachments/20240115/02e0fdb0/attachment.htm>
-------------- next part --------------
/* Convert *TP to a __time64_t value, inverting
   the monotonic and mostly-unit-linear conversion function CONVERT.
   Use *OFFSET to keep track of a guess at the offset of the result,
   compared to what the result would be for UTC without leap seconds.
   If *OFFSET's guess is correct, only one CONVERT call is needed.
   If successful, set *TP to the canonicalized struct tm;
   otherwise leave *TP alone, return ((time_t) -1) and set errno.
   This function is external because it is used also by timegm.c.  */
__time64_t
__mktime_internal (struct tm *tp,
		   struct tm *(*convert) (const __time64_t *, struct tm *),
		   mktime_offset_t *offset)
{
  struct tm tm;

  /* The maximum number of probes (calls to CONVERT) should be enough
     to handle any combinations of time zone rule changes, solar time,
     leap seconds, and oscillations around a spring-forward gap.
     POSIX.1 prohibits leap seconds, but some hosts have them anyway.  */
  int remaining_probes = 6;

  /* Time requested.  Copy it in case CONVERT modifies *TP; this can
     occur if TP is localtime's returned value and CONVERT is localtime.  */
  int sec = tp->tm_sec;
  int min = tp->tm_min;
  int hour = tp->tm_hour;
  int mday = tp->tm_mday;
  int mon = tp->tm_mon;
  int year_requested = tp->tm_year;
  int isdst = tp->tm_isdst;

  /* 1 if the previous probe was DST.  */
  int dst2 = 0;

  /* Ensure that mon is in range, and set year accordingly.  */
  int mon_remainder = mon % 12;
  int negative_mon_remainder = mon_remainder < 0;
  int mon_years = mon / 12 - negative_mon_remainder;
  long_int lyear_requested = year_requested;
  long_int year = lyear_requested + mon_years;

  /* The other values need not be in range:
     the remaining code handles overflows correctly.  */

  /* Calculate day of year from year, month, and day of month.
     The result need not be in range.  */
  int mon_yday = ((__mon_yday[leapyear (year)]
		   [mon_remainder + 12 * negative_mon_remainder])
		  - 1);
  long_int lmday = mday;
  long_int yday = mon_yday + lmday;

  mktime_offset_t off = *offset;
  int negative_offset_guess;

  int sec_requested = sec;

  if (LEAP_SECONDS_POSSIBLE)
    {
      /* Handle out-of-range seconds specially,
	 since ydhms_diff assumes every minute has 60 seconds.  */
      if (sec < 0)
	sec = 0;
      if (59 < sec)
	sec = 59;
    }

  /* Invert CONVERT by probing.  First assume the same offset as last
     time.  */

  INT_SUBTRACT_WRAPV (0, off, &negative_offset_guess);
  long_int t0 = ydhms_diff (year, yday, hour, min, sec,
			    EPOCH_YEAR - TM_YEAR_BASE, 0, 0, 0,
			    negative_offset_guess);
  long_int t = t0, t1 = t0, t2 = t0;

  /* Repeatedly use the error to improve the guess.  */

  while (true)
    {
      if (! ranged_convert (convert, &t, &tm))
	return -1;
      long_int dt = tm_diff (year, yday, hour, min, sec, &tm);
      if (dt == 0)
	break;

      if (t == t1 && t != t2
	  && (tm.tm_isdst < 0
	      || (isdst < 0
		  ? dst2 <= (tm.tm_isdst != 0)
		  : (isdst != 0) != (tm.tm_isdst != 0))))
	/* We can't possibly find a match, as we are oscillating
	   between two values.  The requested time probably falls
	   within a spring-forward gap of size DT.  Follow the common
	   practice in this case, which is to return a time that is DT
	   away from the requested time, preferring a time whose
	   tm_isdst differs from the requested value.  (If no tm_isdst
	   was requested and only one of the two values has a nonzero
	   tm_isdst, prefer that value.)  In practice, this is more
	   useful than returning -1.  */
	goto offset_found;

      remaining_probes--;
      if (remaining_probes == 0)
	{
	  __set_errno (EOVERFLOW);
	  return -1;
	}

      t1 = t2, t2 = t, t += dt, dst2 = tm.tm_isdst != 0;
    }

  /* We have a match.  Check whether tm.tm_isdst has the requested
     value, if any.  */

	// Modified by Brooks Harris
	//
	//  if (isdst_differ (isdst, tm.tm_isdst)) <<<<<<<
	//
	// Checking only isdst_differ() is insufficient in some cases.
	// Example America/New_York, 1945-08-14 19:00:00, where the only 
	// difference is the Abbr (tm_zone), from EWT to EPT
	// Check that Abbr is not same OR tm_isdst and tm_gmtoff differ
	// However, 
	// Example Africa/Johannesburg, 1944-03-19 01:00:00, where the 
	// Abbr (tm_zone) is the same, SAST to SAST
	// Check that Abbr is the same AND tm_isdst and tm_gmtoff differ
	//
	// see below the second block of modified logic 
	// also commented // Modified by Brooks Harris

  if((strcmp(tp->tm_zone, tm.tm_zone) != 0			// Abbr not same
			|| (tp->tm_isdst == tm.tm_isdst					// tm_isdst differ
			&&	tp->tm_gmtoff != tm.tm_gmtoff))			// tm_gmtoff differ
			|| (strcmp(tp->tm_zone, tm.tm_zone) == 0 // Abbr same
			&& tp->tm_isdst != tm.tm_isdst						// tm_isdst differ
			&& tp->tm_gmtoff != tm.tm_gmtoff))				// tm_gmtoff differ    
			{
      /* tm.tm_isdst has the wrong value.  Look for a neighboring
	 time with the right value, and use its UTC offset.

	 Heuristic: probe the adjacent timestamps in both directions,
	 looking for the desired isdst.  If none is found within a
	 reasonable duration bound, assume a one-hour DST difference.
	 This should work for all real time zone histories in the tz
	 database.  */

      /* +1 if we wanted standard time but got DST, -1 if the reverse.  */
      int dst_difference = (isdst == 0) - (tm.tm_isdst == 0);

      /* Distance between probes when looking for a DST boundary.  In
	 tzdata2003a, the shortest period of DST is 601200 seconds
	 (e.g., America/Recife starting 2000-10-08 01:00), and the
	 shortest period of non-DST surrounded by DST is 694800
	 seconds (Africa/Tunis starting 1943-04-17 01:00).  Use the
	 minimum of these two values, so we don't miss these short
	 periods when probing.  */
      int stride = 601200;

      /* In TZDB 2021e, the longest period of DST (or of non-DST), in
	 which the DST (or adjacent DST) difference is not one hour,
	 is 457243209 seconds: e.g., America/Cambridge_Bay with leap
	 seconds, starting 1965-10-31 00:00 in a switch from
	 double-daylight time (-05) to standard time (-07), and
	 continuing to 1980-04-27 02:00 in a switch from standard time
	 (-07) to daylight time (-06).  */
      int duration_max = 457243209;

      /* Search in both directions, so the maximum distance is half
	 the duration; add the stride to avoid off-by-1 problems.  */
      int delta_bound = duration_max / 2 + stride;

      int delta, direction;

      for (delta = stride; delta < delta_bound; delta += stride)
	for (direction = -1; direction <= 1; direction += 2)
	  {
	    long_int ot;
	    if (! INT_ADD_WRAPV (t, delta * direction, &ot))
	      {
		struct tm otm;
		if (! ranged_convert (convert, &ot, &otm))
		  return -1;
		if (! isdst_differ (isdst, otm.tm_isdst))
		
		// Modified by Brooks Harris
		// if (! isdst_differ (isdst, otm.tm_isdst))
			if(strcmp(tp->tm_zone, otm.tm_zone) == 0  // check abbr match instead of isdst
				&& (tp->tm_isdst == otm.tm_isdst
				&&	tp->tm_gmtoff == otm.tm_gmtoff))
		  {
		    /* We found the desired tm_isdst.
		       Extrapolate back to the desired time.  */
		    long_int gt = ot + tm_diff (year, yday, hour, min, sec,
						&otm);
		    if (mktime_min <= gt && gt <= mktime_max)
		      {
			if (convert_time (convert, gt, &tm))
			  {
			    t = gt;
			    goto offset_found;
			  }
			if (errno != EOVERFLOW)
			  return -1;
		      }
		  }
	      }
	  }

      /* No unusual DST offset was found nearby.  Assume one-hour DST.  */
      t += 60 * 60 * dst_difference;
      if (mktime_min <= t && t <= mktime_max && convert_time (convert, t, &tm))
	goto offset_found;

      __set_errno (EOVERFLOW);
      return -1;
    }

 offset_found:
  /* Set *OFFSET to the low-order bits of T - T0 - NEGATIVE_OFFSET_GUESS.
     This is just a heuristic to speed up the next mktime call, and
     correctness is unaffected if integer overflow occurs here.  */
  INT_SUBTRACT_WRAPV (t, t0, offset);
  INT_SUBTRACT_WRAPV (*offset, negative_offset_guess, offset);

  if (LEAP_SECONDS_POSSIBLE && sec_requested != tm.tm_sec)
    {
      /* Adjust time to reflect the tm_sec requested, not the normalized value.
	 Also, repair any damage from a false match due to a leap second.  */
      long_int sec_adjustment = sec == 0 && tm.tm_sec == 60;
      sec_adjustment -= sec;
      sec_adjustment += sec_requested;
      if (INT_ADD_WRAPV (t, sec_adjustment, &t)
	  || ! (mktime_min <= t && t <= mktime_max))
	{
	  __set_errno (EOVERFLOW);
	  return -1;
	}
      if (! convert_time (convert, t, &tm))
	return -1;
    }

  *tp = tm;
  return t;
}


More information about the tz mailing list