Link to home
Start Free TrialLog in
Avatar of Richard Korts
Richard KortsFlag for United States of America

asked on

Impossible arithmetic

See this code:
<?php
// email_reminders.php
// overnight process to remind subscribers
include "db_connect.php";
$qry = "SELECT * from subscribers where subscribe_type = 'YR' or subscribe_type = '90'";
$res = mysqli_query($link, $qry);
$ns = mysqli_num_rows($res);
$thirty = 30 * 86400;
$fifteen = 15 * 86400;
$ten = 864000;
$five = 5 * 86400;
$two = 2 * 86400;
$one = 86400;
echo "mo = " . date("n") . ", day = " . date('j') . ", year = " . date("Y") . "<br>";
$today_secs = mktime (0, 0, 0, date("n"), date("j"), date("Y"));
if ($ns != 0) {
	for ($i = 0; $i < $ns; $i++) {
		$s = mysqli_fetch_array($res,MYSQLI_ASSOC);
		$exdt = $s['exp_date'];
		echo "exp date = " . $exdt . "<br>";
		echo "mo exp date = " . substr($exdt,5,2) . "<br>";
		echo "day exp date = " . substr($exdt,8,2) . "<br>";
		echo "yr exp date = " . substr($exdt,0,4) . "<br>";
		$expsec = mktime (0, 0, 0, substr($exdt,5,2), substr($exdt,8,2), substr($exdt,0,4));
		$remsec = $expsec - $today_secs;
		echo "remsecs for " , $s['uid'] . " = " . $remsec . "<br>";
		echo "thirty = " . $thirty . "<br>";
		$msg = "";
		switch($remsec) {
			case ($thirty):
				$msg = "30 days";
				break;
			case ($fifteen):
				$msg = "15 days";
				break;
			case ($ten):
				$msg = "100 days";
				break;
			case ($five):
				$msg = "30 days";
				break;
			case ($two):
				$msg = "2 days";
				break;
			case ($one):
				$msg = "1 day";
				break;
			default:
				break;
		}
		if ($msg != "") {
			// send email
			$qryu = "SELECT * from users where uid = " . $s['uid'];
			$resu = mysqli_query($link, $qryu);
			$u = mysqli_fetch_array($resu,MYSQLI_ASSOC);
			$link = "<a href='http://backflowtestreport.com/subscribe.php?uid=" . $s['uid'] . "'>Subscribe</a>";
			$headers  = 'MIME-Version: 1.0' . "\r\n";
			$headers .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
			$to = $u['email'];
			$subj = "Backflow Test Report Subscription Expiration";
			$body = "Your subscription to the Backflow Test Report Web app will expire in " . $msg . "<br><br>";
			$body = $body . "To renew, click this link " . $link . "<br><br>";
			$body = $body . "Thank you for your interest.";
			echo "to: " . $to . "<br>";
			echo "body = " . $body . "<br>";
	//echo "subject = " . $subj . "<br>";
			//$mr = mail($to, $subj, $body, $headers);
		}
	}
}
?>

Open in new window


This code, run today 10/16/2016, produces this:

mo = 10, day = 16, year = 2016
exp date = 2016-11-14
mo exp date = 11
day exp date = 14
yr exp date = 2016
remsecs for 1 = 2509200
thirty = 2592000
exp date = 2016-11-15
mo exp date = 11
day exp date = 15
yr exp date = 2016
remsecs for 2 = 2595600
thirty = 2592000

This is impossible.

The remsecs for 1 is 2509200, which in days is 29.04166666666667, which is wrong.

The actual difference is EXACTLY 30 days.

What am I doing wrong?
Avatar of Kim Walker
Kim Walker
Flag of United States of America image

What is the default time zone for the PHP server?
Never mind that. All calculations should be relative to the same time zone regardless of which time zone that is. I'm still looking.
As a general rule, it's not a good idea to write your own date/time calculations.  The "home-grown" computations tend to fail over leap year (a rare, super-annual occurrence), and the boundaries of daylight-savings time (a common, twice-a-year occurrence).  I haven't got time to plow this field again right now, but my guess is that home-grown computations made at this time of year might incorrectly assume that all days have 86,400 seconds.  When the computations span the boundaries of daylight savings time, that assumption is untrue, leading to results that look a lot like what you're finding here.

