How can I divide a date range by number of months? - php

My dilemma is that if I request more than 6 months or so ( I do not know the approximate number ) from my webservices ( which gets called via JS ), I get nothing back. In other words, I have to limit it to 6 months.
So let's consider this scenario:
$a = strtotime('June 3, 2011');
$b = strtotime('June 3, 2012');
I need to split this up by 6 months in order to make 2 distinct web servicerequests so that each call requests 6 months.
So with $a and $b, I need to split these up into as many date ranges as possible when taking the amount of months total divided by 6.
The first date range I need is from June 1, 2011 to November 31, 2011. The second date range I need is from December 1, 2011 to July 1, 2012.
The idea I had in mind was finding the number of months, then if it was greater than the limit variable 6, do a loop and increment the initial date by 6 months per loop.
Pseudo-code ( I actually initially wrote it in JS but figured it'd be easier to do in PHP because I wouldn't have to deal with multiple asynchronous request behaviour ):
var numMonths = monthDiff ( a, b ), ret = [], limit = 6, loopLimit = Math.ceil( numMonths / limit ), ranges = [];
if ( numMonths > limit ) {
for ( var i = 0; i<loopLimit; i++ ) {
var start = new Date(b);
var end = new Date ( b.setMonth( b.getMonth() + limit ) );
ranges.push( start, end );
}
}
Does anyone know of a succinct way of doing this? Can anyone spot any programmatic flaws in this?

Try:
$a = strtotime("June 3, 2011 00:00:00Z");
$b = strtotime("June 3, 2012 00:00:00Z");
fetchAll($a,$b);
function fetchAll($a,$b) {
$fetchLimit = "6 months"; // or, say, "180 days"; a string
if ($b <= strtotime(gmdate("Y-m-d H:i:s\Z",$a)." +".$fetchLimit)) {
// it fits in one chunk
fetchChunk($a,$b);
}
else { // chunkify it!
$lowerBound = $a;
$upperBound = strtotime(gmdate("Y-m-d H:i:s\Z",$a)." +".$fetchLimit);
while ($upperBound < $b) { // fetch full chunks while there're some left
fetchChunk($lowerBound,$upperBound);
$lowerBound = $upperBound;
$upperBound = strtotime(gmdate("Y-m-d H:i:s\Z",$lowerBound)." +".$fetchLimit);
}
fetchChunk($lowerBound,$b); // get last (likely) partial chunk
}
}
function fetchChunk($a,$b) {
/* insert your function that actually grabs the partial data */
//
// for test, just display the chunk range:
echo gmdate("Y-m-d H:i:s\Z",$a)." to ".gmdate("Y-m-d H:i:s\Z",$b)."<br>";
}
...where $fetchLimit in fetchAll() is any duration string parseable by strtotime(). You could then keep appending the output of each fetchChunk() to an initially blank variable which is later returned by fetchAll().
This example fetches two six-month "chunks", as expected. Changing $b to one day later adds a third chunk containing only that extra day:
2011-06-03 00:00:00Z to 2011-12-03 00:00:00Z
2011-12-03 00:00:00Z to 2012-06-03 00:00:00Z
2012-06-03 00:00:00Z to 2012-06-04 00:00:00Z
Of course, PHP 5.3 has somewhat more elegant time functions like DateTime::diff, but the code above should work fine in PHP 5.2.x.

The only way to do this accurately if a month means X days is to have a method that can perform day calculation.
Unless by 6 months you mean 6 literal months.... Do this in a loop:
Subtract 6 from the month portion, and decrement year and add 12 to month if your month number is negative. Then determine which is later, the start date or your decremented date.
If your start date is later, then use that date. If your decremented date is later, then pack that date range and loop up and start the check over.

Another method is:
$a = strtotime("2018-12-01");
$b = strtotime("2020-12-01");
$makeArray = array();
function addDate($date){
return strtotime(gmdate("Y-m-d", $date)." +6 months");
}
array_push($makeArray, array("start" => date("Y-m-d", $a), "end" => date("Y-m-d", addDate($a))));
while ($c < $b) {
$a = addDate($a);
$c = addDate($a);
if ($c >= $b) {
$c = $b;
}
array_push($makeArray, array("start" => date("Y-m-d", $a), "end" => date("Y-m-d", $c)));
}
print_r($makeArray);

