views:

1447

answers:

4

Given a date range how to calculate the number of weekends partially or wholly within that range?

(A few definitions as requested: take 'weekend' to mean Saturday and Sunday. The date range is inclusive i.e. the end date is part of the range 'wholly or partially' means that any part of the weekend falling within the date range means the whole weekend is counted.)

To simplify I imagine you only actually need to know the duration and what day of the week the initial day is...

I darn well now it's going to involve doing integer division by 7 and some logic to add 1 depending on the remainder but I can't quite work out what...

extra points for answers in Python ;-)

Edit

Here's my final code.

Weekends are Friday and Saturday (as we are counting nights stayed) and days are 0-indexed starting from Monday. I used onebyone's algorithm and Tom's code layout. Thanks a lot folks.

def calc_weekends(start_day, duration):
    days_until_weekend = [5, 4, 3, 2, 1, 1, 6]
    adjusted_duration = duration - days_until_weekend[start_day]
    if adjusted_duration < 0:
        weekends = 0
    else:
        weekends = (adjusted_duration/7)+1
    if start_day == 5 and duration % 7 == 0: #Saturday to Saturday is an exception
        weekends += 1
    return weekends

if __name__ == "__main__":
    days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    for start_day in range(0,7):
        for duration in range(1,16):
            print "%s to %s (%s days): %s weekends" % (days[start_day], days[(start_day+duration) % 7], duration, calc_weekends(start_day, duration))
        print
A: 

You would need external logic beside raw math. You need to have a calendar library (or if you have a decent amount of time implement it yourself) to define what a weekend, what day of the week you start on, end on, etc.

Take a look at Python's calendar class.

Without a logical definition of days in your code, a pure mathematical methods would fail on corner case, like a interval of 1 day or, I believe, anything lower then a full week (or lower then 6 days if you allowed partials).

James McMahon
I don't think you do. We could recast the problem with integers. Given an integer range - how many 'strips' of 2 integers at intervals of 7 are wholly or partially in that range...
andybak
A calendar library is not necessary, but it would probably be very helpful. If the goal is to solve this problem quickly, one would probably use the 'calendar' or 'datetime' class. If this is just an intellectual exercise, then by all means, knock yourself out with integer arithmetic. ;-)
las3rjock
@andybak, well as a counter point, consider the case where you have 2 days, Saturday and Sunday, as opposed to say, Monday, Tuesday. Or to go even further, say you had one day, Saturday. I think any raw integer method is going to fail there. Your code is going to need some clear logical idea of what days of the week there are and what constitues a weekend.
James McMahon
@andybak, The downvote should be undoable. Unless they decreased the amount of time you have to undo it. Let me edit my question and then you should be able to undo the vote, if you so desire.
James McMahon
Sorry. Still not seeing any use for a calendar library in anything other than a very peripheral sense. The nugget of the problem would be the same without dates as the chosen answer shows.
andybak
The chosen answer does define days. Wither you use a built in library or roll your own you still can't just take a number divide it to intervals of 7 to get your answer. Your code has to have some concept of the days it is representing.
James McMahon
Yes but that needs a library only in the trivial sense of deciding on a mapping of days>integers. That's hardly the point of the question. Hence recasting the question to refer only to integers keeps the core problem and removes the concept of dates entirely.
andybak
I misread your question then Andy, I was under the impression that you were pursing a purely mathematical approach.
James McMahon
+5  A: 

General approach for this kind of thing:

For each day of the week, figure out how many days are required before a period starting on that day "contains a weekend". For instance, if "contains a weekend" means "contains both the Saturday and the Sunday", then we have the following table:

Sunday: 8 Monday: 7 Tuesday: 6 Wednesday: 5 Thursday: 4 Friday: 3 Saturday: 2

For "partially or wholly", we have:

Sunday: 1 Monday: 6 Tuesday: 5 Wednesday: 4 Thursday: 3 Friday: 2 Saturday: 1

Obviously this doesn't have to be coded as a table, now that it's obvious what it looks like.

Then, given the day-of-week of the start of your period, subtract[*] the magic value from the length of the period in days (probably start-end+1, to include both fenceposts). If the result is less than 0, it contains 0 weekends. If it is equal to or greater than 0, then it contains (at least) 1 weekend.

