2
\$\begingroup\$

I have the algorithm which splits DateTime range into multiple ranges based on night time range.

For example:

Night start: 22:00, Night end: 06:00

Start time: 2020-10-26 21:00:00 End time: 2020-10-27 07:00:00

Expected output:

  • 2020-10-26 21:00:00 - 2020-10-26 22:00:00
  • 2020-10-26 22:00:00 - 2020-10-27 06:00:00
  • 2020-10-27 06:00:00 - 2020-10-27 07:00:00

It basically splits DateTime range by day and night/to day/night batches.

I wonder if there's something I could do to make the code shorter and possibly better performance-wise.

Code:

internal class Helper
{
    public static readonly TimeSpan NightStartTime = new TimeSpan(22, 0, 0);
    public static readonly TimeSpan NightEndTime = new TimeSpan(6, 0, 0);

    public static bool IsNightTime(TimeSpan time) => time >= NightStartTime || time < NightEndTime;
    public static bool IsDayTime(TimeSpan time) => !IsNightTime(time);
}

internal class Program
{
    private static void Main(string[] args)
    {
        var startTime = DateTime.Parse("2020-10-26 21:00:00");
        var endTime = DateTime.Parse("2020-10-27 07:00:00");

        DateTime? currentStartTime = startTime;
        DateTime? currentEndTime = null;
        var currentTimeIsDayTime = Helper.IsDayTime(startTime.TimeOfDay);
        for (var currentTime = startTime; currentTime <= endTime; currentTime = currentTime.AddMinutes(1))
        {
            var isDayTime = Helper.IsDayTime(currentTime.TimeOfDay);
            if (currentTimeIsDayTime != isDayTime)
            {
                currentEndTime = currentTime;
                currentTimeIsDayTime = isDayTime;
            }

            if (currentTime == endTime)
            {
                currentEndTime = endTime;
            }

            if (currentStartTime != null && currentEndTime != null)
            {
                Console.WriteLine($"{currentStartTime} - {currentEndTime}");

                currentStartTime = currentEndTime;
                currentEndTime = null;
            }
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Wait, are you going through the entire range of time between two DateTimes minute by minute (currentTime = currentTime.AddMinutes(1))? Why? You can access NightStartTime and NightEndTime, so why not compare startTime and endTime to those two and work from there? \$\endgroup\$
    – BCdotWEB
    Commented Oct 28, 2020 at 20:11
  • \$\begingroup\$ @BCdotWEB yeah, couldn't think of a better algorithm so far. I don't like that minute by minute thing. I still wonder how could I make it shorter \$\endgroup\$
    – Konrad
    Commented Oct 28, 2020 at 20:51

1 Answer 1

1
\$\begingroup\$

You can make it shorter by tackling the problem smarter, as in shift or work segments in relation to a DateTime or TimeofDay. Trying brute force checks of minute-by-minute is a horrifically bad solution. My answer may point you in the right direction but it is NOT TOTALLY CORRECT FOR ALL POSSIBLE INPUTS.

I would separate the logic into its own class.

public enum Shift { Day, Night }

// CAUTION: this class does not perform validation to make sure end times are after start time, or that
// start and end times are of the same DateTimeKind.  
// This does not work for Kind = Local, specifically on fall back or spring forward transition days.
// It does work for Kind = Utc or Unspecified, since they do not observe DST.
public class WorkShift
{

    // A WorkShift may not be complete shift, but rather segments.
    public Shift Shift { get; }
    public DateTime StartTime { get; }
    public DateTime EndTime { get; }

    public WorkShift(Shift shift, DateTime startTime, DateTime endTime)
    {
        this.Shift = shift;
        this.StartTime = startTime;
        this.EndTime = endTime;
    }

    public static readonly TimeSpan NightShiftStart = new TimeSpan(22, 0, 0);
    public static readonly TimeSpan NightShiftEnd = new TimeSpan(6, 0, 0);

    // Shifts have an inclusive start time but an exclusive end time.
    public static bool IsNightShift(TimeSpan timeOfDay) => timeOfDay >= NightShiftStart || timeOfDay < NightShiftEnd;
    public static bool IsDayShift(TimeSpan timeofDay) => !IsNightShift(timeofDay);

    public static bool IsNightShift(DateTime time) => IsNightShift(time.TimeOfDay);
    public static bool IsDayShift(DateTime time) => IsDayShift(time.TimeOfDay);

    public static IEnumerable<WorkShift> GetShiftSegments(DateTime startTime, DateTime endTime)
    {
        var segmentEnd = startTime;
        while (segmentEnd < endTime)
        {
            var segmentStart = segmentEnd;
            var shift = IsNightShift(segmentStart) ? Shift.Night : Shift.Day;
            segmentEnd = GetShiftEndTime(shift, segmentStart);
            if (segmentEnd > endTime)
            {
                segmentEnd = endTime;
            }
            yield return new WorkShift(shift, segmentStart, segmentEnd);
        }
    }

    private static  DateTime GetShiftEndTime(Shift shift, DateTime segmentStart)
    {
        // Nice thing about Day is it occurs on the same date, but Night may crossover midnight into the next date.
        if (shift == Shift.Day)
        {
            return DateTime.SpecifyKind(segmentStart.Date + NightShiftStart, segmentStart.Kind);
        }

        if (segmentStart.TimeOfDay >= NightShiftStart)
        {
            // ending shift would be tomorrow.
            return DateTime.SpecifyKind(segmentStart.Date.AddDays(1) + NightShiftEnd, segmentStart.Kind);
        }

        return DateTime.SpecifyKind(segmentStart.Date + NightShiftEnd, segmentStart.Kind);
    }
}

Now the Main test can become:

static void Main(string[] args)
{
    var startTime = DateTime.Parse("2020-10-26 21:00:00");
    var endTime = DateTime.Parse("2020-10-27 07:00:00");
    Console.WriteLine($"Time Range From {startTime} To {endTime}");

    var workSegments = WorkShift.GetShiftSegments(startTime, endTime).ToList();

    for (var i = 0; i < workSegments.Count; i++)
    {
        Console.WriteLine($"   [{i}] = {workSegments[i].Shift},  Start:{workSegments[i].StartTime}, End:{workSegments[i].EndTime}");
    }

    Console.WriteLine("\nPress ENTER to close");
    Console.ReadLine();
}

Sample console output is:

Time Range From 10/26/2020 9:00:00 PM To 10/27/2020 7:00:00 AM
   [0] = Day,  Start:10/26/2020 9:00:00 PM, End:10/26/2020 10:00:00 PM
   [1] = Night,  Start:10/26/2020 10:00:00 PM, End:10/27/2020 6:00:00 AM
   [2] = Day,  Start:10/27/2020 6:00:00 AM, End:10/27/2020 7:00:00 AM

Press ENTER to close

CAUTIONS AND WARNINGS

A WorkShift instance may or may not represent a complete Day or Night work shift, but instead a segment depending upon the starting or ending time.

As the code comments say, there are no validation checks to ensure that the ending time occurs after the starting time. Nor are there any checks to ensure that both the starting and ending times have the same DateTimeKind.

I did not check to see if the code works correctly for Local Kind. My intuition tells me to be suspicious or very leery about it. However, it should work when Kind is Utc or Unspecified, where neither observe DST and therefore neither will have a 23-hour Spring Forward Day or a 25-hour Fall Back Day.

It is important that both start and ending times belong the the same DateTimeKind when subtracting start from end. If they belong to different kinds, the time range span may be incorrect.

Even if you are super careful about the Kind, keep in mind that certain operations may return a invalid date and time. For instance, DateTime.Date returns a value at midnight with the same Kind as the input DateTime. But if your local time is Brazil (or Brasil), then the Spring Forward transition day does not have a midnight.

There are so many gotcha's with dates, times, and local time zones, that I would urge you to read up on https://nodatime.org/,

\$\endgroup\$
1
  • \$\begingroup\$ That's much better thanks a lot Rick. I must check NodaTime out if you say it solves time problems of different countries better than the standard API. \$\endgroup\$
    – Konrad
    Commented Oct 29, 2020 at 14:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.