Page 1 of 1

How to calculate the first date of a pay period?

Posted: Mon Apr 09, 2007 1:24 pm
by ahz
Everah | Please use

Code: Select all

,

Code: Select all

and [syntax="..."] tags where appropriate when posting code. Your post has been edited to reflect how we'd like it posted. Please read:  [url=http://forums.devnetwork.net/viewtopic.php?t=21171]Posting Code in the Forums[/url] to learn how to do it too.[/color]


Hi,

I'm coding a GPL time clock, but I'm fuzzing on how to (reliably) calculate the first date of the pay period.   Below is my messy attempt including a simple unit test.

Code: Select all

<?php

//
// CONFIGURATION
//

// The first date of of any pay period in a format understandable to strtotime().
// See http://us.php.net/manual/en/function.strtotime.php for details about format.
$pay_period_begins = "4 April 2007";

// The length of the pay period in days.
$pay_period_days = 14;


//
// CODE
// 

/**
 * Calculate the starting date of the current pay period.
 *
 * @return: unix time of first date of current pay period
 */
function timeclock_get_current_pay_period($now = FALSE)
{
//fixme: ugly code
	global $pay_period_begins;
	global $pay_period_days;

	date_default_timezone_set("GMT");

	if (FALSE == $now)
		$now = time();

	$ppb_unix = strtotime($pay_period_begins);

	assert(is_int($now));
	assert(is_int($pay_period_days));
	assert($pay_period_days > 1);
	assert(is_string($pay_period_begins));
	assert(is_int($ppb_unix));

	$now_unix = strtotime(gmdate("Y-M-d", $now)); // ignore time of day

	$diff_days = ($now_unix - $ppb_unix)/(24*60*60);
	if ($diff_days >= 0)
	{
		// pay_period_begins is in the past
		return  (floor($diff_days/ $pay_period_days)*24*60*60*$pay_period_days) + $ppb_unix;
	}
	else
	{
		// pay_period_begins is in the future
		//fixme!!
		echo "past\n";
		$x = (floor(abs($diff_days)/$pay_period_days));
		$pay_period_begins = gmdate("Y-M-d", $ppb_unix - (($x++*24*60*60) * $pay_period_days));
		echo "new ppb = ". gmdate("D Y-M-d", strtotime($pay_period_begins)). "\n";
		return timeclock_get_current_pay_period($now);


		$x2 = ($x+1) * 60 * 24 * 60 * $pay_period_days;
		$x3 =  $ppb_unix - $x2;
		return $x3;
	}
}

//
// UNIT TESTS
//

function test_same_dates($x, $y)

{
	return date("Y-M-d", $x) == date("Y-M-d", $y);
}

function test_timeclock_get_current_pay_period_helper($in, $out)
{
	assert(is_int($in));
	assert(is_int($out));

	$r = timeclock_get_current_pay_period($in);

	$in_s =  date("D Y-M-d", $in);
	$out_s =  date("D Y-M-d", $out);
	$r_s =  date("D Y-M-d", $r);
	$msg = "Today's date $in_s,  expecting $out_s, actual $r_s\n";
	if (!test_same_dates($out, $r))
		die("FAIL: $msg");
	else
		print("PASS: $msg");
}

function test_timeclock_get_current_pay_period()
{
	global $pay_period_begins;
	global $pay_period_days;

	$pay_period_days = 14;
	$pay_period_begins = "4 April 2007";


	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-20'), strtotime('2007-03-07'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-21'), strtotime('2007-03-21'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-22'), strtotime('2007-03-21'));

	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-03'), strtotime('2007-03-21'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-04'), strtotime('2007-04-04'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-05'), strtotime('2007-04-04'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-09'), strtotime('2007-04-04'));


	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-17'), strtotime('2007-04-04'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-18'), strtotime('2007-04-18'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-19'), strtotime('2007-04-18'));
}		
	
test_timeclock_get_current_pay_period();	

echo "ALL PASS!\n";


?>

Everah | Please use

Code: Select all

,

Code: Select all

and [syntax="..."] tags where appropriate when posting code. Your post has been edited to reflect how we'd like it posted. Please read:  [url=http://forums.devnetwork.net/viewtopic.php?t=21171]Posting Code in the Forums[/url] to learn how to do it too.[/color]

Posted: Mon Apr 09, 2007 1:47 pm
by Weirdan
You might want to convert your dates to julian day count using gregoriantojd for simplier calculations.

Posted: Mon Apr 09, 2007 2:03 pm
by ahz
Weirdan wrote:You might want to convert your dates to julian day count using gregoriantojd for simplier calculations.
"Calendar Functions" seems to be an extension not available on all platforms, but I'd like my web app to run anywhere.

Posted: Mon Apr 09, 2007 2:28 pm
by Weirdan
The conversion algorythms are quite simple and can be found in wikipedia. And working with a simple integers instead of these horrible day-month-year tuples would cut your debugging time to a minimum.

