简体   繁体   中英

C# Calender method

So basically I got the following assignment:

    Returns the number of open count time in the given list of appointments.
    Given appointments [9:30-10:00, 12:00-13:00, 15:15-16:30]
    the result should be 4
    [8:00-9:30, 10:00-12:00, 13:00-15:15, 16:30-17:00].
    name="appointments">The list of current appointments
    The number of open count time in the given list of appointments

What is the best way to check for open slots, without using a lot of IF statements? This is what I got so far:

        public int count (Appointment[] appointments)
        {
            int openslots = 0;
            foreach (var t in appointments)
            {
                if (t.End > t.Start )
                {
                    openslots++;
                }
            }

            return openslots;
        }

NOTE: "Appointmens[]" contains a list with all the appointments.

UPDATE 2: This is what I got so far:


Sometimes it helps to re-frame the problem in a way that makes it easier to tackle, and make sure you haven't missed anything. It can also help to draw a representation of the problem, depending on how visual you are - I think pictures make time range problems easier to grasp.

Given a time range [Tstart = 08:00, Tend = 17:00] and a list of non-overlapping sub-ranges ( appointments ), find the number of sub-ranges that are not covered by any range provided.

From the provided times we can draw a rough visualization of the plan like this:

8   9   10  11  12  13  14  15  16  17
+---+---+---+---+---+---+---+---+---+
|     ■■        ■■■■         ■■■■■  |

Visually the breaks in the used time are clear, giving 4 breaks. If all of the possible appointment lists mapped out like the above then it'd be simple to calculate the number of open blocks as appointments.Length + 1 . Here are some other options that don't fit that calculation:

8   9   10  11  12  13  14  15  16  17  Appointments to Gaps
+---+---+---+---+---+---+---+---+---+
■■■■■■■■        ■■■■         ■■■■■  |    3 : 3
|    ■■■        ■■■■         ■■■■■■■■    3 : 3
■■■■■■■■                     ■■■■■■■■    2 : 1
■■■■■■■■|■■■■■■■■■|■■■■■■■■|■■■■■■■■■    4 : 0

We could go on and test edge cases for days, including how we deal with overlapping appointments, but a better option would be to scan the list and see what we get.

From your code it looks like your Appointment class defines a time range between Start and End . If we step through the list, tracking the end of the previous range, we can enumerate through the open times for the day like this:

static TimeSpan DayStart = DateTime.Parse("08:00");
static TimeSpan DayEnd = DateTime.Parse("17:00");

int CountOpenTimeSlots(Appointment[] appointments)
{
    int count = 0;

    // Tracking variable for the end of the last closed slot.
    var previousEnd = DayStart;

    foreach (var appointment in Appointments)
    {
        // check the length of time between previous end and current start
        var openTime = appointment.Start - previousEnd;
        if (openTime.TotalMinutes > 0)
            count++;

        // update tracking
        previousEnd = appointment.End;
    }

    // Finally, check for open slot at the end of the day
    if ((DayEnd - previousEnd).TotalMinutes > 0)
        count++;

    return count;
}

This assumes that the appointment list is already sorted, and that none of the appointments end before the start of the day or start after the end of the day, and will fail if you have any total overlaps (one appointment completely inside another).

The above can be fairly easily converted to actually generate a list of open timeslots instead of just counting them. And we can make it a little more general, allowing for specification of the work range, filter out some bad inputs, etc.

class TimeRange { public TimeSpan Start, End; }

IEnumerable<TimeRange> EnumerateOpenSlots(TimeSpan DayStart, TimeSpan DayEnd, Appointment[] appointments)
{
    // clean up source list
    var filtered = appointments
        .Where(o => o.End >= DayStart && o.Start < DayEnd)
        .OrderBy(o => o.Start);

    var previousEnd = DayStart;
    foreach (var range in filtered)
    {
        // skip total overlaps
        if (range.End < previousEnd)
            continue;
        var openTime = range.Start - previousEnd;
        if (openTime.TotalMinutes > 0)
            yield return new TimeRange { Start = previousEnd, End = range.Start };
        previousEnd = range.End;
    }
    if ((DayEnd - previousEnd).TotalMinutes > 0)
        yield return new TimeRange { Start = previousEnd, End = DayEnd };
}

Now we can get the count using LINQ extensions:

int CountOpenTimeSlots(TimeRange[] appointments) 
    => EnumerateOpenSlots(DayStart, DayEnd, appointsments).Count();

