Node.js の標準入力に取り憑かれています。
導入
以前も似たような記事を書いたのですが、ただの自作モジュールの紹介記事でした。
今回は「競プロっぽい」をテーマに自分が見てきた・考えてきたコード例を載せます。
自分が考えた例には★印を付けておきます。「信用しないでね」の意味です。
何か面白いパターンを思いついたら随時更新する予定です。
fs.readFileSync
を利用する例
こちらは変数に標準出力の内容を一度に受けてしまおうというものです。
split
して配列を保持する
const inputs = require("fs").readFileSync("/dev/stdin", "utf-8").trim().split("\n");
// 1 行目は inputs[0]
// 2 行目は inputs[1]
// ...
今日日よく見られるものな気がします。
こちらは、N 行目を得るために inputs[N-1]
をする必要があるのですが、インデックスの管理が必要になって面倒だなあという印象を受けます。
★ スキャナみたいなものを書いてみるといいかも
一度変数に受けたのだから、スキャナのようなものを書くといいんじゃないと思って考えてみました。現状次のものが一番楽に書ける気がします。
const scan = {
_buf: require("fs").readFileSync(0, "utf-8"),
line() {
const [s] = this._buf.split("\n", 1);
this._buf = this._buf.substring(s.length + 1);
return s;
},
};
// scan.line() で一行読む
const n = Number(scan.line());
for (let i = 0; i < n; ++i) scan.line();
8 行必要になりますが、一度 _buf
に文字列として標準入力を受けておいてどんどん消費していくようなものを書きました。ただし、改行文字が \n
であることが前提です。
何度も _buf.split(...)
を呼んでいて遅そうに見えるかもしれませんが、個人的に上のようにあらかじめ split
して配列を持っておく例とそんなに速度的に変わらないような気がしています。
for (let i = 0; i < n; ++i) {
const str = scan.line();
console.log(str);
console.log(scan);
}
こんなふうに scan
の内容を出力してみると _buf
の内容がどんどん削れていく様子が見えて気持ちいいです。
実行例
readline
モジュールを利用する例
readline
モジュールという一行ずつ読み取るためのものがあります。お誂え向きに見えますが、どちらかというと CLI で対話するためのものなのでややしんどい思いをすることになります。
一度入力が終わるまで行を全て読み取り、配列に保持する
こちらも競プロで標準入力を取る例としてよく見かけるものな気がします。
const rl = require("readline").createInterface(process.stdin);
const inputs = [];
rl.on("line", (line) => {
inputs.push(line);
});
rl.on("close", () => {
// メイン処理...
});
イベント駆動だということで競プロでは見かけないようなコードをしています。
結局配列に全て受けるところは fs.readFileSync
の初めの例と変わらないので、そこに気をつければ同じようにコーディングできる気がします。
close
イベントを見て「入力が完了した」ということでメイン処理を開始する感じになっています。
★ event
モジュールと組み合わせて「一行読む」関数を作る(async/await
)
色々考えて作ってみました(楽しかった)。
const { stdin, exit } = process;
const { log, error } = console;
const rl = require("events").on(require("readline").createInterface(stdin), "line");
const ln = () => rl.next().then(({ value: [l] }) => l);
async function main() {
const [n, m, q] = (await ln()).split(" ").map(Number);
const mat = [];
for (let i = 0; i < n; ++i) mat.push((await ln()).split(" ").map(Number));
// ...
}
main().then(() => exit(0)).catch(e => error(e) || exit(1));
ゴチャゴチャしていて見にくいかもしれませんが、async function main
の中で await ln()
とすればその場で一行読み出せる(ように見える)コーディングができるようになっています。
悩んで作った割にはそんなに良いコードではないのですが、「必要になったら待ち受ける」といった書き方ができるようになっています。そういうコーディングがしたい場面があればこちらの方が良いでしょう。
途中で入力が終わってしまった場合はどういうエラーが起こるんでしょうか。この辺よくわかっていません。
実行例
2022/11/07 追記
★ fs.readFileSync
の内容を分割して受け取る関数を main
で利用する例
上の fs.readFileSync
の例で、入力が必要ない段階で scan
オブジェクトに fs.readFileSync
を受けるのがなんとなく気に食わなくなって考え直していたのですが、以下の例を思いつきました。
/** @arg {() => string} gets */
function main(gets) {
console.log(gets().split(""));
console.log(gets().split(""));
}
((_buffer) => main(() => {
let [s] = _buffer.split("\n", 1);
_buffer = _buffer.substring(s.length + 1);
return s;
}))(require("fs").readFileSync(0, "utf-8")); // prettier-ignore
main
関数内でのみ引数の gets
関数を利用できるということで、main
実行直前に readFileSync
してくれる感じです。関数名は他とぶつからないように好きなようにすればいいと思います(引数の名前を変えるだけ)。入力云々が必要なのは main
だけということにすれば IO の切り分けもそれっぽいですし、ロジックが入力の上を走る感じでちょっと気に入っています。
main
に渡す関数の中身が 3 行から縮まらないのが JS パワーが足りていないという感じですが、良いものがあればお教えください。
★ 上の readline
版
同様の方針で readline
のものも書き直しました。
/** @arg {() => Promise<string>} gets */
async function main(gets) {
console.log((await gets()).split(""));
console.log((await gets()).split(""));
}
// prettier-ignore
(i => main(() => i.next().then(({ value: [l] }) => l)).then(_ => process.exit(0)))
(require("events").on(require("readline").createInterface(process.stdin), "line"))
2022/11/14 追記
★ 「fs.readFileSync
の内容を〜」を短くした
/** @arg {() => string} gets */
function main(gets) {
console.log(gets().split(""));
console.log(gets().split(""));
}
// 3 行から雑に短くした
((b, s) => main(() => ([s] = b.split("\n", 1), b = b.substring(s.length + 1), s)))
(require("fs").readFileSync(0, "utf-8")); // prettier-ignore
多分お行儀は良くないんですが、ざっくり短くしてみました。
★ 配列を取っておいてインデックス管理
「短くするだけなら配列・インデックス管理を押し込めただけの関数が楽なんでは?」ということに気がついて、当初の fs.readFileSync
して split
してインデックスを管理して... という方針に戻ったものです。
/** @arg {() => string} gets */
function main(gets) {
console.log(gets().split(""));
console.log(gets().split(""));
}
((l, i) => main(() => l[i++]))
(require("fs").readFileSync(0, "utf-8").split("\n"), 0); // prettier-ignore
終わりに
競プロ環境に入ったら使いやすいんじゃないかなと思ってこういうの作ったんですけど、もちろん採用例はないです。
実行例