views:

158

answers:

5

Updated to remove extraneous text and ambiguity.

The Rules:
An employee accrues 8 hours of Paid Time Off on the day after each quarter. Quarters, specifically being:

  • Jan 1 - Mar 31
  • Apr 1 - Jun 30
  • Jul 1 - Sep 30
  • Oct 1 - Dec 31

The Problem
Using python, I need to define the guts of the following function:

def acrued_hours_between(start_date, end_date): 
    # stuff
    return integer

I'm currently using Python, and wondering what the correct approach to something like this would be.

I'm assuming that using DateTime objects, and possibly the dateutil module, would help here, but my brain isn't wrapping around this problem for some reason.

Update
I imagine the calculation being somewhat simple, as the problem is:

"How many hours of Paid Time Off are accrued from start_date to end_date?" given the above "rules".
+1  A: 

I would sort all the events for a particular employee in time order and simulate the events in that order checking that the available days of paid time off never falls below zero. A paid time off request is an event with a value -(number of hours). Jan 1st has an event with value +8 hours.

Every time a modification is made to the data, run the simulation again from the start.

The advantage of this method is that it will detect situations in which a new event is valid at that time but causes the number of free days to drop such that a later event which previously was valid now becomes invalid.

This could be optimized by storing intermediate results in a cache but since you will likely only have a few hundred events per employee this optimization probably won't be necessary.

Mark Byers
The particular 'event' and calculation can be done one-off, as they are requested/submitted one at a time, by the employee in question.
anonymous coward
@anonymous coward: Yes. I would suggest rerunning the calculation for every modification. This requires more CPU cycles than strictly necessary but as long as you complete the calculation faster than people can modify the data it shouldn't be an issue. For a few hundred events the calculation will be close to instant.
Mark Byers
+1 for simple method that accounts for possibility of two requests which were both valid when they were made, but the creation of the second invalidated the first.
BlueRaja - Danny Pflughoeft
@Mark - I made a statement to agree with you. Wasn't asking a question. It was a failed attempt to clarify the point that the problem here isn't events that exist, or occurrences that might suffer from race conditions, or that previously entered events would invalidate others - just that "yes, these are calculated as they're entered". Sorry about the confusion.
anonymous coward
A: 

I would count all free days before the date in question, then subtract the number of used days before then in order to come to a value for the maximum number of allowable days.

Ignacio Vazquez-Abrams
I'm sorry if I'm unclear, but I'm attempting to find the number of accrued hours between "now" and a future date, given the rules of "8 hours per quarter, as defined".
anonymous coward
So then do the calculation in hours instead of days.
Ignacio Vazquez-Abrams
A: 

