const fs = require('fs'); const path = require('path'); const { parse } = require('csv-parse/sync'); const REQUIRED_HEADERS = ['登録先', '開始日', '開始時刻', '終了日', '終了時刻', '分類', '予定', '備考']; const INPUT_DIR = path.join(__dirname, 'data'); const OUTPUT_DIR = path.join(__dirname, 'ical'); function formatDateTime(dateText, timeText) { const [year, month, day] = String(dateText || '').split('/').map(Number); const [hour, minute] = String(timeText || '').split(':').map(Number); if (!year || !month || !day || Number.isNaN(hour) || Number.isNaN(minute)) { throw new Error(`日時形式が不正です: ${dateText} ${timeText}`); } const yyyy = String(year).padStart(4, '0'); const MM = String(month).padStart(2, '0'); const dd = String(day).padStart(2, '0'); const HH = String(hour).padStart(2, '0'); const mm = String(minute).padStart(2, '0'); return `${yyyy}${MM}${dd}T${HH}${mm}00`; } function getUtcStamp() { const now = new Date(); const yyyy = String(now.getUTCFullYear()).padStart(4, '0'); const MM = String(now.getUTCMonth() + 1).padStart(2, '0'); const dd = String(now.getUTCDate()).padStart(2, '0'); const HH = String(now.getUTCHours()).padStart(2, '0'); const mm = String(now.getUTCMinutes()).padStart(2, '0'); const ss = String(now.getUTCSeconds()).padStart(2, '0'); return `${yyyy}${MM}${dd}T${HH}${mm}${ss}Z`; } function escapeIcalText(value) { return String(value || '') .replace(/\\/g, '\\\\') .replace(/\r\n|\n|\r/g, '\\n') .replace(/,/g, '\\,') .replace(/;/g, '\\;'); } function validateHeaders(headers) { const normalized = headers.map(v => String(v || '').trim()); const invalid = REQUIRED_HEADERS.some((expected, index) => normalized[index] !== expected); if (invalid) { throw new Error(`ヘッダー不一致です。期待: ${REQUIRED_HEADERS.join(', ')}`); } } function createIcsEvent(record, index) { const start = formatDateTime(record['開始日'], record['開始時刻']); const end = formatDateTime(record['終了日'], record['終了時刻']); const summary = escapeIcalText(record['予定']); const descriptionParts = []; //if (record['登録先']) descriptionParts.push(`登録先: ${record['登録先']}`); if (record['分類']) descriptionParts.push(`分類: ${record['分類']}`); if (record['備考']) descriptionParts.push(`備考: ${record['備考']}`); const description = escapeIcalText(descriptionParts.join('\n')); const uid = `${Date.now()}-${index}-${Math.random().toString(36).slice(2)}@alpha-office`; let eventText = 'BEGIN:VEVENT\n'; eventText += `UID:${uid}\n`; eventText += `DTSTAMP:${getUtcStamp()}\n`; eventText += `DTSTART;TZID=Asia/Tokyo:${start}\n`; eventText += `DTEND;TZID=Asia/Tokyo:${end}\n`; eventText += `SUMMARY:${summary}\n`; eventText += `DESCRIPTION:${description}\n`; eventText += 'END:VEVENT\n'; return eventText; } function csvToIcal(csvFileName) { try { const csvPath = path.join(INPUT_DIR, csvFileName); const csvContent = fs.readFileSync(csvPath, 'utf-8'); // ダブルクォーテーションと引用符内改行を含むCSVを正しく解析 const records = parse(csvContent, { columns: true, skip_empty_lines: true, bom: true }); if (records.length === 0) { throw new Error('データ行がありません'); } validateHeaders(Object.keys(records[0])); let ical = 'BEGIN:VCALENDAR\n'; ical += 'VERSION:2.0\n'; ical += 'PRODID:-//Alpha Office Schedule//JP\n'; ical += 'CALSCALE:GREGORIAN\n'; ical += 'METHOD:PUBLISH\n'; records.forEach((record, index) => { ical += createIcsEvent(record, index); }); ical += 'END:VCALENDAR\n'; const outputPath = path.join(OUTPUT_DIR, `${path.basename(csvFileName, '.csv')}.ics`); fs.writeFileSync(outputPath, ical, 'utf-8'); console.log(`✓ 変換完了: ${outputPath}`); return true; } catch (error) { console.error(`✗ 変換失敗 (${csvFileName}):`, error.message); return false; } } function main() { if (!fs.existsSync(INPUT_DIR)) { console.error(`入力フォルダが見つかりません: ${INPUT_DIR}`); process.exitCode = 1; return; } if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } const files = fs.readdirSync(INPUT_DIR) .filter(file => file.toLowerCase().endsWith('.csv')) .sort(); if (files.length === 0) { console.log('csvフォルダにCSVファイルがありません'); return; } let successCount = 0; let failCount = 0; files.forEach(file => { if (csvToIcal(file)) { successCount += 1; } else { failCount += 1; } }); console.log(`処理結果: 成功 ${successCount} / 失敗 ${failCount}`); if (failCount > 0) { process.exitCode = 1; } } main();