Reputation: 461
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.
Values
via the array indexer []
must be runtime optimised. It will be called millions of times in a very short period of time.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
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