MBSParseDate speed

Thanks! I appreciate that code and I learned a few clever techniques from it!

At first I thought I could couple this with “ToISO8601Date” and make my own date object but then I realized it still needs all of the logic to convert between totalsecs and all of the year, month, day etc fields while accommodating various size months, leap years and other special cases… ugh…

FWIW Previously I benchmarked Xojo.date vs RB.date and the later is faster for my purposes…

RB date just has some boundary case issues :slight_smile:

For completeness

Public Function ToISO8601Date(d as xojo.Core.Date) as text
  // we'll form these as ISO 8601 format
  // <date>T<time><TZ>
  // where
  //  <dates> are stated as YYYY-MM-DD
  //  <times> are stated as HH:MM:SS.NNNN
  //          HH is hours (NOT 24 hour format)
  //          MM is minutes
  //          SS is seconds
  //          NNNN is nanoseconds
  //  <TZ> is formed as
  //         Z if the TZ is GMT (offset = 0)
  //         -hhmm if the tz is GMT- (offset < 0)
  //         +hhmm if the tz is GMT+ (offset > 0) 
  //             hh is hours before GMT
  //             mm is minutes before GMT
  //       so we can handle offsets that are not whole or half hours
  
  dim pieces() as Text
  
  pieces.append d.Year.ToText(Xojo.Core.Locale.Raw, "0000")
  pieces.append "-"
  pieces.append d.Month.ToText(Xojo.Core.Locale.Raw, "00")
  pieces.append "-"
  pieces.append d.Day.ToText(Xojo.Core.Locale.Raw, "00")
  pieces.append "T"
  pieces.append d.Hour.ToText(Xojo.Core.Locale.Raw, "00")
  pieces.append ":"
  pieces.append d.Minute.ToText(Xojo.Core.Locale.Raw, "00")
  pieces.append ":"
  pieces.append d.Second.ToText(Xojo.Core.Locale.Raw, "00")
  pieces.append "."
  pieces.append d.Nanosecond.ToText(Xojo.Core.Locale.Raw, "0000")
  
  dim hoursBeforeGMT as integer
  dim secondsBeforeGMT as integer
  
  hoursBeforeGMT = d.TimeZone.SecondsFromGMT \\ (3600)
  secondsBeforeGMT = (d.TimeZone.SecondsFromGMT - (hoursBeforeGMT * (3600))) \\ 60
  
  hoursBeforeGMT = abs(hoursBeforeGMT)
  secondsBeforeGMT =  abs(secondsBeforeGMT)
  
  if d.TimeZone.SecondsFromGMT = 0 then
    pieces.append "Z" // zulu time or gmt = 0
  else
    dim prefix as text = "+"
    if d.TimeZone.SecondsFromGMT < 0 then
      prefix = "-"
    end if
    pieces.append prefix
    pieces.append hoursBeforeGMT.ToText(Xojo.Core.Locale.Raw, "00")
    pieces.append secondsBeforeGMT.ToText(Xojo.Core.Locale.Raw, "00")
    
  end if
  
  return Text.Join(pieces,"")
  
  
  
End Function

Cool! Thanks for that too!

Currently I handle the fractional seconds separately from the date/time itself, and your functions would allow me to do this much more cleanly.

Yep, date math is nothing but a big ugly bag of boundary conditions. :slight_smile: Almost impossible to get them all right…

Classic date also has caveats about the order you do things (like setting year then month then day) otherwise you can get weird rollovers if you do it in other orders
And there are a couple other gotcha’s

a really quick & dirty test

created a new desktop project
dropped a listbox on the window
took your code and stuffed it in a method

Public Function convertdate(s as string) as date
  //Syslog date format: 2015-02-07T07:47:04.872175+00:00 
  dim DateStr as string = ReplaceB (s, "T", " ")
  dim d as new date
  d.GMTOffset = 0.0
  d.SQLDateTime = DateStr
  
  return d
End Function

the in the listbox open event I put

const kLimit = 10
const iLimit = 10000

for k as integer = 1 to kLimit
		dim oldstart, newstart, oldend, newend as double
		
		oldstart = Microseconds
		for i as integer = 1 to iLimit
				dim d as date = ConvertDate("2015-02-07T07:47:04.872175")
		next
		oldend = Microseconds
		
		newstart = Microseconds
		for i as integer = 1 to iLimit
				dim d as xojo.core.date = FromISO8601Date("2015-02-07T07:47:04.872175Z")
		next
		newend = Microseconds
		
		dim olddiff, newdiff as double
		
		olddiff = oldend - oldstart
		newdiff = newend - newstart
		
                me.AddRow "old = " + str(olddiff,"00000000.0000") + "   new = " + str(newdiff,"00000000.0000")
