diff --git a/AGENTS.md b/AGENTS.md index ed60e42..8dd4244 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,10 @@ Guidance for AI coding agents working in this repository. ## Project Shape -- `fetch_contests.py` is the data generator. It fetches contests, normalizes records with `platform`, `title`, `start_time`, `end_time`, `status`, and `url`, then writes JSON files in the repository root. +- `fetch_contests.py` is the data generator. It fetches contests, normalizes records with `platform`, `title`, `start_time`, `end_time`, `status`, and `url`, then writes `contests_all.json` in the repository root. - Current contest providers are Codeforces, AtCoder, UOJ, and Nowcoder. - `.github/workflows/deploy.yml` is the GitHub Actions automation. It installs Python dependencies, runs `fetch_contests.py`, and commits generated JSON changes. -- `server/index.php` is the serving/display layer. It reads the public raw `contests.json` and `contests_all.json`, caches them locally, and renders upcoming plus recently finished contests. +- `server/index.php` is the serving/display layer. It reads the public raw `contests_all.json`, caches it locally, and renders upcoming plus recently finished contests. - User-facing documentation lives in [README.md](README.md) and [README_zh.md](README_zh.md). Keep both languages in sync. ## Actions vs Server Responsibilities @@ -23,7 +23,7 @@ Guidance for AI coding agents working in this repository. When adding a new contest provider, check all relevant surfaces: - `fetch_contests.py`: add a `fetch_()` function, normalize records through `make_contest()`, add it to both output flows in `main()`, and preserve `status` semantics. -- `server/index.php`: add the platform name to `$platform_class` and define a matching `.platform.` CSS rule if the provider appears in `contests.json`. +- `server/index.php`: add the platform name to `$platform_class` and define a matching `.platform.` CSS rule if the provider appears in `contests_all.json`. - `README.md` and `README_zh.md`: update supported platforms, data-source notes, and any changed usage/output details. - `AGENTS.md`: update this guidance if the provider introduces a new pattern, dependency, output contract, or operational caveat. - `.github/workflows/deploy.yml`: update only if the provider needs new Python dependencies, generated files, secrets, or schedule behavior. @@ -32,8 +32,7 @@ When removing a provider, remove or adjust the same surfaces so stale platform l ## Output Contracts -- `contests.json` is an object with `generated_at` and `contests`; `contests` contains contests that have not ended yet: `upcoming` and `running`. -- `contests_all.json` is an object with `generated_at` and `contests`; `contests` contains contests that finished within the last 30 days. +- `contests_all.json` is an object with `generated_at` and `contests`; `contests` contains contests that have not ended yet (`upcoming` and `running`) plus contests that finished within the last 30 days. - JSON timestamps are Unix timestamps in seconds. - Keep record keys stable unless the README files and server rendering are updated in the same change. @@ -50,11 +49,10 @@ python fetch_contests.py Then inspect the generated files with `jq` when available, for example: ```bash -jq '.contests | group_by(.platform) | map({platform: .[0].platform, count: length})' contests.json jq '.contests | group_by(.status) | map({status: .[0].status, count: length})' contests_all.json ``` -For PHP-only presentation changes, also check that `server/index.php` still reads `contests.json` and handles unknown platforms with the default styling. +For PHP-only presentation changes, also check that `server/index.php` still reads `contests_all.json` and handles unknown platforms with the default styling. ## Documentation Rule diff --git a/README.md b/README.md index 6b96d47..52497bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ en/[zh](README_zh.md) -Fetch programming contest schedules from several online judge platforms and export them as JSON files. The repository also includes a small PHP page that can read the generated JSON and display upcoming contests. +Fetch programming contest schedules from several online judge platforms and export them as JSON. The repository also includes a small PHP page that can read the generated JSON and display upcoming contests. ## Supported Platforms @@ -13,12 +13,11 @@ Fetch programming contest schedules from several online judge platforms and expo ## Output Files -Running the fetch script writes JSON files to the repository root: +Running the fetch script writes one JSON file to the repository root: -- `contests.json`: contests that have not ended yet, including upcoming and currently running contests. -- `contests_all.json`: contests that finished within the last 30 days. +- `contests_all.json`: contests that have not ended yet, including upcoming and currently running contests, plus contests that finished within the last 30 days. -Each JSON file has this top-level shape. `generated_at` is the Unix timestamp in seconds when the file was written. +The JSON file has this top-level shape. `generated_at` is the Unix timestamp in seconds when the file was written. ```json { @@ -64,30 +63,30 @@ python fetch_contests.py After a successful run, the script prints counts similar to: ```text -Total upcoming contests: 13 +Total active contests: 13 Total contests finished in last 30 days: 139 ``` ## Web Page -`server/index.php` reads the public raw GitHub URLs for `contests.json` and `contests_all.json`, caches them locally for 5 minutes, and renders upcoming contests with a client-side countdown plus recently finished contests. It also shows the JSON `generated_at` value as the last update time in the detected time zone. The page detects the browser/system language and time zone, syncs them to cookies, and uses them for localized text and contest times on later page loads. Unsupported languages fall back to English. +`server/index.php` reads the public raw GitHub URL for `contests_all.json`, caches it locally for 5 minutes, and renders upcoming contests with a client-side countdown plus recently finished contests. It also shows the JSON `generated_at` value as the last update time in the detected time zone. The page detects the browser/system language and time zone, syncs them to cookies, and uses them for localized text and contest times on later page loads. Unsupported languages fall back to English. If you deploy it yourself, update this constant in `server/index.php` if your repository path or branch changes: ```php -define('JSON_URL', 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests.json'); +define('JSON_URL', 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests_all.json'); ``` -The cache files are written next to `index.php` as `contests_cache.json` and `contests_all_cache.json`, so the web server process needs write permission for the `server/` directory. +The cache file is written next to `index.php` as `contests_all_cache.json`, so the web server process needs write permission for the `server/` directory. ## Automation A typical automation flow is: 1. Run `python fetch_contests.py` on a schedule. -2. Commit the updated JSON files. +2. Commit the updated JSON file. 3. Push them to GitHub. -4. Let `server/index.php` read the latest `contests.json` from GitHub raw content. +4. Let `server/index.php` read the latest `contests_all.json` from GitHub raw content. For GitHub Actions, install the Python dependencies before running the script. diff --git a/README_zh.md b/README_zh.md index a1ceb76..9b8ee49 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,7 +2,7 @@ [en](README.md)/zh -从多个在线评测平台抓取编程竞赛日程,并导出为 JSON 文件。仓库中还包含一个简单的 PHP 页面,可以读取生成的 JSON 并展示即将到来的比赛。 +从多个在线评测平台抓取编程竞赛日程,并导出为 JSON。仓库中还包含一个简单的 PHP 页面,可以读取生成的 JSON 并展示即将到来的比赛。 ## 支持平台 @@ -13,12 +13,11 @@ ## 输出文件 -运行抓取脚本后,会在仓库根目录写入以下 JSON 文件: +运行抓取脚本后,会在仓库根目录写入一个 JSON 文件: -- `contests.json`:尚未结束的比赛,包括尚未开始和正在进行中的比赛。 -- `contests_all.json`:最近 30 天内已经结束的比赛。 +- `contests_all.json`:尚未结束的比赛,包括尚未开始和正在进行中的比赛,以及最近 30 天内已经结束的比赛。 -每个 JSON 文件的顶层结构如下。`generated_at` 是文件写入时的秒级 Unix 时间戳。 +JSON 文件的顶层结构如下。`generated_at` 是文件写入时的秒级 Unix 时间戳。 ```json { @@ -64,21 +63,21 @@ python fetch_contests.py 成功运行后,脚本会输出类似下面的统计信息: ```text -Total upcoming contests: 13 +Total active contests: 13 Total contests finished in last 30 days: 139 ``` ## 网页展示 -`server/index.php` 会读取公开的 GitHub Raw 地址中的 `contests.json` 和 `contests_all.json`,在本地缓存 5 分钟,并渲染带客户端倒计时的即将开始比赛,以及最近已结束的比赛。页面还会读取 JSON 中的 `generated_at`,按检测到的时区显示最后更新时间。页面会识别浏览器/系统语言和时区,将其同步到 cookie,并在后续加载时用于本地化文案和比赛时间显示。不支持的语言会回退到英语。 +`server/index.php` 会读取公开 GitHub Raw 地址中的 `contests_all.json`,在本地缓存 5 分钟,并渲染带客户端倒计时的即将开始比赛,以及最近已结束的比赛。页面还会读取 JSON 中的 `generated_at`,按检测到的时区显示最后更新时间。页面会识别浏览器/系统语言和时区,将其同步到 cookie,并在后续加载时用于本地化文案和比赛时间显示。不支持的语言会回退到英语。 如果你自行部署,并且仓库路径或分支发生变化,需要修改 `server/index.php` 中的这个常量: ```php -define('JSON_URL', 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests.json'); +define('JSON_URL', 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests_all.json'); ``` -缓存文件会以 `contests_cache.json` 和 `contests_all_cache.json` 的名字写在 `index.php` 同目录下,因此 Web 服务器进程需要拥有 `server/` 目录的写入权限。 +缓存文件会以 `contests_all_cache.json` 的名字写在 `index.php` 同目录下,因此 Web 服务器进程需要拥有 `server/` 目录的写入权限。 ## 自动化 @@ -87,7 +86,7 @@ define('JSON_URL', 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fe 1. 定时运行 `python fetch_contests.py`。 2. 提交更新后的 JSON 文件。 3. 推送到 GitHub。 -4. 让 `server/index.php` 从 GitHub Raw 读取最新的 `contests.json`。 +4. 让 `server/index.php` 从 GitHub Raw 读取最新的 `contests_all.json`。 如果使用 GitHub Actions,请在运行脚本前安装 Python 依赖。 diff --git a/contests.json b/contests.json deleted file mode 100644 index 07d2840..0000000 --- a/contests.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "generated_at": 1784551373, - "contests": [ - { - "platform": "Nowcoder", - "title": "2026牛客暑期多校训练营2", - "start_time": 1784692800, - "end_time": 1784710800, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/133877" - }, - { - "platform": "Nowcoder", - "title": "2026牛客暑期多校训练营3", - "start_time": 1784865600, - "end_time": 1784883600, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/133878" - }, - { - "platform": "AtCoder", - "title": "AtCoder Beginner Contest 468", - "start_time": 1784980800, - "end_time": 1784986800, - "status": "upcoming", - "url": "https://atcoder.jp/contests/abc468" - }, - { - "platform": "Nowcoder", - "title": "牛客周赛 Round 154", - "start_time": 1785063600, - "end_time": 1785070800, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/137840" - }, - { - "platform": "Codeforces", - "title": "Codeforces Round (Div. 1)", - "start_time": 1785076500, - "end_time": 1785085500, - "status": "upcoming", - "url": "https://codeforces.com/contests/2249" - }, - { - "platform": "Codeforces", - "title": "Codeforces Round (Div. 2)", - "start_time": 1785076500, - "end_time": 1785085500, - "status": "upcoming", - "url": "https://codeforces.com/contests/2250" - }, - { - "platform": "Nowcoder", - "title": "2026牛客暑期多校训练营4", - "start_time": 1785297600, - "end_time": 1785315600, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/133879" - }, - { - "platform": "Nowcoder", - "title": "2026牛客暑期多校训练营5", - "start_time": 1785470400, - "end_time": 1785488400, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/133880" - }, - { - "platform": "AtCoder", - "title": "RECRUIT Nihonbashi Half Marathon 2026 Summer (AtCoder Heuristic Contest 069)", - "start_time": 1785492000, - "end_time": 1786356000, - "status": "upcoming", - "url": "https://atcoder.jp/contests/ahc069" - }, - { - "platform": "Codeforces", - "title": "Codeforces Round (Div. 2)", - "start_time": 1785584100, - "end_time": 1785591300, - "status": "upcoming", - "url": "https://codeforces.com/contests/2248" - }, - { - "platform": "AtCoder", - "title": "AtCoder Beginner Contest 469", - "start_time": 1785585600, - "end_time": 1785591600, - "status": "upcoming", - "url": "https://atcoder.jp/contests/abc469" - }, - { - "platform": "Nowcoder", - "title": "牛客周赛 Round 155", - "start_time": 1785668400, - "end_time": 1785675600, - "status": "upcoming", - "url": "https://ac.nowcoder.com/acm/contest/138240" - }, - { - "platform": "AtCoder", - "title": "UNIQUE VISION Programming Contest 2026 Summer (AtCoder Regular Contest 226)", - "start_time": 1786276800, - "end_time": 1786284000, - "status": "upcoming", - "url": "https://atcoder.jp/contests/arc226" - } - ] -} \ No newline at end of file diff --git a/contests_all.json b/contests_all.json index f79f6bb..0d174dd 100644 --- a/contests_all.json +++ b/contests_all.json @@ -1,5 +1,5 @@ { - "generated_at": 1784551373, + "generated_at": 1784551685, "contests": [ { "platform": "AtCoder", @@ -9,14 +9,6 @@ "status": "finished", "url": "https://atcoder.jp/contests/abc463" }, - { - "platform": "Nowcoder", - "title": "牛客周赛 Round 149", - "start_time": 1782039600, - "end_time": 1782046800, - "status": "finished", - "url": "https://ac.nowcoder.com/acm/contest/136720" - }, { "platform": "AtCoder", "title": "AtCoder Heuristic Contest 067", @@ -25,6 +17,14 @@ "status": "finished", "url": "https://atcoder.jp/contests/ahc067" }, + { + "platform": "Nowcoder", + "title": "牛客周赛 Round 149", + "start_time": 1782039600, + "end_time": 1782046800, + "status": "finished", + "url": "https://ac.nowcoder.com/acm/contest/136720" + }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/06/22 16:00start", @@ -505,6 +505,14 @@ "status": "finished", "url": "https://atcoder.jp/contests/abc465" }, + { + "platform": "UOJ", + "title": "UOJ Long Round #4", + "start_time": 1783216800, + "end_time": 1783432800, + "status": "finished", + "url": "https://uoj.ac/contest/108" + }, { "platform": "Nowcoder", "title": "牛客周赛 Round 151", @@ -561,6 +569,14 @@ "status": "finished", "url": "https://codeforces.com/contests/2242" }, + { + "platform": "AtCoder", + "title": "World Tour Finals 2026 Heuristic", + "start_time": 1783382400, + "end_time": 1783504800, + "status": "finished", + "url": "https://atcoder.jp/contests/awtf2026heuristic" + }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/07/07 16:00start", @@ -601,14 +617,6 @@ "status": "finished", "url": "https://atcoder.jp/contests/awc0107" }, - { - "platform": "UOJ", - "title": "UOJ Long Round #4", - "start_time": 1783216800, - "end_time": 1783432800, - "status": "finished", - "url": "https://uoj.ac/contest/108" - }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/07/08 16:00start", @@ -641,14 +649,6 @@ "status": "finished", "url": "https://atcoder.jp/contests/adt_easy_20260708_2" }, - { - "platform": "AtCoder", - "title": "World Tour Finals 2026 Heuristic", - "start_time": 1783382400, - "end_time": 1783504800, - "status": "finished", - "url": "https://atcoder.jp/contests/awtf2026heuristic" - }, { "platform": "AtCoder", "title": "AtCoder Weekday Contest 0108 Beta", @@ -657,22 +657,6 @@ "status": "finished", "url": "https://atcoder.jp/contests/awc0108" }, - { - "platform": "AtCoder", - "title": "AtCoder Daily Training ALL 2026/07/09 16:00start", - "start_time": 1783580400, - "end_time": 1783584000, - "status": "finished", - "url": "https://atcoder.jp/contests/adt_all_20260709_1" - }, - { - "platform": "AtCoder", - "title": "AtCoder Daily Training EASY 2026/07/09 16:00start", - "start_time": 1783580400, - "end_time": 1783584000, - "status": "finished", - "url": "https://atcoder.jp/contests/adt_easy_20260709_1" - }, { "platform": "AtCoder", "title": "World Tour Finals 2026 Algorithm", @@ -689,6 +673,22 @@ "status": "finished", "url": "https://atcoder.jp/contests/awtf2026algo-open" }, + { + "platform": "AtCoder", + "title": "AtCoder Daily Training ALL 2026/07/09 16:00start", + "start_time": 1783580400, + "end_time": 1783584000, + "status": "finished", + "url": "https://atcoder.jp/contests/adt_all_20260709_1" + }, + { + "platform": "AtCoder", + "title": "AtCoder Daily Training EASY 2026/07/09 16:00start", + "start_time": 1783580400, + "end_time": 1783584000, + "status": "finished", + "url": "https://atcoder.jp/contests/adt_easy_20260709_1" + }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/07/09 18:00start", @@ -993,6 +993,14 @@ "status": "finished", "url": "https://codeforces.com/contests/2245" }, + { + "platform": "Nowcoder", + "title": "2026牛客暑期多校训练营1", + "start_time": 1784260800, + "end_time": 1784278800, + "status": "finished", + "url": "https://ac.nowcoder.com/acm/contest/133876" + }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/07/17 16:00start", @@ -1009,14 +1017,6 @@ "status": "finished", "url": "https://atcoder.jp/contests/adt_easy_20260717_1" }, - { - "platform": "Nowcoder", - "title": "2026牛客暑期多校训练营1", - "start_time": 1784260800, - "end_time": 1784278800, - "status": "finished", - "url": "https://ac.nowcoder.com/acm/contest/133876" - }, { "platform": "AtCoder", "title": "AtCoder Daily Training ALL 2026/07/17 18:00start", @@ -1120,6 +1120,110 @@ "end_time": 1784548800, "status": "finished", "url": "https://atcoder.jp/contests/awc0116" + }, + { + "platform": "Nowcoder", + "title": "2026牛客暑期多校训练营2", + "start_time": 1784692800, + "end_time": 1784710800, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/133877" + }, + { + "platform": "Nowcoder", + "title": "2026牛客暑期多校训练营3", + "start_time": 1784865600, + "end_time": 1784883600, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/133878" + }, + { + "platform": "AtCoder", + "title": "AtCoder Beginner Contest 468", + "start_time": 1784980800, + "end_time": 1784986800, + "status": "upcoming", + "url": "https://atcoder.jp/contests/abc468" + }, + { + "platform": "Nowcoder", + "title": "牛客周赛 Round 154", + "start_time": 1785063600, + "end_time": 1785070800, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/137840" + }, + { + "platform": "Codeforces", + "title": "Codeforces Round (Div. 1)", + "start_time": 1785076500, + "end_time": 1785085500, + "status": "upcoming", + "url": "https://codeforces.com/contests/2249" + }, + { + "platform": "Codeforces", + "title": "Codeforces Round (Div. 2)", + "start_time": 1785076500, + "end_time": 1785085500, + "status": "upcoming", + "url": "https://codeforces.com/contests/2250" + }, + { + "platform": "Nowcoder", + "title": "2026牛客暑期多校训练营4", + "start_time": 1785297600, + "end_time": 1785315600, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/133879" + }, + { + "platform": "Nowcoder", + "title": "2026牛客暑期多校训练营5", + "start_time": 1785470400, + "end_time": 1785488400, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/133880" + }, + { + "platform": "AtCoder", + "title": "RECRUIT Nihonbashi Half Marathon 2026 Summer (AtCoder Heuristic Contest 069)", + "start_time": 1785492000, + "end_time": 1786356000, + "status": "upcoming", + "url": "https://atcoder.jp/contests/ahc069" + }, + { + "platform": "Codeforces", + "title": "Codeforces Round (Div. 2)", + "start_time": 1785584100, + "end_time": 1785591300, + "status": "upcoming", + "url": "https://codeforces.com/contests/2248" + }, + { + "platform": "AtCoder", + "title": "AtCoder Beginner Contest 469", + "start_time": 1785585600, + "end_time": 1785591600, + "status": "upcoming", + "url": "https://atcoder.jp/contests/abc469" + }, + { + "platform": "Nowcoder", + "title": "牛客周赛 Round 155", + "start_time": 1785668400, + "end_time": 1785675600, + "status": "upcoming", + "url": "https://ac.nowcoder.com/acm/contest/138240" + }, + { + "platform": "AtCoder", + "title": "UNIQUE VISION Programming Contest 2026 Summer (AtCoder Regular Contest 226)", + "start_time": 1786276800, + "end_time": 1786284000, + "status": "upcoming", + "url": "https://atcoder.jp/contests/arc226" } ] } \ No newline at end of file diff --git a/fetch_contests.py b/fetch_contests.py index ed1563e..8b348a1 100644 --- a/fetch_contests.py +++ b/fetch_contests.py @@ -95,13 +95,16 @@ def fetch_codeforces(include_all=False): if data["status"] != "OK": return [] contests = [] + now = now_ts() for c in data["result"]: - if include_all or c["phase"] == "BEFORE": + start = c["startTimeSeconds"] + end = start + c["durationSeconds"] + if include_all or end > now: contests.append(make_contest( "Codeforces", c["name"], - c["startTimeSeconds"], - c["startTimeSeconds"] + c["durationSeconds"], + start, + end, f"https://codeforces.com/contests/{c['id']}" )) return contests @@ -121,7 +124,7 @@ def fetch_atcoder(include_all=False): for c in data: start = c["start_epoch_second"] end = start + c["duration_second"] - if start > 0 and (include_all or start > now): + if start > 0 and (include_all or end > now): contests.append(make_contest( "AtCoder", c["title"], @@ -237,12 +240,6 @@ def main(): all_contests.extend(fetch_uoj()) all_contests.extend(fetch_nowcoder()) - all_contests.sort(key=lambda x: x["start_time"]) - - # 直接写入仓库根目录 - with open("contests.json", "w", encoding="utf-8") as f: - json.dump(contest_payload(all_contests, generated_at), f, ensure_ascii=False, indent=2) - recent_finished_contests = [] recent_finished_min_end_time = recent_finished_cutoff() recent_finished_contests.extend(fetch_codeforces(include_all=True)) @@ -251,12 +248,13 @@ def main(): recent_finished_contests.extend(fetch_nowcoder(include_all=True, min_end_time=recent_finished_min_end_time)) recent_finished_contests = filter_recent_finished(deduplicate_contests(recent_finished_contests)) - recent_finished_contests.sort(key=lambda x: x["end_time"]) + all_contests = deduplicate_contests(all_contests + recent_finished_contests) + all_contests.sort(key=lambda x: (x["start_time"], x["end_time"])) with open("contests_all.json", "w", encoding="utf-8") as f: - json.dump(contest_payload(recent_finished_contests, generated_at), f, ensure_ascii=False, indent=2) + json.dump(contest_payload(all_contests, generated_at), f, ensure_ascii=False, indent=2) - print(f"Total upcoming contests: {len(all_contests)}") + print(f"Total active contests: {sum(1 for contest in all_contests if contest['status'] != 'finished')}") print(f"Total contests finished in last 30 days: {len(recent_finished_contests)}") if __name__ == "__main__": main() diff --git a/server/index.php b/server/index.php index cc77a55..7ed7ac1 100644 --- a/server/index.php +++ b/server/index.php @@ -1,11 +1,9 @@ 'cf',