2

This is probably a newbie question, but here goes.

I have a method where a type DayOfWeek List that gets appended with various days of the week (Could be Wednesday & Saturday, Sunday, Monday, & Friday, etc).

Given that list, I need to compare it to a Datetime parameter, find the Week Day the DateTime parameter is closest to in the DayOfWeek list, and add days to the DateTime parameter based on what Week Day it is in the list.

For example, if the DateTime parameter being passed in is a Sunday, and my DayOfWeek list contains a Wednesday and Saturday, the parameter needs to be moved back to Saturday since it is closest in the list.

Similarly, if my list contains Sunday, Monday, and Saturday, and the parameter passed in is Thursday, then the parameter would have to be moved to Saturday.

Finally, if the parameter is equidistant from two week days in the list (Wednesday is passed in and Monday and Friday are in the list... or Sunday is passed in and Tuesday and Friday are in the list), then the parameter needs to be moved forward to the next closest week day (which, in the first case, would be Friday, and Tuesday in the second case).

It would be ideal (at least for me), to convert the distance of the next closest week day from the passed in date to an int, that way I can do something like:

passedInDate = passedInDate.AddDays(dayOfWeekDistance);
return passedInDate;

But I am open to suggestions.

I have tried LINQ statements such as:

int dayOfWeekDistance = targetDayOfWeekList.Min(x => (x - passedInDate));

But to no avail. There has to be some fancy LINQ statements that I'm missing.

Just a heads up, the main item I can't get to work is for the date to backtrack from Sunday back to Saturday if the passed in date is Sunday and the closest week day in the list is Saturday (similarly, if the passed in date is Monday and the closest week day is Friday, the date would need to traverse all the way back to Friday).

Please let me know if I missed anything or I'm just plain not making sense.

All help is welcome! Thanks.

wibby35
  • 97
  • 1
  • 1
  • 9
  • I didn't have time to test this but look at this [question](https://stackoverflow.com/questions/20056048/linq-lambda-expression-find-the-closest-day-of-week-to-current-date-now) – aaronR Sep 12 '17 at 17:22
  • You are calculating a TimeSpan which is a difference of two dates. The default time of a DateTime is midnight so the first question is what time of day are you comparing? Is it midnight or another time? – jdweng Sep 12 '17 at 17:41
  • Should be midnight. – wibby35 Sep 12 '17 at 18:01

2 Answers2

4

Let split the problem to several small parts.

NOTE: All the following methods are supposed to be put inside a class like this

public static class DayOfWeekExtensions
{
}

First, you want Sunday to be the last day of the week, while in the DayOfWeek enum it's defined first. So let make a function accounting for that:

public static int GetIndex(this DayOfWeek source)
{
    return source == DayOfWeek.Sunday ? 6 : (int)source - 1;
}

Then we need a function which calculates the distance (offset) between two DayOfWeek values:

public static int OffsetTo(this DayOfWeek source, DayOfWeek target)
{
    return source.GetIndex() - target.GetIndex();
}

Let also add a function which given a pivot and two DayOfWeek values selects the closest value of the two (applying your forward priority rule):

public static DayOfWeek Closest(this DayOfWeek pivot, DayOfWeek first, DayOfWeek second)
{
    int comp = Math.Abs(first.OffsetTo(pivot)).CompareTo(Math.Abs(second.OffsetTo(pivot)));
    return comp < 0 || (comp == 0 && first.GetIndex() > pivot.GetIndex()) ? first : second;
}

Now we are ready to implement the method which finds the closest day from a sequence. It can be implemented in many ways, here is the implementation using (finally! :) LINQ Aggregate method:

public static DayOfWeek? Closest(this IEnumerable<DayOfWeek> source, DayOfWeek target)
{
    if (!source.Any()) return null;
    return source.Aggregate((first, second) => target.Closest(first, second));
}

Finally, let add a function which calculates the closest distance:

public static int ClosestDistance(this IEnumerable<DayOfWeek> source, DayOfWeek target)
{
    return source.Closest(target)?.OffsetTo(target) ?? 0;
}

And we are done. We just created a small simple reusable utility class.

The usage in your case would be:

int dayOfWeekDistance = targetDayOfWeekList.ClosestDistance(passedInDate.DayOfWeek);

UPDATE: It turns out that your requirement is different.

Applying the same principle, first we need a function which calculates the minimum of the forward and backward distance between two days of the week, applying the forward precedence rule.

public static int MinDistanceTo(this DayOfWeek from, DayOfWeek to)
{
    int dist = to - from;
    return dist >= 4 ? dist - 7 : dist <= -4 ? dist + 7 : dist;
}

What it does basically is to convert the value from the possible -6..6 inclusive range to the value in the -3..3 inclusive range.

Then we'll need just one more function, which will implement the method in question by using Select + Aggregate (it can also be implemented with Min and custom comparer). It basically compares two absolute distances and again applies the forward priority rule:

