mirror of
https://github.com/harivansh-afk/cp.nvim.git
synced 2026-04-15 12:03:50 +00:00
Compare commits
258 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ba39a59 | ||
|
|
760e7d7731 | ||
|
|
49e4233b3f | ||
|
|
622620f6d0 | ||
|
|
976838d981 | ||
|
|
06f72bbe2b | ||
|
|
6045042dfb | ||
|
|
c192afc5d7 | ||
|
|
b6f3398bbc | ||
|
|
e02a29bd40 | ||
|
|
0f9715298e | ||
|
|
2148d9bd07 | ||
|
|
1162e7046b | ||
|
|
b36ffba63a | ||
|
|
04d0c124cf | ||
|
|
da433068ef | ||
|
|
51504b0121 | ||
|
|
49df7e015d | ||
|
|
029ea125b9 | ||
|
|
43193c3762 | ||
|
|
de2bc07532 | ||
|
|
041e09ac04 | ||
|
|
d23b4e59d1 | ||
|
|
19e71ac7fa | ||
|
|
a54a06f939 | ||
|
|
b2c7f16890 | ||
|
|
276241447c | ||
|
|
dc635d5167 | ||
|
|
81ddd1ea87 | ||
|
|
7444a99b22 | ||
|
|
ec487aa489 | ||
|
|
c4af9bf604 | ||
|
|
a4437bc1c6 | ||
|
|
1a7e9517ba | ||
|
|
11b8365aac | ||
|
|
585ebf0daf | ||
|
|
08fb654d23 | ||
|
|
01efc7c344 | ||
|
|
f9f993db0c | ||
|
|
f184a7874a | ||
|
|
89e3c0e21d | ||
|
|
a9ce31a291 | ||
|
|
c8f735617a | ||
|
|
a14f543371 | ||
|
|
56ec178cdd | ||
|
|
5cd6f75419 | ||
|
|
99d907aa7a | ||
|
|
c06d819597 | ||
|
|
682b267019 | ||
|
|
8a2871ec1b | ||
|
|
de1295d361 | ||
|
|
32f449850b | ||
|
|
6966e8e101 | ||
|
|
a5e094d44a | ||
|
|
5de6fb2fee | ||
|
|
bd25f1db0b | ||
|
|
9daa4e4ec4 | ||
|
|
0b5c0f0c40 | ||
|
|
af559b0fa3 | ||
|
|
d496509fce | ||
|
|
383b327442 | ||
|
|
3f677137de | ||
|
|
0a1cea9b43 | ||
|
|
6ba51a92c2 | ||
|
|
86f2e41983 | ||
|
|
d89a40b21f | ||
|
|
3348ac3e51 | ||
|
|
ee38da5074 | ||
|
|
9af359eb01 | ||
|
|
0b21d02f24 | ||
|
|
282d701327 | ||
|
|
dcadf7447d | ||
|
|
89c1a3c683 | ||
|
|
83514c453e | ||
|
|
d5c6783124 | ||
|
|
5293515aca | ||
|
|
7dafb7ea43 | ||
|
|
0f82ae4fdb | ||
|
|
873ddee0d4 | ||
|
|
fb7888b83c | ||
|
|
ae7b571b68 | ||
|
|
4c5c44742e | ||
|
|
d4c5f08b5f | ||
|
|
0f513370ac | ||
|
|
8969dbccf8 | ||
|
|
ba26cee7f9 | ||
|
|
b88e2ce746 | ||
|
|
c8c0da6d61 | ||
|
|
d40d80c541 | ||
|
|
4369fe8b0c | ||
|
|
363a1e88e9 | ||
|
|
702cce959d | ||
|
|
ebeed1887d | ||
|
|
48bafffcde | ||
|
|
b85113b805 | ||
|
|
fa45d912b8 | ||
|
|
d613d3d24a | ||
|
|
445059a498 | ||
|
|
e0596aefff | ||
|
|
3a0c0de599 | ||
|
|
10b3dcd846 | ||
|
|
edb341ae51 | ||
|
|
dfd8275421 | ||
|
|
680a22f303 | ||
|
|
eb3f93587f | ||
|
|
9926965677 | ||
|
|
c7f573a93b | ||
|
|
ac51b2c799 | ||
|
|
ecd76795ce | ||
|
|
3c3e6172fc | ||
|
|
f805251762 | ||
|
|
6647e4120e | ||
|
|
06f8627331 | ||
|
|
5b43b64401 | ||
|
|
99109f5e91 | ||
|
|
944d37dc75 | ||
|
|
f91fbb2ca0 | ||
|
|
bbe04589b8 | ||
|
|
6aca33e371 | ||
|
|
675917796d | ||
|
|
e12b39bda1 | ||
|
|
c9769e04b8 | ||
|
|
864e6ceeae | ||
|
|
9cc2b52111 | ||
|
|
dcf8150cb2 | ||
|
|
71863fde7f | ||
|
|
5bcee87892 | ||
|
|
00987bb0ff | ||
|
|
d121784de5 | ||
|
|
07e4372a4a | ||
|
|
0e778a128e | ||
|
|
d0f1dbf132 | ||
|
|
5995ded7d5 | ||
|
|
e7ba6b4bb4 | ||
|
|
7d8d00c5ad | ||
|
|
13d931ed19 | ||
|
|
96c01bf796 | ||
|
|
127de3d6a5 | ||
|
|
6a1534124d | ||
|
|
8237dc4c16 | ||
|
|
cea90dbda5 | ||
|
|
1b0d5e4d77 | ||
|
|
bd557ab069 | ||
|
|
e1c8c4beaf | ||
|
|
71efb24cda | ||
|
|
aab211902e | ||
|
|
6477fdc20c | ||
|
|
9238118fbe | ||
|
|
6a61780928 | ||
|
|
fef73887e4 | ||
|
|
3654748632 | ||
|
|
73c91e2b28 | ||
|
|
91f85d066d | ||
|
|
71a6aac826 | ||
|
|
7bfa839c84 | ||
|
|
6a2f58430d | ||
|
|
161c4cc113 | ||
|
|
9b1f97dfec | ||
|
|
701d70a7ae | ||
|
|
8fd4ce9651 | ||
|
|
e89c2e1cf5 | ||
|
|
f78e43bdd4 | ||
|
|
2ab03e624c | ||
|
|
fa3de99222 | ||
|
|
4fe623c806 | ||
|
|
8ba2a598fe | ||
|
|
2fda5a74ca | ||
|
|
401494aab0 | ||
|
|
9b90e3a452 | ||
|
|
5de81e55a9 | ||
|
|
8345d147cf | ||
|
|
1d89fa0bdd | ||
|
|
b978c3ed13 | ||
|
|
4eb9c9a21f | ||
|
|
b736fd0131 | ||
|
|
181fff42de | ||
|
|
3fdb74a3d8 | ||
|
|
a45657c583 | ||
|
|
11b6056d8c | ||
|
|
de45fd3393 | ||
|
|
8ffa3cb0d2 | ||
|
|
a842886933 | ||
|
|
4b1b75fd6e | ||
|
|
c857b66998 | ||
|
|
3daf582b7a | ||
|
|
b3168ff3f0 | ||
|
|
6ff0320531 | ||
|
|
9bf3438466 | ||
|
|
0790fa7d6f | ||
|
|
3822348642 | ||
|
|
0418ef4613 | ||
|
|
9d848eba22 | ||
|
|
f52244c534 | ||
|
|
c48cf384a4 | ||
|
|
48d4c6f113 | ||
|
|
bd30fb626c | ||
|
|
36e75ad71b | ||
|
|
ce12ab0e1a | ||
|
|
f715075dbe | ||
|
|
249e84eb5a | ||
|
|
f9a1f79aef | ||
|
|
6f9452c7e1 | ||
|
|
7e2e712b56 | ||
|
|
64b8b03cca | ||
|
|
82021e3d97 | ||
|
|
9ffc285e16 | ||
|
|
6a6cf2c594 | ||
|
|
743c29e634 | ||
|
|
038fcd36f8 | ||
|
|
92ffa41ed0 | ||
|
|
1becd25cc0 | ||
|
|
b735af6a25 | ||
|
|
b77c81d63e | ||
|
|
a9ac06de83 | ||
|
|
ce975d0f1e | ||
|
|
57b4a3ff15 | ||
|
|
00fe1abcf1 | ||
|
|
d4b88be44b | ||
|
|
f45926c094 | ||
|
|
60e5aabd99 | ||
|
|
99544905df | ||
|
|
59f5066327 | ||
|
|
dc6f2fd5b6 | ||
|
|
c312ccbb4d | ||
|
|
9ac2d148d2 | ||
|
|
13933fc7fd | ||
|
|
114187164e | ||
|
|
c9d7d51732 | ||
|
|
92b6ce31f9 | ||
|
|
ad17855532 | ||
|
|
52a4286b70 | ||
|
|
347be72774 | ||
|
|
f0edb103ce | ||
|
|
018d801121 | ||
|
|
c29ec1c6b0 | ||
|
|
352f98f26f | ||
|
|
0c87ce2ac4 | ||
|
|
7f9f60af5b | ||
|
|
c9f6ef8278 | ||
|
|
14b8bded1d | ||
|
|
0a9e83d8f9 | ||
|
|
32a46b4e98 | ||
|
|
600a578a17 | ||
|
|
c0e175d84b | ||
|
|
2bc56195fd | ||
|
|
75f9f5ca4f | ||
|
|
e36a40a9ac | ||
|
|
6ae9488761 | ||
|
|
fa26344cd0 | ||
|
|
617c1741cc | ||
|
|
c9ebdcdda5 | ||
|
|
b30c036478 | ||
|
|
4fac6c8019 | ||
|
|
c143600c5b | ||
|
|
2426e1cbd4 | ||
|
|
c509102b37 | ||
|
|
a7eb731730 | ||
|
|
d4df57bd05 |
75 changed files with 42854 additions and 2756 deletions
13
.busted
13
.busted
|
|
@ -1,13 +0,0 @@
|
|||
return {
|
||||
_all = {
|
||||
coverage = false,
|
||||
lpath = 'lua/?.lua;lua/?/init.lua',
|
||||
lua = 'nlua',
|
||||
},
|
||||
default = {
|
||||
verbose = true,
|
||||
},
|
||||
tests = {
|
||||
verbose = true,
|
||||
},
|
||||
}
|
||||
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
78
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
name: Bug Report
|
||||
description: Report a bug
|
||||
title: 'bug: '
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label:
|
||||
I have searched [existing
|
||||
issues](https://github.com/barrettruth/cp.nvim/issues)
|
||||
required: true
|
||||
- label: I have updated to the latest version
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Neovim version'
|
||||
description: 'Output of `nvim --version`'
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Operating system'
|
||||
placeholder: 'e.g. Arch Linux, macOS 15, Ubuntu 24.04'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: What happened? What did you expect?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Minimal steps to trigger the bug
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Health check'
|
||||
description: 'Output of `:checkhealth cp`'
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal reproduction
|
||||
description: |
|
||||
Save the script below as `repro.lua`, edit if needed, and run:
|
||||
```
|
||||
nvim -u repro.lua
|
||||
```
|
||||
Confirm the bug reproduces with this config before submitting.
|
||||
render: lua
|
||||
value: |
|
||||
vim.env.LAZY_STDPATH = '.repro'
|
||||
load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))()
|
||||
require('lazy.nvim').setup({
|
||||
spec = {
|
||||
{
|
||||
'barrett-ruth/cp.nvim',
|
||||
opts = {},
|
||||
},
|
||||
},
|
||||
})
|
||||
validations:
|
||||
required: true
|
||||
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://github.com/barrettruth/cp.nvim/discussions
|
||||
about: Ask questions and discuss ideas
|
||||
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: 'feat: '
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label:
|
||||
I have searched [existing
|
||||
issues](https://github.com/barrettruth/cp.nvim/issues)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem
|
||||
description: What problem does this solve?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
21
.github/workflows/luarocks.yaml
vendored
Normal file
21
.github/workflows/luarocks.yaml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: luarocks
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yaml
|
||||
|
||||
publish:
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: nvim-neorocks/luarocks-tag-release@v7
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
18
.github/workflows/luarocks.yml
vendored
18
.github/workflows/luarocks.yml
vendored
|
|
@ -1,18 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-luarocks:
|
||||
name: Publish to LuaRocks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publish to LuaRocks
|
||||
uses: nvim-neorocks/luarocks-tag-release@v7
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name: Code Quality
|
||||
name: quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
@ -90,7 +90,7 @@ jobs:
|
|||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Check Python formatting with ruff
|
||||
run: ruff format --check scrapers/ tests/scrapers/
|
||||
run: ruff format --check .
|
||||
|
||||
python-lint:
|
||||
name: Python Lint Check
|
||||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
- name: Install ruff
|
||||
run: uv tool install ruff
|
||||
- name: Lint Python files with ruff
|
||||
run: ruff check scripts/ scrapers/ tests/scrapers/
|
||||
run: ruff check .
|
||||
|
||||
python-typecheck:
|
||||
name: Python Type Check
|
||||
|
|
@ -115,10 +115,10 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with mypy
|
||||
- name: Install dependencies with uv
|
||||
run: uv sync --dev
|
||||
- name: Type check Python files with mypy
|
||||
run: uv run mypy scripts/ scrapers/ tests/scrapers/
|
||||
- name: Type check Python files with ty
|
||||
run: uvx ty check .
|
||||
|
||||
markdown-format:
|
||||
name: Markdown Format Check
|
||||
|
|
@ -138,4 +138,4 @@ jobs:
|
|||
- name: Install prettier
|
||||
run: pnpm add -g prettier@3.1.0
|
||||
- name: Check markdown formatting with prettier
|
||||
run: prettier --check "*.md" "docs/**/*.md" || true
|
||||
run: prettier --check .
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name: Tests
|
||||
name: tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
@ -31,25 +31,10 @@ jobs:
|
|||
python:
|
||||
- 'scripts/**'
|
||||
- 'scrapers/**'
|
||||
- 'tests/scrapers/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
lua-test:
|
||||
name: Lua Tests (${{ matrix.neovim_version }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
strategy:
|
||||
matrix:
|
||||
neovim_version: ['stable', 'nightly']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Lua tests
|
||||
uses: nvim-neorocks/nvim-busted-action@v1
|
||||
with:
|
||||
nvim_version: ${{ matrix.neovim_version }}
|
||||
|
||||
python-test:
|
||||
name: Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -59,9 +44,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with pytest
|
||||
- name: Install dependencies
|
||||
run: uv sync --dev
|
||||
- name: Fetch camoufox data
|
||||
run: uv run camoufox fetch
|
||||
- name: Run Python tests
|
||||
run: uv run pytest tests/scrapers/ -v
|
||||
run: uv run pytest tests/ -v
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,8 +1,19 @@
|
|||
.venv/
|
||||
.venv
|
||||
venv
|
||||
doc/tags
|
||||
*.log
|
||||
build
|
||||
io
|
||||
debug
|
||||
venv/
|
||||
create
|
||||
|
||||
|
||||
.*cache*
|
||||
CLAUDE.md
|
||||
__pycache__
|
||||
.claude/
|
||||
|
||||
node_modules/
|
||||
|
||||
.envrc
|
||||
.direnv/
|
||||
|
|
|
|||
14
.luarc.json
14
.luarc.json
|
|
@ -1,16 +1,8 @@
|
|||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"runtime.path": [
|
||||
"lua/?.lua",
|
||||
"lua/?/init.lua"
|
||||
],
|
||||
"diagnostics.globals": [
|
||||
"vim"
|
||||
],
|
||||
"workspace.library": [
|
||||
"$VIMRUNTIME/lua",
|
||||
"${3rd}/luv/library"
|
||||
],
|
||||
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||
"diagnostics.globals": ["vim"],
|
||||
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||
"workspace.checkThirdParty": false,
|
||||
"completion.callSnippet": "Replace"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,36 @@
|
|||
minimum_pre_commit_version: "3.5.0"
|
||||
minimum_pre_commit_version: '3.5.0'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/JohnnyMorganz/StyLua
|
||||
rev: v2.1.0
|
||||
rev: v2.3.1
|
||||
hooks:
|
||||
- id: stylua-github
|
||||
name: stylua (Lua formatter)
|
||||
args: ["."]
|
||||
files: ^(lua/|spec/|plugin/|after/|ftdetect/|.*\.lua$)
|
||||
additional_dependencies: []
|
||||
files: \.lua$
|
||||
pass_filenames: true
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
name: ruff (format)
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
files: \.py$
|
||||
- id: ruff
|
||||
name: ruff (lint)
|
||||
args: ["--fix", "--select=I"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy (type check)
|
||||
entry: uv run mypy
|
||||
language: system
|
||||
args: ["scrapers/", "tests/scrapers/"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
pass_filenames: false
|
||||
name: ruff (lint imports)
|
||||
args: ['--fix', '--select=I']
|
||||
files: \.py$
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier (format markdown)
|
||||
files: \.(md)$
|
||||
name: prettier
|
||||
files: \.(md|toml|ya?ml|sh)$
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ty-type-check
|
||||
name: ty (Python type checker)
|
||||
language: system
|
||||
entry: uv run ty check
|
||||
types: [python]
|
||||
|
|
|
|||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.pytest_cache/
|
||||
node_modules/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
tests/fixtures/
|
||||
17
.prettierrc
Normal file
17
.prettierrc
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"proseWrap": "always",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.md"],
|
||||
"options": {
|
||||
"parser": "markdown"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
64
README.md
64
README.md
|
|
@ -2,22 +2,38 @@
|
|||
|
||||
**The definitive competitive programming environment for Neovim**
|
||||
|
||||
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
|
||||
Scrape problems, run tests, and debug solutions across multiple platforms with
|
||||
zero configuration.
|
||||
|
||||
https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9
|
||||
https://github.com/user-attachments/assets/e81d8dfb-578f-4a79-9989-210164fc0148
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface
|
||||
- **Multi-platform support**: AtCoder, CodeChef, Codeforces, and CSES
|
||||
- **Automatic problem setup**: Scrape test cases and metadata in seconds
|
||||
- **Rich test output**: 256 color ANSI support for compiler errors and program output
|
||||
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
|
||||
detailed analysis
|
||||
- **Test case management**: Quickly view, edit, add, & remove test cases
|
||||
- **Rich test output**: 256 color ANSI support for compiler errors and program
|
||||
output
|
||||
- **Language agnostic**: Works with any language
|
||||
- **Diff viewer**: Compare expected vs actual output with 3 diff modes
|
||||
|
||||
## Optional Dependencies
|
||||
## Installation
|
||||
|
||||
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||
- GNU [time](https://www.gnu.org/software/time/) and [timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
|
||||
Install using your package manager of choice or via
|
||||
[luarocks](https://luarocks.org/modules/barrettruth/cp.nvim):
|
||||
|
||||
```
|
||||
luarocks install cp.nvim
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GNU [time](https://www.gnu.org/software/time/) and
|
||||
[timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
|
||||
- [uv](https://docs.astral.sh/uv/) or [nix](https://nixos.org/) for problem
|
||||
scraping
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -25,20 +41,20 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
|||
|
||||
### Basic Usage
|
||||
|
||||
1. **Find a contest or problem** on the judge website
|
||||
2. **Set up locally** with `:CP <platform> <contest>`
|
||||
1. Find a contest or problem
|
||||
2. Set up contests locally
|
||||
|
||||
```
|
||||
:CP codeforces 1848
|
||||
```
|
||||
|
||||
3. **Code and test** with instant feedback and rich diffs
|
||||
3. Code and test
|
||||
|
||||
```
|
||||
:CP run
|
||||
```
|
||||
|
||||
4. **Navigate between problems**
|
||||
4. Navigate between problems
|
||||
|
||||
```
|
||||
:CP next
|
||||
|
|
@ -46,7 +62,14 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
|||
:CP e1
|
||||
```
|
||||
|
||||
5. **Submit** on the original website
|
||||
5. Debug and edit test cases
|
||||
|
||||
```
|
||||
:CP edit
|
||||
:CP panel --debug
|
||||
```
|
||||
|
||||
5. Submit on the original website
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
@ -54,7 +77,22 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
|||
:help cp.nvim
|
||||
```
|
||||
|
||||
See [my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua) for a relatively advanced setup.
|
||||
See
|
||||
[my config](https://github.com/barrettruth/dots/blob/main/.config/nvim/lua/plugins/cp.lua)
|
||||
for the setup in the video shown above.
|
||||
|
||||
## Motivation
|
||||
|
||||
I could not find a neovim-centric, efficient, dependency-free, flexible, and
|
||||
easily customizable competitive programming workflow that "just works"--so I
|
||||
made it myself. I conferenced with top competitive programmers at Carnegie
|
||||
Mellon Univerity and the University of Virginia and covered their (and my) pain
|
||||
points:
|
||||
|
||||
- Scraping: contests are automatically loaded asynchronously
|
||||
- Test Case Management: test case editor (`:CP edit`)
|
||||
- UI: both `run` and `panel` layouts cover common formats
|
||||
- Extensibility: snippet plugins, compilation, etc. are left to the programmer
|
||||
|
||||
## Similar Projects
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ rockspec_format = '3.0'
|
|||
package = 'cp.nvim'
|
||||
version = 'scm-1'
|
||||
|
||||
source = { url = 'git://github.com/barrett-ruth/cp.nvim' }
|
||||
source = { url = 'git://github.com/barrettruth/cp.nvim' }
|
||||
build = { type = 'builtin' }
|
||||
|
||||
test_dependencies = {
|
||||
|
|
|
|||
782
doc/cp.nvim.txt
782
doc/cp.nvim.txt
File diff suppressed because it is too large
Load diff
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
72
flake.nix
Normal file
72
flake.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default-linux";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
}:
|
||||
let
|
||||
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
|
||||
mkPythonEnv =
|
||||
pkgs:
|
||||
pkgs.python312.withPackages (ps: [
|
||||
ps.backoff
|
||||
ps.beautifulsoup4
|
||||
ps.curl-cffi
|
||||
ps.httpx
|
||||
ps.ndjson
|
||||
ps.pydantic
|
||||
ps.requests
|
||||
]);
|
||||
|
||||
mkPlugin =
|
||||
pkgs:
|
||||
let
|
||||
pythonEnv = mkPythonEnv pkgs;
|
||||
in
|
||||
pkgs.vimUtils.buildVimPlugin {
|
||||
pname = "cp-nvim";
|
||||
version = "0-unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||
src = self;
|
||||
postPatch = ''
|
||||
substituteInPlace lua/cp/utils.lua \
|
||||
--replace-fail "local _nix_python = nil" \
|
||||
"local _nix_python = '${pythonEnv.interpreter}'"
|
||||
'';
|
||||
nvimSkipModule = [
|
||||
"cp.pickers.telescope"
|
||||
"cp.version"
|
||||
];
|
||||
passthru = { inherit pythonEnv; };
|
||||
meta.description = "Competitive programming plugin for Neovim";
|
||||
};
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
vimPlugins = prev.vimPlugins // {
|
||||
cp-nvim = mkPlugin final;
|
||||
};
|
||||
};
|
||||
|
||||
packages = eachSystem (system: {
|
||||
default = mkPlugin (pkgsFor system);
|
||||
pythonEnv = mkPythonEnv (pkgsFor system);
|
||||
});
|
||||
|
||||
devShells = eachSystem (system: {
|
||||
default = (pkgsFor system).mkShell {
|
||||
packages = with (pkgsFor system); [
|
||||
uv
|
||||
python312
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -9,18 +9,25 @@
|
|||
---@field index_map table<string, number>
|
||||
---@field name string
|
||||
---@field display_name string
|
||||
---@field url string
|
||||
|
||||
---@class ContestSummary
|
||||
---@field display_name string
|
||||
---@field name string
|
||||
---@field id string
|
||||
|
||||
---@class CombinedTest
|
||||
---@field input string
|
||||
---@field expected string
|
||||
|
||||
---@class Problem
|
||||
---@field id string
|
||||
---@field name? string
|
||||
---@field interactive? boolean
|
||||
---@field multi_test? boolean
|
||||
---@field memory_mb? number
|
||||
---@field timeout_ms? number
|
||||
---@field combined_test? CombinedTest
|
||||
---@field test_cases TestCase[]
|
||||
|
||||
---@class TestCase
|
||||
|
|
@ -91,14 +98,36 @@ function M.get_contest_data(platform, contest_id)
|
|||
return cache_data[platform][contest_id]
|
||||
end
|
||||
|
||||
---Get all cached contest IDs for a platform
|
||||
---@param platform string
|
||||
---@return string[]
|
||||
function M.get_cached_contest_ids(platform)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
})
|
||||
|
||||
if not cache_data[platform] then
|
||||
return {}
|
||||
end
|
||||
|
||||
local contest_ids = {}
|
||||
for contest_id, _ in pairs(cache_data[platform]) do
|
||||
table.insert(contest_ids, contest_id)
|
||||
end
|
||||
table.sort(contest_ids)
|
||||
return contest_ids
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problems Problem[]
|
||||
function M.set_contest_data(platform, contest_id, problems)
|
||||
---@param url string
|
||||
function M.set_contest_data(platform, contest_id, problems, url)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problems = { problems, 'table' },
|
||||
url = { url, 'string' },
|
||||
})
|
||||
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
|
|
@ -109,6 +138,7 @@ function M.set_contest_data(platform, contest_id, problems)
|
|||
display_name = prev.display_name,
|
||||
problems = problems,
|
||||
index_map = {},
|
||||
url = url,
|
||||
}
|
||||
for i, p in ipairs(out.problems) do
|
||||
out.index_map[p.id] = i
|
||||
|
|
@ -156,38 +186,64 @@ function M.get_test_cases(platform, contest_id, problem_id)
|
|||
return cache_data[platform][contest_id].problems[index].test_cases or {}
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@return CombinedTest?
|
||||
function M.get_combined_test(platform, contest_id, problem_id)
|
||||
if
|
||||
not cache_data[platform]
|
||||
or not cache_data[platform][contest_id]
|
||||
or not cache_data[platform][contest_id].problems
|
||||
or not cache_data[platform][contest_id].index_map
|
||||
then
|
||||
return nil
|
||||
end
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
return cache_data[platform][contest_id].problems[index].combined_test
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string
|
||||
---@param combined_test? CombinedTest
|
||||
---@param test_cases TestCase[]
|
||||
---@param timeout_ms number
|
||||
---@param memory_mb number
|
||||
---@param interactive boolean
|
||||
---@param multi_test boolean
|
||||
function M.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
combined_test,
|
||||
test_cases,
|
||||
timeout_ms,
|
||||
memory_mb,
|
||||
interactive
|
||||
interactive,
|
||||
multi_test
|
||||
)
|
||||
vim.validate({
|
||||
platform = { platform, 'string' },
|
||||
contest_id = { contest_id, 'string' },
|
||||
problem_id = { problem_id, { 'string', 'nil' }, true },
|
||||
combined_test = { combined_test, { 'table', 'nil' }, true },
|
||||
test_cases = { test_cases, 'table' },
|
||||
timeout_ms = { timeout_ms, { 'number', 'nil' }, true },
|
||||
memory_mb = { memory_mb, { 'number', 'nil' }, true },
|
||||
interactive = { interactive, { 'boolean', 'nil' }, true },
|
||||
multi_test = { multi_test, { 'boolean', 'nil' }, true },
|
||||
})
|
||||
|
||||
local index = cache_data[platform][contest_id].index_map[problem_id]
|
||||
|
||||
cache_data[platform][contest_id].problems[index].combined_test = combined_test
|
||||
cache_data[platform][contest_id].problems[index].test_cases = test_cases
|
||||
cache_data[platform][contest_id].problems[index].timeout_ms = timeout_ms
|
||||
cache_data[platform][contest_id].problems[index].memory_mb = memory_mb
|
||||
cache_data[platform][contest_id].problems[index].interactive = interactive
|
||||
cache_data[platform][contest_id].problems[index].multi_test = multi_test
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,7 +39,21 @@ function M.handle_cache_command(cmd)
|
|||
vim.api.nvim_set_current_buf(buf)
|
||||
elseif cmd.subcommand == 'clear' then
|
||||
cache.load()
|
||||
if cmd.platform then
|
||||
if cmd.platform and cmd.contest then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_contest_data(cmd.platform, cmd.contest)
|
||||
logger.log(
|
||||
("Cache cleared for %s contest '%s'"):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[cmd.platform],
|
||||
cmd.contest
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
else
|
||||
logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR)
|
||||
end
|
||||
elseif cmd.platform then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_platform(cmd.platform)
|
||||
logger.log(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ local actions = constants.ACTIONS
|
|||
---@field platform? string
|
||||
---@field problem_id? string
|
||||
---@field interactor_cmd? string
|
||||
---@field test_index? integer
|
||||
---@field test_indices? integer[]
|
||||
---@field mode? string
|
||||
---@field debug? boolean
|
||||
---@field language? string
|
||||
---@field subcommand? string
|
||||
|
||||
--- Turn raw args into normalized structure to later dispatch
|
||||
---@param args string[] The raw command-line mode args
|
||||
|
|
@ -37,10 +43,12 @@ local function parse_command(args)
|
|||
end
|
||||
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
|
||||
local platform = args[3]
|
||||
local contest = args[4]
|
||||
return {
|
||||
type = 'cache',
|
||||
subcommand = subcommand,
|
||||
platform = platform,
|
||||
contest = contest,
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
||||
|
|
@ -52,8 +60,149 @@ local function parse_command(args)
|
|||
else
|
||||
return { type = 'action', action = 'interact' }
|
||||
end
|
||||
elseif first == 'edit' then
|
||||
local test_index = nil
|
||||
if #args >= 2 then
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number"):format(args[2]),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
test_index = idx
|
||||
end
|
||||
return { type = 'action', action = 'edit', test_index = test_index }
|
||||
elseif first == 'run' or first == 'panel' then
|
||||
local debug = false
|
||||
local test_indices = nil
|
||||
local mode = 'combined'
|
||||
|
||||
if #args == 2 then
|
||||
if args[2] == '--debug' then
|
||||
debug = true
|
||||
elseif args[2] == 'all' then
|
||||
mode = 'individual'
|
||||
else
|
||||
if args[2]:find(',') then
|
||||
local indices = {}
|
||||
for num in args[2]:gmatch('[^,]+') do
|
||||
local idx = tonumber(num)
|
||||
if not idx or idx < 1 or idx ~= math.floor(idx) then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid test index '%s' in list"):format(num),
|
||||
}
|
||||
end
|
||||
table.insert(indices, idx)
|
||||
end
|
||||
if #indices == 0 then
|
||||
return { type = 'error', message = 'No valid test indices provided' }
|
||||
end
|
||||
test_indices = indices
|
||||
mode = 'individual'
|
||||
else
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number(s), 'all', or --debug"):format(
|
||||
args[2]
|
||||
),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
test_indices = { idx }
|
||||
mode = 'individual'
|
||||
end
|
||||
end
|
||||
elseif #args == 3 then
|
||||
if args[2] == 'all' then
|
||||
mode = 'individual'
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
debug = true
|
||||
elseif args[2]:find(',') then
|
||||
local indices = {}
|
||||
for num in args[2]:gmatch('[^,]+') do
|
||||
local idx = tonumber(num)
|
||||
if not idx or idx < 1 or idx ~= math.floor(idx) then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid test index '%s' in list"):format(num),
|
||||
}
|
||||
end
|
||||
table.insert(indices, idx)
|
||||
end
|
||||
if #indices == 0 then
|
||||
return { type = 'error', message = 'No valid test indices provided' }
|
||||
end
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
test_indices = indices
|
||||
mode = 'individual'
|
||||
debug = true
|
||||
else
|
||||
local idx = tonumber(args[2])
|
||||
if not idx then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected test number"):format(args[2]),
|
||||
}
|
||||
end
|
||||
if idx < 1 or idx ~= math.floor(idx) then
|
||||
return { type = 'error', message = ("'%s' is not a valid test index"):format(idx) }
|
||||
end
|
||||
if args[3] ~= '--debug' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Invalid argument '%s': expected --debug"):format(args[3]),
|
||||
}
|
||||
end
|
||||
test_indices = { idx }
|
||||
mode = 'individual'
|
||||
debug = true
|
||||
end
|
||||
elseif #args > 3 then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Too many arguments. Usage: :CP '
|
||||
.. first
|
||||
.. ' [all|test_num[,test_num...]] [--debug]',
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
type = 'action',
|
||||
action = first,
|
||||
test_indices = test_indices,
|
||||
debug = debug,
|
||||
mode = mode,
|
||||
}
|
||||
else
|
||||
return { type = 'action', action = first }
|
||||
local language = nil
|
||||
if #args >= 3 and args[2] == '--lang' then
|
||||
language = args[3]
|
||||
elseif #args >= 2 and args[2] ~= nil and args[2]:sub(1, 2) ~= '--' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = ("Unknown argument '%s' for action '%s'"):format(args[2], first),
|
||||
}
|
||||
end
|
||||
return { type = 'action', action = first, language = language }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -69,13 +218,18 @@ local function parse_command(args)
|
|||
platform = first,
|
||||
contest = args[2],
|
||||
}
|
||||
elseif #args == 3 then
|
||||
elseif #args == 4 and args[3] == '--lang' then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Setup contests with :CP <platform> <contest_id>.',
|
||||
type = 'contest_setup',
|
||||
platform = first,
|
||||
contest = args[2],
|
||||
language = args[4],
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'Too many arguments' }
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Invalid arguments. Usage: :CP <platform> <contest> [--lang <language>]',
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -84,6 +238,12 @@ local function parse_command(args)
|
|||
type = 'problem_jump',
|
||||
problem_id = first,
|
||||
}
|
||||
elseif #args == 3 and args[2] == '--lang' then
|
||||
return {
|
||||
type = 'problem_jump',
|
||||
problem_id = first,
|
||||
language = args[3],
|
||||
}
|
||||
end
|
||||
|
||||
return { type = 'error', message = 'Unknown command or no contest context.' }
|
||||
|
|
@ -104,21 +264,27 @@ function M.handle_command(opts)
|
|||
restore.restore_from_current_file()
|
||||
elseif cmd.type == 'action' then
|
||||
local setup = require('cp.setup')
|
||||
local ui = require('cp.ui.panel')
|
||||
local ui = require('cp.ui.views')
|
||||
|
||||
if cmd.action == 'interact' then
|
||||
ui.toggle_interactive(cmd.interactor_cmd)
|
||||
elseif cmd.action == 'run' then
|
||||
ui.toggle_run_panel()
|
||||
elseif cmd.action == 'debug' then
|
||||
ui.toggle_run_panel({ debug = true })
|
||||
ui.run_io_view(cmd.test_indices, cmd.debug, cmd.mode)
|
||||
elseif cmd.action == 'panel' then
|
||||
ui.toggle_panel({
|
||||
debug = cmd.debug,
|
||||
test_index = cmd.test_indices and cmd.test_indices[1] or nil,
|
||||
})
|
||||
elseif cmd.action == 'next' then
|
||||
setup.navigate_problem(1)
|
||||
setup.navigate_problem(1, cmd.language)
|
||||
elseif cmd.action == 'prev' then
|
||||
setup.navigate_problem(-1)
|
||||
setup.navigate_problem(-1, cmd.language)
|
||||
elseif cmd.action == 'pick' then
|
||||
local picker = require('cp.commands.picker')
|
||||
picker.handle_pick_action()
|
||||
picker.handle_pick_action(cmd.language)
|
||||
elseif cmd.action == 'edit' then
|
||||
local edit = require('cp.ui.edit')
|
||||
edit.toggle_edit(cmd.test_index)
|
||||
end
|
||||
elseif cmd.type == 'problem_jump' then
|
||||
local platform = state.get_platform()
|
||||
|
|
@ -147,15 +313,13 @@ function M.handle_command(opts)
|
|||
end
|
||||
|
||||
local setup = require('cp.setup')
|
||||
setup.setup_contest(platform, contest_id, problem_id)
|
||||
setup.setup_contest(platform, contest_id, problem_id, cmd.language)
|
||||
elseif cmd.type == 'cache' then
|
||||
local cache_commands = require('cp.commands.cache')
|
||||
cache_commands.handle_cache_command(cmd)
|
||||
elseif cmd.type == 'contest_setup' then
|
||||
local setup = require('cp.setup')
|
||||
if setup.set_platform(cmd.platform) then
|
||||
setup.setup_contest(cmd.platform, cmd.contest, nil)
|
||||
end
|
||||
setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ local config_module = require('cp.config')
|
|||
local logger = require('cp.log')
|
||||
|
||||
--- Dispatch `:CP pick` to appropriate picker
|
||||
---@param language? string
|
||||
---@return nil
|
||||
function M.handle_pick_action()
|
||||
function M.handle_pick_action(language)
|
||||
local config = config_module.get_config()
|
||||
|
||||
if not (config.ui and config.ui.picker) then
|
||||
|
|
@ -39,7 +40,7 @@ function M.handle_pick_action()
|
|||
local ok, _ = pcall(require, 'fzf-lua')
|
||||
if not ok then
|
||||
logger.log(
|
||||
'fzf-lua is not available. Install fzf-lua xor change your picker config',
|
||||
'fzf-lua is not available. Install fzf-lua or change your picker config',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -53,7 +54,7 @@ function M.handle_pick_action()
|
|||
picker = fzf_picker
|
||||
end
|
||||
|
||||
picker.pick()
|
||||
picker.pick(language)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@
|
|||
---@field default_language string
|
||||
---@field overrides? table<string, CpPlatformOverrides>
|
||||
|
||||
---@class RunPanelConfig
|
||||
---@field ansi boolean
|
||||
---@field diff_mode "none"|"vim"|"git"
|
||||
---@class PanelConfig
|
||||
---@field diff_modes string[]
|
||||
---@field max_output_lines integer
|
||||
|
||||
---@class DiffGitConfig
|
||||
|
|
@ -32,9 +31,52 @@
|
|||
---@field before_run? fun(state: cp.State)
|
||||
---@field before_debug? fun(state: cp.State)
|
||||
---@field setup_code? fun(state: cp.State)
|
||||
---@field setup_io_input? fun(bufnr: integer, state: cp.State)
|
||||
---@field setup_io_output? fun(bufnr: integer, state: cp.State)
|
||||
|
||||
---@class VerdictFormatData
|
||||
---@field index integer
|
||||
---@field status { text: string, highlight_group: string }
|
||||
---@field time_ms number
|
||||
---@field time_limit_ms number
|
||||
---@field memory_mb number
|
||||
---@field memory_limit_mb number
|
||||
---@field exit_code integer
|
||||
---@field signal string|nil
|
||||
---@field time_actual_width? integer
|
||||
---@field time_limit_width? integer
|
||||
---@field mem_actual_width? integer
|
||||
---@field mem_limit_width? integer
|
||||
|
||||
---@class VerdictHighlight
|
||||
---@field col_start integer
|
||||
---@field col_end integer
|
||||
---@field group string
|
||||
|
||||
---@class VerdictFormatResult
|
||||
---@field line string
|
||||
---@field highlights? VerdictHighlight[]
|
||||
|
||||
---@alias VerdictFormatter fun(data: VerdictFormatData): VerdictFormatResult
|
||||
|
||||
---@class RunConfig
|
||||
---@field width number
|
||||
---@field next_test_key string|nil
|
||||
---@field prev_test_key string|nil
|
||||
---@field format_verdict VerdictFormatter
|
||||
|
||||
---@class EditConfig
|
||||
---@field next_test_key string|nil
|
||||
---@field prev_test_key string|nil
|
||||
---@field delete_test_key string|nil
|
||||
---@field add_test_key string|nil
|
||||
---@field save_and_exit_key string|nil
|
||||
|
||||
---@class CpUI
|
||||
---@field run_panel RunPanelConfig
|
||||
---@field ansi boolean
|
||||
---@field run RunConfig
|
||||
---@field edit EditConfig
|
||||
---@field panel PanelConfig
|
||||
---@field diff DiffConfig
|
||||
---@field picker string|nil
|
||||
|
||||
|
|
@ -43,6 +85,7 @@
|
|||
---@field platforms table<string, CpPlatform>
|
||||
---@field hooks Hooks
|
||||
---@field debug boolean
|
||||
---@field open_url boolean
|
||||
---@field scrapers string[]
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@field ui CpUI
|
||||
|
|
@ -53,11 +96,13 @@
|
|||
local M = {}
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local helpers = require('cp.helpers')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
-- defaults per the new single schema
|
||||
---@type cp.Config
|
||||
M.defaults = {
|
||||
open_url = false,
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
|
|
@ -94,17 +139,41 @@ M.defaults = {
|
|||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
codechef = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
},
|
||||
hooks = { before_run = nil, before_debug = nil, setup_code = nil },
|
||||
hooks = {
|
||||
before_run = nil,
|
||||
before_debug = nil,
|
||||
setup_code = nil,
|
||||
setup_io_input = helpers.clearcol,
|
||||
setup_io_output = helpers.clearcol,
|
||||
},
|
||||
debug = false,
|
||||
scrapers = constants.PLATFORMS,
|
||||
filename = nil,
|
||||
ui = {
|
||||
run_panel = { ansi = true, diff_mode = 'none', max_output_lines = 50 },
|
||||
ansi = true,
|
||||
run = {
|
||||
width = 0.3,
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
format_verdict = helpers.default_verdict_formatter,
|
||||
},
|
||||
edit = {
|
||||
next_test_key = ']t',
|
||||
prev_test_key = '[t',
|
||||
delete_test_key = 'gd',
|
||||
add_test_key = 'ga',
|
||||
save_and_exit_key = 'q',
|
||||
},
|
||||
panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50 },
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||
|
|
@ -145,6 +214,11 @@ local function validate_language(id, lang)
|
|||
extension = { lang.extension, 'string' },
|
||||
commands = { lang.commands, { 'table' } },
|
||||
})
|
||||
|
||||
if not lang.commands.run then
|
||||
error(('[cp.nvim] languages.%s.commands.run is required'):format(id))
|
||||
end
|
||||
|
||||
if lang.commands.build ~= nil then
|
||||
vim.validate({ build = { lang.commands.build, { 'table' } } })
|
||||
if not has_tokens(lang.commands.build, { '{source}', '{binary}' }) then
|
||||
|
|
@ -218,36 +292,146 @@ end
|
|||
---@return cp.Config
|
||||
function M.setup(user_config)
|
||||
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
|
||||
local defaults = vim.deepcopy(M.defaults)
|
||||
if user_config and user_config.platforms then
|
||||
for plat in pairs(defaults.platforms) do
|
||||
if not user_config.platforms[plat] then
|
||||
defaults.platforms[plat] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local cfg = vim.tbl_deep_extend('force', defaults, user_config or {})
|
||||
|
||||
if not next(cfg.languages) then
|
||||
error('[cp.nvim] At least one language must be configured')
|
||||
end
|
||||
|
||||
if not next(cfg.platforms) then
|
||||
error('[cp.nvim] At least one platform must be configured')
|
||||
end
|
||||
|
||||
vim.validate({
|
||||
hooks = { cfg.hooks, { 'table' } },
|
||||
ui = { cfg.ui, { 'table' } },
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
debug = { cfg.debug, { 'boolean', 'nil' }, true },
|
||||
open_url = { cfg.open_url, { 'boolean', 'nil' }, true },
|
||||
filename = { cfg.filename, { 'function', 'nil' }, true },
|
||||
scrapers = {
|
||||
cfg.scrapers,
|
||||
function(v)
|
||||
if type(v) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, s in ipairs(v) do
|
||||
if not vim.tbl_contains(constants.PLATFORMS, s) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end,
|
||||
('one of {%s}'):format(table.concat(constants.PLATFORMS, ',')),
|
||||
},
|
||||
before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true },
|
||||
before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true },
|
||||
setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true },
|
||||
setup_io_input = { cfg.hooks.setup_io_input, { 'function', 'nil' }, true },
|
||||
setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true },
|
||||
})
|
||||
|
||||
local layouts = require('cp.ui.layouts')
|
||||
vim.validate({
|
||||
ansi = { cfg.ui.run_panel.ansi, 'boolean' },
|
||||
diff_mode = {
|
||||
cfg.ui.run_panel.diff_mode,
|
||||
ansi = { cfg.ui.ansi, 'boolean' },
|
||||
diff_modes = {
|
||||
cfg.ui.panel.diff_modes,
|
||||
function(v)
|
||||
return vim.tbl_contains({ 'none', 'vim', 'git' }, v)
|
||||
if type(v) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, mode in ipairs(v) do
|
||||
if not layouts.DIFF_MODES[mode] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end,
|
||||
"diff_mode must be 'none', 'vim', or 'git'",
|
||||
('one of {%s}'):format(table.concat(vim.tbl_keys(layouts.DIFF_MODES), ',')),
|
||||
},
|
||||
max_output_lines = {
|
||||
cfg.ui.run_panel.max_output_lines,
|
||||
cfg.ui.panel.max_output_lines,
|
||||
function(v)
|
||||
return type(v) == 'number' and v > 0 and v == math.floor(v)
|
||||
end,
|
||||
'positive integer',
|
||||
},
|
||||
git = { cfg.ui.diff.git, { 'table' } },
|
||||
git_args = { cfg.ui.diff.git.args, is_string_list, 'string[]' },
|
||||
width = {
|
||||
cfg.ui.run.width,
|
||||
function(v)
|
||||
return type(v) == 'number' and v > 0 and v <= 1
|
||||
end,
|
||||
'decimal between 0 and 1',
|
||||
},
|
||||
next_test_key = {
|
||||
cfg.ui.run.next_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
prev_test_key = {
|
||||
cfg.ui.run.prev_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
format_verdict = {
|
||||
cfg.ui.run.format_verdict,
|
||||
'function',
|
||||
},
|
||||
edit_next_test_key = {
|
||||
cfg.ui.edit.next_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
edit_prev_test_key = {
|
||||
cfg.ui.edit.prev_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
delete_test_key = {
|
||||
cfg.ui.edit.delete_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
add_test_key = {
|
||||
cfg.ui.edit.add_test_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
save_and_exit_key = {
|
||||
cfg.ui.edit.save_and_exit_key,
|
||||
function(v)
|
||||
return v == nil or (type(v) == 'string' and #v > 0)
|
||||
end,
|
||||
'nil or non-empty string',
|
||||
},
|
||||
picker = {
|
||||
cfg.ui.picker,
|
||||
function(v)
|
||||
return v == nil or v == 'telescope' or v == 'fzf-lua'
|
||||
end,
|
||||
"nil, 'telescope', or 'fzf-lua'",
|
||||
},
|
||||
})
|
||||
|
||||
for id, lang in pairs(cfg.languages) do
|
||||
|
|
@ -274,6 +458,66 @@ function M.get_config()
|
|||
return current_config or M.defaults
|
||||
end
|
||||
|
||||
---Validate and get effective language config for a platform
|
||||
---@param platform_id string
|
||||
---@param language_id string
|
||||
---@return { valid: boolean, effective?: CpLanguage, extension?: string, error?: string }
|
||||
function M.get_language_for_platform(platform_id, language_id)
|
||||
local cfg = M.get_config()
|
||||
|
||||
if not cfg.platforms[platform_id] then
|
||||
return { valid = false, error = string.format("Unknown platform '%s'", platform_id) }
|
||||
end
|
||||
|
||||
local platform = cfg.platforms[platform_id]
|
||||
|
||||
if not cfg.languages[language_id] then
|
||||
local available = table.concat(platform.enabled_languages, ', ')
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format("Unknown language '%s'. Available: [%s]", language_id, available),
|
||||
}
|
||||
end
|
||||
|
||||
if not vim.tbl_contains(platform.enabled_languages, language_id) then
|
||||
local available = table.concat(platform.enabled_languages, ', ')
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format(
|
||||
"Language '%s' not enabled for %s. Available: [%s]",
|
||||
language_id,
|
||||
platform_id,
|
||||
available
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
local platform_effective = cfg.runtime.effective[platform_id]
|
||||
if not platform_effective then
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format(
|
||||
'No runtime config for platform %s (plugin not initialized)',
|
||||
platform_id
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
local effective = platform_effective[language_id]
|
||||
if not effective then
|
||||
return {
|
||||
valid = false,
|
||||
error = string.format('No effective config for %s/%s', platform_id, language_id),
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
valid = true,
|
||||
effective = effective,
|
||||
extension = effective.extension,
|
||||
}
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@return string
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
local M = {}
|
||||
|
||||
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
||||
M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache', 'interact' }
|
||||
M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses' }
|
||||
M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit' }
|
||||
|
||||
M.PLATFORM_DISPLAY_NAMES = {
|
||||
atcoder = 'AtCoder',
|
||||
codechef = 'CodeChef',
|
||||
codeforces = 'CodeForces',
|
||||
cses = 'CSES',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ local utils = require('cp.utils')
|
|||
local function check()
|
||||
vim.health.start('cp.nvim [required] ~')
|
||||
|
||||
utils.setup_python_env()
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||
vim.health.ok('Neovim 0.10.0+ detected')
|
||||
else
|
||||
|
|
@ -16,22 +18,37 @@ local function check()
|
|||
vim.health.error('Windows is not supported')
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if utils.is_nix_build() then
|
||||
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
|
||||
vim.health.ok('Nix Python environment detected (' .. source .. ')')
|
||||
local py = utils.get_nix_python()
|
||||
vim.health.info('Python: ' .. py)
|
||||
local r = vim.system({ py, '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
|
||||
vim.health.info('Python version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||
end
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
vim.health.info('nix available but Python environment not resolved via nix')
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||
end
|
||||
end
|
||||
|
||||
local time_cap = utils.time_capability()
|
||||
|
|
@ -41,7 +58,7 @@ local function check()
|
|||
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
||||
end
|
||||
|
||||
local timeout_cap = utils.time_capability()
|
||||
local timeout_cap = utils.timeout_capability()
|
||||
if timeout_cap.ok then
|
||||
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
||||
else
|
||||
|
|
|
|||
101
lua/cp/helpers.lua
Normal file
101
lua/cp/helpers.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
local M = {}
|
||||
|
||||
---@param bufnr integer
|
||||
function M.clearcol(bufnr)
|
||||
for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
|
||||
vim.wo[win].signcolumn = 'no'
|
||||
vim.wo[win].statuscolumn = ''
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
end
|
||||
end
|
||||
|
||||
---Pad text on the right (left-align text within width)
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.pad_right(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
return text .. string.rep(' ', pad)
|
||||
end
|
||||
|
||||
---Pad text on the left (right-align text within width)
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.pad_left(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
return string.rep(' ', pad) .. text
|
||||
end
|
||||
|
||||
---Center text within width
|
||||
---@param text string
|
||||
---@param width integer
|
||||
---@return string
|
||||
function M.center(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
local left = math.ceil(pad / 2)
|
||||
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
|
||||
end
|
||||
|
||||
---Default verdict formatter for I/O view
|
||||
---@param data VerdictFormatData
|
||||
---@return VerdictFormatResult
|
||||
function M.default_verdict_formatter(data)
|
||||
local time_actual = string.format('%.2f', data.time_ms)
|
||||
local time_limit = tostring(data.time_limit_ms)
|
||||
local mem_actual = string.format('%.0f', data.memory_mb)
|
||||
local mem_limit = string.format('%.0f', data.memory_limit_mb)
|
||||
local exit_str = data.signal and string.format('%d (%s)', data.exit_code, data.signal)
|
||||
or tostring(data.exit_code)
|
||||
|
||||
local time_actual_w = data.time_actual_width or 6
|
||||
local time_limit_w = data.time_limit_width or 4
|
||||
local mem_actual_w = data.mem_actual_width or 3
|
||||
local mem_limit_w = data.mem_limit_width or 3
|
||||
|
||||
local test_num_part = 'Test ' .. data.index .. ':'
|
||||
local status_part = M.pad_right(data.status.text, 3)
|
||||
local time_part = M.pad_left(time_actual, time_actual_w)
|
||||
.. '/'
|
||||
.. M.pad_left(time_limit, time_limit_w)
|
||||
.. ' ms'
|
||||
local mem_part = M.pad_left(mem_actual, mem_actual_w)
|
||||
.. '/'
|
||||
.. M.pad_left(mem_limit, mem_limit_w)
|
||||
.. ' MB'
|
||||
local exit_part = 'exit: ' .. exit_str
|
||||
|
||||
local line = test_num_part
|
||||
.. ' '
|
||||
.. status_part
|
||||
.. ' | '
|
||||
.. time_part
|
||||
.. ' | '
|
||||
.. mem_part
|
||||
.. ' | '
|
||||
.. exit_part
|
||||
|
||||
local highlights = {}
|
||||
local status_pos = line:find(data.status.text, 1, true)
|
||||
if status_pos then
|
||||
table.insert(highlights, {
|
||||
col_start = status_pos - 1,
|
||||
col_end = status_pos - 1 + #data.status.text,
|
||||
group = data.status.highlight_group,
|
||||
})
|
||||
end
|
||||
|
||||
return { line = line, highlights = highlights }
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -1,35 +1,54 @@
|
|||
local M = {}
|
||||
|
||||
local config_module = require('cp.config')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
|
||||
M.helpers = helpers
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 0 then
|
||||
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
return {}
|
||||
end
|
||||
|
||||
local user_config = {}
|
||||
local config = nil
|
||||
local initialized = false
|
||||
|
||||
--- Root handler for all `:CP ...` commands
|
||||
---@return nil
|
||||
function M.handle_command(opts)
|
||||
local commands = require('cp.commands')
|
||||
commands.handle_command(opts)
|
||||
local function ensure_initialized()
|
||||
if initialized then
|
||||
return true
|
||||
end
|
||||
local user_config = vim.g.cp or {}
|
||||
local ok, result = pcall(config_module.setup, user_config)
|
||||
if not ok then
|
||||
local msg = tostring(result):gsub('^.+:%d+: ', '')
|
||||
vim.notify(msg, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
config_module.set_current_config(result)
|
||||
initialized = true
|
||||
return true
|
||||
end
|
||||
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
user_config = opts
|
||||
config = config_module.setup(user_config)
|
||||
config_module.set_current_config(config)
|
||||
|
||||
initialized = true
|
||||
---@return nil
|
||||
function M.handle_command(opts)
|
||||
if not ensure_initialized() then
|
||||
return
|
||||
end
|
||||
local commands = require('cp.commands')
|
||||
commands.handle_command(opts)
|
||||
end
|
||||
|
||||
function M.is_initialized()
|
||||
return initialized
|
||||
end
|
||||
|
||||
---@deprecated Use `vim.g.cp` instead
|
||||
function M.setup(user_config)
|
||||
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
||||
|
||||
if user_config then
|
||||
vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ local picker_utils = require('cp.pickers')
|
|||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(platform, refresh)
|
||||
local function contest_picker(platform, refresh, language)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
local fzf = require('fzf-lua')
|
||||
|
|
@ -42,19 +42,22 @@ local function contest_picker(platform, refresh)
|
|||
|
||||
if contest then
|
||||
local cp = require('cp')
|
||||
cp.handle_command({ fargs = { platform, contest.id } })
|
||||
local fargs = { platform, contest.id }
|
||||
if language then
|
||||
table.insert(fargs, '--lang')
|
||||
table.insert(fargs, language)
|
||||
end
|
||||
cp.handle_command({ fargs = fargs })
|
||||
end
|
||||
end,
|
||||
['ctrl-r'] = function()
|
||||
local cache = require('cp.cache')
|
||||
cache.clear_contest_list(platform)
|
||||
contest_picker(platform, true)
|
||||
contest_picker(platform, true, language)
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function M.pick()
|
||||
function M.pick(language)
|
||||
local fzf = require('fzf-lua')
|
||||
local platforms = picker_utils.get_platforms()
|
||||
local entries = vim.tbl_map(function(platform)
|
||||
|
|
@ -79,7 +82,7 @@ function M.pick()
|
|||
end
|
||||
|
||||
if platform then
|
||||
contest_picker(platform.id)
|
||||
contest_picker(platform.id, false, language)
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ local picker_utils = require('cp.pickers')
|
|||
|
||||
local M = {}
|
||||
|
||||
local function contest_picker(opts, platform, refresh)
|
||||
local function contest_picker(opts, platform, refresh, language)
|
||||
local constants = require('cp.constants')
|
||||
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
local contests = picker_utils.get_platform_contests(platform, refresh)
|
||||
|
|
@ -43,13 +43,18 @@ local function contest_picker(opts, platform, refresh)
|
|||
|
||||
if selection then
|
||||
local cp = require('cp')
|
||||
cp.handle_command({ fargs = { platform, selection.value.id } })
|
||||
local fargs = { platform, selection.value.id }
|
||||
if language then
|
||||
table.insert(fargs, '--lang')
|
||||
table.insert(fargs, language)
|
||||
end
|
||||
cp.handle_command({ fargs = fargs })
|
||||
end
|
||||
end)
|
||||
|
||||
map('i', '<c-r>', function()
|
||||
actions.close(prompt_bufnr)
|
||||
contest_picker(opts, platform, true)
|
||||
contest_picker(opts, platform, true, language)
|
||||
end)
|
||||
|
||||
return true
|
||||
|
|
@ -58,9 +63,8 @@ local function contest_picker(opts, platform, refresh)
|
|||
:find()
|
||||
end
|
||||
|
||||
function M.pick(opts)
|
||||
opts = opts or {}
|
||||
|
||||
function M.pick(language)
|
||||
local opts = {}
|
||||
local platforms = picker_utils.get_platforms()
|
||||
|
||||
pickers
|
||||
|
|
@ -83,7 +87,7 @@ function M.pick(opts)
|
|||
actions.close(prompt_bufnr)
|
||||
|
||||
if selection then
|
||||
contest_picker(opts, selection.value.id)
|
||||
contest_picker(opts, selection.value.id, false, language)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ function M.restore_from_current_file()
|
|||
end
|
||||
|
||||
local setup = require('cp.setup')
|
||||
setup.set_platform(file_state.platform)
|
||||
state.set_contest_id(file_state.contest_id)
|
||||
state.set_problem_id(file_state.problem_id)
|
||||
setup.setup_contest(
|
||||
file_state.platform,
|
||||
|
|
|
|||
|
|
@ -39,24 +39,28 @@ end
|
|||
|
||||
---@param compile_cmd string[]
|
||||
---@param substitutions SubstitutableCommand
|
||||
function M.compile(compile_cmd, substitutions)
|
||||
---@param on_complete fun(r: {code: integer, stdout: string})
|
||||
function M.compile(compile_cmd, substitutions, on_complete)
|
||||
local cmd = substitute_template(compile_cmd, substitutions)
|
||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||
logger.log('compile: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
local r = vim.system({ 'sh', '-c', sh }, { text = false }):wait()
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
local ansi = require('cp.ui.ansi')
|
||||
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
||||
|
||||
local ansi = require('cp.ui.ansi')
|
||||
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
||||
if r.code == 0 then
|
||||
logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO)
|
||||
else
|
||||
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
||||
end
|
||||
|
||||
if r.code == 0 then
|
||||
logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO)
|
||||
else
|
||||
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
||||
end
|
||||
|
||||
return r
|
||||
vim.schedule(function()
|
||||
on_complete(r)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function parse_and_strip_time_v(output)
|
||||
|
|
@ -73,13 +77,19 @@ local function parse_and_strip_time_v(output)
|
|||
return s, 0
|
||||
end
|
||||
|
||||
local k = last_i - 1
|
||||
while k >= 1 do
|
||||
local ch = s:sub(k, k)
|
||||
if ch ~= ' ' and ch ~= '\t' then
|
||||
break
|
||||
local tab_before_marker = s:find('\t[^\t]*Command being timed:', 1)
|
||||
local k
|
||||
if tab_before_marker then
|
||||
k = tab_before_marker - 1
|
||||
else
|
||||
k = last_i - 1
|
||||
while k >= 1 do
|
||||
local ch = s:sub(k, k)
|
||||
if ch == '\n' then
|
||||
break
|
||||
end
|
||||
k = k - 1
|
||||
end
|
||||
k = k - 1
|
||||
end
|
||||
|
||||
local head = s:sub(1, k)
|
||||
|
|
@ -97,7 +107,8 @@ local function parse_and_strip_time_v(output)
|
|||
return head, peak_mb
|
||||
end
|
||||
|
||||
function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||
---@param on_complete fun(result: ExecuteResult)
|
||||
function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete)
|
||||
local time_bin = utils.time_path()
|
||||
local timeout_bin = utils.timeout_path()
|
||||
|
||||
|
|
@ -109,76 +120,94 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
|||
local sec = math.ceil(timeout_ms / 1000)
|
||||
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||
logger.log('run: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
local r = vim
|
||||
.system({ 'sh', '-c', sh }, {
|
||||
stdin = stdin,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||
|
||||
local code = r.code or 0
|
||||
local raw = r.stdout or ''
|
||||
local cleaned, peak_mb = parse_and_strip_time_v(raw)
|
||||
local tled = code == 124
|
||||
local code = r.code or 0
|
||||
local raw = r.stdout or ''
|
||||
local cleaned, peak_mb = parse_and_strip_time_v(raw)
|
||||
local tled = code == 124
|
||||
|
||||
local signal = nil
|
||||
if code >= 128 then
|
||||
signal = constants.signal_codes[code]
|
||||
end
|
||||
local signal = nil
|
||||
if code >= 128 then
|
||||
signal = constants.signal_codes[code]
|
||||
end
|
||||
|
||||
local lower = (cleaned or ''):lower()
|
||||
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
||||
or lower:find('cannot allocate memory', 1, true)
|
||||
or lower:find('out of memory', 1, true)
|
||||
or lower:find('oom', 1, true)
|
||||
or lower:find('enomem', 1, true)
|
||||
local near_cap = peak_mb >= (0.90 * memory_mb)
|
||||
local lower = (cleaned or ''):lower()
|
||||
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
||||
or lower:find('cannot allocate memory', 1, true)
|
||||
or lower:find('out of memory', 1, true)
|
||||
or lower:find('oom', 1, true)
|
||||
or lower:find('enomem', 1, true)
|
||||
local near_cap = peak_mb >= (0.90 * memory_mb)
|
||||
|
||||
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled)
|
||||
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint ~= nil and not tled)
|
||||
|
||||
if tled then
|
||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||
elseif mled then
|
||||
logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt))
|
||||
elseif code ~= 0 then
|
||||
logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code))
|
||||
else
|
||||
logger.log(('Execution successful in %.1fms.'):format(dt))
|
||||
end
|
||||
if tled then
|
||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||
elseif mled then
|
||||
logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt))
|
||||
elseif code ~= 0 then
|
||||
logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code))
|
||||
else
|
||||
logger.log(('Execution successful in %.1fms.'):format(dt))
|
||||
end
|
||||
|
||||
return {
|
||||
stdout = cleaned,
|
||||
code = code,
|
||||
time_ms = dt,
|
||||
tled = tled,
|
||||
mled = mled,
|
||||
peak_mb = peak_mb,
|
||||
signal = signal,
|
||||
}
|
||||
vim.schedule(function()
|
||||
on_complete({
|
||||
stdout = cleaned,
|
||||
code = code,
|
||||
time_ms = dt,
|
||||
tled = tled,
|
||||
mled = mled,
|
||||
peak_mb = peak_mb,
|
||||
signal = signal,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.compile_problem()
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: {success: boolean, output: string?})
|
||||
function M.compile_problem(debug, on_complete)
|
||||
local state = require('cp.state')
|
||||
local config = require('cp.config').get_config()
|
||||
local platform = state.get_platform() or ''
|
||||
local language = config.platforms[platform].default_language
|
||||
local platform = state.get_platform()
|
||||
local language = state.get_language() or config.platforms[platform].default_language
|
||||
local eff = config.runtime.effective[platform][language]
|
||||
local compile_config = eff and eff.commands and eff.commands.build
|
||||
|
||||
local source_file = state.get_source_file()
|
||||
if source_file then
|
||||
local buf = vim.fn.bufnr(source_file)
|
||||
if buf ~= -1 and vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].modified then
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.cmd.write({ mods = { silent = true, noautocmd = true } })
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local compile_config = (debug and eff.commands.debug) or eff.commands.build
|
||||
|
||||
if not compile_config then
|
||||
return { success = true, output = nil }
|
||||
on_complete({ success = true, output = nil })
|
||||
return
|
||||
end
|
||||
|
||||
local substitutions = { source = state.get_source_file(), binary = state.get_binary_file() }
|
||||
local r = M.compile(compile_config, substitutions)
|
||||
require('cp.utils').ensure_dirs()
|
||||
|
||||
if r.code ~= 0 then
|
||||
return { success = false, output = r.stdout or 'unknown error' }
|
||||
end
|
||||
return { success = true, output = nil }
|
||||
local binary = debug and state.get_debug_file() or state.get_binary_file()
|
||||
local substitutions = { source = state.get_source_file(), binary = binary }
|
||||
|
||||
M.compile(compile_config, substitutions, function(r)
|
||||
if r.code ~= 0 then
|
||||
on_complete({ success = false, output = r.stdout or 'unknown error' })
|
||||
else
|
||||
on_complete({ success = true, output = nil })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
---@field timeout_ms number
|
||||
---@field memory_mb number
|
||||
|
||||
---@class RunPanelState
|
||||
---@class PanelState
|
||||
---@field test_cases RanTestCase[]
|
||||
---@field current_index number
|
||||
---@field buffer number?
|
||||
|
|
@ -37,8 +37,8 @@ local execute = require('cp.runner.execute')
|
|||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
---@type RunPanelState
|
||||
local run_panel_state = {
|
||||
---@type PanelState
|
||||
local panel_state = {
|
||||
test_cases = {},
|
||||
current_index = 1,
|
||||
buffer = nil,
|
||||
|
|
@ -100,81 +100,82 @@ local function build_command(cmd, substitutions)
|
|||
end
|
||||
|
||||
---@param test_case RanTestCase
|
||||
---@return { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number }
|
||||
local function run_single_test_case(test_case)
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string?, tled: boolean, mled: boolean, rss_mb: number })
|
||||
local function run_single_test_case(test_case, debug, on_complete)
|
||||
local source_file = state.get_source_file()
|
||||
|
||||
local binary_file = state.get_binary_file()
|
||||
local binary_file = debug and state.get_debug_file() or state.get_binary_file()
|
||||
local substitutions = { source = source_file, binary = binary_file }
|
||||
|
||||
local platform_config = config.platforms[state.get_platform() or '']
|
||||
local language = platform_config.default_language
|
||||
local language = state.get_language() or platform_config.default_language
|
||||
local eff = config.runtime.effective[state.get_platform() or ''][language]
|
||||
local run_template = eff and eff.commands and eff.commands.run or {}
|
||||
local cmd = build_command(run_template, substitutions)
|
||||
local stdin_content = (test_case.input or '') .. '\n'
|
||||
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0
|
||||
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or 0
|
||||
local timeout_ms = (panel_state.constraints and panel_state.constraints.timeout_ms) or 0
|
||||
local memory_mb = panel_state.constraints and panel_state.constraints.memory_mb or 0
|
||||
|
||||
local r = execute.run(cmd, stdin_content, timeout_ms, memory_mb)
|
||||
execute.run(cmd, stdin_content, timeout_ms, memory_mb, function(r)
|
||||
local ansi = require('cp.ui.ansi')
|
||||
local out = r.stdout or ''
|
||||
local highlights = {}
|
||||
if out ~= '' then
|
||||
if config.ui.ansi then
|
||||
local parsed = ansi.parse_ansi_text(out)
|
||||
out = table.concat(parsed.lines, '\n')
|
||||
highlights = parsed.highlights
|
||||
else
|
||||
out = out:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
end
|
||||
end
|
||||
|
||||
local ansi = require('cp.ui.ansi')
|
||||
local out = r.stdout or ''
|
||||
local highlights = {}
|
||||
if out ~= '' then
|
||||
if config.ui.run_panel.ansi then
|
||||
local parsed = ansi.parse_ansi_text(out)
|
||||
out = table.concat(parsed.lines, '\n')
|
||||
highlights = parsed.highlights
|
||||
local max_lines = config.ui.panel.max_output_lines
|
||||
local lines = vim.split(out, '\n')
|
||||
if #lines > max_lines then
|
||||
local trimmed = {}
|
||||
for i = 1, max_lines do
|
||||
table.insert(trimmed, lines[i])
|
||||
end
|
||||
table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines))
|
||||
out = table.concat(trimmed, '\n')
|
||||
end
|
||||
|
||||
local expected = test_case.expected or ''
|
||||
local ok = normalize_lines(out) == normalize_lines(expected)
|
||||
|
||||
local signal = r.signal
|
||||
if not signal and r.code and r.code >= 128 then
|
||||
signal = constants.signal_codes[r.code]
|
||||
end
|
||||
|
||||
local status
|
||||
if r.tled then
|
||||
status = 'tle'
|
||||
elseif r.mled then
|
||||
status = 'mle'
|
||||
elseif ok then
|
||||
status = 'pass'
|
||||
else
|
||||
out = out:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
status = 'fail'
|
||||
end
|
||||
end
|
||||
|
||||
local max_lines = config.ui.run_panel.max_output_lines
|
||||
local lines = vim.split(out, '\n')
|
||||
if #lines > max_lines then
|
||||
local trimmed = {}
|
||||
for i = 1, max_lines do
|
||||
table.insert(trimmed, lines[i])
|
||||
end
|
||||
table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines))
|
||||
out = table.concat(trimmed, '\n')
|
||||
end
|
||||
|
||||
local expected = test_case.expected or ''
|
||||
local ok = normalize_lines(out) == normalize_lines(expected)
|
||||
|
||||
local signal = r.signal
|
||||
if not signal and r.code and r.code >= 128 then
|
||||
signal = constants.signal_codes[r.code]
|
||||
end
|
||||
|
||||
local status
|
||||
if r.tled then
|
||||
status = 'tle'
|
||||
elseif r.mled then
|
||||
status = 'mle'
|
||||
elseif ok then
|
||||
status = 'pass'
|
||||
else
|
||||
status = 'fail'
|
||||
end
|
||||
|
||||
return {
|
||||
status = status,
|
||||
actual = out,
|
||||
actual_highlights = highlights,
|
||||
error = (r.code ~= 0 and not ok) and out or '',
|
||||
stderr = '',
|
||||
time_ms = r.time_ms,
|
||||
code = r.code,
|
||||
ok = ok,
|
||||
signal = signal,
|
||||
tled = r.tled or false,
|
||||
mled = r.mled or false,
|
||||
rss_mb = r.peak_mb or 0,
|
||||
}
|
||||
on_complete({
|
||||
status = status,
|
||||
actual = out,
|
||||
actual_highlights = highlights,
|
||||
error = (r.code ~= 0 and not ok) and out or '',
|
||||
stderr = '',
|
||||
time_ms = r.time_ms,
|
||||
code = r.code,
|
||||
ok = ok,
|
||||
signal = signal,
|
||||
tled = r.tled or false,
|
||||
mled = r.mled or false,
|
||||
rss_mb = r.peak_mb or 0,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
|
|
@ -185,9 +186,9 @@ function M.load_test_cases()
|
|||
state.get_problem_id()
|
||||
)
|
||||
|
||||
run_panel_state.test_cases = create_sentinal_panel_data(tcs)
|
||||
run_panel_state.current_index = 1
|
||||
run_panel_state.constraints = load_constraints_from_cache(
|
||||
panel_state.test_cases = create_sentinal_panel_data(tcs)
|
||||
panel_state.current_index = 1
|
||||
panel_state.constraints = load_constraints_from_cache(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
|
|
@ -197,46 +198,109 @@ function M.load_test_cases()
|
|||
return #tcs > 0
|
||||
end
|
||||
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(result: RanTestCase?)
|
||||
function M.run_combined_test(debug, on_complete)
|
||||
local combined = cache.get_combined_test(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id()
|
||||
)
|
||||
|
||||
if not combined then
|
||||
logger.log('No combined test found', vim.log.levels.ERROR)
|
||||
on_complete(nil)
|
||||
return
|
||||
end
|
||||
|
||||
local ran_test = {
|
||||
index = 1,
|
||||
input = combined.input,
|
||||
expected = combined.expected,
|
||||
status = 'running',
|
||||
actual = nil,
|
||||
time_ms = nil,
|
||||
code = nil,
|
||||
ok = nil,
|
||||
signal = nil,
|
||||
tled = false,
|
||||
mled = false,
|
||||
rss_mb = 0,
|
||||
selected = true,
|
||||
}
|
||||
|
||||
run_single_test_case(ran_test, debug, function(result)
|
||||
on_complete(result)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param index number
|
||||
---@return boolean
|
||||
function M.run_test_case(index)
|
||||
local tc = run_panel_state.test_cases[index]
|
||||
---@param debug boolean?
|
||||
---@param on_complete fun(success: boolean)
|
||||
function M.run_test_case(index, debug, on_complete)
|
||||
local tc = panel_state.test_cases[index]
|
||||
if not tc then
|
||||
return false
|
||||
on_complete(false)
|
||||
return
|
||||
end
|
||||
|
||||
tc.status = 'running'
|
||||
local r = run_single_test_case(tc)
|
||||
run_single_test_case(tc, debug, function(r)
|
||||
tc.status = r.status
|
||||
tc.actual = r.actual
|
||||
tc.actual_highlights = r.actual_highlights
|
||||
tc.error = r.error
|
||||
tc.stderr = r.stderr
|
||||
tc.time_ms = r.time_ms
|
||||
tc.code = r.code
|
||||
tc.ok = r.ok
|
||||
tc.signal = r.signal
|
||||
tc.tled = r.tled
|
||||
tc.mled = r.mled
|
||||
tc.rss_mb = r.rss_mb
|
||||
|
||||
tc.status = r.status
|
||||
tc.actual = r.actual
|
||||
tc.actual_highlights = r.actual_highlights
|
||||
tc.error = r.error
|
||||
tc.stderr = r.stderr
|
||||
tc.time_ms = r.time_ms
|
||||
tc.code = r.code
|
||||
tc.ok = r.ok
|
||||
tc.signal = r.signal
|
||||
tc.tled = r.tled
|
||||
tc.mled = r.mled
|
||||
tc.rss_mb = r.rss_mb
|
||||
|
||||
return true
|
||||
on_complete(true)
|
||||
end)
|
||||
end
|
||||
|
||||
---@return RanTestCase[]
|
||||
function M.run_all_test_cases()
|
||||
local results = {}
|
||||
for i = 1, #run_panel_state.test_cases do
|
||||
M.run_test_case(i)
|
||||
results[i] = run_panel_state.test_cases[i]
|
||||
---@param indices? integer[]
|
||||
---@param debug boolean?
|
||||
---@param on_each? fun(index: integer, total: integer)
|
||||
---@param on_done fun(results: RanTestCase[])
|
||||
function M.run_all_test_cases(indices, debug, on_each, on_done)
|
||||
local to_run = indices
|
||||
if not to_run then
|
||||
to_run = {}
|
||||
for i = 1, #panel_state.test_cases do
|
||||
to_run[i] = i
|
||||
end
|
||||
end
|
||||
return results
|
||||
|
||||
local function run_next(pos)
|
||||
if pos > #to_run then
|
||||
logger.log(
|
||||
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
)
|
||||
on_done(panel_state.test_cases)
|
||||
return
|
||||
end
|
||||
|
||||
M.run_test_case(to_run[pos], debug, function()
|
||||
if on_each then
|
||||
on_each(pos, #to_run)
|
||||
end
|
||||
run_next(pos + 1)
|
||||
end)
|
||||
end
|
||||
|
||||
run_next(1)
|
||||
end
|
||||
|
||||
---@return RunPanelState
|
||||
function M.get_run_panel_state()
|
||||
return run_panel_state
|
||||
---@return PanelState
|
||||
function M.get_panel_state()
|
||||
return panel_state
|
||||
end
|
||||
|
||||
---@param output string|nil
|
||||
|
|
@ -247,7 +311,7 @@ function M.handle_compilation_failure(output)
|
|||
local txt
|
||||
local hl = {}
|
||||
|
||||
if config.ui.run_panel.ansi then
|
||||
if config.ui.ansi then
|
||||
local p = ansi.parse_ansi_text(output or '')
|
||||
txt = table.concat(p.lines, '\n')
|
||||
hl = p.highlights
|
||||
|
|
@ -255,7 +319,7 @@ function M.handle_compilation_failure(output)
|
|||
txt = (output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
end
|
||||
|
||||
for _, tc in ipairs(run_panel_state.test_cases) do
|
||||
for _, tc in ipairs(panel_state.test_cases) do
|
||||
tc.status = 'fail'
|
||||
tc.actual = txt
|
||||
tc.actual_highlights = hl
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
local M = {}
|
||||
|
||||
local function strwidth(s)
|
||||
return vim.api.nvim_strwidth(s)
|
||||
end
|
||||
|
||||
local exit_code_names = {
|
||||
[128] = 'SIGHUP',
|
||||
[129] = 'SIGINT',
|
||||
|
|
@ -26,6 +30,12 @@ local exit_code_names = {
|
|||
---@param ran_test_case RanTestCase
|
||||
---@return StatusInfo
|
||||
function M.get_status_info(ran_test_case)
|
||||
if ran_test_case.status == 'pending' then
|
||||
return { text = '...', highlight_group = 'CpTestNA' }
|
||||
elseif ran_test_case.status == 'running' then
|
||||
return { text = 'RUN', highlight_group = 'CpTestNA' }
|
||||
end
|
||||
|
||||
if ran_test_case.ok then
|
||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||
end
|
||||
|
|
@ -34,7 +44,7 @@ function M.get_status_info(ran_test_case)
|
|||
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||
elseif ran_test_case.mled then
|
||||
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
||||
elseif ran_test_case.code > 0 and ran_test_case.code >= 128 then
|
||||
elseif ran_test_case.code and ran_test_case.code >= 128 then
|
||||
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
||||
elseif ran_test_case.code == 0 and not ran_test_case.ok then
|
||||
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||
|
|
@ -63,24 +73,24 @@ local function compute_cols(test_state)
|
|||
|
||||
for i, tc in ipairs(test_state.test_cases) do
|
||||
local prefix = (i == test_state.current_index) and '>' or ' '
|
||||
w.num = math.max(w.num, #(' ' .. prefix .. i .. ' '))
|
||||
w.status = math.max(w.status, #(' ' .. M.get_status_info(tc).text .. ' '))
|
||||
w.num = math.max(w.num, strwidth(' ' .. prefix .. i .. ' '))
|
||||
w.status = math.max(w.status, strwidth(' ' .. M.get_status_info(tc).text .. ' '))
|
||||
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or '—'
|
||||
w.time = math.max(w.time, #(' ' .. time_str .. ' '))
|
||||
w.timeout = math.max(w.timeout, #(' ' .. timeout_str .. ' '))
|
||||
w.time = math.max(w.time, strwidth(' ' .. time_str .. ' '))
|
||||
w.timeout = math.max(w.timeout, strwidth(' ' .. timeout_str .. ' '))
|
||||
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or '—'
|
||||
w.rss = math.max(w.rss, #(' ' .. rss_str .. ' '))
|
||||
w.memory = math.max(w.memory, #(' ' .. memory_str .. ' '))
|
||||
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code) .. ' '))
|
||||
w.rss = math.max(w.rss, strwidth(' ' .. rss_str .. ' '))
|
||||
w.memory = math.max(w.memory, strwidth(' ' .. memory_str .. ' '))
|
||||
w.exit = math.max(w.exit, strwidth(' ' .. format_exit_code(tc.code) .. ' '))
|
||||
end
|
||||
|
||||
w.num = math.max(w.num, #' # ')
|
||||
w.status = math.max(w.status, #' Status ')
|
||||
w.time = math.max(w.time, #' Runtime (ms) ')
|
||||
w.timeout = math.max(w.timeout, #' Time (ms) ')
|
||||
w.rss = math.max(w.rss, #' RSS (MB) ')
|
||||
w.memory = math.max(w.memory, #' Mem (MB) ')
|
||||
w.exit = math.max(w.exit, #' Exit Code ')
|
||||
w.num = math.max(w.num, strwidth(' # '))
|
||||
w.status = math.max(w.status, strwidth(' Status '))
|
||||
w.time = math.max(w.time, strwidth(' Runtime (ms) '))
|
||||
w.timeout = math.max(w.timeout, strwidth(' Time (ms) '))
|
||||
w.rss = math.max(w.rss, strwidth(' RSS (MB) '))
|
||||
w.memory = math.max(w.memory, strwidth(' Mem (MB) '))
|
||||
w.exit = math.max(w.exit, strwidth(' Exit Code '))
|
||||
|
||||
local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
|
||||
local inner = sum + 6
|
||||
|
|
@ -89,7 +99,7 @@ local function compute_cols(test_state)
|
|||
end
|
||||
|
||||
local function center(text, width)
|
||||
local pad = width - #text
|
||||
local pad = width - strwidth(text)
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
|
|
@ -101,7 +111,7 @@ local function format_num_column(prefix, idx, width)
|
|||
local num_str = tostring(idx)
|
||||
local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
|
||||
or (' ' .. prefix .. num_str .. ' ')
|
||||
local total_pad = width - #content
|
||||
local total_pad = width - strwidth(content)
|
||||
if total_pad <= 0 then
|
||||
return content
|
||||
end
|
||||
|
|
@ -275,11 +285,14 @@ local function data_row(c, idx, tc, is_current, test_state)
|
|||
return line, hi
|
||||
end
|
||||
|
||||
---@param test_state RunPanelState
|
||||
---@return string[], Highlight[] lines and highlight positions
|
||||
---@param test_state PanelState
|
||||
---@return string[] lines
|
||||
---@return Highlight[] highlights
|
||||
---@return integer current_test_line
|
||||
function M.render_test_list(test_state)
|
||||
local lines, highlights = {}, {}
|
||||
local c = compute_cols(test_state)
|
||||
local current_test_line = nil
|
||||
|
||||
table.insert(lines, top_border(c))
|
||||
table.insert(lines, header_line(c))
|
||||
|
|
@ -289,6 +302,11 @@ function M.render_test_list(test_state)
|
|||
local is_current = (i == test_state.current_index)
|
||||
local row, hi = data_row(c, i, tc, is_current, test_state)
|
||||
table.insert(lines, row)
|
||||
|
||||
if is_current then
|
||||
current_test_line = #lines
|
||||
end
|
||||
|
||||
if hi then
|
||||
hi.line = #lines - 1
|
||||
table.insert(highlights, hi)
|
||||
|
|
@ -306,10 +324,10 @@ function M.render_test_list(test_state)
|
|||
|
||||
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
|
||||
local s = input_line or ''
|
||||
if #s > c.inner then
|
||||
if strwidth(s) > c.inner then
|
||||
s = string.sub(s, 1, c.inner)
|
||||
end
|
||||
local pad = c.inner - #s
|
||||
local pad = c.inner - strwidth(s)
|
||||
table.insert(lines, '│' .. s .. string.rep(' ', pad) .. '│')
|
||||
end
|
||||
|
||||
|
|
@ -327,7 +345,7 @@ function M.render_test_list(test_state)
|
|||
end
|
||||
end
|
||||
|
||||
return lines, highlights
|
||||
return lines, highlights, current_test_line or 1
|
||||
end
|
||||
|
||||
---@param ran_test_case RanTestCase?
|
||||
|
|
@ -349,14 +367,12 @@ end
|
|||
---@return table<string, table>
|
||||
function M.get_highlight_groups()
|
||||
return {
|
||||
CpTestAC = { fg = '#10b981' },
|
||||
CpTestWA = { fg = '#ef4444' },
|
||||
CpTestTLE = { fg = '#f59e0b' },
|
||||
CpTestMLE = { fg = '#f59e0b' },
|
||||
CpTestRTE = { fg = '#8b5cf6' },
|
||||
CpTestNA = { fg = '#6b7280' },
|
||||
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
||||
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
||||
CpTestAC = { link = 'DiagnosticOk' },
|
||||
CpTestWA = { link = 'DiagnosticError' },
|
||||
CpTestTLE = { link = 'DiagnosticWarn' },
|
||||
CpTestMLE = { link = 'DiagnosticWarn' },
|
||||
CpTestRTE = { link = 'DiagnosticHint' },
|
||||
CpTestNA = { link = 'Comment' },
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
local M = {}
|
||||
|
||||
local constants = require('cp.log')
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
|
|
@ -25,10 +25,27 @@ end
|
|||
---@param args string[]
|
||||
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
|
||||
local function run_scraper(platform, subcommand, args, opts)
|
||||
if not utils.setup_python_env() then
|
||||
local msg = 'no Python environment available (install uv or nix)'
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
if opts and opts.on_exit then
|
||||
opts.on_exit({ success = false, error = msg })
|
||||
end
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. platform, subcommand }
|
||||
local cmd = utils.get_python_cmd(platform, plugin_path)
|
||||
vim.list_extend(cmd, { subcommand })
|
||||
vim.list_extend(cmd, args)
|
||||
|
||||
logger.log('scraper cmd: ' .. table.concat(cmd, ' '))
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
env.CONDA_PREFIX = ''
|
||||
|
||||
if opts and opts.ndjson then
|
||||
local uv = vim.loop
|
||||
local stdout = uv.new_pipe(false)
|
||||
|
|
@ -36,31 +53,32 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
local buf = ''
|
||||
|
||||
local handle
|
||||
handle = uv.spawn(
|
||||
cmd[1],
|
||||
{ args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr } },
|
||||
function(code, signal)
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
if not stderr:is_closing() then
|
||||
stderr:close()
|
||||
end
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
handle = uv.spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { nil, stdout, stderr },
|
||||
env = env,
|
||||
cwd = plugin_path,
|
||||
}, function(code, signal)
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
)
|
||||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
if not stderr:is_closing() then
|
||||
stderr:close()
|
||||
end
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||
|
|
@ -97,7 +115,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
return
|
||||
end
|
||||
|
||||
local sysopts = { text = true, timeout = 30000 }
|
||||
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
|
||||
if opts and opts.sync then
|
||||
local result = vim.system(cmd, sysopts):wait()
|
||||
return syshandle(result)
|
||||
|
|
@ -168,7 +186,11 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
end
|
||||
if ev.error and ev.problem_id then
|
||||
logger.log(
|
||||
("Failed to load tests for problem '%s': %s"):format(contest_id, ev.problem_id, ev.error),
|
||||
("Failed to load tests for problem '%s' in contest '%s': %s"):format(
|
||||
ev.problem_id,
|
||||
contest_id,
|
||||
ev.error
|
||||
),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
|
|
@ -177,7 +199,7 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||
require('cp.utils').ensure_dirs()
|
||||
local config = require('cp.config')
|
||||
local base_name = config.default_filename(contest_id, ev.problem_id)
|
||||
for i, t in ipairs(ev.tests) do
|
||||
|
|
@ -185,15 +207,17 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
local expected_file = 'io/' .. base_name .. '.' .. i .. '.cpout'
|
||||
local input_content = t.input:gsub('\r', '')
|
||||
local expected_content = t.expected:gsub('\r', '')
|
||||
vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file)
|
||||
vim.fn.writefile(vim.split(input_content, '\n'), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n'), expected_file)
|
||||
end
|
||||
if type(callback) == 'function' then
|
||||
callback({
|
||||
combined = ev.combined,
|
||||
tests = ev.tests,
|
||||
timeout_ms = ev.timeout_ms or 0,
|
||||
memory_mb = ev.memory_mb or 0,
|
||||
interactive = ev.interactive or false,
|
||||
multi_test = ev.multi_test or false,
|
||||
problem_id = ev.problem_id,
|
||||
})
|
||||
end
|
||||
|
|
|
|||
370
lua/cp/setup.lua
370
lua/cp/setup.lua
|
|
@ -2,20 +2,50 @@ local M = {}
|
|||
|
||||
local cache = require('cp.cache')
|
||||
local config_module = require('cp.config')
|
||||
local constants = require('cp.constants')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
local scraper = require('cp.scraper')
|
||||
local state = require('cp.state')
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
|
||||
function M.set_platform(platform)
|
||||
if not vim.tbl_contains(platforms, platform) then
|
||||
logger.log(("Unknown platform '%s'"):format(platform), vim.log.levels.ERROR)
|
||||
return false
|
||||
---Get the language of the current file from cache
|
||||
---@return string?
|
||||
local function get_current_file_language()
|
||||
local current_file = vim.fn.expand('%:p')
|
||||
if current_file == '' then
|
||||
return nil
|
||||
end
|
||||
state.set_platform(platform)
|
||||
return true
|
||||
cache.load()
|
||||
local file_state = cache.get_file_state(current_file)
|
||||
return file_state and file_state.language or nil
|
||||
end
|
||||
|
||||
---Check if a problem file exists for any enabled language
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string
|
||||
---@return string?
|
||||
local function get_existing_problem_language(platform, contest_id, problem_id)
|
||||
local config = config_module.get_config()
|
||||
local platform_config = config.platforms[platform]
|
||||
if not platform_config then
|
||||
return nil
|
||||
end
|
||||
|
||||
for _, lang_id in ipairs(platform_config.enabled_languages) do
|
||||
local effective = config.runtime.effective[platform][lang_id]
|
||||
if effective and effective.extension then
|
||||
local basename = config.filename
|
||||
and config.filename(platform, contest_id, problem_id, config, lang_id)
|
||||
or config_module.default_filename(contest_id, problem_id)
|
||||
local filepath = basename .. '.' .. effective.extension
|
||||
if vim.fn.filereadable(filepath) == 1 then
|
||||
return lang_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
---@class TestCaseLite
|
||||
|
|
@ -33,59 +63,146 @@ end
|
|||
---@field succeeded integer|nil
|
||||
---@field failed integer|nil
|
||||
|
||||
---@param cd table|nil
|
||||
---@return boolean
|
||||
local function is_metadata_ready(cd)
|
||||
return cd
|
||||
and type(cd.problems) == 'table'
|
||||
and #cd.problems > 0
|
||||
and type(cd.index_map) == 'table'
|
||||
and next(cd.index_map) ~= nil
|
||||
or false
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id string|nil
|
||||
---@param language? string|nil
|
||||
---@param problems table
|
||||
local function start_tests(platform, contest_id, problems)
|
||||
local cached_len = #vim.tbl_filter(function(p)
|
||||
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
|
||||
end, problems)
|
||||
if cached_len ~= #problems then
|
||||
logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems))
|
||||
scraper.scrape_all_tests(platform, contest_id, function(ev)
|
||||
local cached_tests = {}
|
||||
if not ev.interactive and vim.tbl_isempty(ev.tests) then
|
||||
logger.log(("No tests found for problem '%s'."):format(ev.problem_id), vim.log.levels.WARN)
|
||||
end
|
||||
for i, t in ipairs(ev.tests) do
|
||||
cached_tests[i] = { index = i, input = t.input, expected = t.expected }
|
||||
end
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
ev.problem_id,
|
||||
ev.combined,
|
||||
cached_tests,
|
||||
ev.timeout_ms or 0,
|
||||
ev.memory_mb or 0,
|
||||
ev.interactive,
|
||||
ev.multi_test
|
||||
)
|
||||
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state then
|
||||
local combined_test = cache.get_combined_test(platform, contest_id, state.get_problem_id())
|
||||
if combined_test then
|
||||
local input_lines = vim.split(combined_test.input, '\n')
|
||||
require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param language? string
|
||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||
local old_platform, old_contest_id = state.get_platform(), state.get_contest_id()
|
||||
|
||||
state.set_platform(platform)
|
||||
state.set_contest_id(contest_id)
|
||||
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local is_new_contest = old_platform ~= platform and old_contest_id ~= contest_id
|
||||
|
||||
cache.load()
|
||||
|
||||
local function proceed(contest_data)
|
||||
local problems = contest_data.problems
|
||||
local pid = problems[(problem_id and contest_data.index_map[problem_id] or 1)].id
|
||||
local pid = problem_id and problem_id or problems[1].id
|
||||
M.setup_problem(pid, language)
|
||||
start_tests(platform, contest_id, problems)
|
||||
|
||||
local cached_len = #vim.tbl_filter(function(p)
|
||||
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
|
||||
end, problems)
|
||||
|
||||
if cached_len ~= #problems then
|
||||
logger.log(('Fetching test cases...'):format(cached_len, #problems))
|
||||
scraper.scrape_all_tests(platform, contest_id, function(ev)
|
||||
local cached_tests = {}
|
||||
if not ev.interactive and vim.tbl_isempty(ev.tests) then
|
||||
logger.log(
|
||||
("No tests found for problem '%s'."):format(ev.problem_id),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
for i, t in ipairs(ev.tests) do
|
||||
cached_tests[i] = { index = i, input = t.input, expected = t.expected }
|
||||
end
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
ev.problem_id,
|
||||
cached_tests,
|
||||
ev.timeout_ms or 0,
|
||||
ev.memory_mb or 0,
|
||||
ev.interactive
|
||||
)
|
||||
logger.log('Test cases loaded.')
|
||||
end)
|
||||
if config_module.get_config().open_url and is_new_contest and contest_data.url then
|
||||
vim.ui.open(contest_data.url:format(pid))
|
||||
end
|
||||
end
|
||||
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if not contest_data or not contest_data.problems then
|
||||
if not is_metadata_ready(contest_data) then
|
||||
local cfg = config_module.get_config()
|
||||
local lang = language or (cfg.platforms[platform] and cfg.platforms[platform].default_language)
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
local bufnr = vim.api.nvim_create_buf(true, false)
|
||||
vim.api.nvim_win_set_buf(0, bufnr)
|
||||
vim.bo[bufnr].filetype = lang or ''
|
||||
vim.bo[bufnr].buftype = ''
|
||||
vim.bo[bufnr].swapfile = false
|
||||
|
||||
state.set_language(lang)
|
||||
|
||||
if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||
local ok = pcall(cfg.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
end
|
||||
|
||||
state.set_provisional({
|
||||
bufnr = bufnr,
|
||||
platform = platform,
|
||||
contest_id = contest_id,
|
||||
language = lang,
|
||||
requested_problem_id = problem_id,
|
||||
token = vim.loop.hrtime(),
|
||||
})
|
||||
|
||||
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
||||
scraper.scrape_contest_metadata(platform, contest_id, function(result)
|
||||
local problems = result.problems or {}
|
||||
cache.set_contest_data(platform, contest_id, problems)
|
||||
logger.log(('Found %d problems for %s contest %s.'):format(#problems, platform, contest_id))
|
||||
proceed(cache.get_contest_data(platform, contest_id))
|
||||
end)
|
||||
scraper.scrape_contest_metadata(
|
||||
platform,
|
||||
contest_id,
|
||||
vim.schedule_wrap(function(result)
|
||||
local problems = result.problems or {}
|
||||
cache.set_contest_data(platform, contest_id, problems, result.url)
|
||||
local prov = state.get_provisional()
|
||||
if not prov or prov.platform ~= platform or prov.contest_id ~= contest_id then
|
||||
return
|
||||
end
|
||||
local cd = cache.get_contest_data(platform, contest_id)
|
||||
if not is_metadata_ready(cd) then
|
||||
return
|
||||
end
|
||||
local pid = prov.requested_problem_id
|
||||
if not pid or not cd.index_map or not cd.index_map[pid] then
|
||||
pid = cd.problems[1] and cd.problems[1].id or nil
|
||||
end
|
||||
if not pid then
|
||||
return
|
||||
end
|
||||
proceed(cd)
|
||||
end)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -97,36 +214,109 @@ end
|
|||
function M.setup_problem(problem_id, language)
|
||||
local platform = state.get_platform()
|
||||
if not platform then
|
||||
logger.log('No platform set.', vim.log.levels.ERROR)
|
||||
logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local old_problem_id = state.get_problem_id()
|
||||
state.set_problem_id(problem_id)
|
||||
|
||||
local config = config_module.get_config()
|
||||
|
||||
vim.schedule(function()
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local lang = language or config.platforms[platform].default_language
|
||||
local source_file = state.get_source_file(lang)
|
||||
vim.cmd.e(source_file)
|
||||
|
||||
if config.hooks and config.hooks.setup_code then
|
||||
config.hooks.setup_code(state)
|
||||
if old_problem_id ~= problem_id then
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state and io_state.output_buf and vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||
local utils = require('cp.utils')
|
||||
utils.update_buffer_content(io_state.output_buf, {}, nil, nil)
|
||||
end
|
||||
end
|
||||
local config = config_module.get_config()
|
||||
local lang = language
|
||||
or (config.platforms[platform] and config.platforms[platform].default_language)
|
||||
|
||||
cache.set_file_state(
|
||||
vim.fn.expand('%:p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
end)
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
state.set_language(lang)
|
||||
|
||||
local source_file = state.get_source_file(lang)
|
||||
if not source_file then
|
||||
return
|
||||
end
|
||||
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(source_file, ':h'), 'p')
|
||||
|
||||
local prov = state.get_provisional()
|
||||
if prov and prov.platform == platform and prov.contest_id == (state.get_contest_id() or '') then
|
||||
if vim.api.nvim_buf_is_valid(prov.bufnr) then
|
||||
local existing_bufnr = vim.fn.bufnr(source_file)
|
||||
if existing_bufnr ~= -1 then
|
||||
vim.api.nvim_buf_delete(prov.bufnr, { force = true })
|
||||
state.set_provisional(nil)
|
||||
else
|
||||
vim.api.nvim_buf_set_name(prov.bufnr, source_file)
|
||||
vim.bo[prov.bufnr].swapfile = true
|
||||
-- selene: allow(mixed_table)
|
||||
vim.cmd.write({
|
||||
vim.fn.fnameescape(source_file),
|
||||
bang = true,
|
||||
mods = { silent = true, noautocmd = true, keepalt = true },
|
||||
})
|
||||
state.set_solution_win(vim.api.nvim_get_current_win())
|
||||
if config.hooks and config.hooks.setup_code and not vim.b[prov.bufnr].cp_setup_done then
|
||||
local ok = pcall(config.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[prov.bufnr].cp_setup_done = true
|
||||
end
|
||||
elseif not vim.b[prov.bufnr].cp_setup_done then
|
||||
helpers.clearcol(prov.bufnr)
|
||||
vim.b[prov.bufnr].cp_setup_done = true
|
||||
end
|
||||
cache.set_file_state(
|
||||
vim.fn.fnamemodify(source_file, ':p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
require('cp.ui.views').ensure_io_view()
|
||||
state.set_provisional(nil)
|
||||
return
|
||||
end
|
||||
else
|
||||
state.set_provisional(nil)
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
vim.cmd.e(source_file)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
state.set_solution_win(vim.api.nvim_get_current_win())
|
||||
require('cp.ui.views').ensure_io_view()
|
||||
if config.hooks and config.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||
local ok = pcall(config.hooks.setup_code, state)
|
||||
if ok then
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
elseif not vim.b[bufnr].cp_setup_done then
|
||||
helpers.clearcol(bufnr)
|
||||
vim.b[bufnr].cp_setup_done = true
|
||||
end
|
||||
cache.set_file_state(
|
||||
vim.fn.expand('%:p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
end
|
||||
|
||||
function M.navigate_problem(direction)
|
||||
---@param direction integer
|
||||
---@param language? string
|
||||
function M.navigate_problem(direction, language)
|
||||
if direction == 0 then
|
||||
return
|
||||
end
|
||||
|
|
@ -135,7 +325,6 @@ function M.navigate_problem(direction)
|
|||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local current_problem_id = state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not current_problem_id then
|
||||
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
|
|
@ -143,7 +332,7 @@ function M.navigate_problem(direction)
|
|||
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if not contest_data or not contest_data.problems then
|
||||
if not is_metadata_ready(contest_data) then
|
||||
logger.log(
|
||||
('No data available for %s contest %s.'):format(
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
|
|
@ -161,8 +350,45 @@ function M.navigate_problem(direction)
|
|||
return
|
||||
end
|
||||
|
||||
require('cp.ui.panel').disable()
|
||||
M.setup_contest(platform, contest_id, problems[new_index].id)
|
||||
logger.log(('navigate_problem: %s -> %s'):format(current_problem_id, problems[new_index].id))
|
||||
|
||||
local active_panel = state.get_active_panel()
|
||||
if active_panel == 'run' then
|
||||
require('cp.ui.views').disable()
|
||||
end
|
||||
|
||||
local lang = nil
|
||||
|
||||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
if not lang_result.valid then
|
||||
logger.log(lang_result.error, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
lang = language
|
||||
else
|
||||
local existing_lang =
|
||||
get_existing_problem_language(platform, contest_id, problems[new_index].id)
|
||||
if existing_lang then
|
||||
lang = existing_lang
|
||||
else
|
||||
lang = get_current_file_language()
|
||||
if lang then
|
||||
local lang_result = config_module.get_language_for_platform(platform, lang)
|
||||
if not lang_result.valid then
|
||||
lang = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local io_state = state.get_io_view_state()
|
||||
if io_state and io_state.output_buf and vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||
local utils = require('cp.utils')
|
||||
utils.update_buffer_content(io_state.output_buf, {}, nil, nil)
|
||||
end
|
||||
|
||||
M.setup_contest(platform, contest_id, problems[new_index].id, lang)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
111
lua/cp/state.lua
111
lua/cp/state.lua
|
|
@ -1,3 +1,17 @@
|
|||
---@class cp.ProvisionalState
|
||||
---@field bufnr integer
|
||||
---@field platform string
|
||||
---@field contest_id string
|
||||
---@field language string
|
||||
---@field requested_problem_id string?
|
||||
---@field token integer
|
||||
|
||||
---@class cp.IoViewState
|
||||
---@field output_buf integer
|
||||
---@field input_buf integer
|
||||
---@field current_test_index integer?
|
||||
---@field source_buf integer?
|
||||
|
||||
---@class cp.State
|
||||
---@field get_platform fun(): string?
|
||||
---@field set_platform fun(platform: string)
|
||||
|
|
@ -5,50 +19,80 @@
|
|||
---@field set_contest_id fun(contest_id: string)
|
||||
---@field get_problem_id fun(): string?
|
||||
---@field set_problem_id fun(problem_id: string)
|
||||
---@field get_language fun(): string?
|
||||
---@field set_language fun(language: string)
|
||||
---@field get_active_panel fun(): string?
|
||||
---@field set_active_panel fun(): string?
|
||||
---@field set_active_panel fun(panel: string?)
|
||||
---@field get_base_name fun(): string?
|
||||
---@field get_source_file fun(language?: string): string?
|
||||
---@field get_binary_file fun(): string?
|
||||
---@field get_input_file fun(): string?
|
||||
---@field get_output_file fun(): string?
|
||||
---@field get_expected_file fun(): string?
|
||||
---@field get_provisional fun(): cp.ProvisionalState?
|
||||
---@field set_provisional fun(p: cp.ProvisionalState?)
|
||||
---@field get_saved_session fun(): string?
|
||||
---@field set_saved_session fun(path: string?)
|
||||
---@field get_io_view_state fun(): cp.IoViewState?
|
||||
---@field set_io_view_state fun(s: cp.IoViewState?)
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, any>
|
||||
local state = {
|
||||
platform = nil,
|
||||
contest_id = nil,
|
||||
problem_id = nil,
|
||||
language = nil,
|
||||
test_cases = nil,
|
||||
saved_session = nil,
|
||||
active_panel = nil,
|
||||
provisional = nil,
|
||||
solution_win = nil,
|
||||
io_view_state = nil,
|
||||
}
|
||||
|
||||
---@return string?
|
||||
function M.get_platform()
|
||||
return state.platform
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
function M.set_platform(platform)
|
||||
state.platform = platform
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_contest_id()
|
||||
return state.contest_id
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
function M.set_contest_id(contest_id)
|
||||
state.contest_id = contest_id
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_problem_id()
|
||||
return state.problem_id
|
||||
end
|
||||
|
||||
---@param problem_id string
|
||||
function M.set_problem_id(problem_id)
|
||||
state.problem_id = problem_id
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_language()
|
||||
return state.language
|
||||
end
|
||||
|
||||
---@param language string
|
||||
function M.set_language(language)
|
||||
state.language = language
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_base_name()
|
||||
local platform, contest_id, problem_id = M.get_platform(), M.get_contest_id(), M.get_problem_id()
|
||||
if not platform or not contest_id or not problem_id then
|
||||
|
|
@ -65,10 +109,8 @@ function M.get_base_name()
|
|||
end
|
||||
end
|
||||
|
||||
function M.get_language()
|
||||
return
|
||||
end
|
||||
|
||||
---@param language? string
|
||||
---@return string?
|
||||
function M.get_source_file(language)
|
||||
local base_name = M.get_base_name()
|
||||
if not base_name or not M.get_platform() then
|
||||
|
|
@ -81,43 +123,100 @@ function M.get_source_file(language)
|
|||
if not platform_cfg then
|
||||
return nil
|
||||
end
|
||||
local target_language = language or platform_cfg.default_language
|
||||
|
||||
local target_language = language or state.language or platform_cfg.default_language
|
||||
local eff = config.runtime.effective[plat] and config.runtime.effective[plat][target_language]
|
||||
or nil
|
||||
if not eff or not eff.extension then
|
||||
return nil
|
||||
end
|
||||
|
||||
return base_name .. '.' .. eff.extension
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_binary_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('build/%s.run'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_debug_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('build/%s.dbg'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_input_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.cpin'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_output_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.cpout'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_expected_file()
|
||||
local base_name = M.get_base_name()
|
||||
return base_name and ('io/%s.expected'):format(base_name) or nil
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_active_panel()
|
||||
return state.active_panel
|
||||
end
|
||||
|
||||
---@param panel string?
|
||||
function M.set_active_panel(panel)
|
||||
state.active_panel = panel
|
||||
end
|
||||
|
||||
---@return cp.ProvisionalState?
|
||||
function M.get_provisional()
|
||||
return state.provisional
|
||||
end
|
||||
|
||||
---@param p cp.ProvisionalState?
|
||||
function M.set_provisional(p)
|
||||
state.provisional = p
|
||||
end
|
||||
|
||||
---@return integer?
|
||||
function M.get_solution_win()
|
||||
if state.solution_win and vim.api.nvim_win_is_valid(state.solution_win) then
|
||||
return state.solution_win
|
||||
end
|
||||
return vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
---@param win integer?
|
||||
function M.set_solution_win(win)
|
||||
state.solution_win = win
|
||||
end
|
||||
|
||||
---@return cp.IoViewState?
|
||||
function M.get_io_view_state()
|
||||
return state.io_view_state
|
||||
end
|
||||
|
||||
---@param s cp.IoViewState?
|
||||
function M.set_io_view_state(s)
|
||||
state.io_view_state = s
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M.get_saved_session()
|
||||
return state.saved_session
|
||||
end
|
||||
|
||||
---@param path string?
|
||||
function M.set_saved_session(path)
|
||||
state.saved_session = path
|
||||
end
|
||||
|
||||
M._state = state
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -321,6 +321,25 @@ function M.setup_highlight_groups()
|
|||
vim.api.nvim_set_hl(0, 'CpAnsiBold', { bold = true })
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiItalic', { italic = true })
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
|
||||
|
||||
for _, combo in ipairs(combinations) do
|
||||
for color_name, _ in pairs(color_map) do
|
||||
local parts = { 'CpAnsi' }
|
||||
if combo.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if combo.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
table.insert(parts, color_name)
|
||||
local hl_name = table.concat(parts)
|
||||
dyn_hl_cache[hl_name] = true
|
||||
end
|
||||
end
|
||||
|
||||
dyn_hl_cache['CpAnsiBold'] = true
|
||||
dyn_hl_cache['CpAnsiItalic'] = true
|
||||
dyn_hl_cache['CpAnsiBoldItalic'] = true
|
||||
end
|
||||
|
||||
---@param text string
|
||||
|
|
|
|||
464
lua/cp/ui/edit.lua
Normal file
464
lua/cp/ui/edit.lua
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local config_module = require('cp.config')
|
||||
local helpers = require('cp.helpers')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
---@class TestBufferPair
|
||||
---@field input_buf integer
|
||||
---@field expected_buf integer
|
||||
---@field input_win integer
|
||||
---@field expected_win integer
|
||||
|
||||
---@class EditState
|
||||
---@field test_buffers TestBufferPair[]
|
||||
---@field test_cases TestCase[]
|
||||
---@field constraints ProblemConstraints?
|
||||
|
||||
---@type EditState?
|
||||
local edit_state = nil
|
||||
|
||||
local setup_keybindings
|
||||
|
||||
---@param bufnr integer
|
||||
---@return integer? test_index
|
||||
local function get_current_test_index(bufnr)
|
||||
if not edit_state then
|
||||
return nil
|
||||
end
|
||||
for i, pair in ipairs(edit_state.test_buffers) do
|
||||
if pair.input_buf == bufnr or pair.expected_buf == bufnr then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param index integer
|
||||
local function jump_to_test(index)
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
local pair = edit_state.test_buffers[index]
|
||||
if pair and vim.api.nvim_win_is_valid(pair.input_win) then
|
||||
vim.api.nvim_set_current_win(pair.input_win)
|
||||
end
|
||||
end
|
||||
|
||||
---@param delta integer
|
||||
local function navigate_test(delta)
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local current_index = get_current_test_index(current_buf)
|
||||
if not current_index or not edit_state then
|
||||
return
|
||||
end
|
||||
local new_index = current_index + delta
|
||||
if new_index < 1 or new_index > #edit_state.test_buffers then
|
||||
return
|
||||
end
|
||||
jump_to_test(new_index)
|
||||
end
|
||||
|
||||
---@param test_index integer
|
||||
local function load_test_into_buffer(test_index)
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local tc = edit_state.test_cases[test_index]
|
||||
local pair = edit_state.test_buffers[test_index]
|
||||
|
||||
if not tc or not pair then
|
||||
return
|
||||
end
|
||||
|
||||
local input_lines = vim.split(tc.input or '', '\n', { plain = true, trimempty = false })
|
||||
vim.api.nvim_buf_set_lines(pair.input_buf, 0, -1, false, input_lines)
|
||||
|
||||
local expected_lines = vim.split(tc.expected or '', '\n', { plain = true, trimempty = false })
|
||||
vim.api.nvim_buf_set_lines(pair.expected_buf, 0, -1, false, expected_lines)
|
||||
|
||||
vim.api.nvim_buf_set_name(pair.input_buf, string.format('cp://test-%d-input', test_index))
|
||||
vim.api.nvim_buf_set_name(pair.expected_buf, string.format('cp://test-%d-expected', test_index))
|
||||
end
|
||||
|
||||
local function delete_current_test()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
if #edit_state.test_buffers == 1 then
|
||||
logger.log('Problems must have at least one test case.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local current_index = get_current_test_index(current_buf)
|
||||
if not current_index then
|
||||
return
|
||||
end
|
||||
|
||||
local pair = edit_state.test_buffers[current_index]
|
||||
if vim.api.nvim_win_is_valid(pair.input_win) then
|
||||
vim.api.nvim_win_close(pair.input_win, true)
|
||||
end
|
||||
if vim.api.nvim_win_is_valid(pair.expected_win) then
|
||||
vim.api.nvim_win_close(pair.expected_win, true)
|
||||
end
|
||||
if vim.api.nvim_buf_is_valid(pair.input_buf) then
|
||||
vim.api.nvim_buf_delete(pair.input_buf, { force = true })
|
||||
end
|
||||
if vim.api.nvim_buf_is_valid(pair.expected_buf) then
|
||||
vim.api.nvim_buf_delete(pair.expected_buf, { force = true })
|
||||
end
|
||||
|
||||
table.remove(edit_state.test_buffers, current_index)
|
||||
table.remove(edit_state.test_cases, current_index)
|
||||
|
||||
for i = current_index, #edit_state.test_buffers do
|
||||
load_test_into_buffer(i)
|
||||
end
|
||||
|
||||
local next_index = math.min(current_index, #edit_state.test_buffers)
|
||||
jump_to_test(next_index)
|
||||
|
||||
logger.log(('Deleted test %d'):format(current_index))
|
||||
end
|
||||
|
||||
local function add_new_test()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local last_pair = edit_state.test_buffers[#edit_state.test_buffers]
|
||||
if not last_pair or not vim.api.nvim_win_is_valid(last_pair.input_win) then
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_set_current_win(last_pair.input_win)
|
||||
vim.cmd.vsplit()
|
||||
local input_win = vim.api.nvim_get_current_win()
|
||||
local input_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(input_win, input_buf)
|
||||
vim.bo[input_buf].modifiable = true
|
||||
vim.bo[input_buf].readonly = false
|
||||
vim.bo[input_buf].buftype = 'nofile'
|
||||
vim.bo[input_buf].buflisted = false
|
||||
helpers.clearcol(input_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(last_pair.expected_win)
|
||||
vim.cmd.vsplit()
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
vim.bo[expected_buf].modifiable = true
|
||||
vim.bo[expected_buf].readonly = false
|
||||
vim.bo[expected_buf].buftype = 'nofile'
|
||||
vim.bo[expected_buf].buflisted = false
|
||||
helpers.clearcol(expected_buf)
|
||||
|
||||
local new_index = #edit_state.test_buffers + 1
|
||||
local new_pair = {
|
||||
input_buf = input_buf,
|
||||
expected_buf = expected_buf,
|
||||
input_win = input_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
table.insert(edit_state.test_buffers, new_pair)
|
||||
table.insert(edit_state.test_cases, { index = new_index, input = '', expected = '' })
|
||||
|
||||
setup_keybindings(input_buf)
|
||||
setup_keybindings(expected_buf)
|
||||
load_test_into_buffer(new_index)
|
||||
|
||||
vim.api.nvim_set_current_win(input_win)
|
||||
logger.log(('Added test %d'):format(new_index))
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
setup_keybindings = function(buf)
|
||||
local config = config_module.get_config()
|
||||
local keys = config.ui.edit
|
||||
|
||||
if keys.save_and_exit_key then
|
||||
vim.keymap.set('n', keys.save_and_exit_key, function()
|
||||
M.toggle_edit()
|
||||
end, { buffer = buf, silent = true, desc = 'Save and exit test editor' })
|
||||
end
|
||||
|
||||
if keys.next_test_key then
|
||||
vim.keymap.set('n', keys.next_test_key, function()
|
||||
navigate_test(1)
|
||||
end, { buffer = buf, silent = true, desc = 'Next test' })
|
||||
end
|
||||
|
||||
if keys.prev_test_key then
|
||||
vim.keymap.set('n', keys.prev_test_key, function()
|
||||
navigate_test(-1)
|
||||
end, { buffer = buf, silent = true, desc = 'Previous test' })
|
||||
end
|
||||
|
||||
if keys.delete_test_key then
|
||||
vim.keymap.set(
|
||||
'n',
|
||||
keys.delete_test_key,
|
||||
delete_current_test,
|
||||
{ buffer = buf, silent = true, desc = 'Delete test' }
|
||||
)
|
||||
end
|
||||
|
||||
if keys.add_test_key then
|
||||
vim.keymap.set(
|
||||
'n',
|
||||
keys.add_test_key,
|
||||
add_new_test,
|
||||
{ buffer = buf, silent = true, desc = 'Add test' }
|
||||
)
|
||||
end
|
||||
|
||||
local augroup = vim.api.nvim_create_augroup('cp_edit_guard', { clear = false })
|
||||
vim.api.nvim_create_autocmd({ 'BufDelete', 'BufWipeout' }, {
|
||||
group = augroup,
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local is_tracked = false
|
||||
for _, pair in ipairs(edit_state.test_buffers) do
|
||||
if pair.input_buf == buf or pair.expected_buf == buf then
|
||||
is_tracked = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if is_tracked then
|
||||
logger.log('Test buffer closed unexpectedly. Exiting editor.', vim.log.levels.WARN)
|
||||
M.toggle_edit()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function save_all_tests()
|
||||
if not edit_state then
|
||||
return
|
||||
end
|
||||
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not problem_id then
|
||||
return
|
||||
end
|
||||
|
||||
for i, pair in ipairs(edit_state.test_buffers) do
|
||||
if
|
||||
vim.api.nvim_buf_is_valid(pair.input_buf) and vim.api.nvim_buf_is_valid(pair.expected_buf)
|
||||
then
|
||||
local input_lines = vim.api.nvim_buf_get_lines(pair.input_buf, 0, -1, false)
|
||||
local expected_lines = vim.api.nvim_buf_get_lines(pair.expected_buf, 0, -1, false)
|
||||
|
||||
edit_state.test_cases[i].input = table.concat(input_lines, '\n')
|
||||
edit_state.test_cases[i].expected = table.concat(expected_lines, '\n')
|
||||
end
|
||||
end
|
||||
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
local is_multi_test = contest_data.problems[contest_data.index_map[problem_id]].multi_test
|
||||
or false
|
||||
|
||||
-- Generate combined test from individual test cases
|
||||
local combined_input = table.concat(
|
||||
vim.tbl_map(function(tc)
|
||||
return tc.input
|
||||
end, edit_state.test_cases),
|
||||
'\n'
|
||||
)
|
||||
local combined_expected = table.concat(
|
||||
vim.tbl_map(function(tc)
|
||||
return tc.expected
|
||||
end, edit_state.test_cases),
|
||||
'\n'
|
||||
)
|
||||
|
||||
cache.set_test_cases(
|
||||
platform,
|
||||
contest_id,
|
||||
problem_id,
|
||||
{ input = combined_input, expected = combined_expected },
|
||||
edit_state.test_cases,
|
||||
edit_state.constraints and edit_state.constraints.timeout_ms or 0,
|
||||
edit_state.constraints and edit_state.constraints.memory_mb or 0,
|
||||
false,
|
||||
is_multi_test
|
||||
)
|
||||
|
||||
local config = config_module.get_config()
|
||||
local base_name = config.filename and config.filename(platform, contest_id, problem_id, config)
|
||||
or config_module.default_filename(contest_id, problem_id)
|
||||
|
||||
vim.fn.mkdir('io', 'p')
|
||||
|
||||
for i, tc in ipairs(edit_state.test_cases) do
|
||||
local input_file = string.format('io/%s.%d.cpin', base_name, i)
|
||||
local expected_file = string.format('io/%s.%d.cpout', base_name, i)
|
||||
|
||||
local input_content = (tc.input or ''):gsub('\r', '')
|
||||
local expected_content = (tc.expected or ''):gsub('\r', '')
|
||||
|
||||
vim.fn.writefile(vim.split(input_content, '\n', { trimempty = true }), input_file)
|
||||
vim.fn.writefile(vim.split(expected_content, '\n', { trimempty = true }), expected_file)
|
||||
end
|
||||
|
||||
logger.log('Saved all test cases')
|
||||
end
|
||||
|
||||
function M.toggle_edit(test_index)
|
||||
if edit_state then
|
||||
save_all_tests()
|
||||
edit_state = nil
|
||||
|
||||
pcall(vim.api.nvim_clear_autocmds, { group = 'cp_edit_guard' })
|
||||
|
||||
local saved = state.get_saved_session()
|
||||
if saved then
|
||||
vim.fn.delete(saved)
|
||||
state.set_saved_session(nil)
|
||||
end
|
||||
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
local source_file = state.get_source_file()
|
||||
if source_file and vim.fn.filereadable(source_file) == 1 then
|
||||
vim.cmd.edit(source_file)
|
||||
end
|
||||
|
||||
local views = require('cp.ui.views')
|
||||
views.ensure_io_view()
|
||||
|
||||
logger.log('Closed test editor')
|
||||
return
|
||||
end
|
||||
|
||||
local platform, contest_id, problem_id =
|
||||
state.get_platform(), state.get_contest_id(), state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not problem_id then
|
||||
logger.log('No problem context. Run :CP <platform> <contest> first.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
|
||||
if not test_cases or #test_cases == 0 then
|
||||
logger.log('No test cases available for editing.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
|
||||
local constraints = (timeout_ms and memory_mb)
|
||||
and { timeout_ms = timeout_ms, memory_mb = memory_mb }
|
||||
or nil
|
||||
|
||||
local target_index = test_index or 1
|
||||
if target_index < 1 or target_index > #test_cases then
|
||||
logger.log(
|
||||
('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local io_view_state = state.get_io_view_state()
|
||||
if io_view_state then
|
||||
if io_view_state.output_buf and vim.api.nvim_buf_is_valid(io_view_state.output_buf) then
|
||||
vim.api.nvim_buf_delete(io_view_state.output_buf, { force = true })
|
||||
end
|
||||
if io_view_state.input_buf and vim.api.nvim_buf_is_valid(io_view_state.input_buf) then
|
||||
vim.api.nvim_buf_delete(io_view_state.input_buf, { force = true })
|
||||
end
|
||||
state.set_io_view_state(nil)
|
||||
end
|
||||
|
||||
local session_file = vim.fn.tempname()
|
||||
state.set_saved_session(session_file)
|
||||
-- selene: allow(mixed_table)
|
||||
vim.cmd.mksession({ session_file, bang = true })
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local test_buffers = {}
|
||||
local num_tests = #test_cases
|
||||
|
||||
for _ = 1, num_tests - 1 do
|
||||
vim.cmd.vsplit()
|
||||
end
|
||||
|
||||
vim.cmd('1 wincmd w')
|
||||
|
||||
for col = 1, num_tests do
|
||||
vim.cmd.split()
|
||||
|
||||
vim.cmd.wincmd('k')
|
||||
local input_win = vim.api.nvim_get_current_win()
|
||||
local input_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(input_win, input_buf)
|
||||
vim.bo[input_buf].modifiable = true
|
||||
vim.bo[input_buf].readonly = false
|
||||
vim.bo[input_buf].buftype = 'nofile'
|
||||
vim.bo[input_buf].buflisted = false
|
||||
helpers.clearcol(input_buf)
|
||||
|
||||
vim.cmd.wincmd('j')
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
vim.bo[expected_buf].modifiable = true
|
||||
vim.bo[expected_buf].readonly = false
|
||||
vim.bo[expected_buf].buftype = 'nofile'
|
||||
vim.bo[expected_buf].buflisted = false
|
||||
helpers.clearcol(expected_buf)
|
||||
|
||||
test_buffers[col] = {
|
||||
input_buf = input_buf,
|
||||
expected_buf = expected_buf,
|
||||
input_win = input_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
|
||||
vim.cmd.wincmd('k')
|
||||
vim.cmd.wincmd('l')
|
||||
end
|
||||
|
||||
edit_state = {
|
||||
test_buffers = test_buffers,
|
||||
test_cases = test_cases,
|
||||
constraints = constraints,
|
||||
}
|
||||
|
||||
for i = 1, num_tests do
|
||||
load_test_into_buffer(i)
|
||||
end
|
||||
|
||||
for _, pair in ipairs(test_buffers) do
|
||||
setup_keybindings(pair.input_buf)
|
||||
setup_keybindings(pair.expected_buf)
|
||||
end
|
||||
|
||||
if
|
||||
test_buffers[target_index]
|
||||
and vim.api.nvim_win_is_valid(test_buffers[target_index].input_win)
|
||||
then
|
||||
vim.api.nvim_set_current_win(test_buffers[target_index].input_win)
|
||||
end
|
||||
|
||||
logger.log(('Editing %d test cases'):format(num_tests))
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -26,7 +26,7 @@ local function parse_diff_line(text)
|
|||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'CpDiffRemoved',
|
||||
highlight_group = 'DiffDelete',
|
||||
})
|
||||
pos = removed_end + 1
|
||||
else
|
||||
|
|
@ -38,7 +38,7 @@ local function parse_diff_line(text)
|
|||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'CpDiffAdded',
|
||||
highlight_group = 'DiffAdd',
|
||||
})
|
||||
pos = added_end + 1
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
local M = {}
|
||||
|
||||
local helpers = require('cp.helpers')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local function create_none_diff_layout(parent_win, expected_content, actual_content)
|
||||
M.DIFF_MODES = {
|
||||
['side-by-side'] = 'side-by-side',
|
||||
vim = 'vim',
|
||||
git = 'git',
|
||||
}
|
||||
|
||||
local function create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
local actual_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(expected_buf)
|
||||
helpers.clearcol(actual_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35))
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local actual_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||
|
||||
|
|
@ -18,8 +27,13 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont
|
|||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf })
|
||||
vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win })
|
||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win })
|
||||
local label = M.DIFF_MODES['side-by-side']
|
||||
vim.api.nvim_set_option_value(
|
||||
'winbar',
|
||||
('expected (diff: %s)'):format(label),
|
||||
{ win = expected_win }
|
||||
)
|
||||
vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win })
|
||||
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
|
|
@ -30,6 +44,7 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont
|
|||
return {
|
||||
buffers = { expected_buf, actual_buf },
|
||||
windows = { expected_win, actual_win },
|
||||
mode = 'side-by-side',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, expected_win, true)
|
||||
pcall(vim.api.nvim_win_close, actual_win, true)
|
||||
|
|
@ -42,10 +57,12 @@ end
|
|||
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
local expected_buf = utils.create_buffer_with_options()
|
||||
local actual_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(expected_buf)
|
||||
helpers.clearcol(actual_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35))
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local actual_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||
|
||||
|
|
@ -55,8 +72,13 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte
|
|||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf })
|
||||
vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win })
|
||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win })
|
||||
local label = M.DIFF_MODES.vim
|
||||
vim.api.nvim_set_option_value(
|
||||
'winbar',
|
||||
('expected (diff: %s)'):format(label),
|
||||
{ win = expected_win }
|
||||
)
|
||||
vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win })
|
||||
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
|
|
@ -78,6 +100,7 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte
|
|||
return {
|
||||
buffers = { expected_buf, actual_buf },
|
||||
windows = { expected_win, actual_win },
|
||||
mode = 'vim',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, expected_win, true)
|
||||
pcall(vim.api.nvim_win_close, actual_win, true)
|
||||
|
|
@ -89,15 +112,17 @@ end
|
|||
|
||||
local function create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
local diff_buf = utils.create_buffer_with_options()
|
||||
helpers.clearcol(diff_buf)
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35))
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(diff_win, diff_buf)
|
||||
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('winbar', 'Expected vs Actual', { win = diff_win })
|
||||
local label = M.DIFF_MODES.git
|
||||
vim.api.nvim_set_option_value('winbar', ('diff: %s'):format(label), { win = diff_win })
|
||||
|
||||
local diff_backend = require('cp.ui.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
|
|
@ -115,6 +140,7 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte
|
|||
return {
|
||||
buffers = { diff_buf },
|
||||
windows = { diff_win },
|
||||
mode = 'git',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, diff_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, diff_buf, { force = true })
|
||||
|
|
@ -129,7 +155,7 @@ local function create_single_layout(parent_win, content)
|
|||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35))
|
||||
vim.cmd.resize(math.floor(vim.o.lines * 0.35))
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = buf })
|
||||
|
|
@ -137,6 +163,7 @@ local function create_single_layout(parent_win, content)
|
|||
return {
|
||||
buffers = { buf },
|
||||
windows = { win },
|
||||
mode = 'single',
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, win, true)
|
||||
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
||||
|
|
@ -147,12 +174,14 @@ end
|
|||
function M.create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||
if mode == 'single' then
|
||||
return create_single_layout(parent_win, actual_content)
|
||||
elseif mode == 'none' then
|
||||
return create_none_diff_layout(parent_win, expected_content, actual_content)
|
||||
elseif mode == 'side-by-side' then
|
||||
return create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
elseif mode == 'git' then
|
||||
return create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
else
|
||||
elseif mode == 'vim' then
|
||||
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
else
|
||||
return create_side_by_side_layout(parent_win, expected_content, actual_content)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -164,7 +193,7 @@ function M.update_diff_panes(
|
|||
config,
|
||||
setup_keybindings_for_buffer
|
||||
)
|
||||
local test_state = run.get_run_panel_state()
|
||||
local test_state = run.get_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
|
|
@ -185,12 +214,13 @@ function M.update_diff_panes(
|
|||
actual_content = actual_content
|
||||
end
|
||||
|
||||
local desired_mode = is_compilation_failure and 'single' or config.ui.run_panel.diff_mode
|
||||
local default_mode = config.ui.panel.diff_modes[1]
|
||||
local desired_mode = is_compilation_failure and 'single' or (current_mode or default_mode)
|
||||
local highlight = require('cp.ui.highlight')
|
||||
local diff_namespace = highlight.create_namespace()
|
||||
local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights')
|
||||
|
||||
if current_diff_layout and current_mode ~= desired_mode then
|
||||
if current_diff_layout and current_diff_layout.mode ~= desired_mode then
|
||||
local saved_pos = vim.api.nvim_win_get_cursor(0)
|
||||
current_diff_layout.cleanup()
|
||||
current_diff_layout = nil
|
||||
|
|
@ -245,7 +275,7 @@ function M.update_diff_panes(
|
|||
ansi_namespace
|
||||
)
|
||||
end
|
||||
elseif desired_mode == 'none' then
|
||||
elseif desired_mode == 'side-by-side' then
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true })
|
||||
utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
|
||||
|
|
|
|||
|
|
@ -1,387 +0,0 @@
|
|||
local M = {}
|
||||
|
||||
---@class RunOpts
|
||||
---@field debug? boolean
|
||||
|
||||
local config_module = require('cp.config')
|
||||
local constants = require('cp.constants')
|
||||
local layouts = require('cp.ui.layouts')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local current_diff_layout = nil
|
||||
local current_mode = nil
|
||||
|
||||
function M.disable()
|
||||
local active_panel = state.get_active_panel()
|
||||
if not active_panel then
|
||||
logger.log('No active panel to close')
|
||||
return
|
||||
end
|
||||
|
||||
if active_panel == 'run' then
|
||||
M.toggle_run_panel()
|
||||
elseif active_panel == 'interactive' then
|
||||
M.toggle_interactive()
|
||||
else
|
||||
logger.log(('Unknown panel type: %s'):format(tostring(active_panel)))
|
||||
end
|
||||
end
|
||||
|
||||
---@param interactor_cmd? string
|
||||
function M.toggle_interactive(interactor_cmd)
|
||||
if state.get_active_panel() == 'interactive' then
|
||||
if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then
|
||||
local job = vim.b[state.interactive_buf].terminal_job_id
|
||||
if job then
|
||||
vim.fn.jobstop(job)
|
||||
end
|
||||
end
|
||||
if state.saved_interactive_session then
|
||||
vim.cmd(('source %s'):format(state.saved_interactive_session))
|
||||
vim.fn.delete(state.saved_interactive_session)
|
||||
state.saved_interactive_session = nil
|
||||
end
|
||||
state.set_active_panel(nil)
|
||||
return
|
||||
end
|
||||
|
||||
if state.get_active_panel() then
|
||||
logger.log('Another panel is already active.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local platform, contest_id = state.get_platform(), state.get_contest_id()
|
||||
if not platform then
|
||||
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if not contest_id then
|
||||
logger.log(
|
||||
("No contest %s configured for platform '%s'."):format(
|
||||
contest_id,
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local problem_id = state.get_problem_id()
|
||||
if not problem_id then
|
||||
logger.log('No problem is active.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if
|
||||
not contest_data
|
||||
or not contest_data.index_map
|
||||
or not contest_data.problems[contest_data.index_map[problem_id]]
|
||||
or not contest_data.problems[contest_data.index_map[problem_id]].interactive
|
||||
then
|
||||
logger.log('This problem is not interactive. Use :CP run.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
state.saved_interactive_session = vim.fn.tempname()
|
||||
vim.cmd(('mksession! %s'):format(state.saved_interactive_session))
|
||||
vim.cmd('silent only')
|
||||
|
||||
local execute = require('cp.runner.execute')
|
||||
local compile_result = execute.compile_problem()
|
||||
if not compile_result.success then
|
||||
require('cp.runner.run').handle_compilation_failure(compile_result.output)
|
||||
return
|
||||
end
|
||||
|
||||
local binary = state.get_binary_file()
|
||||
if not binary or binary == '' then
|
||||
logger.log('No binary produced.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local cmdline
|
||||
if interactor_cmd and interactor_cmd ~= '' then
|
||||
local interactor = interactor_cmd
|
||||
if not interactor:find('/') then
|
||||
interactor = './' .. interactor
|
||||
end
|
||||
if vim.fn.executable(interactor) ~= 1 then
|
||||
logger.log(
|
||||
("Interactor '%s' is not executable."):format(interactor_cmd),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
if state.saved_interactive_session then
|
||||
vim.cmd(('source %s'):format(state.saved_interactive_session))
|
||||
vim.fn.delete(state.saved_interactive_session)
|
||||
state.saved_interactive_session = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
local orchestrator = vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
||||
cmdline = table.concat({
|
||||
'uv',
|
||||
'run',
|
||||
vim.fn.shellescape(orchestrator),
|
||||
vim.fn.shellescape(interactor),
|
||||
vim.fn.shellescape(binary),
|
||||
}, ' ')
|
||||
else
|
||||
cmdline = vim.fn.shellescape(binary)
|
||||
end
|
||||
|
||||
vim.cmd('terminal ' .. cmdline)
|
||||
local term_buf = vim.api.nvim_get_current_buf()
|
||||
local term_win = vim.api.nvim_get_current_win()
|
||||
|
||||
local cleaned = false
|
||||
local function cleanup()
|
||||
if cleaned then
|
||||
return
|
||||
end
|
||||
cleaned = true
|
||||
if term_buf and vim.api.nvim_buf_is_valid(term_buf) then
|
||||
local job = vim.b[term_buf] and vim.b[term_buf].terminal_job_id or nil
|
||||
if job then
|
||||
pcall(vim.fn.jobstop, job)
|
||||
end
|
||||
end
|
||||
if state.saved_interactive_session then
|
||||
vim.cmd(('source %s'):format(state.saved_interactive_session))
|
||||
vim.fn.delete(state.saved_interactive_session)
|
||||
state.saved_interactive_session = nil
|
||||
end
|
||||
state.interactive_buf = nil
|
||||
state.interactive_win = nil
|
||||
state.set_active_panel(nil)
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd({ 'BufWipeout', 'BufUnload' }, {
|
||||
buffer = term_buf,
|
||||
callback = function()
|
||||
cleanup()
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('WinClosed', {
|
||||
callback = function()
|
||||
if cleaned then
|
||||
return
|
||||
end
|
||||
local any = false
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == term_buf then
|
||||
any = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not any then
|
||||
cleanup()
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('TermClose', {
|
||||
buffer = term_buf,
|
||||
callback = function()
|
||||
vim.b[term_buf].cp_interactive_exited = true
|
||||
end,
|
||||
})
|
||||
|
||||
vim.keymap.set('t', '<c-q>', function()
|
||||
cleanup()
|
||||
end, { buffer = term_buf, silent = true })
|
||||
vim.keymap.set('n', '<c-q>', function()
|
||||
cleanup()
|
||||
end, { buffer = term_buf, silent = true })
|
||||
|
||||
state.interactive_buf = term_buf
|
||||
state.interactive_win = term_win
|
||||
state.set_active_panel('interactive')
|
||||
end
|
||||
|
||||
---@param run_opts? RunOpts
|
||||
function M.toggle_run_panel(run_opts)
|
||||
if state.get_active_panel() == 'run' then
|
||||
if current_diff_layout then
|
||||
current_diff_layout.cleanup()
|
||||
current_diff_layout = nil
|
||||
current_mode = nil
|
||||
end
|
||||
if state.saved_session then
|
||||
vim.cmd(('source %s'):format(state.saved_session))
|
||||
vim.fn.delete(state.saved_session)
|
||||
state.saved_session = nil
|
||||
end
|
||||
state.set_active_panel(nil)
|
||||
return
|
||||
end
|
||||
|
||||
if state.get_active_panel() then
|
||||
logger.log('another panel is already active', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local platform, contest_id = state.get_platform(), state.get_contest_id()
|
||||
|
||||
if not platform then
|
||||
logger.log(
|
||||
'No platform configured. Use :CP <platform> <contest> [...] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if not contest_id then
|
||||
logger.log(
|
||||
("No contest '%s' configured for platform '%s'."):format(
|
||||
contest_id,
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
if
|
||||
contest_data
|
||||
and contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive
|
||||
then
|
||||
logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local config = config_module.get_config()
|
||||
local run = require('cp.runner.run')
|
||||
local input_file = state.get_input_file()
|
||||
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
|
||||
|
||||
if not run.load_test_cases() then
|
||||
logger.log('no test cases found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
state.saved_session = vim.fn.tempname()
|
||||
vim.cmd(('mksession! %s'):format(state.saved_session))
|
||||
vim.cmd('silent only')
|
||||
|
||||
local tab_buf = utils.create_buffer_with_options()
|
||||
local main_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cp', { buf = tab_buf })
|
||||
|
||||
local test_windows = { tab_win = main_win }
|
||||
local test_buffers = { tab_buf = tab_buf }
|
||||
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
|
||||
|
||||
local setup_keybindings_for_buffer
|
||||
|
||||
local function update_diff_panes()
|
||||
current_diff_layout, current_mode = layouts.update_diff_panes(
|
||||
current_diff_layout,
|
||||
current_mode,
|
||||
main_win,
|
||||
run,
|
||||
config,
|
||||
setup_keybindings_for_buffer
|
||||
)
|
||||
end
|
||||
|
||||
local function refresh_run_panel()
|
||||
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
|
||||
return
|
||||
end
|
||||
local run_render = require('cp.runner.run_render')
|
||||
run_render.setup_highlights()
|
||||
local test_state = run.get_run_panel_state()
|
||||
local tab_lines, tab_highlights = run_render.render_test_list(test_state)
|
||||
utils.update_buffer_content(
|
||||
test_buffers.tab_buf,
|
||||
tab_lines,
|
||||
tab_highlights,
|
||||
test_list_namespace
|
||||
)
|
||||
update_diff_panes()
|
||||
end
|
||||
|
||||
local function navigate_test_case(delta)
|
||||
local test_state = run.get_run_panel_state()
|
||||
if vim.tbl_isempty(test_state.test_cases) then
|
||||
return
|
||||
end
|
||||
test_state.current_index = (test_state.current_index + delta - 1) % #test_state.test_cases + 1
|
||||
refresh_run_panel()
|
||||
end
|
||||
|
||||
setup_keybindings_for_buffer = function(buf)
|
||||
vim.keymap.set('n', 'q', function()
|
||||
M.toggle_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', 't', function()
|
||||
local modes = { 'none', 'git', 'vim' }
|
||||
local current_idx = 1
|
||||
for i, mode in ipairs(modes) do
|
||||
if config.ui.run_panel.diff_mode == mode then
|
||||
current_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
config.ui.run_panel.diff_mode = modes[(current_idx % #modes) + 1]
|
||||
refresh_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', '<c-n>', function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', '<c-p>', function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
setup_keybindings_for_buffer(test_buffers.tab_buf)
|
||||
|
||||
if config.hooks and config.hooks.before_run then
|
||||
vim.schedule_wrap(function()
|
||||
config.hooks.before_run(state)
|
||||
end)
|
||||
end
|
||||
if run_opts and run_opts.debug and config.hooks and config.hooks.before_debug then
|
||||
vim.schedule_wrap(function()
|
||||
config.hooks.before_debug(state)
|
||||
end)
|
||||
end
|
||||
|
||||
local execute = require('cp.runner.execute')
|
||||
local compile_result = execute.compile_problem()
|
||||
if compile_result.success then
|
||||
run.run_all_test_cases()
|
||||
else
|
||||
run.handle_compilation_failure(compile_result.output)
|
||||
end
|
||||
|
||||
refresh_run_panel()
|
||||
|
||||
vim.schedule(function()
|
||||
if config.ui.run_panel.ansi then
|
||||
local ansi = require('cp.ui.ansi')
|
||||
ansi.setup_highlight_groups()
|
||||
end
|
||||
if current_diff_layout then
|
||||
update_diff_panes()
|
||||
end
|
||||
end)
|
||||
|
||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||
state.test_buffers = test_buffers
|
||||
state.test_windows = test_windows
|
||||
state.set_active_panel('run')
|
||||
logger.log('test panel opened')
|
||||
end
|
||||
|
||||
return M
|
||||
1015
lua/cp/ui/views.lua
Normal file
1015
lua/cp/ui/views.lua
Normal file
File diff suppressed because it is too large
Load diff
171
lua/cp/utils.lua
171
lua/cp/utils.lua
|
|
@ -2,6 +2,9 @@ local M = {}
|
|||
|
||||
local logger = require('cp.log')
|
||||
|
||||
local _nix_python = nil
|
||||
local _nix_discovered = false
|
||||
|
||||
local uname = vim.loop.os_uname()
|
||||
|
||||
local _time_cached = false
|
||||
|
|
@ -12,7 +15,7 @@ local _timeout_path = nil
|
|||
local _timeout_reason = nil
|
||||
|
||||
local function is_windows()
|
||||
return uname and uname.sysname == 'Windows_NT'
|
||||
return uname.sysname == 'Windows_NT'
|
||||
end
|
||||
|
||||
local function check_time_is_gnu_time(bin)
|
||||
|
|
@ -57,7 +60,11 @@ local function find_gnu_time()
|
|||
|
||||
_time_cached = true
|
||||
_time_path = nil
|
||||
_time_reason = 'GNU time not found'
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_time_reason = 'GNU time not found (install via: brew install coreutils)'
|
||||
else
|
||||
_time_reason = 'GNU time not found'
|
||||
end
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
|
||||
|
|
@ -79,52 +86,156 @@ function M.get_plugin_path()
|
|||
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_build()
|
||||
return _nix_python ~= nil
|
||||
end
|
||||
|
||||
---@return string|nil
|
||||
function M.get_nix_python()
|
||||
return _nix_python
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_discovered()
|
||||
return _nix_discovered
|
||||
end
|
||||
|
||||
---@param module string
|
||||
---@param plugin_path string
|
||||
---@return string[]
|
||||
function M.get_python_cmd(module, plugin_path)
|
||||
if _nix_python then
|
||||
return { _nix_python, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
|
||||
local python_env_setup = false
|
||||
|
||||
---@return boolean
|
||||
local function discover_nix_python()
|
||||
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
||||
local cache_file = cache_dir .. '/nix-python'
|
||||
|
||||
local f = io.open(cache_file, 'r')
|
||||
if f then
|
||||
local cached = f:read('*l')
|
||||
f:close()
|
||||
if cached and vim.fn.executable(cached) == 1 then
|
||||
_nix_python = cached
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
local result = vim
|
||||
.system(
|
||||
{ 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local store_path = result.stdout:gsub('%s+$', '')
|
||||
local python_path = store_path .. '/bin/python3'
|
||||
|
||||
if vim.fn.executable(python_path) ~= 1 then
|
||||
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.mkdir(cache_dir, 'p')
|
||||
f = io.open(cache_file, 'w')
|
||||
if f then
|
||||
f:write(python_path)
|
||||
f:close()
|
||||
end
|
||||
|
||||
_nix_python = python_path
|
||||
_nix_discovered = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean success
|
||||
function M.setup_python_env()
|
||||
if python_env_setup then
|
||||
return true
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
|
||||
if vim.fn.executable('uv') == 0 then
|
||||
logger.log(
|
||||
'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
if _nix_python then
|
||||
logger.log('Python env: nix (python=' .. _nix_python .. ')')
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
if vim.fn.isdirectory(venv_dir) == 0 then
|
||||
logger.log('Setting up Python environment for scrapers...')
|
||||
local result = vim.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true }):wait()
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
local plugin_path = M.get_plugin_path()
|
||||
logger.log('Python env: uv sync (dir=' .. plugin_path .. ')')
|
||||
vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
env.CONDA_PREFIX = ''
|
||||
local result = vim
|
||||
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
|
||||
logger.log(
|
||||
'Failed to setup Python environment: ' .. (result.stderr or ''),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
end
|
||||
logger.log('Python environment setup complete.')
|
||||
if result.stderr and result.stderr ~= '' then
|
||||
logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', ''))
|
||||
end
|
||||
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
python_env_setup = true
|
||||
return true
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
logger.log('Python env: nix discovery')
|
||||
if discover_nix_python() then
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
logger.log(
|
||||
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
--- Configure the buffer with good defaults
|
||||
---@param filetype? string
|
||||
function M.create_buffer_with_options(filetype)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
||||
|
||||
if filetype then
|
||||
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param lines string[]
|
||||
---@param highlights? Highlight[]
|
||||
---@param namespace? integer
|
||||
function M.update_buffer_content(bufnr, lines, highlights, namespace)
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||
|
||||
|
|
@ -151,20 +262,12 @@ function M.check_required_runtime()
|
|||
|
||||
local time = M.time_capability()
|
||||
if not time.ok then
|
||||
return false, 'GNU time not found: ' .. (time.reason or '')
|
||||
return false, time.reason
|
||||
end
|
||||
|
||||
local timeout = M.timeout_capability()
|
||||
if not timeout.ok then
|
||||
return false, 'GNU timeout not found: ' .. (timeout.reason or '')
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') ~= 1 then
|
||||
return false, 'uv not found (https://docs.astral.sh/uv/)'
|
||||
end
|
||||
|
||||
if not M.setup_python_env() then
|
||||
return false, 'failed to set up Python virtual environment'
|
||||
return false, timeout.reason
|
||||
end
|
||||
|
||||
return true
|
||||
|
|
@ -214,7 +317,11 @@ local function find_gnu_timeout()
|
|||
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
_timeout_reason = 'GNU timeout not found'
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_timeout_reason = 'GNU timeout not found (install via: brew install coreutils)'
|
||||
else
|
||||
_timeout_reason = 'GNU timeout not found'
|
||||
end
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
|
|
@ -251,4 +358,8 @@ function M.cwd_executables()
|
|||
return out
|
||||
end
|
||||
|
||||
function M.ensure_dirs()
|
||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
134
plugin/cp.lua
134
plugin/cp.lua
|
|
@ -3,8 +3,6 @@ if vim.g.loaded_cp then
|
|||
end
|
||||
vim.g.loaded_cp = 1
|
||||
|
||||
local utils = require('cp.utils')
|
||||
|
||||
vim.api.nvim_create_user_command('CP', function(opts)
|
||||
local cp = require('cp')
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -22,12 +20,30 @@ end, {
|
|||
num_args = num_args + 1
|
||||
end
|
||||
|
||||
local function filter_candidates(candidates)
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
end
|
||||
|
||||
local function get_enabled_languages(platform)
|
||||
local config = require('cp.config').get_config()
|
||||
if platform and config.platforms[platform] then
|
||||
return config.platforms[platform].enabled_languages
|
||||
end
|
||||
return vim.tbl_keys(config.languages)
|
||||
end
|
||||
|
||||
if num_args == 2 then
|
||||
local candidates = {}
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
|
||||
vim.list_extend(candidates, platforms)
|
||||
table.insert(candidates, 'cache')
|
||||
table.insert(candidates, 'pick')
|
||||
|
||||
if platform and contest_id then
|
||||
vim.list_extend(candidates, actions)
|
||||
local cache = require('cp.cache')
|
||||
|
|
@ -39,46 +55,116 @@ end, {
|
|||
table.sort(ids)
|
||||
vim.list_extend(candidates, ids)
|
||||
end
|
||||
else
|
||||
vim.list_extend(candidates, platforms)
|
||||
table.insert(candidates, 'cache')
|
||||
table.insert(candidates, 'pick')
|
||||
end
|
||||
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
return filter_candidates(candidates)
|
||||
elseif num_args == 3 then
|
||||
if args[2] == 'cache' then
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, { 'clear', 'read' })
|
||||
if vim.tbl_contains(platforms, args[2]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contests = cache.get_cached_contest_ids(args[2])
|
||||
return filter_candidates(contests)
|
||||
elseif args[2] == 'cache' then
|
||||
return filter_candidates({ 'clear', 'read' })
|
||||
elseif args[2] == 'interact' then
|
||||
local cands = utils.cwd_executables()
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, cands)
|
||||
local utils = require('cp.utils')
|
||||
return filter_candidates(utils.cwd_executables())
|
||||
elseif args[2] == 'edit' then
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
local candidates = {}
|
||||
if platform and contest_id and problem_id then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
if test_cases then
|
||||
for i = 1, #test_cases do
|
||||
table.insert(candidates, tostring(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'run' or args[2] == 'panel' then
|
||||
local state = require('cp.state')
|
||||
local platform = state.get_platform()
|
||||
local contest_id = state.get_contest_id()
|
||||
local problem_id = state.get_problem_id()
|
||||
local candidates = { '--debug' }
|
||||
if platform and contest_id and problem_id then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
if test_cases then
|
||||
for i = 1, #test_cases do
|
||||
table.insert(candidates, tostring(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'next' or args[2] == 'prev' or args[2] == 'pick' then
|
||||
return filter_candidates({ '--lang' })
|
||||
else
|
||||
local state = require('cp.state')
|
||||
if state.get_platform() and state.get_contest_id() then
|
||||
return filter_candidates({ '--lang' })
|
||||
end
|
||||
end
|
||||
elseif num_args == 4 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' then
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, platforms)
|
||||
local candidates = vim.list_extend({}, platforms)
|
||||
table.insert(candidates, '')
|
||||
return filter_candidates(candidates)
|
||||
elseif args[3] == '--lang' then
|
||||
local platform = require('cp.state').get_platform()
|
||||
return filter_candidates(get_enabled_languages(platform))
|
||||
elseif (args[2] == 'run' or args[2] == 'panel') and tonumber(args[3]) then
|
||||
return filter_candidates({ '--debug' })
|
||||
elseif vim.tbl_contains(platforms, args[2]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contest_data = cache.get_contest_data(args[2], args[3])
|
||||
local candidates = { '--lang' }
|
||||
if contest_data and contest_data.problems then
|
||||
local candidates = {}
|
||||
for _, problem in ipairs(contest_data.problems) do
|
||||
table.insert(candidates, problem.id)
|
||||
end
|
||||
return vim.tbl_filter(function(cmd)
|
||||
return cmd:find(ArgLead, 1, true) == 1
|
||||
end, candidates)
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
end
|
||||
elseif num_args == 5 then
|
||||
if args[2] == 'cache' and args[3] == 'clear' and vim.tbl_contains(platforms, args[4]) then
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contests = cache.get_cached_contest_ids(args[4])
|
||||
return filter_candidates(contests)
|
||||
elseif vim.tbl_contains(platforms, args[2]) then
|
||||
if args[4] == '--lang' then
|
||||
return filter_candidates(get_enabled_languages(args[2]))
|
||||
else
|
||||
return filter_candidates({ '--lang' })
|
||||
end
|
||||
end
|
||||
elseif num_args == 6 then
|
||||
if vim.tbl_contains(platforms, args[2]) and args[5] == '--lang' then
|
||||
return filter_candidates(get_enabled_languages(args[2]))
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
local function cp_action(action)
|
||||
return function()
|
||||
require('cp').handle_command({ fargs = { action } })
|
||||
end
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<Plug>(cp-run)', cp_action('run'), { desc = 'CP run tests' })
|
||||
vim.keymap.set('n', '<Plug>(cp-panel)', cp_action('panel'), { desc = 'CP open panel' })
|
||||
vim.keymap.set('n', '<Plug>(cp-edit)', cp_action('edit'), { desc = 'CP edit test cases' })
|
||||
vim.keymap.set('n', '<Plug>(cp-next)', cp_action('next'), { desc = 'CP next problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' })
|
||||
vim.keymap.set('n', '<Plug>(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "scrapers"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
description = "Competitive programming scrapers for a variety of web platforms."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
|
@ -10,19 +10,20 @@ dependencies = [
|
|||
"curl-cffi>=0.13.0",
|
||||
"httpx>=0.28.1",
|
||||
"ndjson>=0.3.1",
|
||||
"pydantic>=2.11.10",
|
||||
"requests>=2.32.5",
|
||||
"scrapling[fetchers]>=0.3.5",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"mypy>=1.18.2",
|
||||
"types-beautifulsoup4>=4.12.0.20250516",
|
||||
"types-requests>=2.32.4.20250913",
|
||||
"pytest>=8.0.0",
|
||||
"pytest-mock>=3.12.0",
|
||||
"pre-commit>=4.3.0",
|
||||
"basedpyright>=1.31.6",
|
||||
"ruff>=0.14.2",
|
||||
"ty>=0.0.1a32",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import json
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
import backoff
|
||||
|
|
@ -17,6 +16,7 @@ from urllib3.util.retry import Retry
|
|||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
CombinedTest,
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
|
|
@ -231,23 +231,19 @@ def _scrape_problem_page_sync(contest_id: str, slug: str) -> dict[str, Any]:
|
|||
|
||||
def _to_problem_summaries(rows: list[dict[str, str]]) -> list[ProblemSummary]:
|
||||
out: list[ProblemSummary] = []
|
||||
seen: set[str] = set()
|
||||
for r in rows:
|
||||
letter = (r.get("letter") or "").strip().upper()
|
||||
title = r.get("title") or ""
|
||||
if not letter:
|
||||
continue
|
||||
pid = letter.lower()
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
out.append(ProblemSummary(id=pid, name=title))
|
||||
return out
|
||||
|
||||
|
||||
async def _fetch_all_contests_async() -> list[ContestSummary]:
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100)
|
||||
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100),
|
||||
) as client:
|
||||
first_html = await _get_async(client, ARCHIVE_URL)
|
||||
last = _parse_last_page(first_html)
|
||||
|
|
@ -270,42 +266,31 @@ class AtcoderScraper(BaseScraper):
|
|||
return "atcoder"
|
||||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
async def impl(cid: str) -> MetadataResult:
|
||||
try:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, cid)
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None and e.response.status_code == 404:
|
||||
return self._create_metadata_error(
|
||||
f"No problems found for contest {cid}", cid
|
||||
)
|
||||
raise
|
||||
|
||||
try:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, contest_id)
|
||||
problems = _to_problem_summaries(rows)
|
||||
if not problems:
|
||||
return self._create_metadata_error(
|
||||
f"No problems found for contest {cid}", cid
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=cid,
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_%s",
|
||||
)
|
||||
|
||||
return await self._safe_execute("metadata", impl, contest_id)
|
||||
except Exception as e:
|
||||
return self._metadata_error(str(e))
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
async def impl() -> ContestListResult:
|
||||
try:
|
||||
contests = await _fetch_all_contests_async()
|
||||
except Exception as e:
|
||||
return self._create_contests_error(str(e))
|
||||
try:
|
||||
contests = await _fetch_all_contests_async()
|
||||
if not contests:
|
||||
return self._create_contests_error("No contests found")
|
||||
return self._contests_error("No contests found")
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
|
||||
return await self._safe_execute("contests", impl)
|
||||
except Exception as e:
|
||||
return self._contests_error(str(e))
|
||||
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, category_id)
|
||||
|
|
@ -317,16 +302,23 @@ class AtcoderScraper(BaseScraper):
|
|||
return
|
||||
data = await asyncio.to_thread(_scrape_problem_page_sync, category_id, slug)
|
||||
tests: list[TestCase] = data.get("tests", [])
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = "\n".join(t.expected for t in tests) if tests else ""
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"problem_id": letter,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": data.get("timeout_ms", 0),
|
||||
"memory_mb": data.get("memory_mb", 0),
|
||||
"interactive": bool(data.get("interactive")),
|
||||
"multi_test": False,
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
|
|
@ -340,8 +332,9 @@ async def main_async() -> int:
|
|||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Usage: atcoder.py metadata <contest_id> OR atcoder.py tests <contest_id> OR atcoder.py contests",
|
||||
url="",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
|
|
@ -350,13 +343,15 @@ async def main_async() -> int:
|
|||
if mode == "metadata":
|
||||
if len(sys.argv) != 3:
|
||||
result = MetadataResult(
|
||||
success=False, error="Usage: atcoder.py metadata <contest_id>"
|
||||
success=False,
|
||||
error="Usage: atcoder.py metadata <contest_id>",
|
||||
url="",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
result = await scraper.scrape_contest_metadata(contest_id)
|
||||
print(json.dumps(asdict(result)))
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
if mode == "tests":
|
||||
|
|
@ -365,12 +360,12 @@ async def main_async() -> int:
|
|||
success=False,
|
||||
error="Usage: atcoder.py tests <contest_id>",
|
||||
problem_id="",
|
||||
url="",
|
||||
combined=CombinedTest(input="", expected=""),
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
print(json.dumps(asdict(tests_result)))
|
||||
print(tests_result.model_dump_json())
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
await scraper.stream_tests_for_category_async(contest_id)
|
||||
|
|
@ -381,17 +376,18 @@ async def main_async() -> int:
|
|||
contest_result = ContestListResult(
|
||||
success=False, error="Usage: atcoder.py contests"
|
||||
)
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
print(contest_result.model_dump_json())
|
||||
return 1
|
||||
contest_result = await scraper.scrape_contest_list()
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
print(contest_result.model_dump_json())
|
||||
return 0 if contest_result.success else 1
|
||||
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Unknown mode. Use 'metadata <contest_id>', 'tests <contest_id>', or 'contests'",
|
||||
url="",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
print(result.model_dump_json())
|
||||
return 1
|
||||
|
||||
|
||||
|
|
|
|||
109
scrapers/base.py
109
scrapers/base.py
|
|
@ -1,20 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Awaitable, Callable, ParamSpec, cast
|
||||
|
||||
from .models import ContestListResult, MetadataResult, TestsResult
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScraperConfig:
|
||||
timeout_seconds: int = 30
|
||||
max_retries: int = 3
|
||||
backoff_base: float = 2.0
|
||||
rate_limit_delay: float = 1.0
|
||||
from .models import CombinedTest, ContestListResult, MetadataResult, TestsResult
|
||||
|
||||
|
||||
class BaseScraper(ABC):
|
||||
|
|
@ -31,50 +19,65 @@ class BaseScraper(ABC):
|
|||
@abstractmethod
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None: ...
|
||||
|
||||
def _create_metadata_error(
|
||||
self, error_msg: str, contest_id: str = ""
|
||||
) -> MetadataResult:
|
||||
return MetadataResult(
|
||||
success=False,
|
||||
error=f"{self.platform_name}: {error_msg}",
|
||||
contest_id=contest_id,
|
||||
)
|
||||
def _usage(self) -> str:
|
||||
name = self.platform_name
|
||||
return f"Usage: {name}.py metadata <id> | tests <id> | contests"
|
||||
|
||||
def _create_tests_error(
|
||||
self, error_msg: str, problem_id: str = "", url: str = ""
|
||||
) -> TestsResult:
|
||||
def _metadata_error(self, msg: str) -> MetadataResult:
|
||||
return MetadataResult(success=False, error=msg, url="")
|
||||
|
||||
def _tests_error(self, msg: str) -> TestsResult:
|
||||
return TestsResult(
|
||||
success=False,
|
||||
error=f"{self.platform_name}: {error_msg}",
|
||||
problem_id=problem_id,
|
||||
url=url,
|
||||
error=msg,
|
||||
problem_id="",
|
||||
combined=CombinedTest(input="", expected=""),
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
|
||||
def _create_contests_error(self, error_msg: str) -> ContestListResult:
|
||||
return ContestListResult(
|
||||
success=False, error=f"{self.platform_name}: {error_msg}"
|
||||
)
|
||||
def _contests_error(self, msg: str) -> ContestListResult:
|
||||
return ContestListResult(success=False, error=msg)
|
||||
|
||||
async def _safe_execute(
|
||||
self,
|
||||
operation: str,
|
||||
func: Callable[P, Awaitable[Any]],
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if operation == "metadata":
|
||||
contest_id = cast(str, args[0]) if args else ""
|
||||
return self._create_metadata_error(str(e), contest_id)
|
||||
elif operation == "tests":
|
||||
problem_id = cast(str, args[1]) if len(args) > 1 else ""
|
||||
return self._create_tests_error(str(e), problem_id)
|
||||
elif operation == "contests":
|
||||
return self._create_contests_error(str(e))
|
||||
else:
|
||||
raise
|
||||
async def _run_cli_async(self, args: list[str]) -> int:
|
||||
if len(args) < 2:
|
||||
print(self._metadata_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
|
||||
mode = args[1]
|
||||
|
||||
match mode:
|
||||
case "metadata":
|
||||
if len(args) != 3:
|
||||
print(self._metadata_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
result = await self.scrape_contest_metadata(args[2])
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case "tests":
|
||||
if len(args) != 3:
|
||||
print(self._tests_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
await self.stream_tests_for_category_async(args[2])
|
||||
return 0
|
||||
|
||||
case "contests":
|
||||
if len(args) != 2:
|
||||
print(self._contests_error(self._usage()).model_dump_json())
|
||||
return 1
|
||||
result = await self.scrape_contest_list()
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
||||
case _:
|
||||
print(
|
||||
self._metadata_error(
|
||||
f"Unknown mode: {mode}. {self._usage()}"
|
||||
).model_dump_json()
|
||||
)
|
||||
return 1
|
||||
|
||||
def run_cli(self) -> None:
|
||||
sys.exit(asyncio.run(self._run_cli_async(sys.argv)))
|
||||
|
|
|
|||
253
scrapers/codechef.py
Normal file
253
scrapers/codechef.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
ContestListResult,
|
||||
ContestSummary,
|
||||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
)
|
||||
|
||||
BASE_URL = "https://www.codechef.com"
|
||||
API_CONTESTS_ALL = "/api/list/contests/all"
|
||||
API_CONTEST = "/api/contests/{contest_id}"
|
||||
API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}"
|
||||
PROBLEM_URL = "https://www.codechef.com/problems/{problem_id}"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
TIMEOUT_S = 15.0
|
||||
CONNECTIONS = 8
|
||||
MEMORY_LIMIT_RE = re.compile(
|
||||
r"Memory\s+[Ll]imit.*?([0-9.]+)\s*(MB|GB)", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict:
|
||||
r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _extract_memory_limit(html: str) -> float:
|
||||
m = MEMORY_LIMIT_RE.search(html)
|
||||
if not m:
|
||||
return 256.0
|
||||
value = float(m.group(1))
|
||||
unit = m.group(2).upper()
|
||||
if unit == "GB":
|
||||
return value * 1024.0
|
||||
return value
|
||||
|
||||
|
||||
def _fetch_html_sync(url: str) -> str:
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
class CodeChefScraper(BaseScraper):
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
return "codechef"
|
||||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=contest_id)
|
||||
)
|
||||
if not data.get("problems"):
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
problems = []
|
||||
for problem_code, problem_data in data["problems"].items():
|
||||
if problem_data.get("category_name") == "main":
|
||||
problems.append(
|
||||
ProblemSummary(
|
||||
id=problem_code,
|
||||
name=problem_data.get("name", problem_code),
|
||||
)
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"{BASE_URL}/{contest_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
return self._metadata_error(f"Failed to fetch contest {contest_id}: {e}")
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
data = await fetch_json(client, API_CONTESTS_ALL)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return self._contests_error(f"Failed to fetch contests: {e}")
|
||||
all_contests = data.get("future_contests", []) + data.get(
|
||||
"past_contests", []
|
||||
)
|
||||
max_num = 0
|
||||
for contest in all_contests:
|
||||
contest_code = contest.get("contest_code", "")
|
||||
if contest_code.startswith("START"):
|
||||
match = re.match(r"START(\d+)", contest_code)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
max_num = max(max_num, num)
|
||||
if max_num == 0:
|
||||
return self._contests_error("No Starters contests found")
|
||||
contests = []
|
||||
sem = asyncio.Semaphore(CONNECTIONS)
|
||||
|
||||
async def fetch_divisions(i: int) -> list[ContestSummary]:
|
||||
parent_id = f"START{i}"
|
||||
async with sem:
|
||||
try:
|
||||
parent_data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=parent_id)
|
||||
)
|
||||
except Exception as e:
|
||||
import sys
|
||||
|
||||
print(f"Error fetching {parent_id}: {e}", file=sys.stderr)
|
||||
return []
|
||||
child_contests = parent_data.get("child_contests", {})
|
||||
if not child_contests:
|
||||
return []
|
||||
base_name = f"Starters {i}"
|
||||
divisions = []
|
||||
for div_key, div_data in child_contests.items():
|
||||
div_code = div_data.get("contest_code", "")
|
||||
div_num = div_data.get("div", {}).get("div_number", "")
|
||||
if div_code and div_num:
|
||||
divisions.append(
|
||||
ContestSummary(
|
||||
id=div_code,
|
||||
name=base_name,
|
||||
display_name=f"{base_name} (Div. {div_num})",
|
||||
)
|
||||
)
|
||||
return divisions
|
||||
|
||||
tasks = [fetch_divisions(i) for i in range(1, max_num + 1)]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
divisions = await coro
|
||||
contests.extend(divisions)
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_connections=CONNECTIONS)
|
||||
) as client:
|
||||
try:
|
||||
contest_data = await fetch_json(
|
||||
client, API_CONTEST.format(contest_id=category_id)
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"Failed to fetch contest {category_id}: {str(e)}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
all_problems = contest_data.get("problems", {})
|
||||
if not all_problems:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"No problems found for contest {category_id}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
problems = {
|
||||
code: data
|
||||
for code, data in all_problems.items()
|
||||
if data.get("category_name") == "main"
|
||||
}
|
||||
if not problems:
|
||||
print(
|
||||
json.dumps(
|
||||
{"error": f"No main problems found for contest {category_id}"}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
sem = asyncio.Semaphore(CONNECTIONS)
|
||||
|
||||
async def run_one(problem_code: str) -> dict[str, Any]:
|
||||
async with sem:
|
||||
try:
|
||||
problem_data = await fetch_json(
|
||||
client,
|
||||
API_PROBLEM.format(
|
||||
contest_id=category_id, problem_id=problem_code
|
||||
),
|
||||
)
|
||||
sample_tests = (
|
||||
problem_data.get("problemComponents", {}).get(
|
||||
"sampleTestCases", []
|
||||
)
|
||||
or []
|
||||
)
|
||||
tests = [
|
||||
TestCase(
|
||||
input=t.get("input", "").strip(),
|
||||
expected=t.get("output", "").strip(),
|
||||
)
|
||||
for t in sample_tests
|
||||
if not t.get("isDeleted", False)
|
||||
]
|
||||
time_limit_str = problem_data.get("max_timelimit", "1")
|
||||
timeout_ms = int(float(time_limit_str) * 1000)
|
||||
problem_url = PROBLEM_URL.format(problem_id=problem_code)
|
||||
loop = asyncio.get_event_loop()
|
||||
html = await loop.run_in_executor(
|
||||
None, _fetch_html_sync, problem_url
|
||||
)
|
||||
memory_mb = _extract_memory_limit(html)
|
||||
interactive = False
|
||||
except Exception:
|
||||
tests = []
|
||||
timeout_ms = 1000
|
||||
memory_mb = 256.0
|
||||
interactive = False
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = (
|
||||
"\n".join(t.expected for t in tests) if tests else ""
|
||||
)
|
||||
return {
|
||||
"problem_id": problem_code,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
"multi_test": False,
|
||||
}
|
||||
|
||||
tasks = [run_one(problem_code) for problem_code in problems.keys()]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
payload = await coro
|
||||
print(json.dumps(payload), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
CodeChefScraper().run_cli()
|
||||
|
|
@ -2,15 +2,12 @@
|
|||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from scrapling.fetchers import StealthyFetcher
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
|
|
@ -19,13 +16,8 @@ from .models import (
|
|||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
TestsResult,
|
||||
)
|
||||
|
||||
# suppress scrapling logging - https://github.com/D4Vinci/Scrapling/issues/31)
|
||||
logging.getLogger("scrapling").setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
BASE_URL = "https://codeforces.com"
|
||||
API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list"
|
||||
TIMEOUT_SECONDS = 30
|
||||
|
|
@ -63,8 +55,6 @@ def _extract_limits(block: Tag) -> tuple[int, float]:
|
|||
|
||||
def _group_lines_by_id(pre: Tag) -> dict[int, list[str]]:
|
||||
groups: dict[int, list[str]] = {}
|
||||
if not isinstance(pre, Tag):
|
||||
return groups
|
||||
for div in pre.find_all("div", class_="test-example-line"):
|
||||
cls = " ".join(div.get("class", []))
|
||||
m = re.search(r"\btest-example-line-(\d+)\b", cls)
|
||||
|
|
@ -86,19 +76,19 @@ def _extract_title(block: Tag) -> tuple[str, str]:
|
|||
return parts[0].strip().upper(), parts[1].strip()
|
||||
|
||||
|
||||
def _extract_samples(block: Tag) -> list[TestCase]:
|
||||
def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]:
|
||||
st = block.find("div", class_="sample-test")
|
||||
if not st:
|
||||
return []
|
||||
if not isinstance(st, Tag):
|
||||
return [], False
|
||||
|
||||
input_pres: list[Tag] = [ # type: ignore[misc]
|
||||
inp.find("pre") # type: ignore[misc]
|
||||
for inp in st.find_all("div", class_="input") # type: ignore[union-attr]
|
||||
input_pres: list[Tag] = [
|
||||
inp.find("pre")
|
||||
for inp in st.find_all("div", class_="input")
|
||||
if isinstance(inp, Tag) and inp.find("pre")
|
||||
]
|
||||
output_pres: list[Tag] = [
|
||||
out.find("pre") # type: ignore[misc]
|
||||
for out in st.find_all("div", class_="output") # type: ignore[union-attr]
|
||||
out.find("pre")
|
||||
for out in st.find_all("div", class_="output")
|
||||
if isinstance(out, Tag) and out.find("pre")
|
||||
]
|
||||
input_pres = [p for p in input_pres if isinstance(p, Tag)]
|
||||
|
|
@ -122,18 +112,19 @@ def _extract_samples(block: Tag) -> list[TestCase]:
|
|||
outputs_by_gid.pop(0, None)
|
||||
keys = sorted(set(inputs_by_gid.keys()) & set(outputs_by_gid.keys()))
|
||||
if keys:
|
||||
return [
|
||||
samples = [
|
||||
TestCase(
|
||||
input="\n".join(inputs_by_gid[k]).strip(),
|
||||
expected="\n".join(outputs_by_gid[k]).strip(),
|
||||
)
|
||||
for k in keys
|
||||
]
|
||||
return samples, True
|
||||
|
||||
inputs = [_text_from_pre(p) for p in input_pres]
|
||||
outputs = [_text_from_pre(p) for p in output_pres]
|
||||
n = min(len(inputs), len(outputs))
|
||||
return [TestCase(input=inputs[i], expected=outputs[i]) for i in range(n)]
|
||||
return [TestCase(input=inputs[i], expected=outputs[i]) for i in range(n)], False
|
||||
|
||||
|
||||
def _is_interactive(block: Tag) -> bool:
|
||||
|
|
@ -144,12 +135,9 @@ def _is_interactive(block: Tag) -> bool:
|
|||
|
||||
def _fetch_problems_html(contest_id: str) -> str:
|
||||
url = f"{BASE_URL}/contest/{contest_id}/problems"
|
||||
page = StealthyFetcher.fetch(
|
||||
url,
|
||||
headless=True,
|
||||
solve_cloudflare=True,
|
||||
)
|
||||
return page.html_content
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
||||
|
|
@ -159,20 +147,38 @@ def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
|||
for b in blocks:
|
||||
holder = b.find_parent("div", class_="problemindexholder")
|
||||
letter = (holder.get("problemindex") if holder else "").strip().upper()
|
||||
name = _extract_title(b)[1] # keep your name extraction
|
||||
name = _extract_title(b)[1]
|
||||
if not letter:
|
||||
continue
|
||||
tests = _extract_samples(b)
|
||||
raw_samples, is_grouped = _extract_samples(b)
|
||||
timeout_ms, memory_mb = _extract_limits(b)
|
||||
interactive = _is_interactive(b)
|
||||
|
||||
if is_grouped and raw_samples:
|
||||
combined_input = f"{len(raw_samples)}\n" + "\n".join(
|
||||
tc.input for tc in raw_samples
|
||||
)
|
||||
combined_expected = "\n".join(tc.expected for tc in raw_samples)
|
||||
individual_tests = [
|
||||
TestCase(input=f"1\n{tc.input}", expected=tc.expected)
|
||||
for tc in raw_samples
|
||||
]
|
||||
else:
|
||||
combined_input = "\n".join(tc.input for tc in raw_samples)
|
||||
combined_expected = "\n".join(tc.expected for tc in raw_samples)
|
||||
individual_tests = raw_samples
|
||||
|
||||
out.append(
|
||||
{
|
||||
"letter": letter,
|
||||
"name": name,
|
||||
"tests": tests,
|
||||
"combined_input": combined_input,
|
||||
"combined_expected": combined_expected,
|
||||
"tests": individual_tests,
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": interactive,
|
||||
"multi_test": is_grouped,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
|
@ -182,12 +188,8 @@ def _scrape_contest_problems_sync(contest_id: str) -> list[ProblemSummary]:
|
|||
html = _fetch_problems_html(contest_id)
|
||||
blocks = _parse_all_blocks(html)
|
||||
problems: list[ProblemSummary] = []
|
||||
seen: set[str] = set()
|
||||
for b in blocks:
|
||||
pid = b["letter"].upper()
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
problems.append(ProblemSummary(id=pid.lower(), name=b["name"]))
|
||||
return problems
|
||||
|
||||
|
|
@ -198,45 +200,46 @@ class CodeforcesScraper(BaseScraper):
|
|||
return "codeforces"
|
||||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
async def impl(cid: str) -> MetadataResult:
|
||||
problems = await asyncio.to_thread(_scrape_contest_problems_sync, cid)
|
||||
try:
|
||||
problems = await asyncio.to_thread(
|
||||
_scrape_contest_problems_sync, contest_id
|
||||
)
|
||||
if not problems:
|
||||
return self._create_metadata_error(
|
||||
f"No problems found for contest {cid}", cid
|
||||
return self._metadata_error(
|
||||
f"No problems found for contest {contest_id}"
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True, error="", contest_id=cid, problems=problems
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url=f"https://codeforces.com/contest/{contest_id}/problem/%s",
|
||||
)
|
||||
|
||||
return await self._safe_execute("metadata", impl, contest_id)
|
||||
except Exception as e:
|
||||
return self._metadata_error(str(e))
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
async def impl() -> ContestListResult:
|
||||
try:
|
||||
r = requests.get(API_CONTEST_LIST_URL, timeout=TIMEOUT_SECONDS)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("status") != "OK":
|
||||
return self._create_contests_error("Invalid API response")
|
||||
try:
|
||||
r = requests.get(API_CONTEST_LIST_URL, timeout=TIMEOUT_SECONDS)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("status") != "OK":
|
||||
return self._contests_error("Invalid API response")
|
||||
|
||||
contests: list[ContestSummary] = []
|
||||
for c in data["result"]:
|
||||
if c.get("phase") != "FINISHED":
|
||||
continue
|
||||
cid = str(c["id"])
|
||||
name = c["name"]
|
||||
contests.append(
|
||||
ContestSummary(id=cid, name=name, display_name=name)
|
||||
)
|
||||
contests: list[ContestSummary] = []
|
||||
for c in data["result"]:
|
||||
if c.get("phase") != "FINISHED":
|
||||
continue
|
||||
cid = str(c["id"])
|
||||
name = c["name"]
|
||||
contests.append(ContestSummary(id=cid, name=name, display_name=name))
|
||||
|
||||
if not contests:
|
||||
return self._create_contests_error("No contests found")
|
||||
if not contests:
|
||||
return self._contests_error("No contests found")
|
||||
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
except Exception as e:
|
||||
return self._create_contests_error(str(e))
|
||||
|
||||
return await self._safe_execute("contests", impl)
|
||||
return ContestListResult(success=True, error="", contests=contests)
|
||||
except Exception as e:
|
||||
return self._contests_error(str(e))
|
||||
|
||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||
html = await asyncio.to_thread(_fetch_problems_html, category_id)
|
||||
|
|
@ -249,81 +252,22 @@ class CodeforcesScraper(BaseScraper):
|
|||
json.dumps(
|
||||
{
|
||||
"problem_id": pid,
|
||||
"combined": {
|
||||
"input": b.get("combined_input", ""),
|
||||
"expected": b.get("combined_expected", ""),
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": b.get("timeout_ms", 0),
|
||||
"memory_mb": b.get("memory_mb", 0),
|
||||
"interactive": bool(b.get("interactive")),
|
||||
"multi_test": bool(b.get("multi_test", False)),
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
async def main_async() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Usage: codeforces.py metadata <contest_id> OR codeforces.py tests <contest_id> OR codeforces.py contests",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
scraper = CodeforcesScraper()
|
||||
|
||||
if mode == "metadata":
|
||||
if len(sys.argv) != 3:
|
||||
result = MetadataResult(
|
||||
success=False, error="Usage: codeforces.py metadata <contest_id>"
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
result = await scraper.scrape_contest_metadata(contest_id)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 0 if result.success else 1
|
||||
|
||||
if mode == "tests":
|
||||
if len(sys.argv) != 3:
|
||||
tests_result = TestsResult(
|
||||
success=False,
|
||||
error="Usage: codeforces.py tests <contest_id>",
|
||||
problem_id="",
|
||||
url="",
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
print(json.dumps(asdict(tests_result)))
|
||||
return 1
|
||||
contest_id = sys.argv[2]
|
||||
await scraper.stream_tests_for_category_async(contest_id)
|
||||
return 0
|
||||
|
||||
if mode == "contests":
|
||||
if len(sys.argv) != 2:
|
||||
contest_result = ContestListResult(
|
||||
success=False, error="Usage: codeforces.py contests"
|
||||
)
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
return 1
|
||||
contest_result = await scraper.scrape_contest_list()
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
return 0 if contest_result.success else 1
|
||||
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Unknown mode. Use 'metadata <contest_id>', 'tests <contest_id>', or 'contests'",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sys.exit(asyncio.run(main_async()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
CodeforcesScraper().run_cli()
|
||||
|
|
|
|||
118
scrapers/cses.py
118
scrapers/cses.py
|
|
@ -3,8 +3,6 @@
|
|||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
|
@ -16,11 +14,10 @@ from .models import (
|
|||
MetadataResult,
|
||||
ProblemSummary,
|
||||
TestCase,
|
||||
TestsResult,
|
||||
)
|
||||
|
||||
BASE_URL = "https://cses.fi"
|
||||
INDEX_PATH = "/problemset/list"
|
||||
INDEX_PATH = "/problemset"
|
||||
TASK_PATH = "/problemset/task/{id}"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
|
|
@ -73,24 +70,24 @@ async def fetch_text(client: httpx.AsyncClient, path: str) -> str:
|
|||
|
||||
|
||||
CATEGORY_BLOCK_RE = re.compile(
|
||||
r'<h2>(?P<cat>[^<]+)</h2>\s*<ul class="task-list">(?P<body>.*?)</ul>',
|
||||
r'<h2>(?P<cat>[^<]+)</h2>\s*<ul\s+class="task-list">(?P<body>.*?)</ul>',
|
||||
re.DOTALL,
|
||||
)
|
||||
TASK_LINK_RE = re.compile(
|
||||
r'<li class="task"><a href="/problemset/task/(?P<id>\d+)/?">(?P<title>[^<]+)</a>',
|
||||
r'<li\s+class="task">\s*<a\s+href="/problemset/task/(?P<id>\d+)/?">(?P<title>[^<]+)</a\s*>',
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
TITLE_RE = re.compile(
|
||||
r'<div class="title-block">.*?<h1>(?P<title>[^<]+)</h1>', re.DOTALL
|
||||
r'<div\s+class="title-block">.*?<h1>(?P<title>[^<]+)</h1>', re.DOTALL
|
||||
)
|
||||
TIME_RE = re.compile(r"<li><b>Time limit:</b>\s*([0-9.]+)\s*s</li>")
|
||||
MEM_RE = re.compile(r"<li><b>Memory limit:</b>\s*(\d+)\s*MB</li>")
|
||||
TIME_RE = re.compile(r"<li>\s*<b>Time limit:</b>\s*([0-9.]+)\s*s\s*</li>")
|
||||
MEM_RE = re.compile(r"<li>\s*<b>Memory limit:</b>\s*(\d+)\s*MB\s*</li>")
|
||||
SIDEBAR_CAT_RE = re.compile(
|
||||
r'<div class="nav sidebar">.*?<h4>(?P<cat>[^<]+)</h4>', re.DOTALL
|
||||
r'<div\s+class="nav sidebar">.*?<h4>(?P<cat>[^<]+)</h4>', re.DOTALL
|
||||
)
|
||||
|
||||
MD_BLOCK_RE = re.compile(r'<div class="md">(.*?)</div>', re.DOTALL | re.IGNORECASE)
|
||||
MD_BLOCK_RE = re.compile(r'<div\s+class="md">(.*?)</div>', re.DOTALL | re.IGNORECASE)
|
||||
EXAMPLE_SECTION_RE = re.compile(
|
||||
r"<h[1-6][^>]*>\s*example[s]?:?\s*</h[1-6]>\s*(?P<section>.*?)(?=<h[1-6][^>]*>|$)",
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
|
|
@ -132,12 +129,17 @@ def parse_category_problems(category_id: str, html: str) -> list[ProblemSummary]
|
|||
return []
|
||||
|
||||
|
||||
def parse_limits(html: str) -> tuple[int, int]:
|
||||
def _extract_problem_info(html: str) -> tuple[int, int, bool]:
|
||||
tm = TIME_RE.search(html)
|
||||
mm = MEM_RE.search(html)
|
||||
t = int(round(float(tm.group(1)) * 1000)) if tm else 0
|
||||
m = int(mm.group(1)) if mm else 0
|
||||
return t, m
|
||||
md = MD_BLOCK_RE.search(html)
|
||||
interactive = False
|
||||
if md:
|
||||
body = md.group(1)
|
||||
interactive = "This is an interactive problem." in body
|
||||
return t, m, interactive
|
||||
|
||||
|
||||
def parse_title(html: str) -> str:
|
||||
|
|
@ -189,9 +191,14 @@ class CSESScraper(BaseScraper):
|
|||
return MetadataResult(
|
||||
success=False,
|
||||
error=f"{self.platform_name}: No problems found for category: {contest_id}",
|
||||
url="",
|
||||
)
|
||||
return MetadataResult(
|
||||
success=True, error="", contest_id=contest_id, problems=problems
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=contest_id,
|
||||
problems=problems,
|
||||
url="https://cses.fi/problemset/task/%s",
|
||||
)
|
||||
|
||||
async def scrape_contest_list(self) -> ContestListResult:
|
||||
|
|
@ -220,18 +227,29 @@ class CSESScraper(BaseScraper):
|
|||
try:
|
||||
html = await fetch_text(client, task_path(pid))
|
||||
tests = parse_tests(html)
|
||||
timeout_ms, memory_mb = parse_limits(html)
|
||||
timeout_ms, memory_mb, interactive = _extract_problem_info(html)
|
||||
except Exception:
|
||||
tests = []
|
||||
timeout_ms, memory_mb = 0, 0
|
||||
timeout_ms, memory_mb, interactive = 0, 0, False
|
||||
|
||||
combined_input = "\n".join(t.input for t in tests) if tests else ""
|
||||
combined_expected = (
|
||||
"\n".join(t.expected for t in tests) if tests else ""
|
||||
)
|
||||
|
||||
return {
|
||||
"problem_id": pid,
|
||||
"combined": {
|
||||
"input": combined_input,
|
||||
"expected": combined_expected,
|
||||
},
|
||||
"tests": [
|
||||
{"input": t.input, "expected": t.expected} for t in tests
|
||||
],
|
||||
"timeout_ms": timeout_ms,
|
||||
"memory_mb": memory_mb,
|
||||
"interactive": False,
|
||||
"interactive": interactive,
|
||||
"multi_test": False,
|
||||
}
|
||||
|
||||
tasks = [run_one(p.id) for p in problems]
|
||||
|
|
@ -240,69 +258,5 @@ class CSESScraper(BaseScraper):
|
|||
print(json.dumps(payload), flush=True)
|
||||
|
||||
|
||||
async def main_async() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error="Usage: cses.py metadata <category_id> OR cses.py tests <category> OR cses.py contests",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
|
||||
mode: str = sys.argv[1]
|
||||
scraper = CSESScraper()
|
||||
|
||||
if mode == "metadata":
|
||||
if len(sys.argv) != 3:
|
||||
result = MetadataResult(
|
||||
success=False, error="Usage: cses.py metadata <category_id>"
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
category_id = sys.argv[2]
|
||||
result = await scraper.scrape_contest_metadata(category_id)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 0 if result.success else 1
|
||||
|
||||
if mode == "tests":
|
||||
if len(sys.argv) != 3:
|
||||
tests_result = TestsResult(
|
||||
success=False,
|
||||
error="Usage: cses.py tests <category>",
|
||||
problem_id="",
|
||||
url="",
|
||||
tests=[],
|
||||
timeout_ms=0,
|
||||
memory_mb=0,
|
||||
)
|
||||
print(json.dumps(asdict(tests_result)))
|
||||
return 1
|
||||
category = sys.argv[2]
|
||||
await scraper.stream_tests_for_category_async(category)
|
||||
return 0
|
||||
|
||||
if mode == "contests":
|
||||
if len(sys.argv) != 2:
|
||||
contest_result = ContestListResult(
|
||||
success=False, error="Usage: cses.py contests"
|
||||
)
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
return 1
|
||||
contest_result = await scraper.scrape_contest_list()
|
||||
print(json.dumps(asdict(contest_result)))
|
||||
return 0 if contest_result.success else 1
|
||||
|
||||
result = MetadataResult(
|
||||
success=False,
|
||||
error=f"Unknown mode: {mode}. Use 'metadata <category>', 'tests <category>', or 'contests'",
|
||||
)
|
||||
print(json.dumps(asdict(result)))
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sys.exit(asyncio.run(main_async()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
CSESScraper().run_cli()
|
||||
|
|
|
|||
|
|
@ -1,47 +1,72 @@
|
|||
from dataclasses import dataclass, field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
class TestCase(BaseModel):
|
||||
input: str
|
||||
expected: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@dataclass
|
||||
class ProblemSummary:
|
||||
|
||||
class CombinedTest(BaseModel):
|
||||
input: str
|
||||
expected: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ProblemSummary(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@dataclass
|
||||
class ContestSummary:
|
||||
|
||||
class ContestSummary(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
display_name: str
|
||||
display_name: str | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScrapingResult:
|
||||
class ScrapingResult(BaseModel):
|
||||
success: bool
|
||||
error: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetadataResult(ScrapingResult):
|
||||
contest_id: str = ""
|
||||
problems: list[ProblemSummary] = field(default_factory=list)
|
||||
problems: list[ProblemSummary] = Field(default_factory=list)
|
||||
url: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContestListResult(ScrapingResult):
|
||||
contests: list[ContestSummary] = field(default_factory=list)
|
||||
contests: list[ContestSummary] = Field(default_factory=list)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestsResult(ScrapingResult):
|
||||
problem_id: str
|
||||
url: str
|
||||
tests: list[TestCase]
|
||||
combined: CombinedTest
|
||||
tests: list[TestCase] = Field(default_factory=list)
|
||||
timeout_ms: int
|
||||
memory_mb: float
|
||||
interactive: bool = False
|
||||
multi_test: bool = False
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ScraperConfig(BaseModel):
|
||||
timeout_seconds: int = 30
|
||||
max_retries: int = 3
|
||||
backoff_base: float = 2.0
|
||||
rate_limit_delay: float = 1.0
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ async def pump(
|
|||
data = await reader.readline()
|
||||
if not data:
|
||||
break
|
||||
sys.stdout.buffer.write(data)
|
||||
sys.stdout.flush()
|
||||
_ = sys.stdout.buffer.write(data)
|
||||
_ = sys.stdout.flush()
|
||||
if writer:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
|
|
@ -42,9 +42,9 @@ async def main(interactor_cmd: Sequence[str], interactee_cmd: Sequence[str]) ->
|
|||
asyncio.create_task(pump(interactor.stdout, interactee.stdin)),
|
||||
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
|
||||
]
|
||||
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||
await interactor.wait()
|
||||
await interactee.wait()
|
||||
_ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||
_ = await interactor.wait()
|
||||
_ = await interactee.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
@ -55,4 +55,4 @@ if __name__ == "__main__":
|
|||
interactor_cmd = shlex.split(sys.argv[1])
|
||||
interactee_cmd = shlex.split(sys.argv[2])
|
||||
|
||||
asyncio.run(main(interactor_cmd, interactee_cmd))
|
||||
_ = asyncio.run(main(interactor_cmd, interactee_cmd))
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
describe('run module', function()
|
||||
local run = require('cp.runner.run')
|
||||
|
||||
describe('basic functionality', function()
|
||||
it('can get panel state', function()
|
||||
local state = run.get_run_panel_state()
|
||||
assert.is_table(state)
|
||||
assert.is_table(state.test_cases)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
275
tests/conftest.py
Normal file
275
tests/conftest.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import asyncio
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import requests
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FIX = Path(__file__).resolve().parent / "fixtures"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_text():
|
||||
def _load(name: str) -> str:
|
||||
p = FIX / name
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
return _load
|
||||
|
||||
|
||||
def _load_scraper_module(module_path: Path, module_name: str):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"scrapers.{module_name}", module_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Cannot load module {module_name}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[f"scrapers.{module_name}"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _capture_stdout(coro):
|
||||
buf = io.StringIO()
|
||||
old = sys.stdout
|
||||
sys.stdout = buf
|
||||
try:
|
||||
rc = asyncio.run(coro)
|
||||
out = buf.getvalue()
|
||||
finally:
|
||||
sys.stdout = old
|
||||
return rc, out
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_scraper_offline(fixture_text):
|
||||
def _router_cses(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not path and not url:
|
||||
raise AssertionError("CSES expects path or url")
|
||||
|
||||
target = path or url
|
||||
if target is None:
|
||||
raise AssertionError(f"No target for CSES (path={path!r}, url={url!r})")
|
||||
|
||||
if target.startswith("https://cses.fi"):
|
||||
target = target.removeprefix("https://cses.fi")
|
||||
|
||||
if target.strip("/") == "problemset":
|
||||
return fixture_text("cses/contests.html")
|
||||
|
||||
if target.startswith("/problemset/task/") or target.startswith(
|
||||
"problemset/task/"
|
||||
):
|
||||
pid = target.rstrip("/").split("/")[-1]
|
||||
return fixture_text(f"cses/task_{pid}.html")
|
||||
|
||||
raise AssertionError(f"No fixture for CSES path={path!r} url={url!r}")
|
||||
|
||||
def _router_atcoder(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not url:
|
||||
raise AssertionError("AtCoder expects url routing")
|
||||
if "/contests/archive" in url:
|
||||
return fixture_text("atcoder/contests.html")
|
||||
if url.endswith("/tasks"):
|
||||
return fixture_text("atcoder/abc100_tasks.html")
|
||||
if "/tasks/" in url:
|
||||
slug = url.rsplit("/", 1)[-1]
|
||||
return fixture_text(f"atcoder/task_{slug}.html")
|
||||
raise AssertionError(f"No fixture for AtCoder url={url!r}")
|
||||
|
||||
def _router_codeforces(*, path: str | None = None, url: str | None = None) -> str:
|
||||
if not url:
|
||||
raise AssertionError("Codeforces expects url routing")
|
||||
if "/contest/" in url and url.endswith("/problems"):
|
||||
contest_id = url.rstrip("/").split("/")[-2]
|
||||
return fixture_text(f"codeforces/{contest_id}_problems.html")
|
||||
if "/contests" in url and "/problem/" not in url:
|
||||
return fixture_text("codeforces/contests.html")
|
||||
if "/problem/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id, index = parts[-3], parts[-1]
|
||||
return fixture_text(f"codeforces/{contest_id}_{index}.html")
|
||||
if "/problemset/problem/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id, index = parts[-2], parts[-1]
|
||||
return fixture_text(f"codeforces/{contest_id}_{index}.html")
|
||||
|
||||
raise AssertionError(f"No fixture for Codeforces url={url!r}")
|
||||
|
||||
def _make_offline_fetches(scraper_name: str):
|
||||
match scraper_name:
|
||||
case "cses":
|
||||
|
||||
async def __offline_fetch_text(client, path: str, **kwargs):
|
||||
html = _router_cses(path=path)
|
||||
return SimpleNamespace(
|
||||
text=html,
|
||||
status_code=200,
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
|
||||
return {
|
||||
"__offline_fetch_text": __offline_fetch_text,
|
||||
}
|
||||
|
||||
case "atcoder":
|
||||
|
||||
def __offline_fetch(url: str, *args, **kwargs):
|
||||
html = _router_atcoder(url=url)
|
||||
return html
|
||||
|
||||
async def __offline_get_async(client, url: str, **kwargs):
|
||||
return _router_atcoder(url=url)
|
||||
|
||||
return {
|
||||
"_fetch": __offline_fetch,
|
||||
"_get_async": __offline_get_async,
|
||||
}
|
||||
|
||||
case "codeforces":
|
||||
|
||||
class MockCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.text = html
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
return MockCurlResponse(_router_codeforces(url=url))
|
||||
|
||||
def _mock_requests_get(url: str, **kwargs):
|
||||
if "api/contest.list" in url:
|
||||
data = {
|
||||
"status": "OK",
|
||||
"result": [
|
||||
{
|
||||
"id": 1550,
|
||||
"name": "Educational Codeforces Round 155 (Rated for Div. 2)",
|
||||
"phase": "FINISHED",
|
||||
},
|
||||
{
|
||||
"id": 1000,
|
||||
"name": "Codeforces Round #1000",
|
||||
"phase": "FINISHED",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
class R:
|
||||
def json(self_inner):
|
||||
return data
|
||||
|
||||
def raise_for_status(self_inner):
|
||||
return None
|
||||
|
||||
return R()
|
||||
raise AssertionError(f"Unexpected requests.get call: {url}")
|
||||
|
||||
return {
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
"requests.get": _mock_requests_get,
|
||||
}
|
||||
|
||||
case "codechef":
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, json_data):
|
||||
self._json_data = json_data
|
||||
self.status_code = 200
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
async def __offline_get_async(client, url: str, **kwargs):
|
||||
if "/api/list/contests/all" in url:
|
||||
data = json.loads(fixture_text("codechef/contests.json"))
|
||||
return MockResponse(data)
|
||||
if "/api/contests/START" in url and "/problems/" not in url:
|
||||
contest_id = url.rstrip("/").split("/")[-1]
|
||||
try:
|
||||
data = json.loads(
|
||||
fixture_text(f"codechef/{contest_id}.json")
|
||||
)
|
||||
return MockResponse(data)
|
||||
except FileNotFoundError:
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
if "/api/contests/START" in url and "/problems/" in url:
|
||||
parts = url.rstrip("/").split("/")
|
||||
contest_id = parts[-3]
|
||||
problem_id = parts[-1]
|
||||
data = json.loads(
|
||||
fixture_text(f"codechef/{contest_id}_{problem_id}.json")
|
||||
)
|
||||
return MockResponse(data)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
class MockCodeChefCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.text = html
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
if "/problems/" in url:
|
||||
problem_id = url.rstrip("/").split("/")[-1]
|
||||
html = fixture_text(f"codechef/{problem_id}.html")
|
||||
return MockCodeChefCurlResponse(html)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
return {
|
||||
"__offline_get_async": __offline_get_async,
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
}
|
||||
|
||||
case _:
|
||||
raise AssertionError(f"Unknown scraper: {scraper_name}")
|
||||
|
||||
scraper_classes = {
|
||||
"cses": "CSESScraper",
|
||||
"atcoder": "AtcoderScraper",
|
||||
"codeforces": "CodeforcesScraper",
|
||||
"codechef": "CodeChefScraper",
|
||||
}
|
||||
|
||||
def _run(scraper_name: str, mode: str, *args: str):
|
||||
mod_path = ROOT / "scrapers" / f"{scraper_name}.py"
|
||||
ns = _load_scraper_module(mod_path, scraper_name)
|
||||
offline_fetches = _make_offline_fetches(scraper_name)
|
||||
|
||||
if scraper_name == "codeforces":
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
requests.get = offline_fetches["requests.get"]
|
||||
elif scraper_name == "atcoder":
|
||||
ns._fetch = offline_fetches["_fetch"]
|
||||
ns._get_async = offline_fetches["_get_async"]
|
||||
elif scraper_name == "cses":
|
||||
httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"]
|
||||
elif scraper_name == "codechef":
|
||||
httpx.AsyncClient.get = offline_fetches["__offline_get_async"]
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
|
||||
scraper_class = getattr(ns, scraper_classes[scraper_name])
|
||||
scraper = scraper_class()
|
||||
|
||||
argv = [str(mod_path), mode, *args]
|
||||
rc, out = _capture_stdout(scraper._run_cli_async(argv))
|
||||
|
||||
json_lines: list[Any] = []
|
||||
for line in (_line for _line in out.splitlines() if _line.strip()):
|
||||
json_lines.append(json.loads(line))
|
||||
return rc, json_lines
|
||||
|
||||
return _run
|
||||
519
tests/fixtures/atcoder/abc100_tasks.html
vendored
Normal file
519
tests/fixtures/atcoder/abc100_tasks.html
vendored
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tasks - AtCoder Beginner Contest 100</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="Tasks - AtCoder Beginner Contest 100" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="Tasks - AtCoder Beginner Contest 100"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'q+4tZ4tLQh/4nobcpVAuiml6OVEZOdZDZURhPenxPbc='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="q+4tZ4tLQh/4nobcpVAuiml6OVEZOdZDZURhPenxPbc="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<h2>Tasks</h2>
|
||||
<hr />
|
||||
|
||||
<div class="panel panel-default table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="3%" class="text-center"></th>
|
||||
<th>Task Name</th>
|
||||
<th width="10%" class="text-right no-break">Time Limit</th>
|
||||
<th width="10%" class="text-right no-break">
|
||||
Memory Limit
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_a">A</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_a"
|
||||
>Happy Birthday!</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_b">B</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_b"
|
||||
>Ringo's Favorite Numbers</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_c">C</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_c">*3 or /2</a>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center no-break">
|
||||
<a href="/contests/abc100/tasks/abc100_d">D</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contests/abc100/tasks/abc100_d"
|
||||
>Patisserie ABC</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-right">2 sec</td>
|
||||
<td class="text-right">976 MiB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="btn-text-group">
|
||||
<a class="btn-text" href="/contests/abc100/tasks_print"
|
||||
>Tasks for printing</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks?lang=en"
|
||||
data-a2a-title="Tasks - AtCoder Beginner Contest 100"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1902
tests/fixtures/atcoder/contests.html
vendored
Normal file
1902
tests/fixtures/atcoder/contests.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
885
tests/fixtures/atcoder/task_abc100_a.html
vendored
Normal file
885
tests/fixtures/atcoder/task_abc100_a.html
vendored
Normal file
|
|
@ -0,0 +1,885 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>A - Happy Birthday!</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="A - Happy Birthday!" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_a"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="A - Happy Birthday!" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'RRHWPnDZqM+tdZpgbKRjH5FxiX5spw9S3/HKRbnyqok='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_a?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_a?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_a"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="RRHWPnDZqM+tdZpgbKRjH5FxiX5spw9S3/HKRbnyqok="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
A - Happy Birthday!
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_a/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>100</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
もうすぐ E869120 君と square1001 君の
|
||||
<var>16</var> 才の誕生日が来る.<br />
|
||||
そこで, AtCoder 王国の高橋君は, 円形のケーキ
|
||||
<var>1</var> 個に放射状に切れ目を入れ
|
||||
<var>16</var> 等分したものを, 彼らにプレゼントした.
|
||||
</p>
|
||||
<p>
|
||||
E869120 君はそのうち <var>A</var> 切れ、square1001 君は
|
||||
<var>B</var> 切れを食べようとした.<br />
|
||||
しかし, ケーキと一緒についていた紙を見ると,
|
||||
「同じ人が隣り合う
|
||||
<var>2</var>
|
||||
切れのケーキを両方取ってはならない」と書かれていた.
|
||||
</p>
|
||||
<p>
|
||||
さて、彼らは紙に書かれたことを守って、<var>2</var>
|
||||
人とも食べたい数のケーキを取ることができるだろうか?
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>A, B</var> は <var>1</var> 以上
|
||||
<var>16</var> 以下の整数
|
||||
</li>
|
||||
<li><var>A+B</var> は <var>16</var> 以下である.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>A</var> <var>B</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>
|
||||
紙に書かれたことを守って, E869120 君と square1001
|
||||
君両方が, 食べたい数のケーキを取ることができるならば
|
||||
<code>Yay!</code>, そうでなければ
|
||||
<code>:(</code> と出力しなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
5 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
下の図のようにケーキを取れば、<var>2</var>
|
||||
人とも目標を達成することができる.<br />
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/e87fa456a900ac9ae36671ae8bd5eeea.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
8 8
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
下の図のようにケーキを取れば、<var>2</var>
|
||||
人とも目標を達成することができる.<br />
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/a7989ac033e6ba86e14078864c20d9c5.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
11 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
:(
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
この場合, 残念ながら目標を達成する方法は
|
||||
<var>1</var> つもない.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>100</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
E869120's and square1001's <var>16</var>-th birthday is
|
||||
coming soon.<br />
|
||||
Takahashi from AtCoder Kingdom gave them a round cake
|
||||
cut into <var>16</var> equal fan-shaped pieces.
|
||||
</p>
|
||||
<p>
|
||||
E869120 and square1001 were just about to eat
|
||||
<var>A</var> and <var>B</var> of those pieces,
|
||||
respectively,<br />
|
||||
when they found a note attached to the cake saying that
|
||||
"the same person should not take two adjacent pieces of
|
||||
cake".
|
||||
</p>
|
||||
<p>
|
||||
Can both of them obey the instruction in the note and
|
||||
take desired numbers of pieces of cake?
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>A</var> and <var>B</var> are integers between
|
||||
<var>1</var> and <var>16</var> (inclusive).
|
||||
</li>
|
||||
<li><var>A+B</var> is at most <var>16</var>.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>A</var> <var>B</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
If both E869120 and square1001 can obey the
|
||||
instruction in the note and take desired numbers of
|
||||
pieces of cake, print <code>Yay!</code>; otherwise,
|
||||
print <code>:(</code>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
5 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
Both of them can take desired number of pieces as
|
||||
follows:
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/e87fa456a900ac9ae36671ae8bd5eeea.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
8 8
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
Yay!
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
Both of them can take desired number of pieces as
|
||||
follows:
|
||||
<img
|
||||
alt=" "
|
||||
src="https://img.atcoder.jp/abc100/a7989ac033e6ba86e14078864c20d9c5.png"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
11 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
:(
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
In this case, there is no way for them to take desired
|
||||
number of pieces, unfortunately.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_a?lang=en"
|
||||
data-a2a-title="A - Happy Birthday!"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
887
tests/fixtures/atcoder/task_abc100_b.html
vendored
Normal file
887
tests/fixtures/atcoder/task_abc100_b.html
vendored
Normal file
|
|
@ -0,0 +1,887 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>B - Ringo's Favorite Numbers</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="B - Ringo's Favorite Numbers" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_b"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="B - Ringo's Favorite Numbers" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'jcqZoHtgdlDPapU/uheo04+cw+2+EssGGNrF7tJr004='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_b?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_b?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_b"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="jcqZoHtgdlDPapU/uheo04+cw+2+EssGGNrF7tJr004="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
B - Ringo's Favorite Numbers
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_b/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>200</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
今日は, 記念すべき AtCoder Beginner Contest 100
|
||||
が開催される. そのため, 高橋君はりんごさんに,
|
||||
ある整数をプレゼントしようと思った.<br />
|
||||
今日のコンテストは「AtCoder Beginner Contest
|
||||
100」なので, りんごさんは <var>100</var> で
|
||||
<strong>ちょうど</strong>
|
||||
<var>D</var>
|
||||
回割りきれる正の整数をプレゼントされると喜ぶ.
|
||||
</p>
|
||||
<p>
|
||||
さて, りんごさんがプレゼントされると喜ぶような整数のうち
|
||||
<var>N</var> 番目に小さいものを求めなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>D</var> は <var>0, 1, 2</var> のいずれかである
|
||||
</li>
|
||||
<li>
|
||||
<var>N</var> は <var>1</var> 以上
|
||||
<var>100</var> 以下の整数
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>D</var> <var>N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>D</var> 回割りきれる正の整数の中で
|
||||
<var>N</var> 番目に小さいものを出力しなさい.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
0 5
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
5
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>0</var> 回割り切れる(すなわち,
|
||||
<var>100</var> で割り切れない)整数は, <var>1</var>,
|
||||
<var>2</var>, <var>3</var>, <var>4</var>, <var>5</var>,
|
||||
<var>6</var>, <var>7</var>, ... と続く.<br />
|
||||
よって, <var>5</var> 番目に小さいりんごさんが喜ぶ整数は
|
||||
<var>5</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
1 11
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
1100
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>1</var> 回割り切れる整数は, <var>100</var>,
|
||||
<var>200</var>, <var>300</var>, <var>400</var>,
|
||||
<var>500</var>, <var>600</var>, <var>700</var>,
|
||||
<var>800</var>, <var>900</var>, <var>1 \ 000</var>,
|
||||
<var>1 \ 100</var>, ... と続く.<br />
|
||||
よって, 求めたい整数は <var>1 \ 100</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
2 85
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
850000
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
<var>100</var> でちょうど
|
||||
<var>2</var> 回割り切れる整数は, <var>10 \ 000</var>,
|
||||
<var>20 \ 000</var>, <var>30 \ 000</var>, ... と続く.<br />
|
||||
よって, 求めたい整数は <var>850 \ 000</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>200</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
Today, the memorable AtCoder Beginner Contest 100 takes
|
||||
place. On this occasion, Takahashi would like to give an
|
||||
integer to Ringo.<br />
|
||||
As the name of the contest is AtCoder Beginner Contest
|
||||
100, Ringo would be happy if he is given a positive
|
||||
integer that can be divided by <var>100</var>
|
||||
<strong>exactly</strong> <var>D</var> times.
|
||||
</p>
|
||||
<p>
|
||||
Find the <var>N</var>-th smallest integer that would
|
||||
make Ringo happy.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>D</var> is <var>0</var>, <var>1</var> or
|
||||
<var>2</var>.
|
||||
</li>
|
||||
<li>
|
||||
<var>N</var> is an integer between <var>1</var> and
|
||||
<var>100</var> (inclusive).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>D</var> <var>N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
Print the <var>N</var>-th smallest integer that can be
|
||||
divided by <var>100</var> exactly <var>D</var> times.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
0 5
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
5
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly <var>0</var> times (that is, not
|
||||
divisible by <var>100</var>) are as follows:
|
||||
<var>1</var>, <var>2</var>, <var>3</var>, <var>4</var>,
|
||||
<var>5</var>, <var>6</var>, <var>7</var>, ...<br />
|
||||
Thus, the <var>5</var>-th smallest integer that would
|
||||
make Ringo happy is <var>5</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
1 11
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
1100
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly once are as follows:
|
||||
<var>100</var>, <var>200</var>, <var>300</var>,
|
||||
<var>400</var>, <var>500</var>, <var>600</var>,
|
||||
<var>700</var>, <var>800</var>, <var>900</var>,
|
||||
<var>1 \ 000</var>, <var>1 \ 100</var>, ...<br />
|
||||
Thus, the integer we are seeking is <var>1 \ 100</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
2 85
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
850000
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The integers that can be divided by
|
||||
<var>100</var> exactly twice are as follows:
|
||||
<var>10 \ 000</var>, <var>20 \ 000</var>,
|
||||
<var>30 \ 000</var>, ...<br />
|
||||
Thus, the integer we are seeking is
|
||||
<var>850 \ 000</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_b?lang=en"
|
||||
data-a2a-title="B - Ringo's Favorite Numbers"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
904
tests/fixtures/atcoder/task_abc100_c.html
vendored
Normal file
904
tests/fixtures/atcoder/task_abc100_c.html
vendored
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>C - *3 or /2</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="nXGC_JxO0yoP1qBzMnYD_xgufO6leSLw1kyNo2HZltM"
|
||||
/>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-RC512FD18N"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
gtag('set', 'user_properties', {
|
||||
login_status: 'logged_out'
|
||||
})
|
||||
gtag('config', 'G-RC512FD18N')
|
||||
</script>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta name="author" content="AtCoder Inc." />
|
||||
|
||||
<meta property="og:site_name" content="AtCoder" />
|
||||
|
||||
<meta property="og:title" content="C - *3 or /2" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://atcoder.jp/contests/abc100/tasks/abc100_c"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.atcoder.jp/assets/atcoder.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@atcoder" />
|
||||
|
||||
<meta property="twitter:title" content="C - *3 or /2" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Lato:400,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/bootstrap.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/base.css"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="//img.atcoder.jp/assets/favicon.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="//img.atcoder.jp/assets/atcoder.png" />
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/jquery-1.9.1.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/bootstrap.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/js.cookie.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment.min.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/moment_js-ja.js"></script>
|
||||
<script>
|
||||
var LANG = 'en'
|
||||
var userScreenName = ''
|
||||
var csrfToken = 'KwoiS7wTPLvccvgUDoQZ6H++fkjXMCchJrW6/YFqOJM='
|
||||
</script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/utils.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/contest.js"></script>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/contest.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
var contestScreenName = 'abc100'
|
||||
var remainingText = 'Remaining Time'
|
||||
var countDownText = 'Contest begins in'
|
||||
var startTime = moment('2018-06-16T21:00:00+09:00')
|
||||
var endTime = moment('2018-06-16T22:40:00+09:00')
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/select2-bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/lib/select2.min.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ace.js"></script>
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/ace/ext-language_tools.js"></script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/cdn/run_prettify.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//img.atcoder.jp/public/6372bb3/css/cdn/katex.min.css"
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/katex.min.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="//img.atcoder.jp/public/6372bb3/js/cdn/auto-render.min.js"
|
||||
></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('var').each(function () {
|
||||
var html = $(this)
|
||||
.html()
|
||||
.replace(/<sub>/g, '_{')
|
||||
.replace(/<\/sub>/g, '}')
|
||||
$(this).html('\\(' + html + '\\)')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
var katexOptions = {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
ignoredTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'code',
|
||||
'option'
|
||||
],
|
||||
ignoredClasses: ['prettyprint', 'source-code-for-copy'],
|
||||
throwOnError: false
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderMathInElement(document.body, katexOptions)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="//img.atcoder.jp/public/6372bb3/js/base.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
var __pParams = __pParams || []
|
||||
__pParams.push({
|
||||
client_id: '468',
|
||||
c_1: 'atcodercontest',
|
||||
c_2: 'ClientSite'
|
||||
})
|
||||
</script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.d2-apps.net/js/tr.js"
|
||||
async
|
||||
></script>
|
||||
|
||||
<div
|
||||
id="modal-contest-start"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest started</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has begun.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-contest-end" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Contest is over</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>AtCoder Beginner Contest 100 has ended.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-div" class="float-container">
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbar-collapse"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="icon-bar"></span><span class="icon-bar"></span
|
||||
><span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home"></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="contest-title" href="/contests/abc100"
|
||||
>AtCoder Beginner Contest 100</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" />
|
||||
English <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_c?lang=ja"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/>
|
||||
日本語</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contests/abc100/tasks/abc100_c?lang=en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png"
|
||||
/>
|
||||
English</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/register?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>Sign Up</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/login?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>Sign In</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
name="form_logout"
|
||||
action="/logout?continue=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc100%2Ftasks%2Fabc100_c"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="KwoiS7wTPLvccvgUDoQZ6H++fkjXMCchJrW6/YFqOJM="
|
||||
/>
|
||||
</form>
|
||||
<div id="main-container" class="container" style="padding-top: 50px">
|
||||
<div class="row">
|
||||
<div id="contest-nav-tabs" class="col-sm-12 mb-2 cnvtb-fixed">
|
||||
<div>
|
||||
<small class="contest-duration">
|
||||
Contest Duration:
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2100&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 21:00:00+0900</time
|
||||
></a
|
||||
>
|
||||
-
|
||||
<a
|
||||
href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20180616T2240&p1=248"
|
||||
target="blank"
|
||||
><time class="fixtime fixtime-full"
|
||||
>2018-06-16 22:40:00+0900</time
|
||||
></a
|
||||
>
|
||||
(local time) (100 minutes)
|
||||
</small>
|
||||
<small class="back-to-home pull-right"
|
||||
><a href="/home">Back to Home</a></small
|
||||
>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="/contests/abc100"
|
||||
><span
|
||||
class="glyphicon glyphicon-home"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Top</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="/contests/abc100/tasks"
|
||||
><span
|
||||
class="glyphicon glyphicon-tasks"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Tasks</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/clarifications"
|
||||
><span
|
||||
class="glyphicon glyphicon-question-sign"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Clarifications <span id="clar-badge" class="badge"></span
|
||||
></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span
|
||||
class="glyphicon glyphicon-list"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Results<span class="caret"></span
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/contests/abc100/submissions"
|
||||
><span
|
||||
class="glyphicon glyphicon-globe"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
All Submissions</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/standings/virtual"
|
||||
><span
|
||||
class="glyphicon glyphicon-sort-by-attributes-alt"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Virtual Standings</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/contests/abc100/editorial"
|
||||
><span
|
||||
class="glyphicon glyphicon-book"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Editorial</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a id="fix-cnvtb" href="javascript:void(0)"
|
||||
><span
|
||||
class="glyphicon glyphicon-pushpin"
|
||||
aria-hidden="true"
|
||||
></span
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="h2">
|
||||
C - *3 or /2
|
||||
<a
|
||||
class="btn btn-default btn-sm"
|
||||
href="/contests/abc100/tasks/abc100_c/editorial"
|
||||
>Editorial</a
|
||||
>
|
||||
</span>
|
||||
<span id="task-lang-btn" class="pull-right"
|
||||
><span data-lang="ja"
|
||||
><img src="//img.atcoder.jp/assets/top/img/flag-lang/ja.png"
|
||||
/></span>
|
||||
/
|
||||
<span data-lang="en"
|
||||
><img
|
||||
src="//img.atcoder.jp/assets/top/img/flag-lang/en.png" /></span
|
||||
></span>
|
||||
<script>
|
||||
$(function () {
|
||||
var ts = $('#task-statement span.lang')
|
||||
if (ts.children('span').size() <= 1) {
|
||||
$('#task-lang-btn').hide()
|
||||
ts.children('span').show()
|
||||
return
|
||||
}
|
||||
|
||||
var REMEMBER_LB = 5
|
||||
var LS_KEY = 'task_lang'
|
||||
var taskLang = getLS(LS_KEY) || ''
|
||||
function isTaskLangSet(taskLang) {
|
||||
return taskLang === 'ja' || taskLang === 'en'
|
||||
}
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('lang')) {
|
||||
setLS(LS_KEY, REMEMBER_LB)
|
||||
taskLang = LANG
|
||||
}
|
||||
} else {
|
||||
taskLang = LANG
|
||||
}
|
||||
ts.children('span.lang-' + taskLang).show()
|
||||
|
||||
$('#task-lang-btn span').click(function () {
|
||||
var l = $(this).data('lang')
|
||||
ts.children('span').hide()
|
||||
ts.children('span.lang-' + l).show()
|
||||
|
||||
taskLang = getLS(LS_KEY) || ''
|
||||
let changeTimes = 0
|
||||
if (isTaskLangSet(taskLang)) {
|
||||
changeTimes = REMEMBER_LB
|
||||
} else {
|
||||
changeTimes = parseInt(taskLang, 10)
|
||||
if (isNaN(changeTimes)) changeTimes = 0
|
||||
changeTimes++
|
||||
}
|
||||
if (changeTimes < REMEMBER_LB) setLS(LS_KEY, changeTimes)
|
||||
else setLS(LS_KEY, l)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<hr />
|
||||
<p>Time Limit: 2 sec / Memory Limit: 976 MiB</p>
|
||||
|
||||
<div id="task-statement">
|
||||
<span class="lang">
|
||||
<span class="lang-ja">
|
||||
<p>配点: <var>300</var> 点</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>問題文</h3>
|
||||
<p>
|
||||
AtCoder Beginner Contest 100 の開催にともなって, AtCoder
|
||||
社では長さ <var>N</var> の数列 <var>a = </var>{<var
|
||||
>a_1, a_2, a_3, ..., a_N</var
|
||||
>} が飾られることになった. <br />
|
||||
社員のすぬけ君は, この数列で遊んでみようと思った.
|
||||
</p>
|
||||
<p>
|
||||
具体的には,
|
||||
以下の操作をできるだけ多くの回数繰り返そうと思った.
|
||||
</p>
|
||||
<pre><var>1 \leq i \leq N</var> を満たす全ての <var>i</var> に対して, それぞれ「<var>a_i</var> の値を <var>2</var> で割る」「<var>a_i</var> の値を <var>3</var> 倍する」のどちらかを行う.
|
||||
ただし, 全ての <var>i</var> に対して <var>3</var> 倍することはできず, 操作後の <var>a_i</var> の値は整数でなければならない.
|
||||
</pre>
|
||||
|
||||
<p>最大で何回の操作が可能か, 求めなさい.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>制約</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>N</var> は <var>1</var> 以上
|
||||
<var>10 \ 000</var> 以下の整数
|
||||
</li>
|
||||
<li>
|
||||
<var>a_i</var> は <var>1</var> 以上
|
||||
<var>1 \ 000 \ 000 \ 000</var> 以下の整数
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力</h3>
|
||||
<p>入力は以下の形式で標準入力から与えられる.</p>
|
||||
<pre><var>N</var>
|
||||
<var>a_1</var> <var>a_2</var> <var>a_3</var> <var>...</var> <var>a_N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力</h3>
|
||||
<p>すぬけ君が行える最大の操作回数を出力しなさい.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 1</h3>
|
||||
<pre>
|
||||
3
|
||||
5 2 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 1</h3>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
最初, 数列は <var>{5, 2, 4}</var> であるが,
|
||||
以下のように操作すれば
|
||||
<var>3</var> 回の操作を行うことができる.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
最初に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>3</var> 倍し, <var>a_3</var> を
|
||||
<var>2</var> で割る. すると数列は
|
||||
<var>{15, 6, 2}</var> となる.
|
||||
</li>
|
||||
<li>
|
||||
次に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>2</var> で割り,
|
||||
<var>a_3</var> を <var>3</var> 倍する. すると数列は
|
||||
<var>{45, 3, 6}</var> となる.
|
||||
</li>
|
||||
<li>
|
||||
最後に, <var>a_1</var> を <var>3</var> 倍し,
|
||||
<var>a_2</var> を <var>3</var> 倍し, <var>a_3</var> を
|
||||
<var>2</var> で割る. すると数列は
|
||||
<var>{135, 9, 3}</var> となる.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 2</h3>
|
||||
<pre>
|
||||
4
|
||||
631 577 243 199
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 2</h3>
|
||||
<pre>
|
||||
0
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
全ての要素が奇数なので, 操作はできない. よって答えは
|
||||
<var>0</var> である.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>入力例 3</h3>
|
||||
<pre>
|
||||
10
|
||||
2184 2126 1721 1800 1024 2528 3360 1945 1280 1776
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>出力例 3</h3>
|
||||
<pre>
|
||||
39
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
<span class="lang-en">
|
||||
<p>Score: <var>300</var> points</p>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Problem Statement</h3>
|
||||
<p>
|
||||
As AtCoder Beginner Contest 100 is taking place, the
|
||||
office of AtCoder, Inc. is decorated with a sequence of
|
||||
length <var>N</var>, <var>a = </var>{<var
|
||||
>a_1, a_2, a_3, ..., a_N</var
|
||||
>}.<br />
|
||||
Snuke, an employee, would like to play with this
|
||||
sequence.
|
||||
</p>
|
||||
<p>
|
||||
Specifically, he would like to repeat the following
|
||||
operation as many times as possible:
|
||||
</p>
|
||||
<pre>For every <var>i</var> satisfying <var>1 \leq i \leq N</var>, perform one of the following: "divide <var>a_i</var> by <var>2</var>" and "multiply <var>a_i</var> by <var>3</var>".
|
||||
Here, choosing "multiply <var>a_i</var> by <var>3</var>" for every <var>i</var> is not allowed, and the value of <var>a_i</var> after the operation must be an integer.
|
||||
</pre>
|
||||
|
||||
<p>At most how many operations can be performed?</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Constraints</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<var>N</var> is an integer between <var>1</var> and
|
||||
<var>10 \ 000</var> (inclusive).
|
||||
</li>
|
||||
<li>
|
||||
<var>a_i</var> is an integer between <var>1</var> and
|
||||
<var>1 \ 000 \ 000 \ 000</var> (inclusive).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="io-style">
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Input</h3>
|
||||
<p>
|
||||
Input is given from Standard Input in the following
|
||||
format:
|
||||
</p>
|
||||
<pre><var>N</var>
|
||||
<var>a_1</var> <var>a_2</var> <var>a_3</var> <var>...</var> <var>a_N</var>
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Output</h3>
|
||||
<p>
|
||||
Print the maximum number of operations that Snuke can
|
||||
perform.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 1</h3>
|
||||
<pre>
|
||||
3
|
||||
5 2 4
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 1</h3>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
The sequence is initially <var>{5, 2, 4}</var>. Three
|
||||
operations can be performed as follows:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
First, multiply <var>a_1</var> by <var>3</var>,
|
||||
multiply <var>a_2</var> by <var>3</var> and divide
|
||||
<var>a_3</var> by <var>2</var>. The sequence is now
|
||||
<var>{15, 6, 2}</var>.
|
||||
</li>
|
||||
<li>
|
||||
Next, multiply <var>a_1</var> by <var>3</var>, divide
|
||||
<var>a_2</var> by <var>2</var> and multiply
|
||||
<var>a_3</var> by <var>3</var>. The sequence is now
|
||||
<var>{45, 3, 6}</var>.
|
||||
</li>
|
||||
<li>
|
||||
Finally, multiply <var>a_1</var> by <var>3</var>,
|
||||
multiply <var>a_2</var> by <var>3</var> and divide
|
||||
<var>a_3</var> by <var>2</var>. The sequence is now
|
||||
<var>{135, 9, 3}</var>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 2</h3>
|
||||
<pre>
|
||||
4
|
||||
631 577 243 199
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 2</h3>
|
||||
<pre>
|
||||
0
|
||||
</pre
|
||||
>
|
||||
|
||||
<p>
|
||||
No operation can be performed since all the elements are
|
||||
odd. Thus, the answer is <var>0</var>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Input 3</h3>
|
||||
<pre>
|
||||
10
|
||||
2184 2126 1721 1800 1024 2528 3360 1945 1280 1776
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="part">
|
||||
<section>
|
||||
<h3>Sample Output 3</h3>
|
||||
<pre>
|
||||
39
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div
|
||||
class="a2a_kit a2a_kit_size_20 a2a_default_style pull-right"
|
||||
data-a2a-url="https://atcoder.jp/contests/abc100/tasks/abc100_c?lang=en"
|
||||
data-a2a-title="C - *3 or /2"
|
||||
>
|
||||
<a class="a2a_button_facebook"></a>
|
||||
<a class="a2a_button_twitter"></a>
|
||||
|
||||
<a class="a2a_button_telegram"></a>
|
||||
|
||||
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
|
||||
</div>
|
||||
|
||||
<script async src="//static.addtoany.com/menu/page.js"></script>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-bottom: 80px">
|
||||
<footer class="footer">
|
||||
<ul>
|
||||
<li><a href="/contests/abc100/rules">Rule</a></li>
|
||||
<li><a href="/contests/abc100/glossary">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li><a href="/tos">Terms of service</a></li>
|
||||
<li><a href="/privacy">Privacy Policy</a></li>
|
||||
<li><a href="/personal">Information Protection Policy</a></li>
|
||||
<li><a href="/company">Company</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<small id="copyright"
|
||||
>Copyright Since 2012 ©<a href="http://atcoder.co.jp"
|
||||
>AtCoder Inc.</a
|
||||
>
|
||||
All rights reserved.</small
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<p id="fixed-server-timer" class="contest-timer"></p>
|
||||
<div id="scroll-page-top" style="display: none">
|
||||
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> Page
|
||||
Top
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1086
tests/fixtures/atcoder/task_abc100_d.html
vendored
Normal file
1086
tests/fixtures/atcoder/task_abc100_d.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
4343
tests/fixtures/codechef/P1209.html
vendored
Normal file
4343
tests/fixtures/codechef/P1209.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
116
tests/fixtures/codechef/START209.json
vendored
Normal file
116
tests/fixtures/codechef/START209.json
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"status": "success",
|
||||
"user": { "username": null },
|
||||
"code": "START209",
|
||||
"isRatedContest": "1",
|
||||
"isParentContestRated": "0",
|
||||
"name": "Starters 209 (Rated till 5 star)",
|
||||
"problems": [],
|
||||
"banner": "https:\/\/cdn.codechef.com\/download\/small-banner\/START209\/1760933061.png",
|
||||
"rules": "<h4>CodeChef: A Platform for Aspiring Programmers<\/h4>\n<p class=\"last\">CodeChef was created as a platform to help programmers make it big in the world of algorithms, computer programming, and programming contests. At CodeChef, our dedicated efforts are aimed at reviving the inner geek within you, as we proudly host a thrilling programming (coding) contest every Wednesday.<\/p>\n<h4>About CodeChef Starters:<\/h4>\n<p>CodeChef Starters is a short programming contest which takes place on every Wednesday\u00a0<\/p>\n<h4>Contest Details:<\/h4>\n<ul class=\"last\">\n<li><strong>D<\/strong><strong>uration: <\/strong>\u00a02.00 hours\u00a0<\/li>\n<li><strong>Start Date: <\/strong>Wednesday, 22nd October , 2025 at 20:00 HRS (IST)<\/li>\n<li><strong>End Date: <\/strong>Wednesday, 22nd October, 2025 at 22:00 HRS (IST)<\/li>\n<li>Check your timezone <a href=\"https:\/\/www.timeanddate.com\/worldclock\/fixedtime.html?msg=CodeChef+Starters+209&iso=20251022T20&p1=44&ah=2\" target=\"_blank\" rel=\"nofollow noreferrer noopener\">here<\/a>.<\/li>\n<\/ul>\n<h4>Eligibility Criteria: Anyone with a knack for programming<\/h4>\n<p class=\"last\">Our contests are open to all programmers across the globe.<\/p>\n<h4>What's in it for you?<\/h4>\n<p>The idea behind these programming contests is that we want you to learn while competing. Also, we believe that it is alright to refer to tutorials, books, and other materials, learn a concept, and then apply the same to solve a problem during a contest. But it is <strong>not alright to copy other people's solutions or seek other people's help to solve a problem. <\/strong>All the participants are expected to abide to <a class=\"button blue\" href=\"..\/codeofconduct\">CodeChef's Code Of Conduct<\/a>.<\/p>\n<h4>Rules and Regulations:<\/h4>\n<ul>\n<li>This is an IOI-style contest. This means that the problems will be partially graded. You will get the score for passing certain test data.<\/li>\n<li>The details of the failed test cases will also be visible on your solution page.<\/li>\n<li>You can submit solutions as many times as you'd like, there are no penalties for incorrect submissions. Only your best correct submission will be considered.<\/li>\n<li>Those who achieve the score first will be placed higher in the ranklist in case of a tie.<\/li>\n<li><strong>We have removed all the Institutions that we could not identify from our database. We request you to update your institutions once again by going to your profile page.<\/strong><\/li>\n<li>You can also send in your queries in an email to <a href=\"mailto:help@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\">help@codechef.com<\/a>, during the contest.<\/li>\n<li>Please do not discuss strategy, suggestions, or tips in the comments during a live contest. Posting questions clarifying the problem statement is ok. If you are unsure, email us at <a href=\"mailto:feedback@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\"> feedback@codechef.com<\/a>.<\/li>\n<li>Discussing CodeChef's problems or any aspect of a problem, on any other platform on the web, on identification, could lead to the disabling of the respective account and banning from the community.<\/li>\n<\/ul>\n<p><strong>Note: You can now \"Code, Compile, and Run\" your codes on our <a href=\"..\/ide\">Online IDE<\/a>.<\/strong><\/p>\n<p>However, if you are using any other online development environment, make sure that other contestants don't have access to your code. As a contestant, you are responsible for making sure others don't access the code that you submit. If you use Ideone, make sure to mark your submission \"private\" (not secret)\".<\/p>",
|
||||
"time": {
|
||||
"start": 1761143400,
|
||||
"end": 1761150600,
|
||||
"freezing": 0,
|
||||
"current": 1761370410
|
||||
},
|
||||
"ip": "2603:7000:3900:1358:3959:b692:6cf3:cb03",
|
||||
"announcements": "<p><strong>CodeChef \u00d7 Coding Club League (2025-26)<\/strong><br \/><br \/>Partner with CodeChef to build a strong coding culture on campus!<\/p>\n<p><strong>Benefits for Clubs:<\/strong><\/p>\n<ul>\n<li>Platform access and support for Annual Technical events \/ hackathons<\/li>\n<li>Pro access for winners<\/li>\n<li>Dashboard to track member progress<\/li>\n<li>Discounts on CodeChef Pro for all members<\/li>\n<li>Co-branding & promotion on CodeChef channels<br \/><br \/>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<strong style=\"text-align:center;\"><a class=\"button blue\" href=\"codechef-coding-club\" target=\"_blank\" rel=\"noreferrer noopener\">\u00a0Click Here To Know More<\/a><\/strong><\/li>\n<\/ul>\n<p><strong>\u00a0<\/strong><\/p>",
|
||||
"problemsstats": {
|
||||
"attempted": [],
|
||||
"partially_solved": [],
|
||||
"solved": [],
|
||||
"locked": []
|
||||
},
|
||||
"todos": [],
|
||||
"stats": null,
|
||||
"partial_scores": [],
|
||||
"isRanklistFrozen": false,
|
||||
"rank_and_score": { "score": "NA", "rank": "NA" },
|
||||
"is_a_parent_contest": true,
|
||||
"is_contest_elements_visible": true,
|
||||
"is_OTP_required": false,
|
||||
"is_linked_problems_contest": "0",
|
||||
"custom_contest_page_title": "",
|
||||
"custom_contest_page_meta_desc": "",
|
||||
"contest_introduction": "https:\/\/discuss.codechef.com\/t\/invitation-to-codechef-starters-209-rated-upto-5-stars-22nd-october\/124401",
|
||||
"contest_editorials": "https:\/\/discuss.codechef.com\/tag\/start209",
|
||||
"contest_video_editorials": "",
|
||||
"is_older_rating_based_division_system": false,
|
||||
"division_generation": 3,
|
||||
"isAssessmentContest": false,
|
||||
"penalisedUsersCount": 0,
|
||||
"ttl": 60,
|
||||
"child_contests": {
|
||||
"div_1": {
|
||||
"div": {
|
||||
"div_number": "1",
|
||||
"code": "div_1",
|
||||
"min_rating": 2000,
|
||||
"max_rating": 50000,
|
||||
"name": "Division 1",
|
||||
"description": "Users with rating above 2000"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209A",
|
||||
"contest_link": "\/START209A"
|
||||
},
|
||||
"div_2": {
|
||||
"div": {
|
||||
"div_number": "2",
|
||||
"code": "div_2",
|
||||
"min_rating": 1600,
|
||||
"max_rating": 1999,
|
||||
"name": "Division 2",
|
||||
"description": "Users with rating between 1600 and 1999"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209B",
|
||||
"contest_link": "\/START209B"
|
||||
},
|
||||
"div_3": {
|
||||
"div": {
|
||||
"div_number": "3",
|
||||
"code": "div_3",
|
||||
"min_rating": 1400,
|
||||
"max_rating": 1599,
|
||||
"name": "Division 3",
|
||||
"description": "Users with rating upto 1599"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209C",
|
||||
"contest_link": "\/START209C"
|
||||
},
|
||||
"div_4": {
|
||||
"div": {
|
||||
"div_number": "4",
|
||||
"code": "div_4",
|
||||
"min_rating": 0,
|
||||
"max_rating": 1399,
|
||||
"name": "Division 4",
|
||||
"description": "Users with rating upto 1399"
|
||||
},
|
||||
"division_generation": 3,
|
||||
"contest_code": "START209D",
|
||||
"contest_link": "\/START209D"
|
||||
}
|
||||
},
|
||||
"user_rating_div": {
|
||||
"rating": -1,
|
||||
"div": {
|
||||
"code": "all",
|
||||
"min_rating": 0,
|
||||
"max_rating": 50000,
|
||||
"name": "All",
|
||||
"description": "All the users"
|
||||
}
|
||||
},
|
||||
"user_contest_code": null,
|
||||
"show_div_based_contest": false,
|
||||
"is_registration_enabled_contest": false,
|
||||
"is_flexi_time_contest": false,
|
||||
"duration": "120",
|
||||
"is_proctored": false,
|
||||
"autoRefresh": true,
|
||||
"visitedContests": []
|
||||
}
|
||||
202
tests/fixtures/codechef/START209D.json
vendored
Normal file
202
tests/fixtures/codechef/START209D.json
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
{
|
||||
"status": "success",
|
||||
"user": { "username": null },
|
||||
"code": "START209D",
|
||||
"isRatedContest": "1",
|
||||
"isParentContestRated": "1",
|
||||
"name": "Starters 209 (Rated)",
|
||||
"problems": {
|
||||
"P1209": {
|
||||
"code": "P1209",
|
||||
"name": "Bitcoin Market",
|
||||
"type": "3",
|
||||
"successful_submissions": "25131",
|
||||
"allow_submission": false,
|
||||
"accuracy": 85.680000000000007,
|
||||
"problem_url": "\/problems\/P1209",
|
||||
"submit_url": "\/problems\/P1209",
|
||||
"status_url": "\/status\/P1209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "33093",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P2209": {
|
||||
"code": "P2209",
|
||||
"name": "Divisible Duel",
|
||||
"type": "3",
|
||||
"successful_submissions": "21888",
|
||||
"allow_submission": false,
|
||||
"accuracy": 64.159999999999997,
|
||||
"problem_url": "\/problems\/P2209",
|
||||
"submit_url": "\/problems\/P2209",
|
||||
"status_url": "\/status\/P2209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "37437",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P3209": {
|
||||
"code": "P3209",
|
||||
"name": "Small GCD Sort",
|
||||
"type": "3",
|
||||
"successful_submissions": "13450",
|
||||
"allow_submission": false,
|
||||
"accuracy": 76.239999999999995,
|
||||
"problem_url": "\/problems\/P3209",
|
||||
"submit_url": "\/problems\/P3209",
|
||||
"status_url": "\/status\/P3209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "19164",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P4209": {
|
||||
"code": "P4209",
|
||||
"name": "Tactical Conversion",
|
||||
"type": "3",
|
||||
"successful_submissions": "1567",
|
||||
"allow_submission": false,
|
||||
"accuracy": 8.4499999999999993,
|
||||
"problem_url": "\/problems\/P4209",
|
||||
"submit_url": "\/problems\/P4209",
|
||||
"status_url": "\/status\/P4209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "20535",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P5209": {
|
||||
"code": "P5209",
|
||||
"name": "Binary Love",
|
||||
"type": "3",
|
||||
"successful_submissions": "3271",
|
||||
"allow_submission": false,
|
||||
"accuracy": 33.530000000000001,
|
||||
"problem_url": "\/problems\/P5209",
|
||||
"submit_url": "\/problems\/P5209",
|
||||
"status_url": "\/status\/P5209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "11128",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P6209E": {
|
||||
"code": "P6209E",
|
||||
"name": "High Score (Easy Version)",
|
||||
"type": "3",
|
||||
"successful_submissions": "285",
|
||||
"allow_submission": false,
|
||||
"accuracy": 7.2800000000000002,
|
||||
"problem_url": "\/problems\/P6209E",
|
||||
"submit_url": "\/problems\/P6209E",
|
||||
"status_url": "\/status\/P6209E",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "4535",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P6209": {
|
||||
"code": "P6209",
|
||||
"name": "High Score (Hard Version)",
|
||||
"type": "3",
|
||||
"successful_submissions": "34",
|
||||
"allow_submission": false,
|
||||
"accuracy": 3.1899999999999999,
|
||||
"problem_url": "\/problems\/P6209",
|
||||
"submit_url": "\/problems\/P6209",
|
||||
"status_url": "\/status\/P6209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1159",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P7209": {
|
||||
"code": "P7209",
|
||||
"name": "Easy Grid Game",
|
||||
"type": "3",
|
||||
"successful_submissions": "80",
|
||||
"allow_submission": false,
|
||||
"accuracy": 5.1100000000000003,
|
||||
"problem_url": "\/problems\/P7209",
|
||||
"submit_url": "\/problems\/P7209",
|
||||
"status_url": "\/status\/P7209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1740",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
},
|
||||
"P8209": {
|
||||
"code": "P8209",
|
||||
"name": "Counting Is Fun",
|
||||
"type": "3",
|
||||
"successful_submissions": "22",
|
||||
"allow_submission": false,
|
||||
"accuracy": 1.8200000000000001,
|
||||
"problem_url": "\/problems\/P8209",
|
||||
"submit_url": "\/problems\/P8209",
|
||||
"status_url": "\/status\/P8209",
|
||||
"is_added_to_practice": true,
|
||||
"total_submissions": "1261",
|
||||
"category_name": "main",
|
||||
"is_direct_submittable": false
|
||||
}
|
||||
},
|
||||
"banner": "https:\/\/cdn.codechef.com\/download\/small-banner\/START209D\/1760933097.png",
|
||||
"rules": "<h4>CodeChef: A Platform for Aspiring Programmers<\/h4>\n<p class=\"last\">CodeChef was created as a platform to help programmers make it big in the world of algorithms, computer programming, and programming contests. At CodeChef, our dedicated efforts are aimed at reviving the inner geek within you, as we proudly host a thrilling programming (coding) contest every Wednesday.<\/p>\n<h4>About CodeChef Starters:<\/h4>\n<p>CodeChef Starters is a short programming contest which takes place on every Wednesday\u00a0<\/p>\n<h4>Contest Details:<\/h4>\n<ul class=\"last\">\n<li><strong>D<\/strong><strong>uration: <\/strong>\u00a02.00 hours\u00a0<\/li>\n<li><strong>Start Date: <\/strong>Wednesday, 22nd October , 2025 at 20:00 HRS (IST)<\/li>\n<li><strong>End Date: <\/strong>Wednesday, 22nd October, 2025 at 22:00 HRS (IST)<\/li>\n<li>Check your timezone <a href=\"https:\/\/www.timeanddate.com\/worldclock\/fixedtime.html?msg=CodeChef+Starters+209&iso=20251022T20&p1=44&ah=2\" target=\"_blank\" rel=\"nofollow noreferrer noopener\">here<\/a>.<\/li>\n<\/ul>\n<h4>Eligibility Criteria: Anyone with a knack for programming<\/h4>\n<p class=\"last\">Our contests are open to all programmers across the globe.<\/p>\n<h4>What's in it for you?<\/h4>\n<p>The idea behind these programming contests is that we want you to learn while competing. Also, we believe that it is alright to refer to tutorials, books, and other materials, learn a concept, and then apply the same to solve a problem during a contest. But it is <strong>not alright to copy other people's solutions or seek other people's help to solve a problem. <\/strong>All the participants are expected to abide to <a class=\"button blue\" href=\"..\/codeofconduct\">CodeChef's Code Of Conduct<\/a>.<\/p>\n<h4>Rules and Regulations:<\/h4>\n<ul>\n<li>This is an IOI-style contest. This means that the problems will be partially graded. You will get the score for passing certain test data.<\/li>\n<li>The details of the failed test cases will also be visible on your solution page.<\/li>\n<li>You can submit solutions as many times as you'd like, there are no penalties for incorrect submissions. Only your best correct submission will be considered.<\/li>\n<li>Those who achieve the score first will be placed higher in the ranklist in case of a tie.<\/li>\n<li><strong>We have removed all the Institutions that we could not identify from our database. We request you to update your institutions once again by going to your profile page.<\/strong><\/li>\n<li>You can also send in your queries in an email to <a href=\"mailto:help@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\">help@codechef.com<\/a>, during the contest.<\/li>\n<li>Please do not discuss strategy, suggestions, or tips in the comments during a live contest. Posting questions clarifying the problem statement is ok. If you are unsure, email us at <a href=\"mailto:feedback@codechef.com\" target=\"_blank\" rel=\"noreferrer noopener\"> feedback@codechef.com<\/a>.<\/li>\n<li>Discussing CodeChef's problems or any aspect of a problem, on any other platform on the web, on identification, could lead to the disabling of the respective account and banning from the community.<\/li>\n<\/ul>\n<p><strong>Note: You can now \"Code, Compile, and Run\" your codes on our <a href=\"..\/ide\">Online IDE<\/a>.<\/strong><\/p>\n<p>However, if you are using any other online development environment, make sure that other contestants don't have access to your code. As a contestant, you are responsible for making sure others don't access the code that you submit. If you use Ideone, make sure to mark your submission \"private\" (not secret)\".<\/p>",
|
||||
"time": {
|
||||
"start": 1761143406,
|
||||
"end": 1761150606,
|
||||
"freezing": 0,
|
||||
"current": 1761365589
|
||||
},
|
||||
"ip": "2603:7000:3900:1358:3959:b692:6cf3:cb03",
|
||||
"announcements": "<p><strong>CodeChef \u00d7 Coding Club League (2025-26)<\/strong><br \/><br \/>Partner with CodeChef to build a strong coding culture on campus!<\/p>\n<p><strong>Benefits for Clubs:<\/strong><\/p>\n<ul>\n<li>Platform access and support for Annual Technical events \/ hackathons<\/li>\n<li>Pro access for winners<\/li>\n<li>Dashboard to track member progress<\/li>\n<li>Discounts on CodeChef Pro for all members<\/li>\n<li>Co-branding & promotion on CodeChef channels<br \/><br \/>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0<strong style=\"text-align:center;\"><a class=\"button blue\" href=\"codechef-coding-club\" target=\"_blank\" rel=\"noreferrer noopener\">\u00a0Click Here To Know More<\/a><\/strong><\/li>\n<\/ul>\n<p><strong>\u00a0<\/strong><\/p>\n<p>\u00a0<\/p>",
|
||||
"problemsstats": {
|
||||
"attempted": [],
|
||||
"partially_solved": [],
|
||||
"solved": [],
|
||||
"locked": []
|
||||
},
|
||||
"todos": [],
|
||||
"stats": null,
|
||||
"partial_scores": {
|
||||
"P7209": [{ "score": "100", "count": "80" }],
|
||||
"P5209": [{ "score": "100", "count": "3271" }],
|
||||
"P4209": [{ "score": "100", "count": "1567" }],
|
||||
"P1209": [{ "score": "100", "count": "25131" }],
|
||||
"P3209": [{ "score": "100", "count": "13450" }],
|
||||
"P2209": [{ "score": "100", "count": "21888" }],
|
||||
"P8209": [{ "score": "100", "count": "22" }],
|
||||
"P6209": [{ "score": "100", "count": "34" }],
|
||||
"P6209E": [{ "score": "100", "count": "285" }]
|
||||
},
|
||||
"isRanklistFrozen": false,
|
||||
"rank_and_score": { "score": "NA", "rank": "NA" },
|
||||
"is_a_parent_contest": false,
|
||||
"is_contest_elements_visible": true,
|
||||
"is_OTP_required": false,
|
||||
"is_linked_problems_contest": "0",
|
||||
"custom_contest_page_title": "",
|
||||
"custom_contest_page_meta_desc": "",
|
||||
"contest_introduction": "https:\/\/discuss.codechef.com\/t\/invitation-to-codechef-starters-209-rated-upto-5-stars-22nd-october\/124401",
|
||||
"contest_editorials": "https:\/\/discuss.codechef.com\/tag\/start209",
|
||||
"contest_video_editorials": "",
|
||||
"is_older_rating_based_division_system": false,
|
||||
"division_generation": 3,
|
||||
"isAssessmentContest": false,
|
||||
"penalisedUsersCount": 0,
|
||||
"ttl": 60,
|
||||
"scorable_heading": "Scorable Problems for Division 4",
|
||||
"scorable_message": "",
|
||||
"division": "Division 4",
|
||||
"non_scorable_heading": "Non Scorable Problems for Practice",
|
||||
"non_scorable_message": "<p>The following problems are <b>NOT part of the contest<\/b>, and will not be counted towards your rankings and ratings. These are problems from the other Division(s), made available for you to practice. Click <a href='\/blogs\/how-does-codechef-rating-system-work'>here<\/a> to know more. They will be considered for plagiarism though.<\/p>",
|
||||
"is_registration_enabled_contest": false,
|
||||
"is_flexi_time_contest": false,
|
||||
"duration": "120",
|
||||
"is_proctored": false,
|
||||
"autoRefresh": true,
|
||||
"visitedContests": [],
|
||||
"user_live_ratings_update_frequency": 15
|
||||
}
|
||||
99
tests/fixtures/codechef/START209D_P1209.json
vendored
Normal file
99
tests/fixtures/codechef/START209D_P1209.json
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"category_name": "main",
|
||||
"contest_code": "START209D",
|
||||
"contest_name": "Starters 209 (Rated)",
|
||||
"status": "success",
|
||||
"submit_error": "You need to login to submit.",
|
||||
"is_verified": false,
|
||||
"problem_code": "P1209",
|
||||
"contest_category": "9",
|
||||
"problem_name": "Bitcoin Market",
|
||||
"intended_contest_code": "START209",
|
||||
"body": "This is an example problem statement in markdown, and a mini guide on writing statements. Please make sure to remove everything here before publishing your problem.\n\n- Codechef uses markdown for its problem statements. Markdown syntax can be found [here](https:\/\/github.com\/showdownjs\/showdown\/wiki\/Showdown's-Markdown-syntax). Note the `[text](link)` syntax to insert a hyperlink.\n- Codechef also uses $\\LaTeX$ to render mathematical expressions, and you are advised to make liberal use of it to make your statement look good.\n- Text can be made **bold** or *italicized*.\n- **Do not** use HTML tags (p, ul, li, pre, br, ...) in the statement.\n- To insert an image, first upload it to an online hosting service (for an official contest, ask a Codechef admin to do this for you \u2014 this is important) and then use the following syntax: ``.\n- If your problem doesn't contain subtasks, ensure that the Subtasks section below is disabled and **all content is deleted from it**.\n\nIf you face any issues, either contact a Codechef admin directly or send us an email at help@codechef.com.\n\nBelow is an example problem statement that uses some of the above-mentioned features.\n\n---------\n\nChef has a simple undirected graph $G$ with $N$ vertices and $M$ edges. A [subgraph](https:\/\/mathworld.wolfram.com\/Subgraph.html) $H$ of $G$ is called *good* if:\n- $H$ is connected\n- $H$ contains all $N$ vertices of $G$\n- There is a unique path between any two vertices in $H$, using only edges in $H$\n\nCount the number of *good* subgraphs of $G$. Since this number might be large, report it modulo $10^9 + 7$.\n\nIn other news, here's a completely unrelated image:\n\n.\n\n\n<aside style='background: #f8f8f8;padding: 10px 15px;'><div>All submissions for this problem are available.<\/div><\/aside>",
|
||||
"problemComponents": {
|
||||
"constraints": "- $1 \\leq R \\leq 10$",
|
||||
"constraintsState": true,
|
||||
"subtasks": "- **Subtask 1 (10 points):** $1 \\leq M \\leq 10$\n- **Subtask 2 (20 points):** The sum of $N$ across all test cases won't exceed $20$.\n- **Subtask 3 (70 points):** No further constraints.",
|
||||
"subtasksState": false,
|
||||
"statement": "Chef has recently started investing in **Bitcoin**. \nHe assigns a **market risk level** $R$ (from $1$ to $10$), where: \n\n- $1$ means the market is *very safe*, \n- $10$ means the market is *very risky*. \n\nChef will **buy Bitcoin** only if the risk level is **$4$ or less**. \n\nGiven the current risk level $R$, determine whether Chef should buy Bitcoin.\n\nPrint **\"YES\"** if Chef should buy, otherwise print **\"NO\"**.",
|
||||
"inputFormat": "- The first and only line of input contains a single integer $R$ \u2014 the current market risk level.",
|
||||
"inputFormatState": true,
|
||||
"outputFormat": "Print `YES` if Chef should buy Bitcoin, Otherwise, print `NO`.\n\nYou may print each character of the string in uppercase or lowercase (for example, the strings `YES`, `yEs`, `yes`, and `yeS` will all be treated as identical).\n",
|
||||
"outputFormatState": true,
|
||||
"sampleTestCases": [
|
||||
{
|
||||
"id": "1",
|
||||
"input": "2",
|
||||
"output": "YES",
|
||||
"explanation": "The current market risk is $2$. \nSince $2$ is not larger than $4$, the risk is small enough, and Chef will buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"input": "4",
|
||||
"output": "YES",
|
||||
"explanation": "The current market risk is $4$. \nSince $4$ is not larger than $4$, the risk is small enough, and Chef will buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"input": "5",
|
||||
"output": "NO",
|
||||
"explanation": "The current market risk is $5$. \nSince $5$ is larger than $4$, the risk is too much, and Chef will **not** buy Bitcoin.",
|
||||
"isDeleted": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"gumlet_video_url": "",
|
||||
"video_editorial_url": "https:\/\/youtu.be\/tjUCV9Ld1Kw?si=minop9943wecj1bh",
|
||||
"text_editorial_body": "<h1><a name=\"problem-link-1\" class=\"anchor\" href=\"#problem-link-1\"><\/a>PROBLEM LINK:<\/h1>\n<p><a href=\"https:\/\/www.codechef.com\/problems\/P1209\">Practice<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209A\/problems\/P1209\">Contest: Division 1<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209B\/problems\/P1209\">Contest: Division 2<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209C\/problems\/P1209\">Contest: Division 3<\/a><br>\n<a href=\"https:\/\/www.codechef.com\/START209D\/problems\/P1209\">Contest: Division 4<\/a><\/p>\n<p><em><strong>Author:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/pols_agyi_pols\">pols_agyi_pols<\/a><br>\n<em><strong>Tester:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/kingmessi\">kingmessi<\/a><br>\n<em><strong>Editorialist:<\/strong><\/em> <a href=\"https:\/\/www.codechef.com\/users\/iceknight1093\">iceknight1093<\/a><\/p>\n<h1><a name=\"difficulty-2\" class=\"anchor\" href=\"#difficulty-2\"><\/a>DIFFICULTY:<\/h1>\n<p>Cakewalk<\/p>\n<h1><a name=\"prerequisites-3\" class=\"anchor\" href=\"#prerequisites-3\"><\/a>PREREQUISITES:<\/h1>\n<p>None<\/p>\n<h1><a name=\"problem-4\" class=\"anchor\" href=\"#problem-4\"><\/a>PROBLEM:<\/h1>\n<p>Chef will buy bitcoin if the market risk level is no more than <span class=\"math\">4<\/span>.<br>\nThe current market risk level is <span class=\"math\">R<\/span>.<br>\nWill Chef buy bitcoin?<\/p>\n<h1><a name=\"explanation-5\" class=\"anchor\" href=\"#explanation-5\"><\/a>EXPLANATION:<\/h1>\n<p>The answer is <code>Yes<\/code> if <span class=\"math\">R \\le 4<\/span> and <code>No<\/code> otherwise.<br>\nThis can be checked using an <code>if<\/code> condition.<\/p>\n<h1><a name=\"time-complexity-6\" class=\"anchor\" href=\"#time-complexity-6\"><\/a>TIME COMPLEXITY:<\/h1>\n<p><span class=\"math\">\\mathcal{O}(1)<\/span> per testcase.<\/p>\n<h1><a name=\"code-7\" class=\"anchor\" href=\"#code-7\"><\/a>CODE:<\/h1>\n<details>\n<summary>\nEditorialist's code (PyPy3)<\/summary>\n<pre><code class=\"lang-python\">r = int(input())\nprint('Yes' if r <= 4 else 'No')\n<\/code><\/pre>\n<\/details>",
|
||||
"text_editorial_is_markdown": 0,
|
||||
"text_editorial_topic_id": 124410,
|
||||
"languages_supported": "CPP20, PYTH 3, C, JAVA, PYP3, CS2, NODEJS, GO, TS, PHP, kotlin, rust, R",
|
||||
"max_timelimit": "1",
|
||||
"source_sizelimit": "50000",
|
||||
"problem_author": "archit_adm",
|
||||
"problem_display_authors": ["archit_adm"],
|
||||
"problem_display_authors_html_handle": "<div class=\"multiple-usernames-container\"><a href='\/users\/archit_adm'>archit_adm<\/a><\/div>",
|
||||
"problem_tester": null,
|
||||
"problem_testers_usernames": ["kingmessi"],
|
||||
"problem_tester_html_handle": "<div class=\"multiple-usernames-container\"><a href='\/users\/kingmessi'><span \n class='rating' \n style='display: inline-block; \n font-size: 10px; \n background: #D0011B;\n padding: 0 3px; \n line-height: 1.3; \n color: white;\n margin-right: 2px;'>7★<\/span><span class='m-username--link'>kingmessi<\/span><\/a><\/div>",
|
||||
"problem_editorialist": "iceknight1093",
|
||||
"date_added": "20-10-2025",
|
||||
"ready_for_debug": false,
|
||||
"problem_stats": {
|
||||
"accuracy": 85.780000000000001,
|
||||
"successful_submissions": "25325",
|
||||
"total_submissions": "33327"
|
||||
},
|
||||
"user_tags": ["archit_adm", "cakewalk", "start209"],
|
||||
"computed_tags": [],
|
||||
"difficulty_rating": "172",
|
||||
"best_tag": "",
|
||||
"editorial_url": "",
|
||||
"time": {
|
||||
"view_start_date": 1761143406,
|
||||
"submit_start_date": 1761143406,
|
||||
"visible_start_date": 1761150606,
|
||||
"end_date": 1761150606,
|
||||
"current": 1761365589,
|
||||
"practice_submission_allowed": false
|
||||
},
|
||||
"user": { "username": null, "access": "default", "isPremiumUser": false },
|
||||
"bookmark_status": false,
|
||||
"contest_problem_status": "unattempted",
|
||||
"problem_status": "unattempted",
|
||||
"is_direct_submittable": false,
|
||||
"problemDiscussURL": "https:\/\/discuss.codechef.com\/search?q=P1209",
|
||||
"is_a_practice_or_college_contest": false,
|
||||
"votes_data": {
|
||||
"SolutionVoteData": { "upvote_count": 0, "user_vote": 0 },
|
||||
"HintsVoteData": { "upvote_count": 0, "user_vote": 0 },
|
||||
"ProblemStatementVoteData": { "upvote_count": 26, "user_vote": 0 },
|
||||
"DoubtSupportVoteData": { "upvote_count": 0, "user_vote": 0 }
|
||||
},
|
||||
"is_proctored": false,
|
||||
"is_user_verified_for_proctoring": false,
|
||||
"visitedContests": [],
|
||||
"isSupportedByJudge": true
|
||||
}
|
||||
330
tests/fixtures/codechef/contests.json
vendored
Normal file
330
tests/fixtures/codechef/contests.json
vendored
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
{
|
||||
"status": "success",
|
||||
"message": "All contests list",
|
||||
"present_contests": [
|
||||
{
|
||||
"contest_code": "DEVWEEKEND21",
|
||||
"contest_name": "Weekend Dev Challenge 21: Full Stack Projects using MERN",
|
||||
"contest_start_date": "25 Oct 2025 00:00:00",
|
||||
"contest_end_date": "27 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-25T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-27T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 8
|
||||
}
|
||||
],
|
||||
"future_contests": [
|
||||
{
|
||||
"contest_code": "START210",
|
||||
"contest_name": "Starters 210",
|
||||
"contest_start_date": "29 Oct 2025 20:00:00",
|
||||
"contest_end_date": "29 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-29T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-29T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 0
|
||||
},
|
||||
{
|
||||
"contest_code": "START211",
|
||||
"contest_name": "Starters 211",
|
||||
"contest_start_date": "05 Nov 2025 20:00:00",
|
||||
"contest_end_date": "05 Nov 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-11-05T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-11-05T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 0
|
||||
}
|
||||
],
|
||||
"practice_contests": [],
|
||||
"past_contests": [
|
||||
{
|
||||
"contest_code": "START209",
|
||||
"contest_name": "Starters 209 (Rated till 5 star)",
|
||||
"contest_start_date": "22 Oct 2025 20:00:00",
|
||||
"contest_end_date": "22 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-22T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-22T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 30408
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY08",
|
||||
"contest_name": "Monday Munch - DSA Challenge 08",
|
||||
"contest_start_date": "20 Oct 2025 18:00:31",
|
||||
"contest_end_date": "20 Oct 2025 21:00:31",
|
||||
"contest_start_date_iso": "2025-10-20T18:00:31+05:30",
|
||||
"contest_end_date_iso": "2025-10-20T21:00:31+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 653
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND20",
|
||||
"contest_name": "Weekend Dev Challenge 20: Full Stack Projects using MERN",
|
||||
"contest_start_date": "18 Oct 2025 00:00:00",
|
||||
"contest_end_date": "20 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-18T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-20T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 318
|
||||
},
|
||||
{
|
||||
"contest_code": "START208",
|
||||
"contest_name": "Starters 208 (Rated till 6 star)",
|
||||
"contest_start_date": "15 Oct 2025 20:00:00",
|
||||
"contest_end_date": "15 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-15T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-15T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 37727
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY07",
|
||||
"contest_name": "Monday Munch - DSA Challenge 07",
|
||||
"contest_start_date": "13 Oct 2025 18:00:00",
|
||||
"contest_end_date": "13 Oct 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-10-13T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-13T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 4934
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND19",
|
||||
"contest_name": "Weekend Dev Challenge 19: Full Stack Projects using MERN",
|
||||
"contest_start_date": "11 Oct 2025 00:00:00",
|
||||
"contest_end_date": "13 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-11T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-13T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 5376
|
||||
},
|
||||
{
|
||||
"contest_code": "START207",
|
||||
"contest_name": "Starters 207 (Rated till 5 star)",
|
||||
"contest_start_date": "08 Oct 2025 20:00:00",
|
||||
"contest_end_date": "08 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-08T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-08T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 32785
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY06",
|
||||
"contest_name": "Monday Munch - DSA Challenge 06",
|
||||
"contest_start_date": "06 Oct 2025 18:00:02",
|
||||
"contest_end_date": "06 Oct 2025 21:00:02",
|
||||
"contest_start_date_iso": "2025-10-06T18:00:02+05:30",
|
||||
"contest_end_date_iso": "2025-10-06T21:00:02+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 892
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND18",
|
||||
"contest_name": "Weekend Dev Challenge 18: Full Stack Projects using MERN",
|
||||
"contest_start_date": "04 Oct 2025 00:00:00",
|
||||
"contest_end_date": "06 Oct 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-10-04T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-06T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 223
|
||||
},
|
||||
{
|
||||
"contest_code": "START206",
|
||||
"contest_name": "Starters 206 (Rated till 5 star)",
|
||||
"contest_start_date": "01 Oct 2025 20:00:00",
|
||||
"contest_end_date": "01 Oct 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-10-01T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-10-01T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 23977
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY05",
|
||||
"contest_name": "Monday Munch - DSA Challenge 05",
|
||||
"contest_start_date": "29 Sep 2025 18:00:00",
|
||||
"contest_end_date": "29 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-29T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-29T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 1160
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND17",
|
||||
"contest_name": "Weekend Dev Challenge 17: GenAI Projects using LLM",
|
||||
"contest_start_date": "27 Sep 2025 00:00:00",
|
||||
"contest_end_date": "29 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-27T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-29T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 130
|
||||
},
|
||||
{
|
||||
"contest_code": "START205",
|
||||
"contest_name": "Starters 205 (Rated till 6 star)",
|
||||
"contest_start_date": "24 Sep 2025 20:00:00",
|
||||
"contest_end_date": "24 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-24T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-24T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 32552
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY04",
|
||||
"contest_name": "Monday Munch - DSA Challenge 04",
|
||||
"contest_start_date": "22 Sep 2025 18:00:00",
|
||||
"contest_end_date": "22 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-22T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-22T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 759
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND16",
|
||||
"contest_name": "Weekend Dev Challenge 16: GenAI Projects using LLM",
|
||||
"contest_start_date": "20 Sep 2025 00:00:00",
|
||||
"contest_end_date": "22 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-20T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-22T00:00:00+05:30",
|
||||
"contest_duration": "2880",
|
||||
"distinct_users": 171
|
||||
},
|
||||
{
|
||||
"contest_code": "START204",
|
||||
"contest_name": "Starters 204 (Rated till 5 star)",
|
||||
"contest_start_date": "17 Sep 2025 20:00:00",
|
||||
"contest_end_date": "17 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-17T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-17T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 36282
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY03",
|
||||
"contest_name": "Monday Munch - DSA Challenge 03",
|
||||
"contest_start_date": "15 Sep 2025 18:00:00",
|
||||
"contest_end_date": "15 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-15T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-15T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 657
|
||||
},
|
||||
{
|
||||
"contest_code": "DEVWEEKEND15",
|
||||
"contest_name": "Weekend Dev Challenge 15: Classify images using Deep Learning",
|
||||
"contest_start_date": "13 Sep 2025 00:00:00",
|
||||
"contest_end_date": "14 Sep 2025 00:00:00",
|
||||
"contest_start_date_iso": "2025-09-13T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-14T00:00:00+05:30",
|
||||
"contest_duration": "1440",
|
||||
"distinct_users": 112
|
||||
},
|
||||
{
|
||||
"contest_code": "START203",
|
||||
"contest_name": "Starters 203 (Rated till 5 star)",
|
||||
"contest_start_date": "10 Sep 2025 20:00:00",
|
||||
"contest_end_date": "10 Sep 2025 22:00:00",
|
||||
"contest_start_date_iso": "2025-09-10T20:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-10T22:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"distinct_users": 36512
|
||||
},
|
||||
{
|
||||
"contest_code": "DSAMONDAY02",
|
||||
"contest_name": "Monday Munch - DSA Challenge 02",
|
||||
"contest_start_date": "08 Sep 2025 18:00:00",
|
||||
"contest_end_date": "08 Sep 2025 21:00:00",
|
||||
"contest_start_date_iso": "2025-09-08T18:00:00+05:30",
|
||||
"contest_end_date_iso": "2025-09-08T21:00:00+05:30",
|
||||
"contest_duration": "180",
|
||||
"distinct_users": 737
|
||||
}
|
||||
],
|
||||
"skill_tests": [
|
||||
{
|
||||
"contest_code": "basic-python",
|
||||
"contest_name": "Python Online Test & Quiz",
|
||||
"contest_start_date": "27 Mar 2024 15:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-27T15:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 61244
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-java",
|
||||
"contest_name": "Java Online Test & Quiz",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 49993
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-c-language",
|
||||
"contest_name": "C language online test",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 41373
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-c-plus-plus",
|
||||
"contest_name": "C++ Online Test and Quiz",
|
||||
"contest_start_date": "28 Mar 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:30:00",
|
||||
"contest_start_date_iso": "2024-03-28T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:30:00+05:30",
|
||||
"contest_duration": "90",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 32507
|
||||
},
|
||||
{
|
||||
"contest_code": "basic-sql",
|
||||
"contest_name": "SQL Online Test and Quiz",
|
||||
"contest_start_date": "01 Jun 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 01:00:00",
|
||||
"contest_start_date_iso": "2024-06-01T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T01:00:00+05:30",
|
||||
"contest_duration": "60",
|
||||
"problem_count": 17,
|
||||
"distinct_users": 17426
|
||||
},
|
||||
{
|
||||
"contest_code": "operating-systems",
|
||||
"contest_name": "Operating Systems Skill Test",
|
||||
"contest_start_date": "01 Jun 2024 00:00:00",
|
||||
"contest_end_date": "01 Jan 2027 00:45:00",
|
||||
"contest_start_date_iso": "2024-06-01T00:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T00:45:00+05:30",
|
||||
"contest_duration": "45",
|
||||
"problem_count": 30,
|
||||
"distinct_users": 8751
|
||||
},
|
||||
{
|
||||
"contest_code": "c-language-dsa",
|
||||
"contest_name": "Data structures and Algorithms in C test",
|
||||
"contest_start_date": "01 Apr 2024 12:00:00",
|
||||
"contest_end_date": "01 Jan 2027 02:00:00",
|
||||
"contest_start_date_iso": "2024-04-01T12:00:00+05:30",
|
||||
"contest_end_date_iso": "2027-01-01T02:00:00+05:30",
|
||||
"contest_duration": "120",
|
||||
"problem_count": 28,
|
||||
"distinct_users": 6611
|
||||
}
|
||||
],
|
||||
"banners": [
|
||||
{
|
||||
"image": "1760933050.png",
|
||||
"link": "https:\/\/www.codechef.com\/START209"
|
||||
},
|
||||
{
|
||||
"image": "1719492535.png",
|
||||
"link": "https:\/\/www.codechef.com\/roadmap\/data-structures-and-algorithms"
|
||||
}
|
||||
]
|
||||
}
|
||||
8210
tests/fixtures/codeforces/1550_A.html
vendored
Normal file
8210
tests/fixtures/codeforces/1550_A.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
4724
tests/fixtures/codeforces/1550_B.html
vendored
Normal file
4724
tests/fixtures/codeforces/1550_B.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
10743
tests/fixtures/codeforces/1550_problems.html
vendored
Normal file
10743
tests/fixtures/codeforces/1550_problems.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
10
tests/fixtures/codeforces/contests.html
vendored
Normal file
10
tests/fixtures/codeforces/contests.html
vendored
Normal file
File diff suppressed because one or more lines are too long
2141
tests/fixtures/cses/contests.html
vendored
Normal file
2141
tests/fixtures/cses/contests.html
vendored
Normal file
File diff suppressed because it is too large
Load diff
156
tests/fixtures/cses/task_1068.html
vendored
Normal file
156
tests/fixtures/cses/task_1068.html
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="stylesheet " type="text/css" href="/cses.css?0" id="styles" />
|
||||
<link
|
||||
rel="stylesheet alternate"
|
||||
type="text/css"
|
||||
href="/cses-dark.css?0"
|
||||
id="styles-dark"
|
||||
/>
|
||||
<meta name="theme-color" content="white" id="theme-color" />
|
||||
<script type="application/json" id="darkmode-enabled">
|
||||
false
|
||||
</script>
|
||||
<script src="/ui.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/lib/fontawesome/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body class="with-sidebar">
|
||||
<div class="header">
|
||||
<div>
|
||||
<a href="/" class="logo"><img src="/logo.png?1" alt="CSES" /></a>
|
||||
<a
|
||||
class="menu-toggle"
|
||||
onclick="document.body.classList.toggle('menu-open')"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
<div class="controls">
|
||||
<a class="account" href="/login">Login</a>
|
||||
<span>—</span>
|
||||
<a
|
||||
href="/darkmode"
|
||||
title="Toggle dark mode"
|
||||
onclick="return toggle_theme()"
|
||||
><i aria-label="Dark mode" class="fas fa-adjust"></i
|
||||
><span>Dark mode</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton">
|
||||
<div class="navigation">
|
||||
<div class="title-block">
|
||||
<h3><a href="/problemset/list/">CSES Problem Set</a></h3>
|
||||
<h1>Weird Algorithm</h1>
|
||||
<ul class="nav">
|
||||
<li><a href="/problemset/task/1068/" class="current">Task</a></li>
|
||||
<li><a href="/problemset/stats/1068/">Statistics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<title>CSES - Weird Algorithm</title
|
||||
><link rel="stylesheet" href="/lib/katex/katex.min.css" />
|
||||
<script defer src="/lib/katex/katex.min.js"></script>
|
||||
<script defer src="/lib/katex/contrib/copy-tex.min.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="/lib/google-code-prettify/run_prettify.js"
|
||||
></script>
|
||||
<script>
|
||||
addEventListener('DOMContentLoaded', function (e) {
|
||||
const mathElements = document.getElementsByClassName('math')
|
||||
const macros = {}
|
||||
for (let element of mathElements) {
|
||||
katex.render(element.textContent, element, {
|
||||
displayMode: element.classList.contains('math-display'),
|
||||
throwOnError: false,
|
||||
globalGroup: true,
|
||||
macros
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<ul class="task-constraints">
|
||||
<li><b>Time limit:</b> 1.00 s</li>
|
||||
<li><b>Memory limit:</b> 512 MB</li>
|
||||
</ul>
|
||||
<div class="md">
|
||||
<p>
|
||||
Consider an algorithm that takes as input a positive integer
|
||||
<span class="math math-inline">n</span>. If
|
||||
<span class="math math-inline">n</span> is even, the algorithm
|
||||
divides it by two, and if
|
||||
<span class="math math-inline">n</span> is odd, the algorithm
|
||||
multiplies it by three and adds one. The algorithm repeats this,
|
||||
until <span class="math math-inline">n</span> is one. For example,
|
||||
the sequence for <span class="math math-inline">n=3</span> is as
|
||||
follows:
|
||||
<span class="math math-display">
|
||||
3 \rightarrow 10 \rightarrow 5 \rightarrow 16 \rightarrow 8
|
||||
\rightarrow 4 \rightarrow 2 \rightarrow 1</span
|
||||
>
|
||||
Your task is to simulate the execution of the algorithm for a
|
||||
given value of <span class="math math-inline">n</span>.
|
||||
</p>
|
||||
<h1 id="input">Input</h1>
|
||||
<p>
|
||||
The only input line contains an integer
|
||||
<span class="math math-inline">n</span>.
|
||||
</p>
|
||||
<h1 id="output">Output</h1>
|
||||
<p>
|
||||
Print a line that contains all values of
|
||||
<span class="math math-inline">n</span> during the algorithm.
|
||||
</p>
|
||||
<h1 id="constraints">Constraints</h1>
|
||||
<ul>
|
||||
<li><span class="math math-inline">1 \le n \le 10^6</span></li>
|
||||
</ul>
|
||||
<h1 id="example">Example</h1>
|
||||
<p>Input:</p>
|
||||
<pre>
|
||||
3
|
||||
</pre
|
||||
>
|
||||
<p>Output:</p>
|
||||
<pre>
|
||||
3 10 5 16 8 4 2 1
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav sidebar">
|
||||
<h4>Introductory Problems</h4>
|
||||
<a class="current" href="/problemset/task/1068"
|
||||
>Weird Algorithm<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1083"
|
||||
>Missing Number<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1069"
|
||||
>Repetitions<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1094"
|
||||
>Increasing Array<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1070"
|
||||
>Permutations<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1071"
|
||||
>Number Spiral<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1072"
|
||||
>Two Knights<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1092"
|
||||
>Two Sets<span class="task-score icon"></span></a
|
||||
>...
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
150
tests/fixtures/cses/task_1621.html
vendored
Normal file
150
tests/fixtures/cses/task_1621.html
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="stylesheet " type="text/css" href="/cses.css?0" id="styles" />
|
||||
<link
|
||||
rel="stylesheet alternate"
|
||||
type="text/css"
|
||||
href="/cses-dark.css?0"
|
||||
id="styles-dark"
|
||||
/>
|
||||
<meta name="theme-color" content="white" id="theme-color" />
|
||||
<script type="application/json" id="darkmode-enabled">
|
||||
false
|
||||
</script>
|
||||
<script src="/ui.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/lib/fontawesome/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body class="with-sidebar">
|
||||
<div class="header">
|
||||
<div>
|
||||
<a href="/" class="logo"><img src="/logo.png?1" alt="CSES" /></a>
|
||||
<a
|
||||
class="menu-toggle"
|
||||
onclick="document.body.classList.toggle('menu-open')"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
<div class="controls">
|
||||
<a class="account" href="/login">Login</a>
|
||||
<span>—</span>
|
||||
<a
|
||||
href="/darkmode"
|
||||
title="Toggle dark mode"
|
||||
onclick="return toggle_theme()"
|
||||
><i aria-label="Dark mode" class="fas fa-adjust"></i
|
||||
><span>Dark mode</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton">
|
||||
<div class="navigation">
|
||||
<div class="title-block">
|
||||
<h3><a href="/problemset/list/">CSES Problem Set</a></h3>
|
||||
<h1>Distinct Numbers</h1>
|
||||
<ul class="nav">
|
||||
<li><a href="/problemset/task/1621/" class="current">Task</a></li>
|
||||
<li><a href="/problemset/stats/1621/">Statistics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<title>CSES - Distinct Numbers</title
|
||||
><link rel="stylesheet" href="/lib/katex/katex.min.css" />
|
||||
<script defer src="/lib/katex/katex.min.js"></script>
|
||||
<script defer src="/lib/katex/contrib/copy-tex.min.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="/lib/google-code-prettify/run_prettify.js"
|
||||
></script>
|
||||
<script>
|
||||
addEventListener('DOMContentLoaded', function (e) {
|
||||
const mathElements = document.getElementsByClassName('math')
|
||||
const macros = {}
|
||||
for (let element of mathElements) {
|
||||
katex.render(element.textContent, element, {
|
||||
displayMode: element.classList.contains('math-display'),
|
||||
throwOnError: false,
|
||||
globalGroup: true,
|
||||
macros
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<ul class="task-constraints">
|
||||
<li><b>Time limit:</b> 1.00 s</li>
|
||||
<li><b>Memory limit:</b> 512 MB</li>
|
||||
</ul>
|
||||
<div class="md">
|
||||
<p>
|
||||
You are given a list of
|
||||
<span class="math math-inline">n</span> integers, and your task is
|
||||
to calculate the number of <em>distinct</em> values in the list.
|
||||
</p>
|
||||
<h1 id="input">Input</h1>
|
||||
<p>
|
||||
The first input line has an integer
|
||||
<span class="math math-inline">n</span>: the number of values.
|
||||
</p>
|
||||
<p>
|
||||
The second line has
|
||||
<span class="math math-inline">n</span> integers
|
||||
<span class="math math-inline">x_1,x_2,\dots,x_n</span>.
|
||||
</p>
|
||||
<h1 id="output">Output</h1>
|
||||
<p>Print one integers: the number of distinct values.</p>
|
||||
<h1 id="constraints">Constraints</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="math math-inline">1 \le n \le 2 \cdot 10^5</span>
|
||||
</li>
|
||||
<li><span class="math math-inline">1 \le x_i \le 10^9</span></li>
|
||||
</ul>
|
||||
<h1 id="example">Example</h1>
|
||||
<p>Input:</p>
|
||||
<pre>
|
||||
5
|
||||
2 3 2 2 3
|
||||
</pre
|
||||
>
|
||||
<p>Output:</p>
|
||||
<pre>
|
||||
2
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav sidebar">
|
||||
<h4>Sorting and Searching</h4>
|
||||
<a class="current" href="/problemset/task/1621"
|
||||
>Distinct Numbers<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1084"
|
||||
>Apartments<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1090"
|
||||
>Ferris Wheel<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1091"
|
||||
>Concert Tickets<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1619"
|
||||
>Restaurant Customers<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1629"
|
||||
>Movie Festival<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1640"
|
||||
>Sum of Two Values<span class="task-score icon"></span></a
|
||||
><a href="/problemset/task/1643"
|
||||
>Maximum Subarray Sum<span class="task-score icon"></span></a
|
||||
>...
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
def test():
|
||||
assert 5 == 5
|
||||
89
tests/test_scrapers.py
Normal file
89
tests/test_scrapers.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import pytest
|
||||
|
||||
from scrapers.models import (
|
||||
ContestListResult,
|
||||
MetadataResult,
|
||||
TestsResult,
|
||||
)
|
||||
|
||||
MATRIX = {
|
||||
"cses": {
|
||||
"metadata": ("introductory_problems",),
|
||||
"tests": ("introductory_problems",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"atcoder": {
|
||||
"metadata": ("abc100",),
|
||||
"tests": ("abc100",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"codeforces": {
|
||||
"metadata": ("1550",),
|
||||
"tests": ("1550",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
"codechef": {
|
||||
"metadata": ("START209D",),
|
||||
"tests": ("START209D",),
|
||||
"contests": tuple(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scraper", MATRIX.keys())
|
||||
@pytest.mark.parametrize("mode", ["metadata", "tests", "contests"])
|
||||
def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode):
|
||||
args = MATRIX[scraper][mode]
|
||||
rc, objs = run_scraper_offline(scraper, mode, *args)
|
||||
assert rc in (0, 1), f"Bad exit code {rc}"
|
||||
assert objs, f"No JSON output for {scraper}:{mode}"
|
||||
|
||||
if mode == "metadata":
|
||||
model = MetadataResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
assert model.url
|
||||
assert len(model.problems) >= 1
|
||||
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
||||
elif mode == "contests":
|
||||
model = ContestListResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
assert len(model.contests) >= 1
|
||||
else:
|
||||
assert len(objs) >= 1, "No test objects returned"
|
||||
validated_any = False
|
||||
for obj in objs:
|
||||
if "success" in obj and "tests" in obj and "problem_id" in obj:
|
||||
tr = TestsResult.model_validate(obj)
|
||||
assert tr.problem_id != ""
|
||||
assert isinstance(tr.tests, list)
|
||||
assert hasattr(tr, "combined"), "Missing combined field"
|
||||
assert tr.combined is not None, "combined field is None"
|
||||
assert hasattr(tr.combined, "input"), "combined missing input"
|
||||
assert hasattr(tr.combined, "expected"), "combined missing expected"
|
||||
assert isinstance(tr.combined.input, str), "combined.input not string"
|
||||
assert isinstance(tr.combined.expected, str), (
|
||||
"combined.expected not string"
|
||||
)
|
||||
assert hasattr(tr, "multi_test"), "Missing multi_test field"
|
||||
assert isinstance(tr.multi_test, bool), "multi_test not boolean"
|
||||
validated_any = True
|
||||
else:
|
||||
assert "problem_id" in obj
|
||||
assert "tests" in obj and isinstance(obj["tests"], list)
|
||||
assert (
|
||||
"timeout_ms" in obj and "memory_mb" in obj and "interactive" in obj
|
||||
)
|
||||
assert "combined" in obj, "Missing combined field in raw JSON"
|
||||
assert isinstance(obj["combined"], dict), "combined not a dict"
|
||||
assert "input" in obj["combined"], "combined missing input key"
|
||||
assert "expected" in obj["combined"], "combined missing expected key"
|
||||
assert isinstance(obj["combined"]["input"], str), (
|
||||
"combined.input not string"
|
||||
)
|
||||
assert isinstance(obj["combined"]["expected"], str), (
|
||||
"combined.expected not string"
|
||||
)
|
||||
assert "multi_test" in obj, "Missing multi_test field in raw JSON"
|
||||
assert isinstance(obj["multi_test"], bool), "multi_test not boolean"
|
||||
validated_any = True
|
||||
assert validated_any, "No valid tests payloads validated"
|
||||
Loading…
Add table
Add a link
Reference in a new issue