diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 4d414a9..8ac52ba 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -28,6 +28,7 @@ class ValueInfo { const T_DATE = 5; // convert to DateTimeInterface instance const T_STRING = 6; // convert to string const T_ARRAY = 7; // convert to array + const T_INTERVAL = 8; // convert to time interval // normalization modes const M_NULL = 1 << 28; // pass nulls through regardless of target type const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match @@ -95,6 +96,29 @@ class ValueInfo { throw new ExceptionType("strictFailure", $type); } return (!$drop) ? (int) $value->getTimestamp(): null; + } elseif ($value instanceof \DateInterval) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } elseif ($drop) { + return null; + } else { + // returns the number of seconds in the interval + // days are assumed to contain (60 * 60 * 24) seconds + // months are assumed to contain 30 days + // years are assumed to contain 365 days + $s = 0; + if ($value->days !== false) { + $s += ($value->days * 24 * 60 * 60); + } else { + $s += ($value->y * 365 * 24 * 60 * 60); + $s += ($value->m * 30 * 24 * 60 * 60); + $s += ($value->d * 24 * 60 * 60); + } + $s += ($value->h * 60 * 60); + $s += ($value->i * 60); + $s += $value->s; + return $s; + } } $info = self::int($value); if ($strict && !($info & self::VALID)) { @@ -124,6 +148,16 @@ class ValueInfo { throw new ExceptionType("strictFailure", $type); } return (!$drop) ? (float) $value->getTimestamp(): null; + } elseif ($value instanceof \DateInterval) { + if ($drop) { + return null; + } elseif ($strict) { + throw new ExceptionType("strictFailure", $type); + } + // convert the interval to an integer, and then add microseconds if available (since PHP 7.1, for intervals created from a DateTime difference operation) + $out = (float) self::normalize($value, self::T_INT); + $out += isset($value->f) ? $value->f : 0.0; + return $out; } elseif (is_bool($value) && $strict) { if ($drop) { return null; @@ -151,6 +185,25 @@ class ValueInfo { } elseif ($value instanceof \DateTime) { return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); } + } elseif ($value instanceof \DateInterval) { + $dateSpec = ""; + $timeSpec = ""; + if ($value->days) { + $dateSpec = $value->days."D"; + } else { + $dateSpec .= $value->y ? $value->y."Y": ""; + $dateSpec .= $value->m ? $value->m."M": ""; + $dateSpec .= $value->d ? $value->d."D": ""; + } + $timeSpec .= $value->h ? $value->h."H": ""; + $timeSpec .= $value->i ? $value->i."M": ""; + $timeSpec .= $value->s ? $value->s."S": ""; + $timeSpec = $timeSpec ? "T".$timeSpec : ""; + if (!$dateSpec && !$timeSpec) { + return "PT0S"; + } else { + return "P".$dateSpec.$timeSpec; + } } elseif (is_float($value) && is_finite($value)) { $out = (string) $value; if (!strpos($out, "E")) { @@ -183,7 +236,7 @@ class ValueInfo { return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC")); } elseif (is_int($value)) { return \DateTimeImmutable::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); - } elseif (is_float($value)) { + } elseif (is_float($value) && is_finite($value)) { return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); } elseif (is_string($value)) { try { @@ -252,6 +305,92 @@ class ValueInfo { } } break; // @codeCoverageIgnore + case self::T_INTERVAL: + if ($value instanceof \DateInterval) { + if ($value->invert) { + $value = clone $value; + $value->invert = 0; + } + $value->f = $value->f ?? 0.0; // add microseconds for PHP 7.0 + return $value; + } elseif (is_null($value)) { + if ($strict && !$drop && !$allowNull) { + throw new ExceptionType("strictFailure", $type); + } else { + return null; + } + } elseif (is_bool($value) || is_array($value) || (is_float($value) && (is_infinite($value) || is_nan($value))) || $value instanceof \DateTimeInterface || (is_object($value) && !method_exists($value, "__toString"))) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } else { + return null; + } + } elseif (is_string($value) || is_object($value)) { + try { + $out = new \DateInterval((string) $value); + $out->f = 0.0; + return $out; + } catch (\Exception $e) { + if ($strict && !$drop) { + throw new ExceptionType("strictFailure", $type); + } elseif ($drop) { + return null; + } elseif (strtotime("now + $value") !== false) { + $out = \DateInterval::createFromDateString($value); + $out->f = 0.0; + return $out; + } else { + return null; + } + } + } elseif ($drop) { + return null; + } elseif ($strict) { + throw new ExceptionType("strictFailure", $type); + } else { + // input is a number, assume this is a number of seconds + // for legibility we convert large numbers to minutes, hours, and days as necessary + // the DateInterval constructor only allows 12 digits for any given part of an interval, + // so we also convert days to 365-day years where we must, and cap the number of years + // at (1e11 - 1); this being a very large number, the loss of precision is probably not + // significant in practical usage + $sec = abs($value); + $msec = (float) ($sec - (int) $sec); + $sec = (int) $sec; + $min = 0; + $hour = 0; + $day = 0; + $year = 0; + if ($sec >= 60) { + $min = ($sec - ($sec % 60)) / 60; + $sec %= 60; + } + if ($min >= 60) { + $hour = ($min - ($min % 60)) / 60; + $min %= 60; + } + if ($hour >= 24) { + $day = ($hour - ($hour % 24)) / 24; + $hour %= 24; + } + if ($day >= 999999999999) { + $year = ($day - ($day % 365)) / 365; + $day %= 365; + } + $spec = "P"; + $spec .= $year ? $year."Y" : ""; + $spec .= $day ? $day."D" : ""; + $spec .= "T"; + $spec .= $hour ? $hour."H" : ""; + $spec .= $min ? $min."M" : ""; + $spec .= $sec ? $sec."S" : ""; + $spec .= ($spec === "PT") ? "0S" : ""; + $spec = trim($spec, "T"); + $out = new \DateInterval($spec); + $out->f = $msec; + return $out; + } + break; // @codeCoverageIgnore default: throw new ExceptionType("typeUnknown", $type); // @codeCoverageIgnore } diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 60c7da0..75a48bc 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -310,8 +310,137 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { } } - public function testNormalizeValues() { + /** @dataProvider provideSimpleNormalizationValues */ + public function testNormalizeSimpleValues($input, string $typeName, $exp, bool $pass, bool $strict, bool $drop) { + $assert = function($exp, $act, string $msg) { + if (is_null($exp)) { + $this->assertNull($act, $msg); + } elseif (is_float($exp) && is_nan($exp)) { + $this->assertNan($act, $msg); + } elseif (is_scalar($exp)) { + $this->assertSame($exp, $act, $msg); + } else { + $this->assertEquals($exp, $act, $msg); + } + }; + $typeConst = [ + 'Mixed' => I::T_MIXED, + 'Null' => I::T_NULL, + 'Boolean' => I::T_BOOL, + 'Integer' => I::T_INT, + 'Floating point' => I::T_FLOAT, + 'String' => I::T_STRING, + 'Array' => I::T_ARRAY, + 'Date interval' => I::T_INTERVAL, + ][$typeName]; + if ($strict && $drop) { + $modeName = "strict drop"; + $modeConst = I::M_STRICT | I::M_DROP; + } elseif ($strict) { + $modeName = "strict conversion"; + $modeConst = I::M_STRICT; + } elseif ($drop) { + $modeName = "drop"; + $modeConst = I::M_DROP; + } else { + $modeName = "loose conversion"; + $modeConst = 0; + } + if (is_null($input)) { + // if the input value is null, perform a null passthrough test in addition to the test itself + $this->assertNull(I::normalize($input, $typeConst | $modeConst | I::M_NULL), "$typeName null passthrough test failed."); + } + if (!$drop && $strict && !$pass) { + // if we're performing a strict comparison and the value is supposed to fail, we should be getting an exception + $this->assertException("strictFailure", "", "ExceptionType"); + I::normalize($input, $typeConst | $modeConst); + $this->assertTrue(false, "$typename $modeName test expected exception"); + } elseif ($drop && !$pass) { + // if we're performing a drop comparison and the value is supposed to fail, change the expectation to null + $exp = null; + } + $assert($exp, I::normalize($input, $typeConst | $modeConst), "$typeName $modeName test failed."); + // check that the result is the same even in null mode + if (!is_null($input)) { + $assert($exp, I::normalize($input, $typeConst | $modeConst | I::M_NULL), "$typeName $modeName (null pass-through) test failed."); + } + } + + /** @dataProvider provideDateNormalizationValues */ + public function testNormalizeDateValues($input, $format, $exp, bool $strict, bool $drop) { + if ($strict && $drop) { + $modeName = "strict drop"; + $modeConst = I::M_STRICT | I::M_DROP; + } elseif ($strict) { + $modeName = "strict conversion"; + $modeConst = I::M_STRICT; + } elseif ($drop) { + $modeName = "drop"; + $modeConst = I::M_DROP; + } else { + $modeName = "loose conversion"; + $modeConst = 0; + } + if (is_null($exp)) { + if (is_null($input)) { + // if the input value is null, perform a null passthrough test before the test itself + $this->assertNull(I::normalize($input, I::T_DATE | $modeConst | I::M_NULL, $format, $format), "Date input format ".var_export($input, true)." failed $modeName (null passthrough) test."); + } + if (!$drop && $strict && is_null($exp)) { + // if we're performing a strict comparison and the value is supposed to fail, we should be getting an exception + $this->assertException("strictFailure", "", "ExceptionType"); + } + $this->assertNull(I::normalize($input, I::T_DATE | $modeConst, $format, $format), "Date input format ".var_export($input, true)." failed $modeName test."); + $this->assertNull(I::normalize($input, I::T_DATE | $modeConst | I::M_NULL, $format, $format), "Date input format ".var_export($input, true)." failed $modeName (null passthrough) test."); + } else { + $this->assertEquals($exp, I::normalize($input, I::T_DATE | $modeConst | I::M_NULL, $format, $format), "Date input format ".var_export($input, true)." failed $modeName (null passthrough) test."); + $this->assertEquals($exp, I::normalize($input, I::T_DATE | $modeConst, $format, $format), "Date input format ".var_export($input, true)." failed $modeName test."); + } + } + + public function testNormalizeComplexValues() { + // Array-mode tests $tests = [ + [I::T_INT | I::M_DROP, [1, 2, 2.2, 3], [1,2,null,3] ], + [I::T_INT, [1, 2, 2.2, 3], [1,2,2,3] ], + [I::T_INT | I::M_DROP, new Result([1, 2, 2.2, 3]), [1,2,null,3] ], + [I::T_INT, new Result([1, 2, 2.2, 3]), [1,2,2,3] ], + [I::T_STRING | I::M_STRICT, "Bare string", ["Bare string"]], + ]; + foreach ($tests as $index => $test) { + list($type, $value, $exp) = $test; + $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index"); + } + // Date-to-string format tests + $test = new \DateTimeImmutable("now", new \DateTimezone("UTC")); + $exp = $test->format(I::DATE_FORMATS['iso8601'][1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null), "Failed test for null output date format"); + foreach (I::DATE_FORMATS as $name => $formats) { + $exp = $test->format($formats[1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $name), "Failed test for output date format '$name'"); + } + foreach (["U", "M j, Y (D)", "r", "c"] as $format) { + $exp = $test->format($format); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $format), "Failed test for output date format '$format'"); + } + } + + public function provideSimpleNormalizationValues() { + $types = [ + "Mixed", + "Null", + "Boolean", + "Integer", + "Floating point", + "String", + "Array", + "Date interval", + ]; + $dateDiff = (new \DateTime("2017-01-01T00:00:00Z"))->diff((new \DateTime("2016-01-01T00:00:00Z"))); // 2016 was a leap year + $dateNorm = clone $dateDiff; + $dateNorm->f = 0.0; + $dateNorm->invert = 0; + foreach ([ /* The test data are very dense for this set. Each value is normalized to each of the following types: - mixed (no normalization performed) @@ -321,134 +450,132 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { - float - string - array + - interval For each of these types, there is an expected output value, as well as a boolean indicating whether the value should pass or fail a strict normalization. Conversion to DateTime is covered below by a different data set */ - /* Input value null bool int float string array */ - [null, [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], false]], - ["", [null,true], [false,true], [0, false], [0.0, false], ["", true], [[""], false]], - [1, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1], false]], - [PHP_INT_MAX, [null,true], [true, false], [PHP_INT_MAX, true], [(float) PHP_INT_MAX,true], [(string) PHP_INT_MAX, true], [[PHP_INT_MAX], false]], - [1.0, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1.0], false]], - ["1.0", [null,true], [true, true], [1, true], [1.0, true], ["1.0", true], [["1.0"], false]], - ["001.0", [null,true], [true, true], [1, true], [1.0, true], ["001.0", true], [["001.0"], false]], - ["1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["1.0e2", true], [["1.0e2"], false]], - ["1", [null,true], [true, true], [1, true], [1.0, true], ["1", true], [["1"], false]], - ["001", [null,true], [true, true], [1, true], [1.0, true], ["001", true], [["001"], false]], - ["1e2", [null,true], [true, false], [100, true], [100.0, true], ["1e2", true], [["1e2"], false]], - ["+1.0", [null,true], [true, true], [1, true], [1.0, true], ["+1.0", true], [["+1.0"], false]], - ["+001.0", [null,true], [true, true], [1, true], [1.0, true], ["+001.0", true], [["+001.0"], false]], - ["+1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["+1.0e2", true], [["+1.0e2"], false]], - ["+1", [null,true], [true, true], [1, true], [1.0, true], ["+1", true], [["+1"], false]], - ["+001", [null,true], [true, true], [1, true], [1.0, true], ["+001", true], [["+001"], false]], - ["+1e2", [null,true], [true, false], [100, true], [100.0, true], ["+1e2", true], [["+1e2"], false]], - [0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0], false]], - ["0", [null,true], [false,true], [0, true], [0.0, true], ["0", true], [["0"], false]], - ["000", [null,true], [false,true], [0, true], [0.0, true], ["000", true], [["000"], false]], - [0.0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0.0], false]], - ["0.0", [null,true], [false,true], [0, true], [0.0, true], ["0.0", true], [["0.0"], false]], - ["000.000", [null,true], [false,true], [0, true], [0.0, true], ["000.000", true], [["000.000"], false]], - ["+0", [null,true], [false,true], [0, true], [0.0, true], ["+0", true], [["+0"], false]], - ["+000", [null,true], [false,true], [0, true], [0.0, true], ["+000", true], [["+000"], false]], - ["+0.0", [null,true], [false,true], [0, true], [0.0, true], ["+0.0", true], [["+0.0"], false]], - ["+000.000", [null,true], [false,true], [0, true], [0.0, true], ["+000.000", true], [["+000.000"], false]], - [-1, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1], false]], - [-1.0, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1.0], false]], - ["-1.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-1.0", true], [["-1.0"], false]], - ["-001.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-001.0", true], [["-001.0"], false]], - ["-1.0e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1.0e2", true], [["-1.0e2"], false]], - ["-1", [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [["-1"], false]], - ["-001", [null,true], [true, false], [-1, true], [-1.0, true], ["-001", true], [["-001"], false]], - ["-1e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1e2", true], [["-1e2"], false]], - [-0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[-0], false]], - ["-0", [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [["-0"], false]], - ["-000", [null,true], [false,true], [0, true], [-0.0, true], ["-000", true], [["-000"], false]], - [-0.0, [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [[-0.0], false]], - ["-0.0", [null,true], [false,true], [0, true], [-0.0, true], ["-0.0", true], [["-0.0"], false]], - ["-000.000", [null,true], [false,true], [0, true], [-0.0, true], ["-000.000", true], [["-000.000"], false]], - [false, [null,true], [false,true], [0, false], [0.0, false], ["", false], [[false], false]], - [true, [null,true], [true, true], [1, false], [1.0, false], ["1", false], [[true], false]], - ["on", [null,true], [true, true], [0, false], [0.0, false], ["on", true], [["on"], false]], - ["off", [null,true], [false,true], [0, false], [0.0, false], ["off", true], [["off"], false]], - ["yes", [null,true], [true, true], [0, false], [0.0, false], ["yes", true], [["yes"], false]], - ["no", [null,true], [false,true], [0, false], [0.0, false], ["no", true], [["no"], false]], - ["true", [null,true], [true, true], [0, false], [0.0, false], ["true", true], [["true"], false]], - ["false", [null,true], [false,true], [0, false], [0.0, false], ["false", true], [["false"], false]], - [INF, [null,true], [true, false], [0, false], [INF, true], ["INF", false], [[INF], false]], - [-INF, [null,true], [true, false], [0, false], [-INF, true], ["-INF", false], [[-INF], false]], - [NAN, [null,true], [false,false], [0, false], [NAN, true], ["NAN", false], [[], false]], - [[], [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], true] ], - ["some string", [null,true], [true, false], [0, false], [0.0, false], ["some string", true], [["some string"], false]], - [" ", [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[" "], false]], - [new \StdClass, [null,true], [true, false], [0, false], [0.0, false], ["", false], [[new \StdClass], false]], - [new StrClass(""), [null,true], [false,true], [0, false], [0.0, false], ["", true], [[new StrClass("")], false]], - [new StrClass("1"), [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[new StrClass("1")], false]], - [new StrClass("0"), [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[new StrClass("0")], false]], - [new StrClass("-1"), [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[new StrClass("-1")], false]], - [new StrClass("Msg"), [null,true], [true, false], [0, false], [0.0, false], ["Msg", true], [[new StrClass("Msg")], false]], - [new StrClass(" "), [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[new StrClass(" ")], false]], - [2.5, [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [[2.5], false]], - [0.5, [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [[0.5], false]], - ["2.5", [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [["2.5"], false]], - ["0.5", [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [["0.5"], false]], - [$this->d("2010-01-01T00:00:00", 0, 0), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 0, 0)],false]], - [$this->d("2010-01-01T00:00:00", 0, 1), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 0, 1)],false]], - [$this->d("2010-01-01T00:00:00", 1, 0), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 1, 0)],false]], - [$this->d("2010-01-01T00:00:00", 1, 1), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 1, 1)],false]], - [1e14, [null,true], [true, false], [100000000000000,true], [1e14, true], ["100000000000000", true], [[1e14], false]], - [1e-6, [null,true], [true, false], [0, false], [1e-6, true], ["0.000001", true], [[1e-6], false]], - [[1,2,3], [null,true], [true, false], [0, false], [0.0, false], ["", false], [[1,2,3], true] ], - [['a'=>1,'b'=>2], [null,true], [true, false], [0, false], [0.0, false], ["", false], [['a'=>1,'b'=>2], true] ], - [new Result([['a'=>1,'b'=>2]]), [null,true], [true, false], [0, false], [0.0, false], ["", false], [[['a'=>1,'b'=>2]], true] ], - ]; - $params = [ - [I::T_MIXED, "Mixed" ], - [I::T_NULL, "Null", ], - [I::T_BOOL, "Boolean", ], - [I::T_INT, "Integer", ], - [I::T_FLOAT, "Floating point"], - [I::T_STRING, "String", ], - [I::T_ARRAY, "Array", ], - ]; - $assert = function($exp, $act, string $msg) { - if (is_null($exp)) { - $this->assertNull($act, $msg); - } elseif (is_float($exp) && is_nan($exp)) { - $this->assertNan($act, $msg); - } elseif (is_scalar($exp)) { - $this->assertSame($exp, $act, $msg); - } else { - $this->assertEquals($exp, $act, $msg); - } - }; - foreach ($params as $index => $param) { - list($type, $name) = $param; - $assert(null, I::normalize(null, $type | I::M_STRICT | I::M_NULL), $name." null-passthrough test failed"); - foreach ($tests as $test) { - list($exp, $pass) = $index ? $test[$index] : [$test[$index], true]; - $value = $test[0]; - $assert($exp, I::normalize($value, $type), $name." test failed for value: ".var_export($value, true)); - if ($pass) { - $assert($exp, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); - $assert($exp, I::normalize($value, $type | I::M_STRICT), $name." error test failed for value: ".var_export($value, true)); - } else { - $assert(null, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); - $exc = new ExceptionType("strictFailure", $type); - try { - $act = I::normalize($value, $type | I::M_STRICT); - } catch (ExceptionType $e) { - $act = $e; - } finally { - $assert($exc, $act, $name." error test failed for value: ".var_export($value, true)); - } + /* Input value null bool int float string array interval */ + [null, [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], false], [null, false]], + ["", [null,true], [false,true], [0, false], [0.0, false], ["", true], [[""], false], [null, false]], + [1, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1], false], [$this->i("PT1S"), false]], + [PHP_INT_MAX, [null,true], [true, false], [PHP_INT_MAX, true], [(float) PHP_INT_MAX,true], [(string) PHP_INT_MAX, true], [[PHP_INT_MAX], false], [$this->i("P292471208677Y195DT15H30M7S"), false]], + [1.0, [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[1.0], false], [$this->i("PT1S"), false]], + ["1.0", [null,true], [true, true], [1, true], [1.0, true], ["1.0", true], [["1.0"], false], [null, false]], + ["001.0", [null,true], [true, true], [1, true], [1.0, true], ["001.0", true], [["001.0"], false], [null, false]], + ["1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["1.0e2", true], [["1.0e2"], false], [null, false]], + ["1", [null,true], [true, true], [1, true], [1.0, true], ["1", true], [["1"], false], [null, false]], + ["001", [null,true], [true, true], [1, true], [1.0, true], ["001", true], [["001"], false], [null, false]], + ["1e2", [null,true], [true, false], [100, true], [100.0, true], ["1e2", true], [["1e2"], false], [null, false]], + ["+1.0", [null,true], [true, true], [1, true], [1.0, true], ["+1.0", true], [["+1.0"], false], [null, false]], + ["+001.0", [null,true], [true, true], [1, true], [1.0, true], ["+001.0", true], [["+001.0"], false], [null, false]], + ["+1.0e2", [null,true], [true, false], [100, true], [100.0, true], ["+1.0e2", true], [["+1.0e2"], false], [null, false]], + ["+1", [null,true], [true, true], [1, true], [1.0, true], ["+1", true], [["+1"], false], [null, false]], + ["+001", [null,true], [true, true], [1, true], [1.0, true], ["+001", true], [["+001"], false], [null, false]], + ["+1e2", [null,true], [true, false], [100, true], [100.0, true], ["+1e2", true], [["+1e2"], false], [null, false]], + [0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0], false], [$this->i("PT0S"), false]], + ["0", [null,true], [false,true], [0, true], [0.0, true], ["0", true], [["0"], false], [null, false]], + ["000", [null,true], [false,true], [0, true], [0.0, true], ["000", true], [["000"], false], [null, false]], + [0.0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[0.0], false], [$this->i("PT0S"), false]], + ["0.0", [null,true], [false,true], [0, true], [0.0, true], ["0.0", true], [["0.0"], false], [null, false]], + ["000.000", [null,true], [false,true], [0, true], [0.0, true], ["000.000", true], [["000.000"], false], [null, false]], + ["+0", [null,true], [false,true], [0, true], [0.0, true], ["+0", true], [["+0"], false], [null, false]], + ["+000", [null,true], [false,true], [0, true], [0.0, true], ["+000", true], [["+000"], false], [null, false]], + ["+0.0", [null,true], [false,true], [0, true], [0.0, true], ["+0.0", true], [["+0.0"], false], [null, false]], + ["+000.000", [null,true], [false,true], [0, true], [0.0, true], ["+000.000", true], [["+000.000"], false], [null, false]], + [-1, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1], false], [$this->i("PT1S"), false]], + [-1.0, [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[-1.0], false], [$this->i("PT1S"), false]], + ["-1.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-1.0", true], [["-1.0"], false], [null, false]], + ["-001.0", [null,true], [true, false], [-1, true], [-1.0, true], ["-001.0", true], [["-001.0"], false], [null, false]], + ["-1.0e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1.0e2", true], [["-1.0e2"], false], [null, false]], + ["-1", [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [["-1"], false], [null, false]], + ["-001", [null,true], [true, false], [-1, true], [-1.0, true], ["-001", true], [["-001"], false], [null, false]], + ["-1e2", [null,true], [true, false], [-100, true], [-100.0, true], ["-1e2", true], [["-1e2"], false], [null, false]], + [-0, [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[-0], false], [$this->i("PT0S"), false]], + ["-0", [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [["-0"], false], [null, false]], + ["-000", [null,true], [false,true], [0, true], [-0.0, true], ["-000", true], [["-000"], false], [null, false]], + [-0.0, [null,true], [false,true], [0, true], [-0.0, true], ["-0", true], [[-0.0], false], [$this->i("PT0S"), false]], + ["-0.0", [null,true], [false,true], [0, true], [-0.0, true], ["-0.0", true], [["-0.0"], false], [null, false]], + ["-000.000", [null,true], [false,true], [0, true], [-0.0, true], ["-000.000", true], [["-000.000"], false], [null, false]], + [false, [null,true], [false,true], [0, false], [0.0, false], ["", false], [[false], false], [null, false]], + [true, [null,true], [true, true], [1, false], [1.0, false], ["1", false], [[true], false], [null, false]], + ["on", [null,true], [true, true], [0, false], [0.0, false], ["on", true], [["on"], false], [null, false]], + ["off", [null,true], [false,true], [0, false], [0.0, false], ["off", true], [["off"], false], [null, false]], + ["yes", [null,true], [true, true], [0, false], [0.0, false], ["yes", true], [["yes"], false], [null, false]], + ["no", [null,true], [false,true], [0, false], [0.0, false], ["no", true], [["no"], false], [null, false]], + ["true", [null,true], [true, true], [0, false], [0.0, false], ["true", true], [["true"], false], [null, false]], + ["false", [null,true], [false,true], [0, false], [0.0, false], ["false", true], [["false"], false], [null, false]], + [INF, [null,true], [true, false], [0, false], [INF, true], ["INF", false], [[INF], false], [null, false]], + [-INF, [null,true], [true, false], [0, false], [-INF, true], ["-INF", false], [[-INF], false], [null, false]], + [NAN, [null,true], [false,false], [0, false], [NAN, true], ["NAN", false], [[], false], [null, false]], + [[], [null,true], [false,false], [0, false], [0.0, false], ["", false], [[], true], [null, false]], + ["some string", [null,true], [true, false], [0, false], [0.0, false], ["some string", true], [["some string"], false], [null, false]], + [" ", [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[" "], false], [null, false]], + [new \StdClass, [null,true], [true, false], [0, false], [0.0, false], ["", false], [[new \StdClass], false], [null, false]], + [new StrClass(""), [null,true], [false,true], [0, false], [0.0, false], ["", true], [[new StrClass("")], false], [null, false]], + [new StrClass("1"), [null,true], [true, true], [1, true], [1.0, true], ["1", true], [[new StrClass("1")], false], [null, false]], + [new StrClass("0"), [null,true], [false,true], [0, true], [0.0, true], ["0", true], [[new StrClass("0")], false], [null, false]], + [new StrClass("-1"), [null,true], [true, false], [-1, true], [-1.0, true], ["-1", true], [[new StrClass("-1")], false], [null, false]], + [new StrClass("Msg"), [null,true], [true, false], [0, false], [0.0, false], ["Msg", true], [[new StrClass("Msg")], false], [null, false]], + [new StrClass(" "), [null,true], [true, false], [0, false], [0.0, false], [" ", true], [[new StrClass(" ")], false], [null, false]], + [2.5, [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [[2.5], false], [$this->i("PT2S", 0.5), false]], + [0.5, [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [[0.5], false], [$this->i("PT0S", 0.5), false]], + ["2.5", [null,true], [true, false], [2, false], [2.5, true], ["2.5", true], [["2.5"], false], [null, false]], + ["0.5", [null,true], [true, false], [0, false], [0.5, true], ["0.5", true], [["0.5"], false], [null, false]], + [$this->d("2010-01-01T00:00:00", 0, 0), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 0, 0)],false], [null, false]], + [$this->d("2010-01-01T00:00:00", 0, 1), [null,true], [true, false], [1262304000, false], [1262304000.0, false], ["2010-01-01T00:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 0, 1)],false], [null, false]], + [$this->d("2010-01-01T00:00:00", 1, 0), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 1, 0)],false], [null, false]], + [$this->d("2010-01-01T00:00:00", 1, 1), [null,true], [true, false], [1262322000, false], [1262322000.0, false], ["2010-01-01T05:00:00Z",true], [[$this->d("2010-01-01T00:00:00", 1, 1)],false], [null, false]], + [1e14, [null,true], [true, false], [pow(10, 14), true], [1e14, true], ["100000000000000", true], [[1e14], false], [$this->i("P1157407407DT9H46M40S"), false]], + [1e-6, [null,true], [true, false], [0, false], [1e-6, true], ["0.000001", true], [[1e-6], false], [$this->i("PT0S", 1e-6), false]], + [[1,2,3], [null,true], [true, false], [0, false], [0.0, false], ["", false], [[1,2,3], true], [null, false]], + [['a'=>1,'b'=>2], [null,true], [true, false], [0, false], [0.0, false], ["", false], [['a'=>1,'b'=>2], true], [null, false]], + [new Result([['a'=>1,'b'=>2]]), [null,true], [true, false], [0, false], [0.0, false], ["", false], [[['a'=>1,'b'=>2]], true], [null, false]], + [$this->i("PT1H"), [null,true], [true, false], [60*60, false], [60.0*60.0, false], ["PT1H", true], [[$this->i("PT1H")], false], [$this->i("PT1H"), true]], + [$this->i("P2DT1H"), [null,true], [true, false], [(48+1)*60*60, false], [1.0*(48+1)*60*60, false], ["P2DT1H", true], [[$this->i("P2DT1H")], false], [$this->i("P2DT1H"), true]], + [$this->i("PT0H"), [null,true], [true, false], [0, false], [0.0, false], ["PT0S", true], [[$this->i("PT0H")], false], [$this->i("PT0H"), true]], + [$dateDiff, [null,true], [true, false], [366*24*60*60, false], [1.0*366*24*60*60, false], ["P366D", true], [[$dateDiff], false], [$dateNorm, true]], + ["1 year, 2 days", [null,true], [true, false], [0, false], [0.0, false], ["1 year, 2 days", true], [["1 year, 2 days"], false], [$this->i("P1Y2D"), false]], + ["P1Y2D", [null,true], [true, false], [0, false], [0.0, false], ["P1Y2D", true], [["P1Y2D"], false], [$this->i("P1Y2D"), true]], + ] as $set) { + // shift the input value off the set + $input = array_shift($set); + // shift a mixed-type passthrough test onto the set + array_unshift($set, [$input, true]); + // generate a set of tests for each target data type + foreach ($set as $type => list($exp, $pass)) { + // emit one test each for loose mode, strict mode, drop mode, and strict+drop mode + foreach ([ + [false, false], + [true, false], + [false, true], + [true, true], + ] as list($strict, $drop)) { + yield [$input, $types[$type], $exp, $pass, $strict, $drop]; } } } - // DateTimeInterface tests - $tests = [ + } + + public function provideDateNormalizationValues() { + $formats = [ + "microtime", + "iso8601", + "iso8601m", + "http", + "sql", + "date", + "time", + "unix", + "float", + "!M j, Y (D)", + null, + ]; + foreach([ /* Input value microtime iso8601 iso8601m http sql date time unix float '!M j, Y (D)' *strtotime* (null) */ [null, null, null, null, null, null, null, null, null, null, null, null, ], + [INF, null, null, null, null, null, null, null, null, null, null, null, ], + [NAN, null, null, null, null, null, null, null, null, null, null, null, ], [$this->d("2010-01-01T00:00:00", 0, 0), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), ], [$this->d("2010-01-01T00:00:00", 0, 1), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), $this->t(1262304000), ], [$this->d("2010-01-01T00:00:00", 1, 0), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), $this->t(1262322000), ], @@ -474,60 +601,25 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { ["1262304000.42", null, null, null, null, null, null, null, null, $this->t(1262304000.42), null, null, ], ["Jan 1, 2010 (Fri)", null, null, null, null, null, null, null, null, null, $this->t(1262304000), null, ], ["First day of Jan 2010 12AM", null, null, null, null, null, null, null, null, null, null, $this->t(1262304000), ], - ]; - $formats = [ - "microtime", - "iso8601", - "iso8601m", - "http", - "sql", - "date", - "time", - "unix", - "float", - "!M j, Y (D)", - null, - ]; - $exc = new ExceptionType("strictFailure", I::T_DATE); - foreach ($formats as $index => $format) { - foreach ($tests as $test) { - $value = $test[0]; - $exp = $test[$index+1]; - $this->assertEquals($exp, I::normalize($value, I::T_DATE, $format), "Test failed for format ".var_export($format, true)." using value ".var_export($value, true)); - $this->assertEquals($exp, I::normalize($value, I::T_DATE | I::M_DROP, $format), "Drop test failed for format ".var_export($format, true)." using value ".var_export($value, true)); - // test for exception in case of errors - $exp = $exp ?? $exc; - try { - $act = I::normalize($value, I::T_DATE | I::M_STRICT, $format); - } catch (ExceptionType $e) { - $act = $e; - } finally { - $this->assertEquals($exp, $act, "Error test failed for format ".var_export($format, true)." using value ".var_export($value, true)); + [[], null, null, null, null, null, null, null, null, null, null, null, ], + [$this->i("P1Y2D"), null, null, null, null, null, null, null, null, null, null, null, ], + ["P1Y2D", null, null, null, null, null, null, null, null, null, null, null, ], + ] as $set) { + // shift the input value off the set + $input = array_shift($set); + // generate a set of tests for each target date formats + foreach ($set as $format => $exp) { + // emit one test each for loose mode, strict mode, drop mode, and strict+drop mode + foreach ([ + [false, false], + [true, false], + [false, true], + [true, true], + ] as list($strict, $drop)) { + yield [$input, $formats[$format], $exp, $strict, $drop]; } } } - // Array-mode tests - $tests = [ - [I::T_INT | I::M_DROP, new Result([1, 2, 2.2, 3]), [1,2,null,3] ], - [I::T_INT, new Result([1, 2, 2.2, 3]), [1,2,2,3] ], - [I::T_STRING | I::M_STRICT, "Bare string", ["Bare string"]], - ]; - foreach ($tests as $index => $test) { - list($type, $value, $exp) = $test; - $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index"); - } - // Date-to-string format tests - $test = new \DateTimeImmutable("now", new \DateTimezone("UTC")); - $exp = $test->format(I::DATE_FORMATS['iso8601'][1]); - $this->assertSame($exp, I::normalize($test, I::T_STRING, null), "Failed test for null output date format"); - foreach (I::DATE_FORMATS as $name => $formats) { - $exp = $test->format($formats[1]); - $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $name), "Failed test for output date format '$name'"); - } - foreach (["U", "M j, Y (D)", "r", "c"] as $format) { - $exp = $test->format($format); - $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $format), "Failed test for output date format '$format'"); - } } protected function d($spec, $local, $immutable): \DateTimeInterface { @@ -542,4 +634,10 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { protected function t(float $spec): \DateTimeImmutable { return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); } + + protected function i(string $spec, float $msec = 0.0): \DateInterval { + $out = new \DateInterval($spec); + $out->f = $msec; + return $out; + } }