Time-Ago Fix for JSON Dates
This is a lightweight two-part helper that upgrades a JSON file that currently stores “createdAt” as strings like "1 week ago" (which can’t update correctly on its own).
What it does:
- Step 1 (one-time PHP converter): reads
data.json, turns"2 days ago"into a real ISO timestamp like"2025-12-26T03:22:10Z", and saves it ascreatedAtISO. - Step 2 (tiny JS renderer): displays “time ago” from that ISO timestamp and keeps it updating as time passes.
Why this is needed: If your JSON only stores "1 week ago" with no anchor date, then a week later it still “looks” like 1 week ago. The fix is to store an actual date (timestamp) and calculate the “ago” text at display time.
Install (newbie-friendly):
- Put your
data.jsonin the same folder as the PHP converter (or edit the path in the config). - Create
convert_createdAt.phpand paste the PHP code below. - Run it once:
- Browser: visit
/convert_createdAt.php?token=CHANGE_ME - CLI (optional):
php convert_createdAt.php
- Browser: visit
- Delete the converter file (or keep it protected) after it runs.
- Add the JS helper to your site and render using
createdAtISO.
<?php
declare(strict_types=1);
/**
* convert_createdAt.php
* One-time converter:
* - Reads data.json
* - Converts "createdAt": "2 days ago" -> adds "createdAtISO": "....Z"
* - Uses an anchor time so conversions are consistent:
* 1) JSON top-level "asOf" (ISO) if present
* 2) else filemtime(data.json)
*
* SECURITY:
* - Set a token and delete this file after running, or keep it protected.
*/
// -------- CONFIG ----------
$JSON_PATH = __DIR__ . '/data.json';
$TOKEN = 'CHANGE_ME'; // set to something random, then run with ?token=...
$BACKUP = true; // create data.json.bak before writing
// -------------------------
function fail(string $msg, int $code = 400): void {
if (PHP_SAPI !== 'cli') http_response_code($code);
header('Content-Type: text/plain; charset=utf-8');
echo $msg;
exit;
}
function parse_iso_to_ts(string $iso): ?int {
$ts = strtotime($iso);
return $ts === false ? null : $ts;
}
function unit_seconds(string $unit): int {
$unit = strtolower($unit);
return match ($unit) {
'second', 'seconds' => 1,
'minute', 'minutes' => 60,
'hour', 'hours' => 3600,
'day', 'days' => 86400,
'week', 'weeks' => 604800,
// months/years are approximations for "ago" strings; for exactness store real timestamps at creation time
'month', 'months' => 2629800, // ~30.44 days
'year', 'years' => 31557600, // ~365.25 days
default => 0
};
}
function parse_relative_ago(string $s): ?array {
// Accepts: "1 week ago", "2 days ago", " 3 hours ago"
$s = trim($s);
if (!preg_match('/^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i', $s, $m)) return null;
$n = (int)$m[1];
$u = strtolower($m[2]) . ($n === 1 ? '' : 's');
return [$n, $u];
}
function normalize_items(&$data): array {
// Supports either:
// 1) top-level array: [ {..}, {..} ]
// 2) object with items: { "items":[...], ... }
if (is_array($data) && array_is_list($data)) return [$data, null]; // items, set back target = root
if (is_array($data) && isset($data['items']) && is_array($data['items'])) return [$data['items'], 'items'];
return [null, null];
}
// --- token check (web only) ---
if (PHP_SAPI !== 'cli') {
$got = $_GET['token'] ?? '';
if (!is_string($got) || $got !== $TOKEN) fail("Forbidden. Provide ?token=YOUR_TOKEN", 403);
}
if (!is_file($JSON_PATH)) fail("Missing file: {$JSON_PATH}", 404);
$raw = file_get_contents($JSON_PATH);
if ($raw === false) fail("Failed reading: {$JSON_PATH}", 500);
$data = json_decode($raw, true);
if (!is_array($data)) fail("data.json is not valid JSON (expected array or object).", 400);
// Determine anchor time ("asOf")
$asOfTs = null;
if (isset($data['asOf']) && is_string($data['asOf'])) {
$asOfTs = parse_iso_to_ts($data['asOf']);
}
if ($asOfTs === null) {
$asOfTs = (int)@filemtime($JSON_PATH);
if ($asOfTs <= 0) $asOfTs = time();
}
// Ensure JSON has asOf stored (so future runs are consistent)
$data['asOf'] = gmdate('c', $asOfTs);
[$items, $target] = normalize_items($data);
if ($items === null) fail("Could not find items. Use a top-level array, or an object with an 'items' array.", 400);
$changed = 0;
$skipped = 0;
foreach ($items as &$it) {
if (!is_array($it)) { $skipped++; continue; }
// If already converted, skip
if (isset($it['createdAtISO']) && is_string($it['createdAtISO']) && $it['createdAtISO'] !== '') {
$skipped++;
continue;
}
$createdAt = $it['createdAt'] ?? null;
if (!is_string($createdAt)) { $skipped++; continue; }
$rel = parse_relative_ago($createdAt);
if ($rel === null) { $skipped++; continue; }
[$n, $unit] = $rel;
$sec = unit_seconds($unit);
if ($sec <= 0) { $skipped++; continue; }
$ts = $asOfTs - ($n * $sec);
$it['createdAtISO'] = gmdate('c', $ts);
$it['createdAtEpoch'] = $ts; // optional, handy for sorting
$changed++;
}
unset($it);
// Put items back if needed
if ($target === 'items') $data['items'] = $items;
// Backup
if ($BACKUP) {
@copy($JSON_PATH, $JSON_PATH . '.bak');
}
// Write updated JSON
$out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($out === false) fail("Failed encoding JSON.", 500);
if (file_put_contents($JSON_PATH, $out) === false) fail("Failed writing: {$JSON_PATH}", 500);
header('Content-Type: text/plain; charset=utf-8');
echo "Done.\nChanged: {$changed}\nSkipped: {$skipped}\nAnchor (asOf): " . $data['asOf'] . "\n";
?>
// ===============================
// timeago.js (drop-in)
// ===============================
<script>
(function(){
function timeAgoFromISO(iso){
var ts = Date.parse(iso);
if (!isFinite(ts)) return '';
var s = Math.floor((Date.now() - ts) / 1000);
if (s < 5) return 'just now';
if (s < 60) return s + ' seconds ago';
var m = Math.floor(s/60);
if (m < 60) return m + (m===1?' minute':' minutes') + ' ago';
var h = Math.floor(m/60);
if (h < 24) return h + (h===1?' hour':' hours') + ' ago';
var d = Math.floor(h/24);
if (d < 7) return d + (d===1?' day':' days') + ' ago';
var w = Math.floor(d/7);
if (w < 5) return w + (w===1?' week':' weeks') + ' ago';
// Approx months/years (good enough for display)
var mo = Math.floor(d/30);
if (mo < 12) return mo + (mo===1?' month':' months') + ' ago';
var y = Math.floor(d/365);
return y + (y===1?' year':' years') + ' ago';
}
function updateTimeAgo(){
var nodes = document.querySelectorAll('[data-timeago]');
for (var i=0;i<nodes.length;i++){
var el = nodes[i];
var iso = el.getAttribute('data-timeago') || '';
var txt = timeAgoFromISO(iso);
if (txt) el.textContent = txt;
}
}
// Update now + every 30s
document.addEventListener('DOMContentLoaded', function(){
updateTimeAgo();
setInterval(updateTimeAgo, 30000);
});
// Export helper if you want to use it manually
window.timeAgoFromISO = timeAgoFromISO;
})();
</script>
// ===============================
// Example usage (rendering)
// ===============================
// After converting, your data items have createdAtISO.
// In your HTML, output something like:
//
// <time data-timeago="2025-12-26T03:22:10Z"></time>
//
// Or if you fetch JSON and build the DOM, set that attribute from item.createdAtISO.
Comments (0)
No comments yet — be the first.