提取一段时间内的子时段

时间:2014-07-09 14:46:02

标签: c# .net datetime intervals

给定从StartingDateEndingDate的句号。 我希望从StartingMonthEndingMonth开始获取该时间段内的时间间隔。

示例:

StartingMonth = april (4) 
EndingMonth = november (11)

期间:

Period A : StartingDate =  (2014, 03, 01); EndingDate = (2015, 02, 28);
Period B : StartingDate =  (2014, 07, 01); EndingDate = (2015, 06, 30);
Period C : StartingDate =  (2014, 01, 01); EndingDate = (2015, 12, 31);

会回来:

Period A : 1 sub-period = (2014, 4, 1) - (2014, 11, 30)
Period B : 2 sub-periods = (2014, 7, 1) - (2014, 11, 30) ; (2015, 4, 1) - (2015, 6, 30)
Period C : 2 sub-periods = (2014, 4, 1) - (2014, 11, 30) ; (2015, 4, 1) - (2015, 11, 30)

我试过这个(似乎很难,并且没有管理多个子时段): 使用LINQ可能更简单吗?

if (StartingDate.Month < startingMonth && EndingDate.Month < endingMonth)
{
    periods.Add(new PeriodInterval
    {
        StartDate = new DateTime(StartingDate.Year, startingMonth, 1), 
        EndDate = new DateTime(StartingDate.Year, endingMonth, EndingDate.Day)
    });
}

if (StartingDate.Month > startingMonth && EndingDate.Month > endingMonth)
{
     periods.Add(new PeriodInterval
    {
       StartDate = new DateTime(StartingDate.Year, startingMonth, 1), 
       EndDate = new DateTime(StartingDate.Year, endingMonth, EndingDate.Day)
     });
}

if (StartingDate.Month < startingMonth && EndingDate.Month > endingMonth)
{
    periods.Add(new PeriodInterval
    {
        StartDate = new DateTime(StartingDate.Year, startingMonth, 1), 
        EndDate = new DateTime(StartingDate.Year, endingMonth, EndingDate.Day)
    });                   
}

if (StartingDate.Month > startingMonth && EndingDate.Month < endingMonth)
{
    periods.Add(new PeriodInterval
    {
        StartDate = new DateTime(StartingDate.Year, startingMonth, 1), 
        EndDate = new DateTime(StartingDate.Year, endingMonth, EndingDate.Day)
    });                    
}

想法是返回红色时段内的蓝色时段:    The idea is to returns the blue periods within the red period

4 个答案:

答案 0 :(得分:1)

class Discount
{
    public int DiscountID { get; set; } //You will need some Key field if you are storing these in a database.
    public DateTime issueDate { get; set; }
    public DateTime expirationDate { get; set; }

    public List<PeriodInterval> intervals { get; set; }

    public Discount(DateTime IssueDate, DateTime ExpirationDate)
    {
        issueDate = IssueDate;
        expirationDate = ExpirationDate;
        intervals = new List<PeriodInterval>();
    }

    public void AddInterval(DateTime StartDate, DateTime EndDate)
    {
        intervals.Add(new PeriodInterval() { 
            StartMonth=StartDate.Month,
            StartDay=StartDate.Day,
            EndMonth=EndDate.Month,
            EndDay=EndDate.Day
        });
    }
    public List<Period> GetPeriods()
    {
        List<Period> periods=new List<Period>();
        int yearCount = expirationDate.Year-issueDate.Year+1; //+1: Run at least one year against the periods.
        for (int i = 0; i < yearCount; i++)
        {
            //Loop through all the years and add 'Periods' from all the PeriodInterval info.
            foreach (PeriodInterval pi in intervals)
            {
                var period = pi.GetPeriod(issueDate, expirationDate, i);
                if (period != null)
                    periods.Add(period);
            }
        }
        return periods;
    }
}
class Period
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}
class PeriodInterval
{
    public int PeriodIntervalID { get; set; } //You will need some Key field if you are storing these in a database.
    public int DiscountID { get; set; } //Foreign Key to Discount. This is alsof for database storage.

    public int StartMonth { get; set; }
    public int StartDay { get; set; }

    public int EndMonth { get; set; }
    public int EndDay { get; set; }

    public Period GetPeriod(DateTime issueDate, DateTime expirationDate, int Year)
    {
        DateTime PeriodStart = new DateTime(issueDate.AddYears(Year).Year, StartMonth, StartDay);
        DateTime PeriodEnd = new DateTime(issueDate.AddYears(Year).Year, EndMonth, EndDay);

        PeriodStart=new DateTime(Math.Max(PeriodStart.Ticks, issueDate.Ticks)); //Limit period to the max of the two start dates.
        PeriodEnd = new DateTime(Math.Min(PeriodEnd.Ticks, expirationDate.Ticks)); //Limit period to the min of the two end dates.

        if(PeriodEnd>PeriodStart) //If a valid period
        {
            return new Period()
            {
                StartDate = PeriodStart,
                EndDate = PeriodEnd
            };
        }
        //Default Return Null
        return null;
    }
}