Posted: Mon Apr 09, 2007 2:47 pm
by feyd
You may be interested in the class I posted recently in Theory and Design.

viewtopic.php?t=65853

Posted: Mon Apr 09, 2007 5:15 pm
by RobertGonzalez
I am not exactly sure how you do it, but somehow, everyday, you amaze me more and more feyd.

Posted: Tue Apr 10, 2007 3:32 am
by onion2k
How are you defining the pay period? All I can see is a single variable. What happens if someone is paid monthly for example?

Posted: Tue Apr 10, 2007 10:23 am
by ahz
onion2k wrote:How are you defining the pay period? All I can see is a single variable. What happens if someone is paid monthly for example?
Uh oh. :) Thanks for catching that. I assume 14 days is most typical, so I still need to figure out this question, but after that, I will plan to do monthly, quarterly, etc. (The software also allows running reports on a manual set of dates, so it's not a big deal for monthly, but for 14 days, it's a time saver.)

At this point, it seems the best thing to do is to convert to JDN or JD and focus really hard on the math of the algorithm. Unfortunately, sometimes math hurts my head, and that's why I dropped my last math class...

Posted: Tue Apr 10, 2007 3:38 pm
by feyd
Then you may benefit from the class I linked to already.

Posted: Tue Apr 10, 2007 3:52 pm
by ahz
feyd wrote:Then you may benefit from the class I linked to already.
I looked. Please excuse my oversight, but I don't see how it helps.

BTW, I came up with my own serial date system. I'm still working out the rest. :?

Code: Select all

/**
 * Calculate UNIX "days" (an ad hoc concept), which is a
 * serial date like JDN.
 *
 * @param seconds integer UNIX time in seconds
 * @return integer UNIX "days"
 **/
function us2days($seconds)
{
        assert(is_int($seconds));
        assert($seconds>0);
        return intval($seconds/(24*60*60));
}

Posted: Tue Apr 10, 2007 4:26 pm
by ahz
us2days() turned out bad in practice for calculating the difference of days, so here's tested progress on calculating the difference of days...

Code: Select all

/**
 * For any given day in UNIX time, returns the same day at 12:00pm in UNIX time.
 */
function unixnoon($t0)
{
        $m = intval(date('m', $t0));
        $d = intval(date('d', $t0));
        $y = intval(date('Y', $t0));
        $t1 = mktime(12, 00, 00, $m, $d, $y);
        return $t1;
}

/**
 * Calculate the number of calendar days between two
 * UNIX times.  Some of the complexity include leap
 * times and daylight savings.
 *
 * @return integer Number of days (positive of negative)
 */
function diff_days($ux1, $ux2)
{
        return round((unixnoon($ux1) - unixnoon($ux2))/(24*60*60));
}

function test_diff_days_helper($x, $y, $expected)
{
        $r = diff_days($x, $y);

        $xs =  date(DATE_RFC822, $x);
        $ys =  date(DATE_RFC822, $y);

        if ($r != $expected)
                printf("FAIL: diff_days($xs, $ys) = $r but expected $expected\n");

}

/**
 * Test diff_days()
 */ 
function test_diff_days()
{
        $old_tz = date_default_timezone_get();
        date_default_timezone_set('America/Denver');

        test_diff_days_helper(strtotime("12:00am"), strtotime("11:59pm"), 0);

        for ($x = 0; $x < 500; $x++)
        {
                $d = date("Y-M-d", mktime(0, 0, 0, 1, $x, 1999));
                $t0 = strtotime("$d 12:00am");
                $t1 = strtotime("$d 11:59pm");
                test_diff_days_helper($t0-1, $t1, -1);
                test_diff_days_helper($t0, $t1, 0);
                test_diff_days_helper($t0, $t1+60, -1);
        }

        date_default_timezone_set($old_tz);
}

Posted: Wed Apr 11, 2007 10:48 am
by ahz
OK. My goal was a short, elegant solution that passes the unit tests. I think I have accomplished the goal. Thank you everyone for your help, and per onion2k's suggestion, I will look into implementing pay periods of other definitions (e.g. months).

Code: Select all

<?php

//
// Copyright (c) 2007 [url=http://springsrescuemission.org]Springs Rescue Mission[/url]
// Licensed under the [url=http://www.gnu.org/copyleft/gpl.html]GNU General Public License[/url] version 2 or greater
//
//


//
// CONFIGURATION
//

// The first date of of any pay period in a format understandable to strtotime().
// See http://us.php.net/manual/en/function.strtotime.php for details about format.
$pay_period_begins = "4 April 2007";

// The length of the pay period in days.
//fixme: pay periods should be definable as months
$pay_period_days = 14;


//
// CODE
//

/**
 * For any given day in UNIX time, returns the same day at 12:00pm in UNIX time.
 */
function unixnoon($t0)
{
//	var_dump($t0);
	assert(is_int($t0));
	assert($t0 > 0);
	$m = intval(date('m', $t0));
	$d = intval(date('d', $t0));
	$y = intval(date('Y', $t0));
	$t1 = mktime(12, 00, 00, $m, $d, $y);
	return $t1;
}

