[Solved] Period Comparaison Between Dates

georgie

Member
I make a room booking system, with a Fabrik calendar.

So I have a start date field and an end date field.
It works very well, even with joins (several rooms occupied for the same booking).
I have also a calc field wich counts the number of nights, greats!

Now I would the booking form does not allow to record a room already occupied.

I have a "room" table. I have a "booking" table joined with a "booking_rooms" table (repeat join with "booking" and ID from "room").

So my "Booking form" has a repeat group "booking_rooms" with the 2 date fields and a field with the ID of the room.

Is it possible, in this repeated form group, to compare the combination of the ID field and each day between the 2 date fields with the entered data ? To not permit this.

What do you think about please ?

Thanks !
 
Hi

Please what do you think about my need ?

To abstract, in a first time, I would recover every days between 2 date fields. To use them in a validation rule or php form plugin.

Finally, a calendar do this. I just want use this in a php code.

Thanks in advance for all advices !
 
Apologies, your first post was sent while I was away dealing with a death in my family.

Yes, it is possible to do this. Several folk have built booking forms which have validation code that checks for availability. I don't think anyone has done it with a repeat group, but the principle should be the same.

The general approach is always the same for availability testing, and there's a number of Stack Overflow QAs that explain it:

http://stackoverflow.com/questions/25549765/find-booking-overlaps-to-check-dates-availability

As the first question explains, the way to test an overlap between [a,b] and [c,d] is "a <= d and b >= c". If true, then there's an overlap.

Here's part of a query I use for finding overlapping bookings, in this case for portable electrical generators rather than rooms. In this query, [a,b] are existing booking in the table, and [c,d] are the requested start/end date from the form:

Code:
   SELECT generator FROM generatoren_reservations
   WHERE
      (
                CONVERT_TZ(generatoren_reservations.startdate, '+00:00', '+02:00') <= '{generatoren_reservations___enddate}'
                AND
                CONVERT_TZ(generatoren_reservations.enddate, '+00:00', '+02:00') >= '{generatoren_reservations___startdate}'
            )
   AND
      generatoren_reservations.generator = '{generatoren_reservations___generator}'

It yields a selection of overlapping rows. I use that approach in two places. In validation, I use it to fail the booking - I run that query, if it yields any rows, there's an overlap, so validation fails. In the booking form itself, I use it in the WHERE clause for the generator join dropdown, like this ...

Code:
WHERE {thistable}.id NOT IN (... that query ...)

... which excludes any overlapped bookings from the dropdown. Combined with "AJAX update", this provides a way of dynamically restricting the generators they can select to ones which don't overlap. In the join WHERE clause you can use this style, of just the query with the placeholders.

In the validation, you would have to write the code to run that as a query, using the usual $myDb->query() stuff, getting your date values from $formModel->formData[]. One tip I can give you is that in the validation PHP, you have to do some sanity checking on the format of the dates, as for historical reasons, the format can be different (like when reloading a page after a validation failure).

So here's an example of a validation which makes sure start date < end date:

Code:
$startDate = $formModel->formData['generatoren_data___startdate'];
if (is_array($startDate)) {
  $sDate = explode(' ', $startDate['date']);
  $sDate = $sDate[0] . ' ' . ltrim($startDate['time']);
}
else {
   $sDate = $startDate;
}
//var_dump($sDate);
$sDate = strtotime($sDate);
$endDate = $formModel->formData['generatoren_data___enddate'];
if (is_array($endDate)) {
  $eDate = explode(' ', $endDate['date']);
  $eDate = $eDate[0] . ' ' . ltrim($endDate['time']);
}
else {
   $eDate = $endDate;
}
//var_dump($eDate);
$eDate = strtotime($eDate);
//var_dump($eDate, $eDate);exit;
return $eDate > $sDate;

To test the overlap, you would replace the last few lines with the database query, and test to make sure the result is empty.

On this site, we use the Full Calendar visualization for doing the booking.

This should give you something to go on. If you need further help, we would probably need to look at doing this for you as billable work, as it goes beyond the scope of subscription support.

-- hugh
 
Hi

Many thanks for this advices, I begin to understand, thanks to you, I work on it.

See you soon !
 
Hi

I come back because thanks to yours advices and your example code, I have now a SQL query which runs perfectly in DB.
It is particular because I have a date value in a text field, and I want remove a day to a date field, but it is full OK in SQL.

In DB I do this, for example, it works:

Code:
SELECT id
FROM app_booking_rooms
   WHERE
      (
                /* COMPARE END DATE -1 WITH EXISTING START DATE */
                DATE_FORMAT(date_1, '%Y%m%d') <=  DATE_FORMAT('2016-11-23 00:00:00', '%Y%m%d') - 1
                AND
                /* COMPARE START DATE WITH EXISTING END DATE */
                REPLACE(substr(date_end_calendar, 1, 10), '-', '') >= DATE_FORMAT('2016-11-19 12:00:00', '%Y%m%d' )
            )
and room = 1

Which returns one or a many lines if I have overlaps, nothing if not.
So I just would to prohibit recording if this query returns anything.

I assume it will be in a PHP form plugin. For now I just would do some echo to test my query, but my knowledge is limited. I do this: :rolleyes:

Code:
$MyRoomId = '{app_booking_rooms___room_raw}';

$myDb = JFactory::getDbo();
$myQuery = $myDb->getQuery(true);
$myQuery->select('id')->from('app_booking_rooms')->where((DATE_FORMAT(date_1, '%Y%m%d') <=  DATE_FORMAT('{app_booking_rooms___date_2}', '%Y%m%d') - 1 AND REPLACE(substr(date_end_calendar, 1, 10), '-', '') >= DATE_FORMAT('{app_booking_rooms___date_1}', '%Y%m%d' ))and('room = ' . $myDb->quote($MyRoomId));

$myDb->setQuery($myQuery);
$MyResults = $myDb->loadResult();

if (!empty($MyResults))
{ echo "No overlap found !";exit; }

else {
echo "OK";exit;
}

I have also tried other syntaxs, with all fields concerned in $variables, or neither, but without success. Not record each time, with but with no error...

And in addition, it as about related data.

Please could you help me anyway?

Thanks !
 
Last edited:
Personally I'd do it as a PHP validation, on either or both of the dates.

Not sure why you are doing that substr() thing. Dates submitted from the form are already in MySQL format (except after a validation failure, as explained in my post).

Code:
// get the room id, might be an array, if so grab first entry
$myRoomId = $formModel->formData['app_booking_rooms___room_raw'];
$myRoomId = is_array($myRoomId) ? $myRoomId[0] : $myRoomId;

// dates should just be MySQL format strings, but after a failed validation might be an array of date and time that needs splicing back together
$startDate = $formModel->formData['app_booking_rooms___date_1'];
if (is_array($startDate)) {
  $sDate = explode(' ', $startDate['date']);
  $sDate = $sDate[0] . ' ' . ltrim($startDate['time']);
}
else {
   $sDate = $startDate;
}
//var_dump($sDate);

$endDate = $formModel->formData['app_booking_rooms___date_2'];
if (is_array($endDate)) {
  $eDate = explode(' ', $endDate['date']);
  $eDate = $eDate[0] . ' ' . ltrim($endDate['time']);
}
else {
   $eDate = $endDate;
}
//var_dump($eDate);

// now do the query ...
$myDb = FabrikWorker::getDbo();
$myQuery = $myDb->getQuery();
$myQuery
    ->select('*')
    ->from('app_booking_rooms')
    ->where("(CONVERT_TZ(app_booking_rooms.date_1, '+00:00', '+02:00') <= '$eDate' AND CONVERT_TZ(app_booking_rooms.date_2, '+00:00', '+02:00') >= '$sDate') AND app_booking_rooms.generator.room = '$myRoomId'");

// uncomment this to test, make sure the query is sane
//var_dump((string) $myQuery);exit;

$myDb>setQuery($myQuery);
$myResults = $myDb->loadObjectList();

// return false if we found any rows
return empty($myResults);

You'll need to play around with the CONVERT_TZ. If you store your Fabrik dates as UTC, just change the +02:00 to whatever your TZ offset is. If you store your dates as local, remove it and just use the field name without running it through CONVERT_TZ.

-- hugh
 
Hi

Thank you, indeed I am not forced to use my text field, I changed to look like your example. Ok in DB.

But I have now a SQL error after form submission:
Code:
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '* FROM `app_rooms` AS `app_rooms`,app_booking_rooms WHERE `app_rooms`.`id` IN ('' at line 1

If I set your last vardump, I have this query:
Code:
string(346) " SELECT DISTINCT(`app_rooms`.`id`) AS value, `name_room` AS text,* FROM `app_rooms` AS `app_rooms`,app_booking_rooms WHERE `app_rooms`.`id` IN ('') AND (CONVERT_TZ(app_booking_rooms.date_1, '+00:00', '+02:00') <= ' ' -1 AND CONVERT_TZ(app_booking_rooms.date_2, '+00:00', '+02:00') -1 >= ' ') AND app_booking_rooms.room = 'Array' ORDER BY text ASC"

And I do not understand the end of the code (return...), I do not need "if" condition?
[Edit] Yes I understand now.

Here my current code:
Code:
// get the room id, might be an array, if so grab first entry
$myRoomId = $formModel->formData['app_booking_rooms___room_raw'];
$myRoomId = is_array($myRoomId) ? $myRoomId[0] : $myRoomId;

// dates should just be MySQL format strings, but after a failed validation might be an array of date and time that needs splicing back together
$startDate = $formModel->formData['app_booking_rooms___date_1'];
if (is_array($startDate)) {
  $sDate = explode(' ', $startDate['date']);
  $sDate = $sDate[0] . ' ' . ltrim($startDate['time']);
}
else {
   $sDate = $startDate;
}
//var_dump($sDate);

$endDate = $formModel->formData['app_booking_rooms___date_2'];
if (is_array($endDate)) {
  $eDate = explode(' ', $endDate['date']);
  $eDate = $eDate[0] . ' ' . ltrim($endDate['time']);
}
else {
   $eDate = $endDate;
}
//var_dump($eDate);

// now do the query ...
$myDb = FabrikWorker::getDbo();
$myQuery = $myDb->getQuery();
$myQuery
    ->select('*')
    ->from('app_booking_rooms')
    ->where("(CONVERT_TZ(app_booking_rooms.date_1, '+00:00', '+02:00') <= '$eDate' -1 AND CONVERT_TZ(app_booking_rooms.date_2, '+00:00', '+02:00') -1 >= '$sDate') AND app_booking_rooms.room = '$myRoomId'" );

// uncomment this to test, make sure the query is sane
var_dump((string) $myQuery);exit;

$myDb->setQuery($myQuery);
$myResults = $myDb->loadObjectList();

// return false if we found any rows
return empty($myResults);

When I see the query, it means to add code query to the original list query, is it normal?

Thank you
 
Last edited:
Hi

After other tests, I think my current code, just above, has two problems:

- $sDate and $eDate are not recovered ('' in query). But I think these variables exist, because if I do a "strtotime" just after their declaration, I recover an integer value (in query also, but I can not use it in this compare query).

- The first array $myRoomId is recovered with a var_dump, but not in query ('').

Please what do you think about ?
 
Hi

Indeed my query is more clean with this "true".
However the fields in the query stay empty ('') or like 'Array'.

Code:
string(233) " SELECT * FROM app_booking_rooms WHERE (CONVERT_TZ(app_booking_rooms.date_1, '+00:00', '+02:00') <= '' -1 AND CONVERT_TZ(app_booking_rooms.date_2, '+00:00', '+02:00') -1 >= '' ) AND app_booking_rooms.room = 'Array'"

Any idea ?

Thanks
 
Modify that var_dump() to be ...

Code:
// uncomment this to test, make sure the query is sane
var_dump((string) $myQuery, $formModel->formData);exit;

... and paste the result.

-- hugh
 
And I do not understand the end of the code (return...), I do not need "if" condition?
[Edit] Yes I understand now.

Yup. The empty() function returns a boolean (true or false), so we simply return that. If we wanted to test if it wasn't empty, we'd return !empty().

-- hugh
 
Modify that var_dump() to be ...

Code:
// uncomment this to test, make sure the query is sane
var_dump((string) $myQuery, $formModel->formData);exit;

... and paste the result.

-- hugh

I just done, ouch:
Code:
string(215) " SELECT * FROM app_booking_rooms WHERE (CONVERT_TZ(app_booking_rooms.date_1, '+00:00', '+02:00') <= ' ' -1 AND CONVERT_TZ(app_booking_rooms.date_2, '+00:00', '+02:00') -1 >= ' ') AND app_booking_rooms.room = 'Array'" array(61) { ["app_booking___id"]=> string(1) "5" ["app_booking___name_booking"]=> string(16) "Personal booking" ["app_booking_rooms___id"]=> array(1) { [0]=> string(1) "6" } ["app_booking_rooms___room"]=> array(1) { [0]=> array(1) { [0]=> string(1) "2" } } ["app_booking_rooms___date_1"]=> array(1) { [0]=> string(19) "2016-12-14 00:00:00" } ["app_booking_rooms___date_2"]=> array(1) { [0]=> string(19) "2016-12-16 00:00:00" } ["app_booking_rooms___id_booking"]=> array(1) { [0]=> string(1) "5" } ["app_booking___customer_notes"]=> string(0) "" ["app_booking___user_edit"]=> array(1) { [0]=> string(3) "836" } ["listid"]=> string(1) "7" ["listref"]=> string(1) "7" ["rowid"]=> string(1) "5" ["Itemid"]=> string(3) "117" ["option"]=> string(10) "com_fabrik" ["task"]=> string(12) "form.process" ["isMambot"]=> string(0) "" ["formid"]=> string(1) "7" ["returntoform"]=> string(1) "0" ["fabrik_referrer"]=> string(35) "http://demo-booking.com/my-bookings" ["fabrik_ajax"]=> string(1) "0" ["package"]=> string(6) "fabrik" ["packageId"]=> string(1) "0" ["8036616e5bf1d38426dd873f4eed8f6b"]=> string(1) "1" ["format"]=> string(4) "html" ["fabrik_repeat_group"]=> array(4) { [13]=> string(1) "1" [15]=> string(1) "1" [14]=> string(1) "1" [26]=> string(1) "1" } ["fabrik_group_rowids"]=> array(1) { [15]=> string(5) "["6"]" } ["fabrik_vars"]=> array(1) { ["querystring"]=> array(4) { ["app_booking___user_submit"]=> string(14) "6D41 B4B6F581F" ["app_booking___status"]=> string(22) "6D41631D371E2B1756772C" ["app_booking___txn_id"]=> string(0) "" ["app_booking___manager_notes"]=> string(0) "" } } ["Submit"]=> string(0) "" ["app_booking___id_raw"]=> string(1) "5" ["app_booking___user_submit_raw"]=> array(1) { [0]=> string(3) "836" } ["app_booking___name_booking_raw"]=> string(16) "Personal booking" ["app_booking___status_raw"]=> array(1) { [0]=> string(7) "Pending" } ["app_booking___price_booking_raw"]=> string(1) "1" ["app_booking_rooms___id_raw"]=> array(1) { [0]=> string(1) "6" } ["app_booking_rooms___room_raw"]=> array(1) { [0]=> array(1) { [0]=> string(1) "2" } } ["app_booking_rooms___date_1_raw"]=> array(1) { [0]=> string(19) "2016-12-14 00:00:00" } ["app_booking_rooms___date_2_raw"]=> array(1) { [0]=> string(19) "2016-12-16 00:00:00" } ["app_booking_rooms___date_end_calendar_raw"]=> string(19) "2016-12-15 00:00:00" ["app_booking_rooms___night_raw"]=> string(1) "2" ["app_booking_rooms___price_room_raw"]=> float(1) ["app_booking_rooms___id_booking_raw"]=> array(1) { [0]=> string(1) "5" } ["app_booking___txn_id_raw"]=> string(0) "" ["app_booking___manager_notes_raw"]=> string(0) "" ["app_booking___customer_notes_raw"]=> string(0) "" ["app_booking___user_edit_raw"]=> array(1) { [0]=> string(3) "836" } ["__pk_val"]=> string(1) "5" ["fabrik_viz_calendar_view"]=> string(9) "monthView" ["5a4a313f952cb16a33c039090d78c2fd"]=> string(26) "q10d5brsofqu13sg5slijm8th6" ["bb9edec907ab4ee058877be385a90bd0"]=> string(26) "u30qsm8mqjpc2jto8oskqoit27" ["joomla_user_state"]=> string(9) "logged_in" ["836:7:0"]=> string(14) "5C55 05F515852" ["836:7:5"]=> string(14) "5C55 05F515857" ["view"]=> string(4) "form" ["app_booking___user_submit"]=> array(1) { [0]=> string(3) "836" } ["app_booking___status"]=> array(1) { [0]=> string(7) "Pending" } ["app_booking___txn_id"]=> string(0) "" ["app_booking___manager_notes"]=> string(0) "" ["app_booking___price_booking"]=> string(1) "1" ["app_booking_rooms___date_end_calendar"]=> string(19) "2016-12-15 00:00:00" ["app_booking_rooms___night"]=> string(1) "2" ["app_booking_rooms___price_room"]=> float(1) }

Sorry for that... Is it normal?
 
Please wait.

I am maybe understood somethings, and I keep to work on it, but to abstract, it seems to be better with a "implode" of the first variable $myRoomId, and a date format on the date variables.

It seems I have still a problem : indeed, because the query works, I can not edit a record already existing...:confused:

I work on it, but if you have a idea, I take :rolleyes:
 
OK, I progress : with an implode on one of my variable, formating date variables and each time grabing only the first entry, it works for the first group.

My actual current code, which works only for the first group:
Code:
// get the room id, might be an array, if so grab first entry
$myRoomId = $formModel->formData['app_booking_rooms___room_raw'];
$myRoomId = is_array($myRoomId) ? $myRoomId[0] : $myRoomId;

$myRoomIdB = implode(",", $myRoomId);

// get the booking-room id
$myBookingRoomId = $formModel->formData['app_booking_rooms___id'];
$myBookingRoomId = is_array($myBookingRoomId) ? $myBookingRoomId[0] : $myBookingRoomId;
//var_dump($myBookingRoomId);

// date 1
$startDate = $formModel->formData['app_booking_rooms___date_1'];
$startDate = is_array($startDate) ? $startDate[0] : $startDate;

$startDate2 = new DateTime($startDate);
$startDate3 = $startDate2->format('Ymd');

// date 2
$endDate = $formModel->formData['app_booking_rooms___date_2'];
$endDate = is_array($endDate) ? $endDate[0] : $endDate;

$endDate2 = new DateTime($endDate);
$endDate3 = $endDate2->format('Ymd');
//var_dump($endDate3);

// now do the query ...
$myDb = FabrikWorker::getDbo();
$myQuery = $myDb->getQuery(true);
$myQuery
    ->select('*')
    ->from('app_booking_rooms, app_booking')
    ->where("app_booking.id=app_booking_rooms.id_booking AND (DATE_FORMAT(app_booking_rooms.date_1, '%Y%m%d') <= '$endDate3' -1 AND DATE_FORMAT(app_booking_rooms.date_2, '%Y%m%d') -1 >= '$startDate3') AND (app_booking_rooms.room = '$myRoomIdB') AND (app_booking_rooms.id<>'$myBookingRoomId')");

// uncomment this to test, make sure the query is sane
//var_dump((string) $myQuery);
var_dump ($startDate3,$endDate3) ;

$myDb->setQuery($myQuery);
$myResults = $myDb->loadObjectList();

// return false if we found any rows
return empty($myResults);

I think the last problem is to manage the "array", for get the other groups, no? About "if is_array...", this for two integer fields and two date fields.

PS: Please note that for my dates and my query, I prefer to use "format('Ymd')" and DATE_FORMAT because it seems easier to not look the hours (I need it for many reasons), and easier to substract a day (the "-1" in query).

Please what do you think about?
 
Ah, OK, I missed that, I wrote code assuming they weren't in a repeat group.

Code:
// get the room id, might be an array, if so grab first entry
$myRoomId = $formModel->formData['app_booking_rooms___room_raw'][$repeatCounter];
$myRoomId = is_array($myRoomId) ? $myRoomId[0] : $myRoomId;


// get the booking-room id
$myBookingRoomId = $formModel->formData['app_booking_rooms___id'][$repeatCounter];
$myBookingRoomId = is_array($myBookingRoomId) ? $myBookingRoomId[0] : $myBookingRoomId;

// date 1
$startDate = $formModel->formData['app_booking_rooms___date_1'][$repeatCounter];
$startDate = is_array($startDate) ? $startDate[0] : $startDate;
$startDate = new DateTime($startDate);
$startDate = $startDate->format('Ymd');

// date 2
$endDate = $formModel->formData['app_booking_rooms___date_2'][$repeatCounter];
$endDate = is_array($endDate) ? $endDate[0] : $endDate;
$endDate = new DateTime($endDate);
$endDate = $endDate->format('Ymd');
//var_dump($endDate3);

// now do the query ...
$myDb = FabrikWorker::getDbo();
$myQuery = $myDb->getQuery(true);
$myQuery
    ->select('*')
    ->from('app_booking_rooms, app_booking')
    ->where("app_booking.id=app_booking_rooms.id_booking")
    ->where("DATE_FORMAT(app_booking_rooms.date_1, '%Y%m%d') <= '$endDate' -1")
    ->where("DATE_FORMAT(app_booking_rooms.date_2, '%Y%m%d') -1 >= '$startDate'")
    ->where("app_booking_rooms.room = '$myRoomId'");

if (!empty($myBookingRoomId)) {
    $myQuery->where("app_booking_rooms.id <> '$myBookingRoomId'");
}

// uncomment this to test, make sure the query is sane
//var_dump((string) $myQuery);
var_dump ($startDate,$endDate) ;

$myDb->setQuery($myQuery);
$myResults = $myDb->loadObjectList();

// return false if we found any rows
return empty($myResults);
 
Hi

Yes it seems to be perfect like this, thanks ! Thanks also to have optimize my code, I learn.

I do also your endate>startdate, it works perfectly, greats :) !

Just a question for my curiosity (I write a web article about Fabrik, a kind of personal memo but for public use) : it seems the last "where" condition of my query must be in an "if" condition, why?
I would think it is in the case of a new record, the id does not exist, but the other fields no more. So I do not understand. Is it particular for an id field?...

Thank you again !!!
 
It's not strictly necessary to not add that condition, it's just a habit of mine. If you don't need it in the query, don't put it in the query. It's just a kind of defensive coding style, which can avoid having unintended side effects. Won't make a difference in this case.

It's also a "teaching moment", showing you how to use the query builder API to split your where clauses up into individual conditions, which makes it easier to have parts of the where clause that don't always get added.

-- hugh
 
We are in need of some funding.
More details.

Thank you.

Members online

Back
Top