Related

Time attendance system how to mach a user with a spesific shift taking into consideration entrance time? Laravel // PHP // Carbon

A time attendance system can have many shifts.
For instance:
Shift a) 08:00 - 16:00,
Shift b) 16:00 - 00:00,
Shift c) 00:00 - 08:00,
A user starts working at 07:55 what is the best way to match this user with the correct shift which is shift a?
Keep in mind that the time attendance system may have many shifts much closer together, for instance a
shift that starts at 8:00 and a shift that starts at 9:00.
Important info:
What i have done is a foreach loop that compares all starting times of the shifts (in our example 09:00, 16:00, 00:00) with the time that user started working. In our example 7:55.
The one that is closer to the users start working time is the correct shift.
This looks like its ok but in reality its not. The reason is that when time is round 00:00:00 and since times of shifts do not have a date, when the comparison is 23:59:59 and 00:00:01 i get 86400 secs instead of just 2 secs.
Additional you never know which date is greater than the other, because a user may come earlier for work or late.
So any ideas must take these into consideration.
Thanks for efforts
I've updated the answer based on the comment, but there is not enough information in the question to show you how to query your database and convert your shifts into the array I'm using in this example.
This codeblock is reference, not code to use. This is the array of start times you need to convert your database table to.
$shift_starts = [
// 1 represents the ID of the shift in your database.
1 => [
// Shift ID 1 starts at midnight, hour 0, minute 0
[0, 0],
],
// 2 represents the ID of the shift in your database.
2 => [
// Shift ID 2 starts at 8am, hour 8, minute 0
[8, 0],
],
// 3 represents the ID of the shift in your database.
3 => [
// Shift ID 3 starts at 4pm, hour 16, minute 0
[16, 0],
],
];
Create a function, something like this. Again, I don't know how you are querying your database, nor the schema. I just know how you are storing the start times:
// Psudeo Code!!! Study and write your own function that returns
// the array as defined above.
function get_shift_start_array() {
$shift_starts = [];
$result = mysqli_query($db, "SELECT * FROM shifts ORDER BY start_time");
while ($row = mysqli_fetch_row($result)) {
// If the start_time is formatted h:m:s, then make it so you can get hours
// and minutes into their own variables:
$arr = explode(':', $row['start_time']);
$hour = $arr[0];
$minute = $arr[1];
$shift_starts[$row->id] = [$hour, $minute];
}
return $shift_starts;
}
Now that we have a way to get your shift data into a format we can code around, this function will take a unix timestamp and return the database ID of the shift. (Notice this function calls the function above)
Read the comments and study the PHP functions you might not understand.
/**
* Get the shift ID for a specific time.
*
* #param int $punchin_time Unix timestamp Default is the current time.
* #return int The shift id (the array key from $shift_starts)
*/
function findShift($punchin_time = null): int
{
if ($punchin_time === null) {
$punchin_time = time();
}
// Call the psudo code function to get an array of shift start times keyed by shift id.
$shift_starts = get_shift_start_array();
// Set $day to the unix timestamp of midnight yesterday.
$day = strtotime(date('Y-m-d', $punchin_time - 86400));
// We'll be checking the difference between punchin time and the shift time.
// $last_diff will be used to compare the diff of the current shift to the last shift.
// Initialize this with an arbitrarily high value beyond the 3 days we're looking at.
$last_diff = 86400 * 5; //
$last_index = null;
// Loop over 3 days starting with yesterday to accommodate punchin times before midnight.
// Return the shift ID when we find the smallest difference between punchin time and shift start.
for ($i = 0; $i <= 3; $i++) {
// Get the month, day, and year numbers for the day we are iterating on.
// We will use these in our calls to mktime()
$m = date('n', $day);
$d = date('j', $day);
$y = date('y', $day);
// Loop over each shift.
foreach ($shift_starts as $index => $start) {
// Create a unix timestamp of the shift start time.
// This is the date and time the shift starts based on the day iteration.
$time = mktime($start[0], $start[1], 0, $m, $d, $y);
// Get the difference in seconds between this shift start time and the punchin time.
$diff = abs($punchin_time - $time);
// $diff should be getting smaller as we get closer to the actual shift.
if ($diff > $last_diff) {
// If $diff got bigger than the last one, we've past the shift.
// Return the index of the last shift.
return $last_index;
}
$last_index = $index;
$last_diff = $diff;
}
$day = strtotime('+1 day', $day);
}
// Return null if no shift found.
return null;
}
Now that the functions are defined, you just need to call the last one to convert specific time, to a shift.
$punchin_time = mktime(15, 55, 0, 4, 15, 2020);
$shift_id = findShift($punchin_time);
Alternatively, don't pass a time in and the current time will be used.
$shift_id = findShift($punchin_time);
mktime
strtotime()
DateTime::getTimestamp()

