Ever had to do date math where you need to detect conflict schedules? Recently, I had to do some data analysis that involved checking for potential conflicts in a schedule, counting them, and figuring out how big the conflicts were. This isn't a live schedule, it's historical scheduling data used to model future demand against available resources. The idea is to surface obvious material shortfalls. If you were an event planner, you might use October 2016 demand to project 2017 demand to see if you have enough chairs, caterers, dancing bears, clowns, kegs of beer, etc. available. \Of course, scheduling efficiency and other constraints might be issues, but that's not what this model is working on...it's trying to measure fixed inventory against projected demand to see if more inventory is needed. Right, too much backstory...welcome to me.
Anyway, I've always "hated" date math. As it turns out, I just wasn't looking for existing solutions efficiently and I don't hate date math at all now. It didn't take long to find out that time interval calculations have been studied exhaustively as have algorithms for doing conflict detection, optimal fitting, etc. In my case, I just need a couple of things: * Do these two time ranges intersect? * By how much? My particular problem takes more than that to sort out, and I also do another form of projection that's a tweak on this, but that's the nub of the interval math right there. As a highlight of what I'm talking about, here are some comments adapted from a StackOverflow post on the subject: // Great explanation on StackOverflow here: // https://stackoverflow.com/questions/143552/comparing-date-ranges/143568#143568 // Best. Ascii. Diagram. Ever. This blocks out eleven possible interval comparison outcomes. // I've added discussion of two special cases and show what catches them. -- DPA // |-------------------| compare to this one // |---------| contained within // |----------| contained within, equal start // |-----------| contained within, equal end // |-------------------| contained within, equal start+end // |------------| not fully contained, overlaps start // |---------------| not fully contained, overlaps end // |-------------------------| overlaps start, bigger // |-----------------------| overlaps end, bigger // |------------------------------| // on the other hand, let me post all those that don't overlap: // // |-------------------| compare to this one // |---| ends before // |---| starts after // Notes about two possibly confusing cases // |-------------------| compare to this one // |-----| ends at start is a case of // |------------| not fully contained, overlaps start // |-------------------| compare to this one // |-----| starts at end is a case of // |---------------| not fully contained, overlaps end Below I'm posting a method for calculating the overlap between two time ranges in seconds. A few notes: * All of this code assumes and requires date times on a consistent timezone. If you do a lot of datetime stuff and haven't already moved everything to Zulu with offsets stored somewhere for display purposes, your making your life needlessly hard. But all this code requires is that datetimes are on the same time zone, whatever that is. * Midnight is just another moment like any other, with datetimes, midnight doesn't take any special consideration. * The routine has minimal error checking on the parameters. It chokes on end times earlier than start times and that's it. It doesn't detect 0000 type times and those might screw you up. * There are a bunch of subroutines listed that I'm not posting. Most folks will have their own versions or use a different style that doesn't require some of my subroutines. (I'm big on parameter checking, most people aren't.) * I didn't write the date time utilities and haven't even looked at them. They work, that's all I know. * I've also included a manual test routine that works in conjunction with the main method. Good luck trying to do something like this without a test case that you can run over an over until all of the bugs quit moving. Stay down bug! Just stay down! * Speaking of bugs, if anyone finds one (or more), PLEASE tell me. Ideally, fix it and tell me...but at least tell me/the list. * If anyone from 4D, etc. ever wants to clean this up and make it into a tech note, please post a link when it's published. I'd use it! * If all you need is to detect that two appointments overlap and don't care by how much, see the notes. It's eaasier to figure than you might expect - I put the relevant comprisons at the top of my big case of scenarios below. * You'll find lots of notes and some great ASCII charts throughout. I give credit to where I found everything. * Tip: If this approach isn't good enough for you and you're after optimal schedule detection (least CPU use), the data structure you want to look for is an "interval tree." Just google scheduling conflicts and interval trees and you'll be eyeballs-deep into big O notation before you can blink. Here's a link to a decent summary: http://www.geeksforgeeks.org/given-n-appointments-find-conflicting-appointments/ I'm not doing anything intensive enough to need interval trees here (I'm not schedule fitting, just counting stuff), but if you have a big scheduling system with lots of conflict detection, check out interval trees. (Not that list processing is easy or pleasurable in 4D, but you can do it.) Okay, here is the method and the test method to match. If (False) // =========================================== utl_DTS_GetIntervalOverlapSecs // DESCRIPTION: Returns overlap of date time stamps, if any. // Result is -1 if you pass bad inputs. // All inputs are date time stamps. // Notes: // There's a always a question of how to treat a case where the // end time of one interval is the start time of the other. In this case, // I've gone with treating it as an overlap when the boundaries touch. // This can be revisited if we need something different. // Great explanation on StackOverflow here: // https://stackoverflow.com/questions/143552/comparing-date-ranges/143568#143568 // Best. Ascii. Diagram. Ever. This blocks out eleven possible interval comparison outcomes. // I've added discussion of two special cases and show what catches them. -- DPA // |-------------------| compare to this one // |---------| contained within // |----------| contained within, equal start // |-----------| contained within, equal end // |-------------------| contained within, equal start+end // |------------| not fully contained, overlaps start // |---------------| not fully contained, overlaps end // |-------------------------| overlaps start, bigger // |-----------------------| overlaps end, bigger // |------------------------------| // on the other hand, let me post all those that don't overlap: // // |-------------------| compare to this one // |---| ends before // |---| starts after // Notes about two possibly confusing cases // |-------------------| compare to this one // |-----| ends at start is a case of // |------------| not fully contained, overlaps start // |-------------------| compare to this one // |-----| starts at end is a case of // |---------------| not fully contained, overlaps end // // The point of the art above is to show the possibilities, and to show a shortcut // to figuring out "do these overlap?" Only 'ends before' and 'starts after' aren't // overlaps. In other words, everything else is an overlap - even if by only 0 seconds. // In this method here, what we're trying to calculate are // the number of seconds of overlap, so all need to be covered explicitly. // The Wikipedia entry on Allen's interval algebra (what we're doing): // https://en.wikipedia.org/wiki/Allen%27s_interval_algebra // Dang, there's an entire book on time-data oriented SQL: // https://www2.cs.arizona.edu/people/rts/tdbbook.pdf // Note: // * You can _definitely_ reduce the number of comparisons in this method by collapsing and shortcutting // some of the comparisons. I was doing that, then I switched to this more explicit code. Your call. // * Not everyone counts the possible combinations the same way. // * You'll need to decide if you want times that overlap exactly to be considered matching or not. // For example, it's normal to say "this appointment runs from 9-10 and the next one from 10-11." // In normal language, Those don't overlap because you treat them as 09:00-09:59:59 and 10:00:00-10:59:59. // This routine treats 10-11 and 11-12 as overlapping but by 0 seconds. There are some notes above about // where these catch in the code, if you want to tweak the behavior. // See test routine utl_DTS_Overlap_Test // CREATED BY: David Adams // DATE: 01/07/2017 // LAST MODIFIED: End if // ============================================ C_LONGINT($0;$overlap_seconds) // Could change to return a descriptor or error. C_TEXT($1;$range_start_dts) C_TEXT($2;$range_end_dts) C_TEXT($3;$check_start_dts) C_TEXT($4;$check_end_dts) $overlap_seconds:=-1 // Result is ambiguous if you pass bad inputs. C_TEXT($error_name) $error_name:=MethodCheck_ParameterCount (Count parameters;4;4) If ($error_name="") $range_start_dts:=$1 $range_end_dts:=$2 $check_start_dts:=$3 $check_end_dts:=$4 Case of : ($range_start_dts>$range_end_dts) // Start after end? Bad input. $error_name:="BadDTS_StartsAfterEnding" : ($check_start_dts>$check_end_dts) // Start after end? Bad input. $error_name:="BadDTS_StartsAfterEnding" End case If ($error_name#"") C_TEXT($diagnostic_text) $diagnostic_text:="" $diagnostic_text:=$diagnostic_text+"$range_start_dts = "+$range_start_dts+Char(Carriage return) $diagnostic_text:=$diagnostic_text+"$range_end_dts = "+$range_end_dts+Char(Carriage return) $diagnostic_text:=$diagnostic_text+"$check_start_dts = "+$check_start_dts+Char(Carriage return) $diagnostic_text:=$diagnostic_text+"$check_end_dts = "+$check_end_dts+Char(Carriage return) ErrorStack_AddOnError ($error_name;Current method name;$diagnostic_text) End if End if If ($error_name="") C_LONGINT($check_length) // We use this a few places below. $check_length:=utl_DT_Difference_Seconds ($check_end_dts;$check_start_dts) Case of : ($check_end_dts<$range_start_dts) // Ends before: NO conflict // |-------------------| compare to this one // |---| ends before $overlap_seconds:=0 : ($check_start_dts>$range_end_dts) // Ends after: NO Conflict // |-------------------| compare to this one // |---| starts after // $overlap_seconds:=0 //--------------------------------------------------------------------------- // All other nine possible cases represent an overlap of some length. //--------------------------------------------------------------------------- : (($check_start_dts>$range_start_dts) & ($check_end_dts<$range_end_dts)) // Contained within // |-------------------| compare to this one // |---------| contained within $overlap_seconds:=$check_length : (($check_start_dts=$range_start_dts) & ($check_end_dts<$range_end_dts)) // contained within, equal start // |-------------------| compare to this one // |----------| contained within, equal start $overlap_seconds:=$check_length : (($check_start_dts>$range_start_dts) & ($check_end_dts=$range_end_dts)) // |-------------------| compare to this one // |-----------| contained within, equal end $overlap_seconds:=$check_length : (($check_start_dts=$range_start_dts) & ($check_end_dts=$range_end_dts)) // |-------------------| compare to this one // |-------------------| contained within, equal start+end $overlap_seconds:=$check_length : (($check_start_dts<$range_start_dts) & ($check_end_dts<$range_end_dts)) // |-------------------| compare to this one // |------------| not fully contained, overlaps start // |-----| ends at start $overlap_seconds:=utl_DT_Difference_Seconds ($check_end_dts;$range_start_dts) : (($check_start_dts>$range_start_dts) & ($check_end_dts>$range_end_dts)) // |-------------------| compare to this one // |---------------| not fully contained, overlaps end $overlap_seconds:=utl_DT_Difference_Seconds ($range_end_dts;$check_start_dts) : (($check_start_dts<$range_start_dts) & ($check_end_dts=$range_end_dts)) // |-------------------| compare to this one // |-------------------------| overlaps start, bigger $overlap_seconds:=$check_length : (($check_start_dts=$range_start_dts) & ($check_end_dts>$range_end_dts)) // |-------------------| compare to this one // |-----------------------| overlaps end, bigger $overlap_seconds:=$check_length : (($check_start_dts<$range_start_dts) & ($check_end_dts>$range_end_dts)) // |-------------------| compare to this one // |------------------------------| overlaps entire period $overlap_seconds:=$check_length Else // Starts during TRACE // If you're reading this note, I screwed up. End case End if $0:=$overlap_seconds And now the test method: If (False) // =========================================== utl_DTS_Overlap_Test // DESCRIPTION: Tests if date times overlap at all. // All inputs are date time stamps. // Checking my work here... // CREATED BY: David Adams // DATE: 01/07/2017 // LAST MODIFIED: End if // ============================================ C_TEXT($dts_9am) C_TEXT($dts_10am) C_TEXT($dts_11am) C_TEXT($dts_12_noon) C_TEXT($dts_1pm) C_TEXT($dts_2pm) C_TEXT($dts_3pm) C_TEXT($dts_4pm) C_TEXT($dts_5pm) $dts_9am:=utl_DT_Get (!2018-01-01!;?09:00:00?;True) $dts_10am:=utl_DT_Get (!2018-01-01!;?10:00:00?;True) $dts_11am:=utl_DT_Get (!2018-01-01!;?11:00:00?;True) $dts_12_noon:=utl_DT_Get (!2018-01-01!;?12:00:00?;True) $dts_1pm:=utl_DT_Get (!2018-01-01!;?13:00:00?;True) $dts_2pm:=utl_DT_Get (!2018-01-01!;?14:00:00?;True) $dts_3pm:=utl_DT_Get (!2018-01-01!;?15:00:00?;True) $dts_4pm:=utl_DT_Get (!2018-01-01!;?16:00:00?;True) $dts_5pm:=utl_DT_Get (!2018-01-01!;?17:00:00?;True) C_TEXT($cr) C_TEXT($tab) C_TEXT($results) $cr:=Char(Carriage return) $tab:=Char(Tab) $results:="" $results:=$results+"Description"+$tab $results:=$results+"Code"+$tab $results:=$results+"Errors Expected"+$tab $results:=$results+"Errors"+$tab $results:=$results+"Seconds Expected"+$tab $results:=$results+"Seconds"+$cr C_LONGINT($overlap_seconds) // |-------------------| compare to this one // |---------| contained within // |----------| contained within, equal start // |-----------| contained within, equal end // |-------------------| contained within, equal start+end // |------------| not fully contained, overlaps start // |---------------| not fully contained, overlaps end // |-------------------------| overlaps start, bigger // |-----------------------| overlaps end, bigger // |------------------------------| overlaps entire period // |---| ends before // |---| starts after // |-----| ends at start (caught by 'not fully contained, overlaps start') // |-----| starts at end (caught by 'not not fully contained, overlaps end') // utl_DTS_GetIntervalOverlapSecs (Range start;Range end;Test start;Test end) : Seconds with -1 for bad input error If (True) // Bad input cases. ErrorStack_ClearStack $results:=$results+"Bad range input"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_9am;$dts_12_noon;$dts_1pm) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_9am;$dts_12_noon;$dts_1pm)"+$tab $results:=$results+"1"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+"-1"+$tab $results:=$results+String($overlap_seconds)+$cr ErrorStack_ClearStack $results:=$results+"Bad check input"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_9am;$dts_3pm;$dts_1pm;$dts_12_noon) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_9am;$dts_3pm;$dts_1pm;$dts_12_noon)"+$tab $results:=$results+"1"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+"-1"+$tab $results:=$results+String($overlap_seconds)+$cr End if // |-------------------| compare to this one // |---------| contained within ErrorStack_ClearStack $results:=$results+"Contained within"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_11am;$dts_12_noon) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_11am;$dts_12_noon)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |----------| contained within, equal start ErrorStack_ClearStack $results:=$results+"Contained within, equal start"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_10am;$dts_11am) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_11am;$dts_11am)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |-----------| contained within, equal end ErrorStack_ClearStack $results:=$results+"Contained within, equal end"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_2pm;$dts_3pm) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_2pm;$dts_3pm)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |-------------------| contained within, equal start+end ErrorStack_ClearStack $results:=$results+"Contained within, equal start+end"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_11am;$dts_10am;$dts_11am) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_11am;$dts_10am;$dts_11am)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |------------| not fully contained, overlaps start ErrorStack_ClearStack $results:=$results+"Not fully contained, overlaps start"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_9am;$dts_11am) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_9am;$dts_11am)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |---------------| not fully contained, overlaps end ErrorStack_ClearStack $results:=$results+"Not fully contained, overlaps end"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_2pm;$dts_4pm) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_3pm;$dts_2pm;$dts_4pm)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(60*60)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |---| ends before ErrorStack_ClearStack $results:=$results+"Ends before"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_11am;$dts_3pm;$dts_9am;$dts_10am) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_11am;$dts_3pm;$dts_9am;$dts_10am)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(0)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |---| starts after ErrorStack_ClearStack $results:=$results+"Ends after"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_3pm;$dts_4pm) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_3pm;$dts_4pm)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(0)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |-----| ends at start ErrorStack_ClearStack $results:=$results+"Ends at start"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_9am;$dts_10am) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_9am;$dts_10am)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(0)+$tab $results:=$results+String($overlap_seconds)+$cr // |-------------------| compare to this one // |-----| starts at end ErrorStack_ClearStack $results:=$results+"Starts at end"+$tab $overlap_seconds:=utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_2pm;$dts_3pm) $results:=$results+"utl_DTS_GetIntervalOverlapSecs ($dts_10am;$dts_2pm;$dts_2pm;$dts_3pm)"+$tab $results:=$results+"0"+$tab $results:=$results+String(ErrorStack_Count )+$tab $results:=$results+String(0)+$tab $results:=$results+String($overlap_seconds)+$cr SET TEXT TO PASTEBOARD($results)// I usually set a breakpoint before or after this. ********************************************************************** 4D Internet Users Group (4D iNUG) FAQ: http://lists.4d.com/faqnug.html Archive: http://lists.4d.com/archives.html Options: http://lists.4d.com/mailman/options/4d_tech Unsub: mailto:4d_tech-unsubscr...@lists.4d.com **********************************************************************