next

and those runs suggest my code is NOT quicker - I really didn’t expect it to be

old = 00177119.3440 new = 01523673.4921
old = 00179606.2590 new = 01531457.8109
old = 00189875.2260 new = 01611258.6799
old = 00195586.4709 new = 01679490.3530
old = 00196628.0620 new = 01549869.6760
old = 00188670.9550 new = 01527660.8311
old = 00179602.3750 new = 01553725.2560
old = 00177261.8450 new = 01553086.5740
old = 00178154.5540 new = 01556845.8679
old = 00188049.6189 new = 01576815.6350

[code] Sub ISODateTime(Extends aDate As Date, Assigns value As String)
if (Trim(value) <> “”) then
DIM aRegEx As NEW RegEx
aRegEx.SearchPattern = “^([\±]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T\s?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\±])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$”

	    DIM match As RegExMatch = aRegEx.Search(Trim(value))
	    
	    if (match <> Nil) then
	      // set the GMT offset to the offset in the ISO date
	      if (match.SubExpressionCount > 21) then
	        aDate.GMTOffset = Val(match.SubExpressionString(21))
	      end if
	      
	      for i as Integer = 0 to (match.SubExpressionCount)
	        select case i
	        case 1  // year
	          aDate.Year = Val(match.SubExpressionString(i))
	        case 5  // month
	          aDate.Month = Val(match.SubExpressionString(i))
	        case 7  // day
	          aDate.Day = Val(match.SubExpressionString(i))
	          
	        case 15  // hour
	          aDate.Hour = Val(match.SubExpressionString(i))
	        case 16  // minutes
	          aDate.Minute = Val(match.SubExpressionString(i))
	        case 19  // seconds
	          aDate.Second = Val(match.SubExpressionString(i))
	        end select
	      next
	      
	      // adjust the GMT offset for local time zone
	      DIM now As NEW Date
	      DIM localGMT As Double = now.GMTOffset
	      aDate.GMTOffset = localGMT
	    end if
	  end if
	End Sub[/code]

Here is an extends method, for the old Date class, that will create an instance based on an ISO-8601… To use with the new immutable Date class, would require a small bit of rewriting, but shouldn’t be that hard…

quick & dirty test

took this method
plopped it in a module
changed driver code to

		const kLimit = 10
		const iLimit = 10000
		
		for k as integer = 1 to kLimit
				dim oldstart, newstart, oldend, newend as double
				
				oldstart = Microseconds
				for i as integer = 1 to iLimit
						dim d as new date 
						d.ISODateTime = "2015-02-07T07:47:04.872175"
				next
				oldend = Microseconds
				
				// newstart = Microseconds
				// for i as integer = 1 to iLimit
				// dim d as xojo.core.date = FromISO8601Date("2015-02-07T07:47:04.872175Z")
				// next
				// newend = Microseconds
				
				dim olddiff, newdiff as double
				
				olddiff = oldend - oldstart
				// newdiff = newend - newstart
				
				                me.AddRow "old = " + str(olddiff,"00000000.0000") + "   new = " + str(newdiff,"00000000.0000")
		next

about 3x slower than joe’s code

old = 00540666.8180 new = 00000000.0000
old = 00526265.3419 new = 00000000.0000
old = 00534578.0790 new = 00000000.0000
old = 00545776.5730 new = 00000000.0000
old = 00550043.1011 new = 00000000.0000
old = 00526998.7090 new = 00000000.0000
old = 00521328.8260 new = 00000000.0000
old = 00525330.3170 new = 00000000.0000
old = 00535230.5740 new = 00000000.0000
old = 00537524.3340 new = 00000000.0000

And I’m sure each of these could be sped up
A real quick speed up is to simply not loop over all the search results since the specific matches are well known

Basically turn the for next loop into something like 6 if’s

if match.SubExpressionCount > 1 then aDate.Year = Val(match.SubExpressionString(1))
if match.SubExpressionCount > 5 then aDate.Month = Val(match.SubExpressionString(5))
if match.SubExpressionCount > 7 then aDate.Day = Val(match.SubExpressionString(7))
						
if match.SubExpressionCount > 15 then aDate.Hour = Val(match.SubExpressionString(15))
if match.SubExpressionCount > 16 then aDate.Minute = Val(match.SubExpressionString(16))
if match.SubExpressionCount > 19 then aDate.Second = Val(match.SubExpressionString(19))

This shaves of 70,000 microseconds over the iterations
Making the new regex be static shaves a tad more

Thanks for the pointer about switching the loop to if statements…

What you mean?

if you put the regex in a static variable, you only need to initialize it once.