tags:

views:

781

answers:

2

I have a list of elements describing events that took place at some time, the time being represented as a Datetime property 'StartTime' on the objects. I now wish to extract a subset of these events containing those elements that are placed in an interval / between two DateTime instances A,B such that StartTime >= A && StartTime <= B. Currently this is accomplished by a simple Linq query, but since I have to run a lot of queries extracting small fractions of the list, its quite inefficient.

Had hoped that the standard SortedList class had some sort of subset functionality over the key, but that doesnt seem to be the case. Any ideas if this can be archived relatively simple with existing framework classes, or would I have to make some sort of custom binary search based on a SortedList?

+4  A: 

If possible, I would keep the instances in an array, sorted by the StartTime, and then call BinarySearch to determine the indexes in the array the bounds end at.

Then, given that, you can easily access a subset based on date range quickly.

I've worked up a generic class which can help you with this as well:

public class BinarySearcher<T>
{
    // Possibly passed to the call to BinarySort.
    private class ComparisonComparer : Comparer<T>
    {
        Comparison<T> comparison;

        internal static IComparer<T> Create(Comparison<T> comparison)
        {
            // If comparison is null, return the default comparer for T.
            if (comparison == null)
            {
                // Return the default.
                return Comparer<T>.Default;
            }

            // Return a new implementation.
            return new ComparisonComparer(comparison);
        }

        private ComparisonComparer(Comparison<T> comparison)
        {
            this.comparison = comparison;
        }

        public override int Compare(T x, T y)
        {
            return comparison(x, y);
        }
    }

    // The elements.
    T[] elements;

    // The IComparable implementation.
    IComparer<T> comparer;

    // Do not assume sorting.
    public BinarySearcher(IEnumerable<T> elements) : 
        this(elements, false, (IComparer<T>) null) { }

    // Use default comparer.
    public BinarySearcher(IEnumerable<T> elements, bool sorted) :
        this(elements, sorted, (IComparer<T>) null) { }

    // Assume no sorting.
    public BinarySearcher(IEnumerable<T> elements, 
        Comparison<T> comparer) :
            this(elements, false, 
                ComparisonComparer.Create(comparer)) { }

    // Convert to IComparable<T>.
    public BinarySearcher(IEnumerable<T> elements, bool sorted, 
        Comparison<T> comparer) :
            this(elements, sorted, 
                ComparisonComparer.Create(comparer)) { }

    // No sorting.
    public BinarySearcher(IEnumerable<T> elements, 
        IComparer<T> comparer) :
            this(elements, false, comparer) { }

    // Convert to array.
    public BinarySearcher(IEnumerable<T> elements, bool sorted, 
        IComparer<T> comparer) :
            this(elements.ToArray(), sorted, comparer) { }

    // Assume no sorting.
    public BinarySearcher(T[] elements) : this(elements, false) { }

    // Pass null for default comparer.
    public BinarySearcher(T[] elements, bool sorted) : 
        this(elements, sorted, (IComparer<T>) null) { }

    // Assume not sorted.
    public BinarySearcher(T[] elements, Comparison<T> comparer) :
        this(elements, false, ComparisonComparer.Create(comparer)) { }

    // Create IComparable<T> from Comparable<T>.
    public BinarySearcher(T[] elements, bool sorted, 
        Comparison<T> comparer) :
            this(elements, sorted, ComparisonComparer.Create(comparer)) { }

    // Assume the elements are not sorted.
    public BinarySearcher(T[] elements, IComparer<T> comparer) : 
        this(elements, false, comparer) { }

    public BinarySearcher(T[] elements, bool sorted, 
        IComparer<T> comparer)
    {
        // If the comparer is null, create the default one.
        if (comparer == null)
        {
            // Set to the default one.
            comparer = Comparer<T>.Default;
        }

        // Set the comparer.
        this.comparer = comparer;

        // Set the elements.  If they are sorted already, don't bother, 
        // otherwise, sort.
        if (!sorted)
        {
            // Sort.
            Array.Sort(elements, this.comparer);
        }

        // Set the elements.
        this.elements = elements;
    }

    public IEnumerable<T> Between(T from, T to)
    {
        // Find the index for the beginning.
        int index = Array.BinarySearch(this.elements, from, comparer);

        // Was the item found?
        bool found = (index >= 0);

        // If the item was not found, take the bitwise 
        // compliment to find out where it would be.
        if (!found)
        {
            // Take the bitwise compliment.
            index = ~index;
        }

        // If the item was found, cycle backwards from
        // the index while there are elements that are the same.
        if (found)
        {
            // Cycle backwards.
            for (; index >= 0 && 
                comparer.Compare(from, elements[index]) == 0; 
                --index) ;

            // Add one to the index, since this is on the element 
            // that didn't match the comparison.
            index++;
        }

        // Go forward now.
        for ( ; index < elements.Length; index++)
        {
            // Return while the comparison is true.
            if (comparer.Compare(elements[index], to) <= 0)
            {
                // Return the element.
                yield return elements[index];
            }
            else
            {
                // Break
                yield break;
            }
        }
    }
}

Most of what is here are helpers to initialize the class, which will need a method of comparing two items (IComparer<T> and Comparable<T> are taken here). The class also lets you take in a raw array which is already sorted, in case you want to add/remove/update elements in the proper positions yourself later on (without having to resort the array all the time).

The meat of the class is in the call to Between. It takes a from value, which will be passed to the static BinarySearch method on the Array class, passing the IComparer implementation that this instance uses (it assumes defaults if none are passed in).

If the value doesn't exist, it finds out where it would exist in the array, and then cycles backwards in case there are multiple elements with the same value in the array.

Then, it cycles forward, returning the items in the enumeration while the current element is less than or equal to the to value.

In your particular situation, assuming you had a class like this:

public class Task
{
    public string Name;
    public DateTime StartTime;
}

You could do the following:

// Create tasks.
Task[] tasks = 
{ 
    new Task() { Name = "Task 1", StartTime = new DateTime(2009, 02, 18) },
    new Task() { Name = "Task 2", StartTime = new DateTime(2009, 02, 16) },
    new Task() { Name = "Task 3", StartTime = new DateTime(2009, 02, 12) },
    new Task() { Name = "Task 4", StartTime = new DateTime(2009, 02, 11) },
    new Task() { Name = "Task 5", StartTime = new DateTime(2009, 02, 10) },
    new Task() { Name = "Task 6", StartTime = new DateTime(2009, 02, 01) },
    new Task() { Name = "Task 7", StartTime = new DateTime(2009, 02, 09) }
};

// Now create the indexer.
BinarySearcher<Task> searcher = new BinarySearcher<Task>(tasks,
    (x, y) => Comparer<DateTime>.Default.Compare(x.StartTime, y.StartTime));

foreach (Task t in searcher.Between(
    new Task() { StartTime = new DateTime(2009, 02, 13) },
    new Task() { StartTime = new DateTime(2009, 02, 10) }))
{
    // Write.
    Console.WriteLine(t);
}
casperOne
Ahh thanks. I didn't know that BinarySearch(..) returned the 'closest index' when no precise match is found. No such search method directly on the SortedList as far as I can see. Just need to figure out if I can access the value array of a SortedList directly then, to reuse the insert logic.
A: 

Have you checked out Array.Find using a custom predicate?

REA_ANDREW