0,'tue'=>1,'wed'=>2,'thu'=>3,'fri'=>4,'sat'=>5,'sun'=>6]; // "... except Sunday" style → start full, remove named days if (strpos($s, 'except') !== false) { $d = [1,1,1,1,1,1,1]; foreach ($map as $a=>$i) if (strpos($s,$a)!==false) $d[$i]=0; return $d; } // Explicit daily if (preg_match('/\b(daily|every ?day|all ?7|7 ?days)\b/', $s)) return [1,1,1,1,1,1,1]; // Day-name list (order-independent; works for "Mon,Wed,Fri", full names, etc.) $d = [0,0,0,0,0,0,0]; $found = false; foreach ($map as $a=>$i) if (strpos($s,$a)!==false) { $d[$i]=1; $found=true; } if ($found) return $d; // "weekly"/"bi-weekly" with no day named → cannot determine pattern return null; } function rd_to_string(?array $d): ?string { return $d === null ? null : implode('', array_map(fn($x)=>$x?1:0, $d)); } /* Pure consensus vote. Input: [sourceName => "1111111"|null]. * Returns [pattern|null, confidence]. confidence: high|medium|low|none. */ function rd_vote(array $perSource): array { $counts = []; foreach ($perSource as $patt) { if ($patt !== null) $counts[$patt] = ($counts[$patt] ?? 0) + 1; } if (!$counts) return [null, 'none']; arsort($counts); $pattern = array_key_first($counts); $usable = array_sum($counts); $distinct = count($counts); if ($distinct === 1 && $usable >= 3) $confidence = 'high'; elseif ($distinct === 1 && $usable == 2) $confidence = 'medium'; elseif ($distinct === 1 && $usable == 1) $confidence = 'low'; // single source only else $confidence = 'low'; // conflict → plurality, flagged return [$pattern, $confidence]; } /* Human label: "Daily", or "Mon, Wed, Fri", or "—" */ function rd_label(?string $pattern): string { if ($pattern === null || !preg_match('/^[01]{7}$/', $pattern)) return '—'; if ($pattern === '1111111') return 'Daily'; $names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; $out = []; for ($i=0;$i<7;$i++) if ($pattern[$i]==='1') $out[] = $names[$i]; return $out ? implode(', ', $out) : '—'; } /* Does this train run on a given date? (Y-m-d) — uses cached pattern. */ function rd_runs_on(?string $pattern, string $ymd): ?bool { if ($pattern === null || !preg_match('/^[01]{7}$/', $pattern)) return null; // unknown $dow = (int)date('N', strtotime($ymd)); // 1=Mon..7=Sun return $pattern[$dow-1] === '1'; } /* ---------- Source adapters ---------- * Each returns [Mon..Sun] 0/1 array, or null on failure/unknown. * Keep each one small and isolated so one failing source never breaks others. */ // (1) irctc1 on RapidAPI — real adapter, only active if RAPIDAPI_KEY is set. function src_irctc1(string $trainNo): ?array { if (!defined('RAPIDAPI_KEY') || RAPIDAPI_KEY === '') return null; $url = 'https://irctc1.p.rapidapi.com/api/v1/getTrainSchedule?trainNo=' . urlencode($trainNo); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 12, CURLOPT_HTTPHEADER => [ 'x-rapidapi-host: irctc1.p.rapidapi.com', 'x-rapidapi-key: ' . RAPIDAPI_KEY, ], ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; $j = json_decode($body, true); // The field name varies by version; check the common ones, then fall back // to normalising whatever string we find. Verify with one real call first. $raw = $j['data']['train_base']['running_days'] ?? $j['data']['running_days'] ?? $j['train_base']['running_days'] ?? null; return rd_normalize(is_string($raw) ? $raw : null); } // (2) Template for an additional web/API source. Returns null until you wire it. // NOTE: if you point this at a public site, confirm its terms allow automated // access and expect to maintain the parser when their HTML changes. function src_secondary(string $trainNo): ?array { return null; // TODO: fetch + parse, then `return rd_normalize($daysTextFromPage);` } // (3) Another source slot — same contract. function src_tertiary(string $trainNo): ?array { return null; // TODO } function rd_adapters(): array { return [ 'irctc1' => 'src_irctc1', 'secondary' => 'src_secondary', 'tertiary' => 'src_tertiary', ]; } /* ---------- The resolver: consensus + confidence + cache ---------- */ function resolve_running_days(string $trainNo, int $maxAgeDays = 180): array { // 1) cache hit? $st = db()->prepare('SELECT * FROM train_running_days WHERE train_no = ? LIMIT 1'); $st->execute([$trainNo]); $row = $st->fetch(); if ($row && strtotime($row['checked_at']) > time() - $maxAgeDays*86400) { return [ 'pattern' => $row['pattern'], 'confidence' => $row['confidence'], 'label' => rd_label($row['pattern']), 'sources' => json_decode($row['sources_json'] ?? '[]', true) ?: [], 'cached' => true, ]; } // 2) query each source $perSource = []; // name => "1111111" | null foreach (rd_adapters() as $name => $fn) { $arr = null; try { $arr = $fn($trainNo); } catch (Throwable $e) { $arr = null; } $perSource[$name] = rd_to_string($arr); } // 3) vote [$pattern, $confidence] = rd_vote($perSource); // 4) cache + return $sources_json = json_encode($perSource); db()->prepare( 'INSERT INTO train_running_days (train_no, pattern, confidence, sources_json) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE pattern=VALUES(pattern), confidence=VALUES(confidence), sources_json=VALUES(sources_json), checked_at=NOW()' )->execute([$trainNo, $pattern, $confidence, $sources_json]); return [ 'pattern' => $pattern, 'confidence' => $confidence, 'label' => rd_label($pattern), 'sources' => $perSource, 'cached' => false, ]; }