Recursive infinite daily dynamic loop array php

I am trying to display a number every day in a loop. After the last element is reached it needs to get to the first element again. This needs to happen daily. I have overworked my brains out but did not managed to solve it. Function needs to return current number by day/hour/minute, like . This is what I tried till now.
<?php
function recursive_daily_deals($i = 1) {
$current_date = strtotime(date('d-m-Y h:i:s'));
$dbs_date_1 = strtotime('29-06-2017 8:20:16');
$current_hour = date('h');
var_dump($current_hour);
$products = [
451,
455,
453
];
if ($i < count($products)) {
return recursive_daily_deals($i+1);
}
}
?>
EXPECTED output
> First day - June 29 2017
> It will appear 451
> Second day - June 30 2017
> It will appear 455
> 3rd day - July 1st 2017
> It will appear 453
> 4th Day - July 2nd 2017
> It will appear 453
> And start over
First you need to know how many days have been since the starting day. To do that you just need to sub the starting timestamp from the actual timestamp :
$dbs_date_1 = strtotime('29-06-2017 8:20:16');
$actualTimestamp = time();
$elapsedSec = $dbs_date_1 - $actualTimestamp;
// we need to get days from seconds
$elapsedDays = $elapsedSec / (3600*24);
$elapsedDays = floor($elapsedDays);
So when you have how many days have been since the starting day. We use floor() instead of round() because if the script runs after the half of the day it will return the number of days +1.
With this number of days we can have the number of cycles already done by dividing the number of elapsed days by the number of items in our array :
$nbItems = count($products);
$cycleCount = $elapsedDays / $nbItems;
$finishedCycles = floor($cycleCount);
We store the number of finished cycles by flooring the number of cycles. Then we just have to sub the days it took to complete those cycles from the elapsed days and we will get the position of the index.
$completeDays = $finishedCycles * $nbItems;
$actualPosition = $elapsedDays - $completeDays;
return $products[$actualPosition];
While this is a simplified version of the code originally posted, I believe it contains the kind of logic that the OP seeks, as follows:
<?php
$products = [
451,
455,
453
];
$modfactor = count($products);
$days = null;
$weekdays = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
for ($i=0, $max = 7; $i < $max; $i++) {
$days[$i] = $i % $modfactor;
}
foreach ($weekdays as $dex => $wday) {
echo "$wday, ";
echo $products[ $days[$dex] ], "\n";
}
See demo
Update: See demo here which makes use of array_map() and gets the current product ID, too.
While the loop is ever present, it is not infinite. If the number of products changes, then the modfactor changes, too. What stays constant are the days of the week. What makes the code work is taking advantage of a modulo operation.

In an array with numeric values, select the successive values within a range and set them all to the highest value of them