我构建了一个控制台应用程序来测试它:

static void Main(string[] args)
{
    List<Discount> Discounts = new List<Discount>();

    Discount d1 = new Discount(new DateTime(2014, 3, 1), new DateTime(2015, 02, 28));
    Discount d2 = new Discount(new DateTime(2014, 7, 1), new DateTime(2015, 06, 30));
    Discount d3 = new Discount(new DateTime(2014, 01, 1), new DateTime(2015, 12, 31));

    Discounts.Add(d1);
    Discounts.Add(d2);
    Discounts.Add(d3);

    foreach (Discount d in Discounts)
    {
        d.AddInterval(new DateTime(2014, 4, 1), new DateTime(2014, 11, 30));

        Console.WriteLine("IssueDate:{0} ExpirationDate:{1}", d.issueDate, d.expirationDate);
        foreach (Period p in d.GetPeriods())
        {
            Console.WriteLine("Start:{0} End:{1}", p.StartDate, p.EndDate);
        }
    }

    Console.ReadLine();
}

这是打印出来的内容:

Results

答案 1 :(得分:1)

您可以使用Time Period Library for .NET

// ----------------------------------------------------------------------
public void ExtractSubPeriods()
{
  foreach ( ITimePeriod subPeriod in GetSubPeriods(
    new TimeRange( new DateTime( 2014, 4, 1 ), new DateTime( 2015, 2, 28 ) ) ) )
  {
    Console.WriteLine( "SubPeriods 1: {0}", subPeriod );

  foreach ( ITimePeriod subPeriod in GetSubPeriods(
    new TimeRange( new DateTime( 2014, 7, 1 ), new DateTime( 2015, 6, 30 ) ) ) )
  {
    Console.WriteLine( "SubPeriods 2: {0}", subPeriod );
  }

  foreach ( ITimePeriod subPeriod in GetSubPeriods(
    new TimeRange( new DateTime( 2014, 4, 1 ), new DateTime( 2015, 12, 31 ) ) ) )
  {
    Console.WriteLine( "SubPeriods 3: {0}", subPeriod );
  }
} // ExtractSubPeriods

// ----------------------------------------------------------------------
public ITimePeriodCollection GetSubPeriods( ITimeRange timeRange )
{
  ITimePeriodCollection periods = new TimePeriodCollection();
  periods.Add( timeRange );

  int startYear = periods.Start.Year;
  int endYear = periods.End.Year + 1;
  for ( int year = startYear; year <= endYear; year++ )
  {
    periods.Add( new TimeRange( new DateTime( year, 4, 1 ), new DateTime( year, 12, 1 ) ) );
  }

  TimePeriodIntersector<TimeRange> intersector = new TimePeriodIntersector<TimeRange>();
  return intersector.IntersectPeriods( periods );
} // GetSubPeriods

答案 2 :(得分:0)

需要考虑的一些事项:

  • 作为人类,我们通常使用完全包含范围作为仅日期值,而我们使用半开时间间隔仅用于时间或日期+时间值。 思考:从1月1日到1月2日2天,但从1:00到2:00 1小时,或从1月1日午夜到1月2日午夜。

  • .Net的内置DateTime类型,是日期+时间类型。省略时间时,它会使用午夜。您无法删除时间部分。

  • 如果您使用DateTime的午夜日期范围,您可以做的最好是选择忽略时间部分。这会产生一些棘手的代码,因为在与范围进行比较之前,您必须将输入规范化为午夜。我不推荐这种方法,因为它容易出错。边缘情况会很快堆积起来。

  • 因此,我建议您使用DateTime切换到半开时间间隔,或者如果您需要继续使用完全包含范围,请考虑使用{{3}中的LocalDate类型}}。我将向您展示两者的例子。

  • 因为您接受月份数作为输入,请考虑您还应该处理它们不按顺序排列的情况。也就是说,两个月的子期间可能是从一年的12月到下一年的1月。

  • 除非保证外部时段在整月的起点和终点完全 ,否则您需要修剪结果。例如,如果您的期间从2014年1月3日到2016年3月9日,则2015年的子期间将持续整个月,但2014年将在开始时进行调整,2016年将在结束时进行调整。

以下是使用DateTime和午夜半开的日期间隔来实现此目的的方法:

public class DateTimeInterval
{
    /// <summary>
    /// The date and time that the interval starts.
    /// The interval includes this exact value.
    /// </summary>
    public DateTime StartDate { get; private set; }

    /// <summary>
    /// The date and time that the interval is over.
    /// The interval excludes this exact value.
    /// </summary>
    public DateTime EndDate { get; private set; }

    public DateTimeInterval(DateTime startDate, DateTime endDate)
    {
        StartDate = startDate;
        EndDate = endDate;
    }

