Tuesday, March 13, 2018

Overlapping ranges made easy

A while ago a team member wrote a routine to determine if two ranges of dates overlapped. He wrote seven different IF statements covering all the scenarios he saw. I tried to explain to him that you can do this with one statement. I don't think he ever understood.

It's really quite simple. Two ranges do not overlap if the largest of the minimums is greater than the smallest of the maximums. For example, if you have Min1-Max1 and Min2-Max2 as your ranges then there is no overlap if Max(Min1,Min2) > Min(Max1,Max2).

Here's a way too complex demo of this algorithm in action. Start a new WPF application project and call it Overlap. Target any Framework from 3.5 onward.


MainWindow.xaml uses a canvas and looks like this...

<Window x:Class="Overlap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OverlappingDates"
        mc:Ignorable="d" ResizeMode="NoResize"
        Title="Overlapping Ranges" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Canvas Background="AliceBlue" MouseMove="Canvas_MouseMove" MouseUp="Canvas_MouseUp" MouseLeave="Canvas_MouseLeave">
        <TextBlock Canvas.Left="10" Canvas.Top="10" Text="{Binding Line1Start, StringFormat='Min: {0}'}"/>
        <Line X1="{Binding Line1Start}" X2="{Binding Line1End}" Y1="30" Y2="30" Stroke="Red" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeThickness="10" MouseLeftButtonDown="Line1_MouseLeftButtonDown"/>
        <TextBlock Canvas.Left="450" Canvas.Top="10" Text="{Binding Line1End,StringFormat='Max: {0}'}"/>
       
        <TextBlock Canvas.Left="10" Canvas.Top="70" Text="{Binding Line2Start, StringFormat='Min: {0}'}"/>
        <Line X1="{Binding Line2Start}" X2="{Binding Line2End}" Y1="90" Y2="90" Stroke="Green" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeThickness="10" MouseLeftButtonDown="Line2_MouseLeftButtonDown"/>
        <TextBlock Canvas.Left="450" Canvas.Top="70" Text="{Binding Line2End, StringFormat='Max: {0}'}"/>

        <TextBlock Canvas.Left="10" Canvas.Top="120" Text="{Binding Path=MaxMin, StringFormat='Maximum Min value is {0}'}"/>
        <TextBlock Canvas.Left="10" Canvas.Top="140" Text="{Binding Path=MinMax, StringFormat='Minimum Max value is {0}'}"/>
        <TextBlock Canvas.Left="10" Canvas.Top="160" Text="{Binding Path=Overlap, StringFormat='Max(Min1,Min2) less than or equal to Min(Max1,Max2) = Ranges overlap: {0}'}"/>

        <TextBlock Canvas.Left="10" Canvas.Top="250" Text="Click and drag the ends of the lines to see the algorithm in action" FontWeight="Bold"/>
    </Canvas>
</Window>


Most of the code behind is involved in the graphics. It looks like this. I have bolded the three lines that are important.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;

namespace Overlap
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {

        public Double Line1Start
        {
            get { return _Line1Start; }
            set
            {
                SetProperty(ref _Line1Start, value);
                OnPropertyChanged("MaxMin");
            }
        }

        public Double Line1End
        {
            get { return _Line1End; }
            set
            {
                SetProperty(ref _Line1End, value);
                OnPropertyChanged("MinMax");
            }
        }

        private Double _Line1Start = 25;
        private Double _Line1End = 475;

        public Double Line2Start
        {
            get { return _Line2Start; }
            set
            {
                SetProperty(ref _Line2Start, value);
                OnPropertyChanged("MaxMin");
            }
        }

        public Double Line2End
        {
            get { return _Line2End; }
            set
            {
                SetProperty(ref _Line2End, value);
                OnPropertyChanged("MinMax");
            }
        }

        private Double _MinMax;
        public Double MinMax
        {
            get
            {
                _MinMax =  Math.Min(Line1End, Line2End);
                OnPropertyChanged("Overlap");
                return _MinMax;
            }
        }

        private Double _MaxMin;
        public double MaxMin
        {
            get
            {
                _MaxMin = Math.Max(Line1Start, Line2Start);
                OnPropertyChanged("Overlap");
                return _MaxMin;
            }
        }

        public string Overlap
        {
            get { return (_MaxMin <= _MinMax ? "True" : "False"); }
        }

        private Double _Line2Start = 25;
        private Double _Line2End = 475;

        private Point PriorMousePosition;
        private bool TrackingLine1Start = false;
        private bool TrackingLine1End = false;
        private bool TrackingLine2Start = false;
        private bool TrackingLine2End = false;

        public event PropertyChangedEventHandler PropertyChanged;
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(storage, value)) return false;
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        private void OnPropertyChanged(String PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Line1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PriorMousePosition = Mouse.GetPosition(this);
            if (PriorMousePosition.X > Line1Start + (Line1End - Line1Start) / 2)
                TrackingLine1End = true;
            else
                TrackingLine1Start = true;
        }
        private void Line2_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PriorMousePosition = Mouse.GetPosition(this);
            if (PriorMousePosition.X > Line2Start + (Line2End - Line2Start) / 2)
                TrackingLine2End = true;
            else
                TrackingLine2Start = true;
        }
        private void Canvas_MouseUp(object sender, MouseButtonEventArgs e)
        {
            StopTracking();
        }

        private void Canvas_MouseLeave(object sender, MouseEventArgs e)
        {
            StopTracking();
        }
        private void StopTracking()
        {
            TrackingLine1End = false;
            TrackingLine1Start = false;
            TrackingLine2End = false;
            TrackingLine2Start = false;
        }

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            double DeltaX = Mouse.GetPosition(this).X - PriorMousePosition.X;
            if (TrackingLine1Start)
            {
                Line1Start += DeltaX;
                if (Line1Start < 25) Line1Start = 25;
                if (Line1Start > Line1End - 10) Line1Start = Line1End - 10;
            }
            if (TrackingLine1End)
            {
                Line1End += DeltaX;
                if (Line1End > 475) Line1End = 475;
                if (Line1End < Line1Start + 10) Line1End = Line1Start + 10;
            }
            if (TrackingLine2Start)
            {
                Line2Start += DeltaX;
                if (Line2Start < 25) Line2Start = 25;
                if (Line2Start > Line2End - 10) Line2Start = Line2End - 10;
            }
            if (TrackingLine2End)
            {
                Line2End += DeltaX;
                if (Line2End > 475) Line2End = 475;
                if (Line2End < Line2Start + 10) Line2End = Line2Start + 10;
            }
            PriorMousePosition = Mouse.GetPosition(this);
        }

    }
}


You can click and drag the ends of the lines to verify that the algorithm works correctly.






Today a team member asked me how to check date ranges when any of the range limits could be null. For example, a range of null-today means any date up to and including today. I mentioned that the datetime class has static minvalue and maxvalue properties that can be substituted for null values. Most primitive classes in .Net do.



No comments:

Post a Comment