Advanced date filtering with multiple future date variables

Hi there,

My goal here is to filter events by multiple dates. Some of my events have recurring dates, which means that any of the following fields could be defined by the panel user:

end_date
addl_end_date1
addl_end_date2
addl_end_date3

I want to initially sort by end_date then cycle through the additional dates. For example, if end_date is in the past, then the filter automatically filters that out and then searches for the next available date that is in the future. Here’s what I thought might work:
$event = page('events') ->children() ->visible() ->filter(function($child) { return $child->date(null, 'end_date') >= time() || $child->date(null, 'addl_end_date1') >= time() || $child->date(null, 'addl_end_date2') >= time() || $child->date(null, 'addl_end_date3') >= time(); });

I looked at this: Filtering by Date, but the way the filter above is set up, every single event is filtered out, regardless of date. Has anyone filtered their events using multiple future dates? Any ideas?

Thanks, all!

@aftereffectsmagic

I don’t quite understand why the above code does not work for you. In a brief test I did, everything worked as expected, so if any of several dates is in the future, the event is part of the resulting collection.

Thanks for the reply, @texnixe!

Strange… can you post the code you’re using, just so I can make sure I’m implementing it correctly? I’m defining the event variable first thing, so that should be ok, but obviously the filter isn’t working correctly. I have debug enabled and there’s no error code- the events just don’t appear.

My previous code doesn’t filter properly, but it at least shows the events:

foreach(page('events')->children()->visible()->sortBy('addl_end_date1', 'asc', 'addl_end_date2', 'asc', 'addl_end_date3', 'asc')->limit(6) as $event):

Another approach I thought of:

foreach(page('events')->children()->visible()->filter(function($child) { return $child->end_date() > time() or strtotime($child->addl_end_date1) > time() or strtotime($child->addl_end_date2) > time() or strtotime($child->addl_end_date3) > time() })->limit(6) as $event):

Anything else I can try with the original code snippet, since it seems to be working perfectly for you?

Thanks!!!

@aftereffectsmagic

Update! I seem to be able to filter correctly using this:

foreach(page('events')->children()->visible()->filter(function($child) { return $child->date(null, 'end_date') >= time() || $child->date(null, 'addl_end_date1') >= time() || $child->date(null, 'addl_end_date2') >= time() || $child->date(null, 'addl_end_date3') >= time(); }) as $event):

I would like to have cleaner code than that though and I don’t understand why my original snippet doesn’t work hmmm. Any ideas on that?

Since I got the general idea working, phew, I went ahead and added in a date increase so older events would stay a bit longer and have this:

foreach(page('events')->children()->visible()->limit(6)->filter(function($child) { return strtotime('+4 days', strtotime($child->end_date())) > time() || strtotime('+4 days', strtotime($child->addl_end_date1())) > time() || strtotime('+4 days', strtotime($child->addl_end_date2())) > time() || strtotime('+4 days', strtotime($child->addl_end_date3())) > time(); }) as $event):

The events only stay two days before or after their respective dates even with the +4 days, which is odd- that’s a minor issue though. I can live with this :slight_smile: But now that the events are filtered, I want to arrange / sort them by the next future date they have- if they are still displaying but their event date is past, I want them to appear first.

For example:

August 23 (past but staying due to date increase) | August 29 | Sept 14

I could sort by begin_date but that wouldn’t give me the next future date, since the next future date could be addl_end_date1 or addl_end_date2 or addl_end_date3.

What would the best sorting approach be? Basically, this is the concept:

->sortBy('end_date', '||', 'addl_end_date1', '||', 'addl_date_3', '>', time(), 'asc')

I’m pretty sure that wouldn’t work, but I need the sortBy function to seek all future dates then sort accordingly.

Thanks a million!!
@aftereffectsmagic

You can get cleaner code by defining the variable first and then loop through that afterwards:

<?php
// e.g. in your controller
$events = page('events')->children()->visible()->filter(function($child) {
return strtotime('+4 days', strtotime($child->end_date())) > time() || strtotime('+4 days', strtotime($child->addl_end_date1())) > time() || strtotime('+4 days', strtotime($child->addl_end_date2())) > time() || strtotime('+4 days', strtotime($child->addl_end_date3())) > time();
});

// then in your template
<?php
foreach($events as $event): ?>
  // some code
<?php endforeach ?>

As regards the sorting, I need to get my brain working first :wink:

Edit: I think that sorting algorithm is beyond my brain capacity as more than one date can lie in the future.

An idea that comes to mind: How about using a panel.page.update hook that compares the dates and writes the nearest in the future to a new field “nearest_future_date”. Then you would be able to sort by that single field.

