I'm trying to group some data in my Laravel project by a date format that is a bit different to the norm. I've got a database that and a query that fetches "Uptime Checks" for a user's website based on the period they want to look over, I then need to display this to the user as some kind of timeline.
In order to reduce "noise" in the data (where there may not be enough uptime checks for a given period) I'd like to group all of my results within say a 3 hour period throughout the day, so I'd have all of the data for:
2021-05-02 03:00:00
2021-05-02 06:00:00
2021-05-02 09:00:00
and so on, right now I'm bringing back data by the hour, but not sure how to modify this to achieve the desired outcome
// get the uptime checks for past X hours
$uptimeData = UptimeChecks::where('user_id', 1)
->where('monitor_id', 1)
->where('checked_at', '>=', '2021-05-02 13:00:00')
->where('checked_at', '<=', '2021-05-03 13:00:00')
->orderBy('checked_at', 'asc')
->select('event', 'checked_at')
->get();
$uptimeDataTimeline = $uptimeData->groupBy(function ($item, $key) {
$date = Carbon::parse($item->checked_at);
// group by hour, how can I get say every 3 hours worth of data?
return $date->format('Y-m-d H:00:00');
});
$uptimeDataTimeline = $uptimeDataTimeline->map(function ($checksInPeriod, $key) {
$down = 0;
$up = 0;
$total = 0;
$uptime = 0;
$fill = '#1fc777'; // green
// $checksInPeriod is all of the data for a given hour at the moment
// I need to group by a bigger period, say, every 3 hours
// add our events
foreach ($checksInPeriod as $key => $value) {
$total++;
if (strtolower($value['event']) == 'down') $down++;
if (strtolower($value['event']) == 'up') $up++;
}
// calculate uptime
$uptime = floatval(number_format(round($up / $total, 5) * 100, 2, '.', ','));
// fill colours
if ($uptime < 100) $fill = '#9deab8'; // lighter green
if ($uptime < 99) $fill = '#fbaa49'; // amber
if ($uptime < 98) $fill = '#e0465e'; // red
return [
'total_events' => $total,
'down_events' => $down,
'up_events' => $up,
'uptime' => $uptime,
'fill' => $fill
];
});
Not sure how to modify the groupBy function which returns the format since my understanding is that it's not possible to do that? I'm using Carbon by the way.
Update
I've been digging, and have come across the CarbonInterval feature, which allows me to generate some intervals, and I've tried implementing this, I seem to get an equally spaced time period, but my data is out and doesn't contain all of the data between two intervals (see attached image)
$intervals = CarbonInterval::hours(2)->toPeriod($from, $to);
$uptimeDataTimeline = $uptimeData->groupBy(function ($item, $key) use ($intervals) {
$date = Carbon::parse($item->checked_at);
foreach ($intervals as $key => $interval) {
if ($date->hour == Carbon::parse($interval)->addHours(1)->hour) {
$actualHour1 = Carbon::parse($interval)->hour;
if (strlen($actualHour1) == 1) $actualHour1 = "0$actualHour1";
return $date->format("Y-m-d $actualHour1:00:00");
} else if ($date->hour == Carbon::parse($interval)->addHours(2)->hour) {
$actualHour2 = Carbon::parse($interval)->subHours(2)->hour;
if (strlen($actualHour2) == 1) $actualHour2 = "0$actualHour2";
return $date->format("Y-m-d $actualHour2:00:00");
}
}
return $date->format('Y-m-d H:00:00');
});
For instance, I should be seeing all of the checks for the hours 7 and 8 within the 07 key, but instead I'm seeing data for just one hour (hour 11)?
The best thing to use whenever you need time slice(s) is DateInterval or better CarbonInterval. What they give you is the ability to loop over those slices and do equality/unequlity operation of your sample data this way you can easily organise your data by those time slices to their respective "slots"
Here is an general idea on how to
$intervals = \Carbon\CarbonInterval::hours(3)->toPeriod('2021-05-02 13:00:00', '2021-05-03 13:00:00');
//we get time slots of 3 hours between provided datetimes
foreach ($intervals as $date) {
$dtArr[] = strtotime($date->format('Y-m-d H:i:s')); //we collect those "time markers"
}
$result = [
'first'=> 0,
'second'=>0.
'third'=>0,
'forth'=>0,
'fifth'=>0,
'sixth'=>0,
'seventh'=>0,
'eighth'=>0
]; //array to accumulate your aggregations to correct time slot
foreach ($uptimeData as $sample) {
//loop over sample set
$ordinality = getSlotNo($sample->checked_at); //eg. third
//read the accumulated total in $result and add this too
$result[$ordinality] += 1;
}
function getSlotNo($dt){
$ts = strtotime($dt);
//eg. say greater than or equal to "13:00" but smaller than "16:00" -> You go in first slot
if($ts>=$dtArr[0] && $ts<$dtArr[1]){
//first slot
return 'first';
}
elseif($ts>=$dtArr[1] && $ts<$dtArr[2]){
//eg. say greater than or equal to "16:00" but smaller than "19:00" -> You go in second slot
//second slot
return 'second';
}
elseif($ts>=$dtArr[2] && $ts<$dtArr[3]){
//third slot
return 'third';
}
// and so on
}
UPDATE
Try something like this may be, modify the slot getter to "look ahead" and decide the result
$i=0;
foreach ($intervals as $date) {
$dtArr[] = strtotime($date->format('Y-m-d H:i:s')); //we collect those "time markers"
$result['int_'.$i] = 0;
$i++;
}
//fake data
$uptimeData=collect([
(object)['checked_at'=>'2021-05-03 10:10:00'],
(object)['checked_at'=>'2021-05-03 11:20:00'],
(object)['checked_at'=>'2021-05-03 12:20:00'],
(object)['checked_at'=>'2021-05-03 13:20:00'],
(object)['checked_at'=>'2021-05-03 14:20:00'],
]);
foreach ($uptimeData as $sample) {
//loop over sample set
$ordinalInfo = getSlotNo($sample->checked_at, $dtArr); //eg. third
//read the accumulated total in $result and add this too
if($ordinalInfo['match']){
$result['int_'.$ordinalInfo['index']] += 1;
}
}
/**
* #param $dt
* #return int index in $dtArr this value belongs to
*/
function getSlotNo($dt, $dtArr){
$ts = strtotime($dt);
$info = [];
for($i =0; $i<count($dtArr); $i++){
if(!empty($dtArr[$i+1])){ // if not reached the last item ie. still there's a next
if($ts>=$dtArr[$i] && $ts<$dtArr[$i+1]){
//i'th slot
$info=['match'=>true,'index'=>$i];
break;
}
}else{
// at last item ie. ( $i == count($dtArr)-1 )
if($ts<=$dtArr[$i])
$info=['match'=>true,'index'=>$i];
else
$info=['match'=>false,'index'=>NULL];
}
}
return $info;
}
Related
i am a creating a system where users can do investments and gain interest. Now i want to calculate interest per day in real time, so the interest accumulate by day until the end date . I want this to be in real time, meaning i want to calculate the interest and up until the end date but my code only displays the results all at once. For instance if the start day is monday and end date is is friday the code should accumulate the interest everyday until friday in real time.
$interest = 89;
$startdate = '2022-06-06';
$enddate = '2023-06-04'
$days = (($enddate - $startdate) / 60 / 60 / 24);
$previousday = date('Y/m/d',strtotime("-1 days"));
$currentdate = date('Y/m/d');
$totalEarning = 0;
$Earningsbyday = [];
if ($startdate <= $enddate && $previousday < $currentdate ) {
for ($i = 0; $i < $days; $i++) {
$totalEarning += $interest;
$Earningsbyday[$i] = $totalEarning;
}
echo var_dump($Earningsbyday)."\n"."\n";
}
You can do this quite simply. First, use create_date to create a DateTime in PHP. With these two converted dates, we can pass it to date_diff and format the amount of days between the two dates.
From here, we can use array_fill to fill an array of nth days with the interest. The total is simply just the array_sum.
We can now array_walk and calculate the previous day + the current interest to build the array you're expecting.
class InterestCalculator
{
private array $dailyInterest = [];
private static int $interest = 89;
public function calculateDailyInterest(string $start, string $end): array
{
$this->dailyInterest = array_fill(0, date_diff(date_create($start), date_create($end))->format('%a'), self::$interest);
$total = array_sum($this->dailyInterest);
array_walk($this->dailyInterest, function (int &$value, int $key): void {
$value = ($this->dailyInterest[$key -1] ?? 0) + $value;
});
return compact('total') + ['dailyInterest' => $this->dailyInterest];
}
}
// total => 32307, dailyInterest => [0 => 89, 1 => 178, 2 => 267, ....]
print_r((new InterestCalculator)->calculateDailyInterest('2022-06-06', '2023-06-04'));
References:
https://www.php.net/manual/en/function.date-create.php
https://www.php.net/manual/en/datetime.diff.php
https://www.php.net/manual/en/function.array-fill.php
https://www.php.net/manual/en/function.array-walk.php
https://www.php.net/manual/en/language.references.pass.php
https://www.php.net/manual/es/function.compact.php
Hopefully, this completes what you need. Feel free to comment if I miss understood your question.
See it working over at 3v4l.org
this is more of a logic question. I have this table design for all the attendance logs of every user
I can calculate the time differences and number of hours worked for each row.
What i need to do is to calculate the hours for each date and subtract 8 hours for each day to get the overtime hours.
for($i=0; $i < count($logs)-1; $i++ ){
if($logs[$i]->status == 'out'){
if ($i != 0) {
$dattime1 = new Carbon($logs[$i]->log_date.' '. $logs[$i]->log_time);
if ($logs[$i-1]->status == 'in') {
$dattime2 = new Carbon($logs[$i-1]->log_date.' '. $logs[$i-1]->log_time);
$diff = $dattime1->diff($dattime2);
$hr_array[] = $diff->h;
$min_array[] = $diff->i;
$sec_array[] = $diff->s;
$arr[] = $diff
}
}
}
}
I can get minutes and hours each row. I want to group the rows with the same dates and calculate the total hours worked so i can get the overtime.
Thanks
I didn't test this out but it could give you an idea on how to solve it
<?php
// for each date, keep adding difference in minutes here
// use date as array key
$workedMinutesPerDay = [
// '2019-11-04' => minutes value
];
// keep 'in' date value here
$inDate = '';
foreach($logs as $log) {
$date = new Carbon($log->log_date.' '. $log->log_time);
// if 'in' value, remember the date and time and continue to next row
// next row should be out
if ($log->status = 'in') {
$inDate = $date;
continue;
}
// if you will always have 'in' then 'out' in order, this code is fine
// but you should handle inconsistencies here
// use remembered 'in' value stored in $inDate to calculate the diff in minutes
$diffInMinutes = $date->diffInMinutes($inDate);
// use $workedMinutesPerDay array with 'log_date' as key
// if we don't have the entry for the day, add new one
if (empty($workedMinutesPerDay[$log->log_date])) {
$workedMinutesPerDay[$log->log_date] = $diffInMinutes;
}
// date entry was already there, add current diffInMinutes to previous value
$workedMinutesPerDay[$log->log_date] += $diffInMinutes;
}
After running this, you would have array like this (hypothetical values), didn't calculate real values from the picture
$workedMinutesPerDay = [
'2019-11-04' => 100, // total minutes for the day
'2019-11-05' => 200,
'2019-11-08' => 300,
'2019-11-11' => 400,
'2019-11-12' => 500,
];
I've got a script that sets up a array of dates which form a dropdown. The array is set up by some variables about the current date and an fixed offset for the last date (two weeks). Stripped down this is what it looks like:
public function getDatesOptionArray()
{
$datesArray = array();
$displayDate = Mage::getModel('core/locale')->storeDate();
$displayDate->add($this->_startDaysOffset, Zend_Date::DAY);
$dayOffset = $this->_startDaysOffset;
do
{
$dayofweek = date('w', strtotime($displayDate->toString(Varien_Date::DATE_INTERNAL_FORMAT)));
$datesArray[$displayDate->toString(Varien_Date::DATE_INTERNAL_FORMAT)] = Mage::helper('core')->formatDate($displayDate, Mage_Core_Model_Locale::FORMAT_TYPE_FULL);
$displayDate->add('1', Zend_Date::DAY);
$dayOffset++;
} while ($dayOffset <= $this->_endDaysOffset);
return $datesArray;
}
The thing is I want to leave out all the 'Sunday' options, and I've got the $dayofweek variable for each, where sunday is 0. I've tried to wrap the whole thing inside the do function in an if-statement (if $dayofweek !== 0), set an if ($dayofweek == 0) { continue;} and every other trick I could think of, but I get only either of these results
1: Only the first date is shown
2: All dates until the first sunday are shown, none after that
3: All dates are shown
I think I might be missing the point on the do-while loop; how do I exclude if $dayofweek == 0?
For me is something like this, i use while because Do...While(...) the first time it will not check your condition you'll enter your loop at last 1 time, and when you use while(...){} every time your program will check your condition
public function getDatesOptionArray()
{
$datesArray = array();
$displayDate = Mage::getModel('core/locale')->storeDate();
$displayDate->add($this->_startDaysOffset, Zend_Date::DAY);
$dayOffset = $this->_startDaysOffset;
while ($dayOffset <= $this->_endDaysOffset)
{
$dayofweek = date('w', strtotime($displayDate->toString(Varien_Date::DATE_INTERNAL_FORMAT)));
if ($dayofweek != 0) {
$datesArray[$displayDate->toString(Varien_Date::DATE_INTERNAL_FORMAT)] = Mage::helper('core')->formatDate($displayDate, Mage_Core_Model_Locale::FORMAT_TYPE_FULL);
}
$displayDate->add('1', Zend_Date::DAY);
$dayOffset++;
}
return $datesArray;
}
Let say the array is
$date = array(
2017-10-12,
2017-10-13,
2017-10-14,
2017-10-15,
2017-10-16,
2017-10-17,
2017-10-18,
2017-10-19,
2017-10-20,
2017-10-21,
2017-10-22,
2017-10-23,
2017-10-24,
2017-10-25
);
Now do this for remove sunday's dates
$i=0;
do{
if(date('w',strtotime($date[$i]))>0) $dateArray[] = $date[$i];
$i++;
} while ($i<count($date));
echo "<pre>";print_r($dateArray);
return $datesArray;
$dateArray will give you dates without sundays.
i have multiple jquery fullcalendars on a page which is used for machine scheduling in our software.
each calender will make a ajax call once a specific input for the workload changes.
In my AJAX script i need to calculate the dates that will be disabled in the calendar based upon the already planned time of the machine and whether or not there are workers with enough time avail on that day.
so here is what i got so far:
$retval = Array();
$objectid = $_REQUEST["objectid"];
$type = $_REQUEST["type"];
$amount = $_REQUEST["amount"];
$date = $_REQUEST["date"];
$month = date("m",strtotime($date));
$year = date("Y",strtotime($date));
if ($month == date("m") && $year == date("Y"))
{
$start = mktime(0,0,0,date("m"),date("d"),date("Y"));
} else {
$mktime1 = mktime(0,0,0,$month,24,$year);
$start = strtotime('-1 month',$mktime1);
}
$mktime2 = mktime(0,0,0,$month,6,$year);
$end = strtotime('+2 month',$mktime2);
$job_arr = Array();
$jobs = PlanningJob::getAllJobs(" AND start>".$start." AND end<".$end);
foreach ($jobs as $job)
{
$date = mktime(0,0,0,date("m",$job->getDate()),date("d",$job->getDate()),date("Y",$job->getDate()));
$job_arr[$date] = 0;
if ($job->getTime()>0)
$job_arr[$date] += $job->getTime();
elseif ($job->getPlannedTime()>0)
$job_arr[$date] += $job->getPlannedTime();
}
if ($type == "ME"){
$me = new Machineentry($objectid);
$machine = $me->getMachine();
for ( $i = $start; $i <= $end; $i = $i + 86400 ) {
$date = mktime(0,0,0,date("m",$i),date("d",$i),date("Y",$i));
$total_seconds = $machine->getRunningtimeForDay($date);
if (isset($job_arr[$date]))
{
$total_seconds -= ($job_arr[$date]*60*60);
}
if ($total_seconds < ($amount*60*60))
$retval[] = date("d.m.Y",$date);
}
} else if ($type == "OP")
{
$op = new Orderposition($objectid);
$jobart = new Article($op->getObjectid());
}
now i wonder if there is a better approach to calculating for so many dates (date range) at once - performance wise. as this one is quite heavy and takes a couple of seconds which really slows down our scheduling process.
i would appreciate any help/advice!
Kind Regards,
Alex
[EDIT] some more info's on what i need to output:
One Array containing: all dates within the range
and within this array per date:
1. all users having spare time on that day and the specific amount of time
2. total time scheduled on the machine that day
Second Array
Per date: if not enough 'free' time on the machine only the date so it can be disabled in the calendar
Here is a function I wrote a while back which will take two dates and return an array of the range between them. I use this function on a variety of things and it's very fast.
Dates provided for $dateFrom and $dateTo should be in YYYY-MM-DD format.
function createDateRangeArray($dateFrom,$dateTo)
{
$arrayRange=array();
$dateFrom=mktime(1,0,0,substr($dateFrom,5,2), substr($dateFrom,8,2),substr($dateFrom,0,4));
$dateTo=mktime(1,0,0,substr($dateTo,5,2), substr($dateTo,8,2),substr($dateTo,0,4));
if ($dateTo>=$dateFrom)
{
array_push($arrayRange,date('Y-m-d',$dateFrom));
while ($dateFrom<$dateTo)
{
$dateFrom+=86400;
array_push($arrayRange,date('Y-m-d',$dateFrom));
}
}
return $arrayRange;
}
I am trying to compare times and I'm not entirely sure the best way to handle this.
I have an array of times that is un-able to edit.
array("500", "1100", "1700", "2300");
500 = 5:00am etc...
If it is 6 or 7am, what kind of logic can I run to find out if it is 7am, what time is closer 5am or 10am?
I don't think it's complex, but i'm just trying to figure out a decent solution vs. me trying to hack something together.
Any help or direction would be greatly appreciated!
Let's start with the array you have:
$values = array("500", "1100", "1700", "2300");
What we want is to format it to a valid time string, that is easy, we just insert ":" in the right position. For that I created a function:
function Format($str)
{
$length = strlen($str);
return substr($str, 0, $length - 2).':'.substr($str, $length - 2);
}
Now we can get valid string that we can convert to unix time with strtotime. The problem now is to find the closer to the current time (which we get with time)
So, we can iterate over the array, convert them, calculate the difference with the current time (in absolute value) and pick the one that results in a lower number. Here is the code:
$now = time(); //current time
$best = false;
$bestDiff = 0;
for ($index = 0; $index < count($values); $index++)
{
$diff = abs($now - strtotime(Format($values[$index])));
if ($best === false || $diff < $bestDiff)
{
$best = $index;
$bestDiff = $diff;
}
}
It will leave the index of the closer time in $best and the difference with the moment of the computation in $bestDiff. Please note that this is all asumming that those times are in the same day and local time.
I adapted Theraot's solution to sort the array by the value's distance to the current time:
<?php
$values = array("500", "1100", "1700", "2300");
$now = time();
/**
* Format an integer-string to a date-string
*/
function format($str)
{
$length = strlen($str);
return substr($str, 0, $length - 2).':'.substr($str, $length - 2);
}
/**
* Callback to compare the distance to now for two entries in $values
*/
$compare = function ($timeA, $timeB) use ($now) {
$diffA = abs($now - strtotime(format($timeA)));
$diffB = abs($now - strtotime(format($timeB)));
if ($diffA == $diffB) {
return 0;
}
return ($diffA < $diffB) ? -1 : 1;
};
usort($values, $compare);
print_r($values);
Your desired result is in $values[0] now.
Note that this solution requires php version >= 5.3
the absolute value of the difference between two times
say 07:00 - 05:00 = 02:00, absolute value of 02:00 is still 02:00
07:00 - 10:00 = -03:00, absolute value of -03:00 is 03:00
In PHP you can convert your time-string to seconds using strtotime:
$time_one = strtotime("07:00");
$time_two = strtotime("05:00");
$time_three = strtotime("09:00");
Here's my solution:
// Input data
$values = array("500", "1100", "1700", "2300");
$time = "12:15";
// turns search time to timestamp
$search = strtotime($time);
// turns "500" to "5:00" and so on
$new = preg_replace('/^(\d?\d)(\d\d)$/','\1:\2', $values);
// converts the array to timestamp
$new = array_map('strtotime', $new);
// sorts the array (just in case)
asort($new);
// Some initialization
$distance = $closest = $result = $result_index = NULL;
// The search itself
foreach($new as $idx => $time_stamp)
{
$distance = abs($time_stamp - $search);
if(is_null($closest) OR $closest > $distance)
{
$closest = $distance;
$result_index = $idx;
$result = $time_stamp;
}
}
echo "The closest to $time is ".date('H:i', $result)." ({$values[$result_index]})";