Try this search.  It looks like we are less than a month away from the boundary.  Might be worth checking.
https://www.google.com/?q=Daylight+savings+time

This looks suspicious.  The difference here is 3,600, or exactly the number of seconds in an hour.  Like the hour adjustment for daylight savings time.
remsecs for 2 = 2595600
thirty = 2592000

It might also be a typo?  This looks incongruous:
remsecs for 1 = 2509200 /*** maybe this should be 2592000 ? ***/
thirty = 2592000

The principles for handling date/time values in PHP are described in these two articles.

Procedural
https://www.experts-exchange.com/articles/201/Handling-Date-and-Time-in-PHP-and-MySQL-Procedural-Version.html

Object-oriented
https://www.experts-exchange.com/articles/20920/Handling-Time-and-Date-in-PHP-and-MySQL-OOP-Version.html
ASKER CERTIFIED SOLUTION
Avatar of Kim Walker
Kim Walker
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of Richard Korts

ASKER

I see the problem is that a daylight savings time change occurs during that period.

Is there a way to use DateDiff in procedural (rather than object) notation? Does Date Diff take into consideration changes in DST?

I cannot understand the usage of DateDiff in object notation.

What I am trying to do is to a calculation that reminds subscribers that there subscription will expire in 30 days, 15 days, 10 days etc.

I think what I have will work if there is NOT a daylight savings time change in the middle. I think mine will work over "leap day" because I think php mktime is aware of that.
If you want procedural code, this article describes how it's done.
https://www.experts-exchange.com/articles/201/Handling-Date-and-Time-in-PHP-and-MySQL-Procedural-Version.html

Here's an example...
https://iconoun.com/demo/temp_rkorts.php
<?php // demo/temp_rkorts.php
/**
 * https://www.experts-exchange.com/questions/28976753/Impossible-arithmetic.html#a41846831
 *
 * REQUIRED READING!
 * https://www.experts-exchange.com/articles/201/Handling-Date-and-Time-in-PHP-and-MySQL-Procedural-Version.html
 */
error_reporting(E_ALL);
echo '<pre>';

// GET TODAY IN ISO-8601 FORMAT
$now = date('c');

// GET A FUTURE DATE IN ISO-8601 FORMAT
$days = '15 days';
$then = date('c', strtotime("$now + $days"));

// SHOW THE WORK PRODUCTS
echo PHP_EOL . $now;
echo PHP_EOL . $then;

Open in new window

For anyone coming across this question in the future, the take-away message is this: Do not write your own date calculations.  Do not use seconds when what you really want is days.  Instead use the PHP built-in functions to your greatest advantage.
Same code with comments. (I really should have commented the original code. Sorry.)
<?php

// WHEN WE ARE DEBUGGING OUR CODE, WE WANT TO SEE ALL THE ERRORS!
error_reporting(E_ALL);
ini_set('display_errors','on');

// set the default time zone to match the time zone of dates in database
// reason: GMT (+0000) could be a day ahead of the viewer
date_default_timezone_set('America/New_York');

// create date/time zone object for GMT (+0000)
$gmt = new DateTimeZone('+0000');

// create a date/time object for today's date in the default time zone
// but use GMT to avoid daylight savings time
$today_new = new DateTime(date("Y-n-j"),$gmt);
echo '$today_new = '.$today_new->format('r').'<br>';

// mimic data from database query
$s = array('exp_date' => "2016-11-14", "uid" => "1");

// create another date/time object for the expiration date
// from the database using GMT to avoid daylight savings time
$exdt_new = new DateTime(date($s['exp_date']),$gmt);
echo '$exdt_new = '.$exdt_new->format('r').'<br>';

// calculate difference between today and expiration date
$remsec_new = $exdt_new->diff($today_new);
// NOTE: the variable $remsec_new contains an array of values for
// years, months, days, hours, minutes, etc.

// echo the number of days from the difference array using the "d" key
echo "remdays for " . $s['uid'] . " = " . $remsec_new->d . " days<br>";

?>

Open in new window