From 80d2215f2b5633f12f5815dd01bf8cfaea005cc2 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 17 Oct 2023 19:09:38 +0900 Subject: [PATCH] Combined\DateTime new intervalStringFormat method new method to replace old timeStringFormat method: - has year/month data too - can format with natural names (minutes, seconds, etc) - can have normal naming (5 hours, 1 minute and 10 seconds) - skip or not skip zero values in between (6h 0m 1s -> 6h 1s) - skip or add trailing zero values (6m 0s -> 6m) - add or not add milliseconds with decimal nano seconds - drop nano seconds (115.55ms -> 115ms) - truncate value after a certain part (eg only show up to days) - add leading 0s to only milli seconds values (115ms -> 0s 115ms) - namespace separator (6h -> 6 h) Bug fix for timeStringFormat - 1.5s and 1.05s and 1.005s all where 5ms -> fixed to 500ms, 50ms 5ms - bug fix for 0ms drop even thought show ms is requested Start unit testing part --- .../Combined/CoreLibsCombinedDateTimeTest.php | 668 ++++++++++++------ www/admin/class_test.datetime.php | 167 ++++- www/lib/CoreLibs/Combined/DateTime.php | 252 ++++++- 3 files changed, 860 insertions(+), 227 deletions(-) diff --git a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php index 7bd68d6a..9d5a00a1 100644 --- a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php +++ b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php @@ -66,6 +66,34 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * date string convert test + * + * @covers ::dateStringFormat + * @dataProvider timestampProvider + * @testdox dateStringFormat $input (microtime $flag) will be $expected [$_dataName] + * + * @param int|float $input + * @param bool $flag + * @param string $expected + * @return void + */ + public function testDateStringFormat( + $input, + bool $flag_show_micro, + bool $flag_micro_as_float, + string $expected + ): void { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::dateStringFormat( + $input, + $flag_show_micro, + $flag_micro_as_float + ) + ); + } + /** * interval for both directions * @@ -74,6 +102,11 @@ final class CoreLibsCombinedDateTimeTest extends TestCase public function intervalProvider(): array { return [ + 'on hour' => [ + 3600, + false, + '1h 0m 0s' + ], 'interval no microtime' => [ 1641515890, false, @@ -82,7 +115,7 @@ final class CoreLibsCombinedDateTimeTest extends TestCase 'interval with microtime' => [ 1641515890, true, - '18999d 0h 38m 10s', + '18999d 0h 38m 10s 0ms', ], 'micro interval no microtime' => [ 1641515890.123456, @@ -92,7 +125,7 @@ final class CoreLibsCombinedDateTimeTest extends TestCase 'micro interval with microtime' => [ 1641515890.123456, true, - '18999d 0h 38m 10s 1235ms', + '18999d 0h 38m 10s 124ms', ], 'negative interval no microtime' => [ -1641515890, @@ -103,27 +136,27 @@ final class CoreLibsCombinedDateTimeTest extends TestCase 'microtime only' => [ 0.123456, true, - '0s 1235ms', + '0s 123ms', ], 'seconds only' => [ 30.123456, true, - '30s 1235ms', + '30s 123ms', ], 'minutes only' => [ 90.123456, true, - '1m 30s 1235ms', + '1m 30s 123ms', ], 'hours only' => [ 3690.123456, true, - '1h 1m 30s 1235ms', + '1h 1m 30s 123ms', ], 'days only' => [ 90090.123456, true, - '1d 1h 1m 30s 1235ms', + '1d 1h 1m 30s 123ms', ], 'already set' => [ '1d 1h 1m 30s 1235ms', @@ -143,6 +176,259 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * time seconds convert test + * + * @covers ::timeStringFormat + * @dataProvider intervalProvider + * @testdox timeStringFormat $input (microtime $flag) will be $expected [$_dataName] + * + * @param string|int|float $input + * @param bool $flag + * @param string $expected + * @return void + */ + public function testTimeStringFormat(string|int|float $input, bool $flag, string $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::timeStringFormat($input, $flag) + ); + } + + /** + * interval seconds convert + * + * @covers ::intervalStringFormat + * @dataProvider intervalProvider + * @testdox intervalStringFormat $input (microtime $show_micro) will be $expected [$_dataName] + * + * @param string|int|float $input + * @param bool $show_micro + * @param string $expected + * @return void + */ + public function testIntervalStringFormat(string|int|float $input, bool $show_micro, string $expected): void + { + // we skip string input, that is not allowed + if (is_string($input)) { + $this->assertTrue(true, 'Skip strings'); + return; + } + // invalid values throw exception in default + if ($input == 999999999999999) { + $this->expectException(\LengthException::class); + } + // below is equal to timeStringFormat + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::intervalStringFormat( + $input, + show_microseconds: $show_micro, + show_only_days: true, + skip_zero: false, + skip_last_zero: false, + truncate_nanoseconds: true, + truncate_zero_seconds_if_microseconds: false + ) + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function intervalExtendedProvider(): array + { + return [ + 'default value' => [ + [ + 'seconds' => 60, + 'truncate_after' => null, + 'natural_seperator' => null, + 'name_space_seperator' => null, + 'show_microseconds' => null, + 'short_time_name' => null, + 'skip_last_zero' => null, + 'skip_zero' => null, + 'show_only_days' => null, + 'auto_fix_microseconds' => null, + 'truncate_nanoseconds' => null, + 'truncate_zero_seconds_if_microseconds' => null, + ], + 'expected' => '1m', + 'exception' => null + ], + 'default value, skip_last_zero:false' => [ + [ + 'seconds' => 60, + 'truncate_after' => null, + 'natural_seperator' => null, + 'name_space_seperator' => null, + 'show_microseconds' => true, + 'short_time_name' => null, + 'skip_last_zero' => false, + 'skip_zero' => null, + 'show_only_days' => null, + 'auto_fix_microseconds' => null, + 'truncate_nanoseconds' => null, + 'truncate_zero_seconds_if_microseconds' => null, + ], + 'expected' => '1m', + 'exception' => null + ], + ]; + } + + /** + * test all options for interval conversion + * + * @covers ::intervalStringFormat + * @dataProvider intervalExtendedProvider + * @testdox intervalStringFormat $input will be $expected / $exception [$_dataName] + * + * @param array $parameter_list + * @param string $expected + * @param string $exception + * @return void + */ + public function testExtendedIntervalStringFormat( + array $parameter_list, + ?string $expected, + ?string $exception + ): void { + if ($expected === null && $exception === null) { + $this->assertFalse(true, 'Cannot have expected and exception null in test data'); + } + $parameters = []; + foreach ( + [ + 'seconds' => null, + 'truncate_after' => '', + 'natural_seperator' => false, + 'name_space_seperator' => false, + 'show_microseconds' => true, + 'short_time_name' => true, + 'skip_last_zero' => true, + 'skip_zero' => true, + 'show_only_days' => false, + 'auto_fix_microseconds' => false, + 'truncate_nanoseconds' => false, + 'truncate_zero_seconds_if_microseconds' => true, + ] as $param => $default + ) { + if (empty($parameter_list[$param]) && $default === null) { + $this->assertFalse(true, 'Parameter ' . $param . ' is mandatory '); + } elseif (empty($parameter_list[$param]) || $parameter_list[$param] === null) { + $parameters[] = $default; + } else { + $parameters[] = $parameter_list[$param]; + } + } + if ($expected !== null) { + $this->assertEquals( + $expected, + call_user_func_array('CoreLibs\Combined\DateTime::intervalStringFormat', $parameters) + ); + } else { + $this->expectException($exception); + call_user_func_array('CoreLibs\Combined\DateTime::intervalStringFormat', $parameters); + } + } + + /** + * Undocumented function + * + * @return array + */ + public function exceptionsIntervalProvider(): array + { + return [ + 'UnexpectedValueException: 1 A' => [ + 'seconds' => 99999999999999999999999, + 'params' => [], + 'exception' => \UnexpectedValueException::class, + 'exception_message' => "/^Seconds value is invalid, too large or more than six decimals: /", + 'excpetion_code' => 1, + ], + 'UnexpectedValueException: 1 B' => [ + 'seconds' => 123.1234567, + 'params' => [], + 'exception' => \UnexpectedValueException::class, + 'exception_message' => "/^Seconds value is invalid, too large or more than six decimals: /", + 'excpetion_code' => 1, + ], + // exception 2 is very likely covered by exception 1 + 'LengthException: 3' => [ + 'seconds' => 999999999999999999, + 'params' => [ + 'show_only_days', + ], + 'exception' => \LengthException::class, + 'exception_message' => "/^Input seconds value is too large for days output: /", + 'excpetion_code' => 3, + ], + 'UnexpectedValueException: 4' => [ + 'seconds' => 1234567, + 'params' => [ + 'truncate_after' + ], + 'exception' => \UnexpectedValueException::class, + 'exception_message' => "/^truncate_after has an invalid value: /", + 'excpetion_code' => 4, + ], + 'UnexpectedValueException: 5' => [ + 'seconds' => 1234567, + 'params' => [ + 'show_only_days:truncate_after' + ], + 'exception' => \UnexpectedValueException::class, + 'exception_message' => + "/^If show_only_days is turned on, the truncate_after cannot be years or months: /", + 'excpetion_code' => 5, + ] + ]; + } + + /** + * Test all exceptions + * + * @covers ::intervalStringFormat + * @dataProvider exceptionsIntervalProvider + * @testdox intervalStringFormat: test Exceptions + * + * @param int|float $seconds + * @param array $params + * @param string $exception + * @param string $exception_message + * @param int $excpetion_code + * @return void + */ + public function testExceptionsIntervalStringFormat( + int|float $seconds, + array $params, + string $exception, + string $exception_message, + int $excpetion_code, + ): void { + $this->expectException($exception); + $this->expectExceptionMessageMatches($exception_message); + $this->expectExceptionCode($excpetion_code); + if (empty($params)) { + \CoreLibs\Combined\DateTime::intervalStringFormat($seconds); + } else { + if (in_array('show_only_days', $params)) { + echo "FOO\n"; + \CoreLibs\Combined\DateTime::intervalStringFormat($seconds, show_only_days:true); + } elseif (in_array('truncate_after', $params)) { + \CoreLibs\Combined\DateTime::intervalStringFormat($seconds, truncate_after: 'v'); + } elseif (in_array('show_only_days:truncate_after', $params)) { + \CoreLibs\Combined\DateTime::intervalStringFormat($seconds, show_only_days:true, truncate_after: 'y'); + } + } + } + /** * Undocumented function * @@ -203,6 +489,25 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * Undocumented function + * + * @covers ::stringToTime + * @dataProvider reverseIntervalProvider + * @testdox stringToTime $input will be $expected [$_dataName] + * + * @param string|int|float $input + * @param string|int|float $expected + * @return void + */ + public function testStringToTime($input, $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::stringToTime($input) + ); + } + /** * Undocumented function * @@ -238,6 +543,25 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * Undocumented function + * + * @covers ::checkDate + * @dataProvider dateProvider + * @testdox checkDate $input will be $expected [$_dataName] + * + * @param string $input + * @param bool $expected + * @return void + */ + public function testCheckDate(string $input, bool $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::checkDate($input) + ); + } + /** * Undocumented function * @@ -297,6 +621,25 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * Undocumented function + * + * @covers ::checkDateTime + * @dataProvider dateTimeProvider + * @testdox checkDateTime $input will be $expected [$_dataName] + * + * @param string $input + * @param bool $expected + * @return void + */ + public function testCheckDateTime(string $input, bool $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::checkDateTime($input) + ); + } + /** * Undocumented function * @@ -371,6 +714,37 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * Undocumented function + * + * @covers ::compareDate + * @dataProvider dateCompareProvider + * @testdox compareDate $input_a compared to $input_b will be $expected [$_dataName] + * + * @param string $input_a + * @param string $input_b + * @param int|bool $expected + * @param string|null $exception + * @param int|null $exception_code + * @return void + */ + public function testCompareDate( + string $input_a, + string $input_b, + int|bool $expected, + ?string $exception, + ?int $exception_code + ): void { + if ($expected === false) { + $this->expectException($exception); + $this->expectExceptionCode($exception_code); + } + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::compareDate($input_a, $input_b) + ); + } + /** * Undocumented function * @@ -466,6 +840,37 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } + /** + * Undocumented function + * + * @covers ::compareDateTime + * @dataProvider dateTimeCompareProvider + * @testdox compareDateTime $input_a compared to $input_b will be $expected [$_dataName] + * + * @param string $input_a + * @param string $input_b + * @param int|bool $expected + * @param string|null $exception + * @param int|null $exception_code + * @return void + */ + public function testCompareDateTime( + string $input_a, + string $input_b, + int|bool $expected, + ?string $exception, + ?int $exception_code + ): void { + if ($expected === false) { + $this->expectException($exception); + $this->expectExceptionCode($exception_code); + } + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::compareDateTime($input_a, $input_b) + ); + } + /** * Undocumented function * @@ -520,214 +925,6 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ]; } - /** - * Undocumented function - * - * @return array - */ - public function dateRangeHasWeekendProvider(): array - { - return [ - 'no weekend' => [ - '2023-07-03', - '2023-07-04', - false - ], - 'start weekend sat' => [ - '2023-07-01', - '2023-07-04', - true - ], - 'start weekend sun' => [ - '2023-07-02', - '2023-07-04', - true - ], - 'end weekend sat' => [ - '2023-07-03', - '2023-07-08', - true - ], - 'end weekend sun' => [ - '2023-07-03', - '2023-07-09', - true - ], - 'long period > 6 days' => [ - '2023-07-03', - '2023-07-27', - true - ] - ]; - } - - /** - * date string convert test - * - * @covers ::dateStringFormat - * @dataProvider timestampProvider - * @testdox dateStringFormat $input (microtime $flag) will be $expected [$_dataName] - * - * @param int|float $input - * @param bool $flag - * @param string $expected - * @return void - */ - public function testDateStringFormat( - $input, - bool $flag_show_micro, - bool $flag_micro_as_float, - string $expected - ): void { - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::dateStringFormat( - $input, - $flag_show_micro, - $flag_micro_as_float - ) - ); - } - - /** - * interval convert test - * - * @covers ::timeStringFormat - * @dataProvider intervalProvider - * @testdox timeStringFormat $input (microtime $flag) will be $expected [$_dataName] - * - * @param int|float $input - * @param bool $flag - * @param string $expected - * @return void - */ - public function testTimeStringFormat($input, bool $flag, string $expected): void - { - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::timeStringFormat($input, $flag) - ); - } - - /** - * Undocumented function - * - * @covers ::stringToTime - * @dataProvider reverseIntervalProvider - * @testdox stringToTime $input will be $expected [$_dataName] - * - * @param string|int|float $input - * @param string|int|float $expected - * @return void - */ - public function testStringToTime($input, $expected): void - { - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::stringToTime($input) - ); - } - - /** - * Undocumented function - * - * @covers ::checkDate - * @dataProvider dateProvider - * @testdox checkDate $input will be $expected [$_dataName] - * - * @param string $input - * @param bool $expected - * @return void - */ - public function testCheckDate(string $input, bool $expected): void - { - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::checkDate($input) - ); - } - - /** - * Undocumented function - * - * @covers ::checkDateTime - * @dataProvider dateTimeProvider - * @testdox checkDateTime $input will be $expected [$_dataName] - * - * @param string $input - * @param bool $expected - * @return void - */ - public function testCheckDateTime(string $input, bool $expected): void - { - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::checkDateTime($input) - ); - } - - /** - * Undocumented function - * - * @covers ::compareDate - * @dataProvider dateCompareProvider - * @testdox compareDate $input_a compared to $input_b will be $expected [$_dataName] - * - * @param string $input_a - * @param string $input_b - * @param int|bool $expected - * @param string|null $exception - * @param int|null $exception_code - * @return void - */ - public function testCompareDate( - string $input_a, - string $input_b, - int|bool $expected, - ?string $exception, - ?int $exception_code - ): void { - if ($expected === false) { - $this->expectException($exception); - $this->expectExceptionCode($exception_code); - } - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::compareDate($input_a, $input_b) - ); - } - - /** - * Undocumented function - * - * @covers ::compareDateTime - * @dataProvider dateTimeCompareProvider - * @testdox compareDateTime $input_a compared to $input_b will be $expected [$_dataName] - * - * @param string $input_a - * @param string $input_b - * @param int|bool $expected - * @param string|null $exception - * @param int|null $exception_code - * @return void - */ - public function testCompareDateTime( - string $input_a, - string $input_b, - int|bool $expected, - ?string $exception, - ?int $exception_code - ): void { - if ($expected === false) { - $this->expectException($exception); - $this->expectExceptionCode($exception_code); - } - $this->assertEquals( - $expected, - \CoreLibs\Combined\DateTime::compareDateTime($input_a, $input_b) - ); - } - /** * Undocumented function * @@ -906,6 +1103,47 @@ final class CoreLibsCombinedDateTimeTest extends TestCase ); } + /** + * Undocumented function + * + * @return array + */ + public function dateRangeHasWeekendProvider(): array + { + return [ + 'no weekend' => [ + '2023-07-03', + '2023-07-04', + false + ], + 'start weekend sat' => [ + '2023-07-01', + '2023-07-04', + true + ], + 'start weekend sun' => [ + '2023-07-02', + '2023-07-04', + true + ], + 'end weekend sat' => [ + '2023-07-03', + '2023-07-08', + true + ], + 'end weekend sun' => [ + '2023-07-03', + '2023-07-09', + true + ], + 'long period > 6 days' => [ + '2023-07-03', + '2023-07-27', + true + ] + ]; + } + /** * Undocumented function * diff --git a/www/admin/class_test.datetime.php b/www/admin/class_test.datetime.php index a20fb41c..b9073fc4 100644 --- a/www/admin/class_test.datetime.php +++ b/www/admin/class_test.datetime.php @@ -51,10 +51,7 @@ if (round($timestamp, 4) == DateTime::stringToTime($time_string)) { } else { print "REVERSE TRIME STRING DO NOT MATCH
"; } -print "ZERO TIME STRING: " . DateTime::timeStringFormat(0, true) . "
"; -print "ZERO TIME STRING: " . DateTime::timeStringFormat(0.0, true) . "
"; -print "ZERO TIME STRING: " . DateTime::timeStringFormat(1.005, true) . "
"; - +print "
"; $timestamps = [ 1622788315.123456, -1622788315.456789 @@ -64,6 +61,159 @@ foreach ($timestamps as $timestamp) { print "DATESTRINGFORMAT(sm:1:0): $timestamp: " . DateTime::dateStringFormat($timestamp, true) . "
"; print "DATESTRINGFORMAT(sm:1:1): $timestamp: " . DateTime::dateStringFormat($timestamp, true, true) . "
"; } +print "
"; +// $interval = 0; +// $interval = 1000000; +// $interval = 123456; +// $interval = 3600; +// $interval = 3601; +// $interval = 86400; +// $interval = 86401; +$interval = (86400 * 606) + 16434.5; +// $interval = 1.5; +// $interval = 123456; +// $interval = 120.1; +// $interval = 1641515890; +// $interval = 0.123456; +// $interval = 1641515890; +// $interval = 999999999999999999; +// $interval = 60; +try { + print "Test-A: [$interval] " + . DateTime::intervalStringFormatDeprecated( + $interval, + truncate_after: 'd', + natural_seperator: false, + name_space_seperator: false, + show_microseconds: true, + short_time_name: true, + skip_last_zero: true, + skip_zero: false, + show_only_days: false, + auto_fix_microseconds: false, + truncate_nanoseconds: false, + truncate_zero_seconds_if_microseconds: true, + ) + // . " => " + // . DateTime::intervalStringFormat($interval) + . "
"; + print "Test-B: [$interval] " + . DateTime::intervalStringFormat( + $interval, + truncate_after: 'd', + natural_seperator: false, + name_space_seperator: false, + show_microseconds: true, + short_time_name: true, + skip_last_zero: true, + skip_zero: false, + show_only_days: false, + auto_fix_microseconds: false, + truncate_nanoseconds: false, + truncate_zero_seconds_if_microseconds: true, + ) + // . " => " + // . DateTime::intervalStringFormat($interval) + . "
"; + print "DEFAULT-A: " . DateTime::intervalStringFormatDeprecated($interval) . "
"; + print "DEFAULT-B: " . DateTime::intervalStringFormat($interval) . "
"; + $show_micro = true; + print "COMPATIBLE Test-A: " . + DateTime::intervalStringFormatDeprecated( + $interval, + show_microseconds: $show_micro, + show_only_days: true, + skip_zero: false, + skip_last_zero: false, + truncate_nanoseconds: true, + truncate_zero_seconds_if_microseconds: false + ) . "
"; + print "COMPATIBLE Test-B: " . + DateTime::intervalStringFormat( + $interval, + show_microseconds: $show_micro, + show_only_days: true, + skip_zero: false, + skip_last_zero: false, + truncate_nanoseconds: true, + truncate_zero_seconds_if_microseconds: false + ) . "
"; + print "ORIGINAL: " . DateTime::timeStringFormat($interval, $show_micro) . "
"; +} catch (\UnexpectedValueException $e) { + print "ERROR: " . $e->getMessage() . "
" . $e . "

"; +} catch (\LengthException $e) { + print "ERROR interval: " . $e->getMessage() . "
" . $e . "

"; +} +print "
"; +$intervals = [ + ['i' => 0, 'sm' => true], + ['i' => 0.0, 'sm' => true], + ['i' => 1.5, 'sm' => true], + ['i' => 1.05, 'sm' => true], + ['i' => 1.005, 'sm' => true], + ['i' => 1.0005, 'sm' => true], +]; +foreach ($intervals as $int) { + $info = 'ts:' . $int['i'] . '|' . 'sm:' . $int['sm']; + print "[tsf] ZERO TIME STRING [$info]: " + . DateTime::timeStringFormat($int['i'], $int['sm']) . "
"; + print "[isf] ZERO TIME STRING [$info]: " + . DateTime::intervalStringFormat($int['i'], show_microseconds:$int['sm']) . "
"; +} +print "
"; +$intervals = [ + [ + 'i' => 788315.123456, + 'truncate_after' => '', + 'natural_seperator' => false, + 'name_space_seperator' => false, + 'show_microseconds' => true, + 'short_time_name' => true, + 'skip_last_zero' => false, + 'skip_zero' => true, + 'show_only_days' => false, + 'auto_fix_microseconds' => false, + 'truncate_nanoseconds' => false + ], + [ + 'i' => 788315.123456, + 'truncate_after' => '', + 'natural_seperator' => true, + 'name_space_seperator' => true, + 'show_microseconds' => true, + 'short_time_name' => true, + 'skip_last_zero' => false, + 'skip_zero' => true, + 'show_only_days' => false, + 'auto_fix_microseconds' => false, + 'truncate_nanoseconds' => false + ], +]; +foreach ($intervals as $int) { + $info = $int['i']; + try { + print "INTRVALSTRINGFORMAT(sm:0): $info: " + . DateTime::intervalStringFormat( + $int['i'], + truncate_after: (string)$int['truncate_after'], + natural_seperator: $int['natural_seperator'], + name_space_seperator: $int['name_space_seperator'], + show_microseconds: $int['show_microseconds'], + short_time_name: $int['short_time_name'], + skip_last_zero: $int['skip_last_zero'], + skip_zero: $int['skip_zero'], + show_only_days: $int['show_only_days'], + auto_fix_microseconds: $int['auto_fix_microseconds'], + truncate_nanoseconds: $int['truncate_nanoseconds'], + ) . "
"; + } catch (\UnexpectedValueException $e) { + print "ERROR: " . $e->getMessage() . "
" . $e . "

"; + } catch (\LengthException $e) { + print "ERROR interval: " . $e->getMessage() . "
" . $e . "

"; + } +} +print "
"; +// convert and reverste tests $intervals = [ 788315.123456, -123.456 @@ -74,6 +224,7 @@ foreach ($intervals as $interval) { print "TIMESTRINGFORMAT(sm:1): $interval: " . $reverse_interval . "
"; print "STRINGTOTIME: $reverse_interval: " . DateTime::stringToTime($reverse_interval) . "
"; } +print "
"; $check_dates = [ '2021-05-01', '2021-05-40' @@ -81,6 +232,7 @@ $check_dates = [ foreach ($check_dates as $check_date) { print "CHECKDATE: $check_date: " . (string)DateTime::checkDate($check_date) . "
"; } +print "
"; $check_datetimes = [ '2021-05-01', '2021-05-40', @@ -91,6 +243,7 @@ $check_datetimes = [ foreach ($check_datetimes as $check_datetime) { print "CHECKDATETIME: $check_datetime: " . (string)DateTime::checkDateTime($check_datetime) . "
"; } +print "
"; $compare_dates = [ [ '2021-05-01', '2021-05-02', ], [ '2021-05-02', '2021-05-01', ], @@ -102,6 +255,7 @@ foreach ($compare_dates as $compare_date) { print "COMPAREDATE: $compare_date[0] = $compare_date[1]: " . (string)DateTime::compareDate($compare_date[0], $compare_date[1]) . "
"; } +print "
"; $compare_datetimes = [ [ '2021-05-01', '2021-05-02', ], [ '2021-05-02', '2021-05-01', ], @@ -114,6 +268,7 @@ foreach ($compare_datetimes as $compare_datetime) { print "COMPAREDATE: $compare_datetime[0] = $compare_datetime[1]: " . (string)DateTime::compareDateTime($compare_datetime[0], $compare_datetime[1]) . "
"; } +print "
"; $compare_dates = [ [ '2021-05-01', '2021-05-10', ], [ '2021-05-10', '2021-05-01', ], @@ -126,7 +281,7 @@ foreach ($compare_dates as $compare_date) { print "CALCDAYSINTERVAL(named): $compare_date[0] = $compare_date[1]: " . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], true)) . "
"; } - +print "
"; // test date conversion $dow = 2; print "DOW[$dow]: " . DateTime::setWeekdayNameFromIsoDow($dow) . "
"; @@ -142,7 +297,7 @@ $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'; diff --git a/www/lib/CoreLibs/Combined/DateTime.php b/www/lib/CoreLibs/Combined/DateTime.php index a807a39d..50532a57 100644 --- a/www/lib/CoreLibs/Combined/DateTime.php +++ b/www/lib/CoreLibs/Combined/DateTime.php @@ -108,7 +108,12 @@ class DateTime if (preg_match("/(h|m|s|ms)/", (string)$timestamp)) { return (string)$timestamp; } - list($timestamp, $ms) = array_pad(explode('.', (string)round((float)$timestamp, 4)), 2, null); + // split to 6 (nano seconds) + list($timestamp, $ms) = array_pad(explode('.', (string)round((float)$timestamp, 6)), 2, null); + // if micro seconds is on and we have none, set to 0 + if ($show_micro && $ms === null) { + $ms = 0; + } // if negative remember $negative = false; if ((int)$timestamp < 0) { @@ -120,6 +125,10 @@ class DateTime $time_string = ''; // if timestamp is zero, return zero string if ($timestamp == 0) { + // if no seconds and we have no microseconds either, show no micro seconds + if ($ms == 0) { + $ms = null; + } $time_string = '0s'; } else { for ($i = 0, $iMax = count($timegroups); $i < $iMax; $i++) { @@ -133,11 +142,8 @@ class DateTime } // only add ms if we have an ms value if ($ms !== null) { - // if we have ms and it has leading zeros, remove them, but only if it is nut just 0 - $ms = preg_replace("/^0+(\d+)$/", '${1}', $ms); - if (!is_string($ms) || empty($ms)) { - $ms = '0'; - } + // prefix the milliseoncds with 0. and round it max 3 digits and then convert to int + $ms = round((float)('0.' . $ms), 3) * 1000; // add ms if there if ($show_micro) { $time_string .= ' ' . $ms . 'ms'; @@ -151,6 +157,240 @@ class DateTime return (string)$time_string; } + /** + * update timeStringFormat with year and month support + * + * The following flags have to be set to be timeStringFormat compatible. + * Not that on seconds overflow this method will throw an exception, timeStringFormat returned -1s + * show_only_days: true, + * skip_zero: false, + * skip_last_zero: false, + * truncate_nanoseconds: true, + * truncate_zero_seconds_if_microseconds: false + * + * @param int|float $seconds Seconds to convert, maxium 6 decimals, + * else \UnexpectedValueException will be thrown + * if days too large or years too large \LengthException is thrown + * @param string $truncate_after [=''] Truncate after which time name, will not round, hard end + * values are parts names or interval short names (y, d, f, ...) + * if illegal value \UnexpectedValueException is thrown + * @param bool $natural_seperator [=false] use ',' and 'and', if off use space + * @param bool $name_space_seperator [=false] add a space between the number and the time name + * @param bool $show_microseconds [=true] show microseconds + * @param bool $short_time_name [=true] use the short time names (eg s instead of seconds) + * @param bool $skip_last_zero [=true] skip all trailing zero values, eg 5m 0s => 5m + * @param bool $skip_zero [=true] do not show zero values anywhere, eg 1h 0m 20s => 1h 20s + * @param bool $show_only_days [=false] do not show years or months, show only days + * if truncate after is set to year or month + * throws \UnexpectedValueException + * @param bool $auto_fix_microseconds [=false] if the micro seconds decimals are more than 6, round them + * on defaul throw \UnexpectedValueException + * @param bool $truncate_nanoseconds [=false] if microseconds decimals >3 then normal we show 123.4ms + * cut the .4 is set to true + * @param bool $truncate_zero_seconds_if_microseconds [=true] if we have 0.123 seconds then if true no seconds + * will be shown + * @return string + * @throws \UnexpectedValueException if seconds has more than 6 decimals + * if truncate has an illegal value + * if truncate is set to year or month and show_only_days is turned on + * @throws \LengthException if seconds is too large and show_days_only is selected and days is negetive + * or if years is negativ + */ + public static function intervalStringFormat( + int|float $seconds, + string $truncate_after = '', + bool $natural_seperator = false, + bool $name_space_seperator = false, + bool $show_microseconds = true, + bool $short_time_name = true, + bool $skip_last_zero = true, + bool $skip_zero = true, + bool $show_only_days = false, + bool $auto_fix_microseconds = false, + bool $truncate_nanoseconds = false, + bool $truncate_zero_seconds_if_microseconds = true, + ): string { + // auto fix long seconds, else \UnexpectedValueException will be thrown on error + // check if we have float and -> round to 6 + if ($auto_fix_microseconds === true && is_float($seconds)) { + $seconds = round($seconds, 6); + } + // flag negative + set abs + $negative = $seconds < 0 ? '-' : ''; + $seconds = abs($seconds); + // create base time + $date_now = new \DateTime("@0"); + try { + $date_seconds = new \DateTime("@$seconds"); + } catch (\Exception $e) { + throw new \UnexpectedValueException( + 'Seconds value is invalid, too large or more than six decimals: ' . $seconds, + 1, + $e + ); + } + $interval = date_diff($date_now, $date_seconds); + // if show_only_days and negative but input postive alert that this has to be done in y/m/d ... + if ($interval->y < 0) { + throw new \LengthException('Input seconds value is too large for years output: ' . $seconds, 2); + } elseif ($interval->days < 0 && $show_only_days === true) { + throw new \LengthException('Input seconds value is too large for days output: ' . $seconds, 3); + } + $parts = [ + 'years' => 'y', + 'months' => 'm', + 'days' => 'd', + 'hours' => 'h', + 'minutes' => 'i', + 'seconds' => 's', + 'microseconds' => 'f', + ]; + $short_name = [ + 'years' => 'y', 'months' => 'm', 'days' => 'd', + 'hours' => 'h', 'minutes' => 'm', 'seconds' => 's', + 'microseconds' => 'ms' + ]; + // $skip = false; + if (!empty($truncate_after)) { + // if truncate after not in key or value in parts + if (!in_array($truncate_after, array_keys($parts)) && !in_array($truncate_after, array_values($parts))) { + throw new \UnexpectedValueException( + 'truncate_after has an invalid value: ' . $truncate_after, + 4 + ); + } + // if truncate after is y or m and we have show_only_days, throw exception + if ($show_only_days === true && in_array($truncate_after, ['y', 'years', 'm', 'months'])) { + throw new \UnexpectedValueException( + 'If show_only_days is turned on, the truncate_after cannot be years or months: ' + . $truncate_after, + 5 + ); + } + // $skip = true; + } + $formatted = []; + $zero_formatted = []; + $value_set = false; + $add_zero_seconds = false; + foreach ($parts as $time_name => $part) { + if ( + // skip for micro seconds + ($show_microseconds === false && $part == 'f') || + // skip for if days only and we have year or month + ($show_only_days === true && in_array($part, ['y', 'm'])) + ) { + continue; + } + $add_value = 0; + if ($show_only_days === true && $part == 'd') { + $value = $interval->days; + } else { + $value = $interval->$part; + } + // print "-> V: $value | $part, $time_name" + // . " | Set: " . ($value_set ? 'Y' : 'N') . ", SkipZ: " . ($skip_zero ? 'Y' : 'N') + // . " | SkipLZ: " . ($skip_last_zero ? 'Y' : 'N') + // . " | " . ($value != 0 ? 'Not zero' : 'ZERO') . "
"; + if ($value != 0) { + if ($part == 'f') { + if ($truncate_nanoseconds === true) { + $value = round($value, 3); + } + $value *= 1000; + // anything above that is nano seconds? + } + if ($value) { + $value_set = true; + } + $add_value = 1; + } elseif ( + $value == 0 && + $value_set === true && ( + $skip_last_zero === false || + $skip_zero === false + ) + ) { + $add_value = 2; + } + // echo "ADD VALUE: $add_value
"; + if ($add_value) { + // build format + $format = "$value"; + if ($name_space_seperator) { + $format .= " "; + } + if ($short_time_name) { + $format .= $short_name[$time_name]; + } elseif ($value == 1) { + $format .= substr($time_name, 0, -1); + } else { + $format .= $time_name; + } + if ($add_value == 1) { + if (count($zero_formatted) && $skip_zero === false) { + $formatted = array_merge($formatted, $zero_formatted); + } + $zero_formatted = []; + $formatted[] = $format; + } elseif ($add_value == 2) { + $zero_formatted[] = $format; + } + } + // if seconds is zero + if ( + $part == 's' && $value == 0 && + $show_microseconds === true && + $truncate_zero_seconds_if_microseconds === false + ) { + $add_zero_seconds = true; + } + // stop after a truncate is matching + if ($part == $truncate_after || $truncate_after == $time_name) { + break; + } + } + // add all zero entries if we have skip last off + if (count($zero_formatted) && $skip_last_zero === false) { + $formatted = array_merge($formatted, $zero_formatted); + } + // print "=> F: " . print_r($formatted, true) + // . " | Z: " . print_r($zero_list, true) + // . " | ZL: " . print_r($zero_last_list, true) + // . "
"; + if (count($formatted) == 0) { + // if we have truncate on, then we assume nothing was found + if (!empty($truncate_after)) { + if (in_array($truncate_after, array_values($parts))) { + $truncate_after = array_flip($parts)[$truncate_after]; + } + $time_name = $truncate_after; + } else { + $time_name = 'seconds'; + } + return '0' . ($name_space_seperator ? ' ' : '') + . ($short_time_name ? $short_name[$time_name] : $time_name); + } elseif (count($formatted) == 1) { + return $negative . + ($add_zero_seconds ? + '0' + . ($name_space_seperator ? ' ' : '') + . ($short_time_name ? $short_name['seconds'] : 'seconds') + . ' ' + : '' + ) + . $formatted[0]; + } elseif ($natural_seperator === false) { + return $negative . implode(' ', $formatted); + } else { + $str = implode(', ', array_slice($formatted, 0, -1)); + if (!empty($formatted[count($formatted) - 1])) { + $str .= ' and ' . $formatted[count($formatted) - 1]; + } + return $negative . $str; + } + } + /** * does a reverse of the timeStringFormat and converts the string from * xd xh xm xs xms to a timestamp.microtime format