mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Standardize date normalization to immutables
Also move date formats to the ValueInfo class Standardizing on immutables avoids any possible ambiguity in the API of the resultant value, as well as any ambiguity as to whether a DateTime output instance is the same instance or a clone (they had been clones)
This commit is contained in:
parent
87faededc6
commit
89bfc23d32
4 changed files with 53 additions and 32 deletions
|
@ -827,7 +827,7 @@ class Database {
|
|||
$limit = Date::normalize("now");
|
||||
if (Arsse::$conf->purgeFeeds) {
|
||||
// if there is a retention period specified, compute it; otherwise feed are deleted immediatelty
|
||||
$limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds));
|
||||
$limit = Date::sub(Arsse::$conf->purgeFeeds, $limit);
|
||||
}
|
||||
$out = (bool) $this->db->prepare("DELETE from arsse_feeds where orphaned <= ?", "datetime")->run($limit);
|
||||
// commit changes and return
|
||||
|
|
15
lib/Feed.php
15
lib/Feed.php
|
@ -328,12 +328,12 @@ class Feed {
|
|||
return [$new, $edited];
|
||||
}
|
||||
|
||||
protected function computeNextFetch(): \DateTime {
|
||||
protected function computeNextFetch(): \DateTimeImmutable {
|
||||
$now = Date::normalize(time());
|
||||
if (!$this->modified) {
|
||||
$diff = $now->getTimestamp() - $this->lastModified->getTimestamp();
|
||||
$offset = $this->normalizeDateDiff($diff);
|
||||
$now->modify("+".$offset);
|
||||
return $now->modify("+".$offset);
|
||||
} else {
|
||||
// the algorithm for updated feeds (returning 200 rather than 304) uses the same parameters as for 304,
|
||||
// save that the last three intervals between item dates are computed, and if any two fall within
|
||||
|
@ -347,20 +347,19 @@ class Feed {
|
|||
$offsets[] = $this->normalizeDateDiff($diff);
|
||||
}
|
||||
if ($offsets[0]==$offsets[1] || $offsets[0]==$offsets[2]) {
|
||||
$now->modify("+".$offsets[0]);
|
||||
return $now->modify("+".$offsets[0]);
|
||||
} elseif ($offsets[1]==$offsets[2]) {
|
||||
$now->modify("+".$offsets[1]);
|
||||
return $now->modify("+".$offsets[1]);
|
||||
} else {
|
||||
$now->modify("+ 1 hour");
|
||||
return $now->modify("+ 1 hour");
|
||||
}
|
||||
} else {
|
||||
$now->modify("+ 1 hour");
|
||||
return $now->modify("+ 1 hour");
|
||||
}
|
||||
}
|
||||
return $now;
|
||||
}
|
||||
|
||||
public static function nextFetchOnError($errCount): \DateTime {
|
||||
public static function nextFetchOnError($errCount): \DateTimeImmutable {
|
||||
if ($errCount < 3) {
|
||||
$offset = "5 minutes";
|
||||
} elseif ($errCount < 15) {
|
||||
|
|
|
@ -19,7 +19,7 @@ class ValueInfo {
|
|||
// strings
|
||||
const EMPTY = 1 << 2;
|
||||
const WHITE = 1 << 3;
|
||||
//normalization types
|
||||
// normalization types
|
||||
const T_MIXED = 0; // pass through unchanged
|
||||
const T_NULL = 1; // convert to null
|
||||
const T_BOOL = 2; // convert to boolean
|
||||
|
@ -28,11 +28,23 @@ class ValueInfo {
|
|||
const T_DATE = 5; // convert to DateTimeInterface instance
|
||||
const T_STRING = 6; // convert to string
|
||||
const T_ARRAY = 7; // convert to array
|
||||
//normalization modes
|
||||
// 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
|
||||
const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
|
||||
const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
|
||||
// symbolic date and time formats
|
||||
const DATE_FORMATS = [ // in out
|
||||
'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
|
||||
'iso8601m' => ["!Y-m-d\TH:i:s.u", "Y-m-d\TH:i:s.u\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
|
||||
'microtime' => ["U.u", "0.u00 U" ], // NOTE: the actual input format at the user level matches the output format; pre-processing is required for PHP not to fail
|
||||
'http' => ["!D, d M Y H:i:s \G\M\T", "D, d M Y H:i:s \G\M\T"],
|
||||
'sql' => ["!Y-m-d H:i:s", "Y-m-d H:i:s" ],
|
||||
'date' => ["!Y-m-d", "Y-m-d" ],
|
||||
'time' => ["!H:i:s", "H:i:s" ],
|
||||
'unix' => ["U", "U" ],
|
||||
'float' => ["U.u", "U.u" ],
|
||||
];
|
||||
|
||||
public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) {
|
||||
$allowNull = ($type & self::M_NULL);
|
||||
|
@ -131,14 +143,14 @@ class ValueInfo {
|
|||
if (is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
$dateOutFormat = $dateOutFormat ?? "iso8601";
|
||||
$dateOutFormat = isset(Date::FORMAT[$dateOutFormat]) ? Date::FORMAT[$dateOutFormat][1] : $dateOutFormat;
|
||||
if ($value instanceof \DateTimeImmutable) {
|
||||
return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat);
|
||||
} elseif ($value instanceof \DateTime) {
|
||||
$out = clone $value;
|
||||
$out->setTimezone(new \DateTimeZone("UTC"));
|
||||
return $out->format($dateOutFormat);
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
$dateOutFormat = $dateOutFormat ?? "iso8601";
|
||||
$dateOutFormat = isset(self::DATE_FORMATS[$dateOutFormat]) ? self::DATE_FORMATS[$dateOutFormat][1] : $dateOutFormat;
|
||||
if ($value instanceof \DateTimeImmutable) {
|
||||
return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat);
|
||||
} elseif ($value instanceof \DateTime) {
|
||||
return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat);
|
||||
}
|
||||
} elseif (is_float($value) && is_finite($value)) {
|
||||
$out = (string) $value;
|
||||
if (!strpos($out, "E")) {
|
||||
|
@ -168,13 +180,11 @@ class ValueInfo {
|
|||
if ($value instanceof \DateTimeImmutable) {
|
||||
return $value->setTimezone(new \DateTimeZone("UTC"));
|
||||
} elseif ($value instanceof \DateTime) {
|
||||
$out = clone $value;
|
||||
$out->setTimezone(new \DateTimeZone("UTC"));
|
||||
return $out;
|
||||
return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC"));
|
||||
} elseif (is_int($value)) {
|
||||
return \DateTime::createFromFormat("U", (string) $value, new \DateTimeZone("UTC"));
|
||||
return \DateTimeImmutable::createFromFormat("U", (string) $value, new \DateTimeZone("UTC"));
|
||||
} elseif (is_float($value)) {
|
||||
return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC"));
|
||||
return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC"));
|
||||
} elseif (is_string($value)) {
|
||||
try {
|
||||
if (!is_null($dateInFormat)) {
|
||||
|
@ -187,28 +197,28 @@ class ValueInfo {
|
|||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
$f = isset(Date::FORMAT[$dateInFormat]) ? Date::FORMAT[$dateInFormat][0] : $dateInFormat;
|
||||
$f = isset(self::DATE_FORMATS[$dateInFormat]) ? self::DATE_FORMATS[$dateInFormat][0] : $dateInFormat;
|
||||
if ($dateInFormat=="iso8601" || $dateInFormat=="iso8601m") {
|
||||
// DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works
|
||||
// DateTimeImmutable::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works
|
||||
if ($dateInFormat=="iso8601m") {
|
||||
$f2 = Date::FORMAT["iso8601"][0];
|
||||
$f2 = self::DATE_FORMATS["iso8601"][0];
|
||||
$zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"];
|
||||
} else {
|
||||
$zones = [$f."", $f."\Z", $f."P", $f."O"];
|
||||
}
|
||||
do {
|
||||
$ftz = array_shift($zones);
|
||||
$out = \DateTime::createFromFormat($ftz, $value, new \DateTimeZone("UTC"));
|
||||
$out = \DateTimeImmutable::createFromFormat($ftz, $value, new \DateTimeZone("UTC"));
|
||||
} while (!$out && $zones);
|
||||
} else {
|
||||
$out = \DateTime::createFromFormat($f, $value, new \DateTimeZone("UTC"));
|
||||
$out = \DateTimeImmutable::createFromFormat($f, $value, new \DateTimeZone("UTC"));
|
||||
}
|
||||
if (!$out) {
|
||||
throw new \Exception;
|
||||
}
|
||||
return $out;
|
||||
} else {
|
||||
return new \DateTime($value, new \DateTimeZone("UTC"));
|
||||
return new \DateTimeImmutable($value, new \DateTimeZone("UTC"));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ($strict && !$drop) {
|
||||
|
|
|
@ -506,6 +506,18 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
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 {
|
||||
|
@ -517,7 +529,7 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
}
|
||||
|
||||
protected function t(float $spec): \DateTime {
|
||||
return \DateTime::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC"));
|
||||
protected function t(float $spec): \DateTimeImmutable {
|
||||
return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue