So I have the following code:
use Carbon\ Carbon;
use Carbon\ CarbonPeriod;
use App\ Models\ Holiday;
// grab holidays of current year
$holidays = app('yasumi');
$holidaysArray = $holidays - > getHolidayDates();
$eventRepeats = $event - > repeats;
foreach($holidays - > getHolidayDates() as $date) {
echo '<br>';
echo $date.PHP_EOL;
echo '<br><br>';
}
// make Carbon out of event times
$eventStart = Carbon::parse($event - > start);
$eventEnd = Carbon::parse($event - > end);
// get custom holidays
$companyHolidays = Holiday::query() - > get() - > toArray();
$customholidayArray = [];
foreach($companyHolidays as $holiday) {
$parsedCompanyHolidays = CarbonPeriod::create($holiday['start'], $holiday['end']) - > toArray();
foreach($parsedCompanyHolidays as $parsedHoliday) {
$customholidayArray[] = $parsedHoliday - > translatedFormat('Y-m-d');
}
}
the issue is on the CarbonPeriod::create($holiday['start']. It is Y-m-d but I need it to also have the start of the day 00:00:00 and on the end I need 23:59:59
I use this data to skip dates in a while loop. The current solution is not working correctly and is one day behind, because of the missing hour, minute and second.
Here is the loop:
while ($i < $eventRepeats) {
// add one week
$eventStart - > addDays(7);
if (in_array($eventStart - > translatedFormat('Y-m-d'), $holidaysArray)) {
continue;
}
}
EDIT, might have found root cause, but no solution:
If I dd($companyHolidays); it shows like this "start" => "2023-02-19T23:00:00.000000Z"
even though the date is stored in the DB like this 2023-02-20.
How could this be fixed, I cannot find good documentation on this?
With the help from #Win I was able to fix this issue.
All I needed to change was the protected $casts in my Model from:
protected $casts = [
'start' => 'date',
'end' => 'date',
];
to:
protected $casts = [
'start' => 'datetime:Y-m-d',
'end' => 'datetime:Y-m-d'
];
More info about this issue can be found here:
https://laravel.com/docs/9.x/eloquent-mutators#date-casting
Related
I want to generate a list of salary coverage dates using the last salary date of an employee. The employee gets a salary every 15 days. So every month the coverage should be 1st - 15th and 16th - last day of the month.
For example,
$last_salary_date = "2020-12-01";
$date_now = "2021-02-27";
// I should get the following start and end dates:
// 2020-12-01 - 2021-12-15
// 2020-12-16 - 2021-15-31
// 2021-01-01 - 2021-01-15
// 2021-01-16 - 2021-01-31
// 2021-02-01 - 2021-02-15
// 2021-02-16 - 2021-02-28
$last_salary_date = "2020-02-16";
$date_now = "2021-02-27";
// I should get the following start and end dates:
// 2021-02-16 - 2021-02-28
So far I've done something like this:
$start_date = new DateTime("2021-01-16");
$end_date = new DateTime(date("Y-m-d"));
$interval = \DateInterval::createFromDateString('1 month');
$period = new \DatePeriod($start_date, $interval, $end_date);
$salary_dates = [];
foreach ($period as $dt) {
if (date("Y-m-d") > $dt->format("Y-m-01")) {
$salary_dates[] = (object) [
'start_dt' => $dt->format("Y-m-01"),
'end_dt' => $dt->format("Y-m-15")
];
}
if (date("Y-m-d") > $dt->format("Y-m-15")) {
$salary_dates[] = (object) [
'start_dt' => $dt->format("Y-m-16"),
'end_dt' => $dt->format("Y-m-t")
];
}
}
return $salary_dates;
The problem is it still gets the 1-15th of the first month even though the start should be the 16th.I'm thinking of a better way to do this. Please help
Here is the modified implementation. It is sketched for understanding, but can also be written in a shorter form.
<?php
$start_date = new DateTime("2021-02-16");
$end_date = new DateTime("2021-02-27");
$interval = \DateInterval::createFromDateString('1 month');
$period = new \DatePeriod($start_date, $interval, $end_date);
$salary_dates = [];
foreach ($period as $dt) {
$check_date = function ($date) use ($start_date, $end_date) {
// check if salary interval is in correct range
return
$start_date->format("Y-m-d") <= $date->start_dt
and $end_date->format("Y-m-d") >= $date->start_dt; // are you sure it shouldn't be ->end_dt ? now it's on order
};
// check first two weeks in month
$salary_date = (object)[
'start_dt' => $dt->format("Y-m-01"),
'end_dt' => $dt->format("Y-m-15"),
];
if ($check_date($salary_date)) {
$salary_dates[] = $salary_date;
}
// check second two weeks in month
$salary_date = (object)[
'start_dt' => $dt->format("Y-m-16"),
'end_dt' => $dt->format("Y-m-t")
];
if ($check_date($salary_date)) {
$salary_dates[] = $salary_date;
}
}
print_r($salary_dates);
I have to create a scheduling component that will plan e-mails that need to be sent out. Users can select a start time, end time, and frequency. Code should produce a random moment for every frequency, between start and end time. Outside of office hours.
Paramaters:
User can select a period between 01/01/2020 (the start) and 01/01/2021 (the end). In this case user selects a timespan of one exactly year.
User can select a frequency. In this case user selects '2 months'.
Function:
Code produces a list of datetimes. The total time (one year) is divided by frequency (2 months). We expect a list of 6 datetimes.
Every datetime is a random moment in said frequency (2 months). Within office hours.
Result:
An example result for these paramaters might as follows, with the calculated frequency bounds for clarity:
[jan/feb] 21-02-2020 11.36
[mrt/apr] 04-03-2020 16.11
[mei/jun] 13-05-2020 09.49
[jul-aug] 14-07-2020 15.25
[sep-okt] 02-09-2020 14.09
[nov-dec] 25-12-2020 13.55
--
I've been thinking about how to implement this best, but I can't figure out an elegant solution.
How could one do this using PHP?
Any insights, references, or code spikes would be greatly appreciated. I'm really stuck on this one.
I think you're just asking for suggestions on how to generate a list of repeating (2 weekly) dates with a random time between say 9am and 5pm? Is that right?
If so - something like this (untested, pseudo code) might be a starting point:
$start = new Datetime('1st January 2021');
$end = new Datetime('1st July 2021');
$day_start = 9;
$day_end = 17;
$date = $start;
$dates = [$date]; // Start date into array
while($date < $end) {
$new_date = clone($date->modify("+ 2 weeks"));
$new_date->setTime(mt_rand($day_start, $day_end), mt_rand(0, 59));
$dates[] = $new_date;
}
var_dump($dates);
Steve's anwser seems good, but you should consider 2 additional things
holiday check, in the while after first $new_date line, like:
$holiday = array('2021-01-01', '2021-01-06', '2021-12-25');
if (!in_array($new_date,$holiday))
also a check if date is a office day or a weekend in a similar way as above with working days as an array.
It's kind of crappy code but I think it will work as you wish.
function getDiffInSeconds(\DateTime $start, \DateTime $end) : int
{
$startTimestamp = $start->getTimestamp();
$endTimestamp = $end->getTimestamp();
return $endTimestamp - $startTimestamp;
}
function getShiftData(\DateTime $start, \DateTime $end) : array
{
$shiftStartHour = \DateTime::createFromFormat('H:i:s', $start->format('H:i:s'));
$shiftEndHour = \DateTime::createFromFormat('H:i:s', $end->format('H:i:s'));
$shiftInSeconds = intval($shiftEndHour->getTimestamp() - $shiftStartHour->getTimestamp());
return [
$shiftStartHour,
$shiftEndHour,
$shiftInSeconds,
];
}
function dayIsWeekendOrHoliday(\DateTime $date, array $holidays = []) : bool
{
$weekendDayIndexes = [
0 => 'Sunday',
6 => 'Saturday',
];
$dayOfWeek = $date->format('w');
if (empty($holidays)) {
$dayIsWeekendOrHoliday = isset($weekendDayIndexes[$dayOfWeek]);
} else {
$dayMonthDate = $date->format('d/m');
$dayMonthYearDate = $date->format('d/m/Y');
$dayIsWeekendOrHoliday = (isset($weekendDayIndexes[$dayOfWeek]) || isset($holidays[$dayMonthDate]) || isset($holidays[$dayMonthYearDate]));
}
return $dayIsWeekendOrHoliday;
}
function getScheduleDates(\DateTime $start, \DateTime $end, int $frequencyInSeconds) : array
{
if ($frequencyInSeconds < (24 * 60 * 60)) {
throw new \InvalidArgumentException('Frequency must be bigger than one day');
}
$diffInSeconds = getDiffInSeconds($start, $end);
// If difference between $start and $end is bigger than two days
if ($diffInSeconds > (2 * 24 * 60 * 60)) {
// If difference is bigger than 2 days we add 1 day to start and subtract 1 day from end
$start->modify('+1 day');
$end->modify('-1 day');
// Getting new $diffInSeconds after $start and $end changes
$diffInSeconds = getDiffInSeconds($start, $end);
}
if ($frequencyInSeconds > $diffInSeconds) {
throw new \InvalidArgumentException('Frequency is bigger than difference between dates');
}
$holidays = [
'01/01' => 'New Year',
'18/04/2020' => 'Easter 1st official holiday because 19/04/2020',
'20/04/2020' => 'Easter',
'21/04/2020' => 'Easter 2nd day',
'27/04' => 'Konings',
'04/05' => '4mei',
'05/05' => '4mei',
'24/12' => 'Christmas 1st day',
'25/12' => 'Christmas 2nd day',
'26/12' => 'Christmas 3nd day',
'27/12' => 'Christmas 3rd day',
'31/12' => 'Old Year'
];
[$shiftStartHour, $shiftEndHour, $shiftInSeconds] = getShiftData($start, $end);
$amountOfNotifications = floor($diffInSeconds / $frequencyInSeconds);
$periodInSeconds = intval($diffInSeconds / $amountOfNotifications);
$maxDaysBetweenNotifications = intval($periodInSeconds / (24 * 60 * 60));
// If $maxDaysBetweenNotifications is equals to 1 then we have to change $periodInSeconds to amount of seconds for one day
if ($maxDaysBetweenNotifications === 1) {
$periodInSeconds = (24 * 60 * 60);
}
$dates = [];
for ($i = 0; $i < $amountOfNotifications; $i++) {
$periodStart = clone $start;
$periodStart->setTimestamp($start->getTimestamp() + ($i * $periodInSeconds));
$seconds = mt_rand(0, $shiftInSeconds);
// If $maxDaysBetweenNotifications is equals to 1 then we have to check only one day without loop through the dates
if ($maxDaysBetweenNotifications === 1) {
$interval = new \DateInterval('P' . $maxDaysBetweenNotifications . 'DT' . $seconds . 'S');
$date = clone $periodStart;
$date->add($interval);
$dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
} else {
// When $maxDaysBetweenNotifications we have to loop through the dates to pick them
$loopsCount = 0;
$maxLoops = 3; // Max loops before breaking and skipping the period
do {
$day = mt_rand(0, $maxDaysBetweenNotifications);
$periodStart->modify($shiftStartHour);
$interval = new \DateInterval('P' . $day . 'DT' . $seconds . 'S');
$date = clone $periodStart;
$date->add($interval);
$dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
// If the day is weekend or holiday then we have to increment $loopsCount by 1 for each loop
if ($dayIsWeekendOrHoliday === true) {
$loopsCount++;
// If $loopsCount is equals to $maxLoops then we have to break the loop
if ($loopsCount === $maxLoops) {
break;
}
}
} while ($dayIsWeekendOrHoliday);
}
// Adds the date to $dates only if the day is not a weekend day and holiday
if ($dayIsWeekendOrHoliday === false) {
$dates[] = $date;
}
}
return $dates;
}
$start = new \DateTime('2020-12-30 08:00:00', new \DateTimeZone('Europe/Sofia'));
$end = new \DateTime('2021-01-18 17:00:00', new \DateTimeZone('Europe/Sofia'));
$frequencyInSeconds = 86400; // 1 day
$dates = getScheduleDates($start, $end, $frequencyInSeconds);
var_dump($dates);
You have to pass $start, $end and $frequencyInSeconds as I showed in example and then you will get your random dates. Notice that I $start and $end must have hours in them because they are used as start and end hours for shifts. Because the rule is to return a date within a shift time only in working days. Also you have to provide frequency in seconds - you can calculate them outside the function or you can change it to calculate them inside. I did it this way because I don't know what are your predefined periods.
This function returns an array of \DateTime() instances so you can do whatever you want with them.
UPDATE 08/01/2020:
Holidays now are part of calculation and they will be excluded from returned dates if they are passed when you are calling the function. You can pass them in d/m and d/m/Y formats because of holidays like Easter and in case when the holiday is on weekend but people will get additional dayoff during the working week.
UPDATE 13/01/2020:
I've made updated code version to fix the issue with infinite loops when $frequencyInSeconds is shorter like 1 day. The new code used few functions getDiffInSeconds, getShiftData and dayIsWeekendOrHoliday as helper methods to reduce code duplication and cleaner and more readable code
Am trying to perform a query and get items where created_at is not greater that 24 hrs
I have tried
$trucks = Orders::find()
->where(["created_at"=>not more than 24 hrs ]) //stuck here
->orderBy(['created_at' => SORT_DESC])->all();
Nb:Created_at is in unix timestamp.
in usual php it would be the equivalent of
$created_at= 1500373706; // time order was created
if ((time() - $created_at) > 86400) {
//Dont get these
} else {
//Get these
}
How do i go about this?
I found this answer and made some very slight modifications to fit your situation: How to compare Dates from database in Yii2
$yesterday = strtotime("-24 hours");
$trucks = Orders::find()->where(['<=', 'created_at', $yesterday])
->orderBy('created_at DESC')->all();
The desired result
I would like to have an associative array that contains a range of times (between opening and closing times) with an interval of 15 minutes. For example:
[
'2017-01-16' => [ // Start of the week (Monday)
'08:00', // Opening time
'08:15',
'08:30',
// Etc..
'18:00', // Closing time
],
'2017-01-17' => [ // Tuesday
'10:00', // Opening time
'10:15',
'10:30',
// Etc..
'22:00', // Closing time
],
// For every day in the week.
];
Another thing I would like to be able to do, is: Take a range of times (e.g. 09:00 - 10:00) and remove it from the array (at a specific date key)
The steps I made (so far)
I have an array that looks just like the one above. But.. it starts with 00:00 and ends at 23:45. With the following code (mainly from another question at Stackoverflow):
private function generateDateRange(Carbon $start_date, Carbon $end_date)
{
$dates = [];
while ($start_date->lte($end_date)) {
if(! array_key_exists($start_date->format('Y-m-d'), $dates)) {
$dates[$start_date->format('Y-m-d')] = [];
} else {
array_push($dates[$start_date->format('Y-m-d')], $start_date->format('H:i'));
if(in_array($start_date->format('H:i'), $dates[$start_date->format('Y-m-d')])) {
$start_date->addMinutes(15);
} else {
$start_date->addDay();
}
}
}
return $dates;
}
$start = Carbon::now()->startOfWeek();
$end = Carbon::now()->endOfWeek();
$range = $this->generateDateRange($start, $end);
My question
How can I do this in PHP (Laravel)? I am planning to make this (more) dynamic by using a database. But first I want to have a working basic.
Does someone know what I could do to reach the desired result?
Try this:
private function generateDateRange(Carbon $start_date, Carbon $end_date,$slot_duration = 15)
{
$dates = [];
$slots = $start_date->diffInMinutes($end_date)/$slot_duration;
//first unchanged time
$dates[$start_date->toDateString()][]=$start_date->toTimeString();
for($s = 1;$s <=$slots;$s++){
$dates[$start_date->toDateString()][]=$start_date->addMinute($slot_duration)->toTimeString();
}
return $dates;
}
I'm writing a PHP function that would use a table of sorts to look up which DB shard the application should go to, based on the datestamp I have.
The shard configuration is something like this (pseudo-code): the first column is the date of the event I'm looking for and the 2nd is the shard the event resides in.
pre-2008 -> shard1
2008-2009 -> shard2
2009_01-2009_06 -> shard3
2009_07 -> shard4
2009_08 -> shard5
2009_09 and up -> shard6
As you can see, the configuration I want is pretty flexible - it can take any date range, however small or big and map to a shard.
I am looking for the quickest way to do a lookup based on a given date.
For example, if my date is 2009-05-02, the shard I am after is shard3. If the date is 2007-08-01, then it's shard1.
Bonus points for actual PHP code, as the application is in PHP.
Thank you.
I am guessing that you don't want to have holes in date ranges, so I propose that you
just need to specify an end date for each shard, and explicitly name one Default Shard
which holds everything that is too new to fit into one of the other shards.
// configure shards
$SHARDS = array(
// <end date> => <shard number>
'2007-12-31' => 'shard1', // shard1 - up to end of 2007
'2008-12-31' => 'shard2', // shard2 - up to end of 2008
'2009-06-30' => 'shard3', // shard3 - up to end of June 09
'2009-07-31' => 'shard4', // shard4 - up to end of July 2009
'2009-08-31' => 'shard5', // shard4 - up to end of August 2009
'DEFAULT' => 'shard6', // everything else in shard 6
);
This makes it easy to get the dates right, and the code for finding a shard based on date is simple:
function findShardByDate($date) {
static $default = false;
static $sorted = false;
if($sorted === false) {
// copy of global $SHARDS
$SHARDS = $GLOBALS['SHARDS'];
$default = $SHARDS['DEFAULT'];
unset($SHARDS['DEFAULT']);
// make sure $SHARDS is sorted
ksort($SHARDS);
$sorted = $SHARDS;
unset($SHARDS);
}
// find the first shard which would contain that date
foreach($sorted as $endDate => $shardName)
if($endDate >= $date)
return $shardName;
// no shard found - use the default shard
return $default;
}
Edit: Used static variables so that sorting is only done once.
<?php
function get_shard($datetime)
{
$timestamp = strtotime($datetime);
$shards = array(array('start' => null, 'end' => '2007-12-31'),
array('start' => '2008-01-01', 'end' => '2008-12-31'),
array('start' => '2009-01-01', 'end' => '2009-06-30'),
array('start' => '2009-07-01', 'end' => '2009-07-31'),
array('start' => '2009-08-01', 'end' => '2009-08-31'),
array('start' => '2009-09-01', 'end' => null),
);
foreach ($shards as $key => $range) {
$start = strtotime($range['start']);
$end = strtotime($range['end']);
if ($timestamp >= $start && $timestamp <= $end) {
return $key + 1;
}
if ($timestamp >= $start && $end === false) {
return $key + 1;
}
}
}
$datetime = '2007-08-01';
echo 'shard' . get_shard($datetime) . "\n";
$datetime = '2009-05-02';
echo 'shard' . get_shard($datetime) . "\n";
$datetime = '2010-01-01';
echo 'shard' . get_shard($datetime) . "\n";
?>
Outputs:
shard1
shard3
shard6
Since your shards can be strictly ordered, it seems like storing them in a binary tree and then simply running a binary search through that tree would give you the fastest results.