All of that in an associative array.
I have an array that holds as keys some IDs and as values some integers:
array([24]=>1620, [49]=>1620, [35]=>1622, [101]=>1623, [214]=>1630, [50]=>1638, [5]=>1640)
What I want to obtain is, while "LastValue - FirstValue <= 4", to set all near values to the highest of them, like this:
array([24]=>1623, [49]=>1623, [35]=>1623, [101]=>1623, [214]=>1630, [50]=>1640, [5]=>1640)
In reality is about some events which have a planned date; that planned date is stored as week number; I want to group the successive individual events within a month to the last week in that group.
So I have created this array with the IDs of events and the dates; the array is sorted ascendant by values. But I can't figured out how to group the successive individual values within a month, set them to the highest of them (last one) then pass to the next values, identify the group, set the values, etc
Any suggestions? Thanks!
The method you describe is ambiguous when you have long series of weeks without gaps. In that case there are several ways to group weeks together keeping the condition that the first and last week of the group should not be more than 4 weeks apart.
Assuming you don't expect such long series of non-interrupted week numbers, I present here an algorithm that goes backwards through the (sorted) array of events. It temporarily transforms week numbers to "absolute" week numbers, which do not reset to 1 each year, but keep increasing with one. That way the algorithm can equally find groups that cross year boundaries:
// Sample data
$data = array(24=>1620, 49=>1620, 35=>1622, 101=>1623, 214=>1630, 50=>1638, 5=>1640);
$last_abs_week = 99999;
$date = new DateTime();
// first week in the "absolute" week numbering:
$ref_date = new DateTime();
$ref_date->setISODate(2000, 1);
// Loop backwards:
foreach (array_reverse(array_keys($data)) as $event) {
$week = $data[$event];
// Get date corresponding to week number
$date->setISODate("20" . substr($week, 0, 2), substr($week, 2));
// Convert date to number of weeks since year 2000 to allow week numbers
// to be subtracted even when belonging to different years
$abs_week = $date->diff($ref_date)->format("%a") / 7;
if ($abs_week < $last_abs_week - 4) {
// Not within same group: start a new group
$last_week = $week;
$last_abs_week = $abs_week;
} else {
// In same group: assign last week of group
$data[$event] = $last_week;
}
}
print_r ($data);
Output for the sample data is:
Array
(
[24] => 1623
[49] => 1623
[35] => 1623
[101] => 1623
[214] => 1630
[50] => 1640
[5] => 1640
)
Forward search
Here is an alternative way, where the array is scanned in the given order:
$first_abs_week = 0;
$group = [];
$date = new DateTime();
$ref_date = new DateTime();
$ref_date->setISODate(2000, 1);
foreach ($data as $event => $week) {
// Get date corresponding to week number
$date->setISODate("20" . substr($week, 0, 2), substr($week, 2));
// Convert date to number of weeks since year 2000 to allow week numbers
// to be subtracted even when belonging to different years
$abs_week = $date->diff($ref_date)->format("%a") / 7;
if (count($group) && $abs_week > $first_abs_week + 4) {
$week = $data[array_pop($group)];
foreach ($group as $prev_event) {
$data[$prev_event] = $week;
}
$group = [];
$first_abs_week = $abs_week;
}
$group[] = $event;
}
print_r ($data);
Output for the sample data is the same as for the first alternative.

Complex date format

