2

I am currently generating an html table which contains dates in it's header

Those dates are the supposedly first days of each week through the whole year (the displayed year may change according to a filter), and they are generated by the following code

<?php $firstDay = strtotime($year . 'W' . str_pad($week, 2, '0', STR_PAD_LEFT)); ?>
<td><?= date('d/m/Y', $firstDay) ?></td>

This is inside a for loop that makes the $week go from 1 to the number of weeks in the whole year.

What I have noticed is that for some years, the last monday of December from the previous year is included in there too, such as 29/12/2014 when I select 2015 in the filter.

So far, to determine the number of weeks that are in a month, I used to count the number of mondays in said month programmatically and used it as the colspan for the row above the dates row, and so, there are uncounted weeks for january for some years, which makes the table shift.

The rendered html for the year 2015 would look like this

<table>
    <thead>
        <tr>
            <th colspan="4">January</th>
            <th colspan="4">February</th>
            [...]
        </tr>
        <tr>
            <!-- Under January cell -->
            <th>29/12/2014</th>
            <th>05/01/2015</th>
            <th>12/01/2015</th>
            <th>19/01/2015</th>
            <!-- Under February cell -->
            <th>26/01/2015</th>
            <th>02/02/2015</th>
            [...]
        </tr>
    </thead>
    [...]

And after searching and testing a lot of week per month counters, none of those that I found seem to include that last monday from the previous year.

Could anyone find out how to count the weeks including that extra week from the previous year when it occurs?

prout
  • 317
  • 5
  • 18
  • So basically the question is how to find out the number of weeks in a year? or the number of Mondays in a year? Can you please reformulate the whole post in a single question? the essence of it.. – Keloo Jul 02 '18 at 13:44
  • Also consider posting the `for loop` that creates the whole thing :) – Keloo Jul 02 '18 at 13:46
  • 1
    @Keloo It would be the number of ISO weeks for each month, and I think I just figured it out, I'll try something and post the answer if it works – prout Jul 02 '18 at 13:49

3 Answers3

5

The number of weeks falling in a given month is basically the number of days in a month, plus an offset that depends on which day of a week the month started in, divided by 7. Here is a solution using DateInterval and DatePeriod:

/**
 * Get number of weeks starting *Monday*, occuring in a given month.
 * A week occurs in a month if any day from that week falls in the month.
 * @param $month_date is expected to be DateTime.
 * @param $count_last count partial week at the end of month as part of month.
 */
function weeks_in_month($month_date, $count_last=true) {
  $fn = $count_last ? 'ceil' : 'floor';
  $start = new DateTime($month_date->format('Y-m'));
  $days = (clone $start)->add(new DateInterval('P1M'))->diff($start)->days;
  $offset = $month_date->format('N') - 1;
  return $fn(($days + $offset)/7);
}

/**
 * Wrapper over weeks_in_month() to get weeks in month for each month of a given year.
 * @param $year_date is expected to be a DateTime.
 */
function year_month_weeks($year_date, $count_last=true) {
  $start = new DateTime($year_date->format('Y') . '-01');
  $year_month_weeks = [];
  foreach(new DatePeriod($start, new DateInterval('P1M'), 11) as $month) {
    $year_month_weeks[] += weeks_in_month($month, $count_last);
  }
  return $year_month_weeks;
}

print_r(year_month_weeks(new DateTime('2008-01'), true));

Gives:

Array
(
    [0] => 5
    [1] => 5
    [2] => 6
    [3] => 5
    [4] => 5
    [5] => 6
    [6] => 5
    [7] => 5
    [8] => 5
    [9] => 5
    [10] => 5
    [11] => 5
)

This can be validated against output of cal. For the first 6 months of 2008 should have 6 weeks in March, Jun, 5 for the rest:

>ncal -Mb -m 1 -A 5 2008
                            2008
      January               February               March          
Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  
    1  2  3  4  5  6               1  2  3                  1  2  
 7  8  9 10 11 12 13   4  5  6  7  8  9 10   3  4  5  6  7  8  9  
14 15 16 17 18 19 20  11 12 13 14 15 16 17  10 11 12 13 14 15 16  
21 22 23 24 25 26 27  18 19 20 21 22 23 24  17 18 19 20 21 22 23  
28 29 30 31           25 26 27 28 29        24 25 26 27 28 29 30  
                                            31                    

       April                  May                   June          
Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  Mo Tu We Th Fr Sa Su  
    1  2  3  4  5  6            1  2  3  4                     1  
 7  8  9 10 11 12 13   5  6  7  8  9 10 11   2  3  4  5  6  7  8  