public static int MinDistanceTo(this DayOfWeek from, IEnumerable<DayOfWeek> to)
{
    if (!to.Any()) return 0;
    return to.Select(x => from.MinDistanceTo(x)).Aggregate((dist1, dist2) =>
    {
        if (dist1 == dist2) return dist1;
        int comp = Math.Abs(dist1).CompareTo(Math.Abs(dist2));
        return comp < 0 || (comp == 0 && dist1 > 0) ? dist1 : dist2;
    });
}

And the usage will be:

int dayOfWeekDistance = passedInDate.DayOfWeek.MinDistanceTo(targetDayOfWeekList);
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • This worked for the most part! I do have one question. I'm not sure if I want Sunday to be the last day of the week, I just need it so that if the passed in date is Monday, and the closest weekday is Saturday (or Friday),, the date can effectively traverse back to Saturday (or Friday)... if that makes sense. For example, Can you modify your answer to account for if the list contains Tuesday & Friday and the passed in date is Sunday that the date be bumped to Tuesday (to account for the equidistant forward rule)? That is my only critique. Otherwise it works perfectly!! – wibby35 Sep 12 '17 at 20:43
  • Excellently explained! – Yair Halberstadt Sep 12 '17 at 21:49
  • @wibby35 You are welcome. I've revisit and updated the answer according to your comment. Unfortunately it required totally different approach than the initial thought, so at the end it became closer (if not equal, just different implementation/coding style) to the other answer posted meanwhile by NetMage. I didn't test the other answer, but in case it works, I think for the sake of correctness you should accept it since it first gets to the right direction. – Ivan Stoev Sep 13 '17 at 19:36
  • @IvanStoev I appreciate you making the modifications! I didn't get around to testing the final changes, but I have been using the answer provided by NetMage and it seems to work well. Thank you for the help though! – wibby35 Sep 13 '17 at 20:27
  • @IvanStoev or anyone else, this is a complete shot in the dark, but are you any good with T-SQL? Do you know how to implement this exact same situation in T-SQL? Basically I have a bunch of stored procedures that have a DateTime variable that need to be set to the closest weekday based on what is contained within the list. So it's basically just the same situation but on the SQL side. – wibby35 Sep 18 '17 at 18:09
  • @wibby35 Unfortunately recently (read - last decade) I'm playing exclusively with C# and ORMs, so I'm afraid I could be in a little if no help. I would suggest you asking a new question under `t-sql` tag (AFAIK there are many quite good SO experts in that area). – Ivan Stoev Sep 18 '17 at 18:22
  • @IvanStoev no worries, I thought I'd try. Thanks anyways! – wibby35 Sep 18 '17 at 18:49
1

With a helper function, LINQ can be used.

The helper function computes the closest day of week using a utility function to compute the number of forward days between the two DOWs:

public int MinDOWDistance(DayOfWeek dow1, DayOfWeek dow2) {
    int FwdDaysDiff(int idow1, int idow2) => idow2 - idow1 + ((idow1 > idow2) ? 7 : 0);
    int fwd12 = FwdDaysDiff((int)dow1, (int)dow2);
    int fwd21 = FwdDaysDiff((int)dow2, (int)dow1);
    return fwd12 < fwd21 ? fwd12 : -fwd21;
}

Then you can find the nearest DOW in the list and return the right number of days to move (and direction) using Aggregate with LINQ:

public int DaysToClosestDOW(DayOfWeek dow1, List<DayOfWeek> dowList) {
    return dowList.Select(dow => {
                                    var cdow = MinDOWDistance(dow1, dow);
                                    return new { dow, dist = cdow, absdist = Math.Abs(cdow) };
                                 })
                  .Aggregate((g1, g2) => (g1.absdist < g2.absdist) ? g1 : ((g1.absdist == g2.absdist) ? ((g1.dist > 0) ? g1 : g2) : g2)).dist;
}

It occurred to me I could use a tuple to return the absdist from the helper function since it already knows it. Then I can just use the Tuple in the LINQ:

public (int dist, int absdist) MinDOWDistance(DayOfWeek dow1, DayOfWeek dow2) {
    int FwdDaysDiff(int idow1, int idow2) => idow2 - idow1 + ((idow1 > idow2) ? 7 : 0);
    int fwd12 = FwdDaysDiff((int)dow1, (int)dow2);
    int fwd21 = FwdDaysDiff((int)dow2, (int)dow1);
    if (fwd12 < fwd21)
        return (fwd12, fwd12);
    else
        return (-fwd21, fwd21);
}

public int DaysToClosestDOW(DayOfWeek dow1, List<DayOfWeek> dowList) {
    return dowList.Select(dow => MinDOWDistance(dow1, dow))
                  .Aggregate((g1, g2) => (g1.absdist < g2.absdist) ? g1 : ((g1.absdist == g2.absdist) ? ((g1.dist > 0) ? g1 : g2) : g2)).dist;
}
NetMage
  • 26,163
  • 3
  • 34
  • 55