Hi there!

Great idea on the panel.page.update hook. Here’s what I came up with- I merged all four date fields into an array but it still throws an error- says the $events variable at the beginning of the array_filter line is unexpected. I thought maybe combining all the dates and comparing them directly would avoid creating a new field and then sorting by that field.

Can you take a look and see what’s going on here and if I have this basic concept down correctly?

Thanks to @servicethinker for the original idea as posted here: Auto Sort and Auto Filter An Structured Event. My events and the dates are not in a structure field, but I took the basic concept as inspiration.

event-sorting-by-future-date.php

<?php
// Sorting events structure field by next future date
kirby()->hook('panel.page.update', function($page) {
  if(isset($page->content()->data['events'])) {
    $events = $page('events')->children();
    $end_date = $page('events')->end_date();
    $addl_end_date1 = $page('events')->addl_end_date1();
    $addl_end_date2 = $page('events')->addl_end_date2();
    $addl_end_date3 = $page('events')->addl_end_date3();
    // Merge all possible date fields into one array
    $alldates = array_merge($end_date, $addl_end_date1, $addl_end_date2, $addl_end_date3)
    // Filtering expired dates
    $events = array_filter($events, function($k) {
      $today = date("Y-m-d");
      return strcmp($k['alldates'], $today) >= 0;
    });
    // Sorting future dates
    usort($events, function($a, $b) {
      return strcmp($a['alldates'], $b['alldates']);
    });
    // Save result
    try {
      $page->update(array(
        'events' => yaml::encode($events)
      ));
    }
    catch(Exception $e) {
      echo $e->getMessage();
    }
  }
});

Thanks for taking a look!

@aftereffectsmagic

I didn’t have time this afternoon, but on second thought it just dawned on me that the hook idea is probably not as brilliant as I thought, as you need to check your dates at runtime, not at the time of updating the page.

Nevertheless, there are some problems with your code:

  1. Creating a yaml array of the children does not make sense, that method is intended for structure fields.
  2. What is $page->events()->children()? You probably mean page('events')->children()?

I take it that your events are children of a page called events, right?

So maybe it would be a better idea to calculate the nearest future date at runtime rather than at page.update.

Thanks for the quick reply! You are right, a couple of errors in my original code and I’ve edited my code accordingly. Yes, my events are children of a page called events, you are correct.

I think it would be best to calculate at runtime instead of using panel.page.update- you’re 100% correct on that. With that in mind, I went back to my snippet and integrated the above, but unfortunately array_merge does not work on date strings themselves- the error thrown specifically says that the variables are not arrays. I have four dates I’d like to merge into one array and then search in that array for the next future date and sort according to that. Does that make sense as far as the correct approach?

If so then this is how it would go:

<?php foreach(page('events')->children()->visible()->limit(6)->filter(function($child) {
    return strtotime('+4 days', strtotime($child->end_date())) > time() || strtotime('+4 days', strtotime($child->addl_end_date1())) > time() || strtotime('+4 days', strtotime($child->addl_end_date2())) > time() || strtotime('+4 days', strtotime($child->addl_end_date3())) > time();
}) as $event): ?>
                  <?php
                  date_default_timezone_set('US/Eastern');
                  $tmpDate = getdate(0);
                  $currentDate = getdate();
                  $somedate = strtotime($event->end_date());
                  $eventdate = strtotime($event->begin_date());
                  $eventbegintime = strtotime($event->begin_time());
                  $eventendtime = strtotime($event->end_time());
                  $addldate1 = strtotime($event->addl_end_date1());
                  $addldate2 = strtotime($event->addl_end_date2());
                  $addldate3 = strtotime($event->addl_end_date3());
                  $alldates = array_merge($somedate, $addldate1, $addldate2, $addldate3);
                  // Filtering expired dates
                  $events = array_filter($event, function($k) {
                    $today = date("Y-m-d");
                    return strcmp($k['alldates'], $today) >= 0;
                  });
                  // Sorting future dates
                  usort($events, function($a, $b) {
                    return strcmp($a['alldates'], $b['alldates']);
                  });

Or am I making it too complicated? Is there an easier way to sort four strings together?

Thanks!

I’ll try to wrap my brain around that tomorrow, I need to get some sleep now :sleeping:

Ok, here’s what I’ve come up with.

We use the callback function to update the page with a new field:

The callback function:

<?php
function getNearestFutureDate($page)  {
  // create an array of all date field names
  $fields = array('end_date', 'another_date', 'some_date');
  // get current time stamp
  $current = time();
 
  // loop through the $fields array
  foreach($fields as $field) {
    
    // check if the field is in the future
    // you need to change this condition to meet your 4 day negative offset
    if($page->date(null, $field) > time()) {
      // if so, add it to the $futureDates array
      $futureDates[$field] = $page->$field()->value();
      // get the key of the array element with the lowest date value
      $nearest_futureDate = array_keys($futureDates, min($futureDates))[0];
    }
  }
  // set the "nearest" field to the value of the $nearest_futureDate field
  $page->update(array(
  'nearest' => $page->$nearest_futureDate(),
  ));
  return $page;
}

The controller

<?php

return function($site, $pages, $page) {

  $projects = $page->children()->filter(function($child) {
    return strtotime('+4 days', strtotime($child->end_date())) > time() || strtotime('+4 days', strtotime($child->some_date())) > time() || strtotime('+4 days', strtotime($child->another_date())) > time();
  });
  
  // apply the getNearestFutureDate callback to the pages collection
  // maybe this can be done via a cron job instead of in the controller
  // or the callback could add a map_modifield timestamp to the parent page
  // and the map method would then be called depending on the value of that field
  $projects->map('getNearestFutureDate'); 

  return compact('projects');

};

The template:

<?php
// filter collection by newly created field
foreach($projects->sortBy('nearest', 'asc') as $project) {
  // some code here;
}
```

I have no idea if this is the best way to go about this but it seems to work. Maybe someone with more programming skills than myself (who are easy to find, @lukasbestle) can check this out and improve on it.

Edit. It would probably make sense to call the mapping method only once a day instead of on every page load.

While the above solution works, it is probably error prone if the data can’t be written to file. So here is an alternative solution:

(I used the starterkit projects to test this, so you would have to change the variables and field names)

<?php
// no need to filter projects in advance as the dates get filtered
$projects = page('projects')->children()->visible();
$dateFields = array('date1', 'date2', 'date3');
$today = date('Y-m-d');
$allDates = array();

foreach($dateFields as $field) {
  $dates = $projects->pluck($field, ',', true);
  $allDates = array_merge($allDates, $dates);
}

$allDates = array_unique($allDates);
asort($allDates);

$allDates = array_filter($allDates, function($date) use($today){
  return $date >= $today;
});

foreach ($allDates as $date):

  foreach($projects as $project):

    foreach($dateFields as $field):
      if($project->date('Y-m-d', $field) > $today) {
        $futureDates[$field] = $project->$field()->value();
      }
    endforeach;

    $nearest_futureDate = array_keys($futureDates, min($futureDates))[0];

    if($project->date('Y-m-d', $nearest_futureDate) == $date) {
      echo $project->$nearest_futureDate() . ": " . $project->title() . "<br />";
    }

  endforeach;

endforeach;
?>

Good afternoon from the West Coast (US)! Bright and sunny here as I try to problem-solve this issue. :wink: Many thanks for your help with your recent comment. I modified your code a bit because $futureDates was not defined and I think you probably meant to define it as:

$allDates = array_unique($allDates);
asort($allDates);

$futureDates = array_filter($allDates, function($date) use($today){
  return $date >= $today;

Let me know if that is correct. When I implement the changes you suggested in your recent post, I get the following error:

Fatal error: Method name must be a string in /data/16/1/21/162/1510651/user/1625459/htdocs/kirby/core/page.php on line 754

Is there some reason I’m throwing this error? Here’s my modified code:

$dateFields = array('end_date', 'addl_end_date1', 'addl_end_date2', 'addl_end_date3');
$today = date('Y-m-d');
$allDates = array();

foreach($dateFields as $datefield) {
  $dates = $events->pluck($datefield, ',', true);
  $allDates = array_merge($allDates, $dates);
}

$allDates = array_unique($allDates);
asort($allDates);

$futureDates = array_filter($allDates, function($date) use($today){
  return $date >= $today;
});

foreach ($allDates as $date):

  foreach($events as $indlevent):

    foreach($dateFields as $datefield):
      if($indlevent->date('Y-m-d', $datefield) > $today) {
        $futureDates[$datefield] = $indlevent->$datefield()->value();
      }
    endforeach;

    $nearest_futureDate = array_keys($futureDates, min($futureDates))[0];

    if($indlevent->date('Y-m-d', $nearest_futureDate) == $date) {
      echo $indlevent->$nearest_futureDate() . ": " . $indlevent->title() . "<br />";
    }
  endforeach;

endforeach;
?>

Thanks for taking a look!! Really appreciate you working through this with me. I think once we have solved this I will create another post explaining the whole thing so it’s easily used as reference for someone else who might need filtering by multiple date fields. :slight_smile:

Cheers,

@aftereffectsmagic

Hi @aftereffectsmagic,

no $futureDates should just be defined as an empty array:

$futureDates = array();

This variable only stores the future dates of an individual project, so that we can then pick the one with the lowest value in $nearest_futureDate = array_keys($futureDates, min($futureDates))[0];. Otherwise, $allDates and $futureDates would be identical.

Thanks for your help again, @texnixe! I corrected the $futureDates array like you said above, thanks for clarifying.

I have a couple of issues when I try to implement the code:

  • All events are displayed with each one of their individual dates. This is not desirable, I just want one instance of each event to display no matter the date.
  1. Event names and dates are rendered incorrectly. The problem is that the panel user will not always enter additional dates, and when an event has no additional dates, the date displays as December 31st and says the event is in the past, even though it is not in the past. When an event is past, it will display with the name of the next future event, which is confusing to say the least. For example, “African Dance” has a past date, but it appears as “Community Happy Hour”, which is the next event with a future date. Weird! Is there a way to filter out date values that == '0' or are false before processing the array?

These are the big ones- then I also want to implement a pastDate array: see my idea building on your original array below. I should be able to then display events with both a pastDate of +/- 4 days (using strtotime). Then I can display all future events as well.

Current code with pastDate array idea:

foreach(page('events')->children()->visible() as $event): ?>
                    <?php
                  date_default_timezone_set('US/Eastern');
                  // no need to filter events in advance as the dates get filtered
                  $uid = $event->instructor();
                  $names = explode("-", $uid);
                  $isotope = "js-isotope-item-";
                  $eventtype = $event->eventtype();
                  //$time = strtotime($event->end_time());
                  $time = time();
                  //first name is $names[0]; first name
                  //last name is $unames[1]; last name
                  ?>
                  <?php
// no need to filter events in advance as the dates get filtered
$events = page('events')->children()->visible();
$dateFields = array('end_date', 'addl_end_date1', 'addl_end_date2', 'addl_end_date3');
$today = date('Y-m-d');
$allDates = array();
$pastDates = array();

foreach($dateFields as $field) {
  $dates = $events->pluck($field, ',', true);
  $allDates = array_merge($allDates, $dates);
  $pastDates = array_merge($pastDates, $dates);
}

$allDates = array_unique($allDates);
asort($allDates);

$allDates = array_filter($allDates, function($date) use($today){
  return $date >= $today;
});

foreach ($allDates as $date):

  foreach($events as $event):

    foreach($dateFields as $field):
      if($event->date('Y-m-d', $field) > $today) {
        $futureDates[$field] = $event->$field()->value();
      }
    endforeach;

    $nearest_futureDate = array_keys($futureDates, min($futureDates))[0];

  endforeach;

endforeach;

$pastDates = array_unique($pastDates);
asort($pastDates);

$pastDates = array_filter($pastDates, function($date) use($today){
  return $date < $today;
});

foreach ($pastDates as $date):

  foreach($events as $event):

    foreach($dateFields as $field):
      if($event->date('Y-m-d', $field) < $today) {
        $pastDates[$field] = $event->$field()->value();
      }
    endforeach;

    $nearest_pastDate = array_keys($pastDates, max($pastDates))[0];

  endforeach;

endforeach;
?>
<?php foreach(page('events')->children()->visible() as $event): ?>
  <li class="[ layout__item small-12 medium-12 large-4 medium-centered large-centered columns ] [ js-isotope-item  <?php echo $isotope . ($event->eventtype()) ?>  ]">
      <article>
        <a class="[ event js-event ] [ <?php echo 'event--' . ($event->eventfeature()) ?> ] " href="<?php echo $event->url() ?>">
<?php if($event->date('Y-m-d', $nearest_futureDate) <= $date): ?>
          <?php $somedate = strtotime($event->$nearest_futureDate()); ?>
          <time datetime="<?php echo $event->end_date()->relative() ?>" class="icon">
                              <em><?php
                                  echo date('l', $somedate);
                                  ?>
                              </em>
                              <strong><?php
                                  echo date('F', $somedate);
                                  ?>
                              </strong>
                              <span><?php
                                  echo date('j', $somedate);
                                  ?>
                              </span>
                    </time>
                   <h2 class="event__title"><?php echo $event->title()->html() ?></h2>
                    <div class="event-icon-box">
                    <?php if($image = $event->images()->sortBy('sort', 'asc')->first()): ?>
                    <img src="<?php echo $image->url() ?>" alt="<?php echo $event->title()->html() ?>" class="event__icon" >
                    <?php endif ?>
                    </div>
                  <strong class="event__subtitle"></strong>
                  <?php if ($eventtype!= 'happyhour'): ?>
                      <p class="event__author">Taught by <strong><?php echo ucwords($names[0]); ?> <?php echo ucwords($names[1]); ?></strong></p>
                  <?php elseif ($eventtype == 'happyhour'): ?>
                      <p class="event__author">Hosted by <strong><!--<a href='<?php echo $site->url() . '/' . '/' . 'instructors' . '/' . urlencode($event->instructor()) ?>'>--><?php echo strtoupper($names[0]); ?> <?php echo ucwords($names[1]); ?></strong></p>
                  <?php endif ?>
                  <!--if any event is in the future, but it is sold out -->
                  <?php if ($event->soldout()== 'true'): ?>
                            <p class="button radius tiny secondary disabled">
                            This Event Is Full- We Sold Out!
                            </p>
                  <?php elseif (($event->soldout()== 'false') && ($eventtype == 'happyhour')): ?>
                            <p class="event__link link button radius small">
                            Learn More
                            </p>
                    <!--if this is a non happy hour event in the future, we want the link to be a bit different -->
                    <?php elseif (($event->soldout() == 'false') && ($eventtype != 'happyhour')): ?>
                        <p class="event__link link button radius small">
                              Sign Me Up
                              </p>
                    <?php endif ?>
                  </a>
            </article>
            </li>
<?php else: ?>
          <?php $somedate = strtotime($event->$nearest_pastDate()); ?>
          <time datetime="<?php echo $event->end_date()->relative() ?>" class="icon">
                              <em><?php
                                  echo date('l', $somedate);
                                  ?>
                              </em>
                              <strong><?php
                                  echo date('F', $somedate);
                                  ?>
                              </strong>
                              <span><?php
                                  echo date('j', $somedate);
                                  ?>
                              </span>
                    </time>
                   <h2 class="event__title"><?php echo $event->title()->html() ?></h2>
                    <div class="event-icon-box">
                    <?php if($image = $event->images()->sortBy('sort', 'asc')->first()): ?>
                    <img src="<?php echo $image->url() ?>" alt="<?php echo $event->title()->html() ?>" class="event__icon" >
                    <?php endif ?>
                    </div>
                  <strong class="event__subtitle"></strong>
                  <?php if ($eventtype!= 'happyhour'): ?>
                      <p class="event__author">Taught by <strong><?php echo ucwords($names[0]); ?> <?php echo ucwords($names[1]); ?></strong></p>
                  <?php elseif ($eventtype == 'happyhour'): ?>
                      <p class="event__author">Hosted by <strong><!--<a href='<?php echo $site->url() . '/' . '/' . 'instructors' . '/' . urlencode($event->instructor()) ?>'>--><?php echo strtoupper($names[0]); ?> <?php echo ucwords($names[1]); ?></strong></p>
                  <?php endif ?>
                  <!--if any event is in the future, but it is sold out -->
                    <?php if (($event->soldout()== 'true')): ?>
                            <p class="button radius tiny secondary disabled">
                            This Event Was Full- We Sold Out!
                            </p>
                    <!--if event wasn't sold out, but you missed all possible dates -->
                    <?php else: ?>
                            <p class="button radius tiny secondary disabled">
                            Sorry, You Missed This Event!
                            </p>
                    <?php endif ?>
                  </a>
            </article>
            </li>
<?php endif ?>
<?php endforeach; ?>
<?php endforeach; ?>

In the above code, I have two foreach instances that get the list of events. Seems redundant to me, but unfortunately when I remove one, the events do not display and I get a fatal error.

Thanks for looking- I think if we can just iron out the kinks, we’ll soon be there!!! Right now my brain seems to be going around and around in a loop. :slight_smile:

Cheers,

@aftereffectsmagic

You don’t have to print the date, do you? I just used the date to make sure the events were in the correct order.[quote=“aftereffectsmagic, post:14, topic:5087”]
Event names and dates are rendered incorrectly. The problem is that the panel user will not always enter additional dates, and when an event has no additional dates, the date displays as December 31st and says the event is in the past, even though it is not in the past.
[/quote]

I don’t understand. Do you mean to say that even if the only date field that was filled in lies in the past, the event is still a future event?

I don’t understand what you mean.

Maybe you can illustrate what you want to achieve with an image. Without having a clear picture of your data and the end result you expect, I’m having a hard time to understand.