コード中で使用されている自作ライブラリを自動で結合し、提出できる状態でクリップボードにコピーするスクリプトを書いてみたので、その紹介です。
この記事・スクリプトはC#向けですが、他の言語に転用できる部分があるかもしれません。
お忙しい方は忙しい人向けの項を読んでいただけると多分大体分かるかと思います。
はじめに
競技プログラミングでライブラリを使用したコードを書くとき、どのようにしていますか?
「スニペットを活用する」「手動で別ファイルからコピペする」等が考えられますが、これは使うときにワンアクション必要になるため、少し煩わしいのではないでしょうか。
そこで、事前のコピペなしに自然体で自作ライブラリが使えて、そのままコピー→ペーストで提出できるようにするためのスクリプトを書いてみました。
/* 私が知らないだけで、実は良い感じのやり方があるならこの記事は無かったことにしてください・・・ */
事前準備
スクリプトの実装を簡単にするために、自作ライブラリは以下のルールで作成します。
- Library名前空間に実装する
- using はメインコードと同じものにする
- クラス名に接頭辞"LIB_"を付与する
- コピーして欲しい実装を ////start ~ ////end で囲む
今回は説明のために以下のライブラリを作成しました。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Library
{
////start
class LIB_Math
{
static public long GCD(long a, long b)
{
while (b > 0)
{
var tmp = b;
b = a % b;
a = tmp;
}
return a;
}
}
////end
}
そして今回の主役なのですが、件のスクリプトを用意します。
perl で書きました。
適当なテキスト処理をするなら perl が強いと思います。
use strict;
use warnings;
use Win32::Clipboard;
use Encode qw/encode decode/;
my %library;
my %classToFilename;
my %filenameToUsedLib;
my @folders = ("lib");
while(@folders > 0) {
my $dir = pop @folders;
opendir my $dh, $dir or die;
while(my $libfile = readdir $dh) {
my $path = "$dir\\$libfile";
if($libfile eq "." or $libfile eq "..") {
next;
}
elsif(-d $path) {
push @folders, $path;
}
elsif($libfile =~ /\.cs$/) {
open my $libfh, "<$path";
my $libstr = "";
my $copyStarted = 0;
while(my $line = decode('UTF-8', <$libfh>)) {
if($copyStarted == 1) {
if($line =~ m|////end|) {
last;
}
if($line =~ /(class|struct) LIB_([A-Za-z0-9_]+)/) {
$classToFilename{$2} = $path;
}
if($line =~ /LIB_([A-Za-z0-9_]+)/) {
${$filenameToUsedLib{$path}}[@{$filenameToUsedLib{$path}}] = $1;
}
$libstr .= $line;
}elsif($line =~ m|////start|) {
$copyStarted = 1;
}
}
close $libfh;
$library{$path} = $libstr;
}
}
closedir $dh;
}
my $filename = $ARGV[0];
open my $fh, "<$filename";
my $str = "";
my %usedLib;
while(my $line = decode('UTF-8', <$fh>)) {
$str .= $line;
while($line =~ /LIB_([A-Za-z0-9_]+)/g) {
my $libFileName = $classToFilename{$1};
$usedLib{$libFileName} = 1;
}
}
close $fh;
my $changed = 1;
while($changed == 1) {
$changed = 0;
foreach my $item (keys(%usedLib)) {
foreach my $item2 (@{$filenameToUsedLib{$item}}) {
if(! defined $usedLib{$classToFilename{$item2}}) {
$changed = 1;
$usedLib{$classToFilename{$item2}} = 1;
}
}
}
}
$str .= "namespace Library {\r\n";
foreach my $item (keys(%usedLib)) {
$str .= $library{$item};
}
$str .= "}\r\n";
Win32::Clipboard()->Set(encode('Shift_JIS', $str));
これで快適なコーディングができます!
コーディングから提出まで
今回はお試しに、入力された2つの数の最大公約数を求めてみます。
Library を using しているため、自然体で LIB_Math.GCD が使えます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Library;
namespace AtCoder
{
class Program
{
static void Main(string[] args)
{
var AB = Console.ReadLine().Split().Select(int.Parse).ToArray();
var A = AB[0];
var B = AB[1];
var gcd = LIB_Math.GCD(A, B);
System.Console.WriteLine(gcd);
}
}
}
さて、このコードをコピペして提出したいのですが、このままではLIB_Math.GCDの実装が含まれないので提出できません。
ここで件のスクリプトが役に立ちます。
コマンドラインから > copy.pl Program.cs
を実行すると、クリップボードに以下の内容がコピーされます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Library;
namespace AtCoder
{
class Program
{
static void Main(string[] args)
{
var AB = Console.ReadLine().Split().Select(int.Parse).ToArray();
var A = AB[0];
var B = AB[1];
var gcd = LIB_Math.GCD(A, B);
System.Console.WriteLine(gcd);
}
}
}
namespace Library {
class LIB_Math
{
static public long GCD(long a, long b)
{
while (b > 0)
{
var tmp = b;
b = a % b;
a = tmp;
}
return a;
}
}
}
あとはそのまま提出欄に貼り付けて提出するだけです!
これは何をやっているか
結構雑に書いたperlのコードなので、非常に読みづらいと思います。すみません。
ちなみに依存関係については、直接ソースコードから正規表現で抽出しています。
これはまずlib直下の*csをすべて読み込んで、そこで定義されている class or struct の名前と実装されているファイル名の対応付けを取得しています。
そして起動引数で渡されたファイルを読み込み、内部で使用されているライブラリを正規表現(LIB_([A-Za-z0-9_]+))で取得します。
すると最初に取得した対応付けによって結合すべきソースファイルが分かるので、その内容を namespace Library { } で囲ってメインのコードに結合します。
ということをやっています。
ライブラリ内部で別なライブラリを呼んでいる場合でも、順次読み出すので過不足無く結合されます。
さらなる快適さを求めて
copy.pl を叩くのが面倒だというのは、それはそうなのでショートカットキーに登録しましょう。
VSCodeの場合は、以下のタスクを定義した上で適当なショートカットキー(Ctrl+Shift+C等)に以下のタスクの実行を割り当てます。
すると、適当にコーディング→Ctrl+Shift+C→貼り付けて提出の流れで提出できるのでQoLが上がります。
{
"version": "2.0.0",
"tasks": [
{
"label": "copyWithLib",
"command": "copy.pl",
"type": "shell",
"args": [
"${file}"
],
"problemMatcher": []
},
]
}
忙しい人向け
以下のルールに従ってライブラリを実装します。
- Library名前空間に実装する
- using はメインコードと同じものにする
- クラス名に接頭辞"LIB_"を付与する
- コピーして欲しい実装を ////start ~ ////end で囲む
以下からスクリプトcopy.plを取得し、libフォルダと同じ階層に配置します。
https://github.com/yupiteru/AtCoder/blob/template.main/copy.pl
お好きなエディタのお好きなショートカットキー(Ctrl+Shift+C等)に以下コマンドを割り当てます。
copy.pl [ソースファイルパス]
何も意識せずライブラリを使ったコードを書いて、書き終わったら上で設定したショートカットキーを呼び出します。
クリップボードにライブラリが結合されたソースが格納されるので、そのまま提出欄に貼り付けて提出→AC! 楽ちん!
ToBe
接頭辞"LIB_"を付与するという条件は不要にできるかもしれません。
その場合は、コード中で使用されているキーワード([A-Za-z_][A-Za-z0-9_]*)のうち、ライブラリに対応するものがあればそれを結合する。という実装にできます。
誤爆して余計なライブラリを結合する可能性がありますが、提出する分には多くても問題ありません。
また、using についても使用されているものを集約してあげればメインコードと同じものにするという制約は無くなります。
以上を実装すれば、ライブラリに要求される制約は「Library名前空間に実装する」だけになるので、実装したいですね。
その他
実用例です。
これは普段私が使用しているライブラリですが、実際にcopy.plを活用するとこんな感じのライブラリになります。コピーの手間がゼロになるので快適です。
https://github.com/yupiteru/AtCoder/tree/template.main/lib