Depends on what you mean by "best". If, for instance, you mean concise, perhaps:

appointments.Count(t => t.End > t.Start)

Sometimes I find it's useful to have a class or struct that does the hard lifting, that you write once, but use over and over again to make these kinds of calculations easy.

Here's my Period struct that does that:

private struct Period : IEquatable<Period>
{
    public DateTime StartTime { get; private set; }
    public DateTime EndTime { get; private set; }

    public Period(DateTime startTime, DateTime endTime)
    {
        this.StartTime = startTime;
        this.EndTime = endTime;
    }

    public override bool Equals(object obj)
    {
        if (obj is Period)
            return Equals((Period)obj);
        return false;
    }

    public bool Equals(Period obj)
    {
        if (!EqualityComparer<DateTime>.Default.Equals(this.StartTime, obj.StartTime))
            return false;
        if (!EqualityComparer<DateTime>.Default.Equals(this.EndTime, obj.EndTime))
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartTime);
        hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.EndTime);
        return hash;
    }

    public override string ToString()
    {
        return $"{{ StartTime = {this.StartTime}, EndTime = {this.EndTime} }}";
    }

    public IEnumerable<Period> Fragment(Period that)
    {
        if (this.StartTime < that.StartTime)
        {
            if (this.EndTime <= that.StartTime)
            {
                yield return this;
                yield return that;
            }
            else if (this.EndTime < that.EndTime)
            {
                yield return new Period(this.StartTime, that.StartTime);
                yield return new Period(that.StartTime, this.EndTime);
                yield return new Period(this.EndTime, that.EndTime);

            }
            else if (this.EndTime == that.EndTime)
            {
                yield return new Period(this.StartTime, that.StartTime);
                yield return that;
            }
            else if (this.EndTime > that.EndTime)
            {
                yield return new Period(this.StartTime, that.StartTime);
                yield return that;
                yield return new Period(that.EndTime, this.EndTime);
            }
        }
        else if (this.StartTime == that.StartTime)
        {
            if (this.EndTime < that.EndTime)
            {
                yield return this;
                yield return new Period(this.EndTime, that.EndTime);
            }
            else if (this.EndTime == that.EndTime)
            {
                yield return this;
            }
            else if (this.EndTime > that.EndTime)
            {
                yield return that;
                yield return new Period(that.EndTime, this.EndTime);
            }
        }
        else if (this.StartTime < that.EndTime)
        {
            if (this.EndTime < that.EndTime)
            {
                yield return new Period(that.StartTime, this.StartTime);
                yield return this;
                yield return new Period(this.EndTime, that.EndTime);
            }
            else if (this.EndTime == that.EndTime)
            {
                yield return new Period(that.StartTime, this.StartTime);
                yield return this;
            }
            else if (this.EndTime > that.EndTime)
            {
                yield return new Period(that.StartTime, this.StartTime);
                yield return new Period(this.StartTime, that.EndTime);
                yield return new Period(that.EndTime, this.EndTime);
            }
        }
        else // (this.StartTime >= that.EndTime)
        {
            yield return that;
            yield return this;
        }
    }
}

It's job is to represent a period in time and to be able to fragment two periods together.

For example, if I write this code:

DateTime today = DateTime.Now.Date;

var p1 = new Period(today.AddHours(9.0), today.AddHours(10.0));
var p2 = new Period(today.AddHours(9.5), today.AddHours(10.5));

var fragments = p1.Fragment(p2);

Then I get out three periods 09:00-09:30, 09:30-10:00, 10:00-10:30 .

Now it's easy to represent your appointments and your full day as periods:

Period[] appointments = new[]
{
    new Period(today.AddHours(9.5), today.AddHours(10.0)),
    new Period(today.AddHours(12.0), today.AddHours(13.0)),
    new Period(today.AddHours(15.25), today.AddHours(16.5)),
};

Period[] full_day = new[]
{
    new Period(today.AddHours(9.0), today.AddHours(17.0)),
};

Computing the free slots is an easy bit of LINQ:

Period[] free_slots =
    appointments
        .Aggregate(full_day, (a, x) => a.SelectMany(y => y.Fragment(x)).ToArray())
        .Except(appointments)
        .ToArray();

That gives me: 09:00-09:30, 10:00-12:00, 13:00-15:15, 16:30-17:00 .

And finally it's super easy to get the count by calling free_slots.Length .

It's a handy struct to have for these kinds of problems.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM