diff --git a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php index d7efa9b7..58040c49 100644 --- a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php +++ b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php @@ -926,48 +926,114 @@ final class CoreLibsCombinedDateTimeTest extends TestCase public function daysIntervalProvider(): array { return [ - 'valid interval /, not named array' => [ - '2020/1/1', - '2020/1/30', - false, - [29, 22, 8], + // normal and format tests + 'valid interval / not named array' => [ + 'input_a' => '2020/1/1', + 'input_b' => '2020/1/30', + 'return_named' => false, // return_named + 'include_end_date' => true, // include_end_date + 'exclude_start_date' => false, // exclude_start_date + 'expected' => [30, 22, 8, false], ], - 'valid interval /, named array' => [ - '2020/1/1', - '2020/1/30', - true, - ['overall' => 29, 'weekday' => 22, 'weekend' => 8], + 'valid interval / named array' => [ + 'input_a' => '2020/1/1', + 'input_b' => '2020/1/30', + 'return_named' => true, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => ['overall' => 30, 'weekday' => 22, 'weekend' => 8, 'reverse' => false], ], - 'valid interval -' => [ - '2020-1-1', - '2020-1-30', - false, - [29, 22, 8], - ], - 'valid interval switched' => [ - '2020/1/30', - '2020/1/1', - false, - [28, 0, 0], + 'valid interval with "-"' => [ + 'input_a' => '2020-1-1', + 'input_b' => '2020-1-30', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, false], ], 'valid interval with time' => [ - '2020/1/1 12:12:12', - '2020/1/30 13:13:13', - false, - [28, 21, 8], + 'input_a' => '2020/1/1 12:12:12', + 'input_b' => '2020/1/30 13:13:13', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, false], ], + // invalid 'invalid dates' => [ - 'abc', - 'xyz', - false, - [0, 0, 0] + 'input_a' => 'abc', + 'input_b' => 'xyz', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [0, 0, 0, false] ], - // this test will take a long imte + // this test will take a long time 'out of bound dates' => [ - '1900-1-1', - '9999-12-31', - false, - [2958463,2113189,845274], + 'input_a' => '1900-1-1', + 'input_b' => '9999-12-31', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [2958463, 2113189, 845274, false], + ], + // tests for include/exclude + 'exclude end date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => false, + 'expected' => [29, 21, 8, false], + ], + 'exclude start date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => true, + 'expected' => [29, 21, 8, false], + ], + 'exclude start and end date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => true, + 'expected' => [28, 20, 8, false], + ], + // reverse + 'reverse: valid interval' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, true], + ], + 'reverse: exclude end date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => false, + 'expected' => [29, 21, 8, true], + ], + 'reverse: exclude start date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => true, + 'expected' => [29, 21, 8, true], + ], + 'reverse: exclude start and end date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => true, + 'expected' => [28, 20, 8, true], ], ]; } @@ -982,19 +1048,27 @@ final class CoreLibsCombinedDateTimeTest extends TestCase * * @param string $input_a * @param string $input_b - * @param bool $flag - * @param array $expected + * @param bool $return_named + * @param array $expected * @return void */ public function testCalcDaysInterval( string $input_a, string $input_b, - bool $flag, + bool $return_named, + bool $include_end_date, + bool $exclude_start_date, $expected ): void { $this->assertEquals( $expected, - \CoreLibs\Combined\DateTime::calcDaysInterval($input_a, $input_b, $flag) + \CoreLibs\Combined\DateTime::calcDaysInterval( + $input_a, + $input_b, + return_named:$return_named, + include_end_date:$include_end_date, + exclude_start_date:$exclude_start_date + ) ); } @@ -1187,7 +1261,38 @@ final class CoreLibsCombinedDateTimeTest extends TestCase '2023-07-03', '2023-07-27', true - ] + ], + // reverse + 'reverse: no weekend' => [ + '2023-07-04', + '2023-07-03', + false + ], + 'reverse: start weekend sat' => [ + '2023-07-04', + '2023-07-01', + true + ], + 'reverse: start weekend sun' => [ + '2023-07-04', + '2023-07-02', + true + ], + 'reverse: end weekend sat' => [ + '2023-07-08', + '2023-07-03', + true + ], + 'reverse: end weekend sun' => [ + '2023-07-09', + '2023-07-03', + true + ], + 'reverse: long period > 6 days' => [ + '2023-07-27', + '2023-07-03', + true + ], ]; } diff --git a/www/admin/class_test.datetime.php b/www/admin/class_test.datetime.php index e0374599..45bca5e8 100644 --- a/www/admin/class_test.datetime.php +++ b/www/admin/class_test.datetime.php @@ -268,7 +268,9 @@ foreach ($compare_datetimes as $compare_datetime) { print "COMPAREDATE: $compare_datetime[0] = $compare_datetime[1]: " . (string)DateTime::compareDateTime($compare_datetime[0], $compare_datetime[1]) . "
"; } + print "
"; +print "

calcDaysInterval

"; $compare_dates = [ [ '2021-05-01', '2021-05-10', ], [ '2021-05-10', '2021-05-01', ], @@ -279,9 +281,21 @@ foreach ($compare_dates as $compare_date) { print "CALCDAYSINTERVAL: $compare_date[0] = $compare_date[1]: " . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1])) . "
"; print "CALCDAYSINTERVAL(named): $compare_date[0] = $compare_date[1]: " - . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], true)) . "
"; + . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], return_named:true)) . "
"; + print "CALCDAYSINTERVAL(EXCLUDE END): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], include_end_date:false)); + print "CALCDAYSINTERVAL(EXCLUDE START): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], exclude_start_date:true)); + print "CALCDAYSINTERVAL(EXCLUDE END, EXCLUDE START): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval( + $compare_date[0], + $compare_date[1], + include_end_date:false, + exclude_start_date:true + )); } print "
"; +print "

setWeekdayNameFromIsoDow

"; // test date conversion $dow = 2; print "DOW[$dow]: " . DateTime::setWeekdayNameFromIsoDow($dow) . "
"; @@ -297,26 +311,25 @@ $date = '2022-70-242'; print "DATE-dow[$date];invalid: " . DateTime::setWeekdayNameFromDate($date) . "
"; print "DATE-dow[$date],long;invalid: " . DateTime::setWeekdayNameFromDate($date, true) . "
"; print "DOW-date[$date];invalid: " . DateTime::setWeekdayNumberFromDate($date) . "
"; -print "
"; -// check date range includes a weekend -// does not: -$start_date = '2023-07-03'; -$end_date = '2023-07-05'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-03'; -$end_date = '2023-07-10'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-03'; -$end_date = '2023-07-31'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-01'; -$end_date = '2023-07-03'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; +print "
"; +print "

dateRangeHasWeekend

"; +// check date range includes a weekend +$has_weekend_list = [ + ['2023-07-03', '2023-07-05'], + ['2023-07-03', '2023-07-10'], + ['2023-07-03', '2023-07-31'], + ['2023-07-01', '2023-07-03'], + ['2023-07-01', '2023-07-01'], + ['2023-07-01', '2023-07-02'], + ['2023-06-30', '2023-07-01'], + ['2023-06-30', '2023-06-30'], + ['2023-07-01', '2023-06-30'], +]; +foreach ($has_weekend_list as $days) { + print "Has Weekend: " . $days[0] . " ~ " . $days[1] . ": " + . Dgs::prBl(DateTime::dateRangeHasWeekend($days[0], $days[1])) . "
"; +} print ""; diff --git a/www/admin/layout/javascript/edit.jq.js b/www/admin/layout/javascript/edit.jq.js index cda95860..6c2605ed 100644 --- a/www/admin/layout/javascript/edit.jq.js +++ b/www/admin/layout/javascript/edit.jq.js @@ -16,6 +16,21 @@ if (!DEBUG) { var GL_OB_S = 100; var GL_OB_BASE = 100; +/** + * Gets html element or throws an error + * @param {string} el_id Element ID to get + * @returns {HTMLElement} + * @throws Error + */ +function loadEl(el_id) +{ + let el = document.getElementById(el_id); + if (el === null) { + throw new Error('Cannot find: ' + el_id); + } + return el; +} + /** * opens a popup window with winName and given features (string) * @param {String} theURL the url @@ -154,6 +169,18 @@ function goToPos(element, offset = 0, duration = 500, base = 'body,html') // esl } } +/** + * go to element, scroll + * non jquery + * @param {string} target +*/ +function goTo(target) // eslint-disable-line no-unused-vars +{ + loadEl(target).scrollIntoView({ + behavior: 'smooth' + }); +} + /** * uses the i18n object created in the translation template * that is filled from gettext in PHP @@ -400,9 +427,9 @@ function keyInObject(key, object) /** * returns matching key of value - * @param {Object} obj object to search value in - * @param {Mixed} value any value (String, Number, etc) - * @return {String} the key found for the first matching value + * @param {Object} object object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {String} the key found for the first matching value */ function getKeyByValue(object, value) // eslint-disable-line no-unused-vars { @@ -414,9 +441,9 @@ function getKeyByValue(object, value) // eslint-disable-line no-unused-vars /** * returns true if value is found in object with a key - * @param {Object} obj object to search value in - * @param {Mixed} value any value (String, Number, etc) - * @return {Boolean} true on value found, false on not found + * @param {Object} object object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {Boolean} true on value found, false on not found */ function valueInObject(object, value) // eslint-disable-line no-unused-vars { @@ -796,7 +823,7 @@ function showOverlayBoxLayers(el_id) // eslint-disable-line no-unused-vars * else just set zIndex to the new GL_OB_S value * @param {String} el_id Target to hide layer */ -function hideOverlayBoxLayers(el_id) +function hideOverlayBoxLayers(el_id='') { // console.log('HIDE overlaybox: %s', GL_OB_S); // remove on layer @@ -1109,7 +1136,9 @@ function phfa(list) // eslint-disable-line no-unused-vars function html_options(name, data, selected = '', options_only = false, return_string = false, sort = '') // eslint-disable-line no-unused-vars { // wrapper to new call - return html_options_block(name, data, selected, false, options_only, return_string, sort); + return html_options_block( + name, data, selected, 0, options_only, return_string, sort + ); } /** @@ -1131,8 +1160,9 @@ function html_options(name, data, selected = '', options_only = false, return_st * @param {String} [onchange=''] onchange trigger call, default unset * @return {String} html with build options block */ -function html_options_block(name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = '') -{ +function html_options_block( + name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = '' +) { var content = []; var element_select; var select_options = {}; @@ -1169,7 +1199,8 @@ function html_options_block(name, data, selected = '', multiple = 0, options_onl // basic options init options = { 'label': value, - 'value': key + 'value': key, + 'selected': '' }; // add selected if matching if (multiple == 0 && !Array.isArray(selected) && selected == key) { @@ -1180,7 +1211,7 @@ function html_options_block(name, data, selected = '', multiple = 0, options_onl options.selected = ''; } // create the element option - element_option = cel('option', '', value, '', options); + element_option = cel('option', '', value, [], options); // attach it to the select element ael(element_select, element_option); } @@ -1232,7 +1263,7 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un [].forEach.call(document.querySelectorAll('#' + name + ' :checked'), function(elm) { option_selected = elm.value; }); - document.getElementById(name).innerHTML = ''; + loadEl(name).innerHTML = ''; for (const key of data_list) { value = data[key]; // console.log('add [%s] options: key: %s, value: %s', name, key, value); @@ -1243,7 +1274,7 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un if (key == option_selected) { element_option.selected = true; } - document.getElementById(name).appendChild(element_option); + loadEl(name).appendChild(element_option); } } } @@ -1307,7 +1338,7 @@ function parseQueryString(query = '', return_key = '') // eslint-disable-line no * all parameters are returned * @param {String} [query=''] different query string to parse, if not * set (default) the current window href is used - * @param {Bool} [single=false] if set to true then only the first found + * @param {Boolean} [single=false] if set to true then only the first found * will be returned * @return {Object|Array|String} if search is empty, object, if search is set * and only one entry, then string, else array @@ -1319,7 +1350,7 @@ function getQueryStringParam(search = '', query = '', single = false) // eslint- query = window.location.href; } const url = new URL(query); - let param = ''; + let param = null; if (search) { let _params = url.searchParams.getAll(search); if (_params.length == 1 || single === true) { diff --git a/www/lib/CoreLibs/ACL/Login.php b/www/lib/CoreLibs/ACL/Login.php index 46b622e9..890119ff 100644 --- a/www/lib/CoreLibs/ACL/Login.php +++ b/www/lib/CoreLibs/ACL/Login.php @@ -2539,7 +2539,7 @@ HTML; $this->login_user_id, -1, $login_user_id_changed - ); + ) ?? ''; // flag unclean input data if ($login_user_id_changed > 0) { $this->login_user_id_unclear = true; diff --git a/www/lib/CoreLibs/Combined/DateTime.php b/www/lib/CoreLibs/Combined/DateTime.php index d46619c5..d123c53a 100644 --- a/www/lib/CoreLibs/Combined/DateTime.php +++ b/www/lib/CoreLibs/Combined/DateTime.php @@ -639,16 +639,26 @@ class DateTime * * @param string $start_date valid start date (y/m/d) * @param string $end_date valid end date (y/m/d) - * @param bool $return_named return array type, false (default), true for named - * @return array 0/overall, 1/weekday, 2/weekend + * @param bool $return_named [default=false] return array type, false (default), true for named + * @param bool $include_end_date [default=true] include end date in calc + * @param bool $exclude_start_date [default=false] include end date in calc + * @return array{0:int,1:int,2:int,3:bool}|array{overall:int,weekday:int,weekend:int,reverse:bool} + * 0/overall, 1/weekday, 2/weekend, 3/reverse */ public static function calcDaysInterval( string $start_date, string $end_date, - bool $return_named = false + bool $return_named = false, + bool $include_end_date = true, + bool $exclude_start_date = false ): array { // pos 0 all, pos 1 weekday, pos 2 weekend - $days = []; + $days = [ + 0 => 0, + 1 => 0, + 2 => 0, + 3 => false, + ]; // if anything invalid, return 0,0,0 try { $start = new \DateTime($start_date); @@ -659,19 +669,30 @@ class DateTime 'overall' => 0, 'weekday' => 0, 'weekend' => 0, + 'reverse' => false ]; } else { - return [0, 0, 0]; + return $days; } } // so we include the last day too, we need to add +1 second in the time - $end->setTime(0, 0, 1); - // if end date before start date, only this will be filled - $days[0] = $end->diff($start)->days; - $days[1] = 0; - $days[2] = 0; + // if start is before end, switch dates and flag + $days[3] = false; + if ($start > $end) { + $new_start = $end; + $end = $start; + $start = $new_start; + $days[3] = true; + } // get period for weekends/weekdays - $period = new \DatePeriod($start, new \DateInterval('P1D'), $end); + $options = 0; + if ($include_end_date) { + $options |= \DatePeriod::INCLUDE_END_DATE; + } + if ($exclude_start_date) { + $options |= \DatePeriod::EXCLUDE_START_DATE; + } + $period = new \DatePeriod($start, new \DateInterval('P1D'), $end, $options); foreach ($period as $dt) { $curr = $dt->format('D'); if ($curr == 'Sat' || $curr == 'Sun') { @@ -679,12 +700,14 @@ class DateTime } else { $days[1]++; } + $days[0]++; } if ($return_named === true) { return [ 'overall' => $days[0], 'weekday' => $days[1], 'weekend' => $days[2], + 'reverse' => $days[3], ]; } else { return $days; @@ -705,6 +728,13 @@ class DateTime ): bool { $dd_start = new \DateTime($start_date); $dd_end = new \DateTime($end_date); + // flip if start is after end + if ($dd_start > $dd_end) { + $new_start = $dd_end; + $dd_end = $dd_start; + $dd_start = $new_start; + } + // if start > end, flip if ( // starts with a weekend $dd_start->format('N') >= 6 || diff --git a/www/lib/CoreLibs/DB/IO.php b/www/lib/CoreLibs/DB/IO.php index 85a4e897..47206323 100644 --- a/www/lib/CoreLibs/DB/IO.php +++ b/www/lib/CoreLibs/DB/IO.php @@ -1413,10 +1413,7 @@ class IO $this->pk_name_table[$table] ? $this->pk_name_table[$table] : 'NULL'; } - if ( - !preg_match(self::REGEX_RETURNING, $this->query) && - $this->pk_name && $this->pk_name != 'NULL' - ) { + if (!preg_match(self::REGEX_RETURNING, $this->query) && $this->pk_name != 'NULL') { // check if this query has a ; at the end and remove it $__query = preg_replace("/(;\s*)$/", '', $this->query); // must be query, if preg replace failed, use query as before @@ -1426,7 +1423,7 @@ class IO } elseif ( preg_match(self::REGEX_RETURNING, $this->query, $matches) ) { - if ($this->pk_name && $this->pk_name != 'NULL') { + if ($this->pk_name != 'NULL') { // add the primary key if it is not in the returning set if (!preg_match("/$this->pk_name/", $matches[1])) { $this->query .= " , " . $this->pk_name;