Set up a tuple for each date range (we'll call them quarters). In the tuple store the quarter (as a cardinal index, or as a begin date), the maximum accrued hours for a quarter, and the number of used hours in a quarter. You'll want to have a set of tuples that are sorted for this to work, so a plain list probably isn't your best option. A dictionary might be a better way to approach this, with the quarter as the key and the max/used entries returned in the tuple, as it can be "sorted".

(Note: I looked at the original explanation and rewrote my answer)

Get a copy of the set of all quarters for a given employee, sorted by the quarter's date. Iterate over each quarter summing the difference between the maximum per-quarter allotment of vacation time and the time "spent" on that quarter until you reach the quarter that the request date falls into. This gives accumulated time.

If accumulated time plus the time alloted for the requested quarter is not as much as the requested hours, fail immediately and reject the request. Otherwise, continue iterating up to the quarter of your quest.

If there is sufficient accumulated time, continue iterating over the copied set, computing the new available times on a per-quarter basis, starting with the left-over time from your initial calculation.

If any quarter has a computed time falling below zero, fail immediately and reject the request. Otherwise, continue until you run out of quarters.

If all quarters are computed, update the original set of data with the copy and grant the request.

Avery Payne
+1  A: 

This can be done with plain old integer math:

from datetime import date

def hours_accrued(start, end):
    '''hours_accrued(date, date) -> int

    Answers the question "How many hours of Paid Time Off
      are accrued from X-date to Y-date?"

    >>> hours_accrued(date(2010, 4, 20), date(2012, 12, 21))
    80
    >>> hours_accrued(date(2010, 12, 21), date(2012, 4, 20))
    48
    '''
    return ( 4*(end.year - start.year)
        + ((end.month-1)/3 - (start.month-1)/3) ) * 8
intuited
+5  A: 

The OP's edit mentions the real underlying problem is:

"How many hours of Paid Time Off are accrued from X-date to Y-date?"

I agree, and I'd compute that in the most direct and straightforward way, e.g.:

import datetime
import itertools

accrual_months_days = (1,1), (4,1), (7,1), (10,1)

def accruals(begin_date, end_date, hours_per=8):
  """Vacation accrued between begin_date and end_date included."""
  cur_year = begin_date.year - 1
  result = 0
  for m, d in itertools.cycle(accrual_months_days):
    if m == 1: cur_year += 1
    d = datetime.date(cur_year, m, d)
    if d < begin_date: continue
    if d > end_date: return result
    result += hours_per

if __name__ == '__main__':  # examples
  print accruals(datetime.date(2010, 1, 12), datetime.date(2010, 9, 20))
  print accruals(datetime.date(2010, 4, 20), datetime.date(2012, 12, 21))
  print accruals(datetime.date(2010, 12, 21), datetime.date(2012, 4, 20))

A direct formula would of course be faster, but could be tricky to do it without bugs -- if nothing else, this "correct by inspection" example can serve to calibrate the faster one automatically, by checking that they agree over a large sample of date pairs (be sure to include in the latter all corner cases such as first and last days of quarters of course).

Alex Martelli
Alex, thanks so much for this highly configurable approach. I believe this is exactly what I'm looking for. You've also made me realize that I'm not familiar with `itertools`, as I probably should be, so I'm headed to the docs to clear that up.
anonymous coward
@anonymous, great -- itertools is really a precious collection of tools (not only are they often handy, but they're very highly optimized, which increases the motivation to learn them [[even though you're not always going to need the extra speed, e.g. in this example I doubt it would be noticeable given all else that's going on;-), _some_times it will come in handy!]]).
Alex Martelli
Alex, I have a question about this code... if I give the function the same date as the start/end date, for instance `datetime.date(2010, 1, 1)` as both start/end, it gives `8` as if a quarter has gone by. I can't simply remove the `-1` from the `cur_year` definition... what's going on there?
anonymous coward
@anon, what's going on is that you said 8 hours are accrued exactly on the day that starts the quarter (well you said "one day after the end of the quarter" but that's the same as the start of the next!-) and my function goes from start **included** to end **included**, so starting on the first day of the quarter means 8 hours are accrued that very day -- following exactly the requirements in your specs. If your original specs were incorrect it's easy to tweak the code accordingly (add or subtract a day here or there, or use >= instead of >, etc, etc) but I'm not going to try to guess!-)
Alex Martelli
@anon: because you said 8 hours accrued each quarter-day; you didn't say the employee had to have worked any qualification period. Also you didn't specify when on the quarter-day the 8-hours accrued, early morning or late at night -- can the employee take 8 hours off on 1 April or do they need to wait until the next day?
John Machin
I see. That makes perfect sense. Thanks.
anonymous coward
@anon: assuming that an employee needs to work for a whole quarter to qualify for 8 hours leave, how do you handle a case like April 1 is a Saturday, April 3 is a public holiday, and the employee starts work on April 4?
John Machin
@John Thankfully, this particular calculation doesn't have to be that granular, as it's not *actually* in charge of *real* employees' time off. Thanks for your concern, tho! I appreciate it.
anonymous coward
@anon: your remark "as if a quarter has gone by" indicates that a whole quarter has to be served to qualify which is NOT compatible with what you said and what Alex's answer implements and it's NOT just fixable by twiddling +/- one day. *PLEASE CLARIFY* what accrues e.g. on 1 April to a worker who started employment 1 month ago on 1 March -- 0 hours, 8 hours, or something else?
John Machin
@John I suppose the phrase I should have used is something a bit more like "as if the day marking the end of a Quarter has passed". This method of calculation is perfectly acceptable, but I misread how the code worked, and didn't initially expect "Jan1-Jan1" to accumulate anything.
anonymous coward