Last active
November 24, 2024 21:46
-
-
Save TwoSquirrels/9d75039f9e5153af5e3eb42955fb0e76 to your computer and use it in GitHub Desktop.
システム英単語テストジェネレータ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
// (c) 2023 TwoSquirrels | |
"use strict"; | |
const fs = require("fs"); | |
const MersenneTwister = new require("mersenne-twister"); | |
const tabletojson = require("tabletojson").Tabletojson; | |
const PDFDocument = require("pdfkit-table"); | |
const clamp = (v, low, high) => Math.min(Math.max(v, low), high); | |
const minmax = (x, y) => [Math.min(x, y), Math.max(x, y)]; | |
const FONT_TTF = __dirname + "/font.ttf"; | |
const SYSTAN_JSON = __dirname + "/systan.json"; | |
(async ( | |
pdf = "./exam.pdf", | |
left = Infinity, | |
right = 1, | |
num = 30, | |
seed = null | |
) => { | |
const systan = await (async (cache) => { | |
try { | |
return JSON.parse(await cache); | |
} catch (e) { | |
const URL = "https://ukaru-eigo.com/systan-word-list/"; | |
console.log(`シス単データを ${URL} からダウンロード中...`); | |
const systan = (await tabletojson.convertUrl(URL))[0].map((word) => ({ | |
id: parseInt(word["番号"]), | |
en: word["単語"], | |
jp: word["意味"], | |
})); | |
await fs.promises.writeFile( | |
SYSTAN_JSON, | |
JSON.stringify(systan, null, 2) + "\n", | |
"UTF8" | |
); | |
return systan; | |
} | |
})(fs.promises.readFile(SYSTAN_JSON, "UTF8")); | |
const [l, r] = minmax( | |
clamp(new Number(left), 1, systan.length), | |
clamp(new Number(right), 1, systan.length) | |
); | |
const n = clamp(new Number(num), 1, r - l + 1); | |
const s = parseInt(seed, 16) || Math.floor(Math.random() * 2 ** 16); | |
const title = `システム英単語テスト ${l}~${r} (SEED:${s | |
.toString(16) | |
.toUpperCase() | |
.padStart(4, "0")})`; | |
const mt19937 = new MersenneTwister(s); | |
console.log(`\n${title} (${n * 2} 点満点)`); | |
const words = systan | |
.map((word) => ({ ...word, r: mt19937.random() })) | |
.slice(l - 1, r) | |
.sort((a, b) => a.r - b.r) | |
.slice(0, n); | |
const tables = { | |
en2jp: words.map(({ id, en, jp }) => [id, en, " ".repeat(jp.length)]), | |
jp2en: words.map(({ id, en, jp }) => [id, `(${en[0]})`, jp]), | |
answer: words.map(({ id, en, jp }) => [id, en, jp]), | |
}; | |
console.table(tables.answer); | |
console.log("\nPDF 出力中..."); | |
const doc = new PDFDocument({ margin: 16, size: "A4" }); | |
doc.pipe(fs.createWriteStream(pdf)); | |
let pageLeft = true; | |
for (const name in tables) { | |
if (!pageLeft) doc.addPage(); | |
doc | |
.font(FONT_TTF) | |
.fontSize(16) | |
.text( | |
`\n${title}【${ | |
{ en2jp: `和訳編`, jp2en: `英訳編`, answer: `解答編` }[name] | |
}】`, | |
{ align: "center" } | |
); | |
doc | |
.fontSize(12) | |
.text(`${name === "answer" ? n * 2 : ""}/${n * 2}`, { align: "right" }); | |
const wordAlign = name === "jp2en" ? "left" : "center"; | |
const headerCommon = { valign: "center", headerColor: "white" }; | |
await doc.table( | |
{ | |
headers: [ | |
{ label: "番号", width: 30, align: "right", ...headerCommon }, | |
{ label: "単語", width: 100, align: wordAlign, ...headerCommon }, | |
{ label: "意味", width: 150, align: "left", ...headerCommon }, | |
{ label: "番号", width: 30, align: "right", ...headerCommon }, | |
{ label: "単語", width: 100, align: wordAlign, ...headerCommon }, | |
{ label: "意味", width: 150, align: "left", ...headerCommon }, | |
], | |
rows: tables[name] | |
.reduce( | |
(rows, row) => | |
rows.length === 0 || rows.at(-1).length >= 6 | |
? [...rows, row] | |
: [...rows.slice(0, rows.length - 1), [...rows.at(-1), ...row]], | |
[] | |
) | |
.map((row) => [...row, ...new Array(6 - row.length)]), | |
}, | |
{ | |
divider: { | |
header: { disabled: false, width: 1, opacity: 1.0 }, | |
horizontal: { disabled: false, width: 0.5, opacity: 1.0 }, | |
vertical: { disabled: false, width: 0.25, opacity: 1.0 }, | |
}, | |
padding: 4, | |
minRowHeight: clamp(500 / Math.ceil(tables[name].length / 2), 20, 45), | |
prepareHeader: () => doc.fontSize(10), | |
prepareRow: (_row, i, _j, _rectRow, { x, y, width, height }) => { | |
(i === 0 ? [0, width] : [width]).forEach((dx) => | |
doc | |
.lineWidth(0.5) | |
.moveTo(x + dx, y) | |
.lineTo(x + dx, y + height) | |
.stroke() | |
); | |
doc.fontSize(i % 3 === 1 ? 10 : 8); | |
}, | |
} | |
); | |
pageLeft = false; | |
} | |
doc.end(); | |
console.log("PDF 出力完了!"); | |
})(...process.argv.slice(2)).catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
使い方
準備
Node.js がインストールされている必要があります。
まずこれ専用のディレクトリを作ってその中にこの
systan.js
を置き、その中でこのコマンドを叩いてください。(pnpm 等でも構いません)
また
./font.ttf
に日本語対応のフォントファイルを置くかそのリンクを張ってください。Yu Gothic UI 想定ですが、別のフォントでも大丈夫だと思います。
挙動
のコマンドで、
{left}
~{right}
の範囲からシード値
{seed}
(16 進数) のランダムで{num}
個の単語テストを生成し、{pdf}
に PDF を出力します。引数は seed, num, left, right, pdf の順に省略することができ、
デフォルト値はそれぞれ (16 進数 4 桁の乱数), 30, 1, Infinity (=2027), exam.pdf です。
./systan.json
に単語データがキャッシュされます。PDF は和訳ページ、英訳ページ、解答ページの順になっています。
ページを選んで印刷して勉強に活用しましょう。