Konstantin
Konstantin

Reputation: 461

Dynamic Key for SortedList<Key, Value>

I am having the following class

public class MyDictionary : SortedList<int, MyData>
{
}

At the moment the Key in the SortedList represents a year number, e.g. 2014, 2015, 2016 etc. The Value represents the data for the year.

Now I have a new requirement saying that having a Value per year is not enough and this class should support a finer granularity.

The new granularity looks like this:

Of course one instance of MyDictionary should represent one time frame, e.g. SortedList<Yearly, MyData>, SortedList<Monthly, MyData>.

The data that goes into MyDictionary spans over several years. That means that I cannot use, e.g. the number of a month in a monthly granularity. Example:

2014-12
2015-01
2015-02
...
2015-12

As you can see the number 12 is twice in the list.

My problem is, that I don't know what data type to use for the Key and how to access the Values in MyDictionary to meet the new requirement.

Any ideas?

Modification from 24.02.2016:

I must add a little more information to the original question.

  1. The granularity is known at runtime only
  2. The access to the Values via the array indexer [] must be runtime optimised. It will be called millions of times in a very short period of time.
  3. The class that uses MyDictionary uses a DateTime object to access the Values. Example:
public class ClientClass
{
    public void AccessMyDictionary(DateTime date)
    {
        MyData data = MyDictionary[date.Year];

        // Do something with data
    }
}

It looks to me that the most obvious thing to do is to have DateTime as an indexer data type. Then create an indexer in the MyDictionary class to take care of granularity. Example:

public enum Period
{
    Yearly,
    Quarterly,
    Monthly,
    Weekly,
    Daily
}

public class MyDictionary
{
    private Period period;
    private SortedList<DateTime, MyData> sortedList;

    public MyDictionary(Period period)
    {
        this.period = period;
        sortedList  = new SortedList<DateTime, MyData>();
    }

    public MyData this[DateTime i]
    {
        get
        {
            // Implement here an algorithm for granularity, similar to the one of Tomas Lycken in the 1st answer
        }

        set
        {
            // Implement here an algorithm for granularity, similar to the one of Tomas Lycken in the 1st answer
        }
    }
}

What do you think? Is that runtime optimised?

Many thanks Konstantin

Upvotes: 0

Views: 260

Answers (1)

Tomas Aschan
Tomas Aschan

Reputation: 60594

I would define some new value objects for the various granularities, all deriving from a common base class Period. You can then use as these keys. For example,

public abstract class Period { }

public class Quarter : Period
{
    public int Quarter { get; }
    public int Year { get; }

    public Quarter(int year, int quarter)
    {
        if (year < 1800 || year > DateTime.UtcNow.Year)
        {
            throw new ArgumentOutOfRangeException(nameof(year));
        }

        if (quarter < 1 || quarter > 4)
        {
            throw new ArgumentOutOfRangeException(nameof(quarter));
        }

        Year = year;
        Quarter = quarter;
    }
}

And of course you'd define similar types for Year (which only has one property), Month (which has a year and a month, and the month must be between 1 and 12), Week (where validation becomes a little more tricky, since not all years have the same number of weeks), Day (don't forget to allow for leap years!).

Then, you also define equality and hashing for these types so that if their properties are equal, they are equal. (This is a good read on the topic!) For Quarter, I'd do something like

public class Quarter
{
    // properties and constructor ommitted

    public override bool Equals(object other)
    {
        if (!(other is Quarter))
        {
            return false;
        }
        var quarter = (Quarter)other;

        return quarter.Year == Year && quarter.Quarter == quarter;
    }

    public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            // The two hard-coded digits below should be primes,
            // uniquely chosen per type (so no two types you define
            // use the same primes).

            int hash = (int) 2166136261;
            // Suitable nullity checks etc, of course :)
            hash = hash * 16777619 ^ Quarter.GetHashCode();
            hash = hash * 16777619 ^ Year.GetHashCode();
            return hash;
        }
    }
}

Depending on how else you're going to use these, you might also want to override == and/or !=.

Now, these types are fully usable as keys in the dictionary, so you can do

var quarterlyReport = new SortedList<Quarter, Data>();

If you want to avoid having to define Equals and GetHashCode manually, most associative collections in .NET have a constructor which takes an equality comparer for the key type, that handles this for you. SortedList<TKey, TValue> has one too, so instead of overriding Equals and GetHashCode above you could create a pendant type for each period like

public class QuarterComparer : IComparer<Quarter>
{
    int IComparer<Quarter>.Compare(Quarter p, Quarter q)
    {
        return p.Year < q.Year
            ? -1
            : p.Year == q.Year
                ? p.Quarter < q.Quarter
                    ? -1
                    : p.Quarter == q.Quarter
                        ? 0
                        : 1
                : 1;
    }

    public int Compare(Quarter p, Quarter q)
    {
        return (this as IComparer<Quarter>).Compare(p, q);
    }
}

and pass this to the constructor of the sorted list:

var quarterlyData = new SortedList<Quarter, MyData>(new QuarterComparer());

Upvotes: 2

Related Questions