    public IEnumerable<DateTimeInterval> GetSubIntervals(int startingMonth,
                                                         int endingMonth)
    {
        // Determine the possible ranges based on the year of this interval
        // and the months provided
        var ranges = Enumerable.Range(StartDate.Year,
                                      EndDate.Year - StartDate.Year + 1)
            .Select(year => new DateTimeInterval(
                              new DateTime(year, startingMonth, 1),
                              new DateTime(
                                startingMonth > endingMonth ? year + 1 : year,
                                endingMonth, 1)
                                .AddMonths(1)));

        // Get the ranges that are overlapping with this interval
        var results = ranges.Where(p => p.StartDate < this.EndDate &&
                                        p.EndDate > this.StartDate)
                            .ToArray();

        // Trim the edges to constrain the results to this interval
        if (results.Length > 0)
        {
            if (results[0].StartDate < this.StartDate)
            {
                results[0] = new DateTimeInterval(
                    this.StartDate,
                    results[0].EndDate);
            }

            if (results[results.Length - 1].EndDate > this.EndDate)
            {
                results[results.Length - 1] = new DateTimeInterval(
                    results[results.Length - 1].StartDate,
                    this.EndDate);
            }
        }

        return results;
    }
}

使用上面的代码:

var interval = new DateTimeInterval(new DateTime(2014, 3, 1),  // inclusive
                                    new DateTime(2015, 3, 1)); // exclusive
var subIntervals = interval.GetSubIntervals(4, 11);

以下是使用NodaTime.LocalDate和完全包含的仅限日期间隔实现相同目标的方法:

using NodaTime;

public class LocalDateInterval
{
    /// <summary>
    /// The date that the interval starts.
    /// The interval includes this exact value.
    /// </summary>
    public LocalDate StartDate { get; private set; }

    /// <summary>
    /// The date that the interval ends.
    /// The interval includes this exact value.
    /// </summary>
    public LocalDate EndDate { get; private set; }

    public LocalDateInterval(LocalDate startDate, LocalDate endDate)
    {
        StartDate = startDate;
        EndDate = endDate;
    }

    public IEnumerable<LocalDateInterval> GetSubIntervals(int startingMonth,
                                                          int endingMonth)
    {
        // Determine the possible ranges based on the year of this interval
        // and the months provided
        var ranges = Enumerable.Range(StartDate.Year,
                                      EndDate.Year - StartDate.Year + 1)
            .Select(year => new LocalDateInterval(
                              new LocalDate(year, startingMonth, 1),
                              new LocalDate(
                                startingMonth > endingMonth ? year + 1 : year,
                                endingMonth, 1)
                                .PlusMonths(1).PlusDays(-1)));

        // Get the ranges that are overlapping with this interval
        var results = ranges.Where(p => p.StartDate <= this.EndDate &&
                                        p.EndDate >= this.StartDate)
                            .ToArray();

        // Trim the edges to constrain the results to this interval
        if (results.Length > 0)
        {
            if (results[0].StartDate < this.StartDate)
            {
                results[0] = new LocalDateInterval(
                    this.StartDate,
                    results[0].EndDate);
            }

            if (results[results.Length - 1].EndDate > this.EndDate)
            {
                results[results.Length - 1] = new LocalDateInterval(
                    results[results.Length - 1].StartDate,
                    this.EndDate);
            }
        }

        return results;
    }
}

使用上面的代码:

var interval = new LocalDateInterval(new LocalDate(2014, 3, 1),   // inclusive
                                     new LocalDate(2015, 2, 28)); // inclusive
var subIntervals = interval.GetSubIntervals(4, 11);

答案 3 :(得分:0)

这应该有效:

var periods = Periods
    .Select(p => new {
            p = p,
            a = p.StartingDate.Year*12 + p.StartingDate.Month - 1,
            b = p.EndingDate.Year*12 + p.EndingDate.Month
        }
    )
    .Select(x => new {
        period = x.p,
        subperiods =
            Enumerable
            .Range(x.a, x.b - x.a)
            .Select(e => new DateTime(e/12, e%12 + 1, 1))
            .Where(d => StartingMonth <= d.Month && d.Month <= EndingMonth)
            .GroupBy(i => i.Year)
            .Where(g => g.Count() > 1)
            .Select(g => new Period {
                StartingDate = g.Min(),
                EndingDate = g.Max()
            })
            .Select(p => new Period {
                StartingDate = p.StartingDate < x.p.StartingDate ? x.p.StartingDate : p.StartingDate,
                EndingDate = (p.EndingDate > x.p.EndingDate ? x.p.EndingDate : p.EndingDate)
                    .AddMonths(1)
                    .AddDays(-1)
            })
        });

results

<强>更新

根据你的形象,这可以解决问题:

var periods = Periods
.Select(p => new {
        p = p,
        a = p.StartingDate.Year*12 + p.StartingDate.Month - 1,
        b = p.EndingDate.Year*12 + p.EndingDate.Month
    }
)
.Select(x => new {
    period = x.p,
    subperiods =
        Enumerable
        .Range(x.a, x.b - x.a)
        .Select(e => new DateTime(e/12, e%12 + 1, 1))
        .Where(d => StartingMonth <= d.Month && d.Month <= EndingMonth)
        .GroupBy(i => i.Year)
        .Where(g => g.Count() > 1)
        .Select(g => g.Select(i => i))
    });

enter image description here