1

I have a collection of objects that I need to iterate through and do stuff with. Seems easy so far. However, I have some conditions which is making it rather complicated.

Here is some info:

The collection contains a bunch of "Planet" objects which have planetary phase times.

Planetary viewing times are combined into blocks if the time span between 2 phases is less than or equal to 30 minutes.

For example, here are 6 phase times:

  • Phase 1: 8:00am - 9:30am
  • Phase 2: 10:00am - 11:00am
  • Phase 3: 11:20am - 12:30pm
  • Phase 4: 2:00pm - 4:00pm
  • Phase 5: 6:30pm - 7:30pm
  • Phase 6: 7:45pm - 9:00pm

With the data above, we have the following blocks:

  • Phase 1 through Phase 3: one continuous viewing block
  • Phase 4: separate viewing block
  • Phase 5 and Phase 6: one continuuous viewing block

Math:

  • (Phase 2 starting time) - (Phase 1 ending time) = 30 minutes
  • (Phase 3 starting time) - (Phase 2 ending time) = 20 minutes
  • (Phase 4 starting time) - (Phase 3 ending time) = 90 minutes
  • (Phase 5 starting time) - (Phase 4 ending time) = 150 minutes
  • (Phase 6 starting time) - (Phase 5 ending time) = 15 minutes

My failed attempt thus far:

int i = 0;
bool continueBlocking = false;

    foreach (var p in GalaxySector)  //IEnumerable
    {
        //ensure that dates are not null
        if (p.StartDatePhase != null || p.EndDatePhase != null) {

            if (continueBlocking) {
                string planetName = p.Name;
                string planetCatalogId = p.CatalogId;
                datetime? StartPhase = p.StartDatePhase.Value;
                datetime? EndPhase = p.EndDatePhase.Value;
            } else {
                string planetName = p.Name;
                string planetCatalogId = p.CatalogId;
                datetime? StartPhase = p.StartDatePhase.Value;
                datetime? EndPhase = p.EndDatePhase.Value;
            }

            if (i < 2) {
         continue;  
            }

            TimeSpan? spanBetweenSections = StartPhase - EndPhase;


    if ( spanBetweenSections.Value.TotalMinues <= 30) {
               continueBlocking = true; 
               continue;

            } else {

                CreateSchedule(planetName, planetCatalogId, StartPhase, EndPhase);
                continueBlocking = false;
            }


      }

     i++;

   }

I've spent hours on this stupid loop and I think another set of eyes would do it good.

It feels/looks too complex, too old-fashioned, and too confusing. Is there a better/modern way of doing this?

Thanks!

SkyeBoniwell
  • 6,345
  • 12
  • 81
  • 185

2 Answers2

1

Assuming that those are dates and not just times of day you could do the following

var galaxySector = new List<PlanetPhase>
{
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 8, 0, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 9, 30, 0)
    },
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 10, 0, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 11, 0, 0)
    },
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 11, 20, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 12, 30, 0)
    },
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 14, 0, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 16, 0, 0)
    },
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 18, 30, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 19, 30, 0)
    },
    new PlanetPhase
    {
        Name = "Saturn",
        StartDatePhase = new DateTime(2016, 7, 22, 19, 45, 0),
        EndDatePhase = new DateTime(2016, 7, 22, 21, 0, 0)
    },
};


PlanetPhase previous = null;
int groupon = 0;
var results = galaxySector.GroupBy(p => p.Name)
    .Select(grp => new
    {
        PlanetName = grp.Key,
        Phases = grp.OrderBy(p => p.StartDatePhase)
            .Select(p =>
            {
                if (previous != null
                    && p.StartDatePhase - previous.EndDatePhase > TimeSpan.FromMinutes(30))
                {
                    groupon++;
                }

                previous = p;

                return new
                {
                    groupOn = groupon,
                    p.StartDatePhase,
                    p.EndDatePhase
                };
            })
            .GroupBy(x => x.groupOn)
            .Select(g => new
            {
                Start = g.Min(x => x.StartDatePhase),
                End = g.Max(x => x.EndDatePhase)
            })
            .ToList()
    });

foreach (var r in results)
{
    Console.WriteLine(r.PlanetName);
    foreach (var p in r.Phases)
        Console.WriteLine($"\t{p.Start} - {p.End}");
}

That will output

Saturn

7/22/2016 8:00:00 AM - 7/22/2016 12:30:00 PM

7/22/2016 2:00:00 PM - 7/22/2016 4:00:00 PM

7/22/2016 6:30:00 PM - 7/22/2016 9:00:00 PM

Community
  • 1
  • 1
juharr
  • 31,741
  • 4
  • 58
  • 93
1

Grouping like that could be done very conveniently if you pack multiple loops into an enumerable-returning method with yield return:

private static readonly TimeSpan HalfHour = TimeSpan.Parse("0:30");

private static IEnumerable<Schedule> Group(IList<GalaxySector> all) {
    // Protect from division by zero
    if (all.Count == 0) {
        yield break;
    }
    // Find initial location
    var pos = 0;
    while (pos < all.Count) {
        var prior = (pos + all.Count - 1) % all.Count;
        if (all[prior].End+HalfHour >= all[pos].Begin) {
            pos++;
        } else {
            break;
        }
    }
    // Protect from wrap-around when all items belong to a single window
    pos = pos % all.Count;
    // Start grouping items together
    var stop = pos;
    do {
        var start = pos;
        var next = (pos+1) % all.Count;
        while (next != stop && all[pos].End+HalfHour >= all[next].Begin) {
            pos = next;
            next = (pos+1) % all.Count;
        }
        yield return new Schedule {Begin = all[start].Begin, End = all[pos].End};
        pos = next;
    } while (pos != stop);
}

The code above performs "wrap around" for midnight (demo).

The approach is relatively simple: first loop finds the location from which to start iterating by looking one step back, so that the schedule is continuous after wrap-around. The second loop remembers the starting position, and advances one step at a time, checking if the windows are closer than a half-hour apart. Once a big enough break is found, or when we reach the starting point again, the second loop stops.

If you would rather not use yield return, you could replace it with adding items to a List<Schedule>.

var all = new GalaxySector[] {
    new GalaxySector {Begin=TimeSpan.Parse("0:15"), End=TimeSpan.Parse("2:30")}
,   new GalaxySector {Begin=TimeSpan.Parse("2:45"), End=TimeSpan.Parse("3:30")}
,   new GalaxySector {Begin=TimeSpan.Parse("8:00"), End=TimeSpan.Parse("9:30")}
,   new GalaxySector {Begin=TimeSpan.Parse("10:00"), End=TimeSpan.Parse("11:00")}
,   new GalaxySector {Begin=TimeSpan.Parse("11:20"), End=TimeSpan.Parse("12:30")}
,   new GalaxySector {Begin=TimeSpan.Parse("14:00"), End=TimeSpan.Parse("16:00")}
,   new GalaxySector {Begin=TimeSpan.Parse("18:30"), End=TimeSpan.Parse("19:30")}
,   new GalaxySector {Begin=TimeSpan.Parse("19:45"), End=TimeSpan.Parse("21:00")}
,   new GalaxySector {Begin=TimeSpan.Parse("22:00"), End=TimeSpan.Parse("23:50")}
};
foreach (var sched in Group(all)) {
    Console.WriteLine("{0}..{1}", sched.Begin, sched.End);
}

Output:

08:00:00..12:30:00
14:00:00..16:00:00
18:30:00..21:00:00
22:00:00..03:30:00
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523