I am trying to come up with the most efficient and best way to accomplish this somewhat of a complex situation. I know that I could build this solution using probably around 5 if else statements, maybe more - however there must be a better way to accomplish what I want to.
So here's what I am trying to do. I have an events page on my website, and what I want to do is display the dates in a minimalistic way when possible. What I mean is the following:
Say I have 3 dates: May 5, May 6, May 7. I want to display it as: May 5 - 7.
However, there will be situations where the dates may be: May 5, May 7. In this case I would like to display it as: May 5 & 7.
However, there may also be situations where the dates may be: May 25, June 2. In this case I would like to display it as: May 25 & June 2.
However! There also may be situations where the dates may be: May 25, May 26, June 2. In this case it should display as: May 25 - 26 & June 2
Of course, there could just be a single date as well. But one other thing, it could be possible that there could be more than 3 dates as well, so it would be nice if it could work regardless of how many dates there are (IE loop through an array).
I know that we are suppose to make an attempt and show some code to debug, however I don't even know where to start with this, if this is too much for someone to put together - just giving me an idea of how to do something like this efficiently would be a huge help.
Thanks
//input data: sorted list of dates
$dates = array('May 5','May 6','May 7','May 30','Jun 2','Jun 3','Dec 11','Dec 12','Dec 14');
array_push($dates,false); //add an extra value so the last range gets printed
//initialize min & previous date as first date
$min_date = array_shift($dates);
$prev_date = $min_date;
$counter = 0; //keep count of # of days between min and max
$formatted_dates = array();
foreach($dates as $date) {
//if the difference in number of days between current date and min date
//is greater than counted number of days then we capture the current range
//and start a new one by resetting $min_date to $date and $counter to 0
if(!$date || ($counter + 1) < diff_in_days($min_date,$date)) {
if($counter == 0) { //format for 1 date
$formatted_dates[] = $min_date;
}
elseif($counter == 1) { //format for 2 dates
$formatted_dates[] = "$min_date & $prev_date";
}
elseif($counter > 1) { //format for > 2 dates
$formatted_dates[] = "$min_date - $prev_date";
}
$counter = 0;
$min_date = $date;
}
else {
$counter++;
}
$prev_date = $date;
}
//may also want to verify that neither formatted date contains an '&'
//so you don't end up with "May 11 & May 12 & June 1 & June 2" which may be confusing
if(count($formatted_dates) == 2) {
print implode(' & ',$formatted_dates);
}
else {
print implode("\n",$formatted_dates);
}
function diff_in_days($day1,$day2) {
$datetime1 = new DateTime($day1);
$datetime2 = new DateTime($day2);
$interval = $datetime1->diff($datetime2);
$ret = (int) $interval->format('%a');
return $ret;
}
Output
May 5 - May 7
May 30
Jun 2 & Jun 3
Dec 11 & Dec 12
Dec 14

count how many days within a date range are within another date range