Then you have to deal with the remaining days. In the first case this is easy, one extra weekend per full 7 days. This is also true in the second case for every starting day except Sunday, which only requires 6 more days to include another weekend. So in the second case for periods starting on Sunday you could count 1 weekend at the start of the period, then subtract 1 from the length and recalculate from Monday.

More generally, what's happening here for "whole or part" weekends is that we're checking to see whether we start midway through the interesting bit (the "weekend"). If so, we can either:

  • 1) Count one, move the start date to the end of the interesting bit, and recalculate.
  • 2) Move the start date back to the beginning of the interesting bit, and recalculate.

In the case of weekends, there's only one special case which starts midway, so (1) looks good. But if you were getting the date as a date+time in seconds rather than day, or if you were interested in 5-day working weeks rather than 2-day weekends, then (2) might be simpler to understand.

[*] Unless you're using unsigned types, of course.

Steve Jessop
Brilliant. Thanks for solving the specific case and explaining the general case. Top answer.
andybak
+1  A: 

To count whole weekends, just adjust the number of days so that you start on a Monday, then divide by seven. (Note that if the start day is a weekday, add days to move to the previous Monday, and if it is on a weekend, subtract days to move to the next Monday since you already missed this weekend.)

days = {"Saturday":-2, "Sunday":-1, "Monday":0, "Tuesday":1, "Wednesday":2, "Thursday":3, "Friday":4}

def n_full_weekends(n_days, start_day):
    n_days += days[start_day]
    if n_days <= 0:
        n_weekends = 0
    else:
        n_weekends = n_days//7
    return n_weekends

if __name__ == "__main__":
    tests = [("Tuesday", 10, 1), ("Monday", 7, 1), ("Wednesday", 21, 3), ("Saturday", 1, 0), ("Friday", 1, 0),
    ("Friday", 3, 1), ("Wednesday", 3, 0), ("Sunday", 8, 1), ("Sunday", 21, 2)]
    for start_day, n_days, expected in tests:
        print start_day, n_days, expected, n_full_weekends(n_days, start_day)

If you want to know partial weekends (or weeks), just look at the fractional part of the division by seven.

tom10
+1  A: 

My general approach for this sort of thing: don't start messing around trying to reimplement your own date logic - it's hard, ie. you'll screw it up for the edge cases and look bad. Hint: if you have mod 7 arithmetic anywhere in your program, or are treating dates as integers anywhere in your program: you fail. If I saw the "accepted solution" anywhere in (or even near) my codebase, someone would need to start over. It beggars the imagination that anyone who considers themselves a programmer would vote that answer up.

Instead, use the built in date/time logic that comes with Python:

First, get a list of all of the days that you're interested in:

from datetime import date, timedelta    
FRI = 5; SAT = 6

# a couple of random test dates
now = date.today()
start_date = now - timedelta(57)
end_date = now - timedelta(13)
print start_date, '...', end_date    # debug

days = [date.fromordinal(d) for d in  
            range( start_date.toordinal(),
                   end_date.toordinal()+1 )]

Next, filter down to just the days which are weekends. In your case you're interested in Friday and Saturday nights, which are 5 and 6. (Notice how I'm not trying to roll this part into the previous list comprehension, since that'd be hard to verify as correct).

weekend_days = [d for d in days if d.weekday() in (FRI,SAT)]

for day in weekend_days:      # debug
    print day, day.weekday()  # debug

Finally, you want to figure out how many weekends are in your list. This is the tricky part, but there are really only four cases to consider, one for each end for either Friday or Saturday. Concrete examples help make it clearer, plus this is really the sort of thing you want documented in your code:

num_weekends = len(weekend_days) // 2

# if we start on Friday and end on Saturday we're ok,
# otherwise add one weekend
#  
# F,S|F,S|F,S   ==3 and 3we, +0
# F,S|F,S|F     ==2 but 3we, +1
# S|F,S|F,S     ==2 but 3we, +1
# S|F,S|F       ==2 but 3we, +1

ends = (weekend_days[0].weekday(), weekend_days[-1].weekday())
if ends != (FRI, SAT):
    num_weekends += 1

print num_weekends    # your answer

Shorter, clearer and easier to understand means that you can have more confidence in your code, and can get on with more interesting problems.

Anthony Briggs