14 15 16 17 18 19 20  12 13 14 15 16 17 18   9 10 11 12 13 14 15  
21 22 23 24 25 26 27  19 20 21 22 23 24 25  16 17 18 19 20 21 22  
28 29 30              26 27 28 29 30 31     23 24 25 26 27 28 29  
                                            30       
spinkus
  • 7,694
  • 4
  • 38
  • 62
  • the problem with you solution is that it stacks with other months, as it can be seen with february and march, in which february's last week which consists of one day is counted, and the rest is also counted in march, but even so, I'm going to have a look at those classes you've used – prout Jul 03 '18 at 06:28
  • Hi. Oh, thought that is what you wanted. Must have misread your intention. The intended behavior is; if any part of a week falls in a month it is counted as being a week of that month, so yeah, a given week can fall in 2 months. Anyway, yep, good idea to look at these two classes. Should give a cleaner solution. – spinkus Jul 03 '18 at 07:12
  • P.S. actually if you change the `ceil()` to `floor()` I think it might give the counts you wanted - last week not counted if falls a partially in a month. Updated with option. – spinkus Jul 03 '18 at 07:31
1

Recently did a listing quite simliar with Carbon. Here a Snipped with some example output:

<?php

use Carbon\Carbon;

$counter = Carbon::createFromDate(2018, 1, 1);
$end = $counter->copy()->endOfYear();
$months = [];

for (; $counter <= $end; $counter->addWeek()) {
    // just to avoid E_NOTICE
    if (!isset($months[$counter->month])) {
        $months[$counter->month] = [
            'count' => 0,
            'weekstarts' => [],
        ];
    }

    ++$months[$counter->month]['count'];
    $months[$counter->month]['weekstarts'][] = $counter->copy();
}

foreach ($months as $month) {
    printf('Month has %d weeks', $month['count']);
    echo PHP_EOL;
    foreach ($month['weekstarts'] as $num => $weekStartDate) {
        printf('Week %d starts at %s', $num + 1, $weekStartDate);
        echo PHP_EOL;
    }
    echo PHP_EOL;
}
binzram
  • 530
  • 4
  • 13
  • I'm going to try this when I have the opportunity, as I'm just discovering what Carbon is – prout Jul 02 '18 at 14:34
  • that library is quite interesting indeed – prout Jul 03 '18 at 12:13
  • I chose this answer as it includes a library which can be used to do so much more with dates if there are more manipulations to use. Anyway, thank you everyone, you all had my +1 – prout Jul 03 '18 at 14:41
0

So, this was my turnaround, using the function used to generate the first days of each week

function getIsoWeeksInYear($year) {
    $date = new DateTime;
    $date->setISODate($year, 53);
    return ($date->format("W") === "53" ? 53 : 52);
}

function getWeeksPerMonth($year) {
    $allWeeks = $this->getIsoWeeksInYear($year);
    $allFirstDays = [];

    for ($i = 0; $i < $allWeeks; $i++) {
        $allFirstDays[] = date('m/Y', strtotime($year . "W" . str_pad($i, 2, "0", STR_PAD_LEFT)));
    }
    $countWeek = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    foreach ($allFirstDays as $firstDay) {
        $theMonth = explode('/', $firstDay);
        for ($i = 1; $i < 13; $i++) {
            if ($theMonth[0] == $i) {
                $countWeek[$i - 1]++;
            }
        }
    }
    return $countWeek;
}

So, the idea was to get a whole array of week numbers and use it in a loop for the colspans

<?php $span = getWeeksPerMonth($year) ?>
<?php for ($i = 1; $i < 13; $i++): ?>
    <th colspan="<?= $span[$i - 1] ?>"><?= strftime('%B', mktime(0, 0, 0, $i)) ?></th>
<?php endfor ?>

And for the row below, I just use what I already used earlier in my question

<?php for ($i = 0; $i < getIsoWeeksInYear($year); $i++): ?>
    <?php $firstDay = strtotime($year . 'W' . str_pad($week, 2, '0', STR_PAD_LEFT)); ?>
    <th><?= date('d/m/Y', $firstDay) ?></th>
<?php endfor ?>

So, there may be some previous year included week in january and some next year excluded week for december.

Maybe, there are still some stuff to tweak somewhere. Anyway, I'll test out the other anwers and I guess I'll mark the easiest one to use as the answer.

Edit

It looks like I didn't have to explode the obtained date, as I initally thought I would have to use the year in some condition for the week starting the previous year

prout
  • 317
  • 5
  • 18