private int GetPeriodCount(DateTime start, DateTime end, SpanPeriod spanPeriod, int spanCount)
        {
            switch (spanPeriod)
            {
                case SpanPeriod.Day:
                case SpanPeriod.Week:
                    var periodSpan = ToTimeSpan(spanPeriod, spanCount);
                    return (int) Math.Ceiling((end - start).TotalHours / periodSpan.TotalHours);
                case SpanPeriod.Month:
                    return MonthsSpanned(start, end);
                case SpanPeriod.Year:
                    return YearsSpanned(start, end);
            }

            throw new Exception("Unhandled period: " + spanPeriod);
        }
        public TimelineResult Timeline(string query, SpanPeriod spanPeriod, int spanCount)
        {
            var q = ToQuery(query);
            var results = new TimelineResult();
            if (CountOfIndexedFiles > 0)
            {
                using (var wrapper = mainSearchManager.Wrapper())
                {
                    var hits = wrapper.IndexSearcher.search(
                        q, 
                        null, 
                        CountOfIndexedFiles, 
                        new Sort(new SortField(FieldName.CreatedDate, SortField.Type.STRING, true)));

                    var dateRangeMatches = hits.totalHits;
                    if (dateRangeMatches > 0)
                    {
                        var docs = hits.scoreDocs;
                        var newestDoc = wrapper.IndexSearcher.doc(docs[0].doc);
                        var oldestDoc = wrapper.IndexSearcher.doc(docs[dateRangeMatches - 1].doc);

                        results.OldestMatchingDate = oldestDoc.ToDateTime(FieldName.CreatedDate).Value;
                        results.NewestMatchingDate = newestDoc.ToDateTime(FieldName.CreatedDate).Value;
                        results.TotalMatches = dateRangeMatches;

                        var periodCount = GetPeriodCount(results.OldestMatchingDate, results.NewestMatchingDate, spanPeriod, spanCount);

                        // The documents are sorted newest date first, so the dates manipulation counts down
                        int matchIndex = 0;
                        results.Timeline = new List<TimelinePeriod>(periodCount);
                        var currentEnd = GetEndDateTime(results.NewestMatchingDate, spanPeriod);
                        for (int timelineIndex = 0; timelineIndex < periodCount; ++timelineIndex)
                        {
                            var currentStart = GetStartDateTime(currentEnd, spanPeriod, spanCount);
                            var currentPeriod = new TimelinePeriod { Start = currentStart };
                            results.Timeline.Add(currentPeriod);

                            while (matchIndex < dateRangeMatches)
                            {
                                var doc = wrapper.IndexSearcher.doc(docs[matchIndex].doc);
                                var docDate = doc.ToDateTime(FieldName.CreatedDate).Value;
                                if (docDate > currentStart)
                                {
                                    ++currentPeriod.Count;
                                    ++matchIndex;
                                }
                                else
                                {
                                    break;
                                }
                            }

                            currentEnd = currentStart;
                        }

                        if (matchIndex != dateRangeMatches)
                            logger.Warn("Missed some matches; should have placed {0}, but placed {1}", dateRangeMatches, matchIndex);
                    }
                }
            }

            return results;
        }
        private DateTime GetEndDateTime(DateTime endDateTime, SpanPeriod spanPeriod)
        {
            switch (spanPeriod)
            {
                case SpanPeriod.Day:
                case SpanPeriod.Week:
                    return new DateTime(endDateTime.Year, endDateTime.Month, endDateTime.Day);

                case SpanPeriod.Month:
                    return new DateTime(endDateTime.Year, endDateTime.Month, DateTime.DaysInMonth(endDateTime.Year, endDateTime.Month));

                case SpanPeriod.Year:
                    return new DateTime(endDateTime.Year, 12, 31);
            }

            throw new Exception("Unhandled period: " + spanPeriod);
        }
        private DateTime GetStartDateTime(DateTime endDateTime, SpanPeriod spanPeriod, int count)
        {
            switch (spanPeriod)
            {
                case SpanPeriod.Day:
                case SpanPeriod.Week:
                    return endDateTime - ToTimeSpan(spanPeriod, count);
                case SpanPeriod.Month:
                    if (endDateTime.Day == 1)
                    {
                        if (endDateTime.Month == 1)
                            return new DateTime(endDateTime.Year - 1, 12, 1);
                        return new DateTime(endDateTime.Year, endDateTime.Month - 1, 1);
                    }
                    return new DateTime(endDateTime.Year, endDateTime.Month, 1);
                case SpanPeriod.Year:
                    if (endDateTime.Month == 1 && endDateTime.Day == 1)
                        return new DateTime(endDateTime.Year - 1, 1, 1);
                    return new DateTime(endDateTime.Year, 1, 1);
            }

            throw new Exception("Unhandled period: " + spanPeriod);
        }
        private TimeSpan ToTimeSpan(SpanPeriod spanPeriod, int count)
        {
            switch (spanPeriod)
            {
                case SpanPeriod.Day:
                    return new TimeSpan(24 * count, 0, 0);
                case SpanPeriod.Week:
                    return new TimeSpan(7 * count, 0, 0, 0);
            }

            throw new Exception("Unhandled period: " + spanPeriod);
        }