From October 1st to March 31 the fee is $1 (season 1). From April 1st to September 30 the fee is $2 (season 2).
How can I calculate the total fee of a given date range (user input) depending on how many days of this date range fall into season 1 and season 2?
The following gives me the number of days of the userĀ“s date range, but I have no idea how to test against season 1 or season 2:
$user_input_start_date = getdate( $a );
$user_input_end_date = getdate( $b );
$start_date_new = mktime( 12, 0, 0, $user_input_start_date['mon'], $user_input_start_date['mday'], $user_input_start_date['year'] );
$end_date_new = mktime( 12, 0, 0, $user_input_end_date['mon'], $user_input_end_date['mday'], $user_input_end_date['year'] );
return round( abs( $start_date_new - $end_date_new ) / 86400 );
Given that a date range starts and ends in 2012 or starts in 2012 and ends in 2013 alone gives me 10 different possibilities of in which season a date range can start and where it can end.
There must be a better solution than iterating if/else and comparing dates over and over again for the following conditions:
Date range is completely within season 1
Date range starts in season 1 and ends in season 2
Date range starts in season 1, spans across season 2 and ends in the second part of season 1
... and so forth with "Starts in season 2", etc
This not a duplicate of How many days until X-Y-Z date? as that only deals with counting the number of days. It does not address the issue of comparing one date range with another.
The key to this problem is to simplify it as much as possible. I think using an array as a lookup table for the cost of each day of the year is the way to go. The first thing to do then, is to generate the array. The array just represents each day of the year and doesn't represent any particular year. I chose to use 2012 to generate the lookup array as it is a leap year and so has every possible day in it.
function getSeasonArray()
{
/**
* I have chosen 2012 as it was a leap year. All we want to do is
* generate an array which has avery day of the year in it.
*/
$startDate = new DateTime('1st January 2012');
//DatePeriod always drops the last day.
$endDate = new DateTime('1st January 2013');
$season2Start = new DateTime('1st April 2012');
$season2End = new DateTime('1st October 2012');
$allDays = new DatePeriod($startDate, new DateInterval('P1D'), $endDate);
$season2Days = new DatePeriod($season2Start, new DateInterval('P1D'), $season2End);
$seasonArray = array();
foreach($allDays as $day){
$seasonArray[] = $day->format('d-M');
$seasonArray[$day->format('d-M')]['season'] = 1;
}
foreach($season2Days as $day){
$seasonArray[$day->format('d-M')]['season'] = 2;
}
return $seasonArray;
}
Once that is done you just need the period over which to calculate:-
$bookingStartDate = new DateTime();//Or wherever you get this from
$bookingEndDate = new DateTime();
$bookingEndDate->setTimestamp(strtotime('+ 7 month'));//Or wherever you get this from
$bookingPeriod = new DatePeriod($bookingStartDate, new DateInterval('P1D'), $bookingEndDate);
Then we can do the calculation:-
$seasons = getSeasonArray();
$totalCost = 0;
foreach($bookingPeriod as $day){
$totalCost += $seasons[$day->format('d-M')]['season'];
var_dump($day->format('d-M') . ' = $' . $seasons[$day->format('d-M')]['season']);
}
var_dump($totalCost);
I have chosen a long booking period, so that you can scan through the var_dump() output and verify the correct price for each day of the year.
This is a quick stab done between distractions at work and I'm sure that with a bit of thought you can mould it into a more elegant solution. I'd like to get rid of the double iteration for example, unfortunately, work pressures prevent me from spending further time on this.
See the PHP DateTime man page for further information on these useful classes.
At first I suggested using the DateTime class that PHP provides, naively assuming that it has some kind of thought-out API that one could use. It turns out that it does not. While it features very basic DateTime functionality, it is mostly unusable because, for most operations, it relies on the DateInterval class. In combination, those classes represent another masterpiece of bad API design.
An interval should be defined like so:
An interval in Joda-Time represents an interval of time from one millisecond instant to another instant. Both instants are fully specified instants in the datetime continuum, complete with time zone.
In PHP, however, an Interval is just a duration:
A date interval stores either a fixed amount of time (in years, months, days, hours etc) or a relative time string [such as "2 days"].
Unfortunately, PHP's DateInterval definition does not allow for intersection/overlap calculation (which the OP needs) because PHP's Intervals have no specific position in the datetime continuum. Therefore, I've implemented a (very rudimentary) class that adheres to JodaTime's definition of an interval. It is not extensively tested, but it should get the work done:
class ProperDateInterval {
private $start = null;
private $end = null;
public function __construct(DateTime $start, DateTime $end) {
$this->start = $start;
$this->end = $end;
}
/**
* Does this time interval overlap the specified time interval.
*/
public function overlaps(ProperDateInterval $other) {
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
return $start < $oEnd && $oStart < $end;
}
/**
* Gets the overlap between this interval and another interval.
*/
public function overlap(ProperDateInterval $other) {
if(!$this->overlaps($other)) {
// I haven't decided what should happen here yet.
// Returning "null" doesn't seem like a good solution.
// Maybe ProperDateInterval::EMPTY?
throw new Exception("No intersection.");
}
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
$overlapStart = NULL;
$overlapEnd = NULL;
if($start === $oStart || $start > $oStart) {
$overlapStart = $this->getStart();
} else {
$overlapStart = $other->getStart();
}
if($end === $oEnd || $end < $oEnd) {
$overlapEnd = $this->getEnd();
} else {
$overlapEnd = $other->getEnd();
}
return new ProperDateInterval($overlapStart, $overlapEnd);
}
/**
* #return long The duration of this interval in seconds.
*/
public function getDuration() {
return $this->getEnd()->getTimestamp() - $this->getStart()->getTimestamp();
}
public function getStart() {
return $this->start;
}
public function getEnd() {
return $this->end;
}
}
It may be used like so:
$seasonStart = DateTime::createFromFormat('j-M-Y', '01-Apr-2012');
$seasonEnd = DateTime::createFromFormat('j-M-Y', '30-Sep-2012');
$userStart = DateTime::createFromFormat('j-M-Y', '01-Jan-2012');
$userEnd = DateTime::createFromFormat('j-M-Y', '02-Apr-2012');
$i1 = new ProperDateInterval($seasonStart, $seasonEnd);
$i2 = new ProperDateInterval($userStart, $userEnd);
$overlap = $i1->overlap($i2);
var_dump($overlap->getDuration());

Categories