/**
 * Calculate the number of calendar days between two
 * UNIX times.  Some of the complexity include leap
 * times and daylight savings.
 *
 * @return integer Number of days (positive of negative)
 */
function diff_days($ux1, $ux2)
{
	return round((unixnoon($ux1) - unixnoon($ux2))/(24*60*60));
}


/**
 * Calculate the starting date of the current pay period.
 *
 * @param int now current UNIX time (optional)
 * @return: UNIX time of first date of current pay period precise to the calendar day
 */
function timeclock_get_current_pay_period($now = FALSE)
{
	global $pay_period_begins;
	global $pay_period_days;

	if (FALSE == $now)
		$now = time();

	$ppb_unix = strtotime($pay_period_begins);

	assert(is_int($now));
	assert(is_int($pay_period_days));
	assert($pay_period_days > 1);
	assert(is_string($pay_period_begins));
	assert(is_int($ppb_unix));

	$diff_days = diff_days($now, $ppb_unix);
	$x = floor($diff_days/ $pay_period_days);
	$x2 = unixnoon($ppb_unix) + ($x*$pay_period_days*24*60*60);
	return $x2;
}

//
// UNIT TESTS
//
 
function test_same_dates($x, $y)

{
	return date("Y-M-d", $x) == date("Y-M-d", $y);
}

/**
 * @param in integer "current time" in UNIX time
 * @param out integer expected output
 */
function test_timeclock_get_current_pay_period_helper($in, $out)
{
	global $pay_period_begins;
	assert(is_int($in));
	assert(is_int($out));

	$r = timeclock_get_current_pay_period($in);

	$in_s =  date("D Y-M-d", $in);
	$out_s =  date("D Y-M-d", $out);
	$r_s =  date("D Y-M-d", $r);
	$diff = diff_days($in, strtotime($pay_period_begins));
	$msg = "Today's date $in_s,  expecting $out_s, actual $r_s, diff_days=$diff\n";
	if (!test_same_dates($out, $r))
		die("FAIL: $msg");
	else
		print("PASS: $msg");
}

function test_timeclock_get_current_pay_period()
{
	global $pay_period_begins;
	global $pay_period_days;

	$pay_period_days = 14;
	$pay_period_begins = "4 April 2007";


	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-20'), strtotime('2007-03-07'));

	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-21'), strtotime('2007-03-21'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-03-22'), strtotime('2007-03-21'));

	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-03'), strtotime('2007-03-21'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-04'), strtotime('2007-04-04'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-05'), strtotime('2007-04-04'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-09'), strtotime('2007-04-04'));


	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-18'), strtotime('2007-04-18'));
	test_timeclock_get_current_pay_period_helper(strtotime('2007-04-19'), strtotime('2007-04-18'));

}

function test_diff_days_helper($x, $y, $expected)
{
	$r = diff_days($x, $y);

	$xs =  date(DATE_RFC822, $x);
	$ys =  date(DATE_RFC822, $y);

	if ($r != $expected)
		die("FAIL: diff_days($xs, $ys) = $r but expected $expected\n");

}

/**
 * Test diff_days()
 */ 
function test_diff_days()
{
	$old_tz = date_default_timezone_get();
	date_default_timezone_set('America/Denver');

	test_diff_days_helper(strtotime("2007-03-21"), strtotime("2007-04-04"), -14);
	test_diff_days_helper(strtotime("12:00am"), strtotime("11:59pm"), 0);

	for ($x = 0; $x < 1000; $x++)
	{
		$d = date("Y-M-d", mktime(0, 0, 0, 1, $x, 1999));
		$t0 = strtotime("$d 12:00am");
		$t1 = strtotime("$d 11:59pm");
		test_diff_days_helper($t0-1, $t1, -1);
		test_diff_days_helper($t0, $t1, 0);
		test_diff_days_helper($t0, $t1+60, -1);
	}

	date_default_timezone_set($old_tz);
}

test_diff_days();	
test_timeclock_get_current_pay_period();	

echo "ALL PASS!\n";

?>
Update: Fixed at 9:52AM. ;)

Posted: Wed Apr 11, 2007 11:18 am
by onion2k
ahz wrote:onion2k's suggestion, I will look into implementing pay periods of other definitions (e.g. months).
In my time I've been paid:

Weekly
Fortnightly
On the last day of the month
On the last Tuesday of the month
Five days prior to the end of the month except in December when I was paid on the 19th.

So I guess you'll need to support all of those.

Posted: Wed Apr 11, 2007 11:29 am
by ahz
The problem with some of those pay period definitions is they cannot align to a work week (i.e. the pay period is not an integer multiple of the work week), which confuses the overtime pay issues (at least here in the USA for non-exempt employees). I will try to get the common ones, and the rest can be supported through manual date selection (e.g. April 1 through April 27).

Thanks again :)